"""
tests/test_fingerprinting.py — Unit tests for src/cv/fingerprinting.py

Tests run WITHOUT requiring real video files.
"""

from __future__ import annotations

import numpy as np
import pytest

from src.cv.fingerprinting import (
    text_safe_crop,
    extract_hs_histograms,
    compare_histograms,
    hist_to_bytes,
    bytes_to_hist,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def solid_blue_frame() -> np.ndarray:
    """256×256 solid blue BGR frame."""
    frame = np.zeros((256, 256, 3), dtype=np.uint8)
    frame[:, :] = (255, 0, 0)  # BGR blue
    return frame


@pytest.fixture
def solid_red_frame() -> np.ndarray:
    """256×256 solid red BGR frame."""
    frame = np.zeros((256, 256, 3), dtype=np.uint8)
    frame[:, :] = (0, 0, 255)  # BGR red
    return frame


# ---------------------------------------------------------------------------
# text_safe_crop
# ---------------------------------------------------------------------------

class TestTextSafeCrop:
    def test_removes_correct_rows(self, solid_blue_frame: np.ndarray) -> None:
        cropped = text_safe_crop(solid_blue_frame, crop_top=0.15, crop_bottom=0.30)
        h = solid_blue_frame.shape[0]  # 256
        expected_h = int(h * (1.0 - 0.30)) - int(h * 0.15)
        assert cropped.shape[0] == expected_h

    def test_zero_crop_returns_same_size(self, solid_blue_frame: np.ndarray) -> None:
        cropped = text_safe_crop(solid_blue_frame, crop_top=0.0, crop_bottom=0.0)
        assert cropped.shape == solid_blue_frame.shape

    def test_invalid_top_raises(self, solid_blue_frame: np.ndarray) -> None:
        with pytest.raises(ValueError, match="crop_top"):
            text_safe_crop(solid_blue_frame, crop_top=1.0, crop_bottom=0.0)

    def test_invalid_bottom_raises(self, solid_blue_frame: np.ndarray) -> None:
        with pytest.raises(ValueError, match="crop_bottom"):
            text_safe_crop(solid_blue_frame, crop_top=0.0, crop_bottom=-0.1)

    def test_overlapping_crops_raise(self, solid_blue_frame: np.ndarray) -> None:
        with pytest.raises(ValueError, match="must be < 1.0"):
            text_safe_crop(solid_blue_frame, crop_top=0.6, crop_bottom=0.5)


# ---------------------------------------------------------------------------
# Histograms
# ---------------------------------------------------------------------------

class TestHistograms:
    def test_output_shape(self, solid_blue_frame: np.ndarray) -> None:
        luma, sat = extract_hs_histograms(solid_blue_frame, bins_hue=50, bins_sat=60)
        assert luma.shape == (50,)
        assert sat.shape  == (60,)

    def test_normalised(self, solid_blue_frame: np.ndarray) -> None:
        import numpy as np
        luma, sat = extract_hs_histograms(solid_blue_frame, bins_hue=50, bins_sat=60)
        # L2-normalised → norm ≈ 1.0
        assert np.linalg.norm(luma) == pytest.approx(1.0, abs=1e-5)
        assert np.linalg.norm(sat)  == pytest.approx(1.0, abs=1e-5)

    def test_same_frame_correl_is_one(self, solid_blue_frame: np.ndarray) -> None:
        import cv2
        luma, _ = extract_hs_histograms(solid_blue_frame, bins_hue=50, bins_sat=60)
        score = compare_histograms(luma, luma, method=cv2.HISTCMP_CORREL)
        assert score == pytest.approx(1.0, abs=1e-5)

    def test_different_frames_correl_lower(
        self,
        solid_blue_frame: np.ndarray,
        solid_red_frame: np.ndarray,
    ) -> None:
        import cv2
        luma_b, _ = extract_hs_histograms(solid_blue_frame, 50, 60)
        luma_r, _ = extract_hs_histograms(solid_red_frame,  50, 60)
        score = compare_histograms(luma_b, luma_r, method=cv2.HISTCMP_CORREL)
        assert score < 1.0


# ---------------------------------------------------------------------------
# Serialisation round-trip
# ---------------------------------------------------------------------------

class TestSerialisation:
    def test_round_trip(self, solid_blue_frame: np.ndarray) -> None:
        luma, _ = extract_hs_histograms(solid_blue_frame, 50, 60)
        restored = bytes_to_hist(hist_to_bytes(luma))
        np.testing.assert_array_almost_equal(luma, restored)
