//! Logical key model: decoding / matching (port of `src/ui/keys.ts`).
//!
//! Understands the legacy xterm/VT escape grammar and the Kitty keyboard
//! protocol (CSI-u, flag-2 press/repeat/release, flag-4 base-layout
//! alternates), `modifyOtherKeys`, and bracketed-paste guards.
//!
//! Public surface (mirrors `keys.ts`):
//! - [`matches_key`]              — does raw input equal a given key id?
//! - [`parse_key`]               — decode raw input back into a key id
//! - [`Key`]                     — builder that yields key-id strings
//! - [`normalize_key_id`]        — canonicalize a key-id string
//! - [`set_kitty_protocol_active`] / [`is_kitty_protocol_active`]
//! - [`is_key_release`] / [`is_key_repeat`]
//! - [`KeyEventType`]
//!
//! Per W-8 these stay decode-agnostic: a `KeyId` is just a canonical string
//! (`ctrl+shift+alt+<key>` order); raw bytes come from crossterm upstream.

use std::sync::atomic::{AtomicBool, Ordering};

/// A logical key identifier such as `"ctrl+c"`, `"escape"`, `"shift+enter"`.
///
/// TS uses a template-literal union; in Rust we keep the same string-shaped
/// contract as a newtype so the canonical grammar is the single source of truth.
pub type KeyId = String;

/// The kind of key event the Kitty protocol reports under flag 2.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyEventType {
    /// Key was pressed (default / flag-2 code 1).
    Press,
    /// Key auto-repeated (flag-2 code 2).
    Repeat,
    /// Key was released (flag-2 code 3).
    Release,
}

// =============================================================================
// Shared Kitty-Protocol Flag
// =============================================================================

// The TS module keeps `_kittyProtocolActive` as module-global mutable state; an
// `AtomicBool` is the faithful, thread-safe Rust analogue. The terminal layer
// flips this on once it has confirmed the protocol is supported.
static KITTY_PROTOCOL_ACTIVE: AtomicBool = AtomicBool::new(false);

/// Record whether the Kitty keyboard protocol is in effect.
pub fn set_kitty_protocol_active(active: bool) {
    KITTY_PROTOCOL_ACTIVE.store(active, Ordering::SeqCst);
}

/// Report the current value of the shared Kitty-protocol flag.
pub fn is_kitty_protocol_active() -> bool {
    KITTY_PROTOCOL_ACTIVE.load(Ordering::SeqCst)
}

// =============================================================================
// Constants — codepoints / modifier bits / synthetic ladders
// =============================================================================

// Modifier bitmask values follow the full Kitty keyboard protocol CSI-u
// modifier byte: one bit each to shift, alt, ctrl, super, hyper, meta,
// capsLock and numLock. We only compose the bottom three but mirror the whole
// published byte.
const MOD_SHIFT: i64 = 1;
const MOD_ALT: i64 = 2;
const MOD_CTRL: i64 = 4;
const MOD_CAPS_LOCK: i64 = 64;
const MOD_NUM_LOCK: i64 = 128;
const LOCK_MASK: i64 = MOD_CAPS_LOCK | MOD_NUM_LOCK;

// Real Unicode/control codepoints for named keys.
const CP_ESCAPE: i64 = 27;
const CP_TAB: i64 = 9;
const CP_ENTER: i64 = 13;
const CP_SPACE: i64 = 32;
const CP_BACKSPACE: i64 = 127;
const CP_KP_ENTER: i64 = 57414; // keypad Enter under Kitty.

// Synthetic negative-integer ladders for arrow + functional keys; these never
// collide with real Unicode codepoints.
const CP_ARROW_UP: i64 = -101;
const CP_ARROW_DOWN: i64 = -102;
const CP_ARROW_RIGHT: i64 = -103;
const CP_ARROW_LEFT: i64 = -104;

const CP_DELETE: i64 = -110;
const CP_INSERT: i64 = -111;
const CP_PAGE_UP: i64 = -112;
const CP_PAGE_DOWN: i64 = -113;
const CP_HOME: i64 = -114;
const CP_END: i64 = -115;

/// The 31 recognized symbol/punctuation keys (mirrors TS `SYMBOL_KEYS`).
const SYMBOL_KEYS: &[char] = &[
    '`', '-', '=', '[', ']', '\\', ';', '\'', ',', '.', '/', '!', '@', '#', '$', '%', '^', '&',
    '*', '(', ')', '_', '+', '|', '~', '{', '}', ':', '<', '>', '?',
];

fn is_symbol_key(c: char) -> bool {
    SYMBOL_KEYS.contains(&c)
}

// =============================================================================
// Legacy escape-sequence tables
// =============================================================================

/// Per-key legacy press sequences (mirrors TS `LEGACY_KEY_SEQUENCES`).
fn legacy_key_sequences(name: &str) -> &'static [&'static str] {
    match name {
        "arrowDown" => &["\x1b[B", "\x1bOB"],
        "arrowLeft" => &["\x1b[D", "\x1bOD"],
        "arrowRight" => &["\x1b[C", "\x1bOC"],
        "arrowUp" => &["\x1b[A", "\x1bOA"],
        "clear" => &["\x1b[E", "\x1bOE"],
        "delete" => &["\x1b[3~"],
        "end" => &["\x1b[F", "\x1bOF", "\x1b[4~", "\x1b[8~"],
        "f1" => &["\x1bOP", "\x1b[11~", "\x1b[[A"],
        "f10" => &["\x1b[21~"],
        "f11" => &["\x1b[23~"],
        "f12" => &["\x1b[24~"],
        "f2" => &["\x1bOQ", "\x1b[12~", "\x1b[[B"],
        "f3" => &["\x1bOR", "\x1b[13~", "\x1b[[C"],
        "f4" => &["\x1bOS", "\x1b[14~", "\x1b[[D"],
        "f5" => &["\x1b[15~", "\x1b[[E"],
        "f6" => &["\x1b[17~"],
        "f7" => &["\x1b[18~"],
        "f8" => &["\x1b[19~"],
        "f9" => &["\x1b[20~"],
        "home" => &["\x1b[H", "\x1bOH", "\x1b[1~", "\x1b[7~"],
        "insert" => &["\x1b[2~"],
        "pageDown" => &["\x1b[6~", "\x1b[[6~"],
        "pageUp" => &["\x1b[5~", "\x1b[[5~"],
        _ => &[],
    }
}

/// Per-key legacy shift ($) sequences (mirrors TS `LEGACY_SHIFT_SEQUENCES`).
fn legacy_shift_sequences(name: &str) -> &'static [&'static str] {
    match name {
        "arrowDown" => &["\x1b[b"],
        "arrowLeft" => &["\x1b[d"],
        "arrowRight" => &["\x1b[c"],
        "arrowUp" => &["\x1b[a"],
        "clear" => &["\x1b[e"],
        "delete" => &["\x1b[3$"],
        "end" => &["\x1b[8$"],
        "home" => &["\x1b[7$"],
        "insert" => &["\x1b[2$"],
        "pageDown" => &["\x1b[6$"],
        "pageUp" => &["\x1b[5$"],
        _ => &[],
    }
}

/// Per-key legacy ctrl (^) sequences (mirrors TS `LEGACY_CTRL_SEQUENCES`).
fn legacy_ctrl_sequences(name: &str) -> &'static [&'static str] {
    match name {
        "arrowDown" => &["\x1bOb"],
        "arrowLeft" => &["\x1bOd"],
        "arrowRight" => &["\x1bOc"],
        "arrowUp" => &["\x1bOa"],
        "clear" => &["\x1bOe"],
        "delete" => &["\x1b[3^"],
        "end" => &["\x1b[8^"],
        "home" => &["\x1b[7^"],
        "insert" => &["\x1b[2^"],
        "pageDown" => &["\x1b[6^"],
        "pageUp" => &["\x1b[5^"],
        _ => &[],
    }
}

