"""Software checks, not clinical validation."""
import contextlib
import io
import math
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

import yaml

from smart_noa_controller import ConfigLoader, Patient, Pharmacokinetics, SmartNOAController


class RevisionRegressionTests(unittest.TestCase):
    def controller(self):
        with contextlib.redirect_stdout(io.StringIO()):
            c = SmartNOAController(Patient(40, 70, 2, 90, [], []))
            c.generate_starting_rates()
        return c

    def test_unknown_drug_is_unassessed(self):
        allowed, message = self.controller().check_contraindication('unrecognized')
        self.assertFalse(allowed)
        self.assertIn('UNASSESSED', message)

    def test_invalid_model_inputs_fail(self):
        for weight in (0, -1, math.nan, math.inf):
            with self.assertRaises(ValueError):
                Pharmacokinetics(weight, 1, 0.1, 0.1)
        model = Pharmacokinetics(70, 1, 0.1, 0.1)
        for rate, dt in ((-1, 1), (math.nan, 1), (1, 0), (1, math.inf), (1, 20)):
            with self.assertRaises(ValueError):
                model.update_concentration(rate, dt)

    def test_mass_balance_without_elimination(self):
        model = Pharmacokinetics(10, 2, 0, 0)
        cp, ce = model.update_concentration(5, 2)
        self.assertAlmostEqual(cp, 0.5)  # 10 mcg / 20 L = 0.5 ng/mL
        self.assertEqual(ce, 0)

    def test_low_hr_triggers_hold_at_zero_concentration(self):
        c = self.controller()
        c.infusions['Dexmedetomidine'] = 0.0
        with patch('smart_noa_controller.random.randint', side_effect=[40, 80]), \
                patch('smart_noa_controller.time.sleep'), contextlib.redirect_stdout(io.StringIO()):
            c.monitor_and_control(1)
        self.assertTrue(c.hold_latched)
        self.assertEqual(c.infusions['Dexmedetomidine'], 0)

    def test_hold_does_not_restart_after_one_normal_reading(self):
        c = self.controller()
        with patch('smart_noa_controller.random.randint', side_effect=[80, 50, 80, 90]), \
                patch('smart_noa_controller.time.sleep'), contextlib.redirect_stdout(io.StringIO()):
            c.monitor_and_control(2)
        self.assertEqual(c.status, 'HOLD')
        self.assertEqual(c.infusions['Dexmedetomidine'], 0)
        self.assertEqual(c.infusions['Lidocaine'], 0)

    def test_invalid_profiles_are_rejected(self):
        base = {'age': 40, 'weight_kg': 70, 'asa_class': 2, 'egfr': 90, 'allergies': [], 'comorbidities': []}
        for field, value in [('age', -1), ('age', True), ('asa_class', 7), ('egfr', math.nan), ('weight_kg', 0),
                             ('allergies', 'NSAID'), ('comorbidities', [None])]:
            with self.subTest(field=field, value=value), self.assertRaises(ValueError):
                Patient(**{**base, field: value})

    def test_invalid_configuration_rejected(self):
        original = yaml.safe_load(Path('config.yaml').read_text(encoding='utf-8'))
        for bad_value in (None, [], {}, {'hemodynamic_thresholds': None}):
            with tempfile.TemporaryDirectory(dir=Path.cwd()) as folder:
                config = Path(folder) / 'bad.yaml'
                config.write_text(yaml.safe_dump(bad_value), encoding='utf-8')
                with self.assertRaises(ValueError):
                    ConfigLoader(str(config))
        original['drug_dosing']['lidocaine']['standard_dose'] = math.nan
        with tempfile.TemporaryDirectory(dir=Path.cwd()) as folder:
            config = Path(folder) / 'bad.yaml'
            config.write_text(yaml.safe_dump(original), encoding='utf-8')
            with self.assertRaises(ValueError):
                ConfigLoader(str(config))

    def test_regenerating_example_rates_does_not_bypass_hold(self):
        c = self.controller()
        c.hold_latched = True
        rates = c.generate_starting_rates()
        for drug in c.infusions:
            self.assertEqual(rates[drug], 0)

    def test_simultaneous_low_hr_does_not_mask_low_pressure(self):
        c = self.controller()
        with patch('smart_noa_controller.random.randint', side_effect=[40, 50]), \
                patch('smart_noa_controller.time.sleep'), contextlib.redirect_stdout(io.StringIO()):
            c.monitor_and_control(1)
        self.assertTrue(c.hold_latched)
        self.assertEqual(c.status, 'RED')
        self.assertEqual(c.infusions['Dexmedetomidine'], 0)
        self.assertEqual(c.infusions['Lidocaine'], 0)

    def test_overflow_rejected_without_partial_state_update(self):
        model = Pharmacokinetics(10, 2, 0, 0)
        model.update_concentration(5, 1)
        before = (model.Cp, model.Ce)
        with self.assertRaises(ValueError):
            model.update_concentration(1e308, 10)
        self.assertEqual((model.Cp, model.Ce), before)

    def test_derived_volume_and_boolean_model_inputs_rejected(self):
        for values in ((1e308, 1e308, 0, 0), (True, 1, 0, 0), (1, 1, True, 0)):
            with self.subTest(values=values), self.assertRaises(ValueError):
                Pharmacokinetics(*values)

    def test_rate_conversion_rejects_other_drug_units(self):
        with self.assertRaises(ValueError):
            self.controller()._rate_to_mcg_per_min('Ketamine')

    def test_invalid_duration_rejected(self):
        for duration in (-1, True, 0.5):
            with self.subTest(duration=duration), self.assertRaises(ValueError):
                self.controller().monitor_and_control(duration)

if __name__ == '__main__':
    unittest.main()
