//! Editor keybindings (port of `src/ui/keybindings.ts`).
//!
//! An `EditorAction` vocabulary, the readline-derived default table, a `vim`
//! preset, and a layered resolver (`EditorKeybindingsManager`) with
//! preset → user-config → per-component-override precedence and conflict
//! detection. Default bindings are derived from GNU readline; the binding-table
//! insertion order (caret motion → deletion → text entry → completion popup →
//! clipboard → history → tool expansion → session mgmt) is load-bearing.

use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};

use crate::keys::{KeyId, matches_key, normalize_key_id};

/// Bindable line-editor behaviors (port of the 33-variant TS `EditorAction`
/// union). Each value is a logical action one or more physical keys may fire.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EditorAction {
    // --- Moving the caret around ---
    CursorUp,
    CursorDown,
    CursorLeft,
    CursorRight,
    CursorWordLeft,
    CursorWordRight,
    CursorLineStart,
    CursorLineEnd,
    PageUp,
    PageDown,
    // --- Removing characters and words ---
    DeleteCharBackward,
    DeleteCharForward,
    DeleteWordBackward,
    DeleteWordForward,
    DeleteToLineStart,
    DeleteToLineEnd,
    // --- Entering / committing text ---
    NewLine,
    Submit,
    Tab,
    // --- Driving the completion popup ---
    SelectUp,
    SelectDown,
    SelectPageUp,
    SelectPageDown,
    SelectConfirm,
    SelectCancel,
    // --- System clipboard ---
    Copy,
    // --- Emacs-style kill ring ---
    Yank,
    YankPop,
    // --- History / revert ---
    Undo,
    // --- Expanding inline tool results ---
    ExpandTools,
    // --- Operating on saved sessions ---
    ToggleSessionPath,
    ToggleSessionSort,
    RenameSession,
    DeleteSession,
    DeleteSessionNoninvasive,
}

/// Which keybinding preset to layer over the defaults.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeybindingPreset {
    #[default]
    Default,
    Vim,
}

/// A single binding entry: an action mapped to one or more keys.
///
/// Ports the TS `KeyId | KeyId[]` value shape — always normalized to a list.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Binding(pub Vec<KeyId>);

impl Binding {
    /// One key.
    pub fn one(key: impl Into<KeyId>) -> Self {
        Binding(vec![key.into()])
    }
    /// Several keys.
    pub fn many(keys: Vec<KeyId>) -> Self {
        Binding(keys)
    }

    /// Normalize each key id in this binding (port of TS `toKeyIdArray`, which
    /// maps every key through `normalizeKeyId`).
    fn normalized(&self) -> Vec<KeyId> {
        self.0.iter().map(|key| normalize_key_id(key)).collect()
    }
}

/// A partial action→keys map (port of TS `EditorKeybindingsConfig`).
///
/// Insertion order is preserved to match `flattenBindingBlocks`; later layers
/// overwrite earlier ones for the same action.
#[derive(Debug, Clone, Default)]
pub struct EditorKeybindingsConfig {
    entries: Vec<(EditorAction, Binding)>,
}

impl EditorKeybindingsConfig {
    /// Empty config.
    pub fn new() -> Self {
        Self::default()
    }
    /// Set (or overwrite) the binding for an action, preserving insertion order.
    ///
    /// Mirrors JS object-assignment semantics: re-setting an action updates its
    /// value in place (keeping its original position) rather than appending.
    pub fn set(&mut self, action: EditorAction, binding: Binding) -> &mut Self {
        if let Some(slot) = self.entries.iter_mut().find(|(a, _)| *a == action) {
            slot.1 = binding;
        } else {
            self.entries.push((action, binding));
        }
        self
    }
    /// Look up the binding for an action, if present.
    pub fn get(&self, action: EditorAction) -> Option<&Binding> {
        self.entries
            .iter()
            .find(|(a, _)| *a == action)
            .map(|(_, b)| b)
    }
    /// Iterate entries in insertion order.
    pub fn entries(&self) -> impl Iterator<Item = (EditorAction, &Binding)> {
        self.entries.iter().map(|(a, b)| (*a, b))
    }
}