/// Direct legacy-sequence → key-id table (mirrors TS `LEGACY_SEQUENCE_KEY_IDS`).
fn legacy_sequence_key_id(data: &str) -> Option<&'static str> {
    let id = match data {
        "\x1bOA" => "up",
        "\x1bOB" => "down",
        "\x1bOC" => "right",
        "\x1bOD" => "left",
        "\x1bOH" => "home",
        "\x1bOF" => "end",
        "\x1b[E" => "clear",
        "\x1bOE" => "clear",
        "\x1bOe" => "ctrl+clear",
        "\x1b[e" => "shift+clear",
        "\x1b[2~" => "insert",
        "\x1b[2$" => "shift+insert",
        "\x1b[2^" => "ctrl+insert",
        "\x1b[3$" => "shift+delete",
        "\x1b[3^" => "ctrl+delete",
        "\x1b[[5~" => "pageUp",
        "\x1b[[6~" => "pageDown",
        "\x1b[a" => "shift+up",
        "\x1b[b" => "shift+down",
        "\x1b[c" => "shift+right",
        "\x1b[d" => "shift+left",
        "\x1bOa" => "ctrl+up",
        "\x1bOb" => "ctrl+down",
        "\x1bOc" => "ctrl+right",
        "\x1bOd" => "ctrl+left",
        "\x1b[5$" => "shift+pageUp",
        "\x1b[6$" => "shift+pageDown",
        "\x1b[7$" => "shift+home",
        "\x1b[8$" => "shift+end",
        "\x1b[5^" => "ctrl+pageUp",
        "\x1b[6^" => "ctrl+pageDown",
        "\x1b[7^" => "ctrl+home",
        "\x1b[8^" => "ctrl+end",
        "\x1bOP" => "f1",
        "\x1bOQ" => "f2",
        "\x1bOR" => "f3",
        "\x1bOS" => "f4",
        "\x1b[11~" => "f1",
        "\x1b[12~" => "f2",
        "\x1b[13~" => "f3",
        "\x1b[14~" => "f4",
        "\x1b[[A" => "f1",
        "\x1b[[B" => "f2",
        "\x1b[[C" => "f3",
        "\x1b[[D" => "f4",
        "\x1b[[E" => "f5",
        "\x1b[15~" => "f5",
        "\x1b[17~" => "f6",
        "\x1b[18~" => "f7",
        "\x1b[19~" => "f8",
        "\x1b[20~" => "f9",
        "\x1b[21~" => "f10",
        "\x1b[23~" => "f11",
        "\x1b[24~" => "f12",
        "\x1bb" => "alt+left",
        "\x1bf" => "alt+right",
        "\x1bp" => "alt+up",
        "\x1bn" => "alt+down",
        _ => return None,
    };
    Some(id)
}

fn matches_legacy_sequence(data: &str, sequences: &[&str]) -> bool {
    sequences.contains(&data)
}

fn matches_legacy_modifier_sequence(data: &str, key: &str, modifier: i64) -> bool {
    if modifier == MOD_SHIFT {
        return matches_legacy_sequence(data, legacy_shift_sequences(key));
    }
    if modifier == MOD_CTRL {
        return matches_legacy_sequence(data, legacy_ctrl_sequences(key));
    }
    false
}

// =============================================================================
// Strongly-Typed Key Identifiers — the `Key` builder
// =============================================================================

/// Builder that produces canonical key-id strings.
///
/// Named keys are associated constants (`Key::ESCAPE`, `Key::ENTER`, …) and
/// punctuation has helpers; the modifier methods compose `ctrl+`, `shift+`,
/// `alt+` prefixes. Mirrors the TS `Key` const object.
pub struct Key;

impl Key {
    // --- Named special keys (mirror SPECIAL_KEY_LITERAL_MAP) ---
    pub const ESCAPE: &'static str = "escape";
    pub const ESC: &'static str = "esc";
    pub const ENTER: &'static str = "enter";
    pub const RETURN: &'static str = "return";
    pub const TAB: &'static str = "tab";
    pub const SPACE: &'static str = "space";
    pub const BACKSPACE: &'static str = "backspace";
    pub const DELETE: &'static str = "delete";
    pub const INSERT: &'static str = "insert";
    pub const CLEAR: &'static str = "clear";
    pub const HOME: &'static str = "home";
    pub const END: &'static str = "end";
    pub const PAGE_UP: &'static str = "pageUp";
    pub const PAGE_DOWN: &'static str = "pageDown";
    pub const UP: &'static str = "up";
    pub const DOWN: &'static str = "down";
    pub const LEFT: &'static str = "left";
    pub const RIGHT: &'static str = "right";
    pub const F1: &'static str = "f1";
    pub const F2: &'static str = "f2";
    pub const F3: &'static str = "f3";
    pub const F4: &'static str = "f4";
    pub const F5: &'static str = "f5";
    pub const F6: &'static str = "f6";
    pub const F7: &'static str = "f7";
    pub const F8: &'static str = "f8";
    pub const F9: &'static str = "f9";
    pub const F10: &'static str = "f10";
    pub const F11: &'static str = "f11";
    pub const F12: &'static str = "f12";

    // --- Punctuation helpers (mirror SYMBOL_KEY_LITERAL_MAP) ---
    pub const BACKTICK: &'static str = "`";
    pub const HYPHEN: &'static str = "-";
    pub const EQUALS: &'static str = "=";
    pub const LEFT_BRACKET: &'static str = "[";
    pub const RIGHT_BRACKET: &'static str = "]";
    pub const BACKSLASH: &'static str = "\\";
    pub const SEMICOLON: &'static str = ";";
    pub const QUOTE: &'static str = "'";
    pub const COMMA: &'static str = ",";
    pub const PERIOD: &'static str = ".";
    pub const SLASH: &'static str = "/";
    pub const EXCLAMATION: &'static str = "!";
    pub const AT: &'static str = "@";
    pub const HASH: &'static str = "#";
    pub const DOLLAR: &'static str = "$";
    pub const PERCENT: &'static str = "%";
    pub const CARET: &'static str = "^";
    pub const AMPERSAND: &'static str = "&";
    pub const ASTERISK: &'static str = "*";
    pub const LEFT_PAREN: &'static str = "(";
    pub const RIGHT_PAREN: &'static str = ")";
    pub const UNDERSCORE: &'static str = "_";
    pub const PLUS: &'static str = "+";
    pub const PIPE: &'static str = "|";
    pub const TILDE: &'static str = "~";
    pub const LEFT_BRACE: &'static str = "{";
    pub const RIGHT_BRACE: &'static str = "}";
    pub const COLON: &'static str = ":";
    pub const LESS_THAN: &'static str = "<";
    pub const GREATER_THAN: &'static str = ">";
    pub const QUESTION: &'static str = "?";

    /// `ctrl+<key>`.
    pub fn ctrl(key: &str) -> KeyId {
        format!("ctrl+{key}")
    }
    /// `shift+<key>`.
    pub fn shift(key: &str) -> KeyId {
        format!("shift+{key}")
    }
    /// `alt+<key>`.
    pub fn alt(key: &str) -> KeyId {
        format!("alt+{key}")
    }
    /// `ctrl+shift+<key>`.
    pub fn ctrl_shift(key: &str) -> KeyId {
        format!("ctrl+shift+{key}")
    }
    /// `shift+ctrl+<key>`.
    pub fn shift_ctrl(key: &str) -> KeyId {
        format!("shift+ctrl+{key}")
    }
    /// `ctrl+alt+<key>`.
    pub fn ctrl_alt(key: &str) -> KeyId {
        format!("ctrl+alt+{key}")
    }
    /// `alt+ctrl+<key>`.
    pub fn alt_ctrl(key: &str) -> KeyId {
        format!("alt+ctrl+{key}")
    }
    /// `shift+alt+<key>`.
    pub fn shift_alt(key: &str) -> KeyId {
        format!("shift+alt+{key}")
    }
    /// `alt+shift+<key>`.
    pub fn alt_shift(key: &str) -> KeyId {
        format!("alt+shift+{key}")
    }
    /// `ctrl+shift+alt+<key>`.
    pub fn ctrl_shift_alt(key: &str) -> KeyId {
        format!("ctrl+shift+alt+{key}")
    }
}

// =============================================================================
// Key-id normalization / validation
// =============================================================================

fn special_key_alias(key: &str) -> &str {
    match key {
        "esc" => "escape",
        "return" => "enter",
        "pageup" => "pageUp",
        "pagedown" => "pageDown",
        other => other,
    }
}

const SPECIAL_KEY_SET: &[&str] = &[
    "escape",
    "enter",
    "tab",
    "space",
    "backspace",
    "delete",
    "insert",
    "clear",
    "home",
    "end",
    "pageUp",
    "pageDown",
    "up",
    "down",
    "left",
    "right",
    "f1",
    "f2",
    "f3",
    "f4",
    "f5",
    "f6",
    "f7",
    "f8",
    "f9",
    "f10",
    "f11",
    "f12",
];

/// Canonicalize a key id: lowercase, split on `+`, dedupe/reorder modifiers to
/// `ctrl+shift+alt+<key>` order, alias `esc→escape`, `return→enter`,
/// `pageup→pageUp`, `pagedown→pageDown`. Mirrors TS `normalizeKeyId`.
pub fn normalize_key_id(key_id: &str) -> String {
    let parts: Vec<&str> = key_id
        .split('+')
        .map(|p| p.trim())
        .filter(|p| !p.is_empty())
        .collect();
    if parts.is_empty() {
        return String::new();
    }
    let key = parts[parts.len() - 1].to_lowercase();
    let normalized_key = special_key_alias(&key).to_string();

    let modifier_set: Vec<String> = parts[..parts.len() - 1]
        .iter()
        .map(|p| p.to_lowercase())
        .collect();

    let mut modifiers: Vec<&str> = Vec::new();
    if modifier_set.iter().any(|m| m == "ctrl") {
        modifiers.push("ctrl");
    }
    if modifier_set.iter().any(|m| m == "shift") {
        modifiers.push("shift");
    }
    if modifier_set.iter().any(|m| m == "alt") {
        modifiers.push("alt");
    }

    if modifiers.is_empty() {
        normalized_key
    } else {
        format!("{}+{normalized_key}", modifiers.join("+"))
    }
}

/// Validates and normalizes raw key-id strings (≈ TS `KeyValidator`).
pub struct KeyValidator;

