//! Keybinding definitions for Tij
//!
//! All keybindings are defined here for easy modification and future config file support.

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::style::Color;

use crate::app::View;
use crate::ui::views::InputMode;

// =============================================================================
// Key detection helpers (for modifier keys)
// =============================================================================

/// Check if key is Ctrl+L (refresh)
/// Note: Accept both 'l' and 'L' for terminal compatibility
pub fn is_refresh_key(key: &KeyEvent) -> bool {
    matches!(key.code, KeyCode::Char('l') | KeyCode::Char('L'))
        && key.modifiers.contains(KeyModifiers::CONTROL)
}

// =============================================================================
// Global keys (available in all views)
// =============================================================================

/// Quit application or go back
pub const QUIT: KeyCode = KeyCode::Char('q');

/// Show help
pub const HELP: KeyCode = KeyCode::Char('?');

/// Switch between views
pub const TAB: KeyCode = KeyCode::Tab;

/// Alternative quit
pub const ESC: KeyCode = KeyCode::Esc;

// =============================================================================
// Navigation keys
// =============================================================================

/// Move cursor up (vim style)
pub const MOVE_UP: KeyCode = KeyCode::Char('k');

/// Move cursor up (arrow key)
pub const MOVE_UP_ARROW: KeyCode = KeyCode::Up;

/// Move cursor down (vim style)
pub const MOVE_DOWN: KeyCode = KeyCode::Char('j');

/// Move cursor down (arrow key)
pub const MOVE_DOWN_ARROW: KeyCode = KeyCode::Down;

/// Go to top
pub const GO_TOP: KeyCode = KeyCode::Char('g');

/// Go to bottom
pub const GO_BOTTOM: KeyCode = KeyCode::Char('G');

/// Check if key is move up (k or ↑)
pub fn is_move_up(code: KeyCode) -> bool {
    matches!(code, MOVE_UP | MOVE_UP_ARROW)
}

/// Check if key is move down (j or ↓)
pub fn is_move_down(code: KeyCode) -> bool {
    matches!(code, MOVE_DOWN | MOVE_DOWN_ARROW)
}

// =============================================================================
// Input keys (used in input modes)
// =============================================================================

/// Submit input (Enter in input mode)
pub const SUBMIT: KeyCode = KeyCode::Enter;

// =============================================================================
// Log View keys
// =============================================================================

/// Open diff view for selected commit
pub const OPEN_DIFF: KeyCode = KeyCode::Enter;

/// Edit change description
pub const DESCRIBE: KeyCode = KeyCode::Char('d');

/// Edit (set working-copy to selected change)
pub const EDIT: KeyCode = KeyCode::Char('e');

/// Create new change
pub const NEW_CHANGE: KeyCode = KeyCode::Char('c');

/// Commit changes (Status View, uppercase)
pub const COMMIT: KeyCode = KeyCode::Char('C');

/// Squash change (select destination, Log View, uppercase)
pub const SQUASH: KeyCode = KeyCode::Char('S');

/// Abandon change (Log View, uppercase)
pub const ABANDON: KeyCode = KeyCode::Char('A');

/// Split change (Log View, opens external diff editor)
pub const SPLIT: KeyCode = KeyCode::Char('x');

/// Delete/forget key shared by all object views (Bookmark, Tag, Workspace).
/// Lowercase `d` — matches the normalized verb: delete (tag/bookmark) or forget (workspace).
pub const OBJECT_DELETE: KeyCode = KeyCode::Char('d');

/// Create/add key shared by all object views (Bookmark, Tag, Workspace).
/// Lowercase `n` — normalized verb: new/create/add.
pub const OBJECT_NEW: KeyCode = KeyCode::Char('n');

/// Rebase change (Log View, uppercase)
pub const REBASE: KeyCode = KeyCode::Char('R');

/// Absorb changes into ancestors (Log View, uppercase)
pub const ABSORB: KeyCode = KeyCode::Char('B');

/// Create new change from selected revision (Log View, uppercase)
/// Note: This is different from COMMIT ('C' in Status View)
pub const NEW_FROM: KeyCode = KeyCode::Char('C');

/// Show file annotation/blame (Status/Diff View)
pub const ANNOTATE: KeyCode = KeyCode::Char('a');

/// Open resolve list view for conflicts (Log View, uppercase)
pub const RESOLVE_LIST: KeyCode = KeyCode::Char('X');

/// Fetch from remote (Log View, uppercase for remote ops)
pub const FETCH: KeyCode = KeyCode::Char('F');

/// Push to remote (Log View, uppercase for remote ops)
pub const PUSH: KeyCode = KeyCode::Char('P');

/// Track remote bookmarks (Bookmark View normal-mode `t`; Log View uppercase remote ops use PUSH/FETCH).
pub const TRACK: KeyCode = KeyCode::Char('t');

/// Compare two revisions (Log View)
pub const COMPARE: KeyCode = KeyCode::Char('=');

/// Interdiff two revisions (Log View)
pub const INTERDIFF: KeyCode = KeyCode::Char('I');

/// Bisect — find bad revision (Log View)
pub const BISECT: KeyCode = KeyCode::Char('W');

/// Metaedit — edit metadata (author, change-id, timestamp) (Log View)
pub const METAEDIT: KeyCode = KeyCode::Char('v');

/// Open Tag View (Log View)
pub const TAG_VIEW: KeyCode = KeyCode::Char('t');

/// Open Workspace View (Log View)
pub const WORKSPACE_VIEW: KeyCode = KeyCode::Char('w');

/// Open Bookmark View (Log View)
pub const BOOKMARK_VIEW: KeyCode = KeyCode::Char('b');

/// Open Command History View (Log View)
pub const COMMAND_HISTORY: KeyCode = KeyCode::Char('H');

/// Toggle preview pane (Log View)
pub const PREVIEW: KeyCode = KeyCode::Char('p');

/// Untrack remote bookmark (Bookmark View) — Shift+T is the inverse of `t` track.
pub const BOOKMARK_UNTRACK: KeyCode = KeyCode::Char('T');

/// Shared rename verb for object views (Bookmark, Workspace).
pub const OBJECT_RENAME: KeyCode = KeyCode::Char('r');

/// Forget bookmark (Bookmark View)
pub const BOOKMARK_FORGET: KeyCode = KeyCode::Char('f');

/// Move bookmark to @ (Bookmark View)
pub const BOOKMARK_MOVE: KeyCode = KeyCode::Char('m');

/// Command palette trigger (Log View Normal mode): `:` opens a searchable
/// list of low-frequency commands. Distinct from the `:` used inside the
/// rebase-mode-select sub-mode (different handler / mode).
pub const COMMAND_PALETTE: KeyCode = KeyCode::Char(':');

/// Move @ to next child (Log View)
pub const NEXT_CHANGE: KeyCode = KeyCode::Char(']');

/// Move @ to previous parent (Log View)
pub const PREV_CHANGE: KeyCode = KeyCode::Char('[');

/// Toggle reversed display order (Log View)
pub const LOG_REVERSE: KeyCode = KeyCode::Char('V');

/// Duplicate change (Log View)
pub const DUPLICATE: KeyCode = KeyCode::Char('Y');

/// Open external diff editor (Log/Status View)
pub const DIFFEDIT: KeyCode = KeyCode::Char('E');

/// Restore file (Status View, lowercase)
pub const RESTORE_FILE: KeyCode = KeyCode::Char('r');

/// Restore all files (Status View, uppercase)
pub const RESTORE_ALL: KeyCode = KeyCode::Char('R');

/// Open evolution log (Log View)
pub const EVOLOG: KeyCode = KeyCode::Char('L');

/// Revert a change (Log View, creates reverse-diff commit)
pub const REVERT: KeyCode = KeyCode::Char('Z');

/// Simplify parents (remove redundant parent edges)
pub const SIMPLIFY_PARENTS: KeyCode = KeyCode::Char('i');

/// Fix (apply configured code formatters, Log View Normal mode only)
pub const FIX: KeyCode = KeyCode::Char('f');

