use std::fmt::Write as _;

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind};

use super::model::KITTY_FLAG_REPORT_ALL_KEYS;
use super::{KeyboardProtocol, MouseProtocolEncoding, TerminalKey};

const KITTY_FLAG_REPORT_EVENT_TYPES: u16 = 0b0000_0010;
const KITTY_FLAG_REPORT_ALTERNATE_KEYS: u16 = 0b0000_0100;
const KITTY_FLAG_REPORT_ASSOCIATED_TEXT: u16 = 0b0001_0000;

/// Encode a key event for a PTY child using the pane's negotiated keyboard protocol.
#[allow(dead_code)] // exercised in input unit tests; production uses TerminalRuntime helpers
pub fn encode_key(key: KeyEvent, protocol: KeyboardProtocol) -> Vec<u8> {
    encode_terminal_key(key.into(), protocol)
}

pub fn encode_terminal_key(key: TerminalKey, protocol: KeyboardProtocol) -> Vec<u8> {
    if key.kind != crossterm::event::KeyEventKind::Release {
        if let Some(text) = &key.generated_text {
            return text.as_bytes().to_vec();
        }
    }

    // A release event only produces bytes when the pane protocol reports event
    // types (Kitty REPORT_EVENT_TYPES). Otherwise the child expects a single
    // legacy byte per keystroke, so re-emitting it on release would double keys
    // like Enter/Backspace. The Ghostty wrapper can route release events through
    // this fallback, so guard the fallback encoder too.
    if key.kind == crossterm::event::KeyEventKind::Release && !protocol.reports_event_types() {
        return Vec::new();
    }

    let kitty_first = protocol.reports_all_keys()
        || (key.kind == crossterm::event::KeyEventKind::Release && protocol.reports_event_types());

    if kitty_first {
        if let KeyboardProtocol::Kitty { flags } = protocol {
            if let Some(bytes) = try_encode_csi_u(&key, flags) {
                return bytes;
            }
        }
    }

    if let Some(bytes) = encode_text_input(&key) {
        return bytes;
    }

    if !kitty_first {
        if let KeyboardProtocol::Kitty { flags } = protocol {
            if let Some(bytes) = try_encode_csi_u(&key, flags) {
                return bytes;
            }
        }
    }
    if key.kind == crossterm::event::KeyEventKind::Release && protocol.reports_event_types() {
        return Vec::new();
    }
    encode_legacy(key)
}

#[allow(dead_code)] // exercised in input unit tests; production uses TerminalRuntime helpers
pub fn encode_cursor_key(code: KeyCode, application_cursor: bool) -> Vec<u8> {
    match (code, application_cursor) {
        (KeyCode::Up, true) => b"\x1bOA".to_vec(),
        (KeyCode::Down, true) => b"\x1bOB".to_vec(),
        (KeyCode::Right, true) => b"\x1bOC".to_vec(),
        (KeyCode::Left, true) => b"\x1bOD".to_vec(),
        (KeyCode::Up, false) => b"\x1b[A".to_vec(),
        (KeyCode::Down, false) => b"\x1b[B".to_vec(),
        (KeyCode::Right, false) => b"\x1b[C".to_vec(),
        (KeyCode::Left, false) => b"\x1b[D".to_vec(),
        _ => encode_legacy(KeyEvent::new(code, KeyModifiers::empty()).into()),
    }
}

#[allow(dead_code)] // exercised in input unit tests; pane runtime uses backend helpers
pub fn encode_mouse_scroll(
    kind: MouseEventKind,
    column: u16,
    row: u16,
    modifiers: KeyModifiers,
    encoding: MouseProtocolEncoding,
) -> Option<Vec<u8>> {
    let button = match kind {
        MouseEventKind::ScrollUp => 64u16,
        MouseEventKind::ScrollDown => 65u16,
        MouseEventKind::ScrollLeft => 66u16,
        MouseEventKind::ScrollRight => 67u16,
        _ => return None,
    };
    encode_mouse_cb(button, false, column, row, modifiers, encoding)
}

#[allow(dead_code)] // exercised in input unit tests; pane runtime uses backend helpers
pub fn encode_mouse_button(
    kind: MouseEventKind,
    column: u16,
    row: u16,
    modifiers: KeyModifiers,
    encoding: MouseProtocolEncoding,
) -> Option<Vec<u8>> {
    let (button, release) = match kind {
        MouseEventKind::Down(MouseButton::Left) => (0u16, false),
        MouseEventKind::Down(MouseButton::Middle) => (1u16, false),
        MouseEventKind::Down(MouseButton::Right) => (2u16, false),
        MouseEventKind::Up(MouseButton::Left) => (0u16, true),
        MouseEventKind::Up(MouseButton::Middle) => (1u16, true),
        MouseEventKind::Up(MouseButton::Right) => (2u16, true),
        MouseEventKind::Drag(MouseButton::Left) => (32u16, false),
        MouseEventKind::Drag(MouseButton::Middle) => (33u16, false),
        MouseEventKind::Drag(MouseButton::Right) => (34u16, false),
        _ => return None,
    };
    encode_mouse_cb(button, release, column, row, modifiers, encoding)
}