impl KeyValidator {
    pub fn new() -> Self {
        Self
    }
    /// Normalize a key id (delegates to [`normalize_key_id`]).
    pub fn normalize(&self, key_id: &str) -> String {
        normalize_key_id(key_id)
    }
    /// Whether the key id names a legal key.
    pub fn is_valid(&self, key_id: &str) -> bool {
        let normalized = normalize_key_id(key_id);
        if normalized.is_empty() {
            return false;
        }
        let parts: Vec<&str> = normalized.split('+').collect();
        let key = parts[parts.len() - 1];
        if key.chars().count() == 1 {
            let c = key.chars().next().unwrap();
            return c.is_ascii_lowercase() || is_symbol_key(c);
        }
        SPECIAL_KEY_SET.contains(&key)
    }
}

impl Default for KeyValidator {
    fn default() -> Self {
        Self::new()
    }
}

/// Decoded modifier/key triple — mirror of TS `DecodedKeyId`.
struct DecodedKeyId {
    key: String,
    ctrl: bool,
    shift: bool,
    alt: bool,
}

fn parse_key_id(key_id: &str) -> Option<DecodedKeyId> {
    let normalized = normalize_key_id(key_id);
    if normalized.is_empty() || !KeyValidator.is_valid(&normalized) {
        return None;
    }
    let parts: Vec<&str> = normalized.split('+').collect();
    let key = parts[parts.len() - 1];
    if key.is_empty() {
        return None;
    }
    Some(DecodedKeyId {
        key: key.to_string(),
        ctrl: parts.contains(&"ctrl"),
        shift: parts.contains(&"shift"),
        alt: parts.contains(&"alt"),
    })
}

// =============================================================================
// Event-type predicates (Kitty flag 2)
// =============================================================================

/// Whether the raw input represents a key release (Kitty flag 2). Guards
/// against bracketed-paste payloads being misread as release events.
pub fn is_key_release(data: &str) -> bool {
    // Pasted text must never be mistaken for a release event; the terminal
    // layer wraps paste payloads in \x1b[200~, so bail the moment we see it.
    if data.contains("\x1b[200~") {
        return false;
    }
    // Under flag 2 a release shows up as ":3" before the final byte.
    data.contains(":3u")
        || data.contains(":3~")
        || data.contains(":3A")
        || data.contains(":3B")
        || data.contains(":3C")
        || data.contains(":3D")
        || data.contains(":3H")
        || data.contains(":3F")
}

/// Whether the raw input represents an auto-repeated key (Kitty flag 2).
/// Same bracketed-paste guard as [`is_key_release`].
pub fn is_key_repeat(data: &str) -> bool {
    if data.contains("\x1b[200~") {
        return false;
    }
    data.contains(":2u")
        || data.contains(":2~")
        || data.contains(":2A")
        || data.contains(":2B")
        || data.contains(":2C")
        || data.contains(":2D")
        || data.contains(":2H")
        || data.contains(":2F")
}

fn parse_event_type(event_type_str: Option<&str>) -> KeyEventType {
    match event_type_str {
        None => KeyEventType::Press,
        // An empty string fails to parse and falls through to `Press`, the same
        // as the explicit empty guard the TS uses.
        Some(s) => match s.parse::<i64>() {
            Ok(2) => KeyEventType::Repeat,
            Ok(3) => KeyEventType::Release,
            _ => KeyEventType::Press,
        },
    }
}

// =============================================================================
// Decoding Kitty-Protocol Sequences
// =============================================================================

struct ParsedKittySequence {
    codepoint: i64,
    shifted_key: Option<i64>,
    base_layout_key: Option<i64>,
    modifier: i64,
}

// Recognizes a non-negative decimal run starting at `from`, returning the run's
// text and the index just past it. Indices are byte offsets; all relevant input
// here is ASCII so byte and char offsets coincide.
fn scan_digits(bytes: &[u8], from: usize) -> (String, usize) {
    let mut cursor = from;
    while cursor < bytes.len() {
        let ch = bytes[cursor];
        if !(0x30..=0x39).contains(&ch) {
            break;
        }
        cursor += 1;
    }
    let text = String::from_utf8_lossy(&bytes[from..cursor]).into_owned();
    (text, cursor)
}

struct CsiUFields {
    codepoint: String,
    shifted: Option<String>,
    base: Option<String>,
    mod_field: Option<String>,
    event: Option<String>,
}

// Attempts to peel one "<separator><digits>" trailer off `bytes` at `cursor`.
fn take_trailer(
    bytes: &[u8],
    cursor: usize,
    separator: u8,
    require_digit: bool,
) -> (Option<String>, usize) {
    if cursor >= bytes.len() || bytes[cursor] != separator {
        return (None, cursor);
    }
    let after_sep = cursor + 1;
    let (digits, next) = scan_digits(bytes, after_sep);
    if require_digit && digits.is_empty() {
        // Separator present but no digit: trailer simply does not apply.
        return (None, cursor);
    }
    (Some(digits), next)
}

// Parses the body of a CSI-u sequence (between "\x1b[" and the closing "u").
fn decode_csi_u_body(body: &str) -> Option<CsiUFields> {
    let bytes = body.as_bytes();
    let (codepoint, mut cursor) = scan_digits(bytes, 0);
    if codepoint.is_empty() {
        return None;
    }

    // The shifted-key slot allows an empty digit run (e.g. "\x1b[99:u"), so we
    // only require the leading colon, not a following digit.
    let mut shifted: Option<String> = None;
    if cursor < bytes.len() && bytes[cursor] == b':' {
        let after_colon = cursor + 1;
        let (digits, next) = scan_digits(bytes, after_colon);
        shifted = Some(digits);
        cursor = next;
    }

    let (base, next) = take_trailer(bytes, cursor, b':', true);
    cursor = next;

    let (mod_field, next) = take_trailer(bytes, cursor, b';', true);
    cursor = next;

    let (event, next) = take_trailer(bytes, cursor, b':', true);
    cursor = next;

    // Any leftover characters before the closing "u" make this an invalid body.
    if cursor != bytes.len() {
        return None;
    }

    Some(CsiUFields {
        codepoint,
        shifted,
        base,
        mod_field,
        event,
    })
}

// Converts a captured modifier-field string into the zero-based modifier mask
// (Kitty reports 1-based; absent means 1).
fn modifier_mask_from_field(field: Option<&str>) -> i64 {
    let reported = match field {
        None => 1,
        Some(s) => s.parse::<i64>().unwrap_or(0),
    };
    reported - 1
}

fn nav_letter_codepoint(letter: char) -> Option<i64> {
    match letter {
        'A' => Some(CP_ARROW_UP),
        'B' => Some(CP_ARROW_DOWN),
        'C' => Some(CP_ARROW_RIGHT),
        'D' => Some(CP_ARROW_LEFT),
        'H' => Some(CP_HOME),
        'F' => Some(CP_END),
        _ => None,
    }
}

fn tilde_number_codepoint(num: i64) -> Option<i64> {
    match num {
        2 => Some(CP_INSERT),
        3 => Some(CP_DELETE),
        5 => Some(CP_PAGE_UP),
        6 => Some(CP_PAGE_DOWN),
        7 => Some(CP_HOME),
        8 => Some(CP_END),
        _ => None,
    }
}

// Decodes "\x1b[1;<mod>[:<event>]<L>" where <L> is A/B/C/D (arrows) or H/F.
fn decode_nav_letter_sequence(data: &str) -> Option<ParsedKittySequence> {
    // Shape: ESC [ 1 ; <digits> (: <digits>)? <ABCDHF>
    let rest = data.strip_prefix("\x1b[1;")?;
    let bytes = rest.as_bytes();
    let (mod_digits, cursor) = scan_digits(bytes, 0);
    if mod_digits.is_empty() {
        return None;
    }
    let mut cursor = cursor;
    let mut event: Option<String> = None;
    if cursor < bytes.len() && bytes[cursor] == b':' {
        let (ev, next) = scan_digits(bytes, cursor + 1);
        if ev.is_empty() {
            return None;
        }
        event = Some(ev);
        cursor = next;
    }
    // Exactly one trailing letter must remain.
    if cursor + 1 != bytes.len() {
        return None;
    }
    let letter = bytes[cursor] as char;
    let codepoint = nav_letter_codepoint(letter)?;
    let _ = parse_event_type(event.as_deref());
    Some(ParsedKittySequence {
        codepoint,
        shifted_key: None,
        base_layout_key: None,
        modifier: modifier_mask_from_field(Some(&mod_digits)),
    })
}

