//! Input handling for the application

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

use crate::app::helpers::revision::short_id;

use super::state::{App, View};
use crate::keys;
use crate::ui::views::{
    BlameAction, BookmarkAction, CommandHistoryAction, DiffAction, EvologAction, InputMode,
    LogAction, OperationAction, RenameState, ResolveAction, StatusAction, StatusInputMode,
    TagAction,
};

impl App {
    /// Handle key events
    pub fn on_key_event(&mut self, key: KeyEvent) {
        // Handle active dialog first (blocks other input)
        if let Some(ref mut dialog) = self.active_dialog {
            if let Some(result) = dialog.handle_key(key) {
                // handle_dialog_result() clears active_dialog internally.
                // Do NOT clear again here — dialog chains (e.g. remote select → push confirm)
                // set a new active_dialog inside handle_dialog_result().
                self.handle_dialog_result(result);
            }
            return;
        }

        // Command palette active: capture all keys (Phase 46-C). Like the dialog
        // block above, this intentionally traps every key (including Ctrl+C) while
        // open — close with Esc first. Kept consistent with dialog behavior.
        if self.palette_active {
            self.handle_palette_key(key);
            return;
        }

        // Clear error message and expired notification on any key press
        self.error_message = None;
        self.clear_expired_notification();

        // Handle Ctrl+C globally
        if key.modifiers.contains(KeyModifiers::CONTROL)
            && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
        {
            self.quit();
            return;
        }

        // Chord resolve (Phase 46-B): if a `b` chord is pending, the next key
        // resolves or cancels it — including Ctrl-modified keys (which cancel
        // via resolve_chord). This MUST run before Ctrl+R/Ctrl+L below so those
        // don't fire while a chord is pending (and leave pending_chord stuck).
        if let Some(prefix) = self.pending_chord.take() {
            self.resolve_chord(prefix, key);
            return;
        }

        // Handle Ctrl+R for redo (only in Log view, normal mode)
        // Only active in Normal mode to avoid conflicts with input modes
        if key.modifiers.contains(KeyModifiers::CONTROL)
            && matches!(key.code, KeyCode::Char('r') | KeyCode::Char('R'))
            && self.current_view == View::Log
            && matches!(self.log_view.input_mode, InputMode::Normal)
        {
            self.notification = None; // Clear any existing notification
            self.execute_redo();
            return;
        }

        // Handle Ctrl+L for refresh (all views, normal mode)
        if keys::is_refresh_key(&key) {
            // Skip if in input mode or special mode (like RebaseSelect)
            let in_special_mode = match self.current_view {
                View::Log => !matches!(self.log_view.input_mode, InputMode::Normal),
                View::Status => self.status_view.input_mode != StatusInputMode::Normal,
                View::Help => self.help_search_input,
                _ => false,
            };
            if !in_special_mode {
                self.execute_refresh();
                return;
            }
        }

        // If in input mode or rebase select mode, delegate all keys to the view (skip global handling)
        if self.current_view == View::Log && !matches!(self.log_view.input_mode, InputMode::Normal)
        {
            let action = self.log_view.handle_key(key);
            self.handle_log_action(action);
            return;
        }

        // Handle Status View input mode
        if self.current_view == View::Status
            && self.status_view.input_mode != StatusInputMode::Normal
        {
            let action = self.status_view.handle_key(key);
            self.handle_status_action(action);
            return;
        }

        // Handle Help search input mode (skip global keys so Esc/q/Tab stay in search)
        if self.current_view == View::Help && self.help_search_input {
            self.handle_view_key(key);
            return;
        }

        // Chord start (Phase 46-B): begin a `b` chord in Log View Normal mode.
        // Resolution happens earlier (right after Ctrl+C), so a pending chord is
        // always resolved/cancelled before Ctrl+R/Ctrl+L can fire.
        if self.current_view == View::Log
            && matches!(self.log_view.input_mode, InputMode::Normal)
            && key.code == keys::CHORD_BOOKMARK
            && key.modifiers.is_empty()
        {
            self.pending_chord = Some(keys::CHORD_BOOKMARK);
            return;
        }

        // Command palette trigger (Phase 46-C): Log View Normal mode.
        if self.current_view == View::Log
            && matches!(self.log_view.input_mode, InputMode::Normal)
            && key.code == keys::COMMAND_PALETTE
            && key.modifiers.is_empty()
        {
            self.palette_active = true;
            self.palette_input.clear();
            self.palette_selected = 0;
            return;
        }

        if self.handle_global_key(key) {
            return;
        }

        self.handle_view_key(key);
    }

    /// Resolve a bookmark chord (prefix already consumed via `take()`).
    /// Unknown or modified sub-keys cancel the chord silently.
    fn resolve_chord(&mut self, prefix: KeyCode, key: KeyEvent) {
        if prefix != keys::CHORD_BOOKMARK {
            return; // only the bookmark chord exists in Phase 46-B
        }
        // P4: only plain sub-keys resolve. Ctrl-d / Alt-x etc. must NOT be
        // treated as `b d` — they cancel the chord silently.
        if !key.modifiers.is_empty() {
            return;
        }
        match key.code {
            keys::chord_bookmark::CREATE => self.log_view.start_bookmark_input(),
            keys::chord_bookmark::DELETE => self.handle_log_action(LogAction::StartBookmarkDelete),
            keys::chord_bookmark::TRACK => self.handle_log_action(LogAction::StartTrack),
            keys::chord_bookmark::JUMP => self.handle_log_action(LogAction::StartBookmarkJump),
            keys::chord_bookmark::ADVANCE => self.handle_log_action(LogAction::AdvanceBookmark),
            keys::chord_bookmark::VIEW => self.handle_log_action(LogAction::OpenBookmarkView),
            // Esc or any unknown sub-key: silent cancel (pending_chord already taken).
            _ => {}
        }
    }