/// A detected conflict: one key wired to more than one action
/// (port of TS `KeybindingConflict`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeybindingConflict {
    pub key: String,
    pub actions: Vec<EditorAction>,
}

/// The default keybinding layer applied before any user overrides
/// (port of TS `DEFAULT_EDITOR_KEYBINDINGS`). Returns a fully-populated config
/// in canonical insertion order.
///
/// Provenance: bindings derived from GNU readline are marked "[readline]" and
/// use readline's chord exactly. Bindings unique to the indus editor are marked
/// "[indus-original]". The category-block order is load-bearing — it is the
/// order callers (layer merging, conflict scanning) observe.
pub fn default_editor_keybindings() -> EditorKeybindingsConfig {
    let mut config = EditorKeybindingsConfig::new();

    // caret motion
    // [readline]   ctrl+a   -> beginning-of-line  (cursorLineStart)
    // [readline]   ctrl+e   -> end-of-line        (cursorLineEnd)
    // [readline]   alt+b/f  -> backward/forward-word
    // [indus-original]  arrow keys, home/end, pageUp/pageDown
    config
        .set(EditorAction::CursorUp, Binding::one("up"))
        .set(EditorAction::CursorDown, Binding::one("down"))
        .set(EditorAction::CursorLeft, Binding::one("left"))
        .set(EditorAction::CursorRight, Binding::one("right"))
        .set(
            EditorAction::CursorWordLeft,
            Binding::many(vec!["alt+left".into(), "ctrl+left".into()]),
        )
        .set(
            EditorAction::CursorWordRight,
            Binding::many(vec!["alt+right".into(), "ctrl+right".into()]),
        )
        .set(
            EditorAction::CursorLineStart,
            Binding::many(vec!["home".into(), "ctrl+a".into()]),
        )
        .set(
            EditorAction::CursorLineEnd,
            Binding::many(vec!["end".into(), "ctrl+e".into()]),
        )
        .set(EditorAction::PageUp, Binding::one("pageUp"))
        .set(EditorAction::PageDown, Binding::one("pageDown"));

    // deletion
    // [readline]   ctrl+w   -> unix-word-rubout    (deleteWordBackward)
    // [readline]   alt+d    -> kill-word           (deleteWordForward)
    // [readline]   ctrl+u   -> unix-line-discard   (deleteToLineStart)
    // [readline]   ctrl+k   -> kill-line           (deleteToLineEnd)
    // [indus-original]  backspace/delete + alt+backspace/alt+delete
    config
        .set(EditorAction::DeleteCharBackward, Binding::one("backspace"))
        .set(EditorAction::DeleteCharForward, Binding::one("delete"))
        .set(
            EditorAction::DeleteWordBackward,
            Binding::many(vec!["ctrl+w".into(), "alt+backspace".into()]),
        )
        .set(
            EditorAction::DeleteWordForward,
            Binding::many(vec!["alt+d".into(), "alt+delete".into()]),
        )
        .set(EditorAction::DeleteToLineStart, Binding::one("ctrl+u"))
        .set(EditorAction::DeleteToLineEnd, Binding::one("ctrl+k"));

    // text entry
    // [readline]   enter    -> accept-line         (submit)
    // [indus-original]  shift+enter inserts a literal newline; tab reserved
    config
        .set(EditorAction::NewLine, Binding::one("shift+enter"))
        .set(EditorAction::Submit, Binding::one("enter"))
        .set(EditorAction::Tab, Binding::one("tab"));

    // completion popup -- all [indus-original]; readline's completion is a
    // single-shot menu, not a navigable popup, so it has no analogue.
    config
        .set(EditorAction::SelectUp, Binding::one("up"))
        .set(EditorAction::SelectDown, Binding::one("down"))
        .set(EditorAction::SelectPageUp, Binding::one("pageUp"))
        .set(EditorAction::SelectPageDown, Binding::one("pageDown"))
        .set(EditorAction::SelectConfirm, Binding::one("enter"))
        .set(
            EditorAction::SelectCancel,
            Binding::many(vec!["escape".into(), "ctrl+c".into()]),
        );

    // clipboard + kill ring
    // [readline]   ctrl+y   -> yank                (yank)
    // [readline]   alt+y    -> yank-pop            (yankPop)
    // [indus-original]  ctrl+c -> copy selection to the OS clipboard
    config
        .set(EditorAction::Copy, Binding::one("ctrl+c"))
        .set(EditorAction::Yank, Binding::one("ctrl+y"))
        .set(EditorAction::YankPop, Binding::one("alt+y"));

    // history
    // [indus-original]  ctrl+- -> undo (readline binds ctrl+_; we accept the
    //                   more terminal-portable ctrl+- chord)
    config.set(EditorAction::Undo, Binding::one("ctrl+-"));

    // tool expansion -- [indus-original], no readline analogue
    config.set(EditorAction::ExpandTools, Binding::one("ctrl+o"));

    // session management -- all [indus-original], no readline analogue
    config
        .set(EditorAction::ToggleSessionPath, Binding::one("ctrl+p"))
        .set(EditorAction::ToggleSessionSort, Binding::one("ctrl+s"))
        .set(EditorAction::RenameSession, Binding::one("ctrl+r"))
        .set(EditorAction::DeleteSession, Binding::one("ctrl+d"))
        .set(
            EditorAction::DeleteSessionNoninvasive,
            Binding::one("ctrl+backspace"),
        );

    config
}