#[allow(dead_code)] // only reached through mouse encoding helpers above
fn encode_mouse_cb(
    base_button: u16,
    release: bool,
    column: u16,
    row: u16,
    modifiers: KeyModifiers,
    encoding: MouseProtocolEncoding,
) -> Option<Vec<u8>> {
    let mut cb = match (encoding, release) {
        (MouseProtocolEncoding::Sgr | MouseProtocolEncoding::SgrPixels, true) => base_button,
        (_, true) => 3,
        (_, false) => base_button,
    };
    if modifiers.contains(KeyModifiers::SHIFT) {
        cb += 4;
    }
    if modifiers.contains(KeyModifiers::ALT) {
        cb += 8;
    }
    if modifiers.contains(KeyModifiers::CONTROL) {
        cb += 16;
    }

    let column = column as u32 + 1;
    let row = row as u32 + 1;

    match encoding {
        MouseProtocolEncoding::Sgr | MouseProtocolEncoding::SgrPixels => Some(
            format!(
                "\x1b[<{cb};{column};{row}{}",
                if release { 'm' } else { 'M' }
            )
            .into_bytes(),
        ),
        MouseProtocolEncoding::Default => {
            let cb = u8::try_from(cb + 32).ok()?;
            let column = u8::try_from(column + 32).ok()?;
            let row = u8::try_from(row + 32).ok()?;
            Some(vec![0x1b, b'[', b'M', cb, column, row])
        }
        MouseProtocolEncoding::Utf8 => {
            let mut bytes = Vec::with_capacity(16);
            bytes.extend_from_slice(b"\x1b[M");
            push_mouse_codepoint(&mut bytes, cb as u32 + 32)?;
            push_mouse_codepoint(&mut bytes, column + 32)?;
            push_mouse_codepoint(&mut bytes, row + 32)?;
            Some(bytes)
        }
    }
}

#[allow(dead_code)] // only reached through mouse encoding helpers above
fn push_mouse_codepoint(bytes: &mut Vec<u8>, value: u32) -> Option<()> {
    let ch = char::from_u32(value)?;
    let mut buf = [0u8; 4];
    bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
    Some(())
}

/// CSI u encoding: \e[{codepoint};{modifiers}u
/// Used when the child has pushed Kitty keyboard enhancement.
/// Returns None if the key doesn't need CSI u (unmodified basic keys).
fn try_encode_csi_u(key: &TerminalKey, flags: u16) -> Option<Vec<u8>> {
    let mods = key.modifiers;
    let event_suffix = kitty_event_suffix(key, flags);
    let report_all_keys = flags & KITTY_FLAG_REPORT_ALL_KEYS != 0;

    if !report_all_keys
        && key.modifiers.is_empty()
        && matches!(key.code, KeyCode::Enter | KeyCode::Tab | KeyCode::Backspace)
    {
        return None;
    }

    // Unmodified keys use legacy encoding (more compatible)
    if mods.is_empty() && event_suffix.is_none() && !report_all_keys {
        return None;
    }

    // Special keys (arrows, F-keys, etc.) have well-established legacy
    // xterm modified formats (\x1b[1;3A for Alt+Up, etc.) that are universally
    // understood. Even Ghostty sends these in legacy format with kitty mode on.
    // Only use CSI u for character keys and keys without legacy representations.
    match key.code {
        KeyCode::Up
        | KeyCode::Down
        | KeyCode::Left
        | KeyCode::Right
        | KeyCode::Home
        | KeyCode::End
        | KeyCode::PageUp
        | KeyCode::PageDown
        | KeyCode::Insert
        | KeyCode::Delete
        | KeyCode::F(_)
            if event_suffix.is_none() && !report_all_keys =>
        {
            return None; // let legacy handle these
        }
        _ => {}
    }

    let (codepoint, alternate_shifted) = match key.code {
        KeyCode::Char(c) => {
            let base = canonical_kitty_char(c, mods);
            let shifted = alternate_shifted_codepoint(key, flags);
            (base as u32, shifted)
        }
        KeyCode::Enter => (13, None),
        KeyCode::Tab => (9, None),
        KeyCode::Backspace => (127, None),
        KeyCode::Esc => (27, None),
        KeyCode::Left => (57417, None),
        KeyCode::Right => (57418, None),
        KeyCode::Up => (57419, None),
        KeyCode::Down => (57420, None),
        KeyCode::PageUp => (57421, None),
        KeyCode::PageDown => (57422, None),
        KeyCode::Home => (57423, None),
        KeyCode::End => (57424, None),
        KeyCode::Insert => (57425, None),
        KeyCode::Delete => (57426, None),
        _ => return None, // fall back to legacy for unhandled keys
    };

    let modifier = kitty_modifier(mods);

    let mut sequence = String::with_capacity(32);
    sequence.push_str("\x1b[");
    write!(&mut sequence, "{codepoint}").ok()?;
    if let Some(shifted) = alternate_shifted {
        write!(&mut sequence, ":{shifted}").ok()?;
    }
    write!(&mut sequence, ";{modifier}").ok()?;
    if let Some(event) = event_suffix {
        write!(&mut sequence, ":{event}").ok()?;
    }
    if flags & KITTY_FLAG_REPORT_ASSOCIATED_TEXT != 0 {
        if let Some(text) = text_codepoint_for_key(key) {
            write!(&mut sequence, ";{text}").ok()?;
        }
    }
    sequence.push('u');

    Some(sequence.into_bytes())
}