/// Parallelize commits (convert linear chain to siblings)
pub const PARALLELIZE: KeyCode = KeyCode::Char('|');

/// Arrange — interactively arrange the commit graph (Log View, jj 0.40+)
pub const ARRANGE: KeyCode = KeyCode::Char('O');

// Note: Blame's "jump to Log" key is matched directly as Char('J') in
// blame/input.rs (handles terminal SHIFT-modifier variance), so no constant here.

/// Jump to first conflict file (Status View)
pub const JUMP_CONFLICT: KeyCode = KeyCode::Char('f');

/// Open text search input (for n/N navigation)
pub const SEARCH_INPUT: KeyCode = KeyCode::Char('/');

/// Open revset input (for jj filtering)
pub const REVSET_INPUT: KeyCode = KeyCode::Char('r');

/// Next search result
pub const SEARCH_NEXT: KeyCode = KeyCode::Char('n');

/// Previous search result
pub const SEARCH_PREV: KeyCode = KeyCode::Char('N');

// =============================================================================
// Diff View keys
// =============================================================================

/// Next file in diff
pub const NEXT_FILE: KeyCode = KeyCode::Char(']');

/// Previous file in diff
pub const PREV_FILE: KeyCode = KeyCode::Char('[');

/// Half page down
pub const HALF_PAGE_DOWN: KeyCode = KeyCode::Char('d');

/// Half page up
pub const HALF_PAGE_UP: KeyCode = KeyCode::Char('u');

/// Copy diff to clipboard (full: jj show output)
pub const YANK: KeyCode = KeyCode::Char('y');

/// Copy diff to clipboard (diff only: jj diff output)
pub const YANK_DIFF: KeyCode = KeyCode::Char('Y');

/// Export diff to file
pub const WRITE_FILE: KeyCode = KeyCode::Char('w');

/// Cycle diff display format (color-words → stat → git)
pub const DIFF_FORMAT_CYCLE: KeyCode = KeyCode::Char('m');

/// Toggle full description in DiffView header (compact ↔ full)
pub const DIFF_DESC_TOGGLE: KeyCode = KeyCode::Char('t');

// =============================================================================
// Undo/Redo keys
// =============================================================================

/// Undo last operation
pub const UNDO: KeyCode = KeyCode::Char('u');

// Note: Redo is Ctrl+R, handled via KeyModifiers in input.rs

// =============================================================================
// View switching keys
// =============================================================================

/// Go to status view
pub const STATUS_VIEW: KeyCode = KeyCode::Char('s');

/// Open operation history view
pub const OPERATION_HISTORY: KeyCode = KeyCode::Char('o');

// =============================================================================
// Help text generation
// =============================================================================

/// Key binding entry for help display
pub struct KeyBindEntry {
    pub key: &'static str,
    pub description: &'static str,
}

/// Global key bindings for help display
pub const GLOBAL_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "q",
        description: "Quit / Back",
    },
    KeyBindEntry {
        key: "?",
        description: "Help",
    },
    KeyBindEntry {
        key: "Tab",
        description: "Switch view",
    },
    KeyBindEntry {
        key: "Esc",
        description: "Back to previous",
    },
    KeyBindEntry {
        key: "Ctrl+l",
        description: "Refresh",
    },
];

/// Navigation key bindings for help display
pub const NAV_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
];

/// Log view key bindings for help display
pub const LOG_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: ":",
        description: "Command palette (search low-frequency commands)",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Show diff",
    },
    KeyBindEntry {
        key: "d",
        description: "Describe (1-line quick edit; opens editor for multi-line)",
    },
    KeyBindEntry {
        key: "Ctrl+e",
        description: "Describe in external editor (full text)",
    },
    KeyBindEntry {
        key: "e",
        description: "Edit change",
    },
    KeyBindEntry {
        key: "c",
        description: "Create new change",
    },
    KeyBindEntry {
        key: "C",
        description: "New from selected (Log)",
    },
    KeyBindEntry {
        key: "/",
        description: "Search in list",
    },
    KeyBindEntry {
        key: "r",
        description: "Revset filter",
    },
    KeyBindEntry {
        key: "n/N",
        description: "Next/prev search",
    },
    KeyBindEntry {
        key: "s",
        description: "Status view",
    },
    KeyBindEntry {
        key: "o",
        description: "Operation history",
    },
    KeyBindEntry {
        key: "u",
        description: "Undo",
    },
    KeyBindEntry {
        key: "Ctrl+r",
        description: "Redo",
    },
    KeyBindEntry {
        key: "S",
        description: "Squash (select target)",
    },
    KeyBindEntry {
        key: "A",
        description: "Abandon change",
    },
    KeyBindEntry {
        key: "x",
        description: "Split change",
    },
    KeyBindEntry {
        key: "b",
        description: "Bookmark view",
    },
    KeyBindEntry {
        key: "R",
        description: "Rebase (r/s/b/A/B)",
    },
    KeyBindEntry {
        key: "B",
        description: "Absorb changes",
    },
    KeyBindEntry {
        key: "X",
        description: "Resolve conflicts",
    },
    KeyBindEntry {
        key: "F",
        description: "Git fetch",
    },
    KeyBindEntry {
        key: "P",
        description: "Git push",
    },
    KeyBindEntry {
        key: "t",
        description: "Tag view",
    },
    KeyBindEntry {
        key: "w",
        description: "Workspace view",
    },
    KeyBindEntry {
        key: "p",
        description: "Toggle preview pane",
    },
    KeyBindEntry {
        key: "]/[",
        description: "Move @ to next/prev",
    },
    KeyBindEntry {
        key: "V",
        description: "Toggle reversed order",
    },
    KeyBindEntry {
        key: "L",
        description: "Evolution log (change history)",
    },
    KeyBindEntry {
        key: "H",
        description: "Command history",
    },
];

/// Input mode key bindings (describe, search, revset, bookmark)
pub const INPUT_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "Enter",
        description: "Submit input",
    },
    KeyBindEntry {
        key: "Esc",
        description: "Cancel input",
    },
    KeyBindEntry {
        key: "Backspace",
        description: "Delete character",
    },
];

/// Diff view key bindings for help display
pub const DIFF_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "m",
        description: "Cycle diff display mode (color-words/stat/git)",
    },
    KeyBindEntry {
        key: "t",
        description: "Toggle full description (expand/collapse header)",
    },
    KeyBindEntry {
        key: "j/k",
        description: "Scroll down/up",
    },
    KeyBindEntry {
        key: "d/u",
        description: "Half page down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
    KeyBindEntry {
        key: "[/]",
        description: "Prev/next file",
    },
    KeyBindEntry {
        key: "a",
        description: "Show file blame",
    },
    KeyBindEntry {
        key: "y",
        description: "Copy to clipboard (full: jj show)",
    },
    KeyBindEntry {
        key: "Y",
        description: "Copy to clipboard (diff only: jj diff)",
    },
    KeyBindEntry {
        key: "w",
        description: "Export to .patch file",
    },
    KeyBindEntry {
        key: "q",
        description: "Back to log",
    },
];

// =============================================================================
// Status bar hints
// =============================================================================

/// Key hint for status bar display (colored badges)
#[derive(Clone, Copy)]
pub struct KeyHint {
    pub key: &'static str,
    pub label: &'static str,
    pub color: Color,
}