/// The `vim` preset overlay (port of TS `VIM_EDITOR_KEYBINDINGS`).
pub fn vim_editor_keybindings() -> EditorKeybindingsConfig {
    let mut config = EditorKeybindingsConfig::new();
    config
        .set(
            EditorAction::CursorUp,
            Binding::many(vec!["up".into(), "k".into()]),
        )
        .set(
            EditorAction::CursorDown,
            Binding::many(vec!["down".into(), "j".into()]),
        )
        .set(
            EditorAction::CursorLeft,
            Binding::many(vec!["left".into(), "h".into()]),
        )
        .set(
            EditorAction::CursorRight,
            Binding::many(vec!["right".into(), "l".into()]),
        )
        .set(
            EditorAction::DeleteCharBackward,
            Binding::many(vec!["backspace".into(), "x".into()]),
        );
    config
}

/// Lazily-built handle for the canonical default table, exposed under the same
/// name as the TS const for call-site parity.
#[allow(non_snake_case)]
pub fn DEFAULT_EDITOR_KEYBINDINGS() -> EditorKeybindingsConfig {
    default_editor_keybindings()
}

// =============================================================================
// Internal registry / resolver machinery
// =============================================================================

/// Action→keys map plus an insertion-ordered set of registered actions
/// (port of TS `KeybindingRegistry`). Keys are stored already normalized.
#[derive(Debug, Default)]
struct KeybindingRegistry {
    /// action → normalized key ids, in insertion (first-set) order.
    action_to_keys: Vec<(EditorAction, Vec<KeyId>)>,
    /// registered actions, in first-registration order.
    registered_actions: Vec<EditorAction>,
}

impl KeybindingRegistry {
    fn reset(&mut self) {
        self.action_to_keys.clear();
        self.registered_actions.clear();
    }

    fn set_action(&mut self, action: EditorAction, binding: &Binding) {
        let normalized = binding.normalized();
        if let Some(slot) = self.action_to_keys.iter_mut().find(|(a, _)| *a == action) {
            slot.1 = normalized;
        } else {
            self.action_to_keys.push((action, normalized));
        }
        if !self.registered_actions.contains(&action) {
            self.registered_actions.push(action);
        }
    }

    fn get_action_keys(&self, action: EditorAction) -> Vec<KeyId> {
        self.action_to_keys
            .iter()
            .find(|(a, _)| *a == action)
            .map(|(_, keys)| keys.clone())
            .unwrap_or_default()
    }

    fn entries(&self) -> impl Iterator<Item = (EditorAction, &Vec<KeyId>)> {
        self.action_to_keys.iter().map(|(a, keys)| (*a, keys))
    }

