from __future__ import annotations

from io import BytesIO
from typing import Any

import pytest

from cmvr_edge_ai.contracts import BoundingBox, Detection, EncodedImage, ImageFrame
from cmvr_edge_ai.detection.annotation import render_annotated_jpeg


Image = pytest.importorskip("PIL.Image")
ImageDraw = pytest.importorskip("PIL.ImageDraw")


def _solid_frame(
    *,
    pixel_format: str,
    rgb: tuple[int, int, int],
    width: int = 64,
    height: int = 48,
) -> ImageFrame:
    stored_pixel = rgb if pixel_format == "RGB8" else tuple(reversed(rgb))
    return ImageFrame(
        data=bytes(stored_pixel) * width * height,
        width=width,
        height=height,
        pixel_format=pixel_format,
    )


def _detection(
    label: str,
    box: BoundingBox,
    *,
    confidence: float = 0.93,
) -> Detection:
    return Detection(label=label, confidence=confidence, box=box)


def _record_drawing_calls(
    monkeypatch: pytest.MonkeyPatch,
) -> tuple[list[tuple[float, float, float, float]], list[str]]:
    rectangle_calls: list[tuple[float, float, float, float]] = []
    text_calls: list[str] = []
    original_rectangle = ImageDraw.ImageDraw.rectangle
    original_text = ImageDraw.ImageDraw.text

    def recording_rectangle(
        draw: ImageDraw.ImageDraw,
        xy: Any,
        *args: Any,
        **kwargs: Any,
    ) -> None:
        coordinates = tuple(xy)
        if len(coordinates) == 2 and all(
            isinstance(point, (tuple, list)) for point in coordinates
        ):
            coordinates = tuple(
                coordinate for point in coordinates for coordinate in point
            )
        if len(coordinates) == 4:
            rectangle_calls.append(tuple(float(value) for value in coordinates))
        original_rectangle(draw, xy, *args, **kwargs)

    def recording_text(
        draw: ImageDraw.ImageDraw,
        xy: Any,
        text: str,
        *args: Any,
        **kwargs: Any,
    ) -> None:
        text_calls.append(text)
        original_text(draw, xy, text, *args, **kwargs)

    monkeypatch.setattr(ImageDraw.ImageDraw, "rectangle", recording_rectangle)
    monkeypatch.setattr(ImageDraw.ImageDraw, "text", recording_text)
    return rectangle_calls, text_calls


@pytest.mark.parametrize("pixel_format", ["BGR8", "RGB8"])
def test_rendered_alert_image_preserves_color_and_is_a_decodable_jpeg(
    pixel_format: str,
) -> None:
    expected_rgb = (24, 96, 184)
    frame = _solid_frame(pixel_format=pixel_format, rgb=expected_rgb)
    detection = _detection(
        "No-Helmet",
        BoundingBox(x_min=8.0, y_min=8.0, x_max=32.0, y_max=32.0),
    )

    encoded = render_annotated_jpeg(frame, (detection,), jpeg_quality=90)

    assert isinstance(encoded, EncodedImage)
    assert encoded.media_type == "image/jpeg"
    assert (encoded.width, encoded.height) == (frame.width, frame.height)
    assert encoded.data.startswith(b"\xff\xd8")
    assert encoded.data.endswith(b"\xff\xd9")

    with Image.open(BytesIO(encoded.data)) as decoded:
        assert decoded.format == "JPEG"
        assert decoded.size == (frame.width, frame.height)
        rgb_image = decoded.convert("RGB")

    # Sample well away from the box and its label. JPEG is lossy, so compare
    # with a tolerance while still catching an RGB/BGR channel swap.
    assert rgb_image.getpixel((frame.width - 3, frame.height - 3)) == pytest.approx(
        expected_rgb,
        abs=12,
    )


def test_renderer_draws_each_detection_box_and_label(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    rectangle_calls, text_calls = _record_drawing_calls(monkeypatch)

    frame = _solid_frame(pixel_format="BGR8", rgb=(0, 0, 0))
    detections = (
        _detection(
            "No-Helmet",
            BoundingBox(x_min=5.0, y_min=10.0, x_max=30.0, y_max=35.0),
        ),
        _detection(
            "No-Vest",
            BoundingBox(x_min=34.0, y_min=12.0, x_max=58.0, y_max=40.0),
            confidence=0.81,
        ),
    )

    render_annotated_jpeg(frame, detections, jpeg_quality=85)

    assert (5.0, 10.0, 30.0, 35.0) in rectangle_calls
    assert (34.0, 12.0, 58.0, 40.0) in rectangle_calls
    assert any("No-Helmet" in text for text in text_calls)
    assert any("No-Vest" in text for text in text_calls)


def test_renderer_clips_out_of_bounds_box_and_skips_invalid_boxes(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    rectangle_calls, text_calls = _record_drawing_calls(monkeypatch)
    frame = _solid_frame(pixel_format="RGB8", rgb=(0, 0, 0))
    detections = (
        _detection(
            "Clipped",
            BoundingBox(x_min=-5.0, y_min=-10.0, x_max=80.0, y_max=60.0),
        ),
        _detection(
            "Reversed",
            BoundingBox(x_min=30.0, y_min=30.0, x_max=10.0, y_max=10.0),
        ),
        _detection(
            "Outside",
            BoundingBox(x_min=70.0, y_min=5.0, x_max=90.0, y_max=20.0),
        ),
        _detection(
            "Not-Finite",
            BoundingBox(x_min=float("nan"), y_min=1.0, x_max=10.0, y_max=10.0),
        ),
    )

    render_annotated_jpeg(frame, detections, jpeg_quality=85)

    assert (0.0, 0.0, 63.0, 47.0) in rectangle_calls
    assert any("Clipped" in text for text in text_calls)
    assert all("Reversed" not in text for text in text_calls)
    assert all("Outside" not in text for text in text_calls)
    assert all("Not-Finite" not in text for text in text_calls)