// Individual KeyHint constants (used by builder functions)
pub const HINT_HELP: KeyHint = KeyHint {
    key: "?",
    label: "Help",
    color: Color::Cyan,
};
pub const HINT_DESC: KeyHint = KeyHint {
    key: "d",
    label: "Desc",
    color: Color::Green,
};
pub const HINT_EDITOR: KeyHint = KeyHint {
    key: "^E",
    label: "Editor",
    color: Color::Green,
};
pub const HINT_EDIT: KeyHint = KeyHint {
    key: "e",
    label: "Edit",
    color: Color::Yellow,
};
pub const HINT_NEW: KeyHint = KeyHint {
    key: "c",
    label: "New",
    color: Color::Magenta,
};
pub const HINT_NEW_AT: KeyHint = KeyHint {
    key: "C",
    label: "New@",
    color: Color::Magenta,
};
pub const HINT_SQUASH: KeyHint = KeyHint {
    key: "S",
    label: "Squash",
    color: Color::Red,
};
pub const HINT_ABANDON: KeyHint = KeyHint {
    key: "A",
    label: "Abandon",
    color: Color::Red,
};
pub const HINT_SPLIT: KeyHint = KeyHint {
    key: "x",
    label: "Split",
    color: Color::Yellow,
};
pub const HINT_BOOKMARK: KeyHint = KeyHint {
    key: "b",
    label: "Bookmark",
    color: Color::Cyan,
};
/// Command palette hint (Log Normal only — palette is Log View scoped).
pub const HINT_PALETTE: KeyHint = KeyHint {
    key: ":",
    label: "Palette",
    color: Color::Magenta,
};
pub const HINT_DEL_BKM: KeyHint = KeyHint {
    key: "d",
    label: "Delete",
    color: Color::Red,
};
pub const HINT_REBASE: KeyHint = KeyHint {
    key: "R",
    label: "Rebase",
    color: Color::Yellow,
};
pub const HINT_ABSORB: KeyHint = KeyHint {
    key: "B",
    label: "Absorb",
    color: Color::Magenta,
};
pub const HINT_RESOLVE: KeyHint = KeyHint {
    key: "X",
    label: "Resolve",
    color: Color::Red,
};
pub const HINT_FETCH: KeyHint = KeyHint {
    key: "F",
    label: "Fetch",
    color: Color::Blue,
};
pub const HINT_PUSH: KeyHint = KeyHint {
    key: "P",
    label: "Push",
    color: Color::Blue,
};
pub const HINT_TRACK: KeyHint = KeyHint {
    key: "t",
    label: "Track",
    color: Color::Cyan,
};
pub const HINT_UNDO: KeyHint = KeyHint {
    key: "u",
    label: "Undo",
    color: Color::Green,
};
// Caret notation ("^R"/"^L") for space-constrained status-bar hints;
// the `*_KEYS` help arrays use the long form ("Ctrl+r"/"Ctrl+l").
pub const HINT_REDO: KeyHint = KeyHint {
    key: "^R",
    label: "Redo",
    color: Color::Green,
};
pub const HINT_REFRESH: KeyHint = KeyHint {
    key: "^L",
    label: "Refresh",
    color: Color::Blue,
};
pub const HINT_SWITCH: KeyHint = KeyHint {
    key: "Tab",
    label: "Switch",
    color: Color::Blue,
};
pub const HINT_QUIT: KeyHint = KeyHint {
    key: "q",
    label: "Quit",
    color: Color::Red,
};
pub const HINT_BACK: KeyHint = KeyHint {
    key: "q",
    label: "Back",
    color: Color::Red,
};
// Navigation/action hints for special modes
pub const HINT_NAV: KeyHint = KeyHint {
    key: "j/k",
    label: "Navigate",
    color: Color::Blue,
};
pub const HINT_SQUASH_CONFIRM: KeyHint = KeyHint {
    key: "Enter",
    label: "Squash",
    color: Color::Green,
};
pub const HINT_CANCEL: KeyHint = KeyHint {
    key: "Esc",
    label: "Cancel",
    color: Color::Red,
};
pub const HINT_SUBMIT: KeyHint = KeyHint {
    key: "Enter",
    label: "Confirm",
    color: Color::Green,
};
pub const HINT_CANCEL_ESC: KeyHint = KeyHint {
    key: "Esc",
    label: "Cancel",
    color: Color::Red,
};
// Dialog hints
pub const HINT_YES: KeyHint = KeyHint {
    key: "y/Enter",
    label: "Yes",
    color: Color::Green,
};
pub const HINT_NO: KeyHint = KeyHint {
    key: "n/Esc",
    label: "No",
    color: Color::Red,
};
pub const HINT_TOGGLE: KeyHint = KeyHint {
    key: "Space",
    label: "Toggle",
    color: Color::Yellow,
};
pub const HINT_CONFIRM: KeyHint = KeyHint {
    key: "Enter",
    label: "Confirm",
    color: Color::Green,
};
pub const HINT_DIALOG_CANCEL: KeyHint = KeyHint {
    key: "Esc",
    label: "Cancel",
    color: Color::Red,
};
pub const HINT_SELECT: KeyHint = KeyHint {
    key: "Enter",
    label: "Select",
    color: Color::Green,
};
// Resolve view hints
pub const HINT_RESOLVE_ENTER: KeyHint = KeyHint {
    key: "Enter",
    label: "Resolve",
    color: Color::Green,
};
pub const HINT_OURS: KeyHint = KeyHint {
    key: "o",
    label: "Ours",
    color: Color::Cyan,
};
pub const HINT_THEIRS: KeyHint = KeyHint {
    key: "t",
    label: "Theirs",
    color: Color::Cyan,
};
pub const HINT_DIFF: KeyHint = KeyHint {
    key: "d",
    label: "Diff",
    color: Color::Magenta,
};
// Tag view hints
pub const HINT_TAG_VIEW: KeyHint = KeyHint {
    key: "t",
    label: "Tags",
    color: Color::Cyan,
};
pub const HINT_TAG_CREATE: KeyHint = KeyHint {
    key: "n",
    label: "Create Tag",
    color: Color::Green,
};
pub const HINT_TAG_DELETE: KeyHint = KeyHint {
    key: "d",
    label: "Delete",
    color: Color::Red,
};
pub const HINT_BKM_CREATE: KeyHint = KeyHint {
    key: "n",
    label: "Create",
    color: Color::Green,
};
pub const HINT_JUMP_ENTER: KeyHint = KeyHint {
    key: "Enter",
    label: "Jump",
    color: Color::Green,
};
pub const HINT_UNTRACK: KeyHint = KeyHint {
    key: "T",
    label: "Untrack",
    color: Color::Yellow,
};
pub const HINT_LOG_JUMP: KeyHint = KeyHint {
    key: "J",
    label: "Log Jump",
    color: Color::Yellow,
};
pub const HINT_PREVIEW: KeyHint = KeyHint {
    key: "p",
    label: "Preview",
    color: Color::Blue,
};
pub const HINT_REVERSE: KeyHint = KeyHint {
    key: "V",
    label: "Reverse",
    color: Color::Yellow,
};
pub const HINT_RENAME: KeyHint = KeyHint {
    key: "r",
    label: "Rename",
    color: Color::Yellow,
};
pub const HINT_FORGET: KeyHint = KeyHint {
    key: "f",
    label: "Forget",
    color: Color::Red,
};
pub const HINT_MOVE_BKM: KeyHint = KeyHint {
    key: "m",
    label: "Move",
    color: Color::Yellow,
};
pub const HINT_EVOLOG: KeyHint = KeyHint {
    key: "L",
    label: "Evolog",
    color: Color::Cyan,
};
pub const HINT_RESTORE: KeyHint = KeyHint {
    key: "r",
    label: "Restore",
    color: Color::Red,
};
pub const HINT_RESTORE_ALL: KeyHint = KeyHint {
    key: "R",
    label: "RestoreAll",
    color: Color::Red,
};
pub const HINT_DETAIL: KeyHint = KeyHint {
    key: "Enter",
    label: "Detail",
    color: Color::Green,
};

/// Command History: cycle the kind filter (All / Write / Read)
pub const HINT_CMD_FILTER: KeyHint = KeyHint {
    key: "f",
    label: "Filter",
    color: Color::Cyan,
};

/// Command History: copy the shell-pasteable command line
pub const HINT_CMD_COPY: KeyHint = KeyHint {
    key: "y",
    label: "Copy cmd",
    color: Color::Magenta,
};

// =============================================================================
// HintContext + DialogHintKind
// =============================================================================

