import os

import pytest

from matterlab_serial_device import SerialDevice
from matterlab_valves.valco_selection_valve import ValcoSelectionValve


PORT = "/dev/ttyUSB0" if os.name == "posix" else "COM1"


@pytest.fixture
def mock_serial(mocker):
    mock_serial_cls = mocker.patch("matterlab_serial_device.transport.serial.Serial", autospec=True)
    serial_instances = []
    primary_instance = mocker.MagicMock()
    primary_instance.com_port = PORT
    primary_instance._factory = mock_serial_cls
    primary_instance._instances = serial_instances

    def _make_serial_instance(*args, **kwargs):
        if not serial_instances:
            instance = primary_instance
        else:
            instance = mocker.MagicMock()
            instance.com_port = kwargs.get("port", PORT)
        instance.is_open = True
        instance.open.side_effect = lambda: setattr(instance, "is_open", True)
        instance.close.side_effect = lambda: setattr(instance, "is_open", False)
        serial_instances.append(instance)
        return instance

    mock_serial_cls.return_value = primary_instance
    mock_serial_cls.side_effect = _make_serial_instance
    yield primary_instance


@pytest.fixture(autouse=True)
def clear_shared_devices():
    SerialDevice._shared_devices.clear()
    yield
    SerialDevice._shared_devices.clear()


@pytest.fixture
def valve_fixture(mock_serial, mocker):
    query_spy = mocker.patch("matterlab_serial_device.serial_device.SerialDevice.query")
    query_spy.return_value = "3"
    return ValcoSelectionValve(
        com_port=PORT,
        num_port=3,
        ports={"A": 1, "B": 2, "C": 3},
    )


def test_initialization_defaults(mock_serial, valve_fixture):
    assert valve_fixture.com_port == PORT
    assert valve_fixture.direction_CW is True
    assert valve_fixture.ports == {"A": 1, "B": 2, "C": 3}
    assert valve_fixture.device._connection is mock_serial

    mock_serial._factory.assert_called_once_with(
        port=PORT,
        baudrate=9600,
        parity=valve_fixture.parity,
        bytesize=valve_fixture.bytesize,
        timeout=1.0,
        stopbits=valve_fixture.stopbits,
    )
    assert mock_serial.close.call_count >= 1


def test_initialization_calls_connect_when_requested(mock_serial, mocker):
    connect_spy = mocker.patch.object(ValcoSelectionValve, "connect", autospec=True)

    ValcoSelectionValve(com_port=PORT, num_port=3, ports={"A": 1}, connect_hardware=True)

    connect_spy.assert_called_once()


def test_initialization_skips_connect_when_deferred(mock_serial, mocker):
    connect_spy = mocker.patch.object(ValcoSelectionValve, "connect", autospec=True)

    ValcoSelectionValve(com_port=PORT, num_port=3, ports={"A": 1}, connect_hardware=False)

    connect_spy.assert_not_called()


def test_connect_initializes_valve(mocker, valve_fixture):
    set_response_mode_spy = mocker.patch.object(valve_fixture, "_set_response_mode")
    set_device_mode_spy = mocker.patch.object(valve_fixture, "_set_device_mode")
    home_spy = mocker.patch.object(valve_fixture, "_home_valve")
    num_port_spy = mocker.patch.object(type(valve_fixture), "num_port", new_callable=mocker.PropertyMock, return_value=3)

    valve_fixture.connect()

    set_response_mode_spy.assert_called_once_with()
    set_device_mode_spy.assert_called_once_with()
    home_spy.assert_called_once_with()
    assert num_port_spy.call_count >= 2


def test_query_valve_uses_expected_wire_format(mocker, valve_fixture):
    query_spy = mocker.patch.object(valve_fixture, "query", return_value="response")

    result = valve_fixture._query_valve("CP")

    assert result == "response"
    query_spy.assert_called_once_with(
        write_command="CP\r",
        read_until=b"\r",
        remove_from_start=2,
        remove_from_end=1,
    )


def test_execute_valve_writes_expected_command(mocker, valve_fixture):
    write_spy = mocker.patch.object(valve_fixture, "write")

    valve_fixture._execute_valve("HM")

    write_spy.assert_called_once_with(command="HM\r")


def test_set_response_mode_executes_ifm1(mocker, valve_fixture):
    execute_spy = mocker.patch.object(valve_fixture, "_execute_valve")

    valve_fixture._set_response_mode()

    execute_spy.assert_called_once_with("IFM1")


def test_query_device_mode_uses_am_command(mocker, valve_fixture):
    query_spy = mocker.patch.object(valve_fixture, "_query_valve", return_value="3")

    assert valve_fixture._query_device_mode() == "3"

    query_spy.assert_called_once_with(command="AM")


def test_set_device_mode_retries_until_success(mocker, valve_fixture):
    execute_spy = mocker.patch.object(valve_fixture, "_execute_valve")
    query_device_mode_spy = mocker.patch.object(valve_fixture, "_query_device_mode", side_effect=["2", "3"])

    valve_fixture._set_device_mode(max_trial=3)

    assert execute_spy.call_count == 2
    assert query_device_mode_spy.call_count == 2


