"""Tests du système de tracking."""

import pytest
import numpy as np
from unittest.mock import Mock
import time

from ..mp import FaceDetection
from ..tracker import FaceTracker, TargetSelector, TrackedFace


def create_mock_detection(
    cx: float, cy: float, w: float, h: float, conf: float, yaw: float = 0.0
) -> FaceDetection:
    """Crée détection simulée."""
    return FaceDetection(
        bbox=(cx, cy, w, h), confidence=conf, yaw=yaw, pitch=0.0, roll=0.0
    )


class TestFaceTracker:
    """Tests du tracker de visages."""

    def test_single_face_tracking(self):
        """Test tracking d'un seul visage."""
        tracker = FaceTracker(max_targets=3, max_age_seconds=2.0)

        # Première détection
        det1 = create_mock_detection(0.5, 0.5, 0.2, 0.3, 0.9)
        tracked = tracker.update([det1])

        assert len(tracked) == 1
        assert tracked[0].id == 1
        assert tracked[0].detection.confidence == 0.9
        assert tracked[0].age_frames == 1

    def test_stable_id_tracking(self):
        """Test stabilité des IDs sur plusieurs frames."""
        tracker = FaceTracker(max_targets=3, max_age_seconds=2.0)

        # Séquence de détections avec léger déplacement
        positions = [
            (0.5, 0.5, 0.2, 0.3),
            (0.52, 0.51, 0.2, 0.3),  # Léger mouvement
            (0.54, 0.52, 0.2, 0.3),
            (0.53, 0.51, 0.2, 0.3),  # Retour arrière
        ]

        face_id = None
        for i, (cx, cy, w, h) in enumerate(positions):
            det = create_mock_detection(cx, cy, w, h, 0.9)
            tracked = tracker.update([det])

            assert len(tracked) == 1

            if face_id is None:
                face_id = tracked[0].id
            else:
                # ID doit rester stable
                assert tracked[0].id == face_id

            # Age doit augmenter
            assert tracked[0].age_frames == i + 1

    def test_multi_face_tracking(self):
        """Test tracking de plusieurs visages."""
        tracker = FaceTracker(max_targets=3, max_age_seconds=2.0)

        # Deux détections distinctes
        det1 = create_mock_detection(0.3, 0.4, 0.2, 0.3, 0.9)
        det2 = create_mock_detection(0.7, 0.6, 0.18, 0.25, 0.8)

        tracked = tracker.update([det1, det2])

        assert len(tracked) == 2
        assert tracked[0].id != tracked[1].id

        # Vérifier association correcte (par position)
        face1 = None
        face2 = None
        for face in tracked:
            if abs(face.detection.bbox[0] - 0.3) < 0.1:
                face1 = face
            elif abs(face.detection.bbox[0] - 0.7) < 0.1:
                face2 = face

        assert face1 is not None
        assert face2 is not None
        assert face1.detection.confidence == 0.9
        assert face2.detection.confidence == 0.8

    def test_face_association_iou(self):
        """Test association par IoU."""
        tracker = FaceTracker(max_targets=3, max_age_seconds=2.0)

        # Frame 1: une détection
        det1 = create_mock_detection(0.5, 0.5, 0.2, 0.3, 0.9)
        tracked1 = tracker.update([det1])
        initial_id = tracked1[0].id

        # Frame 2: détection légèrement déplacée (bon IoU)
        det2 = create_mock_detection(0.52, 0.51, 0.21, 0.31, 0.85)
        tracked2 = tracker.update([det2])

        assert len(tracked2) == 1
        assert tracked2[0].id == initial_id  # Même ID

    def test_face_cleanup_old(self):
        """Test nettoyage des visages anciens."""
        tracker = FaceTracker(max_targets=3, max_age_seconds=0.1)  # Très court

        # Ajouter visage
        det = create_mock_detection(0.5, 0.5, 0.2, 0.3, 0.9)
        tracked = tracker.update([det])
        assert len(tracked) == 1

        # Attendre expiration
        time.sleep(0.15)

        # Update sans détection -> nettoyage
        tracked = tracker.update([])
        assert len(tracked) == 0

    def test_max_targets_limit(self):
        """Test limite max_targets."""
        tracker = FaceTracker(max_targets=2, max_age_seconds=2.0)

        # Ajouter 3 détections distinctes
        detections = [
            create_mock_detection(0.2, 0.4, 0.15, 0.2, 0.9),
            create_mock_detection(0.5, 0.5, 0.2, 0.3, 0.8),
            create_mock_detection(0.8, 0.6, 0.18, 0.25, 0.7),
        ]

        tracked = tracker.update(detections)

        # Seuls 2 visages doivent être trackés
        assert len(tracked) <= 2