fn text_codepoint_for_key(key: &TerminalKey) -> Option<u32> {
    let ch = text_char_for_key(key)?;
    (!ch.is_control()).then_some(ch as u32)
}

/// Legacy terminal encoding (standard escape sequences).
fn encode_legacy(key: TerminalKey) -> Vec<u8> {
    let mods = key.modifiers;

    // Modified special keys (arrows, home, end, etc.) use xterm format:
    //   \x1b[1;{modifier}A  for arrows/home/end
    //   \x1b[{n};{modifier}~ for insert/delete/pgup/pgdn
    // The ESC-prefix hack doesn't work for these since they're already escape sequences.
    if !mods.is_empty() {
        if let Some(bytes) = encode_modified_special(key.code, mods) {
            return bytes;
        }
    }

    // Alt modifier on character keys: prefix with ESC
    if mods.contains(KeyModifiers::ALT) {
        let inner = key.with_modifiers(mods.difference(KeyModifiers::ALT));
        let mut bytes = vec![0x1b];
        bytes.extend(encode_legacy_inner(inner));
        return bytes;
    }
    encode_legacy_inner(key)
}

/// xterm-style encoding for modified special keys.
/// Modifier value: 1 + (shift?1:0) + (alt?2:0) + (ctrl?4:0)
fn encode_modified_special(code: KeyCode, mods: KeyModifiers) -> Option<Vec<u8>> {
    let modifier = xterm_modifier(mods);
    if modifier <= 1 {
        return None; // no modifiers to encode
    }

    match code {
        // CSI 1;{mod}{letter} format
        KeyCode::Up => Some(format!("\x1b[1;{modifier}A").into_bytes()),
        KeyCode::Down => Some(format!("\x1b[1;{modifier}B").into_bytes()),
        KeyCode::Right => Some(format!("\x1b[1;{modifier}C").into_bytes()),
        KeyCode::Left => Some(format!("\x1b[1;{modifier}D").into_bytes()),
        KeyCode::Home => Some(format!("\x1b[1;{modifier}H").into_bytes()),
        KeyCode::End => Some(format!("\x1b[1;{modifier}F").into_bytes()),
        // CSI {n};{mod}~ format
        KeyCode::Insert => Some(format!("\x1b[2;{modifier}~").into_bytes()),
        KeyCode::Delete => Some(format!("\x1b[3;{modifier}~").into_bytes()),
        KeyCode::PageUp => Some(format!("\x1b[5;{modifier}~").into_bytes()),
        KeyCode::PageDown => Some(format!("\x1b[6;{modifier}~").into_bytes()),
        // F1-F4: CSI 1;{mod}{P-S}
        KeyCode::F(1) => Some(format!("\x1b[1;{modifier}P").into_bytes()),
        KeyCode::F(2) => Some(format!("\x1b[1;{modifier}Q").into_bytes()),
        KeyCode::F(3) => Some(format!("\x1b[1;{modifier}R").into_bytes()),
        KeyCode::F(4) => Some(format!("\x1b[1;{modifier}S").into_bytes()),
        // F5-F12: CSI {n};{mod}~
        KeyCode::F(n @ 5..=12) => {
            let code = match n {
                5 => 15,
                6 => 17,
                7 => 18,
                8 => 19,
                9 => 20,
                10 => 21,
                11 => 23,
                12 => 24,
                _ => unreachable!(),
            };
            Some(format!("\x1b[{code};{modifier}~").into_bytes())
        }
        _ => None,
    }
}

/// xterm modifier encoding: 1 + shift(1) + alt(2) + ctrl(4)
/// Used for legacy modified special keys (arrows, function keys, etc.)
fn xterm_modifier(mods: KeyModifiers) -> u32 {
    let mut m = 1u32;
    if mods.contains(KeyModifiers::SHIFT) {
        m += 1;
    }
    if mods.contains(KeyModifiers::ALT) {
        m += 2;
    }
    if mods.contains(KeyModifiers::CONTROL) {
        m += 4;
    }
    m
}

