"""Tests for the serial transport abstraction."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from ledgrab.core.devices.serial_transport import (
    DEFAULT_BAUD_RATE,
    PySerialTransport,
    SerialPortInfo,
    list_serial_ports,
    open_transport,
    parse_serial_url,
    port_exists,
)

# ── URL parsing ────────────────────────────────────────────────────


@pytest.mark.parametrize(
    "url,expected",
    [
        ("COM3", ("COM3", DEFAULT_BAUD_RATE)),
        ("COM3:230400", ("COM3", 230400)),
        ("/dev/ttyUSB0", ("/dev/ttyUSB0", DEFAULT_BAUD_RATE)),
        ("/dev/ttyUSB0:230400", ("/dev/ttyUSB0", 230400)),
        # USB URLs use @ for baud since `:` separates VID:PID:serial
        ("usb:1a86:7523", ("usb:1a86:7523", DEFAULT_BAUD_RATE)),
        ("usb:1a86:7523:AB01", ("usb:1a86:7523:AB01", DEFAULT_BAUD_RATE)),
        ("usb:1a86:7523@230400", ("usb:1a86:7523", 230400)),
        ("usb:1a86:7523:AB01@500000", ("usb:1a86:7523:AB01", 500000)),
    ],
)
def test_parse_serial_url(url, expected):
    assert parse_serial_url(url) == expected


def test_parse_serial_url_strips_whitespace():
    assert parse_serial_url("  COM3  ") == ("COM3", DEFAULT_BAUD_RATE)


# ── PySerialTransport ──────────────────────────────────────────────


def test_pyserial_transport_open_and_write(monkeypatch):
    fake_serial_module = MagicMock()
    fake_handle = MagicMock(is_open=True)
    fake_serial_module.Serial.return_value = fake_handle

    monkeypatch.setitem(__import__("sys").modules, "serial", fake_serial_module)

    t = PySerialTransport("COM3", 115200)
    assert not t.is_open
    t.open()
    fake_serial_module.Serial.assert_called_once_with(port="COM3", baudrate=115200, timeout=1.0)
    assert t.is_open

    t.write(b"hello")
    fake_handle.write.assert_called_once_with(b"hello")

    t.flush()
    fake_handle.flush.assert_called_once()

    t.close()
    fake_handle.close.assert_called_once()


def test_pyserial_transport_write_before_open_raises():
    t = PySerialTransport("COM3", 115200)
    with pytest.raises(RuntimeError, match="not open"):
        t.write(b"x")


# ── Factory ────────────────────────────────────────────────────────


def test_open_transport_picks_pyserial_for_com_url():
    t = open_transport("COM3:230400")
    assert isinstance(t, PySerialTransport)
    assert t._device == "COM3"
    assert t._baud_rate == 230400


def test_open_transport_explicit_baud_overrides_url():
    t = open_transport("COM3:230400", baud_rate=500000)
    assert t._baud_rate == 500000


def test_open_transport_picks_android_for_usb_url(monkeypatch):
    """usb: URLs should route to the Android transport even off-Android."""
    # Importing AndroidSerialTransport itself works on any host; only the
    # bridge call inside .open() fails when not on Android.
    t = open_transport("usb:1a86:7523@230400")
    assert type(t).__name__ == "AndroidSerialTransport"
    assert t._baud_rate == 230400


# ── Discovery ──────────────────────────────────────────────────────


def test_list_serial_ports_uses_pyserial_on_desktop(monkeypatch):
    monkeypatch.setattr("ledgrab.core.devices.serial_transport.is_android", lambda: False)

    fake_module = MagicMock()
    fake_module.tools.list_ports.comports.return_value = [
        MagicMock(device="COM3", description="USB Serial CH340"),
        MagicMock(device="COM4", description=None),
    ]
    with patch.dict(
        "sys.modules",
        {
            "serial": fake_module,
            "serial.tools": fake_module.tools,
            "serial.tools.list_ports": fake_module.tools.list_ports,
        },
    ):
        ports = list_serial_ports()

    assert len(ports) == 2
    assert ports[0] == SerialPortInfo(device="COM3", description="USB Serial CH340")
    # Falls back to device id when description is None
    assert ports[1].description == "COM4"


def test_list_serial_ports_routes_to_android_when_on_android(monkeypatch):
    monkeypatch.setattr("ledgrab.core.devices.serial_transport.is_android", lambda: True)

    fake_devices = [SerialPortInfo(device="usb:1a86:7523", description="CH340 LED")]
    fake_mod = MagicMock()
    fake_mod.list_android_usb_devices.return_value = fake_devices
    monkeypatch.setitem(
        __import__("sys").modules,
        "ledgrab.core.devices.android_serial_transport",
        fake_mod,
    )

    assert list_serial_ports() == fake_devices


def test_port_exists_is_case_insensitive(monkeypatch):
    fake_ports = [
        SerialPortInfo(device="COM3", description="x"),
        SerialPortInfo(device="/dev/ttyUSB0", description="y"),
    ]
    monkeypatch.setattr(
        "ledgrab.core.devices.serial_transport.list_serial_ports", lambda: fake_ports
    )
    assert port_exists("com3") is True
    assert port_exists("/dev/ttyusb0") is True
    assert port_exists("COM99") is False