class TestTargetSelector:
    """Tests du sélecteur de cible."""

    def create_mock_tracked_face(
        self, face_id: int, cx: float, cy: float, w: float, h: float, conf: float
    ) -> TrackedFace:
        """Crée visage tracké simulé."""
        detection = create_mock_detection(cx, cy, w, h, conf)
        return TrackedFace(
            id=face_id,
            detection=detection,
            last_seen=time.time(),
            age_frames=10,
            history_bbox=[(cx, cy, w, h)],
        )

    def test_closest_policy(self):
        """Test politique 'closest' (plus proche du centre)."""
        selector = TargetSelector(policy="closest", dwell_ms=1000)

        faces = [
            self.create_mock_tracked_face(1, 0.3, 0.4, 0.2, 0.3, 0.8),  # Plus loin
            self.create_mock_tracked_face(
                2, 0.52, 0.48, 0.18, 0.25, 0.9
            ),  # Plus proche centre
            self.create_mock_tracked_face(3, 0.8, 0.6, 0.15, 0.2, 0.85),  # Loin
        ]

        selected = selector.select_target(faces)

        assert selected is not None
        assert selected.id == 2  # Le plus proche du centre (0.5, 0.5)

    def test_largest_policy(self):
        """Test politique 'largest' (plus grande bbox)."""
        selector = TargetSelector(policy="largest", dwell_ms=1000)

        faces = [
            self.create_mock_tracked_face(1, 0.3, 0.4, 0.15, 0.2, 0.8),  # Petit
            self.create_mock_tracked_face(2, 0.5, 0.5, 0.25, 0.35, 0.9),  # Grand
            self.create_mock_tracked_face(3, 0.7, 0.6, 0.18, 0.25, 0.85),  # Moyen
        ]

        selected = selector.select_target(faces)

        assert selected is not None
        assert selected.id == 2  # Le plus grand (0.25 * 0.35 = 0.0875)

    def test_round_robin_policy(self):
        """Test politique 'round_robin'."""
        selector = TargetSelector(policy="round_robin", dwell_ms=100)  # Court pour test

        faces = [
            self.create_mock_tracked_face(1, 0.3, 0.4, 0.2, 0.3, 0.8),
            self.create_mock_tracked_face(2, 0.7, 0.6, 0.18, 0.25, 0.9),
        ]

        # Première sélection
        selected1 = selector.select_target(faces)
        first_id = selected1.id if selected1 else None

        # Attendre dwell_ms
        time.sleep(0.12)

        # Deuxième sélection -> doit changer
        selected2 = selector.select_target(faces)
        second_id = selected2.id if selected2 else None

        assert first_id is not None
        assert second_id is not None
        assert first_id != second_id  # Doit alterner

    def test_empty_faces_list(self):
        """Test avec liste vide."""
        selector = TargetSelector(policy="largest", dwell_ms=1000)

        selected = selector.select_target([])
        assert selected is None

    def test_policy_change(self):
        """Test changement de politique."""
        selector = TargetSelector(policy="largest", dwell_ms=1000)

        faces = [
            self.create_mock_tracked_face(
                1, 0.3, 0.4, 0.25, 0.35, 0.8
            ),  # Grand mais loin
            self.create_mock_tracked_face(
                2, 0.51, 0.49, 0.15, 0.2, 0.9
            ),  # Petit mais proche
        ]

        # Politique largest -> sélectionne le grand
        selected1 = selector.select_target(faces)
        assert selected1.id == 1

        # Changer politique
        selector.update_config("closest", 1000)

        # Politique closest -> sélectionne le proche
        selected2 = selector.select_target(faces)
        assert selected2.id == 2


class TestTrackingStability:
    """Tests de stabilité tracking."""

    def test_tracking_with_noise(self):
        """Test tracking avec bruit sur positions."""
        tracker = FaceTracker(max_targets=3, max_age_seconds=2.0)

        base_pos = (0.5, 0.5, 0.2, 0.3)
        face_id = None

        # 20 frames avec bruit gaussien
        for i in range(20):
            # Ajouter bruit ±2%
            noise_x = np.random.normal(0, 0.02)
            noise_y = np.random.normal(0, 0.02)

            noisy_pos = (
                base_pos[0] + noise_x,
                base_pos[1] + noise_y,
                base_pos[2],
                base_pos[3],
            )

            det = create_mock_detection(*noisy_pos, 0.9)
            tracked = tracker.update([det])

            assert len(tracked) == 1

            if face_id is None:
                face_id = tracked[0].id
            else:
                # ID doit rester stable malgré le bruit
                assert tracked[0].id == face_id

    def test_temporary_occlusion(self):
        """Test occlusion temporaire."""
        tracker = FaceTracker(max_targets=3, max_age_seconds=1.0)

        # Frame 1: détection normale
        det1 = create_mock_detection(0.5, 0.5, 0.2, 0.3, 0.9)
        tracked1 = tracker.update([det1])
        face_id = tracked1[0].id

        # Frames 2-5: occlusion (pas de détection)
        for i in range(4):
            tracked_occluded = tracker.update([])
            # Visage doit persister pendant max_age_seconds
            if i < 3:  # Dans les premières frames d'occlusion
                assert len(tracked_occluded) == 1
                assert tracked_occluded[0].id == face_id

        # Frame 6: réapparition
        det6 = create_mock_detection(0.52, 0.51, 0.2, 0.3, 0.85)
        tracked6 = tracker.update([det6])

        # ID doit être préservé si pas trop de temps écoulé
        assert len(tracked6) == 1
        # Note: selon timing exact, pourrait être nouveau ID si trop de temps écoulé
