"""FIRE pre-development integration-contract tests; no SDK/GPU/network I/O."""
from pathlib import Path
import tempfile
import unittest

from app.contracts.frames import FrameEnvelope
from app.contracts.visualization import FrameResult
from app.event_artifact_contract import media_path, relative_directory
from app.fire_scenario import FireScenarioConfig
from app.provider_composition import BoundedFrameComposer, LatestWinsLane, ProviderFrameKey
from control_sidecar.main import validate_event


def key(seq, pts=None):
    return ProviderFrameKey('run-a', 1, seq, pts)


class FireContractTest(unittest.TestCase):
    def test_frame_result_uses_fire_scenario_key_without_new_top_level_fields(self):
        frame = FrameEnvelope('server', '77', 'stream', 'run-a', 1, 1, 100, 1, 1280, 720)
        result = FrameResult(frame, (), (), FireScenarioConfig(enabled=True), 'rev')
        snap = result.snapshot()
        self.assertEqual(set(snap['scenarioConfig']), {'FIRE'})
        self.assertFalse({'fireObjects','fireWindow','firePositiveRatio',
                          'providerMissing','partialFrame'} & set(snap))

    def test_fire_artifact_namespace_reuses_existing_shape(self):
        event = dict(cameraId=77, eventId='fire-1', scenario='FIRE',
                     aiRecordedAtWallMs=1789610000123)
        relative = relative_directory(event)
        self.assertIn('+0900_FIRE_fire-1', relative)
        with tempfile.TemporaryDirectory() as root:
            target = Path(root) / relative / 'clip.mp4'
            target.parent.mkdir(parents=True)
            target.write_bytes(b'x')
            self.assertEqual(media_path(Path(root), relative + '/clip.mp4'), target)

    def test_sidecar_accepts_fire_with_existing_event_schema(self):
        doc = {'serverId':'AI-test','events':[{
            'eventId':'fire-1','cameraId':77,'eventType':'FIRE',
            'detectedAtWallMs':1789610000100,'startWallTimeMs':1789610000000,
            'rtpTimestamp':9000,'score':.7,
        }]}
        validate_event(doc, 'AI-test')


class ProviderCompositionTest(unittest.TestCase):
    def test_latest_wins_is_bounded(self):
        lane = LatestWinsLane('fire')
        self.assertEqual(lane.offer(key(1), 'a')[0], key(1))
        self.assertIsNone(lane.offer(key(2), 'b'))
        self.assertIsNone(lane.offer(key(3), 'c'))
        self.assertEqual(lane.snapshot()['pending_replaced'], 1)
        self.assertEqual(lane.complete(key(1))[0], key(3))
        self.assertEqual((lane.snapshot()['inflight'], lane.snapshot()['pending']), (1,0))

    def test_complete_frame_releases_immediately(self):
        composer = BoundedFrameComposer(('person','fire'))
        called = []
        accepted, releases, _ = composer.submit(
            'person', key(1), lambda: called.append('p') or 'P', now_ns=0)
        self.assertTrue(accepted); self.assertFalse(releases)
        accepted, releases, _ = composer.submit(
            'fire', key(1), lambda: called.append('f') or 'F', now_ns=10_000_000)
        self.assertTrue(accepted)
        self.assertTrue(releases[0].complete)
        self.assertEqual(releases[0].results, {'person':'P','fire':'F'})
        self.assertEqual(called, ['p','f'])

    def test_timeout_releases_partial_and_late_result_is_not_evaluated(self):
        composer = BoundedFrameComposer(('person','fire'), wait_ms=50)
        called = []
        composer.submit('person', key(1), lambda: called.append('person') or 'P', now_ns=0)
        releases = composer.expire(now_ns=50_000_000)
        self.assertTrue(releases[0].timed_out)
        accepted, _, value = composer.submit(
            'fire', key(1), lambda: called.append('late') or 'F', now_ns=60_000_000)
        self.assertFalse(accepted)
        self.assertIsNone(value)
        self.assertEqual(called, ['person'])
        self.assertEqual(composer.snapshot()['provider_result_late_fire'], 1)

    def test_late_result_never_contaminates_next_frame(self):
        composer = BoundedFrameComposer(('person','fire'), wait_ms=50)
        composer.submit('person', key(1), lambda:'P1', now_ns=0)
        composer.expire(now_ns=50_000_000)
        composer.submit('person', key(2), lambda:'P2', now_ns=60_000_000)
        accepted, releases, _ = composer.submit(
            'fire', key(2), lambda:'F2', now_ns=70_000_000)
        self.assertTrue(accepted)
        self.assertEqual(releases[0].results, {'person':'P2','fire':'F2'})


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