"""
Tests for the serial manager module.
"""

import pytest
import threading
import time
from unittest.mock import Mock, patch, MagicMock
from esppal.serial_manager import SerialManager, PortState
from esppal.buffer import RingBuffer


@pytest.fixture
def buffer():
    """Create a test buffer."""
    return RingBuffer(max_size_bytes=1024 * 1024)


@pytest.fixture
def mock_serial():
    """Create a mock serial module."""
    with patch("esppal.serial_manager.serial") as mock:
        yield mock


@pytest.fixture
def mock_detector():
    """Create a mock device detector."""
    with patch("esppal.serial_manager.DeviceDetector") as mock:
        detector = Mock()
        detector.find_device.return_value = "/dev/ttyUSB0"
        detector.start_monitoring = Mock()
        detector.stop_monitoring = Mock()
        mock.return_value = detector
        yield detector


class TestSerialManagerBasics:
    """Basic tests for SerialManager."""

    def test_initial_state(self, buffer):
        """Manager starts in disconnected state."""
        with patch("esppal.serial_manager.DeviceDetector"):
            manager = SerialManager(buffer)
            assert manager.state == PortState.DISCONNECTED
            assert manager.device_path is None

    def test_baudrate_property(self, buffer):
        """Baudrate can be get/set."""
        with patch("esppal.serial_manager.DeviceDetector"):
            manager = SerialManager(buffer, baudrate=9600)
            assert manager.baudrate == 9600

            manager.baudrate = 115200
            assert manager.baudrate == 115200


class TestSerialManagerConnection:
    """Tests for connection handling."""

    def test_state_change_callback(self, buffer, mock_detector, mock_serial):
        """State change callback is called."""
        state_changes = []

        def on_state_change(state, device):
            state_changes.append((state, device))

        # Set up mock serial
        mock_port = MagicMock()
        mock_port.is_open = True
        mock_port.read.return_value = b""
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(
            buffer,
            on_state_change=on_state_change
        )

        try:
            manager.start()
            time.sleep(0.3)  # Allow time for connection

            # Should have transitioned to connected
            assert any(s == PortState.CONNECTED for s, d in state_changes)
        finally:
            manager.stop()

    def test_read_to_buffer(self, buffer, mock_detector, mock_serial):
        """Data read from serial goes to buffer."""
        # Set up mock serial
        mock_port = MagicMock()
        mock_port.is_open = True
        read_count = [0]

        def mock_read(size):
            read_count[0] += 1
            if read_count[0] == 1:
                return b"test data"
            return b""

        mock_port.read.side_effect = mock_read
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(buffer)

        try:
            manager.start()
            time.sleep(0.5)  # Allow time for reading
        finally:
            manager.stop()

        # Check buffer has data
        entries = list(buffer.iterate_from_seq(0))
        assert len(entries) >= 1

        # Find serial data entry
        serial_entries = [e for seq, e in entries if e.source == "serial"]
        assert len(serial_entries) >= 1
        assert serial_entries[0].data == b"test data"


class TestPortReleaseReclaim:
    """Tests for port release/reclaim."""

    def test_port_release(self, buffer, mock_detector, mock_serial):
        """Port can be released."""
        mock_port = MagicMock()
        mock_port.is_open = True
        mock_port.read.return_value = b""
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(buffer)

        try:
            manager.start()
            time.sleep(0.2)

            # Should be connected
            assert manager.state == PortState.CONNECTED

            # Release port
            assert manager.release_port(timeout=2.0)
            assert manager.state == PortState.RELEASED

            # Serial should be closed
            mock_port.close.assert_called()
        finally:
            manager.stop()

    def test_port_reclaim(self, buffer, mock_detector, mock_serial):
        """Port can be reclaimed after release."""
        mock_port = MagicMock()
        mock_port.is_open = True
        mock_port.read.return_value = b""
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(buffer)

        try:
            manager.start()
            time.sleep(0.2)

            # Release and reclaim
            manager.release_port(timeout=2.0)
            manager.reclaim_port()

            time.sleep(0.5)

            # Should reconnect
            assert manager.state == PortState.CONNECTED
        finally:
            manager.stop()

    def test_release_when_disconnected(self, buffer):
        """Release succeeds when already disconnected."""
        with patch("esppal.serial_manager.DeviceDetector") as mock:
            detector = Mock()
            detector.find_device.return_value = None
            detector.start_monitoring = Mock()
            detector.stop_monitoring = Mock()
            mock.return_value = detector

            manager = SerialManager(buffer)

            try:
                manager.start()
                time.sleep(0.1)

                # Should be disconnected
                assert manager.state == PortState.DISCONNECTED

                # Release should succeed immediately
                assert manager.release_port(timeout=1.0)
                assert manager.state == PortState.RELEASED
            finally:
                manager.stop()