/// Bookmark kind for context-dependent Bookmark View hints
#[derive(Clone, Copy, Debug)]
pub enum BookmarkKind {
    /// Local bookmark with change_id (jumpable)
    LocalJumpable,
    /// Local bookmark without change_id
    LocalNoChange,
    /// Tracked remote bookmark
    TrackedRemote,
    /// Untracked remote bookmark
    UntrackedRemote,
}

/// Context for dynamic hint selection
#[derive(Default)]
pub struct HintContext {
    /// Selected change has bookmarks
    pub has_bookmarks: bool,
    /// Selected change has conflicts
    pub has_conflicts: bool,
    /// Selected change is the working copy (@)
    pub is_working_copy: bool,
    /// Active dialog kind (overrides view hints)
    pub dialog: Option<DialogHintKind>,
    /// Selected bookmark kind (Bookmark View only)
    pub selected_bookmark_kind: Option<BookmarkKind>,
    /// Whether --skip-emptied is toggled ON in rebase select mode
    pub skip_emptied: bool,
    /// Whether --simplify-parents is toggled ON in rebase select mode
    pub simplify_parents: bool,
    /// Current rebase mode (for revset hint visibility)
    pub rebase_mode: crate::model::RebaseMode,
}

/// Dialog kind for hint selection
pub enum DialogHintKind {
    /// y/n confirmation
    Confirm,
    /// Multi-select with Space toggle
    Select,
    /// Single-select (Enter immediately confirms)
    SingleSelect,
}

// =============================================================================
// Unified dispatch
// =============================================================================

/// Get the appropriate hints for the current context.
///
/// Priority: dialog > view × input_mode.
/// Diff, Blame, and Help views use dedicated rendering and should not call this.
pub fn current_hints(view: View, input_mode: InputMode, ctx: &HintContext) -> Vec<KeyHint> {
    // Priority 1: dialog overrides everything
    if let Some(ref kind) = ctx.dialog {
        return dialog_hints(kind);
    }
    // Priority 2: view × input_mode
    match view {
        View::Log => log_hints(input_mode, ctx),
        View::Resolve => resolve_hints(ctx),
        View::Bookmark => bookmark_view_hints(ctx),
        View::Tag => tag_view_hints(),
        View::Workspace => workspace_view_hints(),
        View::CommandHistory => command_history_hints(),
        View::Status => STATUS_VIEW_HINTS.to_vec(),
        View::Operation => OPERATION_VIEW_HINTS.to_vec(),
        // Diff, Blame use prefix-based rendering; Help has no status bar.
        // Return empty as a safety fallback.
        _ => vec![],
    }
}

fn dialog_hints(kind: &DialogHintKind) -> Vec<KeyHint> {
    match kind {
        DialogHintKind::Confirm => vec![HINT_YES, HINT_NO],
        DialogHintKind::Select => vec![HINT_NAV, HINT_TOGGLE, HINT_CONFIRM, HINT_DIALOG_CANCEL],
        DialogHintKind::SingleSelect => vec![HINT_NAV, HINT_SELECT, HINT_DIALOG_CANCEL],
    }
}

fn log_hints(input_mode: InputMode, ctx: &HintContext) -> Vec<KeyHint> {
    match input_mode {
        InputMode::Normal => log_normal_hints(ctx),
        InputMode::SquashSelect => vec![HINT_NAV, HINT_SQUASH_CONFIRM, HINT_CANCEL],
        InputMode::RebaseModeSelect => REBASE_MODE_SELECT_HINTS.to_vec(),
        InputMode::RebaseSelect => {
            rebase_select_hints(ctx.skip_emptied, ctx.simplify_parents, ctx.rebase_mode)
        }
        InputMode::CompareSelect => COMPARE_SELECT_HINTS.to_vec(),
        InputMode::InterdiffSelect => INTERDIFF_SELECT_HINTS.to_vec(),
        InputMode::BisectSelect => BISECT_SELECT_HINTS.to_vec(),
        InputMode::ParallelizeSelect => PARALLELIZE_SELECT_HINTS.to_vec(),
        InputMode::RebaseRevsetInput => vec![HINT_SUBMIT, HINT_CANCEL_ESC],
        InputMode::SearchInput
        | InputMode::RevsetInput
        | InputMode::DescribeInput
        | InputMode::BookmarkInput => vec![HINT_SUBMIT, HINT_CANCEL_ESC],
    }
}

fn log_normal_hints(ctx: &HintContext) -> Vec<KeyHint> {
    // Show only the most essential hints here; full list is in Help (?)
    let mut h = vec![
        HINT_HELP,
        HINT_PALETTE,
        HINT_DESC,
        HINT_EDIT,
        HINT_NEW,
        HINT_SQUASH,
        HINT_ABANDON,
        HINT_BOOKMARK,
        HINT_REBASE,
        HINT_FETCH,
    ];
    if ctx.has_conflicts {
        h.push(HINT_RESOLVE);
    }
    if ctx.has_bookmarks {
        h.push(HINT_PUSH);
    }
    h.extend([HINT_UNDO, HINT_REDO, HINT_QUIT]);
    h
}

/// Common trailing hints for mutating views that support undo + redo + refresh +
/// back (Phase 46-D; redo added in Phase 48-D).
fn undo_redo_refresh_back() -> [KeyHint; 4] {
    [HINT_UNDO, HINT_REDO, HINT_REFRESH, HINT_BACK]
}

fn resolve_hints(ctx: &HintContext) -> Vec<KeyHint> {
    let mut h = vec![HINT_HELP];
    if ctx.is_working_copy {
        h.push(HINT_RESOLVE_ENTER);
    }
    h.extend([
        HINT_OURS,
        HINT_THEIRS,
        HINT_DIFF,
        HINT_UNDO,
        HINT_REDO,
        HINT_REFRESH,
        HINT_BACK,
    ]);
    h
}

fn bookmark_view_hints(ctx: &HintContext) -> Vec<KeyHint> {
    let mut h = vec![HINT_HELP, HINT_BKM_CREATE];
    match ctx.selected_bookmark_kind {
        Some(BookmarkKind::LocalJumpable) => {
            h.push(HINT_JUMP_ENTER);
            h.push(HINT_DEL_BKM);
            h.push(HINT_RENAME);
            h.push(HINT_FORGET);
            h.push(HINT_MOVE_BKM);
        }
        Some(BookmarkKind::LocalNoChange) => {
            h.push(HINT_DEL_BKM);
            h.push(HINT_RENAME);
            h.push(HINT_FORGET);
            h.push(HINT_MOVE_BKM);
        }
        Some(BookmarkKind::TrackedRemote) => {
            h.push(HINT_UNTRACK);
        }
        Some(BookmarkKind::UntrackedRemote) => {
            h.push(HINT_TRACK);
        }
        None => {}
    }
    h.extend(undo_redo_refresh_back());
    h
}

fn tag_view_hints() -> Vec<KeyHint> {
    let mut h = vec![
        HINT_HELP,
        HINT_NAV,
        HINT_JUMP_ENTER,
        HINT_TAG_CREATE,
        HINT_TAG_DELETE,
    ];
    h.extend(undo_redo_refresh_back());
    h
}

fn workspace_view_hints() -> Vec<KeyHint> {
    let mut h = vec![
        HINT_HELP,
        HINT_NAV,
        HINT_JUMP_ENTER,
        KeyHint {
            key: "n",
            label: "Add",
            color: Color::Green,
        },
        KeyHint {
            key: "d",
            label: "Forget",
            color: Color::Red,
        },
        HINT_RENAME,
    ];
    h.extend(undo_redo_refresh_back());
    h
}

fn command_history_hints() -> Vec<KeyHint> {
    vec![
        HINT_HELP,
        HINT_NAV,
        HINT_DETAIL,
        HINT_CMD_FILTER,
        HINT_CMD_COPY,
        HINT_BACK,
    ]
}