// Decodes "\x1b[<num>[;<mod>][:<event>]~" editing/navigation keys.
fn decode_tilde_sequence(data: &str) -> Option<ParsedKittySequence> {
    let rest = data.strip_prefix("\x1b[")?;
    let rest = rest.strip_suffix('~')?;
    let bytes = rest.as_bytes();
    let (num_str, mut cursor) = scan_digits(bytes, 0);
    if num_str.is_empty() {
        return None;
    }

    let mut mod_field: Option<String> = None;
    if cursor < bytes.len() && bytes[cursor] == b';' {
        let (digits, next) = scan_digits(bytes, cursor + 1);
        if digits.is_empty() {
            return None;
        }
        mod_field = Some(digits);
        cursor = next;
    }

    let mut event: Option<String> = None;
    if cursor < bytes.len() && bytes[cursor] == b':' {
        let (digits, next) = scan_digits(bytes, cursor + 1);
        if digits.is_empty() {
            return None;
        }
        event = Some(digits);
        cursor = next;
    }

    if cursor != bytes.len() {
        return None;
    }

    let num: i64 = num_str.parse().ok()?;
    let codepoint = tilde_number_codepoint(num)?;
    let _ = parse_event_type(event.as_deref());
    Some(ParsedKittySequence {
        codepoint,
        shifted_key: None,
        base_layout_key: None,
        modifier: modifier_mask_from_field(mod_field.as_deref()),
    })
}

// Decodes the CSI-u family ("\x1b[...u").
fn decode_csi_u_sequence(data: &str) -> Option<ParsedKittySequence> {
    if !data.starts_with("\x1b[") || !data.ends_with('u') {
        return None;
    }
    // Slice off the leading "\x1b[" (2 bytes) and trailing "u" (1 byte).
    let body = &data[2..data.len() - 1];
    let fields = decode_csi_u_body(body)?;

    let codepoint: i64 = fields.codepoint.parse().ok()?;
    let shifted_key = match &fields.shifted {
        Some(s) if !s.is_empty() => s.parse::<i64>().ok(),
        _ => None,
    };
    let base_layout_key = match &fields.base {
        Some(s) => s.parse::<i64>().ok(),
        None => None,
    };
    let _ = parse_event_type(fields.event.as_deref());
    Some(ParsedKittySequence {
        codepoint,
        shifted_key,
        base_layout_key,
        modifier: modifier_mask_from_field(fields.mod_field.as_deref()),
    })
}

fn parse_kitty_sequence(data: &str) -> Option<ParsedKittySequence> {
    decode_csi_u_sequence(data)
        .or_else(|| decode_nav_letter_sequence(data))
        .or_else(|| decode_tilde_sequence(data))
}

fn matches_kitty_sequence(data: &str, expected_codepoint: i64, expected_modifier: i64) -> bool {
    let parsed = match parse_kitty_sequence(data) {
        Some(p) => p,
        None => return false,
    };
    let actual_mod = parsed.modifier & !LOCK_MASK;
    let expected_mod = expected_modifier & !LOCK_MASK;

    if actual_mod != expected_mod {
        return false;
    }
    if parsed.codepoint == expected_codepoint {
        return true;
    }
    // Fallback for non-Latin layouts: compare against the base-layout key.
    if let Some(base) = parsed.base_layout_key
        && base == expected_codepoint
    {
        return true;
    }
    let _ = parsed.shifted_key; // parity: parsed but unused in matching
    false
}

/// Recognize xterm's modifyOtherKeys form, CSI 27 ; modifiers ; keycode ~.
fn matches_modify_other_keys(data: &str, expected_keycode: i64, expected_modifier: i64) -> bool {
    let rest = match data.strip_prefix("\x1b[27;") {
        Some(r) => r,
        None => return false,
    };
    let rest = match rest.strip_suffix('~') {
        Some(r) => r,
        None => return false,
    };
    let bytes = rest.as_bytes();
    let (mod_str, cursor) = scan_digits(bytes, 0);
    if mod_str.is_empty() || cursor >= bytes.len() || bytes[cursor] != b';' {
        return false;
    }
    let (keycode_str, next) = scan_digits(bytes, cursor + 1);
    if keycode_str.is_empty() || next != bytes.len() {
        return false;
    }
    let mod_value: i64 = match mod_str.parse() {
        Ok(v) => v,
        Err(_) => return false,
    };
    let keycode: i64 = match keycode_str.parse() {
        Ok(v) => v,
        Err(_) => return false,
    };
    // Shift xterm's 1-based modifier down to the 0-based scheme.
    let actual_mod = mod_value - 1;
    keycode == expected_keycode && actual_mod == expected_modifier
}

// =============================================================================
// General Key Matching
// =============================================================================

// Return the control byte a key would produce when ctrl is held (code & 0x1f).
fn raw_ctrl_char(key: &str) -> Option<String> {
    let char = key.to_lowercase();
    let c = char.chars().next()?;
    let code = c as u32;
    if (97..=122).contains(&code) || c == '[' || c == '\\' || c == ']' || c == '_' {
        let masked = (code & 0x1f) as u8;
        return Some((masked as char).to_string());
    }
    // Treat "-" like "_" because they share one physical key on US keyboards.
    if c == '-' {
        return Some((31u8 as char).to_string());
    }
    None
}

fn compute_modifier_mask(decoded: &DecodedKeyId) -> i64 {
    let mut bits = 0;
    if decoded.shift {
        bits |= MOD_SHIFT;
    }
    if decoded.alt {
        bits |= MOD_ALT;
    }
    if decoded.ctrl {
        bits |= MOD_CTRL;
    }
    bits
}

fn match_function_key(data: &str, function_key: &str, modifier_bits: i64) -> bool {
    if modifier_bits != 0 {
        return false;
    }
    matches_legacy_sequence(data, legacy_key_sequences(function_key))
}

fn match_enter_like_key(data: &str, decoded: &DecodedKeyId, modifier_bits: i64) -> bool {
    let kitty = is_kitty_protocol_active();
    let (ctrl, shift, alt) = (decoded.ctrl, decoded.shift, decoded.alt);

    if shift && !ctrl && !alt {
        if matches_kitty_sequence(data, CP_ENTER, MOD_SHIFT)
            || matches_kitty_sequence(data, CP_KP_ENTER, MOD_SHIFT)
        {
            return true;
        }
        if matches_modify_other_keys(data, CP_ENTER, MOD_SHIFT) {
            return true;
        }
        if kitty {
            return data == "\x1b\r" || data == "\n";
        }
        return false;
    }

    if alt && !ctrl && !shift {
        if matches_kitty_sequence(data, CP_ENTER, MOD_ALT)
            || matches_kitty_sequence(data, CP_KP_ENTER, MOD_ALT)
        {
            return true;
        }
        if matches_modify_other_keys(data, CP_ENTER, MOD_ALT) {
            return true;
        }
        if !kitty {
            return data == "\x1b\r";
        }
        return false;
    }

    if modifier_bits == 0 {
        return data == "\r"
            || (!kitty && data == "\n")
            || data == "\x1bOM"
            || matches_kitty_sequence(data, CP_ENTER, 0)
            || matches_kitty_sequence(data, CP_KP_ENTER, 0);
    }

    matches_kitty_sequence(data, CP_ENTER, modifier_bits)
        || matches_kitty_sequence(data, CP_KP_ENTER, modifier_bits)
}

fn match_functional_key(data: &str, legacy_name: &str, codepoint: i64, modifier_bits: i64) -> bool {
    if modifier_bits == 0 {
        return matches_legacy_sequence(data, legacy_key_sequences(legacy_name))
            || matches_kitty_sequence(data, codepoint, 0);
    }
    if matches_legacy_modifier_sequence(data, legacy_name, modifier_bits) {
        return true;
    }
    matches_kitty_sequence(data, codepoint, modifier_bits)
}

fn match_arrow_key(
    data: &str,
    arrow_name: &str,
    codepoint: i64,
    decoded: &DecodedKeyId,
    modifier_bits: i64,
) -> bool {
    let (ctrl, shift, alt) = (decoded.ctrl, decoded.shift, decoded.alt);
    if alt && !ctrl && !shift {
        let alt_legacy = if arrow_name == "arrowUp" {
            "\x1bp"
        } else {
            "\x1bn"
        };
        return data == alt_legacy || matches_kitty_sequence(data, codepoint, MOD_ALT);
    }
    if modifier_bits == 0 {
        return matches_legacy_sequence(data, legacy_key_sequences(arrow_name))
            || matches_kitty_sequence(data, codepoint, 0);
    }
    if matches_legacy_modifier_sequence(data, arrow_name, modifier_bits) {
        return true;
    }
    matches_kitty_sequence(data, codepoint, modifier_bits)
}

fn match_left_arrow(data: &str, decoded: &DecodedKeyId, modifier_bits: i64) -> bool {
    let kitty = is_kitty_protocol_active();
    let (ctrl, shift, alt) = (decoded.ctrl, decoded.shift, decoded.alt);
    if alt && !ctrl && !shift {
        return data == "\x1b[1;3D"
            || (!kitty && data == "\x1bB")
            || data == "\x1bb"
            || matches_kitty_sequence(data, CP_ARROW_LEFT, MOD_ALT);
    }
    if ctrl && !alt && !shift {
        return data == "\x1b[1;5D"
            || matches_legacy_modifier_sequence(data, "arrowLeft", MOD_CTRL)
            || matches_kitty_sequence(data, CP_ARROW_LEFT, MOD_CTRL);
    }
    if modifier_bits == 0 {
        return matches_legacy_sequence(data, legacy_key_sequences("arrowLeft"))
            || matches_kitty_sequence(data, CP_ARROW_LEFT, 0);
    }
    if matches_legacy_modifier_sequence(data, "arrowLeft", modifier_bits) {
        return true;
    }
    matches_kitty_sequence(data, CP_ARROW_LEFT, modifier_bits)
}

