import importlib.machinery
import importlib.util
from pathlib import Path
from unittest.mock import Mock

import pytest

from fenrirscreenreader.inputDriver.x11Driver import X
from fenrirscreenreader.inputDriver.x11Driver import XK
from fenrirscreenreader.inputDriver.x11Driver import driver as X11Driver


def load_fenrir_entrypoint():
    fenrir_path = Path(__file__).parents[2] / "src" / "fenrir"
    loader = importlib.machinery.SourceFileLoader(
        "fenrir_entrypoint_x11", str(fenrir_path)
    )
    spec = importlib.util.spec_from_loader(loader.name, loader)
    module = importlib.util.module_from_spec(spec)
    loader.exec_module(module)
    return module


@pytest.mark.unit
def test_x11_mode_runs_in_foreground():
    fenrir = load_fenrir_entrypoint()
    args = fenrir.create_argument_parser().parse_args(["-x"])

    assert fenrir.should_run_foreground(args) is True


@pytest.mark.unit
def test_removed_emulated_pty_flag_is_rejected():
    fenrir = load_fenrir_entrypoint()
    with pytest.raises(SystemExit):
        fenrir.create_argument_parser().parse_args(["-e"])


@pytest.mark.unit
def test_removed_emulated_evdev_flag_is_rejected():
    fenrir = load_fenrir_entrypoint()
    with pytest.raises(SystemExit):
        fenrir.create_argument_parser().parse_args(["-E"])


@pytest.mark.unit
def test_x11_cli_accepts_window_id():
    fenrir = load_fenrir_entrypoint()
    args = fenrir.create_argument_parser().parse_args(
        ["-x", "--x11-window-id", "0x123"]
    )

    assert args.x11_window_id == "0x123"


@pytest.mark.unit
def test_x11_window_id_requires_x11_mode():
    fenrir = load_fenrir_entrypoint()
    args = fenrir.create_argument_parser().parse_args(
        ["--x11-window-id", "0x123"]
    )

    is_valid, error = fenrir.validate_arguments(args)

    assert is_valid is False
    assert "--x11-window-id requires --x11" == error


@pytest.mark.unit
def test_x11_key_name_mapping_for_keypad_and_capslock():
    x11 = X11Driver()

    assert x11.keysym_name_to_key_name("KP_0") == "KEY_KP0"
    assert x11.keysym_name_to_key_name("KP_Insert") == "KEY_KP0"
    assert x11.keysym_name_to_key_name("Caps_Lock") == "KEY_CAPSLOCK"
    assert x11.keysym_name_to_key_name("Super_L") == "KEY_LEFTMETA"
    assert x11.keysym_name_to_key_name("Multi_key") == "KEY_COMPOSE"
    assert x11.keysym_name_to_key_name("Next") == "KEY_PAGEDOWN"
    assert x11.keysym_name_to_key_name("Prior") == "KEY_PAGEUP"
    assert x11.keysym_name_to_key_name("a") == "KEY_A"
    assert x11.keysym_name_to_key_name("F10") == "KEY_F10"
    assert x11.keysym_name_to_key_name("plus") == "KEY_EQUAL"
    assert x11.keysym_name_to_key_name("+") == "KEY_EQUAL"
    assert x11.keysym_name_to_key_name("underscore") == "KEY_MINUS"
    assert x11.keysym_name_to_key_name("_") == "KEY_MINUS"


@pytest.mark.unit
@pytest.mark.parametrize(
    ("keysym_name", "key_name"),
    [
        ("BackSpace", "KEY_BACKSPACE"),
        ("Tab", "KEY_TAB"),
        ("Return", "KEY_ENTER"),
        ("Escape", "KEY_ESC"),
        ("Delete", "KEY_DELETE"),
    ],
)
def test_x11_special_keysyms_use_symbolic_names(keysym_name, key_name):
    x11 = X11Driver()
    resolved_name = x11.keysym_to_name(XK.string_to_keysym(keysym_name))

    assert resolved_name == keysym_name
    assert x11.keysym_name_to_key_name(resolved_name) == key_name


@pytest.mark.unit
def test_x11_keycode_mapping_prefers_keypad_aliases():
    x11 = X11Driver()
    x11.display = Mock()
    keysyms = {
        0: XK.string_to_keysym("Up"),
        1: XK.string_to_keysym("KP_8"),
        2: 0,
        3: 0,
        4: 0,
        5: 0,
        6: 0,
        7: 0,
    }
    x11.display.keycode_to_keysym.side_effect = lambda keycode, column: keysyms[column]

    assert x11.keycode_to_key_name(80) == "KEY_KP8"


