"""Tests for VBoxDispatch scancode generation and combo parsing.

Tests: _KEYS table validation, _MODIFIERS validation, _combo_cmds parsing.
How: Unit tests against VBoxDispatch class (no VirtualBox required).
Why: Scancode generation is critical for keyboard combos; must be exact.
"""

from __future__ import annotations

import string

import pytest

from mcp_vm_blackbox.vbox_dispatch import VBoxDispatch


class TestKeysTable:
    """Tests for VBoxDispatch._KEYS scancode table."""

    def test_keys_table_has_letters(self) -> None:
        """_KEYS contains all letters a-z."""
        for letter in string.ascii_lowercase:
            assert letter in VBoxDispatch._KEYS, f"Missing letter: {letter}"

    def test_keys_table_has_numbers(self) -> None:
        """_KEYS contains all numbers 0-9."""
        for num in string.digits:
            assert num in VBoxDispatch._KEYS, f"Missing number: {num}"

    def test_keys_table_has_function_keys(self) -> None:
        """_KEYS contains F1-F12."""
        for i in range(1, 13):
            key = f"f{i}"
            assert key in VBoxDispatch._KEYS, f"Missing function key: {key}"

    def test_keys_table_has_navigation_keys(self) -> None:
        """_KEYS contains navigation keys with E0 prefix."""
        nav_keys = ["up", "down", "left", "right", "home", "end", "pageup", "pagedown"]
        for key in nav_keys:
            assert key in VBoxDispatch._KEYS, f"Missing navigation key: {key}"
            press, release = VBoxDispatch._KEYS[key]
            assert press.startswith("e0"), f"Navigation key {key} should have E0 prefix"
            assert release.startswith("e0"), f"Navigation key {key} release should have E0 prefix"

    def test_keys_table_has_editing_keys(self) -> None:
        """_KEYS contains insert and delete."""
        for key in ["insert", "delete"]:
            assert key in VBoxDispatch._KEYS, f"Missing editing key: {key}"

    def test_keys_table_has_special_keys(self) -> None:
        """_KEYS contains enter, tab, escape, space, backspace."""
        for key in ["Return", "enter", "Tab", "tab", "Escape", "escape", "space", "BackSpace", "backspace"]:
            assert key in VBoxDispatch._KEYS, f"Missing special key: {key}"

    def test_scancodes_are_hex_pairs(self) -> None:
        """All scancodes are valid hex strings (press, release pair)."""
        for key, (press, release) in VBoxDispatch._KEYS.items():
            # Validate hex format
            assert all(c in string.hexdigits for c in press), f"Invalid hex in press for {key}: {press}"
            assert all(c in string.hexdigits for c in release), f"Invalid hex in release for {key}: {release}"
            # Release code should be press code with high bit set (or E0 prefix + high bit)
            if press.startswith("e0"):
                # Extended scancode: release = e0 + (rest of press with high bit)
                expected_release = "e0" + chr(ord(press[2:4][0]) + 0x40) + press[3:4] if len(press) > 3 else press
                # For simplicity, just check it starts with e0
                assert release.startswith("e0"), f"Extended key {key} release should start with e0"
            else:
                # Standard scancode: release = press + 0x80
                expected_release = format(int(press, 16) + 0x80, "02x")
                assert release.lower() == expected_release.lower(), (
                    f"Release for {key} should be {expected_release}, got {release}"
                )


class TestModifiersTable:
    """Tests for VBoxDispatch._MODIFIERS scancode table."""

    def test_modifiers_table_has_ctrl(self) -> None:
        """_MODIFIERS contains ctrl."""
        assert "ctrl" in VBoxDispatch._MODIFIERS
        press, release = VBoxDispatch._MODIFIERS["ctrl"]
        assert press == "1d"
        assert release == "9d"

    def test_modifiers_table_has_shift(self) -> None:
        """_MODIFIERS contains shift."""
        assert "shift" in VBoxDispatch._MODIFIERS
        press, release = VBoxDispatch._MODIFIERS["shift"]
        assert press == "2a"
        assert release == "aa"

    def test_modifiers_table_has_alt(self) -> None:
        """_MODIFIERS contains alt."""
        assert "alt" in VBoxDispatch._MODIFIERS
        press, release = VBoxDispatch._MODIFIERS["alt"]
        assert press == "38"
        assert release == "b8"

    def test_modifiers_table_has_win(self) -> None:
        """_MODIFIERS contains win (left Windows key)."""
        assert "win" in VBoxDispatch._MODIFIERS
        press, release = VBoxDispatch._MODIFIERS["win"]
        assert press == "e05b"
        assert release == "e0db"

    def test_modifiers_table_has_right_variants(self) -> None:
        """_MODIFIERS contains right_ctrl, right_shift, right_alt, right_win."""
        for mod in ["right_ctrl", "right_shift", "right_alt", "right_win"]:
            assert mod in VBoxDispatch._MODIFIERS, f"Missing modifier: {mod}"