fn match_right_arrow(data: &str, decoded: &DecodedKeyId, modifier_bits: i64) -> bool {
    let kitty = is_kitty_protocol_active();
    let (ctrl, shift, alt) = (decoded.ctrl, decoded.shift, decoded.alt);
    if alt && !ctrl && !shift {
        return data == "\x1b[1;3C"
            || (!kitty && data == "\x1bF")
            || data == "\x1bf"
            || matches_kitty_sequence(data, CP_ARROW_RIGHT, MOD_ALT);
    }
    if ctrl && !alt && !shift {
        return data == "\x1b[1;5C"
            || matches_legacy_modifier_sequence(data, "arrowRight", MOD_CTRL)
            || matches_kitty_sequence(data, CP_ARROW_RIGHT, MOD_CTRL);
    }
    if modifier_bits == 0 {
        return matches_legacy_sequence(data, legacy_key_sequences("arrowRight"))
            || matches_kitty_sequence(data, CP_ARROW_RIGHT, 0);
    }
    if matches_legacy_modifier_sequence(data, "arrowRight", modifier_bits) {
        return true;
    }
    matches_kitty_sequence(data, CP_ARROW_RIGHT, modifier_bits)
}

fn match_printable_key(data: &str, decoded: &DecodedKeyId, modifier_bits: i64) -> bool {
    let kitty = is_kitty_protocol_active();
    let (ctrl, shift, alt) = (decoded.ctrl, decoded.shift, decoded.alt);
    let key = &decoded.key;

    let key_char = match key.chars().next() {
        Some(c) if key.chars().count() == 1 => c,
        _ => return false,
    };
    if !(key_char.is_ascii_lowercase() || is_symbol_key(key_char)) {
        return false;
    }

    let codepoint = key_char as i64;
    let control_character = raw_ctrl_char(key);

    if ctrl
        && alt
        && !shift
        && !kitty
        && let Some(cc) = &control_character
    {
        return data == format!("\x1b{cc}");
    }

    if alt
        && !ctrl
        && !shift
        && !kitty
        && key_char.is_ascii_lowercase()
        && data == format!("\x1b{key}")
    {
        return true;
    }

    if ctrl && !shift && !alt {
        if let Some(cc) = &control_character
            && data == *cc
        {
            return true;
        }
        return matches_kitty_sequence(data, codepoint, MOD_CTRL);
    }

    if ctrl && shift && !alt {
        return matches_kitty_sequence(data, codepoint, MOD_SHIFT + MOD_CTRL);
    }

    if shift && !ctrl && !alt {
        if data == key.to_uppercase() {
            return true;
        }
        return matches_kitty_sequence(data, codepoint, MOD_SHIFT);
    }

    if modifier_bits != 0 {
        return matches_kitty_sequence(data, codepoint, modifier_bits);
    }

    data == *key || matches_kitty_sequence(data, codepoint, 0)
}

fn match_key_engine(data: &str, key_id: &str) -> bool {
    let decoded = match parse_key_id(key_id) {
        Some(d) => d,
        None => return false,
    };
    let modifier_bits = compute_modifier_mask(&decoded);

    // The special-key resolver registry. Note the pageUp/pageDown quirk: TS
    // registers resolvers under lowercase "pageup"/"pagedown" but parse_key_id
    // normalizes to "pageUp"/"pageDown", so the lookup never finds them and
    // matches always fall through to the printable path (which rejects them).
    // Ported bug-for-bug.
    match decoded.key.as_str() {
        "escape" | "esc" => {
            if modifier_bits != 0 {
                return false;
            }
            data == "\x1b" || matches_kitty_sequence(data, CP_ESCAPE, 0)
        }
        "space" => {
            if !is_kitty_protocol_active() {
                if decoded.ctrl && !decoded.alt && !decoded.shift && data == "\x00" {
                    return true;
                }
                if decoded.alt && !decoded.ctrl && !decoded.shift && data == "\x1b " {
                    return true;
                }
            }
            if modifier_bits == 0 {
                return data == " " || matches_kitty_sequence(data, CP_SPACE, 0);
            }
            matches_kitty_sequence(data, CP_SPACE, modifier_bits)
        }
        "tab" => {
            if decoded.shift && !decoded.ctrl && !decoded.alt {
                return data == "\x1b[Z" || matches_kitty_sequence(data, CP_TAB, MOD_SHIFT);
            }
            if modifier_bits == 0 {
                return data == "\t" || matches_kitty_sequence(data, CP_TAB, 0);
            }
            matches_kitty_sequence(data, CP_TAB, modifier_bits)
        }
        "enter" | "return" => match_enter_like_key(data, &decoded, modifier_bits),
        "backspace" => {
            if decoded.alt && !decoded.ctrl && !decoded.shift {
                if data == "\x1b\x7f" || data == "\x1b\x08" {
                    return true;
                }
                return matches_kitty_sequence(data, CP_BACKSPACE, MOD_ALT);
            }
            if modifier_bits == 0 {
                return data == "\x7f"
                    || data == "\x08"
                    || matches_kitty_sequence(data, CP_BACKSPACE, 0);
            }
            matches_kitty_sequence(data, CP_BACKSPACE, modifier_bits)
        }
        "insert" => match_functional_key(data, "insert", CP_INSERT, modifier_bits),
        "delete" => match_functional_key(data, "delete", CP_DELETE, modifier_bits),
        "clear" => {
            if modifier_bits == 0 {
                return matches_legacy_sequence(data, legacy_key_sequences("clear"));
            }
            matches_legacy_modifier_sequence(data, "clear", modifier_bits)
        }
        "home" => match_functional_key(data, "home", CP_HOME, modifier_bits),
        "end" => match_functional_key(data, "end", CP_END, modifier_bits),
        "up" => match_arrow_key(data, "arrowUp", CP_ARROW_UP, &decoded, modifier_bits),
        "down" => match_arrow_key(data, "arrowDown", CP_ARROW_DOWN, &decoded, modifier_bits),
        "left" => match_left_arrow(data, &decoded, modifier_bits),
        "right" => match_right_arrow(data, &decoded, modifier_bits),
        "f1" | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" => {
            match_function_key(data, &decoded.key, modifier_bits)
        }
        _ => match_printable_key(data, &decoded, modifier_bits),
    }
}

// =============================================================================
// Key Parsing
// =============================================================================

fn legacy_literal_parse(data: &str) -> Option<&'static str> {
    let id = match data {
        "\x1b" => "escape",
        "\x1c" => "ctrl+\\",
        "\x1d" => "ctrl+]",
        "\x1f" => "ctrl+-",
        "\x1b\x1b" => "ctrl+alt+[",
        "\x1b\x1c" => "ctrl+alt+\\",
        "\x1b\x1d" => "ctrl+alt+]",
        "\x1b\x1f" => "ctrl+alt+-",
        "\t" => "tab",
        "\x00" => "ctrl+space",
        " " => "space",
        "\x1b[Z" => "shift+tab",
        "\x1b[A" => "up",
        "\x1b[B" => "down",
        "\x1b[C" => "right",
        "\x1b[D" => "left",
        "\x1b[3~" => "delete",
        "\x1b[5~" => "pageUp",
        "\x1b[6~" => "pageDown",
        _ => return None,
    };
    Some(id)
}

fn format_modifier_names(modifier_bits: i64) -> Vec<&'static str> {
    let mut ordered: Vec<&'static str> = Vec::new();
    if modifier_bits & MOD_SHIFT != 0 {
        ordered.push("shift");
    }
    if modifier_bits & MOD_CTRL != 0 {
        ordered.push("ctrl");
    }
    if modifier_bits & MOD_ALT != 0 {
        ordered.push("alt");
    }
    ordered
}

fn resolve_key_name(codepoint: i64) -> Option<String> {
    if codepoint == CP_ESCAPE {
        return Some("escape".to_string());
    }
    if codepoint == CP_TAB {
        return Some("tab".to_string());
    }
    if codepoint == CP_ENTER || codepoint == CP_KP_ENTER {
        return Some("enter".to_string());
    }
    if codepoint == CP_SPACE {
        return Some("space".to_string());
    }
    if codepoint == CP_BACKSPACE {
        return Some("backspace".to_string());
    }
    if codepoint == CP_DELETE {
        return Some("delete".to_string());
    }
    if codepoint == CP_INSERT {
        return Some("insert".to_string());
    }
    if codepoint == CP_HOME {
        return Some("home".to_string());
    }
    if codepoint == CP_END {
        return Some("end".to_string());
    }
    if codepoint == CP_PAGE_UP {
        return Some("pageUp".to_string());
    }
    if codepoint == CP_PAGE_DOWN {
        return Some("pageDown".to_string());
    }
    if codepoint == CP_ARROW_UP {
        return Some("up".to_string());
    }
    if codepoint == CP_ARROW_DOWN {
        return Some("down".to_string());
    }
    if codepoint == CP_ARROW_LEFT {
        return Some("left".to_string());
    }
    if codepoint == CP_ARROW_RIGHT {
        return Some("right".to_string());
    }
    if (97..=122).contains(&codepoint) {
        return char::from_u32(codepoint as u32).map(|c| c.to_string());
    }

    // TS uses String.fromCharCode here, which truncates to 16 bits. Mirror that
    // for symbol lookup; the synthetic negative codepoints are already handled.
    let truncated = (codepoint & 0xFFFF) as u32;
    if let Some(c) = char::from_u32(truncated)
        && is_symbol_key(c)
    {
        return Some(c.to_string());
    }
    None
}