    fn actions(&self) -> Vec<EditorAction> {
        self.registered_actions.clone()
    }
}

/// Layers presets over the defaults, then the user config (port of TS
/// `PresetResolver`).
struct PresetResolver;

impl PresetResolver {
    fn layers_for(preset: KeybindingPreset) -> Vec<EditorKeybindingsConfig> {
        let mut layers = vec![default_editor_keybindings()];
        if preset == KeybindingPreset::Vim {
            layers.push(vim_editor_keybindings());
        }
        layers
    }

    fn merge_into(
        registry: &mut KeybindingRegistry,
        preset: KeybindingPreset,
        config: &EditorKeybindingsConfig,
    ) {
        for layer in Self::layers_for(preset) {
            Self::apply_layer(registry, &layer);
        }
        Self::apply_layer(registry, config);
    }

    fn apply_layer(registry: &mut KeybindingRegistry, layer: &EditorKeybindingsConfig) {
        for (action, binding) in layer.entries() {
            registry.set_action(action, binding);
        }
    }
}

/// Finds keys wired to more than one action (port of TS `ConflictScanner`).
struct ConflictScanner;

impl ConflictScanner {
    fn scan<'a>(
        entries: impl Iterator<Item = (EditorAction, &'a Vec<KeyId>)>,
    ) -> Vec<KeybindingConflict> {
        // key → ordered set of actions (Vec used as an insertion-ordered set,
        // mirroring the TS Set semantics).
        let mut order: Vec<String> = Vec::new();
        let mut key_to_actions: HashMap<String, Vec<EditorAction>> = HashMap::new();
        for (action, keys) in entries {
            for key in keys {
                let normalized = normalize_key_id(key);
                let bucket = key_to_actions.entry(normalized.clone()).or_insert_with(|| {
                    order.push(normalized.clone());
                    Vec::new()
                });
                if !bucket.contains(&action) {
                    bucket.push(action);
                }
            }
        }
        let mut conflicts = Vec::new();
        for key in order {
            let actions = &key_to_actions[&key];
            if actions.len() > 1 {
                conflicts.push(KeybindingConflict {
                    key,
                    actions: actions.clone(),
                });
            }
        }
        conflicts
    }
}

/// Resolves which keys are wired to which editor action, layering presets,
/// user config, and per-component overrides (port of TS
/// `EditorKeybindingsManager`).
pub struct EditorKeybindingsManager {
    registry: KeybindingRegistry,
    component_overrides: HashMap<String, EditorKeybindingsConfig>,
}

impl EditorKeybindingsManager {
    /// Build a manager from a user config + preset, merging over the defaults.
    pub fn new(config: EditorKeybindingsConfig, preset: KeybindingPreset) -> Self {
        let mut manager = Self {
            registry: KeybindingRegistry::default(),
            component_overrides: HashMap::new(),
        };
        manager.rebuild_registry(&config, preset);
        manager
    }

    fn rebuild_registry(&mut self, config: &EditorKeybindingsConfig, preset: KeybindingPreset) {
        self.registry.reset();
        PresetResolver::merge_into(&mut self.registry, preset, config);
    }

    fn resolve_keys(&self, action: EditorAction, component_id: Option<&str>) -> Vec<KeyId> {
        if let Some(id) = component_id
            && let Some(override_binding) = self
                .component_overrides
                .get(id)
                .and_then(|cfg| cfg.get(action))
        {
            return override_binding.normalized();
        }
        self.registry.get_action_keys(action)
    }

    /// Report whether the raw key data triggers the named action, honoring a
    /// per-component override scope when `component_id` is given.
    pub fn matches(&self, data: &str, action: EditorAction, component_id: Option<&str>) -> bool {
        for key in self.resolve_keys(action, component_id) {
            if matches_key(data, &key) {
                return true;
            }
        }
        false
    }

    /// List every key currently wired to the given action.
    pub fn get_keys(&self, action: EditorAction) -> Vec<KeyId> {
        self.registry.get_action_keys(action)
    }

    /// Register/override the keys for an action directly.
    pub fn register_action(&mut self, action: EditorAction, binding: Binding) {
        self.registry.set_action(action, &binding);
    }

