import os
import queue
import unittest
from collections import Counter, deque
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import Mock, patch

from app.contracts.frames import DetectionObservation, PoseObservation
from app.contracts.visualization import FrameResult, VisualizationCommand
from app.event_capture import EventCapture
from app.multi_provider_results import public_positions
from app.pose_config import PoseConfig, POSE_MODEL
from tests.test_provider_linear import LinearTest
from tests.test_pr13_multi_provider import frame


class ResultQueueBTest(unittest.TestCase):
    def test_under_cap_does_not_rank_but_validates_event_refs(self):
        commands = [object()] * 300
        with patch('builtins.sorted', side_effect=AssertionError('Unexpected ranking')):
            self.assertEqual(public_positions(commands, [SimpleNamespace(object_ref='299')]), list(range(300)))
        with self.assertRaises(ValueError):
            public_positions(commands, [SimpleNamespace(object_ref='300')])

    def test_single_result_preserves_identity_nms_and_snapshot(self):
        fixture = LinearTest();c = fixture.make();f = frame(0)
        c.pose_config = PoseConfig();c.camera_id = 1
        detection = DetectionObservation(f, POSE_MODEL, 0, .9, (.1,.1,.5,.5))
        pose = PoseObservation(f, POSE_MODEL, .9, detection.bbox_xyxy, ((.2,.2,.8),)*17)
        command = VisualizationCommand(detection, 'person', pose=pose)
        result = FrameResult(f, (command,), scenario_revision=c.scenario_revision,
            pose_config=c.pose_config, nms_kept=(True,), provider_model=POSE_MODEL, camera_id=1)
        state = SimpleNamespace(results={'pose':object()})
        value = c._compose_result(f, [result], 'pose', state)
        self.assertIs(value, result)
        self.assertEqual(value.snapshot(), result.snapshot())
        self.assertEqual(value.snapshot()['nms']['kept'], 1)
        with self.assertRaises(ValueError):c._compose_result(frame(1), [result], 'pose', state)

    def test_queue_two_seconds_after_completion_and_finite_validation(self):
        fixture = LinearTest()
        with patch.dict(os.environ, CONTEXTRA_COMPLETED_QUEUE_MAX_AGE_SECONDS='2'):
            c=fixture.make();fixture.now[0]=10
            fixture.submit(c,1)
            self.assertEqual(c.capacity,32)
            self.assertEqual(c._ready_locked(11.5),1)
            self.assertIsNone(c._ready_locked(12.01))
            self.assertTrue(c._gap_pending['medium'])
        for bad in ('0','-1','nan','inf','6','bad'):
            with self.subTest(bad=bad), patch.dict(os.environ,CONTEXTRA_COMPLETED_QUEUE_MAX_AGE_SECONDS=bad):
                with self.assertRaises(ValueError):fixture.make()

    def capture(self, result, camera=1):
        capture=object.__new__(EventCapture)
        capture.native=SimpleNamespace(pull=lambda:None)
        capture.results=queue.Queue();capture.results.put(result)
        capture.metadata=deque(maxlen=300);capture.camera_id=camera
        capture.training=None;capture.store=None;capture.metrics=Counter()
        return capture

    def test_capture_validates_canonical_camera_and_keeps_legacy_type_adapter(self):
        result=FrameResult(frame(0),(),camera_id=1)
        capture=self.capture(result);capture._collect()
        self.assertEqual(capture.metadata[0]['cameraId'],1)
        with self.assertRaises(ValueError):self.capture(result,'1')._collect()
        with self.assertRaises(ValueError):self.capture(result,2)._collect()
        legacy=self.capture(replace(result,camera_id=None));legacy._collect()
        self.assertIs(type(legacy.metadata[0]['cameraId']),int)

    def test_private_timing_bound_and_off(self):
        from app.metadata_publisher import MetadataPublisher, MetadataSettings
        p=MetadataPublisher(MetadataSettings('redis://localhost:6379/0'),sink=Mock())
        p.note_frame_timing(frame(0),1,2)
        self.assertEqual(len(p._frame_timing),0)
        p.configure_metrics(True,30)
        for seq in range(100):p.note_frame_timing(frame(seq),1,2)
        self.assertEqual(len(p._frame_timing),64)
        p.configure_metrics(False)
        self.assertEqual(len(p._frame_timing),0)

    def test_same_frame_private_age_reaches_successful_sender_only(self):
        import time
        from app.metadata_publisher import MetadataPublisher, MetadataSettings
        sink=Mock();p=MetadataPublisher(MetadataSettings('redis://localhost:6379/0'),sink=sink)
        p.start()
        try:
            p.channel('1','run',1,'RUNNING');p.configure_metrics(True,30)
            f=frame(1);now=time.monotonic_ns()
            p.note_frame_timing(f,now-200_000_000,now-100_000_000)
            snapshot=FrameResult(f,()).snapshot()
            self.assertNotIn('selected_ns',snapshot)
            self.assertTrue(p.submit(snapshot))
            deadline=time.monotonic()+2
            while p.metrics().get('sent_frames',0)<1 and time.monotonic()<deadline:time.sleep(.005)
            channels=p.metrics(True)['profile']['channels']
            timings=next(iter(channels.values()))['timings']
            self.assertGreaterEqual(timings['selected_to_redis']['mean_ms'],200)
            self.assertGreaterEqual(timings['completed_to_redis']['mean_ms'],100)
        finally:self.assertTrue(p.close(2))
