from __future__ import annotations

import ctypes
import types

import pytest

from pymulator.core import can as can_module


def test_default_backend_is_gs_usb_on_macos(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "darwin")
    wires = can_module.SocketCANInterface("can0")
    assert wires.backend == "gs_usb"
    assert wires.channel == 0


def test_default_backend_is_socketcan_on_linux(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "linux")
    wires = can_module.SocketCANInterface("can0")
    assert wires.backend == "socketcan"
    assert wires.channel == "can0"


def test_explicit_backend_prefix_overrides_platform_default(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "darwin")
    wires = can_module.SocketCANInterface("socketcan:can0")
    assert wires.backend == "socketcan"
    assert wires.channel == "can0"


def test_gs_usb_accepts_numeric_channels() -> None:
    wires = can_module.SocketCANInterface("gs_usb:2")
    assert wires.backend == "gs_usb"
    assert wires.channel == 2


def test_gs_usb_rejects_non_numeric_channels() -> None:
    with pytest.raises(can_module.UnsupportedConfiguration, match="gs_usb channel must be numeric"):
        can_module.SocketCANInterface("gs_usb:usbcan")


def test_toucan_accepts_numeric_channels() -> None:
    wires = can_module.SocketCANInterface("toucan:1")
    assert wires.backend == "toucan"
    assert wires.channel == 1


def test_toucan_rejects_non_numeric_channels() -> None:
    with pytest.raises(can_module.UnsupportedConfiguration, match="toucan channel must be numeric"):
        can_module.SocketCANInterface("toucan:usbcan")


def test_toucan_open_read_write_close(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "darwin")

    class _MessageFlags(ctypes.LittleEndianStructure):
        _fields_ = [
            ("xtd", ctypes.c_uint8, 1),
            ("rtr", ctypes.c_uint8, 1),
            ("fdf", ctypes.c_uint8, 1),
            ("brs", ctypes.c_uint8, 1),
            ("esi", ctypes.c_uint8, 1),
            ("_unused", ctypes.c_uint8, 2),
            ("sts", ctypes.c_uint8, 1),
        ]

    class _Message(ctypes.LittleEndianStructure):
        _fields_ = [
            ("id", ctypes.c_uint32),
            ("flags", _MessageFlags),
            ("dlc", ctypes.c_uint8),
            ("data", ctypes.c_uint8 * 64),
        ]

    class _FakeLib:
        def __init__(self) -> None:
            self.writes: list[tuple[int, int, int, bytes, int]] = []

        def can_write(self, handle: int, msg_ptr) -> int:  # noqa: ANN001
            msg = ctypes.cast(msg_ptr, ctypes.POINTER(_Message)).contents
            payload = bytes(msg.data[: msg.dlc])
            self.writes.append((handle, int(msg.id), int(msg.dlc), payload, int(msg.flags.xtd)))
            return 0

    class _FakeTouCANInterface:
        def __init__(self) -> None:
            self._lib = _FakeLib()
            self._handle = 7
            self._started_with_index: int | None = None
            self._open = False
            self.reset_called = False
            self.exit_called = False
            self._reads: list[tuple[int, object | None]] = []

        def init(self, channel: int, mode) -> int:  # noqa: ANN001
            del mode
            if channel != 0:
                return -1
            self._open = True
            return 0

        def start(self, bitrate) -> int:  # noqa: ANN001
            self._started_with_index = int(bitrate.index)
            return 0

        def reset(self) -> int:
            self.reset_called = True
            return 0

        def exit(self) -> int:
            self.exit_called = True
            self._open = False
            return 0

        def read(self, timeout: int = 0) -> tuple[int, object | None]:
            del timeout
            if self._reads:
                return self._reads.pop(0)
            return (can_module.TOUCAN_CANERR_RX_EMPTY, None)

    created: list[_FakeTouCANInterface] = []

    def _factory() -> _FakeTouCANInterface:
        instance = _FakeTouCANInterface()
        created.append(instance)
        return instance

    fake_module = types.SimpleNamespace(
        CANMODE_DEFAULT=types.SimpleNamespace(value=0),
        CANERR_NOERROR=0,
        CANERR_RX_EMPTY=can_module.TOUCAN_CANERR_RX_EMPTY,
        OpMode=type("OpMode", (), {"__init__": lambda self: setattr(self, "byte", 0)}),
        Bitrate=type("Bitrate", (), {"__init__": lambda self: setattr(self, "index", 0)}),
        Message=_Message,
        TouCANInterface=_factory,
    )
    monkeypatch.setattr(can_module.importlib, "import_module", lambda _name: fake_module)

    wires = can_module.SocketCANInterface("toucan:0")
    wires.open(500000)
    assert wires.backend == "toucan"
    assert created and created[0]._started_with_index == -2

    status_msg = _Message()
    status_msg.flags.sts = 1
    rx_msg = _Message()
    rx_msg.id = 0x7E8
    rx_msg.dlc = 3
    rx_msg.data[0] = 0x62
    rx_msg.data[1] = 0xF1
    rx_msg.data[2] = 0x90
    created[0]._reads = [
        (can_module.TOUCAN_CANERR_NOERROR, status_msg),
        (can_module.TOUCAN_CANERR_NOERROR, rx_msg),
    ]

    frame = wires.read(timeout_ms=100)
    assert frame.arbitration_id == 0x7E8
    assert frame.data == [0x62, 0xF1, 0x90]

    wires.write(can_module.CANFrame(arbitration_id=0x7E0, data=[0x22, 0xF1, 0x90], dlc=3))
    assert created[0]._lib.writes == [(7, 0x7E0, 3, b"\x22\xF1\x90", 0)]

    wires.close()
    assert created[0].reset_called is True
    assert created[0].exit_called is True


