"""Tests for NMEA serial port reader."""

import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from uuid import UUID

from fishinfo.exceptions import ConnectionError
from fishinfo.types import TelemetryPoint
from fishinfo.nmea.converter import NMEAConverter
from fishinfo.nmea.serial import NMEASerialReader, NMEASerialBatcher


class TestNMEASerialReader:
    """Tests for NMEASerialReader class."""

    def setup_method(self) -> None:
        """Set up test fixtures."""
        self.vessel_id = UUID("12345678-1234-1234-1234-123456789abc")
        self.tenant_id = UUID("87654321-4321-4321-4321-cba987654321")
        self.converter = NMEAConverter(vessel_id=self.vessel_id, tenant_id=self.tenant_id)

    def test_reader_initialization(self) -> None:
        """Test reader initialization."""
        reader = NMEASerialReader(
            port="/dev/ttyUSB0",
            baudrate=4800,
            converter=self.converter,
        )

        assert reader.port == "/dev/ttyUSB0"
        assert reader.baudrate == 4800
        assert reader.converter == self.converter
        assert reader.validate_checksums is True
        assert reader.skip_invalid is True

    def test_reader_initialization_custom_settings(self) -> None:
        """Test reader initialization with custom settings."""
        reader = NMEASerialReader(
            port="COM3",
            baudrate=38400,
            validate_checksums=False,
            skip_invalid=False,
        )

        assert reader.baudrate == 38400
        assert reader.validate_checksums is False
        assert reader.skip_invalid is False

    @pytest.mark.asyncio
    async def test_context_manager(self) -> None:
        """Test async context manager."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()
            mock_open.return_value = (mock_reader, mock_writer)

            async with reader:
                assert reader._running is True

            assert reader._running is False
            mock_writer.close.assert_called_once()
            mock_writer.wait_closed.assert_called_once()

    @pytest.mark.asyncio
    async def test_connect_success(self) -> None:
        """Test successful connection."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()

            assert reader._running is True
            assert reader._reader is not None
            assert reader._writer is not None

    @pytest.mark.asyncio
    async def test_connect_failure(self) -> None:
        """Test connection failure."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_open.side_effect = Exception("Port not found")

            with pytest.raises(ConnectionError, match="Failed to open"):
                await reader.connect()

    @pytest.mark.asyncio
    async def test_disconnect(self) -> None:
        """Test disconnection."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()
            await reader.disconnect()

            assert reader._running is False
            mock_writer.close.assert_called_once()
            mock_writer.wait_closed.assert_called_once()


class TestReadSentences:
    """Tests for read_sentences method."""

    def setup_method(self) -> None:
        """Set up test fixtures."""
        self.vessel_id = UUID("12345678-1234-1234-1234-123456789abc")
        self.tenant_id = UUID("87654321-4321-4321-4321-cba987654321")
        self.converter = NMEAConverter(vessel_id=self.vessel_id, tenant_id=self.tenant_id)

    @pytest.mark.asyncio
    async def test_read_sentences_basic(self) -> None:
        """Test reading basic NMEA sentences."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        test_sentences = [
            b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47\r\n",
            b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n",
        ]

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()

            # Setup readline to return sentences
            mock_reader.readline = AsyncMock(side_effect=test_sentences + [b""])
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()

            sentences = []
            async for sentence in reader.read_sentences():
                sentences.append(sentence)
                if len(sentences) >= 2:
                    reader._running = False
                    break

            assert len(sentences) == 2
            assert "$GPGGA" in sentences[0]
            assert "$GPRMC" in sentences[1]

            await reader.disconnect()

    @pytest.mark.asyncio
    async def test_read_sentences_skip_empty(self) -> None:
        """Test reading sentences skips empty lines."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        test_sentences = [
            b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47\r\n",
            b"\r\n",
            b"\n",
            b"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r\n",
        ]

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()

            mock_reader.readline = AsyncMock(side_effect=test_sentences + [b""])
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()

            sentences = []
            async for sentence in reader.read_sentences():
                sentences.append(sentence)
                if len(sentences) >= 2:
                    reader._running = False
                    break

            assert len(sentences) == 2
            await reader.disconnect()

    @pytest.mark.asyncio
    async def test_read_sentences_not_connected(self) -> None:
        """Test reading sentences when not connected."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        with pytest.raises(ConnectionError, match="Not connected"):
            async for _ in reader.read_sentences():
                pass


class TestStreamTelemetry:
    """Tests for stream method."""

    def setup_method(self) -> None:
        """Set up test fixtures."""
        self.vessel_id = UUID("12345678-1234-1234-1234-123456789abc")
        self.tenant_id = UUID("87654321-4321-4321-4321-cba987654321")
        self.converter = NMEAConverter(vessel_id=self.vessel_id, tenant_id=self.tenant_id)

    @pytest.mark.asyncio
    async def test_stream_basic(self) -> None:
        """Test streaming telemetry points."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        test_sentences = [
            b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47\r\n",
            b"$HEHDT,274.07,T*03\r\n",
        ]

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()

            mock_reader.readline = AsyncMock(side_effect=test_sentences + [b""])
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()

            points = []
            async for point in reader.stream():
                points.append(point)
                if len(points) >= 2:
                    reader._running = False
                    break

            assert len(points) == 2
            assert all(isinstance(p, TelemetryPoint) for p in points)

            await reader.disconnect()

    @pytest.mark.asyncio
    async def test_stream_ais_messages(self) -> None:
        """Test streaming AIS messages."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        test_sentences = [
            b"!AIVDM,1,1,,B,13u@Dp0P00PlT>HMGN4?vhR00<02,0*66\r\n",
        ]

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()

            mock_reader.readline = AsyncMock(side_effect=test_sentences + [b""])
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()

            points = []
            async for point in reader.stream():
                points.append(point)
                if len(points) >= 1:
                    reader._running = False
                    break

            assert len(points) >= 1
            assert all(isinstance(p, TelemetryPoint) for p in points)

            await reader.disconnect()

    @pytest.mark.asyncio
    async def test_stream_no_converter(self) -> None:
        """Test streaming without converter raises error."""
        reader = NMEASerialReader("/dev/ttyUSB0")

        with pytest.raises(ValueError, match="No converter"):
            async for _ in reader.stream():
                pass

    @pytest.mark.asyncio
    async def test_stream_skip_invalid(self) -> None:
        """Test streaming skips invalid sentences."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter, skip_invalid=True)

        test_sentences = [
            b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*99\r\n",  # Invalid checksum
            b"$HEHDT,274.07,T*03\r\n",  # Valid
        ]

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()

            mock_reader.readline = AsyncMock(side_effect=test_sentences + [b""])
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()

            points = []
            async for point in reader.stream():
                points.append(point)
                if len(points) >= 1:
                    reader._running = False
                    break

            # Should only get the valid sentence
            assert len(points) == 1

            await reader.disconnect()