/// ParallelizeSelect mode status bar hints
pub const PARALLELIZE_SELECT_HINTS: &[KeyHint] = &[
    KeyHint {
        key: "j/k",
        label: "Navigate",
        color: Color::Blue,
    },
    KeyHint {
        key: "Enter",
        label: "Parallelize",
        color: Color::Green,
    },
    KeyHint {
        key: "Esc",
        label: "Cancel",
        color: Color::Red,
    },
];

/// CompareSelect mode status bar hints
pub const COMPARE_SELECT_HINTS: &[KeyHint] = &[
    KeyHint {
        key: "j/k",
        label: "Navigate",
        color: Color::Blue,
    },
    KeyHint {
        key: "Enter",
        label: "Compare",
        color: Color::Green,
    },
    KeyHint {
        key: "Esc",
        label: "Cancel",
        color: Color::Red,
    },
];

/// InterdiffSelect mode status bar hints
pub const INTERDIFF_SELECT_HINTS: &[KeyHint] = &[
    KeyHint {
        key: "j/k",
        label: "Navigate",
        color: Color::Blue,
    },
    KeyHint {
        key: "Enter",
        label: "Interdiff",
        color: Color::Green,
    },
    KeyHint {
        key: "Esc",
        label: "Cancel",
        color: Color::Red,
    },
];

/// BisectSelect mode status bar hints
pub const BISECT_SELECT_HINTS: &[KeyHint] = &[
    KeyHint {
        key: "j/k",
        label: "Navigate",
        color: Color::Blue,
    },
    KeyHint {
        key: "Enter",
        label: "Select Good",
        color: Color::Green,
    },
    KeyHint {
        key: "Esc",
        label: "Cancel",
        color: Color::Red,
    },
];

/// RebaseModeSelect mode status bar hints
pub const REBASE_MODE_SELECT_HINTS: &[KeyHint] = &[
    KeyHint {
        key: "r",
        label: "Revision",
        color: Color::Yellow,
    },
    KeyHint {
        key: "s",
        label: "Source",
        color: Color::Magenta,
    },
    KeyHint {
        key: "b",
        label: "Branch",
        color: Color::LightBlue,
    },
    KeyHint {
        key: "A",
        label: "After",
        color: Color::Cyan,
    },
    KeyHint {
        key: "B",
        label: "Before",
        color: Color::Green,
    },
    KeyHint {
        key: "Esc",
        label: "Cancel",
        color: Color::Red,
    },
];

/// Build RebaseSelect mode status bar hints (dynamic for skip-emptied/simplify-parents state and rebase mode)
fn rebase_select_hints(
    skip_emptied: bool,
    simplify_parents: bool,
    rebase_mode: crate::model::RebaseMode,
) -> Vec<KeyHint> {
    use crate::model::RebaseMode;

    let skip_label = if skip_emptied {
        "Skip-empty:ON"
    } else {
        "Skip-empty:OFF"
    };
    let skip_color = if skip_emptied {
        Color::Green
    } else {
        Color::DarkGray
    };
    let simplify_label = if simplify_parents {
        "Simplify:ON"
    } else {
        "Simplify:OFF"
    };
    let simplify_color = if simplify_parents {
        Color::Green
    } else {
        Color::DarkGray
    };
    let mut hints = vec![
        KeyHint {
            key: "j/k",
            label: "Navigate",
            color: Color::Blue,
        },
        KeyHint {
            key: "S",
            label: skip_label,
            color: skip_color,
        },
        KeyHint {
            key: "P",
            label: simplify_label,
            color: simplify_color,
        },
    ];
    // Show revset hint only for modes that support it
    if matches!(
        rebase_mode,
        RebaseMode::Revision | RebaseMode::Source | RebaseMode::Branch
    ) {
        hints.push(KeyHint {
            key: ":",
            label: "Revset",
            color: Color::Magenta,
        });
    }
    hints.push(KeyHint {
        key: "Enter",
        label: "Rebase",
        color: Color::Green,
    });
    hints.push(KeyHint {
        key: "Esc",
        label: "Cancel",
        color: Color::Red,
    });
    hints
}

/// Diff view status bar hints
pub const DIFF_VIEW_HINTS: &[KeyHint] = &[
    HINT_HELP,
    KeyHint {
        key: "j/k",
        label: "Scroll",
        color: Color::Cyan,
    },
    KeyHint {
        key: "m",
        label: "Mode",
        color: Color::Green,
    },
    KeyHint {
        key: "[/]",
        label: "Prev/next file",
        color: Color::Magenta,
    },
    KeyHint {
        key: "a",
        label: "Blame",
        color: Color::Magenta,
    },
    KeyHint {
        key: "y/Y",
        label: "Copy",
        color: Color::Yellow,
    },
    KeyHint {
        key: "w",
        label: "Export",
        color: Color::Yellow,
    },
    KeyHint {
        key: "^L",
        label: "Refresh",
        color: Color::Blue,
    },
    KeyHint {
        key: "q",
        label: "Back",
        color: Color::Red,
    },
];

/// Status view status bar hints
pub const STATUS_VIEW_HINTS: &[KeyHint] = &[
    KeyHint {
        key: "?",
        label: "Help",
        color: Color::Cyan,
    },
    KeyHint {
        key: "Enter",
        label: "Diff",
        color: Color::Green,
    },
    KeyHint {
        key: "a",
        label: "Blame",
        color: Color::Magenta,
    },
    KeyHint {
        key: "C",
        label: "Commit",
        color: Color::Yellow,
    },
    KeyHint {
        key: "f",
        label: "Conflict",
        color: Color::Red,
    },
    KeyHint {
        key: "r",
        label: "Restore",
        color: Color::Red,
    },
    KeyHint {
        key: "R",
        label: "RestoreAll",
        color: Color::Red,
    },
    KeyHint {
        key: "E",
        label: "DiffEdit",
        color: Color::Yellow,
    },
    HINT_UNDO,
    HINT_REDO,
    KeyHint {
        key: "^L",
        label: "Refresh",
        color: Color::Blue,
    },
    KeyHint {
        key: "Tab",
        label: "Switch",
        color: Color::Blue,
    },
    KeyHint {
        key: "q",
        label: "Back",
        color: Color::Red,
    },
];

/// Status view key bindings for help display
pub const STATUS_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Show file diff",
    },
    KeyBindEntry {
        key: "a",
        description: "Show file blame",
    },
    KeyBindEntry {
        key: "C",
        description: "Commit changes",
    },
    KeyBindEntry {
        key: "f",
        description: "Jump to conflict",
    },
    KeyBindEntry {
        key: "r",
        description: "Restore file",
    },
    KeyBindEntry {
        key: "R",
        description: "Restore all files",
    },
    KeyBindEntry {
        key: "E",
        description: "Diffedit (external diff editor)",
    },
    KeyBindEntry {
        key: "u",
        description: "Undo",
    },
    KeyBindEntry {
        key: "Ctrl+r",
        description: "Redo",
    },
    KeyBindEntry {
        key: "Tab",
        description: "Switch to log",
    },
    KeyBindEntry {
        key: "q",
        description: "Back to log",
    },
];

/// Operation history view key bindings for help display
pub const OPERATION_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Restore operation",
    },
    KeyBindEntry {
        key: "u",
        description: "Undo",
    },
    KeyBindEntry {
        key: "Ctrl+r",
        description: "Redo",
    },
    KeyBindEntry {
        key: "q",
        description: "Back to log",
    },
];

/// Bookmark view key bindings for help display
pub const BOOKMARK_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Jump to bookmark in log",
    },
    KeyBindEntry {
        key: "n",
        description: "Create bookmark",
    },
    KeyBindEntry {
        key: "d",
        description: "Delete local bookmark",
    },
    KeyBindEntry {
        key: "t",
        description: "Track remote bookmark",
    },
    KeyBindEntry {
        key: "T",
        description: "Untrack remote bookmark",
    },
    KeyBindEntry {
        key: "r",
        description: "Rename bookmark",
    },
    KeyBindEntry {
        key: "f",
        description: "Forget bookmark (remove tracking)",
    },
    KeyBindEntry {
        key: "m",
        description: "Move bookmark to @",
    },
    KeyBindEntry {
        key: "u",
        description: "Undo",
    },
    KeyBindEntry {
        key: "Ctrl+r",
        description: "Redo",
    },
    KeyBindEntry {
        key: "q",
        description: "Back to log",
    },
];