    /// All actions that currently have a binding.
    pub fn get_registered_actions(&self) -> Vec<EditorAction> {
        self.registry.actions()
    }

    /// Swap in a fresh config (and preset), rebuilding the registry.
    pub fn set_config(&mut self, config: EditorKeybindingsConfig, preset: KeybindingPreset) {
        self.rebuild_registry(&config, preset);
    }

    /// Attach a per-component keybinding override scope.
    pub fn set_component_override(&mut self, component_id: &str, config: EditorKeybindingsConfig) {
        self.component_overrides
            .insert(component_id.to_string(), config);
    }

    /// Remove a per-component override scope.
    pub fn clear_component_override(&mut self, component_id: &str) {
        self.component_overrides.remove(component_id);
    }

    /// Find keys wired to more than one action.
    pub fn detect_conflicts(&self) -> Vec<KeybindingConflict> {
        ConflictScanner::scan(self.registry.entries())
    }
}

impl Default for EditorKeybindingsManager {
    fn default() -> Self {
        Self::new(EditorKeybindingsConfig::new(), KeybindingPreset::Default)
    }
}

// =============================================================================
// Process-wide singleton (port of TS getEditorKeybindings/setEditorKeybindings)
// =============================================================================

/// Holds the lazily-created, replaceable process-wide manager. The manager is
/// leaked to obtain the `&'static` reference the public accessor returns; this
/// mirrors the TS module-level `let globalEditorKeybindings` whose lifetime is
/// the whole process.
fn global_slot() -> &'static RwLock<Option<&'static EditorKeybindingsManager>> {
    static SLOT: OnceLock<RwLock<Option<&'static EditorKeybindingsManager>>> = OnceLock::new();
    SLOT.get_or_init(|| RwLock::new(None))
}

/// Fetch (lazily creating) the process-wide editor-keybindings singleton
/// (port of TS `getEditorKeybindings`).
pub fn get_editor_keybindings() -> &'static EditorKeybindingsManager {
    let slot = global_slot();
    if let Some(existing) = *slot.read().expect("keybindings singleton lock poisoned") {
        return existing;
    }
    let mut guard = slot.write().expect("keybindings singleton lock poisoned");
    // Re-check after acquiring the write lock in case of a race.
    if let Some(existing) = *guard {
        return existing;
    }
    let leaked: &'static EditorKeybindingsManager =
        Box::leak(Box::new(EditorKeybindingsManager::default()));
    *guard = Some(leaked);
    leaked
}

/// Replace the process-wide editor-keybindings singleton
/// (port of TS `setEditorKeybindings`).
pub fn set_editor_keybindings(manager: EditorKeybindingsManager) {
    let leaked: &'static EditorKeybindingsManager = Box::leak(Box::new(manager));
    *global_slot()
        .write()
        .expect("keybindings singleton lock poisoned") = Some(leaked);
}

#[cfg(test)]
mod tests {
    use super::*;

    // --- default table shape ---------------------------------------------

    #[test]
    fn default_table_has_every_action_in_canonical_order() {
        let config = default_editor_keybindings();
        let actions: Vec<EditorAction> = config.entries().map(|(a, _)| a).collect();
        let expected = vec![
            EditorAction::CursorUp,
            EditorAction::CursorDown,
            EditorAction::CursorLeft,
            EditorAction::CursorRight,
            EditorAction::CursorWordLeft,
            EditorAction::CursorWordRight,
            EditorAction::CursorLineStart,
            EditorAction::CursorLineEnd,
            EditorAction::PageUp,
            EditorAction::PageDown,
            EditorAction::DeleteCharBackward,
            EditorAction::DeleteCharForward,
            EditorAction::DeleteWordBackward,
            EditorAction::DeleteWordForward,
            EditorAction::DeleteToLineStart,
            EditorAction::DeleteToLineEnd,
            EditorAction::NewLine,
            EditorAction::Submit,
            EditorAction::Tab,
            EditorAction::SelectUp,
            EditorAction::SelectDown,
            EditorAction::SelectPageUp,
            EditorAction::SelectPageDown,
            EditorAction::SelectConfirm,
            EditorAction::SelectCancel,
            EditorAction::Copy,
            EditorAction::Yank,
            EditorAction::YankPop,
            EditorAction::Undo,
            EditorAction::ExpandTools,
            EditorAction::ToggleSessionPath,
            EditorAction::ToggleSessionSort,
            EditorAction::RenameSession,
            EditorAction::DeleteSession,
            EditorAction::DeleteSessionNoninvasive,
        ];
        assert_eq!(actions, expected);
        // All 35 distinct EditorAction variants are present.
        assert_eq!(actions.len(), 35);
    }