@pytest.mark.unit
def test_x11_keycode_mapping_preserves_plain_insert():
    x11 = X11Driver()
    x11.display = Mock()
    keysyms = {
        0: XK.string_to_keysym("Insert"),
        1: 0,
        2: 0,
        3: 0,
        4: 0,
        5: 0,
        6: 0,
        7: 0,
    }
    x11.display.keycode_to_keysym.side_effect = lambda keycode, column: keysyms[column]

    assert x11.keycode_to_key_name(118) == "KEY_INSERT"


@pytest.mark.unit
def test_x11_keycode_mapping_detects_keypad_insert():
    x11 = X11Driver()
    x11.display = Mock()
    keysyms = {
        0: XK.string_to_keysym("Insert"),
        1: XK.string_to_keysym("KP_Insert"),
        2: 0,
        3: 0,
        4: 0,
        5: 0,
        6: 0,
        7: 0,
    }
    x11.display.keycode_to_keysym.side_effect = lambda keycode, column: keysyms[column]

    assert x11.keycode_to_key_name(90) == "KEY_KP0"


@pytest.mark.unit
def test_x11_key_name_to_keysym_names_includes_numlock_aliases():
    x11 = X11Driver()

    assert x11.key_name_to_keysym_names("KEY_KP0") == ["KP_0", "KP_Insert"]
    assert "KP_Delete" in x11.key_name_to_keysym_names("KEY_KPDOT")
    assert "Caps_Lock" in x11.key_name_to_keysym_names("KEY_CAPSLOCK")
    assert "Super_L" in x11.key_name_to_keysym_names("KEY_META")
    assert "Meta_R" in x11.key_name_to_keysym_names("KEY_META")
    assert x11.key_name_to_keysym_names("KEY_COMPOSE") == ["Multi_key"]
    assert "Next" in x11.key_name_to_keysym_names("KEY_PAGEDOWN")
    assert "Prior" in x11.key_name_to_keysym_names("KEY_PAGEUP")
    assert x11.key_name_to_keysym_names("KEY_EQUAL") == ["equal", "plus"]
    assert x11.key_name_to_keysym_names("KEY_MINUS") == [
        "minus",
        "underscore",
    ]


@pytest.mark.unit
def test_x11_should_emit_unbound_mapped_keys_for_speech_interrupt():
    x11 = X11Driver()
    x11.interesting_keys = set()

    assert x11.should_emit_key("KEY_ESC") is True
    assert x11.should_emit_key("KEY_ENTER") is True


@pytest.mark.unit
def test_x11_build_passive_grabs_for_fenrir_keys_and_shortcuts():
    x11 = X11Driver()
    input_manager = Mock(convert_event_name=lambda key: key)
    x11.env = {
        "input": {
            "fenrir_key": ["KEY_KP0", "KEY_META"],
            "script_key": [],
        },
        "rawBindings": {
            "fenrir_combo": [1, ["KEY_FENRIR", "KEY_KP8"]],
            "bare_keypad": [1, ["KEY_KP5"]],
            "ctrl_keypad": [1, ["KEY_CTRL", "KEY_KP2"]],
            "meta_combo": [1, ["KEY_FENRIR", "KEY_BACKSPACE"]],
        },
        "runtime": {"InputManager": input_manager},
    }

    grabs = x11.build_passive_grabs()

    assert ("KEY_KP0", 0, True) in grabs
    assert ("KEY_META", 0, True) in grabs
    assert ("KEY_NUMLOCK", 0, True) in grabs
    assert ("KEY_KP5", 0, False) in grabs
    assert ("KEY_KP2", X.ControlMask, False) in grabs
    assert ("KEY_BACKSPACE", X.Mod4Mask, True) in grabs


@pytest.mark.unit
def test_x11_optional_modifier_masks_can_exclude_numlock():
    x11 = X11Driver()
    x11.num_lock_mask = X.Mod2Mask

    masks = x11.optional_modifier_masks(0, include_num_lock=False)

    assert X.Mod2Mask not in masks
    assert X.Mod2Mask | X.LockMask not in masks