def test_toucan_open_rejects_can_fd(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "darwin")
    wires = can_module.SocketCANInterface("toucan:0")
    with pytest.raises(can_module.UnsupportedConfiguration, match="does not support CAN-FD"):
        wires.open(500000, fd=True)


def test_socketcan_open_passes_fd_and_data_bitrate_when_enabled(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "linux")
    monkeypatch.setattr(can_module, "_configure_socketcan_interface", lambda *a, **kw: None)

    calls: list[dict[str, object]] = []

    class _FakeCan:
        class Message:  # pragma: no cover - structural stub
            def __init__(self, **kwargs):
                self.kwargs = kwargs

        @staticmethod
        def Bus(**kwargs):
            calls.append(kwargs)
            return types.SimpleNamespace(**kwargs)

    monkeypatch.setattr(can_module, "python_can", _FakeCan)

    wires = can_module.SocketCANInterface("can0")
    wires.open(500000, fd=True, data_bitrate=2000000)

    assert calls == [
        {
            "interface": "socketcan",
            "channel": "can0",
            "bitrate": 500000,
            "fd": True,
            "data_bitrate": 2000000,
        }
    ]


def test_socketcan_open_omits_fd_kwargs_in_classic_mode(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "linux")
    monkeypatch.setattr(can_module, "_configure_socketcan_interface", lambda *a, **kw: None)

    calls: list[dict[str, object]] = []

    class _FakeCan:
        class Message:  # pragma: no cover - structural stub
            def __init__(self, **kwargs):
                self.kwargs = kwargs

        @staticmethod
        def Bus(**kwargs):
            calls.append(kwargs)
            return types.SimpleNamespace(**kwargs)

    monkeypatch.setattr(can_module, "python_can", _FakeCan)

    wires = can_module.SocketCANInterface("can0")
    wires.open(500000)

    assert calls == [
        {
            "interface": "socketcan",
            "channel": "can0",
            "bitrate": 500000,
        }
    ]


def test_socketcan_open_wraps_backend_error_with_context(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "linux")
    monkeypatch.setattr(can_module, "_configure_socketcan_interface", lambda *a, **kw: None)

    class _FakeCan:
        class Message:  # pragma: no cover - structural stub
            def __init__(self, **kwargs):
                self.kwargs = kwargs

        @staticmethod
        def Bus(**_kwargs):
            raise RuntimeError("No suitable bit timings found.")

    monkeypatch.setattr(can_module, "python_can", _FakeCan)

    wires = can_module.SocketCANInterface("can0")
    with pytest.raises(
        can_module.UnsupportedConfiguration,
        match=(
            r"Failed to open CAN interface "
            r"\(backend=socketcan, channel=can0, mode=classic, listen_only=False, bitrate=666666, data_bitrate=-, termination=-\): "
            r"No suitable bit timings found\."
        ),
    ):
        wires.open(666666)


