"""Tests for the camera capture engine — focus on resolution selection logic.

The probe-for-max behavior in ``_enumerate_cameras`` and the resolution
priority logic in ``CameraCaptureStream.initialize`` are exercised here
without requiring a real webcam, by stubbing ``cv2.VideoCapture``.
"""

from __future__ import annotations

import sys
import types
from typing import Any, Dict, List, Tuple

import pytest

from ledgrab.core.capture_engines import camera_engine as ce


# ---------------------------------------------------------------------------
# _parse_resolution — pure function, no stubs needed
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "value,expected",
    [
        ("auto", None),
        ("AUTO", None),
        ("", None),
        (None, None),
        (0, None),
        ("1920x1080", (1920, 1080)),
        ("1920X1080", (1920, 1080)),
        ("2560×1440", (2560, 1440)),  # unicode multiplication sign
        ("  1280x720  ", (1280, 720)),
        ("garbage", None),
        ("1920x", None),
        ("0x0", None),
        ("-1x100", None),
    ],
)
def test_parse_resolution(value: Any, expected: Tuple[int, int] | None) -> None:
    assert ce._parse_resolution(value) == expected


# ---------------------------------------------------------------------------
# Stubs that emulate cv2.VideoCapture without needing a real camera
# ---------------------------------------------------------------------------


CAP_PROP_FRAME_WIDTH = 3
CAP_PROP_FRAME_HEIGHT = 4
CAP_PROP_FPS = 5


class _StubCap:
    """Minimal cv2.VideoCapture stand-in.

    - ``default_dims``: what get() returns immediately after open.
    - ``max_dims``: what set(9999) clamps to (the camera's hardware ceiling).
    - ``set()`` honors any width/height up to ``max_dims`` and clamps higher.
    """

    def __init__(
        self,
        default_dims: Tuple[int, int],
        max_dims: Tuple[int, int],
        opened: bool = True,
    ):
        self._default = default_dims
        self._max = max_dims
        self._w, self._h = default_dims
        self._opened = opened
        self.read_calls = 0
        self.set_calls: List[Tuple[int, float]] = []

    def isOpened(self) -> bool:
        return self._opened

    def get(self, prop: int) -> float:
        if prop == CAP_PROP_FRAME_WIDTH:
            return float(self._w)
        if prop == CAP_PROP_FRAME_HEIGHT:
            return float(self._h)
        if prop == CAP_PROP_FPS:
            return 30.0
        return 0.0

    def set(self, prop: int, value: float) -> bool:
        self.set_calls.append((prop, value))
        if prop == CAP_PROP_FRAME_WIDTH:
            self._w = int(min(value, self._max[0]))
        elif prop == CAP_PROP_FRAME_HEIGHT:
            self._h = int(min(value, self._max[1]))
        return True

    def read(self):
        self.read_calls += 1
        # Return a 3-channel frame of the current size as a list of zeros —
        # the engine only inspects shape[:2].
        import numpy as np

        return True, np.zeros((self._h, self._w, 3), dtype=np.uint8)

    def release(self) -> None:
        self._opened = False


def _install_cv2_stub(monkeypatch: pytest.MonkeyPatch, factory) -> List[_StubCap]:
    """Install a fake `cv2` module so the engine code can `import cv2`.

    `factory(index, backend) -> _StubCap` produces a stub for each open call.
    Returns the list that all created stubs are appended to (for assertions).
    """
    created: List[_StubCap] = []

    def _video_capture(index, backend=None):
        cap = factory(index, backend)
        created.append(cap)
        return cap

    fake_cv2 = types.SimpleNamespace(
        VideoCapture=_video_capture,
        CAP_PROP_FRAME_WIDTH=CAP_PROP_FRAME_WIDTH,
        CAP_PROP_FRAME_HEIGHT=CAP_PROP_FRAME_HEIGHT,
        CAP_PROP_FPS=CAP_PROP_FPS,
        COLOR_BGR2RGB=4,
        cvtColor=lambda frame, _flag: frame,  # passthrough
    )
    monkeypatch.setitem(sys.modules, "cv2", fake_cv2)
    # Also bypass the SetupAPI friendly-name probe (Windows-only ctypes call)
    monkeypatch.setattr(ce, "_get_camera_friendly_names", lambda: {0: "Stub Cam"})
    # Reset the enumeration cache so each test sees a fresh probe
    ce._camera_cache = None
    ce._camera_cache_time = 0
    ce._active_cv2_indices.clear()
    return created


# ---------------------------------------------------------------------------
# _enumerate_cameras — probe-for-max
# ---------------------------------------------------------------------------


def test_enumerate_reports_camera_max_not_default(monkeypatch: pytest.MonkeyPatch) -> None:
    """A camera whose default mode is 640x480 but max is 2560x1440 should be
    reported at its max — that's what shows up in the display selector."""

    def factory(index, _backend):
        if index == 0:
            return _StubCap(default_dims=(640, 480), max_dims=(2560, 1440))
        return _StubCap(default_dims=(0, 0), max_dims=(0, 0), opened=False)

    _install_cv2_stub(monkeypatch, factory)

    cams = ce._enumerate_cameras("auto")

    assert len(cams) == 1
    assert cams[0]["width"] == 2560
    assert cams[0]["height"] == 1440
    assert cams[0]["name"] == "Stub Cam"