/// Tag view key bindings for help display
pub const TAG_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Jump to tag in log",
    },
    KeyBindEntry {
        key: "n",
        description: "Create tag on @",
    },
    KeyBindEntry {
        key: "d",
        description: "Delete tag",
    },
    KeyBindEntry {
        key: "u",
        description: "Undo",
    },
    KeyBindEntry {
        key: "Ctrl+r",
        description: "Redo",
    },
    KeyBindEntry {
        key: "q",
        description: "Back to log",
    },
];

/// Workspace view key bindings for help display
pub const WORKSPACE_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Jump to working copy in log",
    },
    KeyBindEntry {
        key: "n",
        description: "Add workspace",
    },
    KeyBindEntry {
        key: "d",
        description: "Forget workspace",
    },
    KeyBindEntry {
        key: "r",
        description: "Rename workspace (current only)",
    },
    KeyBindEntry {
        key: "u",
        description: "Undo",
    },
    KeyBindEntry {
        key: "Ctrl+r",
        description: "Redo",
    },
    KeyBindEntry {
        key: "q",
        description: "Back to log",
    },
];

/// Command history view key bindings for help display
pub const COMMAND_HISTORY_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Toggle detail",
    },
    KeyBindEntry {
        key: "f",
        description: "Filter: All / Write / Read",
    },
    KeyBindEntry {
        key: "y",
        description: "Copy command line",
    },
    KeyBindEntry {
        key: "q",
        description: "Back",
    },
];

/// Blame view key bindings for help display
#[allow(dead_code)] // planned for Help View integration
pub const BLAME_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "g/G",
        description: "Go to top/bottom",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Show diff",
    },
    KeyBindEntry {
        key: "J",
        description: "Jump to change in log",
    },
    KeyBindEntry {
        key: "q",
        description: "Back",
    },
];

/// Evolog view key bindings for help display.
/// Only lists Evolog-specific actions; navigation (j/k, g/G) and quit (q)
/// are shown via the shared Navigation/Global sections in current-view help.
pub const EVOLOG_KEYS: &[KeyBindEntry] = &[KeyBindEntry {
    key: "Enter",
    description: "Show this revision's diff",
}];

/// Trace Detail view key bindings for help display (Agent Trace A6+A3).
pub const TRACE_DETAIL_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Select URL",
    },
    KeyBindEntry {
        key: "y",
        description: "Copy selected URL",
    },
    KeyBindEntry {
        key: "q",
        description: "Back",
    },
];

/// Controls available inside the Help panel itself.
pub const HELP_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "/",
        description: "Search help",
    },
    KeyBindEntry {
        key: "n/N",
        description: "Next/prev match",
    },
    KeyBindEntry {
        key: "a",
        description: "Toggle all views / current view",
    },
    KeyBindEntry {
        key: "q/Esc",
        description: "Close help",
    },
];

/// Resolve view key bindings for help display
#[allow(dead_code)] // planned for Help View integration
pub const RESOLVE_KEYS: &[KeyBindEntry] = &[
    KeyBindEntry {
        key: "j/k",
        description: "Move down/up",
    },
    KeyBindEntry {
        key: "Enter",
        description: "Resolve (external tool, @ only)",
    },
    KeyBindEntry {
        key: "o",
        description: "Resolve with :ours",
    },
    KeyBindEntry {
        key: "t",
        description: "Resolve with :theirs",
    },
    KeyBindEntry {
        key: "d",
        description: "Show diff",
    },
    KeyBindEntry {
        key: "u",
        description: "Undo",
    },
    KeyBindEntry {
        key: "Ctrl+r",
        description: "Redo",
    },
    KeyBindEntry {
        key: "q",
        description: "Back to log",
    },
];

/// Toggle between current-view and all-views in Help (Help View only).
pub const HELP_TOGGLE_ALL: KeyCode = KeyCode::Char('a');

/// Map a View to its view-specific key bindings (for current-view help).
pub fn keys_for_view(view: View) -> &'static [KeyBindEntry] {
    match view {
        View::Log => LOG_KEYS,
        View::Status => STATUS_KEYS,
        View::Diff => DIFF_KEYS,
        View::Operation => OPERATION_KEYS,
        View::Bookmark => BOOKMARK_KEYS,
        View::Tag => TAG_KEYS,
        View::Workspace => WORKSPACE_KEYS,
        View::CommandHistory => COMMAND_HISTORY_KEYS,
        View::Blame => BLAME_KEYS,
        View::Resolve => RESOLVE_KEYS,
        View::Evolog => EVOLOG_KEYS,
        View::TraceDetail => TRACE_DETAIL_KEYS,
        View::Help => HELP_KEYS,
    }
}

/// Operation history view status bar hints
pub const OPERATION_VIEW_HINTS: &[KeyHint] = &[
    HINT_HELP,
    HINT_NAV,
    KeyHint {
        key: "Enter",
        label: "Restore",
        color: Color::Green,
    },
    HINT_UNDO,
    HINT_REDO,
    KeyHint {
        key: "^L",
        label: "Refresh",
        color: Color::Blue,
    },
    KeyHint {
        key: "q",
        label: "Back",
        color: Color::Red,
    },
];

/// Blame view status bar hints
pub const BLAME_VIEW_HINTS: &[KeyHint] = &[
    HINT_HELP,
    HINT_NAV,
    KeyHint {
        key: "Enter",
        label: "Diff",
        color: Color::Green,
    },
    KeyHint {
        key: "J",
        label: "Log Jump",
        color: Color::Yellow,
    },
    KeyHint {
        key: "^L",
        label: "Refresh",
        color: Color::Blue,
    },
    KeyHint {
        key: "q",
        label: "Back",
        color: Color::Red,
    },
];

/// Trace Detail view status bar hints — used when the view has selectable URLs.
pub const TRACE_DETAIL_VIEW_HINTS: &[KeyHint] = &[
    HINT_HELP,
    HINT_NAV,
    KeyHint {
        key: "y",
        label: "Copy URL",
        color: Color::Green,
    },
    KeyHint {
        key: "q",
        label: "Back",
        color: Color::Red,
    },
];

/// Trace Detail view status bar hints when there is no copyable URL.
/// Drops `[y] Copy URL` so the hint only shows actions that actually work
/// (G4: "show only operations that do something").
pub const TRACE_DETAIL_VIEW_HINTS_NO_URL: &[KeyHint] = &[
    HINT_HELP,
    HINT_NAV,
    KeyHint {
        key: "q",
        label: "Back",
        color: Color::Red,
    },
];

/// Evolog view status bar hints (Phase 46-D follow-up: Evolog had no status bar).
pub const EVOLOG_VIEW_HINTS: &[KeyHint] = &[
    HINT_HELP,
    HINT_NAV,
    KeyHint {
        key: "Enter",
        label: "Diff",
        color: Color::Cyan,
    },
    HINT_REFRESH,
    HINT_BACK,
];