@pytest.mark.unit
def test_x11_get_led_state_reads_cached_lock_modifiers_without_x_round_trip():
    x11 = X11Driver()
    x11.num_lock_mask = X.Mod2Mask
    x11.modifier_state = X.Mod2Mask | X.LockMask
    x11.root = Mock()

    assert x11.get_led_state(0) is True
    assert x11.get_led_state(1) is True
    assert x11.get_led_state(2) is False
    x11.root.query_pointer.assert_not_called()


@pytest.mark.unit
def test_x11_update_modifier_state_tracks_event_masks_and_lock_toggles():
    x11 = X11Driver()
    x11.num_lock_mask = X.Mod2Mask

    x11.update_modifier_state_from_event(
        {
            "event_name": "KEY_KP8",
            "event_state": 1,
            "event_raw_state": X.Mod2Mask,
        }
    )
    assert x11.get_led_state(0) is True

    x11.update_modifier_state_from_event(
        {
            "event_name": "KEY_NUMLOCK",
            "event_state": 1,
            "event_raw_state": X.Mod2Mask,
        }
    )
    assert x11.get_led_state(0) is False

    x11.update_modifier_state_from_event(
        {
            "event_name": "KEY_CAPSLOCK",
            "event_state": 1,
            "event_raw_state": 0,
        }
    )
    assert x11.get_led_state(1) is True


@pytest.mark.unit
def test_x11_parse_window_id_accepts_decimal_and_hex():
    x11 = X11Driver()

    assert x11.parse_window_id("123") == 123
    assert x11.parse_window_id("0x123") == 0x123
    assert x11.parse_window_id("") is None


@pytest.mark.unit
def test_x11_write_event_buffer_replays_grabbed_key_press():
    x11 = X11Driver()
    x11._initialized = True
    x11.display = Mock()
    x11.env = {
        "input": {
            "event_buffer": [
                {
                    "event_name": "KEY_LEFTMETA",
                    "event_state": 1,
                    "event_type": X.KeyPress,
                    "event_x_time": 1234,
                }
            ]
        }
    }

    x11.write_event_buffer()

    x11.display.allow_events.assert_called_once_with(X.ReplayKeyboard, 1234)
    assert x11.env["input"]["event_buffer"] == []


@pytest.mark.unit
def test_x11_write_event_buffer_does_not_replay_key_release():
    x11 = X11Driver()
    x11._initialized = True
    x11.display = Mock()
    x11.env = {
        "input": {
            "event_buffer": [
                {
                    "event_name": "KEY_LEFTMETA",
                    "event_state": 0,
                    "event_type": X.KeyRelease,
                    "event_x_time": 1235,
                }
            ]
        }
    }

    x11.write_event_buffer()

    x11.display.allow_events.assert_not_called()
    assert x11.env["input"]["event_buffer"] == []


@pytest.mark.unit
def test_x11_map_event_keeps_x_event_time_for_replay():
    x11 = X11Driver()
    x11.keycode_to_key_name = Mock(return_value="KEY_LEFTMETA")
    event = Mock(type=X.KeyPress, detail=133, state=X.Mod4Mask, time=5678)

    input_event = x11.map_event(event)

    assert input_event["event_x_time"] == 5678


@pytest.mark.unit
def test_x11_find_num_lock_mask_uses_modifier_mapping():
    x11 = X11Driver()
    x11.display = Mock()
    x11.display.keysym_to_keycode.return_value = 77
    x11.display.get_modifier_mapping.return_value = [
        [],
        [],
        [],
        [],
        [77],
        [],
        [],
        [],
    ]

    assert x11.find_num_lock_mask() == X.Mod2Mask


@pytest.mark.unit
def test_x11_handle_key_event_keeps_event_buffer_for_input_manager():
    x11 = X11Driver()
    x11.active = True
    x11.interesting_keys = {"KEY_KP0"}
    x11.env = {
        "input": {
            "curr_input": [],
            "event_buffer": [],
        },
        "runtime": {
            "InputManager": Mock(convert_event_name=lambda key: key),
            "DebugManager": Mock(),
        },
    }
    event_queue = Mock()
    event = Mock(type=X.KeyPress, detail=90, state=0)
    x11.keycode_to_key_name = Mock(return_value="KEY_KP0")

    x11.handle_x_event(event, event_queue)

    assert x11.env["input"]["event_buffer"][0]["event_name"] == "KEY_KP0"
    event_queue.put.assert_called_once()
