import unittest
import os
from unittest.mock import patch

from app.event_cooldown import EventCooldown
from app.region_scenario import alerted, event_ids
from tests.test_scenario_demo_sensitivity import configured_fall, configured_fight, down, pair
from tests import test_intrusion, test_fire_scenario
from tests.test_loitering import Harness


class CooldownTest(unittest.TestCase):
    def setUp(self):
        self.now = [0.]
        self.gate = EventCooldown(10, clock=lambda: self.now[0])

    def test_boundary_scenario_and_run_isolation(self):
        self.assertTrue(self.gate.allow('FALL'))
        self.assertTrue(self.gate.allow('FIGHT'))
        self.now[0] = 9.999
        self.assertFalse(self.gate.allow('FALL'))
        self.assertTrue(EventCooldown(10, clock=lambda: self.now[0]).allow('FALL'))
        self.now[0] = 10
        self.assertTrue(self.gate.allow('FALL'))
        self.assertEqual(self.gate.status()['counts']['FALL_suppressed_candidates'], 1)
        for value in (-1, float('inf'), float('nan')):
            with self.assertRaises(ValueError):EventCooldown(value)

    def test_runtime_gate_binding_and_explicit_off(self):
        from tests.test_provider_linear import LinearTest
        with patch.dict(os.environ, CONTEXTRA_EVENT_COOLDOWN_SECONDS='10'):
            c=LinearTest().make('fire')
        self.assertEqual(c.status()['event_cooldown']['seconds'],10)
        self.assertTrue(c.fire.event_admission('FIRE'))
        self.assertFalse(c.fire.event_admission('FIRE'))
        with patch.dict(os.environ, CONTEXTRA_EVENT_COOLDOWN_SECONDS='0'):
            c=LinearTest().make('fire')
        self.assertTrue(c.fire.event_admission('FIRE'))
        self.assertTrue(c.fire.event_admission('FIRE'))

    def test_intrusion_pending_id_and_current_evidence(self):
        h=test_intrusion.IntrusionTest();h.setUp();h.engine.event_admission=self.gate.allow
        self.assertTrue(self.gate.allow('INTRUSION'))
        for i in range(6):rows,events=h.step(i/10)
        self.assertTrue(alerted(rows[0]));self.assertIsNone(rows[0].event_id)
        self.assertFalse(events);self.assertIsNone(h.engine.states[1].event_id)
        self.now[0]=10
        self.assertFalse(h.step(.6,empty=True)[1])
        rows,events=h.step(.7);self.assertEqual(len(events),1)
        self.assertEqual(rows[0].event_id,events[0].event_id)
        self.assertFalse(h.step(.8)[1])

    def test_loitering_same_episode_not_reissued(self):
        h=Harness(dwell_seconds=.1);h.engine.event_admission=self.gate.allow
        self.gate.allow('LOITERING');h.step(0)
        rows,events=h.step(.1);self.assertEqual(rows[0].state,'LOITERING')
        self.assertIsNone(rows[0].event_id);self.assertFalse(events)
        self.now[0]=10
        rows,events=h.step(.2);self.assertEqual(len(events),1);eid=events[0].event_id
        self.now[0]=30
        rows,events=h.step(.3);self.assertFalse(events);self.assertEqual(rows[0].event_id,eid)

    def test_fall_suppressed_episode_requires_current_pose(self):
        s=configured_fall(direct_down_cooldown_seconds=0);s.engine.event_admission=self.gate.allow
        first=down(s,0,1)[1][0].event_id
        rows,events=down(s,.1,2);self.assertTrue(alerted(rows[0]));self.assertFalse(events)
        self.assertFalse(event_ids(rows[0]));self.assertNotIn(2,s._events)
        self.now[0]=10
        self.assertFalse(down(s,.2,2,missing=(5,6))[1])
        rows,events=down(s,.3,2);self.assertEqual(len(events),1)
        self.assertNotEqual(events[0].event_id,first)
        self.now[0]=30
        self.assertFalse(down(s,.4,2)[1])

    def test_temporal_fall_retries_current_down_not_cooldown_alone(self):
        from tests.fall_fixtures import Sequence
        h=Sequence();h.engine.event_admission=self.gate.allow
        self.gate.allow('FALL');h.arm()
        for i in range(7,15):self.assertFalse(h.down(i/10).event_candidates)
        self.assertTrue(h.engine.inspect(1)['latched'])
        self.now[0]=10
        self.assertFalse(h.step(1.5,angle=0).event_candidates)
        self.assertEqual(len(h.down(1.6).event_candidates),1)
        self.now[0]=30
        self.assertFalse(h.down(1.7).event_candidates)

    def test_fight_pending_pair_current_evidence_and_ids(self):
        s=configured_fight(current_frame_fallback=True,event_cooldown_seconds=0)
        s.engine.event_admission=self.gate.allow
        self.assertEqual(len(pair(s,0)[1]),1)
        rows,events=pair(s,.1,ids=(31,32));self.assertFalse(events)
        self.assertTrue(any(alerted(r) for r in rows));self.assertNotIn((31,32),s._events)
        self.now[0]=10
        self.assertFalse(pair(s,.2,ids=(31,32),far=True)[1])
        self.assertEqual(len(pair(s,.3,ids=(31,32))[1]),1)
        self.now[0]=30
        self.assertFalse(pair(s,.4,ids=(31,32))[1])

    def test_fire_alert_without_id_and_no_missing_trigger(self):
        h=test_fire_scenario.FireEvidenceTest();h.setUp();h.engine.event_admission=self.gate.allow
        self.gate.allow('FIRE');r=h.warm()
        self.assertFalse(r.events);self.assertIsNone(h.engine.event_id)
        self.assertEqual(r.decisions[0].state,'FIRE')
        self.now[0]=10
        self.assertFalse(h.step(1.1,smoke=False).events)
        r=h.step(1.2);self.assertEqual(len(r.events),1)
        self.now[0]=30
        self.assertFalse(h.step(1.3).events)
