import unittest
from unittest.mock import patch
import numpy as np
from app.byte_tracking import ByteTracker, TrackingSettings, _prepare_observations
from app.contracts.frames import FrameEnvelope, DetectionObservation


class ReferenceTracker(ByteTracker):
    """Pre-optimization public wrapper path, retained only as a regression oracle."""
    def update(self, observations):
        ids = [None] * len(observations)
        groups, boxes, scores = _prepare_observations(observations)
        for class_id in sorted(groups.keys() | self._trackers.keys()):
            if class_id not in self._trackers:
                s = self.settings
                self._trackers[class_id] = self.sv.ByteTrack(
                    track_activation_threshold=s.activation, lost_track_buffer=s.lost_buffer,
                    minimum_matching_threshold=s.matching, frame_rate=s.frame_rate,
                    minimum_consecutive_frames=s.consecutive)
            indices = np.asarray(groups.get(class_id, ()), dtype=int)
            detections = self.sv.Detections(xyxy=boxes[indices], confidence=scores[indices],
                class_id=np.full(len(indices), class_id, dtype=int), data={'candidate': indices})
            tracked = self._trackers[class_id].update_with_detections(detections)
            for index, track_id in zip(tracked.data.get('candidate', ()), tracked.tracker_id):
                ids[int(index)] = (int(track_id)-1)*80+class_id+1
        return ids


def observations(seq, classes):
    frame = FrameEnvelope('test','test','test','test',1,seq,seq*100000000,0,1920,1080)
    return tuple(DetectionObservation(frame,'RFDETRMedium',c,score,
                 (.1+c*.1,.1,.15+c*.1,.2)) for c,score in classes)


class EmptyTrackingUpdateTest(unittest.TestCase):
    def test_empty_uses_public_core_and_preserves_instance(self):
        tracker = ByteTracker(TrackingSettings(frame_rate=10))
        tracker.update(observations(0,[(0,.9)]))
        core = tracker._trackers[0]
        before = core.frame_id
        with patch.object(core,'update_with_detections',side_effect=AssertionError('empty wrapper')), \
             patch.object(core,'update_with_tensors',wraps=core.update_with_tensors) as update:
            self.assertEqual(tracker.update(()),[])
        self.assertIs(tracker._trackers[0],core)
        self.assertEqual(core.frame_id,before+1)
        update.assert_called_once()
        self.assertEqual(update.call_args.args[0].shape,(0,5))
        self.assertEqual(update.call_args.args[0].dtype,np.float32)

    def test_identity_and_state_parity_with_occlusion_and_reappearance(self):
        for consecutive in (1,3):
            settings = TrackingSettings(frame_rate=10, consecutive=consecutive)
            old,new = ReferenceTracker(settings),ByteTracker(settings)
            first = None
            for seq in range(70):
                values = [(0,.9),(1,.9),(2,.05)]
                if seq in range(5,8):values=[(1,.2)]
                if seq in range(15,40):values=[]
                if seq>=50:values=[(1,.9)]
                frame=observations(seq,values)
                expected=old.update(frame)
                self.assertEqual(new.update(frame),expected)
                if first is None:first=expected
                for c in old._trackers:
                    a,b=old._trackers[c],new._trackers[c]
                    self.assertEqual(a.frame_id,b.frame_id)
                    for attr in ('tracked_tracks','lost_tracks','removed_tracks'):
                        def state(t):
                            return [(x.internal_track_id,x.external_track_id,x.frame_id,x.state)
                                    for x in getattr(t,attr)]
                        self.assertEqual(state(a),state(b))
            old.reset();new.reset()
            self.assertEqual(new.active_tracks(),0)
            self.assertEqual(new.update(observations(0,[(0,.9),(1,.9),(2,.05)])),first)
