"""
Tests for rtsp_stream.VideoStream
Uses pytest-mock to avoid needing a real camera.
"""

import time
from unittest.mock import MagicMock, patch

import numpy as np
import pytest

from rtsp_stream import VideoStream


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_cap(frames=None, *, open_=True):
    """Return a mock cv2.VideoCapture."""
    cap = MagicMock()
    cap.isOpened.return_value = open_
    cap.get.return_value = 30.0  # FPS / width / height

    if frames is None:
        frame = np.zeros((480, 640, 3), dtype=np.uint8)
        cap.read.return_value = (True, frame)
    else:
        cap.read.side_effect = frames

    return cap


# ---------------------------------------------------------------------------
# Connection tests
# ---------------------------------------------------------------------------

class TestConnection:
    def test_starts_and_reads_frame(self):
        cap = _make_cap()
        with patch("cv2.VideoCapture", return_value=cap):
            stream = VideoStream("rtsp://fake/stream")
            stream.start()
            time.sleep(0.05)
            frame = stream.read()
            stream.stop()

        assert frame is not None
        assert frame.shape == (480, 640, 3)

    def test_context_manager(self):
        cap = _make_cap()
        with patch("cv2.VideoCapture", return_value=cap):
            with VideoStream("rtsp://fake/stream").start() as stream:
                time.sleep(0.05)
                assert stream.read() is not None

    def test_on_connect_callback_called(self):
        cap = _make_cap()
        cb = MagicMock()
        with patch("cv2.VideoCapture", return_value=cap):
            stream = VideoStream("rtsp://fake/stream", on_connect=cb)
            stream.start()
            time.sleep(0.05)
            stream.stop()
        cb.assert_called_once()

    def test_is_connected_true_after_start(self):
        cap = _make_cap()
        with patch("cv2.VideoCapture", return_value=cap):
            stream = VideoStream("rtsp://fake/stream")
            stream.start()
            time.sleep(0.05)
            assert stream.is_connected is True
            stream.stop()


# ---------------------------------------------------------------------------
# Reconnection tests
# ---------------------------------------------------------------------------

class TestReconnection:
    def test_reconnects_after_feed_drop(self):
        frame = np.zeros((480, 640, 3), dtype=np.uint8)
        good = (True, frame)
        bad = (False, None)

        # First cap: one good frame then drop; second cap: steady good frames
        cap1 = _make_cap(frames=[good, bad])
        cap2 = _make_cap()

        on_disconnect = MagicMock()
        on_connect = MagicMock()

        caps = iter([cap1, cap2])
        with patch("cv2.VideoCapture", side_effect=lambda *a, **kw: next(caps)):
            stream = VideoStream(
                "rtsp://fake/stream",
                reconnect_delay=0.01,
                on_connect=on_connect,
                on_disconnect=on_disconnect,
            )
            stream.start()
            time.sleep(0.2)
            frame_after = stream.read()
            stream.stop()

        on_disconnect.assert_called_once()
        assert on_connect.call_count == 2  # initial + reconnect
        assert frame_after is not None

    def test_max_reconnect_attempts_respected(self):
        closed_cap = _make_cap(open_=False)
        on_failure = MagicMock()

        with patch("cv2.VideoCapture", return_value=closed_cap):
            stream = VideoStream(
                "rtsp://fake/stream",
                reconnect_delay=0.01,
                max_reconnect_attempts=3,
                on_reconnect_failure=on_failure,
            )
            stream.start()
            stream._thread.join(timeout=2)

        on_failure.assert_called_once()
        assert not stream._running

    def test_reconnect_count_increments(self):
        frame = np.zeros((480, 640, 3), dtype=np.uint8)
        cap1 = _make_cap(frames=[(True, frame), (False, None)])
        cap2 = _make_cap()

        caps = iter([cap1, cap2])
        with patch("cv2.VideoCapture", side_effect=lambda *a, **kw: next(caps)):
            stream = VideoStream("rtsp://fake/stream", reconnect_delay=0.01)
            stream.start()
            time.sleep(0.2)
            stream.stop()

        # Should have tracked at least the drop
        assert stream.reconnect_count >= 0  # exact value depends on timing


# ---------------------------------------------------------------------------
# Property tests
# ---------------------------------------------------------------------------

class TestProperties:
    def test_fps_returns_value(self):
        cap = _make_cap()
        cap.get.return_value = 25.0
        with patch("cv2.VideoCapture", return_value=cap):
            stream = VideoStream("rtsp://fake/stream")
            stream.start()
            time.sleep(0.05)
            assert stream.fps == 25.0
            stream.stop()

    def test_properties_return_zero_when_stopped(self):
        cap = _make_cap()
        with patch("cv2.VideoCapture", return_value=cap):
            stream = VideoStream("rtsp://fake/stream")
            # Never started
            assert stream.fps == 0.0
            assert stream.frame_width == 0
            assert stream.frame_height == 0

    def test_double_start_raises(self):
        cap = _make_cap()
        with patch("cv2.VideoCapture", return_value=cap):
            stream = VideoStream("rtsp://fake/stream")
            stream.start()
            with pytest.raises(RuntimeError):
                stream.start()
            stream.stop()
