"""
tests/test_deep_scan.py — Unit tests for frame_extractor and deep_scan

Uses synthetic in-memory videos (cv2.VideoWriter → temp file) so no real
video files are required. Tests cover the pure logic, not hardware decoding.
"""

from __future__ import annotations

import tempfile
from pathlib import Path

import cv2
import numpy as np
import pytest

from src.cv.frame_extractor import (
    get_video_info,
    grab_frame_at,
    iter_frames_stepped,
    open_video,
)
from src.cv.fingerprinting import text_safe_crop


# ---------------------------------------------------------------------------
# Helpers: build a tiny synthetic video on disk
# ---------------------------------------------------------------------------

FPS    = 24
WIDTH  = 320
HEIGHT = 240
SECS   = 3


def _make_synthetic_video(path: Path, color_bgr: tuple[int, int, int] = (0, 128, 255)) -> Path:
    """Write a 3-second single-colour video to *path*."""
    fourcc = cv2.VideoWriter_fourcc(*"mp4v")
    writer = cv2.VideoWriter(str(path), fourcc, float(FPS), (WIDTH, HEIGHT))
    frame  = np.full((HEIGHT, WIDTH, 3), color_bgr, dtype=np.uint8)
    for _ in range(FPS * SECS):
        writer.write(frame)
    writer.release()
    return path


@pytest.fixture
def synthetic_video(tmp_path: Path) -> Path:
    return _make_synthetic_video(tmp_path / "test.mp4")


# ---------------------------------------------------------------------------
# open_video
# ---------------------------------------------------------------------------

class TestOpenVideo:
    def test_opens_valid_file(self, synthetic_video: Path) -> None:
        with open_video(synthetic_video) as cap:
            assert cap.isOpened()

    def test_raises_on_missing_file(self, tmp_path: Path) -> None:
        with pytest.raises(FileNotFoundError):
            with open_video(tmp_path / "ghost.mp4"):
                pass


# ---------------------------------------------------------------------------
# get_video_info
# ---------------------------------------------------------------------------

class TestGetVideoInfo:
    def test_returns_correct_fps(self, synthetic_video: Path) -> None:
        info = get_video_info(synthetic_video)
        assert info["fps"] == pytest.approx(FPS, rel=0.05)

    def test_duration_approx(self, synthetic_video: Path) -> None:
        info = get_video_info(synthetic_video)
        assert info["duration_s"] == pytest.approx(SECS, rel=0.1)

    def test_resolution(self, synthetic_video: Path) -> None:
        info = get_video_info(synthetic_video)
        assert info["width"]  == WIDTH
        assert info["height"] == HEIGHT


# ---------------------------------------------------------------------------
# grab_frame_at
# ---------------------------------------------------------------------------

class TestGrabFrameAt:
    def test_returns_ndarray(self, synthetic_video: Path) -> None:
        with open_video(synthetic_video) as cap:
            frame = grab_frame_at(cap, 1.0)
        assert frame is not None
        assert isinstance(frame, np.ndarray)
        assert frame.shape == (HEIGHT, WIDTH, 3)

    def test_returns_none_past_end(self, synthetic_video: Path) -> None:
        with open_video(synthetic_video) as cap:
            frame = grab_frame_at(cap, 9999.0)
        # May return None or a repeated last frame depending on codec;
        # we only assert no exception is raised.
        assert frame is None or isinstance(frame, np.ndarray)


# ---------------------------------------------------------------------------
# iter_frames_stepped
# ---------------------------------------------------------------------------

class TestIterFramesStepped:
    def test_yields_correct_count(self, synthetic_video: Path) -> None:
        with open_video(synthetic_video) as cap:
            frames = list(iter_frames_stepped(cap, 0.0, 1.0, 0.5))
        # Expect timestamps: 0.0, 0.5, 1.0 → 3 frames
        assert len(frames) == 3

    def test_timestamps_increasing(self, synthetic_video: Path) -> None:
        with open_video(synthetic_video) as cap:
            frames = list(iter_frames_stepped(cap, 0.0, 2.0, 0.5))
        timestamps = [t for t, _ in frames]
        assert timestamps == sorted(timestamps)

    def test_invalid_step_raises(self, synthetic_video: Path) -> None:
        with open_video(synthetic_video) as cap:
            with pytest.raises(ValueError, match="step_s"):
                list(iter_frames_stepped(cap, 0.0, 1.0, 0.0))


# ---------------------------------------------------------------------------
# text_safe_crop integration (sanity: cropped height consistent)
# ---------------------------------------------------------------------------

class TestCropSanity:
    def test_crop_reduces_height(self, synthetic_video: Path) -> None:
        with open_video(synthetic_video) as cap:
            frame = grab_frame_at(cap, 0.5)
        assert frame is not None
        cropped = text_safe_crop(frame, 0.15, 0.30)
        assert cropped.shape[0] < frame.shape[0]
        assert cropped.shape[1] == frame.shape[1]  # width unchanged
