"""Tests for BboxOverlay — coordinate mapping and state management."""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest
from PySide6.QtCore import QRect, QSize

from app.inference.bbox_overlay import BboxOverlay, Detection


class TestDetection:
    def test_namedtuple_fields(self) -> None:
        d = Detection(x1=10.0, y1=20.0, x2=100.0, y2=200.0, conf=0.87, label="label")
        assert d.x1 == 10.0
        assert d.label == "label"
        assert d.conf == pytest.approx(0.87)

    def test_immutable(self) -> None:
        d = Detection(0, 0, 1, 1, 0.5, "x")
        with pytest.raises(AttributeError):
            d.conf = 0.9  # type: ignore[misc]


class TestBboxOverlayState:
    def setup_method(self) -> None:
        self.overlay = BboxOverlay()

    def test_initially_no_detections(self) -> None:
        assert self.overlay._detections == []

    def test_initially_source_size_empty(self) -> None:
        assert self.overlay._source_size.isEmpty()

    def test_on_detections_stores_data(self) -> None:
        dets = [Detection(0, 0, 100, 100, 0.9, "label")]
        self.overlay.on_detections(dets, (640, 480))
        assert self.overlay._detections == dets
        assert self.overlay._source_size == QSize(640, 480)

    def test_clear_removes_detections(self) -> None:
        self.overlay.on_detections([Detection(0, 0, 10, 10, 0.5, "x")], (100, 100))
        self.overlay.clear()
        assert self.overlay._detections == []

    def test_visible_by_default(self) -> None:
        assert self.overlay.visible is True

    def test_multiple_detections_stored(self) -> None:
        dets = [
            Detection(0, 0, 50, 50, 0.9, "label"),
            Detection(100, 100, 200, 200, 0.75, "label"),
        ]
        self.overlay.on_detections(dets, (640, 480))
        assert len(self.overlay._detections) == 2

    def test_replace_detections_on_new_call(self) -> None:
        self.overlay.on_detections([Detection(0, 0, 10, 10, 0.5, "x")], (100, 100))
        self.overlay.on_detections([], (100, 100))
        assert self.overlay._detections == []


class TestBboxOverlayCoordinateMapping:
    """
    Verify that BboxOverlay correctly maps source-frame pixel coordinates
    onto the letterboxed video_rect when painting.

    We don't test actual QPainter output — instead we verify that the
    QRect values passed to painter.drawRect() correspond to the expected
    scaled coordinates.
    """

    def setup_method(self) -> None:
        self.overlay = BboxOverlay()

    def _make_painter_mock(self):
        painter = MagicMock()
        fm = MagicMock()
        fm.height.return_value = 14
        fm.ascent.return_value = 11
        fm.horizontalAdvance.return_value = 60
        painter.fontMetrics.return_value = fm
        return painter

    def test_paint_skips_when_no_detections(self) -> None:
        painter = self._make_painter_mock()
        self.overlay.paint(painter, QRect(0, 0, 640, 480))
        painter.drawRect.assert_not_called()

    def test_paint_skips_when_source_size_empty(self) -> None:
        # detections present but source_size not set
        self.overlay._detections = [Detection(0, 0, 100, 100, 0.9, "label")]
        painter = self._make_painter_mock()
        self.overlay.paint(painter, QRect(0, 0, 640, 480))
        painter.drawRect.assert_not_called()

    def test_bbox_scaled_to_full_video_rect(self) -> None:
        """
        Source: 640×480, covers full frame.
        video_rect: 640×480 at origin.
        Detection: full-frame box → should map 1:1.
        """
        self.overlay.on_detections(
            [Detection(0.0, 0.0, 640.0, 480.0, 0.99, "label")],
            (640, 480),
        )
        painter = self._make_painter_mock()
        video_rect = QRect(0, 0, 640, 480)
        self.overlay.paint(painter, video_rect)

        # First drawRect call = the bounding box
        first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
        assert first_call_rect.x() == 0
        assert first_call_rect.y() == 0
        assert first_call_rect.width() == 640
        assert first_call_rect.height() == 480

    def test_bbox_scaled_with_half_size_video_rect(self) -> None:
        """
        Source: 640×480, video_rect: 320×240 at origin (0.5× scale).
        Detection at (64, 48)→(128, 96) should map to (32, 24)→(64, 48).
        """
        self.overlay.on_detections(
            [Detection(64.0, 48.0, 128.0, 96.0, 0.8, "label")],
            (640, 480),
        )
        painter = self._make_painter_mock()
        video_rect = QRect(0, 0, 320, 240)
        self.overlay.paint(painter, video_rect)

        first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
        assert first_call_rect.x() == 32
        assert first_call_rect.y() == 24
        assert first_call_rect.width() == 32   # (128-64) * 0.5
        assert first_call_rect.height() == 24  # (96-48) * 0.5

    def test_bbox_offset_by_video_rect_origin(self) -> None:
        """
        video_rect at (100, 50) — letterboxed with margins.
        Detection at origin of source should map to (100, 50).
        """
        self.overlay.on_detections(
            [Detection(0.0, 0.0, 100.0, 100.0, 0.9, "label")],
            (640, 480),
        )
        painter = self._make_painter_mock()
        # video_rect 320×240 starting at (100, 50)
        video_rect = QRect(100, 50, 320, 240)
        self.overlay.paint(painter, video_rect)

        first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
        # x: 100 + int(0 * 320/640) = 100
        # y:  50 + int(0 * 240/480) =  50
        assert first_call_rect.x() == 100
        assert first_call_rect.y() == 50


class TestBboxOverlayWorkerPacket:
    """Test FramePacket and ResultPacket data structures."""

    def test_frame_packet_fields(self) -> None:
        from app.inference.worker import FramePacket
        pkt = FramePacket(
            frame_id=1,
            raw_bytes=b"\x00" * 12,
            width=2,
            height=2,
            channels=3,
        )
        assert pkt.frame_id == 1
        assert pkt.width == 2
        assert pkt.channels == 3

    def test_result_packet_fields(self) -> None:
        from app.inference.worker import ResultPacket
        pkt = ResultPacket(frame_id=5, detections=[], width=640, height=480)
        assert pkt.frame_id == 5
        assert pkt.detections == []
        assert pkt.width == 640