def test_socketcan_open_sets_passive_state_for_listen_only(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "linux")
    monkeypatch.setattr(can_module, "_configure_socketcan_interface", lambda *a, **kw: None)

    class _FakeBus:
        def __init__(self, **kwargs):
            self.kwargs = kwargs
            self.state = None

    class _FakeCan:
        class Message:  # pragma: no cover - structural stub
            def __init__(self, **kwargs):
                self.kwargs = kwargs

        class BusState:
            PASSIVE = "PASSIVE"

        @staticmethod
        def Bus(**kwargs):
            return _FakeBus(**kwargs)

    monkeypatch.setattr(can_module, "python_can", _FakeCan)

    wires = can_module.SocketCANInterface("can0")
    wires.open(500000, listen_only=True)
    assert wires.listen_only is True
    assert getattr(wires.bus, "state", None) == _FakeCan.BusState.PASSIVE


def test_gs_usb_open_applies_termination_when_supported(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "darwin")
    monkeypatch.setattr(can_module.importlib.util, "find_spec", lambda _name: object())
    monkeypatch.setattr(can_module, "_install_macos_gs_usb_start_shim", lambda: True)

    class _FakeUsbHandle:
        def __init__(self) -> None:
            self.calls: list[tuple[int, int, int, int, object]] = []

        def ctrl_transfer(self, request_type: int, req: int, value: int, index: int, payload):  # noqa: ANN001
            self.calls.append((request_type, req, value, index, payload))
            if request_type == 0xC1 and req == can_module.GS_USB_BREQ_GET_TERMINATION:
                return b"\x01\x00\x00\x00"
            return None

    class _FakeCan:
        class Message:  # pragma: no cover - structural stub
            def __init__(self, **kwargs):
                self.kwargs = kwargs

        @staticmethod
        def Bus(**kwargs):
            usb = _FakeUsbHandle()
            adapter = types.SimpleNamespace(
                device_capability=types.SimpleNamespace(feature=can_module.GS_USB_FEATURE_TERMINATION),
                gs_usb=usb,
            )
            bus = types.SimpleNamespace(**kwargs)
            bus.gs_usb = adapter
            return bus

    monkeypatch.setattr(can_module, "python_can", _FakeCan)

    wires = can_module.SocketCANInterface("gs_usb:0")
    wires.open(500000, termination=True)

    usb_calls = wires.bus.gs_usb.gs_usb.calls  # type: ignore[union-attr]
    assert usb_calls[0] == (
        0x41,
        can_module.GS_USB_BREQ_SET_TERMINATION,
        0,
        0,
        b"\x01\x00\x00\x00",
    )
    assert usb_calls[1][:4] == (0xC1, can_module.GS_USB_BREQ_GET_TERMINATION, 0, 0)


def test_gs_usb_open_rejects_termination_when_not_supported(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "darwin")
    monkeypatch.setattr(can_module.importlib.util, "find_spec", lambda _name: object())
    monkeypatch.setattr(can_module, "_install_macos_gs_usb_start_shim", lambda: True)

    class _FakeCan:
        class Message:  # pragma: no cover - structural stub
            def __init__(self, **kwargs):
                self.kwargs = kwargs

        @staticmethod
        def Bus(**kwargs):
            adapter = types.SimpleNamespace(
                device_capability=types.SimpleNamespace(feature=0),
                gs_usb=types.SimpleNamespace(ctrl_transfer=lambda *a, **k: None),
            )
            bus = types.SimpleNamespace(**kwargs)
            bus.gs_usb = adapter
            return bus

    monkeypatch.setattr(can_module, "python_can", _FakeCan)

    wires = can_module.SocketCANInterface("gs_usb:0")
    with pytest.raises(can_module.UnsupportedConfiguration, match="does not support termination control"):
        wires.open(500000, termination=True)


def test_gs_usb_open_skips_termination_off_when_not_supported(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(can_module.sys, "platform", "darwin")
    monkeypatch.setattr(can_module.importlib.util, "find_spec", lambda _name: object())
    monkeypatch.setattr(can_module, "_install_macos_gs_usb_start_shim", lambda: True)

    class _FakeCan:
        class Message:  # pragma: no cover - structural stub
            def __init__(self, **kwargs):
                self.kwargs = kwargs

        @staticmethod
        def Bus(**kwargs):
            adapter = types.SimpleNamespace(
                device_capability=types.SimpleNamespace(feature=0),
                gs_usb=types.SimpleNamespace(ctrl_transfer=lambda *a, **k: None),
            )
            bus = types.SimpleNamespace(**kwargs)
            bus.gs_usb = adapter
            return bus

    monkeypatch.setattr(can_module, "python_can", _FakeCan)

    wires = can_module.SocketCANInterface("gs_usb:0")
    wires.open(500000, termination=False)
