from types import SimpleNamespace

import pytest

from src.utils import ino


@pytest.fixture(autouse=True)
def _reset(tmp_path, monkeypatch):
    monkeypatch.setattr(ino, '_method', None)
    monkeypatch.setattr(ino, '_serial', None)
    monkeypatch.setattr(ino, 'INPUT_METHOD_FILE', tmp_path / '.input-method')
    for var in (
        ino.INPUT_METHOD_ENV, ino.BRIDGE_ADDRESS_ENV,
        ino.BRIDGE_TOKEN_FILE_ENV, ino.ARDUINO_PORT_ENV,
    ):
        monkeypatch.delenv(var, raising=False)
    # Humanisierung in Tests deterministisch abschalten.
    monkeypatch.setenv(ino.humanize.HUMANIZE_ENV, '0')
    yield


def _configureBridge(monkeypatch, tmp_path):
    monkeypatch.setenv(ino.BRIDGE_ADDRESS_ENV, '100.80.170.78:43877')
    token = tmp_path / 'token'
    token.write_text('a' * 40)
    monkeypatch.setenv(ino.BRIDGE_TOKEN_FILE_ENV, str(token))


class FakeSerial:
    is_open = True

    def __init__(self, response=b'OK PILOTNG\n'):
        self.commands = []
        self._response = response

    def write(self, payload):
        self.commands.append(payload)

    def flush(self):
        pass

    def readline(self):
        return self._response

    def reset_input_buffer(self):
        pass

    def close(self):
        self.is_open = False


# --- Methoden-Auswahl -------------------------------------------------------
def test_default_method_is_usb_arduino():
    assert ino.getInputMethod() == ino.INPUT_METHOD_USB


def test_set_input_method_persists_to_file(monkeypatch):
    ino.setInputMethod('arduino')
    monkeypatch.setattr(ino, '_method', None)
    assert ino.getInputMethod() == ino.INPUT_METHOD_USB


def test_env_overrides_stored_method(monkeypatch):
    ino.setInputMethod('usb')
    monkeypatch.setenv(ino.INPUT_METHOD_ENV, 'mac')
    assert ino.getInputMethod() == ino.INPUT_METHOD_MAC_BT


def test_unknown_method_is_rejected():
    with pytest.raises(ValueError):
        ino.setInputMethod('bogus')


# --- USB-Transport ----------------------------------------------------------
def test_should_use_configured_arduino_port(mocker, monkeypatch):
    monkeypatch.setenv('TIBIAPILOT_ARDUINO_PORT', 'COM7')
    # Der eingestellte Port zaehlt nur, wenn es ihn wirklich gibt.
    mocker.patch.object(ino.list_ports, 'comports', return_value=[
        SimpleNamespace(device='COM7', vid=0x2341, description='Arduino',
                        manufacturer='Arduino', product='Leonardo'),
    ])

    assert ino.getArduinoPort() == 'COM7'


def test_should_detect_32u4_port(mocker):
    mocker.patch.object(ino.list_ports, 'comports', return_value=[
        SimpleNamespace(
            device='COM9', vid=0x1B4F, description='SparkFun Pro Micro',
            manufacturer='SparkFun', product='Pro Micro',
        ),
    ])
    assert ino.getArduinoPort() == 'COM9'


def test_usb_command_requires_firmware_ack(mocker):
    ino.setInputMethod('usb')
    fake = FakeSerial()
    mocker.patch.object(ino, 'getArduinoPort', return_value='COM9')
    mocker.patch.object(ino.serial, 'Serial', return_value=fake)
    mocker.patch.object(ino, 'sleep')
    assert ino.sendCommandArduino('press,218') is True
    assert fake.commands == [b'health\n', b'press,218\n']


def test_missing_arduino_is_a_hard_error(mocker):
    ino.setInputMethod('usb')
    mocker.patch.object(ino, 'getArduinoPort', return_value=None)
    with pytest.raises(ino.ArduinoUnavailableError):
        ino.sendCommandArduino('press,218')


def test_require_arduino_usb_raises_when_missing(mocker):
    ino.setInputMethod('usb')
    mocker.patch.object(ino, 'getArduinoPort', return_value=None)
    with pytest.raises(ino.ArduinoUnavailableError):
        ino.requireArduino()