// =============================================================================
// Tests
// =============================================================================

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

    // --- Log Normal: context-dependent hints ---

    #[test]
    fn object_view_verbs_are_normalized() {
        assert_eq!(OBJECT_NEW, KeyCode::Char('n'), "create/add = n");
        assert_eq!(OBJECT_DELETE, KeyCode::Char('d'), "delete/forget = d");
        assert_eq!(TRACK, KeyCode::Char('t'), "track = t");
        assert_eq!(
            BOOKMARK_UNTRACK,
            KeyCode::Char('T'),
            "untrack = T (shift inverse)"
        );
        assert_eq!(OBJECT_RENAME, KeyCode::Char('r'), "rename = r");
        assert_eq!(BOOKMARK_FORGET, KeyCode::Char('f'), "forget(tracking) = f");
        assert_eq!(BOOKMARK_MOVE, KeyCode::Char('m'), "move = m");
    }

    #[test]
    fn bookmark_view_opener_is_single_key_b() {
        assert_eq!(BOOKMARK_VIEW, KeyCode::Char('b'), "b opens Bookmark View");
    }

    #[test]
    fn undo_hint_in_all_mutating_views() {
        // Covers the `current_hints`-routed subset of mutating views (Log/Status/Bookmark/Tag/Workspace/Resolve).
        // `undo_redo_hints_match_mutating_views` is the authoritative both-keys/all-views check (including Operation).
        let ctx = HintContext::default();
        let has_undo = |v| {
            current_hints(v, InputMode::Normal, &ctx)
                .iter()
                .any(|h| h.key == "u")
        };
        // Phase 48-D: undo/redo are repo-wide, active in ALL mutating views
        // (Log/Status/Bookmark/Tag/Workspace/Resolve via current_hints; Operation
        // via OPERATION_VIEW_HINTS). Hints must match the input.rs global handler.
        for v in [
            View::Log,
            View::Status,
            View::Bookmark,
            View::Tag,
            View::Workspace,
            View::Resolve,
        ] {
            assert!(has_undo(v), "{v:?} (mutating) must show undo");
        }
        // CommandHistory is read-only: no undo hint.
        assert!(
            !has_undo(View::CommandHistory),
            "CommandHistory must NOT show undo (read-only)"
        );
    }

    #[test]
    fn delete_forget_terminology_matches_function() {
        // tag delete = "Delete", workspace forget = "Forget" (jj `workspace forget`).
        assert!(
            TAG_KEYS.iter().any(|e| e.description.contains("Delete")),
            "tag uses Delete"
        );
        assert!(
            WORKSPACE_KEYS
                .iter()
                .any(|e| e.description.contains("Forget")),
            "workspace uses Forget"
        );
    }

    #[test]
    fn bookmark_delete_status_hint_uses_delete_label() {
        // G2: bookmark deletion is "Delete" (full delete), not an abbreviation.
        // Both local kinds expose the `d` hint via the Bookmark View status bar.
        for kind in [BookmarkKind::LocalJumpable, BookmarkKind::LocalNoChange] {
            let ctx = HintContext {
                selected_bookmark_kind: Some(kind),
                ..HintContext::default()
            };
            let hints = current_hints(View::Bookmark, InputMode::Normal, &ctx);
            let d = hints
                .iter()
                .find(|h| h.key == "d")
                .unwrap_or_else(|| panic!("{kind:?} should expose d (delete) hint"));
            assert_eq!(d.label, "Delete", "{kind:?} delete hint label");
        }
    }

    #[test]
    fn resolve_and_evolog_have_refresh_hint() {
        let ctx = HintContext::default();
        let resolve = current_hints(View::Resolve, InputMode::Normal, &ctx);
        assert!(
            resolve.iter().any(|h| h.key == "^L"),
            "Resolve needs ^L refresh"
        );
        assert!(
            EVOLOG_VIEW_HINTS.iter().any(|h| h.key == "^L"),
            "Evolog needs ^L refresh"
        );
    }

    #[test]
    fn undo_redo_hints_match_mutating_views() {
        let ctx = HintContext::default();
        // Views routed through current_hints.
        let has_cur = |v, k| {
            current_hints(v, InputMode::Normal, &ctx)
                .iter()
                .any(|h| h.key == k)
        };
        for v in [
            View::Log,
            View::Status,
            View::Bookmark,
            View::Tag,
            View::Workspace,
            View::Resolve,
        ] {
            assert!(has_cur(v, "u"), "{v:?} must show undo hint");
            assert!(has_cur(v, "^R"), "{v:?} must show redo hint");
        }
        // Read-only view routed through current_hints must NOT show undo/redo.
        let ch = View::CommandHistory;
        assert!(!has_cur(ch, "u"), "{ch:?} must not show undo hint");
        assert!(!has_cur(ch, "^R"), "{ch:?} must not show redo hint");
        // Operation uses a constant array (not current_hints) — check directly.
        assert!(
            OPERATION_VIEW_HINTS.iter().any(|h| h.key == "u"),
            "Operation must show undo hint"
        );
        assert!(
            OPERATION_VIEW_HINTS.iter().any(|h| h.key == "^R"),
            "Operation must show redo hint"
        );
        // Read-only constant-array views must NOT show undo/redo.
        assert!(
            !BLAME_VIEW_HINTS
                .iter()
                .any(|h| h.key == "u" || h.key == "^R"),
            "Blame must not show undo/redo hints"
        );
        assert!(
            !EVOLOG_VIEW_HINTS
                .iter()
                .any(|h| h.key == "u" || h.key == "^R"),
            "Evolog must not show undo/redo hints"
        );
        assert!(
            !DIFF_VIEW_HINTS
                .iter()
                .any(|h| h.key == "u" || h.key == "^R"),
            "Diff must not show undo/redo hints"
        );
    }

    #[test]
    fn command_history_has_no_refresh_hint() {
        // CommandHistory Ctrl+L is a no-op (in-memory), so it must NOT show refresh.
        let ctx = HintContext::default();
        let ch = current_hints(View::CommandHistory, InputMode::Normal, &ctx);
        assert!(
            !ch.iter().any(|h| h.key == "^L"),
            "CommandHistory must not show refresh (no-op)"
        );
    }

    #[test]
    fn operation_and_blame_nav_label_is_navigate() {
        // Operation/Blame use constant arrays (not current_hints), so check directly.
        let op = OPERATION_VIEW_HINTS
            .iter()
            .find(|h| h.key == "j/k")
            .expect("op has j/k");
        assert_eq!(op.label, "Navigate", "Operation nav label");
        let bl = BLAME_VIEW_HINTS
            .iter()
            .find(|h| h.key == "j/k")
            .expect("blame has j/k");
        assert_eq!(bl.label, "Navigate", "Blame nav label");
    }

    #[test]
    fn all_status_bar_views_include_help_hint() {
        let ctx = HintContext::default();
        for view in [
            View::Log,
            View::Status,
            View::Operation,
            View::Resolve,
            View::Bookmark,
            View::Tag,
            View::Workspace,
            View::CommandHistory,
        ] {
            let hints = current_hints(view, InputMode::Normal, &ctx);
            assert!(
                hints.iter().any(|h| h.key == "?"),
                "{view:?} status bar must include the ? help hint"
            );
        }
    }

    #[test]
    fn diff_and_blame_hints_include_help() {
        assert!(
            DIFF_VIEW_HINTS.iter().any(|h| h.key == "?"),
            "Diff status bar needs ? help"
        );
        assert!(
            BLAME_VIEW_HINTS.iter().any(|h| h.key == "?"),
            "Blame status bar needs ? help"
        );
    }

    #[test]
    fn help_hint_appears_exactly_once_per_view() {
        let ctx = HintContext::default();
        for view in [
            View::Log,
            View::Status,
            View::Operation,
            View::Resolve,
            View::Bookmark,
            View::Tag,
            View::Workspace,
            View::CommandHistory,
        ] {
            let hints = current_hints(view, InputMode::Normal, &ctx);
            let n = hints.iter().filter(|h| h.key == "?").count();
            assert_eq!(n, 1, "{view:?} must have exactly one ? hint, got {n}");
        }
        assert_eq!(DIFF_VIEW_HINTS.iter().filter(|h| h.key == "?").count(), 1);
        assert_eq!(BLAME_VIEW_HINTS.iter().filter(|h| h.key == "?").count(), 1);
    }

    #[test]
    fn log_normal_with_bookmarks_includes_push() {
        let ctx = HintContext {
            has_bookmarks: true,
            ..HintContext::default()
        };
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert!(hints.iter().any(|h| h.key == "P"), "Push hint missing");
        // `D` is not a single-key hint in Log Normal (it belongs to Bookmark View).
        assert!(
            !hints.iter().any(|h| h.key == "D"),
            "Del Bkm should not be a single-key hint in Log Normal"
        );
        // `b` opens Bookmark View directly — hint must be present in Log Normal.
        assert!(
            hints.iter().any(|h| h.key == "b"),
            "bookmark view opener hint missing"
        );
    }

    #[test]
    fn log_normal_without_bookmarks_excludes_push_and_del() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert!(
            !hints.iter().any(|h| h.key == "P"),
            "Push hint should not appear"
        );
        assert!(
            !hints.iter().any(|h| h.key == "D"),
            "Del Bkm hint should not appear"
        );
    }

    #[test]
    fn log_normal_with_conflicts_includes_resolve() {
        let ctx = HintContext {
            has_conflicts: true,
            ..HintContext::default()
        };
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert!(hints.iter().any(|h| h.key == "X"), "Resolve hint missing");
    }

    #[test]
    fn log_normal_without_conflicts_excludes_resolve() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert!(
            !hints.iter().any(|h| h.key == "X"),
            "Resolve hint should not appear"
        );
    }

    #[test]
    fn log_normal_always_includes_core_hints() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert!(hints.iter().any(|h| h.key == "?"), "Help hint missing");
        assert!(hints.iter().any(|h| h.key == "d"), "Desc hint missing");
        assert!(hints.iter().any(|h| h.key == "q"), "Quit hint missing");
        assert!(hints.iter().any(|h| h.key == "e"), "Edit hint missing");
        assert!(hints.iter().any(|h| h.key == "c"), "New hint missing");
        assert!(hints.iter().any(|h| h.key == "S"), "Squash hint missing");
        assert!(hints.iter().any(|h| h.key == "F"), "Fetch hint missing");
    }

    // --- Log View: InputMode-specific hint counts ---

    #[test]
    fn log_squash_select_hints() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::SquashSelect, &ctx);
        assert_eq!(hints.len(), 3);
        assert!(hints.iter().any(|h| h.label == "Navigate"));
        assert!(hints.iter().any(|h| h.label == "Squash"));
        assert!(hints.iter().any(|h| h.label == "Cancel"));
    }

    #[test]
    fn log_search_input_hints() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::SearchInput, &ctx);
        assert_eq!(hints.len(), 2);
    }

    #[test]
    fn log_revset_input_hints() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::RevsetInput, &ctx);
        assert_eq!(hints.len(), 2);
    }

    #[test]
    fn log_describe_input_hints() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::DescribeInput, &ctx);
        assert_eq!(hints.len(), 2);
    }

    #[test]
    fn log_bookmark_input_hints() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::BookmarkInput, &ctx);
        assert_eq!(hints.len(), 2);
    }

    // --- Dialog hints ---

    #[test]
    fn dialog_confirm_hints() {
        let ctx = HintContext {
            dialog: Some(DialogHintKind::Confirm),
            ..HintContext::default()
        };
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert_eq!(hints.len(), 2);
        assert!(hints.iter().any(|h| h.label == "Yes"));
        assert!(hints.iter().any(|h| h.label == "No"));
    }

    #[test]
    fn dialog_select_hints() {
        let ctx = HintContext {
            dialog: Some(DialogHintKind::Select),
            ..HintContext::default()
        };
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert_eq!(hints.len(), 4);
        assert!(hints.iter().any(|h| h.label == "Navigate"));
        assert!(hints.iter().any(|h| h.label == "Toggle"));
        assert!(hints.iter().any(|h| h.label == "Confirm"));
        assert!(hints.iter().any(|h| h.label == "Cancel"));
    }

    #[test]
    fn dialog_single_select_hints() {
        let ctx = HintContext {
            dialog: Some(DialogHintKind::SingleSelect),
            ..HintContext::default()
        };
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert_eq!(hints.len(), 3);
        assert!(hints.iter().any(|h| h.label == "Navigate"));
        assert!(hints.iter().any(|h| h.label == "Select"));
        assert!(hints.iter().any(|h| h.label == "Cancel"));
    }

    // --- Dialog overrides ---

    #[test]
    fn dialog_overrides_log_normal() {
        let ctx = HintContext {
            has_bookmarks: true,
            dialog: Some(DialogHintKind::Confirm),
            ..HintContext::default()
        };
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert_eq!(hints.len(), 2);
        assert!(hints.iter().any(|h| h.label == "Yes"));
        assert!(
            !hints.iter().any(|h| h.key == "P"),
            "Log hints should be suppressed"
        );
    }

    #[test]
    fn dialog_overrides_squash_select() {
        let ctx = HintContext {
            dialog: Some(DialogHintKind::Select),
            ..HintContext::default()
        };
        let hints = current_hints(View::Log, InputMode::SquashSelect, &ctx);
        assert_eq!(hints.len(), 4);
        assert!(hints.iter().any(|h| h.label == "Toggle"));
    }

    #[test]
    fn dialog_overrides_status_view() {
        let ctx = HintContext {
            dialog: Some(DialogHintKind::Confirm),
            ..HintContext::default()
        };
        let hints = current_hints(View::Status, InputMode::Normal, &ctx);
        assert_eq!(hints.len(), 2);
        assert!(hints.iter().any(|h| h.label == "Yes"));
    }

    #[test]
    fn no_dialog_returns_view_hints() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Log, InputMode::Normal, &ctx);
        assert!(hints.len() > 10, "Should return full Log Normal hints");
    }

    // --- Resolve view ---

    #[test]
    fn resolve_working_copy_includes_enter() {
        let ctx = HintContext {
            is_working_copy: true,
            ..HintContext::default()
        };
        let hints = current_hints(View::Resolve, InputMode::Normal, &ctx);
        assert!(
            hints
                .iter()
                .any(|h| h.key == "Enter" && h.label == "Resolve"),
            "Resolve Enter hint missing for working copy"
        );
    }

    #[test]
    fn resolve_non_working_copy_excludes_enter() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Resolve, InputMode::Normal, &ctx);
        assert!(
            !hints.iter().any(|h| h.key == "Enter"),
            "Enter hint should not appear for non-working-copy"
        );
    }

    #[test]
    fn resolve_always_includes_ours_theirs_diff_back() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Resolve, InputMode::Normal, &ctx);
        assert!(hints.iter().any(|h| h.key == "o"), "Ours hint missing");
        assert!(hints.iter().any(|h| h.key == "t"), "Theirs hint missing");
        assert!(hints.iter().any(|h| h.key == "d"), "Diff hint missing");
        assert!(hints.iter().any(|h| h.key == "q"), "Back hint missing");
    }

    // --- Safety fallback for Diff/Help ---

    #[test]
    fn diff_view_returns_empty() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Diff, InputMode::Normal, &ctx);
        assert!(hints.is_empty());
    }

    #[test]
    fn help_view_returns_empty() {
        let ctx = HintContext::default();
        let hints = current_hints(View::Help, InputMode::Normal, &ctx);
        assert!(hints.is_empty());
    }

    #[test]
    fn help_arrays_list_undo_redo_for_mutating_views() {
        // Every view where undo/redo works must document both in its help array,
        // since u/Ctrl+r are not in GLOBAL_KEYS.
        for v in [
            View::Log,
            View::Status,
            View::Bookmark,
            View::Tag,
            View::Workspace,
            View::Operation,
            View::Resolve,
        ] {
            let ks = keys_for_view(v);
            assert!(
                ks.iter().any(|e| e.description.contains("Undo")),
                "{v:?} help missing Undo"
            );
            assert!(
                ks.iter().any(|e| e.description.contains("Redo")),
                "{v:?} help missing Redo"
            );
        }
        // Read-only views must NOT list them.
        for v in [
            View::Diff,
            View::Blame,
            View::Evolog,
            View::CommandHistory,
            View::Help,
        ] {
            let ks = keys_for_view(v);
            assert!(
                !ks.iter().any(|e| e.description.contains("Undo")),
                "{v:?} help should not list Undo"
            );
            assert!(
                !ks.iter().any(|e| e.description.contains("Redo")),
                "{v:?} help should not list Redo"
            );
        }
    }
}