fn parse_from_kitty(data: &str) -> Option<String> {
    let parsed = parse_kitty_sequence(data)?;
    let ordered_modifiers = format_modifier_names(parsed.modifier & !LOCK_MASK);
    let lookup_codepoint = parsed.base_layout_key.unwrap_or(parsed.codepoint);
    let key_name = resolve_key_name(lookup_codepoint)?;
    if ordered_modifiers.is_empty() {
        Some(key_name)
    } else {
        Some(format!("{}+{key_name}", ordered_modifiers.join("+")))
    }
}

fn parse_from_legacy(data: &str) -> Option<String> {
    let kitty = is_kitty_protocol_active();

    if kitty && (data == "\x1b\r" || data == "\n") {
        return Some("shift+enter".to_string());
    }

    if let Some(id) = legacy_sequence_key_id(data) {
        return Some(id.to_string());
    }

    if data == "\r" || (!kitty && data == "\n") || data == "\x1bOM" {
        return Some("enter".to_string());
    }

    if data == "\x7f" || data == "\x08" {
        return Some("backspace".to_string());
    }

    if !kitty && data == "\x1b\r" {
        return Some("alt+enter".to_string());
    }
    if !kitty && data == "\x1b " {
        return Some("alt+space".to_string());
    }
    if data == "\x1b\x7f" || data == "\x1b\x08" {
        return Some("alt+backspace".to_string());
    }
    if !kitty && data == "\x1bB" {
        return Some("alt+left".to_string());
    }
    if !kitty && data == "\x1bF" {
        return Some("alt+right".to_string());
    }
    if data == "\x1b[H" || data == "\x1bOH" {
        return Some("home".to_string());
    }
    if data == "\x1b[F" || data == "\x1bOF" {
        return Some("end".to_string());
    }

    if let Some(id) = legacy_literal_parse(data) {
        return Some(id.to_string());
    }

    let chars: Vec<char> = data.chars().collect();
    if !kitty && chars.len() == 2 && chars[0] == '\x1b' {
        let legacy_code = chars[1] as u32;
        if (1..=26).contains(&legacy_code) {
            let c = char::from_u32(legacy_code + 96).unwrap();
            return Some(format!("ctrl+alt+{c}"));
        }
        if (97..=122).contains(&legacy_code) {
            let c = char::from_u32(legacy_code).unwrap();
            return Some(format!("alt+{c}"));
        }
    }

    if chars.len() == 1 {
        let raw_code = chars[0] as u32;
        if (1..=26).contains(&raw_code) {
            let c = char::from_u32(raw_code + 96).unwrap();
            return Some(format!("ctrl+{c}"));
        }
        if (32..=126).contains(&raw_code) {
            return Some(data.to_string());
        }
    }

    None
}

fn parse_key_engine(data: &str) -> Option<String> {
    if let Some(id) = parse_from_kitty(data) {
        return Some(id);
    }
    parse_from_legacy(data)
}

// =============================================================================
// Public matching / parsing entry points
// =============================================================================

/// Decide whether raw terminal input corresponds to a particular key id.
///
/// Understands named keys, the four arrows, ctrl/shift/alt forms, and
/// multi-modifier combinations. Mirrors TS `matchesKey`.
pub fn matches_key(data: &str, key_id: &str) -> bool {
    match_key_engine(data, key_id)
}

/// Decode raw terminal input back into its key id, or `None` when nothing
/// matches. Mirrors TS `parseKey`.
pub fn parse_key(data: &str) -> Option<KeyId> {
    parse_key_engine(data)
}