class TestNMEASerialBatcher:
    """Tests for NMEASerialBatcher class."""

    def setup_method(self) -> None:
        """Set up test fixtures."""
        self.vessel_id = UUID("12345678-1234-1234-1234-123456789abc")
        self.tenant_id = UUID("87654321-4321-4321-4321-cba987654321")
        self.converter = NMEAConverter(vessel_id=self.vessel_id, tenant_id=self.tenant_id)

    def test_batcher_initialization(self) -> None:
        """Test batcher initialization."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)
        batcher = NMEASerialBatcher(reader, batch_size=50, flush_interval=10.0)

        assert batcher.reader == reader
        assert batcher.batch_size == 50
        assert batcher.flush_interval == 10.0

    @pytest.mark.asyncio
    async def test_batcher_run(self) -> None:
        """Test batcher run method."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)
        batcher = NMEASerialBatcher(reader, batch_size=2, flush_interval=1.0)

        test_sentences = [
            b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47\r\n",
            b"$HEHDT,274.07,T*03\r\n",
            b"$YXMTW,15.5,C*1F\r\n",
        ]

        # Mock client
        mock_client = AsyncMock()
        mock_client.send_batch = AsyncMock(return_value=Mock(
            points_sent=2,
            points_buffered=0,
            points_dropped=0,
        ))

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader_stream = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()

            mock_reader_stream.readline = AsyncMock(side_effect=test_sentences + [b""])
            mock_open.return_value = (mock_reader_stream, mock_writer)

            # Run batcher for a short time
            run_task = asyncio.create_task(batcher.run(mock_client))

            # Wait a bit then cancel
            await asyncio.sleep(0.5)
            run_task.cancel()

            try:
                await run_task
            except asyncio.CancelledError:
                pass

            # Should have called send_batch at least once
            assert mock_client.send_batch.call_count >= 1


class TestEdgeCases:
    """Tests for edge cases and error conditions."""

    def setup_method(self) -> None:
        """Set up test fixtures."""
        self.vessel_id = UUID("12345678-1234-1234-1234-123456789abc")
        self.tenant_id = UUID("87654321-4321-4321-4321-cba987654321")
        self.converter = NMEAConverter(vessel_id=self.vessel_id, tenant_id=self.tenant_id)

    @pytest.mark.asyncio
    async def test_connection_lost_during_read(self) -> None:
        """Test handling connection loss during read."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter)

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()

            # Simulate connection loss (readline returns empty)
            mock_reader.readline = AsyncMock(return_value=b"")
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()

            with pytest.raises(ConnectionError, match="connection lost"):
                async for _ in reader.read_sentences():
                    pass

            await reader.disconnect()

    @pytest.mark.asyncio
    async def test_unicode_decode_error(self) -> None:
        """Test handling unicode decode errors."""
        reader = NMEASerialReader("/dev/ttyUSB0", converter=self.converter, skip_invalid=True)

        test_sentences = [
            b"\xff\xfe\x00\x00",  # Invalid UTF-8
            b"$HEHDT,274.07,T*03\r\n",  # Valid
        ]

        with patch("serial_asyncio.open_serial_connection") as mock_open:
            mock_reader = AsyncMock()
            mock_writer = MagicMock()
            mock_writer.close = MagicMock()
            mock_writer.wait_closed = AsyncMock()

            mock_reader.readline = AsyncMock(side_effect=test_sentences + [b""])
            mock_open.return_value = (mock_reader, mock_writer)

            await reader.connect()

            sentences = []
            async for sentence in reader.read_sentences():
                sentences.append(sentence)
                if len(sentences) >= 1:
                    reader._running = False
                    break

            # Should skip invalid bytes and continue
            assert len(sentences) == 1

            await reader.disconnect()