/// Kitty protocol modifier encoding: 1 + shift(1) + alt(2) + ctrl(4) + super(8) + hyper(16) + meta(32)
/// Superset of xterm — adds Super/Hyper/Meta bits.
fn kitty_modifier(mods: KeyModifiers) -> u32 {
    let mut m = xterm_modifier(mods);
    if mods.contains(KeyModifiers::SUPER) {
        m += 8;
    }
    if mods.contains(KeyModifiers::HYPER) {
        m += 16;
    }
    if mods.contains(KeyModifiers::META) {
        m += 32;
    }
    m
}

fn encode_text_input(key: &TerminalKey) -> Option<Vec<u8>> {
    let ch = text_char_for_key(key)?;
    let mut buf = [0u8; 4];
    Some(ch.encode_utf8(&mut buf).as_bytes().to_vec())
}

fn text_char_for_key(key: &TerminalKey) -> Option<char> {
    if key.kind == crossterm::event::KeyEventKind::Release {
        return None;
    }

    let KeyCode::Char(ch) = key.code else {
        return None;
    };

    if key.modifiers.is_empty() {
        return Some(ch);
    }
    if key.modifiers == KeyModifiers::SHIFT {
        return shifted_text_char(key, ch);
    }
    None
}

fn shifted_text_char(key: &TerminalKey, ch: char) -> Option<char> {
    if let Some(shifted) = key.shifted_codepoint.and_then(char::from_u32) {
        return Some(shifted);
    }

    if ch.is_ascii_uppercase() {
        return Some(ch);
    }

    if ch.is_ascii_lowercase() {
        return Some(ch.to_ascii_uppercase());
    }

    if is_shifted_ascii_punctuation(ch) {
        return Some(ch);
    }

    None
}

fn is_shifted_ascii_punctuation(ch: char) -> bool {
    matches!(
        ch,
        '!' | '@'
            | '#'
            | '$'
            | '%'
            | '^'
            | '&'
            | '*'
            | '('
            | ')'
            | '_'
            | '+'
            | '{'
            | '}'
            | '|'
            | ':'
            | '"'
            | '<'
            | '>'
            | '?'
            | '~'
    )
}

fn canonical_kitty_char(ch: char, mods: KeyModifiers) -> char {
    if mods.contains(KeyModifiers::SHIFT) && ch.is_ascii_uppercase() {
        ch.to_ascii_lowercase()
    } else {
        ch
    }
}

fn alternate_shifted_codepoint(key: &TerminalKey, flags: u16) -> Option<u32> {
    if flags & KITTY_FLAG_REPORT_ALTERNATE_KEYS == 0 {
        return None;
    }

    if let Some(shifted) = key.shifted_codepoint {
        return Some(shifted);
    }

    match key.code {
        KeyCode::Char(ch)
            if key.modifiers.contains(KeyModifiers::SHIFT) && ch.is_ascii_uppercase() =>
        {
            Some(ch as u32)
        }
        _ => None,
    }
}

fn kitty_event_suffix(key: &TerminalKey, flags: u16) -> Option<u8> {
    if flags & KITTY_FLAG_REPORT_EVENT_TYPES == 0 {
        return None;
    }

    Some(match key.kind {
        crossterm::event::KeyEventKind::Press => 1,
        crossterm::event::KeyEventKind::Repeat => 2,
        crossterm::event::KeyEventKind::Release => 3,
    })
}

fn encode_legacy_inner(key: TerminalKey) -> Vec<u8> {
    match key.code {
        KeyCode::Char(ch) => {
            if key.modifiers.contains(KeyModifiers::CONTROL) {
                let upper = ch.to_ascii_uppercase();
                match upper {
                    'A'..='Z' => vec![upper as u8 - 64],
                    ' ' | '@' | '2' => vec![0],
                    '[' | '3' => vec![27],
                    '\\' | '4' => vec![28],
                    ']' | '5' => vec![29],
                    '^' | '6' => vec![30],
                    '_' | '/' | '7' | '-' => vec![31],
                    _ => vec![ch as u8],
                }
            } else {
                let ch = if key.modifiers == KeyModifiers::SHIFT {
                    shifted_text_char(&key, ch).unwrap_or(ch)
                } else {
                    ch
                };
                let mut buf = [0u8; 4];
                ch.encode_utf8(&mut buf).as_bytes().to_vec()
            }
        }
        KeyCode::Enter => vec![b'\r'],
        KeyCode::Backspace => vec![127],
        KeyCode::Tab => vec![9],
        KeyCode::BackTab => vec![27, 91, 90],
        KeyCode::Esc => vec![27],
        KeyCode::Left => vec![27, 91, 68],
        KeyCode::Right => vec![27, 91, 67],
        KeyCode::Up => vec![27, 91, 65],
        KeyCode::Down => vec![27, 91, 66],
        KeyCode::Home => vec![27, 91, 72],
        KeyCode::End => vec![27, 91, 70],
        KeyCode::PageUp => vec![27, 91, 53, 126],
        KeyCode::PageDown => vec![27, 91, 54, 126],
        KeyCode::Delete => vec![27, 91, 51, 126],
        KeyCode::Insert => vec![27, 91, 50, 126],
        KeyCode::F(n) => encode_f_key(n),
        _ => vec![],
    }
}