// =============================================================================
// Tests — mirror the behaviors of indus-rebuild/src/ui/keys.ts
// =============================================================================
//
// All expected values were derived by reading the TypeScript ground truth
// (indus-rebuild/src/ui/keys.ts): its CSI-u grammar, the LEGACY_* tables, the
// KeyParseEngine fallback order, and the KeyMatchEngine resolver registry. The
// Python port's test_keys.py served as a cross-reference only.
#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Mutex;

    // The Kitty flag is process-global; serialize tests that touch it so they
    // do not interleave with each other under the multi-threaded test harness.
    static FLAG_GUARD: Mutex<()> = Mutex::new(());

    fn with_kitty(active: bool, f: impl FnOnce()) {
        let _guard = FLAG_GUARD.lock().unwrap_or_else(|e| e.into_inner());
        set_kitty_protocol_active(active);
        f();
        set_kitty_protocol_active(false);
    }

    // ----- CSI-u parsing -----

    #[test]
    fn csi_u_parse() {
        with_kitty(false, || {
            let cases: &[(&str, &str)] = &[
                ("\x1b[97u", "a"),
                ("\x1b[122u", "z"),
                ("\x1b[59u", ";"),
                ("\x1b[63u", "?"),
                ("\x1b[97;1u", "a"),
                ("\x1b[97;2u", "shift+a"),
                ("\x1b[97;3u", "alt+a"),
                ("\x1b[97;5u", "ctrl+a"),
                ("\x1b[97;4u", "shift+alt+a"),
                ("\x1b[97;6u", "shift+ctrl+a"),
                ("\x1b[97;7u", "ctrl+alt+a"),
                ("\x1b[97;8u", "shift+ctrl+alt+a"),
                ("\x1b[27u", "escape"),
                ("\x1b[9u", "tab"),
                ("\x1b[13u", "enter"),
                ("\x1b[57414u", "enter"),
                ("\x1b[32u", "space"),
                ("\x1b[127u", "backspace"),
                ("\x1b[13;2u", "shift+enter"),
                ("\x1b[9;2u", "shift+tab"),
                ("\x1b[32;5u", "ctrl+space"),
                ("\x1b[127;3u", "alt+backspace"),
                ("\x1b[97;65u", "a"),
                ("\x1b[97;129u", "a"),
                ("\x1b[97;193u", "a"),
                ("\x1b[97;69u", "ctrl+a"),
                ("\x1b[97:65;2u", "shift+a"),
                ("\x1b[1089:1057:99;5u", "ctrl+c"),
                ("\x1b[99::99;5u", "ctrl+c"),
                ("\x1b[97:u", "a"),
                ("\x1b[97;5:1u", "ctrl+a"),
                ("\x1b[97;5:2u", "ctrl+a"),
                ("\x1b[97;5:3u", "ctrl+a"),
            ];
            for (data, expected) in cases {
                assert_eq!(
                    parse_key(data).as_deref(),
                    Some(*expected),
                    "parse {data:?}"
                );
            }
        });
    }

    #[test]
    fn csi_u_invalid_bodies_do_not_parse() {
        with_kitty(false, || {
            for data in ["\x1b[u", "\x1b[97;u", "\x1b[97;5xu", "\x1b[;5u"] {
                assert_eq!(parse_key(data), None, "invalid {data:?}");
            }
        });
    }

    #[test]
    fn csi_u_matches_key() {
        with_kitty(false, || {
            let cases: &[(&str, &str, bool)] = &[
                ("\x1b[97u", "a", true),
                ("\x1b[97;5u", "ctrl+a", true),
                ("\x1b[97;5u", "a", false),
                ("\x1b[97;5u", "ctrl+b", false),
                ("\x1b[112;6u", "ctrl+shift+p", true),
                ("\x1b[120;4u", "shift+alt+x", true),
                ("\x1b[120;8u", "ctrl+shift+alt+x", true),
                ("\x1b[1089:1057:99;5u", "ctrl+c", true),
                ("\x1b[97;65u", "a", true),
                ("\x1b[97;129u", "a", true),
                ("\x1b[57414;2u", "shift+enter", true),
                ("\x1b[105;5u", "ctrl+i", true),
                ("\x1b[59;5u", "ctrl+;", true),
            ];
            for (data, key_id, expected) in cases {
                assert_eq!(
                    matches_key(data, key_id),
                    *expected,
                    "match {data:?} {key_id}"
                );
            }
        });
    }

    // ----- Event types and bracketed-paste guards -----

    #[test]
    fn release_and_repeat_detection() {
        let cases: &[(&str, bool, bool)] = &[
            ("\x1b[97;5:3u", true, false),
            ("\x1b[97;5:2u", false, true),
            ("\x1b[97;5:1u", false, false),
            ("\x1b[97;5u", false, false),
            ("\x1b[1;1:3A", true, false),
            ("\x1b[1;1:2B", false, true),
            ("\x1b[3;1:3~", true, false),
            ("\x1b[1;1:3H", true, false),
            ("\x1b[1;1:3F", true, false),
        ];
        for (data, release, repeat) in cases {
            assert_eq!(is_key_release(data), *release, "release {data:?}");
            assert_eq!(is_key_repeat(data), *repeat, "repeat {data:?}");
        }
    }

    #[test]
    fn event_type_junk_code_collapses_to_press() {
        with_kitty(false, || {
            assert_eq!(parse_key("\x1b[97;5:9u").as_deref(), Some("ctrl+a"));
            assert!(!is_key_release("\x1b[97;5:9u"));
            assert!(!is_key_repeat("\x1b[97;5:9u"));
        });
    }

    #[test]
    fn bracketed_paste_guard_for_release_and_repeat() {
        let paste = "\x1b[200~90:62:3F:A5\x1b[201~";
        assert!(!is_key_release(paste));
        assert!(!is_key_repeat("\x1b[200~AA:2F:BB\x1b[201~"));
        // Without the paste marker the substring scan fires (TS ground truth).
        assert!(is_key_release("90:62:3F:A5"));
        assert!(is_key_repeat("AA:2F:BB"));
    }

    #[test]
    fn paste_markers_themselves_do_not_decode() {
        with_kitty(false, || {
            assert_eq!(parse_key("\x1b[200~"), None);
            assert_eq!(parse_key("\x1b[201~"), None);
            assert_eq!(parse_key("\x1b[200~hello"), None);
        });
    }

    // ----- Kitty nav-letter and tilde sequences -----

    #[test]
    fn nav_letter_and_tilde_parse() {
        with_kitty(false, || {
            let cases: &[(&str, &str)] = &[
                ("\x1b[1;5A", "ctrl+up"),
                ("\x1b[1;5B", "ctrl+down"),
                ("\x1b[1;5C", "ctrl+right"),
                ("\x1b[1;5D", "ctrl+left"),
                ("\x1b[1;2A", "shift+up"),
                ("\x1b[1;3C", "alt+right"),
                ("\x1b[1;6B", "shift+ctrl+down"),
                ("\x1b[1;2H", "shift+home"),
                ("\x1b[1;5F", "ctrl+end"),
                ("\x1b[1;1A", "up"),
                ("\x1b[2~", "insert"),
                ("\x1b[3~", "delete"),
                ("\x1b[5~", "pageUp"),
                ("\x1b[6~", "pageDown"),
                ("\x1b[7~", "home"),
                ("\x1b[8~", "end"),
                ("\x1b[3;5~", "ctrl+delete"),
                ("\x1b[5;2~", "shift+pageUp"),
                ("\x1b[6;5~", "ctrl+pageDown"),
                ("\x1b[2;3~", "alt+insert"),
            ];
            for (data, expected) in cases {
                assert_eq!(
                    parse_key(data).as_deref(),
                    Some(*expected),
                    "parse {data:?}"
                );
            }
        });
    }

    #[test]
    fn unknown_tilde_number_does_not_parse_but_may_match() {
        with_kitty(false, || {
            assert_eq!(parse_key("\x1b[4~"), None);
            assert_eq!(parse_key("\x1b[1~"), None);
            assert!(matches_key("\x1b[1~", "home"));
            assert!(matches_key("\x1b[4~", "end"));
            assert!(matches_key("\x1b[8~", "end"));
        });
    }

    // ----- xterm modifyOtherKeys -----

    #[test]
    fn modify_other_keys_enter_variants() {
        with_kitty(false, || {
            assert!(matches_key("\x1b[27;2;13~", "shift+enter"));
            assert!(matches_key("\x1b[27;3;13~", "alt+enter"));
            assert!(!matches_key("\x1b[27;5;13~", "ctrl+enter"));
            assert_eq!(parse_key("\x1b[27;2;13~"), None);
        });
    }

    // ----- Legacy arrows / home-end / editing / function keys -----

    #[test]
    fn legacy_sequences_parse() {
        with_kitty(false, || {
            let cases: &[(&str, &str)] = &[
                ("\x1b[A", "up"),
                ("\x1b[B", "down"),
                ("\x1b[C", "right"),
                ("\x1b[D", "left"),
                ("\x1bOA", "up"),
                ("\x1bOB", "down"),
                ("\x1bOC", "right"),
                ("\x1bOD", "left"),
                ("\x1b[H", "home"),
                ("\x1bOH", "home"),
                ("\x1b[F", "end"),
                ("\x1bOF", "end"),
                ("\x1b[E", "clear"),
                ("\x1bOE", "clear"),
                ("\x1b[e", "shift+clear"),
                ("\x1bOe", "ctrl+clear"),
                ("\x1b[2$", "shift+insert"),
                ("\x1b[2^", "ctrl+insert"),
                ("\x1b[3$", "shift+delete"),
                ("\x1b[3^", "ctrl+delete"),
                ("\x1b[5$", "shift+pageUp"),
                ("\x1b[6$", "shift+pageDown"),
                ("\x1b[7$", "shift+home"),
                ("\x1b[8$", "shift+end"),
                ("\x1b[5^", "ctrl+pageUp"),
                ("\x1b[6^", "ctrl+pageDown"),
                ("\x1b[7^", "ctrl+home"),
                ("\x1b[8^", "ctrl+end"),
                ("\x1b[a", "shift+up"),
                ("\x1b[b", "shift+down"),
                ("\x1b[c", "shift+right"),
                ("\x1b[d", "shift+left"),
                ("\x1bOa", "ctrl+up"),
                ("\x1bOb", "ctrl+down"),
                ("\x1bOc", "ctrl+right"),
                ("\x1bOd", "ctrl+left"),
                ("\x1b[Z", "shift+tab"),
            ];
            for (data, expected) in cases {
                assert_eq!(
                    parse_key(data).as_deref(),
                    Some(*expected),
                    "parse {data:?}"
                );
            }
        });
    }

    #[test]
    fn function_keys_parse_and_match() {
        with_kitty(false, || {
            let table: &[(&str, &[&str])] = &[
                ("f1", &["\x1bOP", "\x1b[11~", "\x1b[[A"]),
                ("f2", &["\x1bOQ", "\x1b[12~", "\x1b[[B"]),
                ("f3", &["\x1bOR", "\x1b[13~", "\x1b[[C"]),
                ("f4", &["\x1bOS", "\x1b[14~", "\x1b[[D"]),
                ("f5", &["\x1b[15~", "\x1b[[E"]),
                ("f6", &["\x1b[17~"]),
                ("f7", &["\x1b[18~"]),
                ("f8", &["\x1b[19~"]),
                ("f9", &["\x1b[20~"]),
                ("f10", &["\x1b[21~"]),
                ("f11", &["\x1b[23~"]),
                ("f12", &["\x1b[24~"]),
            ];
            for (key_id, seqs) in table {
                for seq in seqs.iter() {
                    assert_eq!(parse_key(seq).as_deref(), Some(*key_id), "parse {seq:?}");
                    assert!(matches_key(seq, key_id), "match {seq:?} {key_id}");
                    assert!(!matches_key(seq, &format!("ctrl+{key_id}")));
                    assert!(!matches_key(seq, &format!("shift+{key_id}")));
                }
            }
        });
    }

    #[test]
    fn legacy_matches_key() {
        with_kitty(false, || {
            let cases: &[(&str, &str, bool)] = &[
                ("\x1b[A", "up", true),
                ("\x1bOA", "up", true),
                ("\x1b[a", "shift+up", true),
                ("\x1bOa", "ctrl+up", true),
                ("\x1b[3~", "delete", true),
                ("\x1b[3^", "ctrl+delete", true),
                ("\x1b[3$", "shift+delete", true),
                ("\x1b[2~", "insert", true),
                ("\x1b[H", "home", true),
                ("\x1b[7^", "ctrl+home", true),
                ("\x1b[F", "end", true),
                ("\x1b[E", "clear", true),
                ("\x1b[e", "shift+clear", true),
                ("\x1bOe", "ctrl+clear", true),
                ("\x1b[1;5D", "ctrl+left", true),
                ("\x1b[1;5C", "ctrl+right", true),
                ("\x1b[1;2H", "shift+home", true),
                ("\x1b[1;5F", "ctrl+end", true),
                ("\x1b[A", "down", false),
                ("\x1b[A", "ctrl+up", false),
            ];
            for (data, key_id, expected) in cases {
                assert_eq!(
                    matches_key(data, key_id),
                    *expected,
                    "match {data:?} {key_id}"
                );
            }
        });
    }

    // ----- Alt-prefixed legacy input (kitty OFF) -----

    #[test]
    fn alt_prefixed_parse_kitty_off() {
        with_kitty(false, || {
            let cases: &[(&str, &str)] = &[
                ("\x1bb", "alt+left"),
                ("\x1bf", "alt+right"),
                ("\x1bp", "alt+up"),
                ("\x1bn", "alt+down"),
                ("\x1bB", "alt+left"),
                ("\x1bF", "alt+right"),
                ("\x1ba", "alt+a"),
                ("\x1bx", "alt+x"),
                ("\x1bz", "alt+z"),
                ("\x1b\x01", "ctrl+alt+a"),
                ("\x1b\x1a", "ctrl+alt+z"),
                ("\x1b\x1b", "ctrl+alt+["),
                ("\x1b\x1c", "ctrl+alt+\\"),
                ("\x1b\x1d", "ctrl+alt+]"),
                ("\x1b\x1f", "ctrl+alt+-"),
                ("\x1b\x7f", "alt+backspace"),
                ("\x1b\x08", "alt+backspace"),
                ("\x1b\r", "alt+enter"),
                ("\x1b ", "alt+space"),
            ];
            for (data, expected) in cases {
                assert_eq!(
                    parse_key(data).as_deref(),
                    Some(*expected),
                    "parse {data:?}"
                );
            }
        });
    }

    #[test]
    fn alt_b_matches_both_alt_left_and_alt_b() {
        with_kitty(false, || {
            assert!(matches_key("\x1bb", "alt+left"));
            assert!(matches_key("\x1bb", "alt+b"));
        });
    }

    #[test]
    fn ctrl_alt_letter_matching_kitty_off_vs_on() {
        with_kitty(false, || {
            assert!(matches_key("\x1b\x18", "ctrl+alt+x"));
            assert!(!matches_key("\x1b[120;7u", "ctrl+alt+x"));
        });
        with_kitty(true, || {
            assert!(matches_key("\x1b[120;7u", "ctrl+alt+x"));
            assert!(!matches_key("\x1b\x18", "ctrl+alt+x"));
        });
    }

    #[test]
    fn kitty_flag_changes_legacy_interpretation() {
        with_kitty(true, || {
            assert!(is_kitty_protocol_active());
            assert_eq!(parse_key("\x1b\r").as_deref(), Some("shift+enter"));
            assert_eq!(parse_key("\n").as_deref(), Some("shift+enter"));
            assert_eq!(parse_key("\x1bx"), None);
            assert_eq!(parse_key("\x1b "), None);
            assert_eq!(parse_key("\x1bB"), None);
            assert!(matches_key("\n", "shift+enter"));
            assert!(!matches_key("\n", "enter"));
            assert!(!matches_key("\x1b ", "alt+space"));
        });
        with_kitty(false, || {
            assert!(!is_kitty_protocol_active());
            assert_eq!(parse_key("\n").as_deref(), Some("enter"));
            assert_eq!(parse_key("\x1b\r").as_deref(), Some("alt+enter"));
            assert!(matches_key("\n", "enter"));
            assert!(matches_key("\x1b ", "alt+space"));
        });
    }

    // ----- Plain characters and bare control bytes -----

    #[test]
    fn single_byte_parse() {
        with_kitty(false, || {
            let cases: &[(&str, &str)] = &[
                ("a", "a"),
                ("Z", "Z"),
                ("?", "?"),
                ("1", "1"),
                ("\x01", "ctrl+a"),
                ("\x03", "ctrl+c"),
                ("\x1a", "ctrl+z"),
                ("\x00", "ctrl+space"),
                ("\t", "tab"),
                ("\r", "enter"),
                ("\n", "enter"),
                ("\x1bOM", "enter"),
                (" ", "space"),
                ("\x7f", "backspace"),
                ("\x08", "backspace"),
                ("\x1b", "escape"),
                ("\x1c", "ctrl+\\"),
                ("\x1d", "ctrl+]"),
                ("\x1f", "ctrl+-"),
            ];
            for (data, expected) in cases {
                assert_eq!(
                    parse_key(data).as_deref(),
                    Some(*expected),
                    "parse {data:?}"
                );
            }
        });
    }

    #[test]
    fn printable_and_control_matches() {
        with_kitty(false, || {
            let cases: &[(&str, &str, bool)] = &[
                ("a", "a", true),
                ("a", "b", false),
                ("A", "shift+a", true),
                ("a", "shift+a", false),
                ("\x03", "ctrl+c", true),
                ("\x03", "ctrl+d", false),
                ("\r", "enter", true),
                ("\r", "ctrl+m", true),
                ("\x1f", "ctrl+-", true),
                ("\x1f", "ctrl+_", true),
                ("\x1c", "ctrl+\\", true),
                ("\x1d", "ctrl+]", true),
                ("\x1b", "escape", true),
                ("\x1b", "esc", true),
                ("\r", "return", true),
                ("\x00", "ctrl+space", true),
                ("\x1b[Z", "shift+tab", true),
                ("\t", "tab", true),
                (" ", "space", true),
                ("\x7f", "backspace", true),
                ("\x10", "ctrl+shift+p", false),
            ];
            for (data, key_id, expected) in cases {
                assert_eq!(
                    matches_key(data, key_id),
                    *expected,
                    "match {data:?} {key_id}"
                );
            }
        });
    }

    // ----- The pageUp/pageDown matcher quirk (ported bug-for-bug) -----

    #[test]
    fn pageup_pagedown_matcher_quirk() {
        with_kitty(false, || {
            assert_eq!(parse_key("\x1b[5~").as_deref(), Some("pageUp"));
            assert_eq!(parse_key("\x1b[6~").as_deref(), Some("pageDown"));
            assert!(!matches_key("\x1b[5~", "pageUp"));
            assert!(!matches_key("\x1b[6~", "pageDown"));
            assert!(!matches_key("\x1b[5$", "shift+pageUp"));
            assert!(!matches_key("\x1b[6^", "ctrl+pageDown"));
        });
    }

    // ----- Partial-buffer resumption -----

    #[test]
    fn partial_buffers_never_decode_then_full_buffer_does() {
        with_kitty(false, || {
            let cases: &[(&str, &str)] = &[
                ("\x1b[1;5D", "ctrl+left"),
                ("\x1b[1;2H", "shift+home"),
                ("\x1b[97;5u", "ctrl+a"),
                ("\x1b[57414;2u", "shift+enter"),
                ("\x1b[3;5~", "ctrl+delete"),
                ("\x1bOP", "f1"),
                ("\x1b[24~", "f12"),
            ];
            for (sequence, key_id) in cases {
                let bytes = sequence.as_bytes();
                for cut in 2..bytes.len() {
                    let prefix = &sequence[..cut];
                    assert_eq!(parse_key(prefix), None, "prefix {prefix:?} decoded");
                    assert!(!matches_key(prefix, key_id), "prefix {prefix:?} matched");
                }
                assert_eq!(parse_key(sequence).as_deref(), Some(*key_id));
                assert!(matches_key(sequence, key_id));
            }
        });
    }

    #[test]
    fn lone_escape_byte_is_escape() {
        with_kitty(false, || {
            assert_eq!(parse_key("\x1b").as_deref(), Some("escape"));
            assert!(matches_key("\x1b", "escape"));
        });
    }

    // ----- Key builder, normalization, validation -----

    #[test]
    fn key_builder_produces_ts_identical_ids() {
        assert_eq!(Key::ESCAPE, "escape");
        assert_eq!(Key::PAGE_UP, "pageUp");
        assert_eq!(Key::PAGE_DOWN, "pageDown");
        assert_eq!(Key::BACKTICK, "`");
        assert_eq!(Key::QUESTION, "?");
        assert_eq!(Key::ctrl("c"), "ctrl+c");
        assert_eq!(Key::shift("tab"), "shift+tab");
        assert_eq!(Key::alt("x"), "alt+x");
        assert_eq!(Key::ctrl_shift("p"), "ctrl+shift+p");
        assert_eq!(Key::shift_ctrl("p"), "shift+ctrl+p");
        assert_eq!(Key::ctrl_alt("x"), "ctrl+alt+x");
        assert_eq!(Key::alt_shift("x"), "alt+shift+x");
        assert_eq!(Key::ctrl_shift_alt("x"), "ctrl+shift+alt+x");
    }

    #[test]
    fn normalize_key_id_cases() {
        let cases: &[(&str, &str)] = &[
            ("CTRL+C", "ctrl+c"),
            ("alt+ctrl+x", "ctrl+alt+x"),
            ("Shift+Ctrl+P", "ctrl+shift+p"),
            ("alt+shift+x", "shift+alt+x"),
            ("return", "enter"),
            ("esc", "escape"),
            ("PAGEUP", "pageUp"),
            ("pagedown", "pageDown"),
            (" ctrl + c ", "ctrl+c"),
            ("", ""),
            ("ctrl+", "ctrl"),
        ];
        for (raw, normalized) in cases {
            assert_eq!(&normalize_key_id(raw), normalized, "normalize {raw:?}");
        }
    }

    #[test]
    fn key_validator() {
        let validator = KeyValidator::new();
        assert!(validator.is_valid("ctrl+c"));
        assert!(validator.is_valid("shift+pageUp"));
        assert!(validator.is_valid("ctrl+shift+alt+z"));
        assert!(validator.is_valid("?"));
        assert!(!validator.is_valid("banana"));
        assert!(!validator.is_valid(""));
        assert!(!validator.is_valid("ctrl+"));
        assert_eq!(validator.normalize("Return"), "enter");
    }

    #[test]
    fn matches_key_with_invalid_id_returns_false() {
        with_kitty(false, || {
            assert!(!matches_key("a", "notakey"));
            assert!(!matches_key("\x01", ""));
        });
    }

    #[test]
    fn protocol_flag_round_trips() {
        with_kitty(false, || {
            assert!(!is_kitty_protocol_active());
            set_kitty_protocol_active(true);
            assert!(is_kitty_protocol_active());
            set_kitty_protocol_active(false);
            assert!(!is_kitty_protocol_active());
        });
    }
}
