from contextlib import nullcontext
from logging import getLogger
from types import SimpleNamespace

import pytest

from matterlab_serial_device import SerialDevice
from matterlab_rotavaps.ika_rotavap import IKARV10


@pytest.fixture
def serial_device_mocks(mocker):
    def fake_serial_init(
        self,
        com_port,
        baudrate=9600,
        bytesize=8,
        parity="none",
        stopbits=1,
        timeout=None,
        encoding="utf-8",
        connect_hardware=True,
        share_port=False,
        logger=None,
        **kwargs,
    ):
        self.com_port = com_port
        self.baudrate = baudrate
        self.bytesize = bytesize
        self.parity = parity
        self.stopbits = stopbits
        self.timeout = timeout
        self.encoding = encoding
        self.connect_hardware = connect_hardware
        self.share_port = share_port
        self.logger = logger if logger is not None else getLogger("test.serial")
        self.device = SimpleNamespace(is_open=False)

    serial_init = mocker.patch.object(SerialDevice, "__init__", autospec=True, side_effect=fake_serial_init)
    ensure_device = mocker.patch.object(SerialDevice, "_ensure_device", autospec=True)
    lock = mocker.patch.object(SerialDevice, "_transport_lock", autospec=True, side_effect=lambda self: nullcontext())
    open_comm = mocker.patch.object(SerialDevice, "open_device_comm", autospec=True)
    close_comm = mocker.patch.object(SerialDevice, "close_device_comm", autospec=True)
    query = mocker.patch.object(SerialDevice, "query", autospec=True, return_value="OK 123")

    return {
        "serial_init": serial_init,
        "ensure_device": ensure_device,
        "lock": lock,
        "open_comm": open_comm,
        "close_comm": close_comm,
        "query": query,
    }


@pytest.fixture
def rotavap(serial_device_mocks):
    return IKARV10("COM9", max_rpm=250, connect_hardware=False)


def test_init_resets_when_connecting_hardware(serial_device_mocks, mocker) -> None:
    reset = mocker.patch.object(IKARV10, "reset", autospec=True)

    device = IKARV10("COM4", connect_hardware=True)

    assert device.com_port == "COM4"
    assert device.connect_hardware is True
    reset.assert_called_once_with(device)


def test_init_passes_connect_hardware_to_serial_base(serial_device_mocks) -> None:
    device = IKARV10("COM7", connect_hardware=False)

    assert device.connect_hardware is False


def test_query_rotavap_writes_expected_command(rotavap, serial_device_mocks) -> None:
    response = rotavap._query_rotavap("IN_PV_4")

    assert response == "123"
    serial_device_mocks["query"].assert_called_once_with(
        rotavap,
        write_command="IN_PV_4\r\n",
        read_delay=0.5,
    )
    serial_device_mocks["open_comm"].assert_called_once_with(rotavap)
    serial_device_mocks["close_comm"].assert_called_once_with(rotavap)


def test_name_and_software_properties(rotavap, mocker) -> None:
    responses = {"IN_NAME": "RV10", "IN_SOFTWARE": "1.2.3"}
    query = mocker.patch.object(rotavap, "_query_rotavap", side_effect=lambda command: responses[command])

    assert rotavap.name == "RV10"
    assert rotavap.software_version == "1.2.3"
    assert query.call_count == 2


def test_rpm_and_target_rpm_properties(rotavap, mocker) -> None:
    responses = {"IN_PV_4": "85", "IN_SP_4": "120"}
    query = mocker.patch.object(rotavap, "_query_rotavap", side_effect=lambda command: responses[command])

    assert rotavap.rpm == 85
    assert rotavap.target_rpm == 120
    assert query.call_count == 2


def test_rpm_setter_starts_rotation(rotavap, mocker) -> None:
    query = mocker.patch.object(rotavap, "_query_rotavap", return_value="OK")

    rotavap.rpm = 150

    assert rotavap._stir_switch is True
    assert query.call_args_list == [
        mocker.call("OUT_SP_4 150"),
        mocker.call("START_4"),
    ]


def test_rpm_setter_stops_rotation_at_zero(rotavap, mocker) -> None:
    query = mocker.patch.object(rotavap, "_query_rotavap", return_value="OK")

    rotavap.rpm = 0

    assert rotavap._stir_switch is False
    assert query.call_args_list == [
        mocker.call("OUT_SP_4 0"),
        mocker.call("STOP_4"),
    ]


def test_rpm_setter_validates_value(rotavap) -> None:
    with pytest.raises(TypeError):
        rotavap.rpm = 1.5
    with pytest.raises(ValueError):
        rotavap.rpm = -1
    with pytest.raises(ValueError):
        rotavap.rpm = 300


def test_interval_and_timer_setters(rotavap, mocker) -> None:
    query = mocker.patch.object(rotavap, "_query_rotavap", return_value="OK")

    rotavap.interval = 12
    rotavap.timer = 34

    assert rotavap.interval == 12
    assert rotavap.timer == 34
    assert query.call_args_list == [
        mocker.call("OUT_SP_60 12"),
        mocker.call("OUT_SP_61 34"),
    ]


def test_interval_and_timer_type_validation(rotavap) -> None:
    with pytest.raises(TypeError):
        rotavap.interval = 1.2
    with pytest.raises(TypeError):
        rotavap.timer = "5"


def test_move_commands_and_reset(rotavap, mocker) -> None:
    query = mocker.patch.object(rotavap, "_query_rotavap", return_value="OK")

    rotavap.reset()
    rotavap.move_up()
    rotavap.move_down()
    rotavap.move_stop()

    assert query.call_args_list == [
        mocker.call("RESET"),
        mocker.call("OUT_SP_62 1"),
        mocker.call("START_62"),
        mocker.call("STOP_62"),
    ]


def test_standby_sets_zero_speed(rotavap, mocker) -> None:
    rpm_setter = mocker.patch.object(type(rotavap), "rpm", new_callable=mocker.PropertyMock)

    rotavap.standby()

    rpm_setter.assert_called_once_with(0)