    #[test]
    fn default_table_single_and_multi_key_bindings() {
        let config = default_editor_keybindings();
        assert_eq!(
            config.get(EditorAction::Submit).unwrap().0,
            vec!["enter".to_string()]
        );
        assert_eq!(
            config.get(EditorAction::CursorWordLeft).unwrap().0,
            vec!["alt+left".to_string(), "ctrl+left".to_string()]
        );
        assert_eq!(
            config.get(EditorAction::SelectCancel).unwrap().0,
            vec!["escape".to_string(), "ctrl+c".to_string()]
        );
        assert_eq!(
            config.get(EditorAction::Undo).unwrap().0,
            vec!["ctrl+-".to_string()]
        );
    }

    // --- manager get_keys / registration ---------------------------------

    #[test]
    fn manager_exposes_default_keys() {
        let manager = EditorKeybindingsManager::default();
        assert_eq!(
            manager.get_keys(EditorAction::CursorLineStart),
            vec!["home".to_string(), "ctrl+a".to_string()]
        );
        assert_eq!(
            manager.get_keys(EditorAction::Copy),
            vec!["ctrl+c".to_string()]
        );
    }

    #[test]
    fn manager_registers_all_default_actions() {
        let manager = EditorKeybindingsManager::default();
        let registered = manager.get_registered_actions();
        // Every default action is registered, in declaration order.
        assert_eq!(registered.len(), 35);
        assert_eq!(registered.first(), Some(&EditorAction::CursorUp));
        assert_eq!(
            registered.last(),
            Some(&EditorAction::DeleteSessionNoninvasive)
        );
    }

    #[test]
    fn register_action_overrides_existing_keys() {
        let mut manager = EditorKeybindingsManager::default();
        manager.register_action(EditorAction::Submit, Binding::one("ctrl+enter"));
        assert_eq!(
            manager.get_keys(EditorAction::Submit),
            vec!["ctrl+enter".to_string()]
        );
        // It does not double-register an already-known action.
        let count = manager
            .get_registered_actions()
            .iter()
            .filter(|a| **a == EditorAction::Submit)
            .count();
        assert_eq!(count, 1);
    }

    // --- user config layering --------------------------------------------

    #[test]
    fn user_config_overrides_defaults() {
        let mut user = EditorKeybindingsConfig::new();
        user.set(EditorAction::Submit, Binding::one("ctrl+j"));
        let manager = EditorKeybindingsManager::new(user, KeybindingPreset::Default);
        assert_eq!(
            manager.get_keys(EditorAction::Submit),
            vec!["ctrl+j".to_string()]
        );
        // An action the user didn't touch keeps its default.
        assert_eq!(
            manager.get_keys(EditorAction::Copy),
            vec!["ctrl+c".to_string()]
        );
    }

    #[test]
    fn config_set_overwrites_in_place_preserving_order() {
        let mut config = EditorKeybindingsConfig::new();
        config.set(EditorAction::CursorUp, Binding::one("up"));
        config.set(EditorAction::CursorDown, Binding::one("down"));
        config.set(EditorAction::CursorUp, Binding::one("k"));
        let entries: Vec<_> = config.entries().map(|(a, _)| a).collect();
        assert_eq!(
            entries,
            vec![EditorAction::CursorUp, EditorAction::CursorDown]
        );
        assert_eq!(
            config.get(EditorAction::CursorUp).unwrap().0,
            vec!["k".to_string()]
        );
    }