# --- Router: absolute Maus -> relative HID-Schritte -------------------------
def test_absolute_mouse_target_uses_relative_hid_steps(mocker):
    mocker.patch.object(ino, '_cursorPosition', side_effect=[
        (0, 0), (127, 127), (150, 150),
    ])
    send = mocker.patch.object(ino, '_send', return_value=True)
    assert ino.sendCommandArduino('moveTo,150,150') is True
    # Weite Strecke gedrosselt (Windows verstaerkt schnelle HID-Moves),
    # nahe am Ziel nur noch kleine Schritte.
    assert send.call_args_list == [
        mocker.call('move,60,60'),
        mocker.call('move,10,10'),
    ]


def test_move_target_gets_human_jitter(mocker, monkeypatch):
    monkeypatch.setenv(ino.humanize.HUMANIZE_ENV, '1')
    monkeypatch.setattr(ino.humanize.random, 'triangular', lambda a, b: 2)
    monkeypatch.setattr(ino.humanize, 'sleep', lambda _: None)
    mocker.patch.object(ino, '_cursorPosition', side_effect=[
        (0, 0), (127, 127), (152, 152),
    ])
    send = mocker.patch.object(ino, '_send', return_value=True)
    assert ino.sendCommandArduino('moveTo,150,150') is True
    # Jitter verschiebt das Ziel auf 152; die Schrittgroessen bleiben
    # gedrosselt bzw. klein.
    assert send.call_args_list == [
        mocker.call('move,60,60'),
        mocker.call('move,10,10'),
    ]


# --- Mac-Bluetooth-Transport ------------------------------------------------
def test_mac_send_translates_writehex_to_write(mocker, monkeypatch, tmp_path):
    ino.setInputMethod('mac')
    _configureBridge(monkeypatch, tmp_path)
    request = mocker.patch.object(ino, '_bridgeRequest', return_value='OK')
    payload = 'exana mort'.encode('utf-8').hex()
    assert ino.sendCommandArduino(f'writeHex,{payload}') is True
    assert request.call_args[0][0] == 'write,exana mort'


def test_mac_send_translates_scroll(mocker, monkeypatch, tmp_path):
    ino.setInputMethod('mac')
    _configureBridge(monkeypatch, tmp_path)
    request = mocker.patch.object(ino, '_bridgeRequest', return_value='OK')
    assert ino.sendCommandArduino('scroll,-3') is True
    assert request.call_args[0][0] == 'scroll,0,0,-3'


def test_mac_send_passes_key_commands_through(mocker, monkeypatch, tmp_path):
    ino.setInputMethod('mac')
    _configureBridge(monkeypatch, tmp_path)
    request = mocker.patch.object(ino, '_bridgeRequest', return_value='OK')
    assert ino.sendCommandArduino('keyDown,128') is True
    assert request.call_args[0][0] == 'keyDown,128'


def test_mac_send_raises_when_disconnected(mocker, monkeypatch, tmp_path):
    ino.setInputMethod('mac')
    _configureBridge(monkeypatch, tmp_path)
    mocker.patch.object(ino, '_bridgeRequest', return_value='ERR disconnected')
    with pytest.raises(ino.ArduinoUnavailableError):
        ino.sendCommandArduino('press,32')


def test_mac_send_raises_when_unreachable(mocker, monkeypatch, tmp_path):
    ino.setInputMethod('mac')
    _configureBridge(monkeypatch, tmp_path)
    mocker.patch.object(ino, '_bridgeRequest', return_value=None)
    with pytest.raises(ino.ArduinoUnavailableError):
        ino.sendCommandArduino('press,32')


def test_mac_requires_bridge_configuration():
    ino.setInputMethod('mac')
    with pytest.raises(ino.ArduinoUnavailableError):
        ino.sendCommandArduino('press,32')


def test_mac_health_reports_connected(mocker, monkeypatch, tmp_path):
    ino.setInputMethod('mac')
    _configureBridge(monkeypatch, tmp_path)
    mocker.patch.object(ino, '_bridgeRequest', return_value='OK connected')
    assert ino.isArduinoAvailable() is True


def test_mac_health_reports_disconnected(mocker, monkeypatch, tmp_path):
    ino.setInputMethod('mac')
    _configureBridge(monkeypatch, tmp_path)
    mocker.patch.object(ino, '_bridgeRequest', return_value='OK disconnected')
    assert ino.isArduinoAvailable() is False