fn encode_f_key(n: u8) -> Vec<u8> {
    match n {
        1 => vec![27, 79, 80],
        2 => vec![27, 79, 81],
        3 => vec![27, 79, 82],
        4 => vec![27, 79, 83],
        5 => vec![27, 91, 49, 53, 126],
        6 => vec![27, 91, 49, 55, 126],
        7 => vec![27, 91, 49, 56, 126],
        8 => vec![27, 91, 49, 57, 126],
        9 => vec![27, 91, 50, 48, 126],
        10 => vec![27, 91, 50, 49, 126],
        11 => vec![27, 91, 50, 51, 126],
        12 => vec![27, 91, 50, 52, 126],
        _ => vec![],
    }
}

#[cfg(test)]
mod tests {
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

    use super::*;
    use crate::input::parse_terminal_key_sequence;

    fn assert_terminal_key_eq(
        actual: TerminalKey,
        code: KeyCode,
        modifiers: KeyModifiers,
        kind: crossterm::event::KeyEventKind,
        shifted_codepoint: Option<u32>,
    ) {
        assert_eq!(actual.code, code);
        assert_eq!(actual.modifiers, modifiers);
        assert_eq!(actual.kind, kind);
        assert_eq!(actual.shifted_codepoint, shifted_codepoint);
    }