    // --- vim preset ------------------------------------------------------

    #[test]
    fn vim_preset_layers_over_defaults() {
        let manager =
            EditorKeybindingsManager::new(EditorKeybindingsConfig::new(), KeybindingPreset::Vim);
        // vim overlay adds the hjkl alternates.
        assert_eq!(
            manager.get_keys(EditorAction::CursorUp),
            vec!["up".to_string(), "k".to_string()]
        );
        assert_eq!(
            manager.get_keys(EditorAction::CursorLeft),
            vec!["left".to_string(), "h".to_string()]
        );
        assert_eq!(
            manager.get_keys(EditorAction::DeleteCharBackward),
            vec!["backspace".to_string(), "x".to_string()]
        );
        // An action vim doesn't override keeps the default.
        assert_eq!(
            manager.get_keys(EditorAction::Submit),
            vec!["enter".to_string()]
        );
    }

    #[test]
    fn vim_overlay_then_user_config_precedence() {
        let mut user = EditorKeybindingsConfig::new();
        user.set(EditorAction::CursorUp, Binding::one("ctrl+p"));
        let manager = EditorKeybindingsManager::new(user, KeybindingPreset::Vim);
        // user config wins over both default and vim overlay.
        assert_eq!(
            manager.get_keys(EditorAction::CursorUp),
            vec!["ctrl+p".to_string()]
        );
    }

    // --- set_config rebuild ----------------------------------------------

    #[test]
    fn set_config_rebuilds_registry() {
        let mut manager = EditorKeybindingsManager::default();
        let mut next = EditorKeybindingsConfig::new();
        next.set(EditorAction::Submit, Binding::one("ctrl+s"));
        manager.set_config(next, KeybindingPreset::Vim);
        assert_eq!(
            manager.get_keys(EditorAction::Submit),
            vec!["ctrl+s".to_string()]
        );
        // vim overlay is now active.
        assert_eq!(
            manager.get_keys(EditorAction::CursorUp),
            vec!["up".to_string(), "k".to_string()]
        );
    }

    // --- matches / chord resolution --------------------------------------

    #[test]
    fn matches_resolves_via_keys_module() {
        let manager = EditorKeybindingsManager::default();
        // enter -> submit
        assert!(manager.matches("\r", EditorAction::Submit, None));
        // a stray byte does not trigger submit
        assert!(!manager.matches("z", EditorAction::Submit, None));
    }

    #[test]
    fn matches_any_of_multiple_keys() {
        let manager = EditorKeybindingsManager::default();
        // SelectCancel is bound to both escape and ctrl+c.
        assert!(manager.matches("\u{1b}", EditorAction::SelectCancel, None));
        assert!(manager.matches("\u{03}", EditorAction::SelectCancel, None));
    }

    // --- component overrides ---------------------------------------------

    #[test]
    fn component_override_scopes_resolution() {
        let mut manager = EditorKeybindingsManager::default();
        let mut override_cfg = EditorKeybindingsConfig::new();
        override_cfg.set(EditorAction::Submit, Binding::one("ctrl+j"));
        manager.set_component_override("popup", override_cfg);

        // With the component scope, submit resolves to the override (ctrl+j),
        // and the default (enter) no longer matches inside that scope.
        assert!(manager.matches("\n", EditorAction::Submit, Some("popup")));
        assert!(!manager.matches("\r", EditorAction::Submit, Some("popup")));

        // Without the scope, the global default still applies.
        assert!(manager.matches("\r", EditorAction::Submit, None));

        // An unknown component id falls back to the registry.
        assert!(manager.matches("\r", EditorAction::Submit, Some("other")));
    }

    #[test]
    fn component_override_falls_through_for_unbound_action() {
        let mut manager = EditorKeybindingsManager::default();
        let mut override_cfg = EditorKeybindingsConfig::new();
        override_cfg.set(EditorAction::Submit, Binding::one("ctrl+j"));
        manager.set_component_override("popup", override_cfg);
        // Copy is not in the override -> falls back to the registry default.
        assert!(manager.matches("\u{03}", EditorAction::Copy, Some("popup")));
    }

