import importlib.resources
from typing import cast
from unittest.mock import patch

import pytest
import serial

from agiltron_selfalign.module import (
    AgiltronSelfAlign,
    AgiltronSelfAlignError,
    check_port,
)


class FakeSerial:
    def __init__(self, *_args, **_kwargs):
        self.is_open = True
        self.written: list[bytes] = []
        self._response = b"\x01\x35\xff\xff"
        self.raise_on_write = False
        self.raise_on_read = False
        self.raise_on_close = False

    def write(self, data: bytes) -> int:
        if self.raise_on_write:
            raise serial.SerialException("write failed")
        self.written.append(data)
        return len(data)

    def read(self, size: int = 1) -> bytes:
        if self.raise_on_read:
            raise serial.SerialException("read failed")
        return self._response[:size]

    def close(self) -> None:
        if self.raise_on_close:
            raise serial.SerialException("close failed")
        self.is_open = False


def test_check_port_valid() -> None:
    assert check_port(1, 16) is True
    assert check_port(16, 16) is True


def test_check_port_invalid_type() -> None:
    with pytest.raises(TypeError):
        check_port("1", 16)  # type: ignore[arg-type]


def test_check_port_out_of_range() -> None:
    with pytest.raises(ValueError):
        check_port(0, 16)
    with pytest.raises(ValueError):
        check_port(17, 16)


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_set_fiber_port_writes_command_once(_mock_serial) -> None:
    switch = AgiltronSelfAlign("COM8", number_of_ports=16)
    instrument = cast(FakeSerial, switch.instrument)

    switch.set_fiber_port(14)
    switch.set_fiber_port(14)

    assert switch.fiber_port == 14
    assert instrument.written == [b"\x01\x35\x00\x0d"]


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_home_writes_home_command(_mock_serial) -> None:
    switch = AgiltronSelfAlign("COM8", number_of_ports=16)
    instrument = cast(FakeSerial, switch.instrument)

    switch.home()

    assert instrument.written == [b"\x01\x30\x00\x00"]


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_raises_on_invalid_switch_response(_mock_serial) -> None:
    switch = AgiltronSelfAlign("COM8", number_of_ports=16)
    instrument = cast(FakeSerial, switch.instrument)
    instrument._response = b"\x00\x00\x00\x00"

    with pytest.raises(AgiltronSelfAlignError):
        switch.set_fiber_port(1)


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_raises_on_partial_switch_response(_mock_serial) -> None:
    switch = AgiltronSelfAlign("COM8", number_of_ports=16)
    instrument = cast(FakeSerial, switch.instrument)
    instrument._response = b"\x01\x35"

    with pytest.raises(AgiltronSelfAlignError):
        switch.set_fiber_port(1)


@patch(
    "agiltron_selfalign.module.serial.Serial",
    side_effect=serial.SerialException("open failed"),
)
def test_raises_on_serial_open_error(_mock_serial) -> None:
    with pytest.raises(AgiltronSelfAlignError):
        AgiltronSelfAlign("COM8", number_of_ports=16)


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_raises_on_serial_write_error(_mock_serial) -> None:
    switch = AgiltronSelfAlign("COM8", number_of_ports=16)
    instrument = cast(FakeSerial, switch.instrument)
    instrument.raise_on_write = True

    with pytest.raises(AgiltronSelfAlignError):
        switch.set_fiber_port(1)


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_context_manager_closes_serial(_mock_serial) -> None:
    with AgiltronSelfAlign("COM8", number_of_ports=16) as switch:
        instrument = cast(FakeSerial, switch.instrument)
        assert instrument.is_open

    assert instrument.is_open is False


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_invalid_number_of_ports_raises(_mock_serial) -> None:
    with pytest.raises(ValueError):
        AgiltronSelfAlign("COM8", number_of_ports=0)


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_custom_serial_settings_are_forwarded(mock_serial) -> None:
    AgiltronSelfAlign(
        "COM8",
        timeout=1.5,
        number_of_ports=16,
        baudrate=19200,
        write_timeout=0.75,
        inter_byte_timeout=0.2,
    )

    mock_serial.assert_called_once_with(
        port="COM8",
        baudrate=19200,
        bytesize=serial.EIGHTBITS,
        parity=serial.PARITY_NONE,
        timeout=1.5,
        write_timeout=0.75,
        inter_byte_timeout=0.2,
    )


@patch("agiltron_selfalign.module.serial.Serial", return_value=FakeSerial())
def test_default_write_timeout_matches_timeout(mock_serial) -> None:
    AgiltronSelfAlign("COM8", timeout=1.25)

    kwargs = mock_serial.call_args.kwargs
    assert kwargs["write_timeout"] == 1.25


def test_package_contains_py_typed_marker() -> None:
    marker = importlib.resources.files("agiltron_selfalign").joinpath("py.typed")
    assert marker.is_file()
