"""
Unit tests for input adapters.
"""

import json
import socket
import time
from typing import Callable, Dict
from unittest.mock import MagicMock, Mock, patch

from multilat_solver import INPUT_ADAPTERS, InputAdapter
from multilat_solver.input_adapters import (
    CircularBuffer,
    FileInputAdapter,
    Message,
    SerialInputAdapter,
    UDPInputAdapter,
    register_input_adapter,
)


class TestMessage:
    """Test Message parser."""

    def test_message_parsing(self):
        """Test message parsing from bytes."""
        # Create a test message: id=1, data=5000 (in mm)
        data = b"\x01\x88\x13\x00\x00"  # Little-endian: id=1, int=5000
        msg = Message(data)
        assert msg.id == 1
        assert msg.data == 5000


class TestCircularBuffer:
    """Test CircularBuffer."""

    def test_buffer_append_pop(self):
        """Test basic append and pop."""
        buffer = CircularBuffer(10, element_size=5)
        assert buffer.size == 0

        buffer.append(b"test")
        assert buffer.size == 1

        item = buffer.pop()
        assert item == b"test"
        assert buffer.size == 0

    def test_buffer_wrap_around(self):
        """Test buffer wraps around correctly."""
        buffer = CircularBuffer(3, element_size=5)

        buffer.append(b"1")
        buffer.append(b"2")
        buffer.append(b"3")
        assert buffer.size == 3

        buffer.append(b"4")  # Should wrap around
        assert buffer.size == 3

        assert buffer.pop() == b"2"  # First item was overwritten
        assert buffer.pop() == b"3"
        assert buffer.pop() == b"4"

    def test_buffer_empty_msg(self):
        """Test buffer splits on marker."""
        buffer = CircularBuffer(10, element_size=5)
        data = b"\xff\xff\xff\x55"
        buffer.append(data)

        assert buffer.size == 0


class TestSerialInputAdapter:
    """Test SerialInputAdapter."""

    @patch("multilat_solver.input_adapters.serial.Serial")
    def test_serial_adapter_start_stop(self, mock_serial):
        """Test serial adapter start and stop."""
        mock_port = MagicMock()
        mock_port.in_waiting = 0
        mock_serial.return_value = mock_port

        adapter = SerialInputAdapter("/dev/ttyUSB0", 460800)
        callback = Mock()

        adapter.start(callback)
        assert adapter.is_running() is True

        adapter.stop()
        assert adapter.is_running() is False
        mock_port.close.assert_called_once()


class TestUDPInputAdapter:
    """Test UDPInputAdapter."""

    @patch("multilat_solver.input_adapters.socket.socket")
    def test_udp_adapter_json_dict(self, mock_socket_class):
        """Test UDP adapter with JSON dict format."""
        payload = json.dumps({"1": 2.5, "2": 3.1}).encode()
        call_count = [0]

        def recv_into(buffer):
            call_count[0] += 1
            if call_count[0] == 1:
                n = min(len(payload), len(buffer))
                buffer[:n] = payload[:n]
                return n
            raise socket.timeout  # subsequent calls: timeout so loop can check self.running

        mock_socket = MagicMock()
        mock_socket.recv_into.side_effect = recv_into

        mock_socket_class.return_value = mock_socket

        adapter = UDPInputAdapter("127.0.0.1", 5005)
        callback = Mock()

        adapter.start(callback)
        time.sleep(0.3)  # Let the read loop process one message
        adapter.stop()

        callback.assert_called_once()
        args = callback.call_args[0]
        assert args[0] == {1: 2.5, 2: 3.1}


class TestFileInputAdapter:
    """Test FileInputAdapter."""

    @patch("builtins.open", create=True)
    def test_file_adapter_json_dict(self, mock_open):
        """Test file adapter with JSON dict format."""
        mock_file = MagicMock()
        mock_file.__enter__.return_value = mock_file
        mock_file.__exit__.return_value = None
        mock_file.read.return_value = json.dumps({"1": 2.5, "2": 3.1})
        mock_open.return_value = mock_file

        adapter = FileInputAdapter("test.json", poll_interval=0.1)
        callback = Mock()

        adapter.start(callback)
        time.sleep(0.2)  # Wait for poll
        adapter.stop()

        # File should have been read
        mock_open.assert_called()


class TestRegisterInputAdapter:
    """Test register_input_adapter."""

    def test_register_input_adapter(self):
        """Test register_input_adapter."""

        class CustomInputAdapter(InputAdapter):
            """Custom input adapter."""

            def __init__(self, config: dict):
                self.config = config
                super().__init__(config)

            def start(self, callback: Callable[[Dict[int, Dict[str, float]]], None]):
                pass

            def stop(self):
                pass

            def is_running(self) -> bool:
                return False

        register_input_adapter("custom", CustomInputAdapter)
        assert "custom" in INPUT_ADAPTERS