def test_set_device_mode_raises_after_max_trials(mocker, valve_fixture):
    mocker.patch.object(valve_fixture, "_execute_valve")
    mocker.patch.object(valve_fixture, "_query_device_mode", return_value="2")

    with pytest.raises(IOError, match="Failed to set device mode to selection valve"):
        valve_fixture._set_device_mode(max_trial=3)


def test_num_port_getter_queries_np(mocker, valve_fixture):
    query_spy = mocker.patch.object(valve_fixture, "_query_valve", return_value="6")

    assert valve_fixture.num_port == 6

    query_spy.assert_called_once_with(command="NP")


def test_num_port_setter_sends_np_command(mocker, valve_fixture):
    execute_spy = mocker.patch.object(valve_fixture, "_execute_valve")

    type(valve_fixture).num_port.fset(valve_fixture, 10)

    execute_spy.assert_called_once_with("NP10")


def test_home_valve_executes_hm(mocker, valve_fixture):
    execute_spy = mocker.patch.object(valve_fixture, "_execute_valve")

    valve_fixture._home_valve()

    execute_spy.assert_called_once_with("HM")


@pytest.mark.parametrize(
    ("port", "direction_cw", "expected_command"),
    [
        (2, None, "GO2"),
        (2, True, "CW2"),
        (3, False, "CC3"),
    ],
)
def test_set_current_port_executes_expected_command(mocker, valve_fixture, port, direction_cw, expected_command):
    execute_spy = mocker.patch.object(valve_fixture, "_execute_valve")
    get_port_spy = mocker.patch.object(valve_fixture, "get_current_port", return_value=port)
    info_spy = mocker.patch.object(valve_fixture.logger, "info")
    mocker.patch.object(type(valve_fixture), "num_port", new_callable=mocker.PropertyMock, return_value=10)

    result = valve_fixture.set_current_port(port=port, direction_CW=direction_cw)

    assert result is None
    execute_spy.assert_called_once_with(expected_command)
    get_port_spy.assert_called_once_with()
    info_spy.assert_any_call(
        "Requesting Valco valve move on %s to port %s (direction_CW=%s)",
        PORT,
        port,
        direction_cw,
    )
    info_spy.assert_any_call("Valve on %s has been moved to %s position.", PORT, port)


def test_set_current_port_raises_for_non_integer(valve_fixture):
    with pytest.raises(AssertionError, match="port number must be int!"):
        valve_fixture.set_current_port("invalid")


def test_set_current_port_raises_for_out_of_range_port(mocker, valve_fixture):
    mocker.patch.object(type(valve_fixture), "num_port", new_callable=mocker.PropertyMock, return_value=3)

    with pytest.raises(AssertionError, match="Valve port number out of range!"):
        valve_fixture.set_current_port(10)


def test_set_current_port_retries_then_raises(mocker, valve_fixture):
    execute_spy = mocker.patch.object(valve_fixture, "_execute_valve")
    get_port_spy = mocker.patch.object(valve_fixture, "get_current_port", return_value=3)
    sleep_spy = mocker.patch("matterlab_valves.valco_selection_valve.time.sleep")
    mocker.patch.object(type(valve_fixture), "num_port", new_callable=mocker.PropertyMock, return_value=10)

    with pytest.raises(IOError, match=f"Move port of valve on {PORT} exceed max try."):
        valve_fixture.set_current_port(port=2, max_try=3)

    assert execute_spy.call_count == 3
    assert get_port_spy.call_count == 3
    assert sleep_spy.call_count == 3


def test_get_current_port_returns_integer(mocker, valve_fixture):
    query_spy = mocker.patch.object(valve_fixture, "_query_valve", return_value="2")

    assert valve_fixture.get_current_port() == 2

    query_spy.assert_called_once_with(command="CP")


def test_get_current_port_retries_then_raises(mocker, valve_fixture):
    query_spy = mocker.patch.object(valve_fixture, "_query_valve", side_effect=ValueError)
    sleep_spy = mocker.patch("matterlab_valves.valco_selection_valve.time.sleep")

    with pytest.raises(IOError, match=f"Query port of valve on {PORT} exceed max try."):
        valve_fixture.get_current_port(max_try=3)

    assert query_spy.call_count == 3
    assert sleep_spy.call_count == 3


def test_port_property_reads_from_device(mocker, valve_fixture):
    get_port_spy = mocker.patch.object(valve_fixture, "get_current_port", return_value=2)

    assert valve_fixture.port == 2

    get_port_spy.assert_called_once_with()


def test_port_property_sets_active_port(mocker, valve_fixture):
    set_port_spy = mocker.patch.object(valve_fixture, "set_current_port")

    valve_fixture.port = 3

    set_port_spy.assert_called_once_with(port=3)