class TestComboCmds:
    """Tests for VBoxDispatch._combo_cmds parsing."""

    def test_combo_cmds_ctrl_c(self) -> None:
        """ctrl+c generates correct scancode sequence."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        commands, parsed, scancodes = dispatch._combo_cmds("ctrl+c")

        assert parsed == "ctrl+c"
        # Sequence: press ctrl, press c, release c, release ctrl
        assert scancodes == ["1d", "2e", "ae", "9d"]
        assert len(commands) == 3  # 1 press ctrl, 1 press+release c, 1 release ctrl

    def test_combo_cmds_win_r(self) -> None:
        """win+r generates correct scancode sequence."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        commands, parsed, scancodes = dispatch._combo_cmds("win+r")

        assert parsed == "win+r"
        # Sequence: press win, press r, release r, release win
        assert scancodes == ["e05b", "13", "93", "e0db"]
        assert len(commands) == 3

    def test_combo_cmds_alt_f4(self) -> None:
        """alt+f4 generates correct scancode sequence."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        commands, parsed, scancodes = dispatch._combo_cmds("alt+f4")

        assert parsed == "alt+f4"
        # Sequence: press alt, press f4, release f4, release alt
        assert scancodes == ["38", "3e", "be", "b8"]
        assert len(commands) == 3

    def test_combo_cmds_shift_tab(self) -> None:
        """shift+tab generates correct scancode sequence."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        commands, parsed, scancodes = dispatch._combo_cmds("shift+tab")

        assert parsed == "shift+tab"
        # Sequence: press shift, press tab, release tab, release shift
        assert scancodes == ["2a", "0f", "8f", "aa"]
        assert len(commands) == 3

    def test_combo_cmds_ctrl_shift_escape(self) -> None:
        """ctrl+shift+escape generates correct scancode sequence (multi-modifier)."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        commands, parsed, scancodes = dispatch._combo_cmds("ctrl+shift+escape")

        assert parsed == "ctrl+shift+escape"
        # Sequence: press ctrl, press shift, press+release escape, release shift, release ctrl
        assert scancodes == ["1d", "2a", "01", "81", "aa", "9d"]
        assert len(commands) == 5  # 2 press mods, 1 press+release key, 2 release mods

    def test_combo_cmds_case_insensitive(self) -> None:
        """Combo parsing is case-insensitive."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        _, parsed1, _ = dispatch._combo_cmds("Ctrl+C")
        _, parsed2, _ = dispatch._combo_cmds("CTRL+C")
        _, parsed3, _ = dispatch._combo_cmds("ctrl+c")

        assert parsed1 == "ctrl+c"
        assert parsed2 == "ctrl+c"
        assert parsed3 == "ctrl+c"

    def test_combo_cmds_whitespace_tolerant(self) -> None:
        """Combo parsing tolerates whitespace."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        _, parsed, _ = dispatch._combo_cmds("ctrl + c")

        assert parsed == "ctrl+c"

    def test_combo_cmds_invalid_modifier_raises(self) -> None:
        """Invalid modifier raises ValueError."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        with pytest.raises(ValueError, match="Unknown modifier"):
            dispatch._combo_cmds("super+c")

    def test_combo_cmds_invalid_key_raises(self) -> None:
        """Invalid key raises ValueError."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        with pytest.raises(ValueError, match="Unknown key"):
            dispatch._combo_cmds("ctrl+f13")

    def test_combo_cmds_missing_modifier_raises(self) -> None:
        """Combo without modifier raises ValueError."""
        dispatch = VBoxDispatch("test-vm", _make_local_cfg())
        with pytest.raises(ValueError, match="at least one modifier"):
            dispatch._combo_cmds("c")


def _make_local_cfg():
    """Create a minimal local TargetConfig for testing."""
    from mcp_vm_blackbox.targets import TargetConfig

    return TargetConfig(ssh_host="", winrm_port=5985)