    /// Handle a key while the command palette is active (Phase 46-C).
    fn handle_palette_key(&mut self, key: KeyEvent) {
        use crate::ui::components::{filter_commands, palette_commands};
        match key.code {
            KeyCode::Esc => self.close_palette(),
            KeyCode::Enter => {
                // Resolve current selection from the filtered list, then dispatch
                // by delegating its key to the Log handler (reuses all logic).
                let filtered = filter_commands(palette_commands(), &self.palette_input);
                let chosen = filtered.get(self.palette_selected).map(|c| c.key);
                self.close_palette();
                if let Some(code) = chosen {
                    let action = self.log_view.handle_key(KeyEvent::from(code));
                    self.handle_log_action(action);
                }
            }
            // Movement: arrows + Ctrl+n/Ctrl+p (NOT j/k — those are text input).
            KeyCode::Down => self.palette_move(1),
            KeyCode::Up => self.palette_move(-1),
            KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.palette_move(1)
            }
            KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.palette_move(-1)
            }
            KeyCode::Backspace => {
                self.palette_input.pop();
                self.palette_selected = 0;
            }
            KeyCode::Char(c)
                if !key
                    .modifiers
                    .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
            {
                self.palette_input.push(c);
                self.palette_selected = 0;
            }
            _ => {}
        }
    }

    /// Close the command palette and reset its state.
    fn close_palette(&mut self) {
        self.palette_active = false;
        self.palette_input.clear();
        self.palette_selected = 0;
    }

    /// Move palette selection within the current filtered list bounds (wraps).
    fn palette_move(&mut self, delta: isize) {
        let len = crate::ui::components::filter_commands(
            crate::ui::components::palette_commands(),
            &self.palette_input,
        )
        .len();
        if len == 0 {
            self.palette_selected = 0;
            return;
        }
        let next = (self.palette_selected as isize + delta).rem_euclid(len as isize);
        self.palette_selected = next as usize;
    }

    fn handle_global_key(&mut self, key: KeyEvent) -> bool {
        match key.code {
            keys::QUIT => {
                self.handle_quit();
                true
            }
            keys::ESC => {
                // Don't intercept Esc when bookmark rename is active
                if self.current_view == View::Bookmark && self.bookmark_view.rename_state.is_some()
                {
                    return false;
                }
                self.handle_back();
                true
            }
            keys::HELP => {
                self.go_to_view(View::Help);
                true
            }
            keys::TAB => {
                self.next_view();
                true
            }
            keys::STATUS_VIEW if self.current_view == View::Log => {
                self.go_to_view(View::Status);
                true
            }
            keys::UNDO if matches!(self.current_view, View::Log | View::Bookmark | View::Tag) => {
                self.notification = None; // Clear any existing notification
                self.execute_undo();
                true
            }
            keys::OPERATION_HISTORY if self.current_view == View::Log => {
                self.open_operation_history();
                true
            }
            _ => false,
        }
    }

    fn handle_quit(&mut self) {
        if self.current_view == View::Log {
            self.quit();
        } else {
            self.go_back();
        }
    }

    fn handle_back(&mut self) {
        if self.current_view != View::Log {
            self.go_back();
        }
    }

    fn handle_view_key(&mut self, key: KeyEvent) {
        match self.current_view {
            View::Log => {
                // Preview toggle (handled at App level since it's App state)
                if key.code == keys::PREVIEW
                    && matches!(self.log_view.input_mode, InputMode::Normal)
                {
                    self.preview_enabled = !self.preview_enabled;
                    if self.preview_enabled {
                        // Immediate fetch on toggle-ON (no 200ms wait)
                        self.update_preview_if_needed();
                        self.resolve_pending_preview();
                    } else {
                        // Clear pending fetch on disable (keep cache for toggle back ON)
                        self.preview_pending_id = None;
                    }
                    return;
                }

                let action = self.log_view.handle_key(key);
                self.handle_log_action(action);

                // Update preview after key processing (debounced)
                // Guard: only if still on Log view (Enter → Diff would have transitioned away)
                if self.preview_enabled && self.current_view == View::Log {
                    self.update_preview_if_needed();
                }
            }
            View::Diff => {
                if let Some(ref mut diff_view) = self.diff_view {
                    let visible_height = self.last_frame_height.get() as usize;
                    let action = diff_view.handle_key_with_height(key, visible_height);
                    self.handle_diff_action(action);
                }
            }
            View::Status => {
                let visible_height = self.last_frame_height.get() as usize;
                let action = self.status_view.handle_key_with_height(key, visible_height);
                self.handle_status_action(action);
            }
            View::Operation => {
                let action = self.operation_view.handle_key(key);
                self.handle_operation_action(action);
            }
            View::Blame => {
                if let Some(ref mut blame_view) = self.blame_view {
                    let action = blame_view.handle_key(key);
                    self.handle_blame_action(action);
                }
            }
            View::Bookmark => {
                let action = self.bookmark_view.handle_key(key);
                self.handle_bookmark_action(action);
            }
            View::Tag => {
                let action = self.tag_view.handle_key(key);
                self.handle_tag_action(action);
            }
            View::Workspace => {
                let action = self.workspace_view.handle_key(key);
                self.handle_workspace_action(action);
            }
            View::Resolve => {
                if let Some(ref mut resolve_view) = self.resolve_view {
                    let action = resolve_view.handle_key(key);
                    self.handle_resolve_action(action);
                }
            }
            View::Evolog => {
                if let Some(ref mut evolog_view) = self.evolog_view {
                    let action = evolog_view.handle_key(key);
                    self.handle_evolog_action(action);
                }
            }
            View::CommandHistory => {
                let total = self.command_history.len();
                let action = self.command_history_view.handle_key(key, total);
                self.handle_command_history_action(action);
            }
            View::Help => {
                if self.help_search_input {
                    // Search input mode: capture text
                    match key.code {
                        KeyCode::Esc => {
                            self.help_search_input = false;
                            self.help_input_buffer.clear();
                        }
                        KeyCode::Enter => {
                            let query = std::mem::take(&mut self.help_input_buffer);
                            self.help_search_input = false;
                            if query.is_empty() {
                                self.help_search_query = None;
                            } else {
                                self.help_search_query = Some(query.clone());
                                // Jump to first match
                                let indices = crate::ui::widgets::matching_line_indices(
                                    &query,
                                    self.previous_view,
                                    self.help_show_all,
                                );
                                if let Some(&first) = indices.first() {
                                    self.help_scroll = first;
                                }
                            }
                        }
                        KeyCode::Char(c)
                            if !key
                                .modifiers
                                .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
                        {
                            self.help_input_buffer.push(c);
                        }
                        KeyCode::Backspace => {
                            self.help_input_buffer.pop();
                        }
                        _ => {}
                    }
                } else {
                    // Normal mode: scrolling + search start + n/N navigation
                    if keys::is_move_down(key.code) {
                        self.help_scroll = self.help_scroll.saturating_add(1);
                    } else if keys::is_move_up(key.code) {
                        self.help_scroll = self.help_scroll.saturating_sub(1);
                    } else if key.code == keys::GO_BOTTOM {
                        self.help_scroll = u16::MAX; // clamped during render
                    } else if key.code == keys::GO_TOP {
                        self.help_scroll = 0;
                    } else if key.code == keys::SEARCH_INPUT {
                        self.help_search_input = true;
                        self.help_input_buffer.clear();
                    } else if key.code == keys::SEARCH_NEXT {
                        if let Some(ref query) = self.help_search_query {
                            let indices = crate::ui::widgets::matching_line_indices(
                                query,
                                self.previous_view,
                                self.help_show_all,
                            );
                            if let Some(next) = indices.iter().find(|&&i| i > self.help_scroll) {
                                self.help_scroll = *next;
                            } else if let Some(&first) = indices.first() {
                                // Wrap to top
                                self.help_scroll = first;
                            }
                        }
                    } else if key.code == keys::SEARCH_PREV
                        && let Some(ref query) = self.help_search_query
                    {
                        let indices = crate::ui::widgets::matching_line_indices(
                            query,
                            self.previous_view,
                            self.help_show_all,
                        );
                        if let Some(prev) = indices.iter().rev().find(|&&i| i < self.help_scroll) {
                            self.help_scroll = *prev;
                        } else if let Some(&last) = indices.last() {
                            // Wrap to bottom
                            self.help_scroll = last;
                        }
                    } else if key.code == keys::HELP_TOGGLE_ALL {
                        self.help_show_all = !self.help_show_all;
                        self.help_scroll = 0;
                    }
                }
            }
        }
    }

    fn handle_log_action(&mut self, action: LogAction) {
        match action {
            LogAction::None => {}

            // Navigation
            LogAction::OpenDiff(_)
            | LogAction::ExecuteRevset(_)
            | LogAction::ClearRevset
            | LogAction::OpenBookmarkView
            | LogAction::OpenTagView
            | LogAction::OpenWorkspaceView
            | LogAction::OpenCommandHistory
            | LogAction::OpenEvolog(_)
            | LogAction::OpenResolveList { .. } => {
                self.handle_log_navigation(action);
            }

            // Editing
            LogAction::StartDescribe(_)
            | LogAction::Describe { .. }
            | LogAction::DescribeExternal(_)
            | LogAction::Edit(_)
            | LogAction::NewChange
            | LogAction::NewChangeFrom { .. }
            | LogAction::NewChangeFromCurrent
            | LogAction::SquashInto { .. }
            | LogAction::Abandon(_)
            | LogAction::Split(_)
            | LogAction::Duplicate(_)
            | LogAction::DiffEdit(_)
            | LogAction::Revert(_)
            | LogAction::SimplifyParents(_)
            | LogAction::Fix { .. }
            | LogAction::Metaedit { .. } => {
                self.handle_log_editing(action);
            }

            // Bookmark
            LogAction::CreateBookmark { .. }
            | LogAction::StartBookmarkDelete
            | LogAction::StartBookmarkJump
            | LogAction::AdvanceBookmark => {
                self.handle_log_bookmark(action);
            }

            // Git
            LogAction::Fetch | LogAction::StartPush | LogAction::StartTrack => {
                self.handle_log_git(action);
            }

            // Rebase / Parallelize
            LogAction::Rebase { .. }
            | LogAction::Absorb
            | LogAction::StartParallelize(_)
            | LogAction::Parallelize { .. }
            | LogAction::ParallelizeSameRevision => {
                self.handle_log_rebase(action);
            }

            // Compare / Interdiff
            LogAction::StartCompare(_)
            | LogAction::Compare { .. }
            | LogAction::CompareSameRevision
            | LogAction::StartInterdiff(_)
            | LogAction::Interdiff { .. }
            | LogAction::InterdiffSameRevision => {
                self.handle_log_compare(action);
            }

            // Bisect
            LogAction::StartBisect(_)
            | LogAction::Bisect { .. }
            | LogAction::BisectSameRevision => {
                self.handle_log_bisect(action);
            }

            // Arrange (interactive TUI)
            LogAction::Arrange => {
                self.execute_arrange();
            }

            // Misc
            LogAction::NextChange | LogAction::PrevChange | LogAction::ToggleReversed => {
                self.handle_log_misc(action);
            }
        }
    }

    fn handle_log_navigation(&mut self, action: LogAction) {
        match action {
            LogAction::OpenDiff(change_id) => self.open_diff(&change_id),
            LogAction::ExecuteRevset(revset) => self.refresh_log(Some(&revset)),
            LogAction::ClearRevset => self.refresh_log(None),
            LogAction::OpenBookmarkView => self.open_bookmark_view(),
            LogAction::OpenTagView => self.open_tag_view(),
            LogAction::OpenWorkspaceView => self.open_workspace_view(),
            LogAction::OpenCommandHistory => self.go_to_view(View::CommandHistory),
            LogAction::OpenEvolog(change_id) => self.open_evolog(&change_id),
            LogAction::OpenResolveList {
                revision,
                is_working_copy,
            } => self.open_resolve_view(&revision, is_working_copy),
            _ => {}
        }
    }

    fn handle_log_editing(&mut self, action: LogAction) {
        use crate::ui::components::{Dialog, DialogCallback, SelectItem};

        match action {
            LogAction::StartDescribe(revision) => self.start_describe_input(&revision),
            LogAction::Describe { revision, message } => {
                self.execute_describe(&revision, &message);
            }
            LogAction::DescribeExternal(revision) => self.execute_describe_external(&revision),
            LogAction::Edit(revision) => self.execute_edit(&revision),
            LogAction::NewChange => self.execute_new_change(),
            LogAction::NewChangeFrom {
                revision,
                display_name,
            } => self.execute_new_change_from(&revision, &display_name),
            LogAction::NewChangeFromCurrent => {
                self.notify_info("Use 'c' to create from current change");
            }
            LogAction::SquashInto {
                source,
                destination,
            } => self.execute_squash_into(&source, &destination),
            LogAction::Abandon(revision) => self.execute_abandon(&revision),
            LogAction::Split(revision) => self.execute_split(&revision),
            LogAction::Duplicate(revision) => self.duplicate(&revision),
            LogAction::DiffEdit(revision) => self.execute_diffedit(&revision, None),
            LogAction::Revert(revision) => {
                let short_id = short_id(&revision);
                self.active_dialog = Some(Dialog::confirm(
                    "Revert Change",
                    format!("Revert changes from {}?", short_id),
                    Some(
                        "Creates a new commit that undoes these changes. Undo with 'u' if needed."
                            .to_string(),
                    ),
                    DialogCallback::Revert { revision },
                ));
            }
            LogAction::SimplifyParents(revision) => {
                let short_id = short_id(&revision);
                self.active_dialog = Some(Dialog::confirm(
                    "Simplify Parents",
                    format!("Simplify parents for {}?", short_id),
                    None,
                    DialogCallback::SimplifyParents { revision },
                ));
            }
            LogAction::Fix {
                revision,
                change_id,
            } => {
                if self.jj.is_immutable(&revision) {
                    self.set_error("Cannot fix: commit is immutable");
                    return;
                }
                let short_id = short_id(&revision);
                let items = vec![
                    SelectItem {
                        label: "Default (respect fix.tools line-range-arg)".to_string(),
                        value: "default".to_string(),
                        selected: false,
                    },
                    SelectItem {
                        label: "All lines (--all-lines, format entire files)".to_string(),
                        value: "all-lines".to_string(),
                        selected: false,
                    },
                ];
                self.active_dialog = Some(Dialog::select_single(
                    "Fix",
                    format!("Apply code formatters to {} and descendants", short_id),
                    items,
                    None,
                    DialogCallback::Fix {
                        revision,
                        change_id,
                    },
                ));
            }
            LogAction::Metaedit {
                change_id,
                commit_id,
            } => {
                if self.jj.is_immutable(&commit_id) {
                    self.set_error("Cannot metaedit: commit is immutable");
                    return;
                }
                let short = short_id(&change_id);
                let items = vec![
                    SelectItem {
                        label: "Update author".to_string(),
                        value: "update-author".to_string(),
                        selected: false,
                    },
                    SelectItem {
                        label: "Set author...".to_string(),
                        value: "set-author".to_string(),
                        selected: false,
                    },
                    SelectItem {
                        label: "Update author timestamp".to_string(),
                        value: "update-timestamp".to_string(),
                        selected: false,
                    },
                    SelectItem {
                        label: "Generate new change-id".to_string(),
                        value: "new-change-id".to_string(),
                        selected: false,
                    },
                    SelectItem {
                        label: "Force rewrite".to_string(),
                        value: "force-rewrite".to_string(),
                        selected: false,
                    },
                ];
                self.active_dialog = Some(Dialog::select_single(
                    "Metaedit",
                    format!("Edit metadata for {}", short),
                    items,
                    None,
                    DialogCallback::MetaeditSelect {
                        commit_id,
                        change_id,
                    },
                ));
            }
            _ => {}
        }
    }

    fn handle_log_bookmark(&mut self, action: LogAction) {
        match action {
            LogAction::CreateBookmark { revision, name } => {
                self.execute_bookmark_create(&revision, &name);
            }
            LogAction::StartBookmarkDelete => self.start_bookmark_delete(),
            LogAction::StartBookmarkJump => self.start_bookmark_jump(),
            LogAction::AdvanceBookmark => {
                self.start_bookmark_advance();
            }
            _ => {}
        }
    }

    fn handle_log_git(&mut self, action: LogAction) {
        match action {
            LogAction::Fetch => self.start_fetch(),
            LogAction::StartPush => self.start_push(),
            LogAction::StartTrack => self.start_track(),
            _ => {}
        }
    }

    fn handle_log_rebase(&mut self, action: LogAction) {
        use crate::ui::components::{Dialog, DialogCallback};

        match action {
            LogAction::Rebase {
                source,
                destination,
                mode,
                skip_emptied,
                use_revset,
                simplify_parents,
            } => self.execute_rebase(
                &source,
                &destination,
                mode,
                skip_emptied,
                simplify_parents,
                use_revset,
            ),
            LogAction::Absorb => self.execute_absorb(),
            LogAction::StartParallelize(from_id) => {
                self.notify_info(format!("From: {}. Select end and press Enter", from_id));
            }
            LogAction::Parallelize { from, to } => {
                let from_short = &from[..8.min(from.len())];
                let to_short = &to[..8.min(to.len())];
                self.active_dialog = Some(Dialog::confirm(
                    "Parallelize",
                    format!("Parallelize {}::{}?", from_short, to_short),
                    None,
                    DialogCallback::Parallelize { from, to },
                ));
            }
            LogAction::ParallelizeSameRevision => {
                self.notify_info("Cannot parallelize single revision");
            }
            _ => {}
        }
    }

    fn handle_log_compare(&mut self, action: LogAction) {
        match action {
            LogAction::StartCompare(from_id) => {
                self.notify_info(format!("From: {}. Select 'To' and press Enter", from_id));
            }
            LogAction::Compare { ref from, ref to } => {
                let msg = format!("Comparing {} -> {}", from, to);
                self.open_compare_diff(from, to);
                if self.error_message.is_none() {
                    self.notify_info(&msg);
                }
            }
            LogAction::CompareSameRevision => {
                self.notify_info("Cannot compare revision with itself");
            }
            LogAction::StartInterdiff(from_id) => {
                self.notify_info(format!(
                    "Interdiff From: {}. Select 'To' and press Enter",
                    from_id
                ));
            }
            LogAction::Interdiff { ref from, ref to } => {
                let msg = format!("Interdiff {} -> {}", from, to);
                self.open_interdiff(from, to);
                if self.error_message.is_none() {
                    self.notify_info(&msg);
                }
            }
            LogAction::InterdiffSameRevision => {
                self.notify_info("Cannot interdiff revision with itself");
            }
            _ => {}
        }
    }

    fn handle_log_bisect(&mut self, action: LogAction) {
        use crate::ui::components::{Dialog, DialogCallback};

        match action {
            LogAction::StartBisect(bad_id) => {
                self.notify_info(format!(
                    "Bisect: Bad={}. Select Good and press Enter",
                    bad_id
                ));
            }
            LogAction::Bisect { good, bad } => {
                self.active_dialog = Some(Dialog::input(
                    "Bisect Command (empty = bash for manual test)",
                    "bash",
                    DialogCallback::BisectRun { good, bad },
                ));
            }
            LogAction::BisectSameRevision => {
                self.notify_info("Cannot bisect: same revision selected for good and bad");
            }
            _ => {}
        }
    }

    fn handle_log_misc(&mut self, action: LogAction) {
        match action {
            LogAction::NextChange => self.execute_next(),
            LogAction::PrevChange => self.execute_prev(),
            LogAction::ToggleReversed => {
                let selected_id = self
                    .log_view
                    .selected_change()
                    .map(|c| c.change_id.to_string());
                self.log_view.reversed = !self.log_view.reversed;
                let revset = self.log_view.current_revset.clone();
                self.refresh_log(revset.as_deref());
                if let Some(ref id) = selected_id
                    && !self.log_view.select_change_by_id(id)
                {
                    self.log_view.select_working_copy();
                }
                let label = if self.log_view.reversed {
                    "oldest first"
                } else {
                    "newest first"
                };
                self.notify_info(format!("Log order: {}", label));
            }
            _ => {}
        }
    }

    fn handle_bookmark_action(&mut self, action: BookmarkAction) {
        match action {
            BookmarkAction::None => {}
            BookmarkAction::Jump(change_id) => {
                self.execute_bookmark_jump(&change_id);
                self.go_to_view(View::Log);
            }
            BookmarkAction::Track(full_name) => {
                self.execute_track(&[full_name]);
            }
            BookmarkAction::Untrack(full_name) => {
                self.execute_untrack(&full_name);
            }
            BookmarkAction::Delete(name) => {
                self.execute_bookmark_delete(&[name]);
            }
            BookmarkAction::StartRename(old_name) => {
                self.bookmark_view.rename_state = Some(RenameState::new(old_name));
            }
            BookmarkAction::ConfirmRename { old_name, new_name } => {
                self.execute_bookmark_rename(&old_name, &new_name);
            }
            BookmarkAction::CancelRename => {
                // rename_state already cleared by BookmarkView
            }
            BookmarkAction::Forget(name) => {
                use crate::ui::components::{Dialog, DialogCallback};
                self.active_dialog = Some(Dialog::confirm(
                    "Forget Bookmark",
                    format!(
                        "Forget bookmark '{}'?\n\n\
                         This removes remote tracking.\n\
                         Use 'D' for local delete only.\n\
                         Undo with 'u' if needed.",
                        name
                    ),
                    None,
                    DialogCallback::BookmarkForget,
                ));
                self.pending_forget_bookmark = Some(name);
            }
            BookmarkAction::Move(name) => {
                self.start_bookmark_move(&name);
            }
            BookmarkAction::MoveUnavailable => {
                self.notify_info("Move is available only for local bookmarks");
            }
        }
    }

    fn handle_tag_action(&mut self, action: TagAction) {
        match action {
            TagAction::None => {}
            TagAction::Jump(change_id) => {
                self.jump_to_log(&change_id);
            }
            TagAction::StartCreate => {
                use crate::ui::components::{Dialog, DialogCallback};
                self.active_dialog = Some(Dialog::input(
                    "Create Tag",
                    "Create tag on @ (working copy)",
                    DialogCallback::TagCreate,
                ));
            }
            TagAction::Delete(name) => {
                use crate::ui::components::{Dialog, DialogCallback};
                self.active_dialog = Some(Dialog::confirm(
                    "Delete Tag",
                    format!("Delete tag '{}'?", name),
                    None,
                    DialogCallback::TagDelete { name: name.clone() },
                ));
            }
        }
    }

    fn handle_diff_action(&mut self, action: DiffAction) {
        match action {
            DiffAction::None => {}
            DiffAction::Back => {
                self.go_back();
            }
            DiffAction::OpenBlame { file_path } => {
                // Get the current change_id from diff_view for proper revision
                let revision = self.diff_view.as_ref().map(|v| v.revision.clone());
                self.open_blame(&file_path, revision.as_deref());
            }
            DiffAction::ShowNotification(message) => {
                self.notify_info(&message);
            }
            DiffAction::CopyToClipboard { full } => {
                self.copy_diff_to_clipboard(full);
            }
            DiffAction::ExportToFile => {
                self.export_diff_to_file();
            }
            DiffAction::CycleFormat => {
                self.cycle_diff_format();
            }
        }
    }

    fn handle_status_action(&mut self, action: StatusAction) {
        match action {
            StatusAction::None => {}
            StatusAction::ShowFileDiff {
                change_id,
                file_path,
            } => {
                self.open_diff_at_file(&change_id, &file_path);
            }
            StatusAction::OpenBlame { file_path } => {
                self.open_blame(&file_path, None);
            }
            StatusAction::Commit { message } => {
                self.execute_commit(&message);
            }
            StatusAction::JumpToConflict => {
                // Selection already moved by StatusView; no further action needed
            }
            StatusAction::RestoreFile { file_path } => {
                // Show confirm dialog before restoring
                use crate::ui::components::{Dialog, DialogCallback};
                self.active_dialog = Some(Dialog::confirm(
                    "Restore File",
                    format!(
                        "Restore '{}'?\nThis discards your changes to this file.",
                        file_path
                    ),
                    Some("Undo with 'u' if needed.".to_string()),
                    DialogCallback::RestoreFile {
                        file_path: file_path.clone(),
                    },
                ));
            }
            StatusAction::RestoreAll => {
                use crate::ui::components::{Dialog, DialogCallback};
                self.active_dialog = Some(Dialog::confirm(
                    "Restore All Files",
                    "Restore all files?\nThis discards ALL your changes in the working copy.",
                    Some("Undo with 'u' if needed.".to_string()),
                    DialogCallback::RestoreAll,
                ));
            }
            StatusAction::DiffEdit { file_path } => {
                self.execute_diffedit("@", Some(&file_path));
            }
        }
    }

    fn handle_operation_action(&mut self, action: OperationAction) {
        match action {
            OperationAction::None => {}
            OperationAction::Back => {
                self.go_back();
            }
            OperationAction::Restore(operation_id) => {
                self.execute_op_restore(&operation_id);
            }
        }
    }

    fn handle_resolve_action(&mut self, action: ResolveAction) {
        match action {
            ResolveAction::None => {}
            ResolveAction::Back => {
                self.resolve_view = None;
                // Mark log dirty so conflict indicators update on return
                self.dirty.log = true;
                self.dirty.op_log = true;
                self.go_back();
            }
            ResolveAction::ResolveExternal(file_path) => {
                self.execute_resolve_external(&file_path);
            }
            ResolveAction::ResolveOurs(file_path) => {
                self.execute_resolve_ours(&file_path);
            }
            ResolveAction::ResolveTheirs(file_path) => {
                self.execute_resolve_theirs(&file_path);
            }
            ResolveAction::ShowDiff(file_path) => {
                // Open diff for the change, jumping to the file
                let revision = self
                    .resolve_view
                    .as_ref()
                    .map(|v| v.revision.clone())
                    .unwrap_or_default();
                self.open_diff_at_file(&revision, &file_path);
            }
        }
    }

    fn handle_evolog_action(&mut self, action: EvologAction) {
        match action {
            EvologAction::None => {}
            EvologAction::Back => {
                self.go_back();
            }
            EvologAction::OpenDiff(change_id) => {
                self.open_diff(&change_id);
            }
        }
    }

    fn handle_command_history_action(&mut self, action: CommandHistoryAction) {
        match action {
            CommandHistoryAction::None => {}
            CommandHistoryAction::Back => {
                self.go_back();
            }
            CommandHistoryAction::ToggleDetail(_) => {
                // Detail toggle is handled internally by CommandHistoryView
            }
        }
    }

    fn handle_blame_action(&mut self, action: BlameAction) {
        match action {
            BlameAction::None => {}
            BlameAction::Back => {
                self.go_back();
            }
            BlameAction::OpenDiff(change_id) => {
                self.open_diff(&change_id);
            }
            BlameAction::JumpToLog(change_id) => {
                self.jump_to_log(&change_id);
            }
        }
    }
}

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

    /// Simulate a key press on the App (through on_key_event, which runs
    /// handle_global_key before handle_view_key).
    fn press(app: &mut App, code: KeyCode) {
        app.on_key_event(KeyEvent::from(code));
    }

    /// Put App into Help view with search input active.
    fn enter_help_search(app: &mut App) {
        app.current_view = View::Help;
        app.help_search_input = true;
        app.help_input_buffer.clear();
    }

    // =========================================================================
    // Help search input: global key conflict tests
    // =========================================================================

    #[test]
    fn help_search_esc_cancels_search_not_back() {
        let mut app = App::new_for_test();
        enter_help_search(&mut app);

        press(&mut app, KeyCode::Esc);

        // Search input should be cancelled
        assert!(!app.help_search_input);
        // Should still be on Help view (not navigated back)
        assert_eq!(app.current_view, View::Help);
    }

    #[test]
    fn help_search_q_types_character_not_quit() {
        let mut app = App::new_for_test();
        enter_help_search(&mut app);

        press(&mut app, KeyCode::Char('q'));

        // 'q' should be added to input buffer
        assert_eq!(app.help_input_buffer, "q");
        // Should still be in search input mode
        assert!(app.help_search_input);
        // Should still be on Help view and running
        assert_eq!(app.current_view, View::Help);
        assert!(app.running);
    }

    #[test]
    fn help_search_tab_stays_in_help_not_switch() {
        let mut app = App::new_for_test();
        enter_help_search(&mut app);

        press(&mut app, KeyCode::Tab);

        // Should still be on Help view (not switched to next view)
        assert_eq!(app.current_view, View::Help);
        // Should still be in search input mode
        assert!(app.help_search_input);
    }

    #[test]
    fn help_search_question_mark_stays_in_search() {
        let mut app = App::new_for_test();
        enter_help_search(&mut app);

        press(&mut app, KeyCode::Char('?'));

        // '?' should be added to input buffer (it's a Char)
        assert_eq!(app.help_input_buffer, "?");
        // Should still be in search input mode on Help view
        assert!(app.help_search_input);
        assert_eq!(app.current_view, View::Help);
    }

    #[test]
    fn help_search_enter_confirms_and_exits_input() {
        let mut app = App::new_for_test();
        enter_help_search(&mut app);
        app.help_input_buffer = "quit".to_string();

        press(&mut app, KeyCode::Enter);

        // Search input should be deactivated
        assert!(!app.help_search_input);
        // Query should be stored
        assert_eq!(app.help_search_query, Some("quit".to_string()));
        // Should still be on Help view
        assert_eq!(app.current_view, View::Help);
    }

    #[test]
    fn help_search_typing_multiple_chars() {
        let mut app = App::new_for_test();
        enter_help_search(&mut app);

        press(&mut app, KeyCode::Char('h'));
        press(&mut app, KeyCode::Char('e'));
        press(&mut app, KeyCode::Char('l'));
        press(&mut app, KeyCode::Char('p'));

        assert_eq!(app.help_input_buffer, "help");
        assert!(app.help_search_input);
    }

    #[test]
    fn help_search_backspace_removes_char() {
        let mut app = App::new_for_test();
        enter_help_search(&mut app);
        app.help_input_buffer = "test".to_string();

        press(&mut app, KeyCode::Backspace);

        assert_eq!(app.help_input_buffer, "tes");
        assert!(app.help_search_input);
    }

    #[test]
    fn help_search_ctrl_l_suppressed() {
        let mut app = App::new_for_test();
        enter_help_search(&mut app);

        // Ctrl+L should NOT trigger refresh while in search input mode
        app.on_key_event(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL));

        // Should still be in search input mode on Help view
        assert!(app.help_search_input);
        assert_eq!(app.current_view, View::Help);
    }

    #[test]
    fn help_normal_mode_esc_goes_back() {
        let mut app = App::new_for_test();
        app.go_to_view(View::Help); // sets previous_view to Log
        assert!(!app.help_search_input);

        press(&mut app, KeyCode::Esc);

        // Should navigate back (not Help anymore)
        assert_ne!(app.current_view, View::Help);
    }

    #[test]
    fn help_normal_mode_q_goes_back() {
        let mut app = App::new_for_test();
        app.go_to_view(View::Help);
        assert!(!app.help_search_input);

        press(&mut app, KeyCode::Char('q'));

        // Should go back from Help view
        assert_ne!(app.current_view, View::Help);
    }

    // =========================================================================
    // Bookmark chord (Phase 46-B)
    // =========================================================================

    #[test]
    fn chord_b_sets_pending_in_log_normal() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char('b'));
        assert_eq!(app.pending_chord, Some(KeyCode::Char('b')));
    }

    #[test]
    fn chord_b_then_esc_cancels() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char('b'));
        press(&mut app, KeyCode::Esc);
        assert_eq!(app.pending_chord, None);
    }

    #[test]
    fn chord_b_then_unknown_key_cancels() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char('b'));
        press(&mut app, KeyCode::Char('z'));
        assert_eq!(app.pending_chord, None);
    }

    #[test]
    fn chord_b_not_a_prefix_outside_log() {
        let mut app = App::new_for_test();
        app.current_view = View::Status;
        press(&mut app, KeyCode::Char('b'));
        assert_eq!(
            app.pending_chord, None,
            "b must not start a chord outside Log Normal mode"
        );
    }

    #[test]
    fn chord_pending_cleared_on_view_change() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char('b'));
        assert_eq!(app.pending_chord, Some(KeyCode::Char('b')));
        app.go_to_view(View::Status);
        assert_eq!(
            app.pending_chord, None,
            "view change must clear a pending chord"
        );
    }

    #[test]
    fn chord_b_then_ctrl_l_cancels_not_refresh() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char('b'));
        assert_eq!(app.pending_chord, Some(KeyCode::Char('b')));
        // Ctrl+L must cancel the chord (modified key), NOT trigger refresh
        // while leaving the chord pending.
        app.on_key_event(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL));
        assert_eq!(
            app.pending_chord, None,
            "Ctrl+L after b must cancel the chord, not leave it pending"
        );
    }

    #[test]
    fn chord_b_then_ctrl_r_cancels_not_redo() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char('b'));
        assert_eq!(app.pending_chord, Some(KeyCode::Char('b')));
        // Ctrl+R must cancel the chord, not run redo with the chord still pending.
        app.on_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
        assert_eq!(
            app.pending_chord, None,
            "Ctrl+R after b must cancel the chord, not leave it pending"
        );
    }

    // =========================================================================
    // Command palette (Phase 46-C)
    // =========================================================================

    #[test]
    fn palette_colon_opens_in_log_normal() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char(':'));
        assert!(app.palette_active);
    }

    #[test]
    fn palette_esc_closes() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char(':'));
        press(&mut app, KeyCode::Esc);
        assert!(!app.palette_active);
    }

    #[test]
    fn palette_typing_filters_and_resets_selection() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char(':'));
        press(&mut app, KeyCode::Char('f'));
        press(&mut app, KeyCode::Char('i'));
        press(&mut app, KeyCode::Char('x'));
        assert_eq!(app.palette_input, "fix");
        assert_eq!(app.palette_selected, 0);
    }

    #[test]
    fn palette_not_opened_outside_log() {
        let mut app = App::new_for_test();
        app.current_view = View::Status;
        press(&mut app, KeyCode::Char(':'));
        assert!(!app.palette_active);
    }

    #[test]
    fn palette_cleared_on_view_change() {
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char(':'));
        assert!(app.palette_active);
        app.go_to_view(View::Status);
        assert!(!app.palette_active);
    }

    #[test]
    fn palette_enter_dispatches_command_history() {
        // Core dispatch path: filter to a jj-independent command and Enter it.
        // `command-history` opens View::CommandHistory via the delegated key.
        let mut app = App::new_for_test();
        app.current_view = View::Log;
        press(&mut app, KeyCode::Char(':'));
        for c in "command-history".chars() {
            press(&mut app, KeyCode::Char(c));
        }
        press(&mut app, KeyCode::Enter);
        assert!(!app.palette_active, "palette closes after Enter");
        assert_eq!(
            app.current_view,
            View::CommandHistory,
            "Enter dispatched the command (via log_view.handle_key delegation)"
        );
    }
}