    #[test]
    fn clear_component_override_restores_global() {
        let mut manager = EditorKeybindingsManager::default();
        let mut override_cfg = EditorKeybindingsConfig::new();
        override_cfg.set(EditorAction::Submit, Binding::one("ctrl+j"));
        manager.set_component_override("popup", override_cfg);
        manager.clear_component_override("popup");
        // After clearing, the popup scope sees the global default again:
        // submit -> enter, so the enter chord matches and an unrelated byte
        // does not. (Note: enter also matches "\n" when the kitty protocol is
        // inactive, so we probe with a clearly-unbound byte for the negative.)
        assert!(manager.matches("\r", EditorAction::Submit, Some("popup")));
        assert!(!manager.matches("z", EditorAction::Submit, Some("popup")));
    }

    // --- conflict detection ----------------------------------------------

    #[test]
    fn detect_conflicts_finds_shared_keys() {
        let manager = EditorKeybindingsManager::default();
        let conflicts = manager.detect_conflicts();
        let by_key = |k: &str| conflicts.iter().find(|c| c.key == k);

        // "up" is wired to both cursorUp and selectUp in the defaults.
        let up = by_key("up").expect("up should conflict");
        assert!(up.actions.contains(&EditorAction::CursorUp));
        assert!(up.actions.contains(&EditorAction::SelectUp));

        // "enter" is wired to submit and selectConfirm.
        let enter = by_key("enter").expect("enter should conflict");
        assert!(enter.actions.contains(&EditorAction::Submit));
        assert!(enter.actions.contains(&EditorAction::SelectConfirm));

        // "ctrl+c" is wired to copy and selectCancel.
        let ctrl_c = by_key("ctrl+c").expect("ctrl+c should conflict");
        assert!(ctrl_c.actions.contains(&EditorAction::Copy));
        assert!(ctrl_c.actions.contains(&EditorAction::SelectCancel));
    }

    #[test]
    fn detect_conflicts_ignores_unique_keys() {
        let manager = EditorKeybindingsManager::default();
        let conflicts = manager.detect_conflicts();
        // ctrl+o (expandTools) is unique -> never reported as a conflict.
        assert!(conflicts.iter().all(|c| c.key != "ctrl+o"));
    }

    #[test]
    fn detect_conflicts_normalizes_before_comparing() {
        // Two actions bound to the same chord written differently
        // (modifier-order / casing) should still collide once normalized.
        let mut user = EditorKeybindingsConfig::new();
        user.set(EditorAction::ExpandTools, Binding::one("Ctrl+Shift+X"));
        user.set(EditorAction::Undo, Binding::one("shift+ctrl+x"));
        let manager = EditorKeybindingsManager::new(user, KeybindingPreset::Default);
        let conflicts = manager.detect_conflicts();
        let shared = conflicts
            .iter()
            .find(|c| c.key == "ctrl+shift+x")
            .expect("normalized chord should collide");
        assert!(shared.actions.contains(&EditorAction::ExpandTools));
        assert!(shared.actions.contains(&EditorAction::Undo));
    }

    // --- binding constructors --------------------------------------------

    #[test]
    fn binding_one_and_many() {
        assert_eq!(Binding::one("enter").0, vec!["enter".to_string()]);
        assert_eq!(
            Binding::many(vec!["a".into(), "b".into()]).0,
            vec!["a".to_string(), "b".to_string()]
        );
    }

    // --- singleton -------------------------------------------------------

    #[test]
    fn singleton_replaceable() {
        // Default singleton resolves enter -> submit.
        assert!(get_editor_keybindings().matches("\r", EditorAction::Submit, None));

        let mut user = EditorKeybindingsConfig::new();
        user.set(EditorAction::Submit, Binding::one("ctrl+j"));
        set_editor_keybindings(EditorKeybindingsManager::new(
            user,
            KeybindingPreset::Default,
        ));

        let manager = get_editor_keybindings();
        assert!(manager.matches("\n", EditorAction::Submit, None));
        assert!(!manager.matches("\r", EditorAction::Submit, None));
    }
}