def test_enumerate_falls_back_when_probe_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
    """If a driver rejects the probe set() with an exception, fall back to the
    default mode rather than reporting 0x0."""

    class _RejectingCap(_StubCap):
        def set(self, prop: int, value: float) -> bool:
            raise RuntimeError("driver hates large values")

    def factory(index, _backend):
        if index == 0:
            return _RejectingCap(default_dims=(800, 600), max_dims=(800, 600))
        return _StubCap(default_dims=(0, 0), max_dims=(0, 0), opened=False)

    _install_cv2_stub(monkeypatch, factory)

    cams = ce._enumerate_cameras("auto")

    assert len(cams) == 1
    assert cams[0]["width"] == 800
    assert cams[0]["height"] == 600


# ---------------------------------------------------------------------------
# CameraCaptureStream.initialize — resolution selection priority
# ---------------------------------------------------------------------------


def _open_stream_with_config(
    monkeypatch: pytest.MonkeyPatch,
    config: Dict[str, Any],
    cam_max: Tuple[int, int] = (2560, 1440),
) -> Tuple[ce.CameraCaptureStream, List[_StubCap]]:
    def factory(index, _backend):
        if index == 0:
            return _StubCap(default_dims=(640, 480), max_dims=cam_max)
        return _StubCap(default_dims=(0, 0), max_dims=(0, 0), opened=False)

    created = _install_cv2_stub(monkeypatch, factory)
    stream = ce.CameraCaptureStream(display_index=0, config=config)
    stream.initialize()
    return stream, created


def _set_calls_for_size(cap: _StubCap) -> List[Tuple[int, float]]:
    return [c for c in cap.set_calls if c[0] in (CAP_PROP_FRAME_WIDTH, CAP_PROP_FRAME_HEIGHT)]


def test_init_default_auto_opens_at_max(monkeypatch: pytest.MonkeyPatch) -> None:
    """No resolution config = open at camera max via _PROBE_MAX_DIM."""
    stream, created = _open_stream_with_config(monkeypatch, config={})
    # Two caps: one for enumeration probe, one for the stream's own open.
    assert len(created) >= 2
    stream_cap = created[-1]
    size_sets = _set_calls_for_size(stream_cap)
    # Stream should request the max-probe sentinel for both width and height.
    assert (CAP_PROP_FRAME_WIDTH, float(ce._PROBE_MAX_DIM)) in size_sets
    assert (CAP_PROP_FRAME_HEIGHT, float(ce._PROBE_MAX_DIM)) in size_sets


def test_init_resolution_auto_opens_at_max(monkeypatch: pytest.MonkeyPatch) -> None:
    stream, created = _open_stream_with_config(monkeypatch, config={"resolution": "auto"})
    stream_cap = created[-1]
    size_sets = _set_calls_for_size(stream_cap)
    assert (CAP_PROP_FRAME_WIDTH, float(ce._PROBE_MAX_DIM)) in size_sets
    assert (CAP_PROP_FRAME_HEIGHT, float(ce._PROBE_MAX_DIM)) in size_sets


def test_init_explicit_resolution_string(monkeypatch: pytest.MonkeyPatch) -> None:
    stream, created = _open_stream_with_config(monkeypatch, config={"resolution": "1280x720"})
    stream_cap = created[-1]
    size_sets = _set_calls_for_size(stream_cap)
    assert (CAP_PROP_FRAME_WIDTH, 1280.0) in size_sets
    assert (CAP_PROP_FRAME_HEIGHT, 720.0) in size_sets
    # Should not request the max sentinel when an explicit size is given.
    assert (CAP_PROP_FRAME_WIDTH, float(ce._PROBE_MAX_DIM)) not in size_sets


def test_init_legacy_numeric_keys_take_precedence(monkeypatch: pytest.MonkeyPatch) -> None:
    """Stored configs from before the rename still work and override the
    new `resolution` field."""
    stream, created = _open_stream_with_config(
        monkeypatch,
        config={
            "resolution": "1920x1080",
            "resolution_width": 1280,
            "resolution_height": 720,
        },
    )
    stream_cap = created[-1]
    size_sets = _set_calls_for_size(stream_cap)
    assert (CAP_PROP_FRAME_WIDTH, 1280.0) in size_sets
    assert (CAP_PROP_FRAME_HEIGHT, 720.0) in size_sets
    # `resolution` should be ignored when legacy numeric keys are set.
    assert (CAP_PROP_FRAME_WIDTH, 1920.0) not in size_sets


def test_init_legacy_zeroes_fall_through_to_resolution(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Legacy keys with value 0 are the historical 'no override' state — they
    should fall through to the new `resolution` field."""
    stream, created = _open_stream_with_config(
        monkeypatch,
        config={
            "resolution": "1280x720",
            "resolution_width": 0,
            "resolution_height": 0,
        },
    )
    stream_cap = created[-1]
    size_sets = _set_calls_for_size(stream_cap)
    assert (CAP_PROP_FRAME_WIDTH, 1280.0) in size_sets
    assert (CAP_PROP_FRAME_HEIGHT, 720.0) in size_sets


# ---------------------------------------------------------------------------
# Engine class surface — config defaults and choices
# ---------------------------------------------------------------------------


def test_default_config_exposes_resolution() -> None:
    cfg = ce.CameraEngine.get_default_config()
    assert cfg["resolution"] == "auto"
    # Legacy numeric keys are no longer surfaced — UI shouldn't render them.
    assert "resolution_width" not in cfg
    assert "resolution_height" not in cfg


def test_config_choices_include_resolution_presets() -> None:
    choices = ce.CameraEngine.get_config_choices()
    assert "resolution" in choices
    # Exact set: auto + the 5 standard sizes the UI lists.
    assert choices["resolution"] == [
        "auto",
        "640x480",
        "1280x720",
        "1920x1080",
        "2560x1440",
        "3840x2160",
    ]
