import unittest
from unittest.mock import patch
import threading
import numpy as np

from app.analysis_metrics import AnalysisMetrics
from app.byte_tracking import ByteTracker
from app.contracts.frames import DetectionObservation, FrameEnvelope


class TrackingProbeTest(unittest.TestCase):
    def test_idle_fast_path_preserves_clock_and_candidates(self):
        tracker = ByteTracker()
        frame = FrameEnvelope('t', 't', 't', 't', 1, 0, 0, 0, 100, 100)
        obs = (DetectionObservation(frame, 'RFDETRMedium', 0, .2, (.1, .1, .3, .5)),)
        self.assertEqual(tracker.update(obs), [None])
        core = tracker._trackers[0]
        with patch.object(core, 'update_with_tensors', side_effect=AssertionError('idle library call')):
            self.assertEqual(tracker.update(obs), [None])
            self.assertEqual(tracker.update(()), [])
        self.assertEqual(core.frame_id, 3)

    def test_full_reference_parity_with_class_churn_and_threshold_boundaries(self):
        from test_tracking_empty_update import ReferenceTracker
        from app.byte_tracking import TrackingSettings
        for consecutive in (1, 3):
            old, new = ReferenceTracker(TrackingSettings(frame_rate=10, consecutive=consecutive)), ByteTracker(TrackingSettings(frame_rate=10, consecutive=consecutive))
            for seq in range(80):
                frame = FrameEnvelope('t', 't', 't', 't', 1, seq, seq*100000000, 0, 1280, 720)
                obs = []
                for i in range(300 if seq < 15 else 12 if seq < 30 or seq >= 55 else 0):
                    score = (.05, .1, .2, float(np.nextafter(np.float32(.25),np.float32(0))), .25, .35, .9)[(i+seq)//3 % 7]
                    x, y = (i%20)*.04, (i//20)*.04
                    obs.append(DetectionObservation(frame, 'RFDETRMedium', i%80, score, (x,y,x+.03,y+.03)))
                self.assertEqual(new.update(tuple(obs)), old.update(tuple(obs)))
                for c in old._trackers:
                    a, b = old._trackers[c], new._trackers[c]
                    self.assertEqual(a.frame_id, b.frame_id)
                    for key in ('tracked_tracks','lost_tracks','removed_tracks'):
                        aa, bb = getattr(a,key), getattr(b,key)
                        self.assertEqual(len(aa),len(bb))
                        for x,y in zip(aa,bb):
                            self.assertEqual((x.internal_track_id,x.external_track_id,x.frame_id,x.state),
                                             (y.internal_track_id,y.external_track_id,y.frame_id,y.state))
                            np.testing.assert_array_equal(x.mean,y.mean)
                            np.testing.assert_array_equal(x.covariance,y.covariance)
            old.reset();new.reset()
            self.assertEqual(new._trackers,{})

    def test_two_threads_have_independent_bounded_timing(self):
        results, errors = [], []
        def run():
            try:
                m=AnalysisMetrics();t=ByteTracker(metrics=m);m.configure(True,10)
                for _ in range(4):t.update(())
                results.append(len(m.snapshot()['tracking_profiles']))
            except Exception as error:errors.append(error)
        threads=[threading.Thread(target=run) for _ in range(2)]
        for t in threads:t.start()
        for t in threads:t.join(10)
        self.assertFalse(any(t.is_alive() for t in threads))
        self.assertEqual(errors,[])
        self.assertEqual(results,[3,3])

    def test_probe_has_no_global_profiler(self):
        from app import tracking_probe
        self.assertFalse(hasattr(tracking_probe, 'cProfile'))

    def test_bounded_profiles_preserve_ids_and_reset(self):
        metrics = AnalysisMetrics()
        tracker, reference = ByteTracker(metrics=metrics), ByteTracker()
        frame = FrameEnvelope('t', 't', 't', 't', 1, 0, 0, 0, 100, 100)
        obs = (DetectionObservation(frame, 'RFDETRMedium', 0, .9, (.1, .1, .3, .5)),)
        self.assertIsNone(metrics.reserve_tracking_profile())
        metrics.configure(True, 10)
        for _ in range(5):
            self.assertEqual(tracker.update(obs), reference.update(obs))
        profiles = metrics.snapshot()['tracking_profiles']
        self.assertEqual(len(profiles), 3)
        self.assertEqual(profiles[0]['classes'][0]['candidates'], 1)
        self.assertLessEqual(len(profiles[0]['classes']), 80)
        metrics.configure(False, 1)
        self.assertEqual(metrics.snapshot()['tracking_profiles'], [])
        metrics.configure(True, 10)
        revision = metrics.reserve_tracking_profile()
        metrics.configure(False, 1)
        metrics.record_tracking_profile(revision, {'stale': True})
        self.assertEqual(metrics.snapshot()['tracking_profiles'], [])
        metrics.close()
        self.assertIsNone(metrics.reserve_tracking_profile())