class TestReconnection:
    """Tests for reconnection handling."""

    def test_reconnect_after_disconnect(self, buffer, mock_detector, mock_serial):
        """Manager reconnects after disconnect."""
        mock_port = MagicMock()
        mock_port.is_open = True

        # First read succeeds, then fails to simulate disconnect
        read_count = [0]

        def mock_read(size):
            read_count[0] += 1
            if read_count[0] < 3:
                return b"data"
            raise Exception("Simulated disconnect")

        mock_port.read.side_effect = mock_read
        mock_serial.Serial.return_value = mock_port
        mock_serial.SerialException = Exception

        manager = SerialManager(buffer)

        try:
            manager.start()
            time.sleep(1.0)  # Allow time for disconnect and reconnect attempt
        finally:
            manager.stop()

        # Should have attempted to reopen
        assert mock_serial.Serial.call_count >= 2


class TestBaudrateConfiguration:
    """Tests for baudrate configuration."""

    def test_baudrate_used_in_connection(self, buffer, mock_detector, mock_serial):
        """Baudrate is used when opening connection."""
        mock_port = MagicMock()
        mock_port.is_open = True
        mock_port.read.return_value = b""
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(buffer, baudrate=9600)

        try:
            manager.start()
            time.sleep(0.2)
        finally:
            manager.stop()

        # Check Serial was called with correct baudrate
        mock_serial.Serial.assert_called()
        call_kwargs = mock_serial.Serial.call_args
        assert call_kwargs[1]["baudrate"] == 9600


class TestStartStop:
    """Tests for start/stop lifecycle."""

    def test_start_stop(self, buffer, mock_detector, mock_serial):
        """Manager can be started and stopped."""
        mock_port = MagicMock()
        mock_port.is_open = True
        mock_port.read.return_value = b""
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(buffer)

        manager.start()
        time.sleep(0.1)

        manager.stop()
        assert manager.state == PortState.DISCONNECTED

    def test_double_start(self, buffer, mock_detector, mock_serial):
        """Double start is safe."""
        mock_port = MagicMock()
        mock_port.is_open = True
        mock_port.read.return_value = b""
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(buffer)

        try:
            manager.start()
            manager.start()  # Should not raise
            time.sleep(0.1)
        finally:
            manager.stop()

    def test_double_stop(self, buffer, mock_detector, mock_serial):
        """Double stop is safe."""
        mock_port = MagicMock()
        mock_port.is_open = True
        mock_port.read.return_value = b""
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(buffer)

        manager.start()
        time.sleep(0.1)
        manager.stop()
        manager.stop()  # Should not raise


class TestWaitForDevice:
    """Tests for wait_for_device."""

    def test_wait_when_connected(self, buffer, mock_detector, mock_serial):
        """Wait succeeds immediately when connected."""
        mock_port = MagicMock()
        mock_port.is_open = True
        mock_port.read.return_value = b""
        mock_serial.Serial.return_value = mock_port

        manager = SerialManager(buffer)

        try:
            manager.start()
            time.sleep(0.2)

            # Should succeed quickly
            start = time.time()
            assert manager.wait_for_device(timeout=5.0)
            elapsed = time.time() - start
            assert elapsed < 1.0
        finally:
            manager.stop()

    def test_wait_timeout(self, buffer):
        """Wait times out when device not found."""
        with patch("esppal.serial_manager.DeviceDetector") as mock:
            detector = Mock()
            detector.find_device.return_value = None
            detector.start_monitoring = Mock()
            detector.stop_monitoring = Mock()
            mock.return_value = detector

            manager = SerialManager(buffer)

            try:
                manager.start()

                start = time.time()
                result = manager.wait_for_device(timeout=1.0)
                elapsed = time.time() - start

                assert result is False
                assert elapsed >= 1.0
            finally:
                manager.stop()


class TestVidPidConfiguration:
    """Tests for VID/PID configuration."""

    def test_vidpid_passed_to_detector(self, buffer, mock_serial):
        """VID/PID is passed to device detector."""
        with patch("esppal.serial_manager.DeviceDetector") as mock:
            detector = Mock()
            detector.find_device.return_value = None
            detector.start_monitoring = Mock()
            detector.stop_monitoring = Mock()
            mock.return_value = detector

            manager = SerialManager(
                buffer,
                vid="10c4",
                pid="ea60"
            )

            try:
                manager.start()
                time.sleep(0.2)
            finally:
                manager.stop()

            # Detector should have been called with VID/PID
            detector.find_device.assert_called_with("10c4", "ea60")