    #[test]
    fn legacy_enter() {
        let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![b'\r']);
    }

    #[test]
    fn legacy_ctrl_c() {
        let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![3]);
    }

    #[test]
    fn legacy_ctrl_slash_aliases_ctrl_underscore() {
        let key = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![31]);
    }

    #[test]
    fn legacy_shift_enter_is_just_cr() {
        let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![b'\r']);
    }

    #[test]
    fn legacy_alt_up() {
        let key = KeyEvent::new(KeyCode::Up, KeyModifiers::ALT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;3A");
    }

    #[test]
    fn legacy_shift_right() {
        let key = KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;2C");
    }

    #[test]
    fn legacy_ctrl_left() {
        let key = KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;5D");
    }

    #[test]
    fn legacy_ctrl_shift_end() {
        let key = KeyEvent::new(KeyCode::End, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;6F");
    }

    #[test]
    fn legacy_alt_delete() {
        let key = KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[3;3~");
    }

    #[test]
    fn legacy_shift_f5() {
        let key = KeyEvent::new(KeyCode::F(5), KeyModifiers::SHIFT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[15;2~");
    }

    #[test]
    fn legacy_alt_char_still_esc_prefix() {
        let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1ba");
    }

    #[test]
    fn legacy_alt_shift_punctuation_uses_shifted_text() {
        let key = parse_terminal_key_sequence("\x1b[44:60;4u").unwrap();
        assert_eq!(encode_terminal_key(key, KeyboardProtocol::Legacy), b"\x1b<");
    }

    #[test]
    fn legacy_alt_backspace_sends_escape_delete() {
        let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b\x7f");
    }

    #[test]
    fn application_cursor_keys_use_ss3_sequences() {
        assert_eq!(encode_cursor_key(KeyCode::Up, true), b"\x1bOA");
        assert_eq!(encode_cursor_key(KeyCode::Down, true), b"\x1bOB");
    }

    #[test]
    fn normal_cursor_keys_use_csi_sequences() {
        assert_eq!(encode_cursor_key(KeyCode::Up, false), b"\x1b[A");
        assert_eq!(encode_cursor_key(KeyCode::Down, false), b"\x1b[B");
    }

    #[test]
    fn sgr_mouse_scroll_encodes_wheel_button_and_coordinates() {
        let encoded = encode_mouse_scroll(
            crossterm::event::MouseEventKind::ScrollDown,
            4,
            6,
            KeyModifiers::SHIFT,
            MouseProtocolEncoding::Sgr,
        )
        .expect("mouse scroll should encode");

        assert_eq!(encoded, b"\x1b[<69;5;7M");
    }

    #[test]
    fn sgr_mouse_release_keeps_button_code() {
        let encoded = encode_mouse_button(
            crossterm::event::MouseEventKind::Up(crossterm::event::MouseButton::Left),
            11,
            9,
            KeyModifiers::empty(),
            MouseProtocolEncoding::Sgr,
        )
        .expect("mouse release should encode");

        assert_eq!(encoded, b"\x1b[<0;12;10m");
    }

    #[test]
    fn kitty_shift_enter() {
        let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT);
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[13;2u"
        );
    }

    #[test]
    fn kitty_ctrl_shift_a() {
        let key = KeyEvent::new(
            KeyCode::Char('a'),
            KeyModifiers::CONTROL | KeyModifiers::SHIFT,
        );
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[97;6u"
        );
    }

    #[test]
    fn kitty_shift_uppercase_letter_sends_text() {
        let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT);
        assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"L");
    }

    #[test]
    fn kitty_shift_uppercase_letter_ignores_alternate_key_reporting_for_text() {
        let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT);
        assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 7 }), b"L");
    }

    #[test]
    fn kitty_shift_lowercase_letter_sends_uppercase_text() {
        let key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::SHIFT);
        assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"L");
    }

    #[test]
    fn kitty_alt_shift_uppercase_letter_uses_base_codepoint() {
        let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::ALT | KeyModifiers::SHIFT);
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[108;4u"
        );
    }

    #[test]
    fn kitty_ctrl_shift_uppercase_letter_uses_base_codepoint() {
        let key = KeyEvent::new(
            KeyCode::Char('L'),
            KeyModifiers::CONTROL | KeyModifiers::SHIFT,
        );
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[108;6u"
        );
    }

    #[test]
    fn legacy_shift_uppercase_letter_stays_uppercase() {
        let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT);
        assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"L");
    }

    #[test]
    fn kitty_alt_enter() {
        let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT);
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[13;3u"
        );
    }

    #[test]
    fn kitty_alt_backspace_uses_csi_u() {
        let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT);
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[127;3u"
        );
    }

    #[test]
    fn kitty_plain_ctrl_c_uses_csi_u() {
        let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[99;5u"
        );
    }

    #[test]
    fn kitty_plain_ctrl_c_includes_press_event_when_requested() {
        let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 3 }),
            b"\x1b[99;5:1u"
        );
    }

    #[test]
    fn kitty_unmodified_uses_legacy() {
        let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
        assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"a");
    }

    #[test]
    fn kitty_report_event_types_keeps_basic_compatibility_keys_legacy() {
        let cases = [
            (KeyCode::Enter, b"\r".as_slice()),
            (KeyCode::Tab, b"\t".as_slice()),
            (KeyCode::Backspace, b"\x7f".as_slice()),
        ];

        for (code, expected) in cases {
            let press = KeyEvent::new_with_kind(
                code,
                KeyModifiers::empty(),
                crossterm::event::KeyEventKind::Press,
            );
            assert_eq!(
                encode_key(press, KeyboardProtocol::Kitty { flags: 3 }),
                expected,
                "{code:?} press should stay legacy-compatible without REPORT_ALL_KEYS"
            );

            let repeat = KeyEvent::new_with_kind(
                code,
                KeyModifiers::empty(),
                crossterm::event::KeyEventKind::Repeat,
            );
            assert_eq!(
                encode_key(repeat, KeyboardProtocol::Kitty { flags: 3 }),
                expected,
                "{code:?} repeat should stay legacy-compatible without REPORT_ALL_KEYS"
            );

            let release = KeyEvent::new_with_kind(
                code,
                KeyModifiers::empty(),
                crossterm::event::KeyEventKind::Release,
            );
            assert_eq!(
                encode_key(release, KeyboardProtocol::Kitty { flags: 3 }),
                b"",
                "{code:?} release should not fall back to legacy bytes"
            );
        }
    }

    #[test]
    fn kitty_report_all_keys_encodes_basic_compatibility_keys_with_events() {
        let enter_press = KeyEvent::new_with_kind(
            KeyCode::Enter,
            KeyModifiers::empty(),
            crossterm::event::KeyEventKind::Press,
        );
        assert_eq!(
            encode_key(enter_press, KeyboardProtocol::Kitty { flags: 9 }),
            b"\x1b[13;1u"
        );

        let backspace_press = KeyEvent::new_with_kind(
            KeyCode::Backspace,
            KeyModifiers::empty(),
            crossterm::event::KeyEventKind::Press,
        );
        assert_eq!(
            encode_key(backspace_press, KeyboardProtocol::Kitty { flags: 11 }),
            b"\x1b[127;1:1u"
        );

        let backspace_release = KeyEvent::new_with_kind(
            KeyCode::Backspace,
            KeyModifiers::empty(),
            crossterm::event::KeyEventKind::Release,
        );
        assert_eq!(
            encode_key(backspace_release, KeyboardProtocol::Kitty { flags: 11 }),
            b"\x1b[127;1:3u"
        );
    }

    #[test]
    fn kitty_report_all_keys_encodes_printable_event_kinds() {
        for (kind, expected) in [
            (
                crossterm::event::KeyEventKind::Press,
                b"\x1b[106;1:1u".as_slice(),
            ),
            (
                crossterm::event::KeyEventKind::Repeat,
                b"\x1b[106;1:2u".as_slice(),
            ),
            (
                crossterm::event::KeyEventKind::Release,
                b"\x1b[106;1:3u".as_slice(),
            ),
        ] {
            let key = KeyEvent::new_with_kind(KeyCode::Char('j'), KeyModifiers::empty(), kind);
            assert_eq!(
                encode_key(key, KeyboardProtocol::Kitty { flags: 15 }),
                expected
            );
        }
    }

    #[test]
    fn kitty_report_associated_text_embeds_shifted_printables() {
        let cases = [
            (
                TerminalKey::new(KeyCode::Char('A'), KeyModifiers::SHIFT),
                b"\x1b[97;2;65u".as_slice(),
            ),
            (
                TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT)
                    .with_shifted_codepoint('!' as u32),
                b"\x1b[49;2;33u".as_slice(),
            ),
            (
                TerminalKey::new(KeyCode::Char(':'), KeyModifiers::SHIFT),
                b"\x1b[58;2;58u".as_slice(),
            ),
        ];

        for (key, expected) in cases {
            assert_eq!(
                encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 25 }),
                expected
            );
        }
    }

    #[test]
    fn kitty_associated_text_composes_with_alternates_and_events() {
        for (kind, expected) in [
            (
                crossterm::event::KeyEventKind::Press,
                b"\x1b[97:65;2:1;65u".as_slice(),
            ),
            (
                crossterm::event::KeyEventKind::Repeat,
                b"\x1b[97:65;2:2;65u".as_slice(),
            ),
            (
                crossterm::event::KeyEventKind::Release,
                b"\x1b[97:65;2:3u".as_slice(),
            ),
        ] {
            let key = TerminalKey::new(KeyCode::Char('A'), KeyModifiers::SHIFT).with_kind(kind);
            assert_eq!(
                encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 31 }),
                expected
            );
        }
    }

    #[test]
    fn kitty_printable_release_is_encoded_without_report_all() {
        let release = KeyEvent::new_with_kind(
            KeyCode::Char('j'),
            KeyModifiers::empty(),
            crossterm::event::KeyEventKind::Release,
        );
        assert_eq!(
            encode_key(release, KeyboardProtocol::Kitty { flags: 3 }),
            b"\x1b[106;1:3u"
        );

        let mut malformed_release = TerminalKey::from(release);
        malformed_release.generated_text = Some("j".to_owned());
        assert_eq!(
            encode_terminal_key(malformed_release, KeyboardProtocol::Kitty { flags: 3 }),
            b"\x1b[106;1:3u"
        );
    }

    #[test]
    fn kitty_shift_tab() {
        let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT);
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[9;2u"
        );
    }

    #[test]
    fn kitty_ctrl_shift_enter() {
        let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
            b"\x1b[13;6u"
        );
    }

    #[test]
    fn kitty_repeat_event_type_is_encoded_when_requested() {
        let key = KeyEvent::new_with_kind(
            KeyCode::Enter,
            KeyModifiers::SHIFT,
            crossterm::event::KeyEventKind::Repeat,
        );
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 3 }),
            b"\x1b[13;2:2u"
        );
    }

    #[test]
    fn kitty_shift_letter_release_uses_csi_u() {
        let key = KeyEvent::new_with_kind(
            KeyCode::Char('L'),
            KeyModifiers::SHIFT,
            crossterm::event::KeyEventKind::Release,
        );
        assert_eq!(
            encode_key(key, KeyboardProtocol::Kitty { flags: 7 }),
            b"\x1b[108:76;2:3u"
        );
    }

    #[test]
    fn kitty_shifted_punctuation_literals_send_text() {
        for ch in "!@#$%^&*()_+{}|:\"<>?~".chars() {
            let key = TerminalKey::new(KeyCode::Char(ch), KeyModifiers::SHIFT);
            let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
            assert_eq!(encoded, ch.to_string().into_bytes(), "ch={ch}");
        }
    }

    #[test]
    fn kitty_shifted_punctuation_release_does_not_emit_text() {
        let key = TerminalKey::new(KeyCode::Char('?'), KeyModifiers::SHIFT)
            .with_kind(crossterm::event::KeyEventKind::Release);
        assert_eq!(
            encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }),
            b"\x1b[63;2:3u"
        );
    }

    #[test]
    fn kitty_shifted_punctuation_does_not_infer_layout() {
        let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT);
        assert_eq!(
            encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }),
            b"\x1b[49;2:1u"
        );
    }

    #[test]
    fn kitty_modified_shifted_punctuation_stays_modified_key() {
        for (modifiers, expected) in [
            (
                KeyModifiers::CONTROL | KeyModifiers::SHIFT,
                b"\x1b[33;6:1u".as_slice(),
            ),
            (
                KeyModifiers::ALT | KeyModifiers::SHIFT,
                b"\x1b[33;4:1u".as_slice(),
            ),
            (
                KeyModifiers::SUPER | KeyModifiers::SHIFT,
                b"\x1b[33;10:1u".as_slice(),
            ),
        ] {
            let key = TerminalKey::new(KeyCode::Char('!'), modifiers);
            let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
            assert_eq!(encoded, expected, "modifiers={modifiers:?}");
        }
    }

    #[test]
    fn release_bytes_gated_on_report_event_types() {
        for code in [KeyCode::Enter, KeyCode::Backspace] {
            let release = KeyEvent::new_with_kind(
                code,
                KeyModifiers::empty(),
                crossterm::event::KeyEventKind::Release,
            );

            // Legacy and Kitty disambiguate-only (no REPORT_EVENT_TYPES) must not
            // emit a byte on release, otherwise Enter/Backspace double (issue #769).
            assert_eq!(encode_key(release, KeyboardProtocol::Legacy), b"");
            assert_eq!(
                encode_key(release, KeyboardProtocol::Kitty { flags: 1 }),
                b""
            );
        }

        let modified_release = KeyEvent::new_with_kind(
            KeyCode::Enter,
            KeyModifiers::CONTROL,
            crossterm::event::KeyEventKind::Release,
        );
        assert_eq!(
            encode_key(modified_release, KeyboardProtocol::Kitty { flags: 3 }),
            b"\x1b[13;5:3u"
        );
    }

    #[test]
    fn kitty_shifted_symbol_sends_text() {
        let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT)
            .with_shifted_codepoint('!' as u32);
        assert_eq!(
            encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }),
            b"!"
        );
    }

    #[test]
    fn legacy_modified_special_roundtrip_matrix() {
        let cases = [
            KeyEvent::new(KeyCode::Up, KeyModifiers::ALT),
            KeyEvent::new(KeyCode::Down, KeyModifiers::ALT),
            KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT),
            KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL),
            KeyEvent::new(KeyCode::Home, KeyModifiers::CONTROL),
            KeyEvent::new(KeyCode::End, KeyModifiers::CONTROL | KeyModifiers::SHIFT),
            KeyEvent::new(KeyCode::PageUp, KeyModifiers::ALT),
            KeyEvent::new(KeyCode::PageDown, KeyModifiers::CONTROL),
            KeyEvent::new(KeyCode::Insert, KeyModifiers::SHIFT),
            KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT),
        ];

        for key in cases {
            let encoded = encode_key(key, KeyboardProtocol::Legacy);
            let parsed =
                parse_terminal_key_sequence(std::str::from_utf8(&encoded).unwrap()).unwrap();
            assert_terminal_key_eq(parsed, key.code, key.modifiers, key.kind, None);
        }
    }

    #[test]
    fn kitty_shifted_symbol_prefers_text_over_roundtrip_key_identity() {
        let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT)
            .with_shifted_codepoint('!' as u32);
        let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
        assert_eq!(encoded, b"!");
    }

    #[test]
    fn legacy_basic_special_roundtrip_matrix() {
        let cases = [
            KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Tab, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Up, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Left, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Right, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Home, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::End, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::PageUp, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::PageDown, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Insert, KeyModifiers::empty()),
            KeyEvent::new(KeyCode::Delete, KeyModifiers::empty()),
        ];

        for key in cases {
            let encoded = encode_key(key, KeyboardProtocol::Legacy);
            let parsed =
                parse_terminal_key_sequence(std::str::from_utf8(&encoded).unwrap()).unwrap();
            assert_terminal_key_eq(parsed, key.code, key.modifiers, key.kind, None);
        }
    }

    #[test]
    fn kitty_shifted_symbol_pair_matrix_is_encoded_as_text() {
        let cases = [('1', '!'), ('/', '?'), ('[', '{')];

        for (base, shifted) in cases {
            let key = TerminalKey::new(KeyCode::Char(base), KeyModifiers::SHIFT)
                .with_shifted_codepoint(shifted as u32);
            let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
            assert_eq!(encoded, shifted.to_string().into_bytes(), "base={base}");
        }
    }

    #[test]
    fn chinese_char_encodes_as_utf8() {
        let key = TerminalKey::new(KeyCode::Char('中'), KeyModifiers::empty());
        let encoded = encode_terminal_key(key, KeyboardProtocol::Legacy);
        assert_eq!(encoded, "中".as_bytes());
    }

    #[test]
    fn chinese_char_with_kitty_protocol_encodes_as_utf8() {
        let key = TerminalKey::new(KeyCode::Char('文'), KeyModifiers::empty());
        let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
        assert_eq!(encoded, "文".as_bytes());
    }

    #[test]
    fn chinese_char_with_modifiers_falls_back_to_kitty_encoding() {
        let key = TerminalKey::new(KeyCode::Char('测'), KeyModifiers::ALT);
        let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
        assert!(!encoded.is_empty());
        assert_ne!(encoded, "测".as_bytes());
    }
}
