//! End-to-end integration tests for the settings modal.
//!
//! Every new `SettingMeta` in `default_settings()` MUST add an entry to
//! `ALL_SETTINGS_EXERCISED` and a test for its keyboard AND mouse paths.
//!
//! Drives the modal directly through `SettingsModalState` + public
//! key/mouse handlers (same dispatch path as runtime, without chrome).

use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind};
use ratatui::layout::Rect;
use std::sync::Arc;

use kigi_shell::agent::config::UiConfig;
use kigi_tui::app::actions::Action;
use kigi_tui::settings::{
    EnumChoice, PagerLocalSnapshot, SettingCategory, SettingKind, SettingMeta, SettingOwner,
    SettingValue, SettingsRegistry,
};
use kigi_tui::views::settings_modal::{
    RowEntry, SettingsKeyOutcome, SettingsModalMode, SettingsModalState, handle_settings_key,
    handle_settings_mouse,
};

// ---------------------------------------------------------------------------
// Compile-time exhaustive matrix
// ---------------------------------------------------------------------------

/// Every setting exercised by this file. Must stay in sync with
/// `SettingsRegistry::defaults().all()`.
const ALL_SETTINGS_EXERCISED: &[&str] = &[
    "compact_mode",
    "screen_mode",
    "show_timestamps",
    "show_timeline",
    "simple_mode",
    "vim_mode",
    "remember_tool_approvals",
    "toolset.ask_user_question.timeout_enabled",
    "keep_text_selection",
    "theme",
    "auto_dark_theme",
    "auto_light_theme",
    "render_mermaid",
    "multiline_mode",
    "permission_mode",
    "default_model",
    "max_thoughts_width",
    "scroll_speed",
    "scroll_mode",
    "scroll_lines",
    "invert_scroll",
    "display_refresh_auto_cadence",
    "default_selected_permission",
    "plan_mode",
    "show_tips",
    "auto_update",
    "fork_secondary_model",
    "show_thinking_blocks",
    "prompt_suggestions",
    "group_tool_verbs",
    "collapsed_edit_blocks",
    "respect_manual_folds",
    "hunk_tracker_mode",
    // Contextual-hints group + its per-tip child toggles (exercised via the
    // group sub-sheet, not as top-level rows).
    "contextual_hints",
    "contextual_hints.undo",
    "contextual_hints.plan_mode",
    "contextual_hints.image_input",
    "contextual_hints.send_now",
    "contextual_hints.small_screen",
    "contextual_hints.word_select",
];

#[test]
fn every_registered_setting_is_exercised() {
    let reg = SettingsRegistry::defaults();
    let mut missing: Vec<&str> = Vec::new();
    for meta in reg.all() {
        if !ALL_SETTINGS_EXERCISED.contains(&meta.key) {
            missing.push(meta.key);
        }
    }
    assert!(
        missing.is_empty(),
        "settings registered but not exercised in tests/settings_e2e.rs: {missing:?}\n\
         Add a row to ALL_SETTINGS_EXERCISED + a keyboard test + a mouse test."
    );
}

#[test]
fn matrix_is_subset_of_registry() {
    // Reverse: catches stale entries left after a setting is removed.
    let reg = SettingsRegistry::defaults();
    let registered: std::collections::HashSet<&str> = reg.all().iter().map(|m| m.key).collect();
    for &key in ALL_SETTINGS_EXERCISED {
        assert!(
            registered.contains(key),
            "ALL_SETTINGS_EXERCISED contains `{key}` but no setting is registered with that key"
        );
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn make_state() -> SettingsModalState {
    SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        // auto_mode_gate on so the permission_mode picker shows the full catalog
        // (including Auto); the gate-off filtering is covered by a dedicated test.
        PagerLocalSnapshot {
            auto_mode_gate: true,
            ..PagerLocalSnapshot::default()
        },
    )
}

fn press(key: KeyCode) -> KeyEvent {
    KeyEvent {
        code: key,
        modifiers: KeyModifiers::NONE,
        kind: KeyEventKind::Press,
        state: crossterm::event::KeyEventState::NONE,
    }
}

fn press_with(key: KeyCode, mods: KeyModifiers) -> KeyEvent {
    KeyEvent {
        code: key,
        modifiers: mods,
        kind: KeyEventKind::Press,
        state: crossterm::event::KeyEventState::NONE,
    }
}

/// Find the row index of a given setting key in the modal's row list.
fn row_idx_for(state: &SettingsModalState, target: &str) -> usize {
    state
        .rows
        .iter()
        .position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == target))
        .unwrap_or_else(|| panic!("setting `{target}` not present in modal rows"))
}

/// Whether `key` is hidden inside a group sub-sheet (a `SettingKind::Group`
/// child). Such keys are NOT emitted as top-level rows (`build_rows` skips
/// them), so they can't be navigated to or `d`-reset directly — only toggled
/// inside the sub-sheet. Mirrors `build_rows`'s group-child skip.
fn is_group_child(reg: &SettingsRegistry, key: &str) -> bool {
    reg.all().iter().any(|m| match &m.kind {
        SettingKind::Group { children } => children.contains(&key),
        _ => false,
    })
}

/// Move keyboard selection forward to a given setting key via `j`.
/// Target must be at-or-after current selection (forward-only).
fn navigate_to(state: &mut SettingsModalState, target: &str) {
    let goal = row_idx_for(state, target);
    assert!(
        state.selected <= goal,
        "navigate_to(`{target}`): test misconfigured — target row {goal} is BEFORE current \
         selection {}; navigate_to only walks forward",
        state.selected,
    );
    let mut guard = 0;
    while state.selected != goal {
        let outcome = handle_settings_key(state, &press(KeyCode::Char('j')));
        if matches!(outcome, SettingsKeyOutcome::Unchanged) {
            panic!(
                "navigate_to(`{target}`): walked off the bottom of the row list \
                 without finding the target (currently at row {})",
                state.selected,
            );
        }
        guard += 1;
        if guard > 100 {
            panic!("navigate_to(`{target}`): runaway navigation (100+ keystrokes)");
        }
    }
}

/// Assert that `outcome` is the typed setter Action for the given bool key.
fn assert_set_bool_action(outcome: SettingsKeyOutcome, key: &str, expected: bool) {
    let action = match outcome {
        SettingsKeyOutcome::Action(a) => a,
        other => panic!(
            "expected typed setter Action for `{key}={expected}`, but no Action was emitted \
             — outcome was {other:?}. Likely causes: focused row isn't a Bool, registry skew, \
             or click target outside the row's hit-rect."
        ),
    };
    match (key, action) {
        ("compact_mode", Action::SetCompactMode(b)) => {
            assert_eq!(b, expected, "SetCompactMode value differs from expected")
        }
        ("show_timestamps", Action::SetTimestamps(b)) => {
            assert_eq!(b, expected, "SetTimestamps value differs from expected")
        }
        ("show_timeline", Action::SetTimeline(b)) => {
            assert_eq!(b, expected, "SetTimeline value differs from expected")
        }
        ("simple_mode", Action::SetSimpleMode(b)) => {
            assert_eq!(b, expected, "SetSimpleMode value differs from expected")
        }
        ("multiline_mode", Action::SetMultilineMode(b)) => {
            assert_eq!(b, expected, "SetMultilineMode value differs from expected")
        }
        ("vim_mode", Action::SetVimMode(b)) => {
            assert_eq!(b, expected, "SetVimMode value differs from expected")
        }
        ("remember_tool_approvals", Action::SetRememberToolApprovals(b)) => {
            assert_eq!(
                b, expected,
                "SetRememberToolApprovals value differs from expected"
            )
        }
        (
            "toolset.ask_user_question.timeout_enabled",
            Action::SetAskUserQuestionTimeoutEnabled(b),
        ) => {
            assert_eq!(
                b, expected,
                "SetAskUserQuestionTimeoutEnabled value differs from expected"
            )
        }

        ("show_tips", Action::SetShowTips(b)) => {
            assert_eq!(b, expected, "SetShowTips value differs from expected")
        }
        ("auto_update", Action::SetAutoUpdate(b)) => {
            assert_eq!(b, expected, "SetAutoUpdate value differs from expected")
        }
        ("respect_manual_folds", Action::SetRespectManualFolds(b)) => {
            assert_eq!(
                b, expected,
                "SetRespectManualFolds value differs from expected"
            )
        }
        ("show_thinking_blocks", Action::SetShowThinkingBlocks(b)) => {
            assert_eq!(
                b, expected,
                "SetShowThinkingBlocks value differs from expected"
            )
        }
        ("prompt_suggestions", Action::SetPromptSuggestions(b)) => {
            assert_eq!(
                b, expected,
                "SetPromptSuggestions value differs from expected"
            )
        }
        ("group_tool_verbs", Action::SetGroupToolVerbs(b)) => {
            assert_eq!(b, expected, "SetGroupToolVerbs value differs from expected")
        }
        ("collapsed_edit_blocks", Action::SetCollapsedEditBlocks(b)) => {
            assert_eq!(
                b, expected,
                "SetCollapsedEditBlocks value differs from expected"
            )
        }
        ("invert_scroll", Action::SetInvertScroll(b)) => {
            assert_eq!(b, expected, "SetInvertScroll value differs from expected")
        }
        ("display_refresh_auto_cadence", Action::SetDisplayRefreshAutoCadence(b)) => {
            assert_eq!(
                b, expected,
                "SetDisplayRefreshAutoCadence value differs from expected"
            )
        }
        (key, action) => panic!(
            "expected typed setter for `{key}={expected}`, got wrong Action variant: {action:?}"
        ),
    }
}

// ---------------------------------------------------------------------------
// Modal lifecycle — F2 / Esc / Ctrl+,
// ---------------------------------------------------------------------------

#[test]
fn f2_closes_modal_from_any_browse_position() {
    let mut s = make_state();
    let outcome = handle_settings_key(&mut s, &press(KeyCode::F(2)));
    assert!(matches!(outcome, SettingsKeyOutcome::Close));

    // Same expectation when focus is on a later row.
    let mut s = make_state();
    navigate_to(&mut s, "simple_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::F(2)));
    assert!(matches!(outcome, SettingsKeyOutcome::Close));
}

/// Esc in Browse mode is handled by the modal chrome, not the settings
/// handler — so the handler returns `Unchanged`.
#[test]
fn esc_in_browse_mode_is_chrome_handled_not_modal_handled() {
    let mut s = make_state();
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "Esc should fall through to the chrome, not be matched here: {outcome:?}"
    );
}

#[test]
fn ctrl_comma_closes_modal_in_kitty_terminals() {
    let mut s = make_state();
    let outcome = handle_settings_key(
        &mut s,
        &press_with(KeyCode::Char(','), KeyModifiers::CONTROL),
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Close));
}

#[test]
fn cmd_comma_closes_modal_on_macos() {
    // SUPER == Cmd on macOS.
    let mut s = make_state();
    let outcome = handle_settings_key(&mut s, &press_with(KeyCode::Char(','), KeyModifiers::SUPER));
    assert!(matches!(outcome, SettingsKeyOutcome::Close));
}

#[test]
fn esc_in_filter_mode_exits_filter_not_modal() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    assert!(matches!(s.mode, SettingsModalMode::FilterFocused));
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    // Esc inside filter should NOT close the modal.
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

// ---------------------------------------------------------------------------
// Per-setting keyboard paths
// ---------------------------------------------------------------------------

#[test]
fn space_on_compact_mode_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "compact_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "compact_mode", true);
}

#[test]
fn space_on_show_timestamps_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "show_timestamps");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "show_timestamps", false);
}

#[test]
fn space_on_show_timeline_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "show_timeline");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    // Toggling flips the default; derived so it follows SHOW_TIMELINE_DEFAULT.
    let default_on = UiConfig::default().show_timeline_enabled();
    assert_set_bool_action(outcome, "show_timeline", !default_on);
}

#[test]
fn space_on_simple_mode_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "simple_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "simple_mode", false);
}

#[test]
fn space_on_remember_tool_approvals_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "remember_tool_approvals");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    // Default is false, so toggling flips it on.
    assert_set_bool_action(outcome, "remember_tool_approvals", true);
}

/// The Ask-Question timeout row renders in Agent & Approval directly above
/// Plan Mode, reads the resolved default ON, and Space dispatches the typed
/// setter toggling it off.
#[test]
fn space_on_ask_user_question_timeout_dispatches_typed_setter() {
    let mut s = make_state();
    let row = row_idx_for(&s, "toolset.ask_user_question.timeout_enabled");
    assert_eq!(
        row_idx_for(&s, "plan_mode"),
        row + 1,
        "Ask-Question timeout must render directly above Plan Mode"
    );
    navigate_to(&mut s, "toolset.ask_user_question.timeout_enabled");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    // Default is true (timer armed), so toggling flips it off.
    assert_set_bool_action(outcome, "toolset.ask_user_question.timeout_enabled", false);
}

#[test]
fn enter_on_bool_row_also_toggles() {
    let mut s = make_state();
    navigate_to(&mut s, "compact_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "compact_mode", true);
}

/// The contextual-hints group: Enter opens the sub-sheet, j navigates the three
/// child toggles, Space toggles the focused child (default ON → false), Esc
/// returns to Browse. Exercises the group row + all three children.
#[test]
fn enter_on_contextual_hints_group_opens_sub_sheet_and_toggles_children() {
    let mut s = make_state();
    navigate_to(&mut s, "contextual_hints");

    let out = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(matches!(out, SettingsKeyOutcome::Changed));
    assert!(matches!(
        s.mode,
        SettingsModalMode::PickingGroup { child_idx: 0, .. }
    ));

    // child 0: undo.
    let out = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert!(
        matches!(
            out,
            SettingsKeyOutcome::Action(Action::SetContextualHintUndo(false))
        ),
        "Space on undo must toggle it off, got {out:?}",
    );
    // child 1: plan_mode.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('j')));
    let out = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert!(
        matches!(
            out,
            SettingsKeyOutcome::Action(Action::SetContextualHintPlanMode(false))
        ),
        "Space on plan_mode must toggle it off, got {out:?}",
    );
    // child 2: image_input.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('j')));
    let out = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert!(
        matches!(
            out,
            SettingsKeyOutcome::Action(Action::SetContextualHintImageInput(false))
        ),
        "Space on image_input must toggle it off, got {out:?}",
    );

    // Esc returns to Browse.
    let out = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(matches!(out, SettingsKeyOutcome::Changed));
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

/// Mouse parity for the group: clicking the group row's value column opens the
/// sub-sheet, and clicking a child row toggles it in one click.
#[test]
fn mouse_click_on_contextual_hints_group_opens_sub_sheet_and_toggles_child() {
    let mut s = make_state();
    synth_rects(&mut s);
    let group_row = row_idx_for(&s, "contextual_hints") as u16;

    // Click the value column (chevron) → opens the sub-sheet in one click.
    let out = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        group_row,
    );
    assert!(matches!(out, SettingsKeyOutcome::Changed));
    assert!(
        matches!(s.mode, SettingsModalMode::PickingGroup { .. }),
        "click on the group value column must open the sub-sheet, got {:?}",
        s.mode,
    );

    // Synthesize child hit-rects (the renderer doesn't run in tests) and click
    // the first child → toggles undo off in one click.
    s.picker_choice_rects = (0..3)
        .map(|i| Rect {
            x: 0,
            y: i as u16,
            width: 80,
            height: 1,
        })
        .collect();
    let out = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        1,
        0,
    );
    assert!(
        matches!(
            out,
            SettingsKeyOutcome::Action(Action::SetContextualHintUndo(false))
        ),
        "click on the first child must toggle undo off, got {out:?}",
    );
}

// ---------------------------------------------------------------------------
// Per-setting MOUSE paths (keyboard ↔ mouse parity)
// ---------------------------------------------------------------------------

/// Lay out enough row_rects so that `handle_settings_mouse` can resolve
/// a click to the desired row index. We bypass the renderer here because
/// the test doesn't run inside a real terminal.
fn synth_rects(state: &mut SettingsModalState) {
    state.list_area = Rect {
        x: 0,
        y: 0,
        width: 80,
        height: state.rows.len() as u16,
    };
    state.row_rects = (0..state.rows.len())
        .map(|i| Rect {
            x: 0,
            y: i as u16,
            width: 80,
            height: 1,
        })
        .collect();
    // Value column hit-rects on the right side of each row.
    state.value_hit_rects = (0..state.rows.len())
        .map(|i| Rect {
            x: 70,
            y: i as u16,
            width: 8,
            height: 1,
        })
        .collect();
}

/// Click on already-selected `compact_mode` toggles immediately.
#[test]
fn mouse_click_on_compact_mode_dispatches_typed_setter() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "compact_mode") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        5,
        row_y,
    );
    assert_set_bool_action(outcome, "compact_mode", true);
}

/// Click on an unselected row only selects it; a second click toggles.
#[test]
fn mouse_click_on_show_timestamps_two_stage_select_then_toggle() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "show_timestamps") as u16;

    // First click: select-only.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );

    // Second click on the SAME row should now toggle (already focused).
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_set_bool_action(outcome, "show_timestamps", false);
}

/// Value-column click toggles `show_timeline` in one click.
#[test]
fn mouse_click_on_show_timeline_indicator_toggles_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "show_timeline") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    let default_on = UiConfig::default().show_timeline_enabled();
    assert_set_bool_action(outcome, "show_timeline", !default_on);
}

/// Value-column click toggles `remember_tool_approvals` in one click.
#[test]
fn mouse_click_on_remember_tool_approvals_indicator_toggles_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "remember_tool_approvals") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert_set_bool_action(outcome, "remember_tool_approvals", true);
}

/// Value-column click toggles the Ask-Question timeout in one click.
#[test]
fn mouse_click_on_ask_user_question_timeout_indicator_toggles_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "toolset.ask_user_question.timeout_enabled") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert_set_bool_action(outcome, "toolset.ask_user_question.timeout_enabled", false);
}

/// Click on the value column toggles in one click regardless of selection.
#[test]
fn mouse_click_on_simple_mode_indicator_toggles_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "simple_mode") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert_set_bool_action(outcome, "simple_mode", false);
}

/// Click on the already-selected row body toggles.
#[test]
fn mouse_click_on_already_selected_row_body_toggles() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "compact_mode") as u16;
    // compact_mode is the initial selection, so a body click toggles.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_set_bool_action(outcome, "compact_mode", true);
}

/// Click anywhere on the focused row toggles.
#[test]
fn mouse_click_at_row_edge_on_focused_row_toggles() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "compact_mode") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        70, // far right, within list_area width=80
        row_y,
    );
    assert_set_bool_action(outcome, "compact_mode", true);
}

#[test]
fn mouse_click_on_header_is_a_no_op() {
    let mut s = make_state();
    synth_rects(&mut s);
    // The first header (Appearance) is at row index 0.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        5,
        0,
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
}

#[test]
fn mouse_click_outside_list_is_a_no_op() {
    let mut s = make_state();
    synth_rects(&mut s);
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        100, // outside list_area (width=80)
        0,
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
}

#[test]
fn mouse_scroll_down_advances_selection() {
    let mut s = make_state();
    synth_rects(&mut s);
    // Click inside list_area — synth_rects sets height = rows.len().
    let outcome = handle_settings_mouse(&mut s, MouseEventKind::ScrollDown, 5, 1);
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    // Scroll-down emits 3 advances. From the initial selection
    // (compact_mode), this lands 3 settings later. Later changes add more
    // settings between compact_mode and the end, so we compute the
    // expected landing row by inspecting the row list — keeps the
    // test stable across such additions.
    let setting_keys: Vec<&str> = s
        .rows
        .iter()
        .filter_map(|r| match r {
            RowEntry::Setting { key, .. } => Some(*key),
            _ => None,
        })
        .collect();
    let compact_pos = setting_keys
        .iter()
        .position(|k| *k == "compact_mode")
        .unwrap();
    let expected_key = setting_keys
        .get(compact_pos + 3)
        // If there are fewer than 3 settings after compact_mode, the
        // last setting absorbs all extra advances.
        .copied()
        .unwrap_or(*setting_keys.last().unwrap());
    match &s.rows[s.selected] {
        RowEntry::Setting { key, .. } => assert_eq!(*key, expected_key),
        _ => panic!("expected setting row after scroll"),
    }
}

#[test]
fn mouse_scroll_up_returns_selection_to_first() {
    let mut s = make_state();
    synth_rects(&mut s);
    // First advance to the bottom.
    let _ = handle_settings_mouse(&mut s, MouseEventKind::ScrollDown, 5, 1);
    // Then scroll back up.
    let outcome = handle_settings_mouse(&mut s, MouseEventKind::ScrollUp, 5, 1);
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    match &s.rows[s.selected] {
        RowEntry::Setting { key, .. } => assert_eq!(*key, "compact_mode"),
        _ => panic!("expected setting row at top"),
    }
}

// ---------------------------------------------------------------------------
// Filter mode
// ---------------------------------------------------------------------------

/// Filter mode accepts chars into `state.query` and must never leak
/// an `Action`.
#[test]
fn slash_enters_filter_mode_and_chars_go_to_query_no_action_leak() {
    let mut s = make_state();
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(matches!(s.mode, SettingsModalMode::FilterFocused));

    for c in "compact density".chars() {
        let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
        match outcome {
            SettingsKeyOutcome::Changed | SettingsKeyOutcome::Unchanged => {}
            SettingsKeyOutcome::Action(a) => {
                panic!("filter mode leaked Action({a:?}) for char {c:?}");
            }
            SettingsKeyOutcome::ActionPair(a, b) => {
                panic!("filter mode leaked ActionPair({a:?}, {b:?}) for char {c:?}");
            }
            SettingsKeyOutcome::Close => {
                panic!("filter mode unexpectedly closed on char {c:?}");
            }
        }
    }
    assert_eq!(s.query, "compact density");

    let reg = SettingsRegistry::defaults();
    let hits = reg.search(&s.query);
    assert_eq!(hits.len(), 1);
    assert_eq!(hits[0].key, "compact_mode");
}

/// `Ctrl+,` closes the modal from FilterFocused mode.
#[test]
fn ctrl_comma_from_filter_mode_closes_modal() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    assert!(matches!(s.mode, SettingsModalMode::FilterFocused));
    let outcome = handle_settings_key(
        &mut s,
        &press_with(KeyCode::Char(','), KeyModifiers::CONTROL),
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Close));
}

/// F2 from FilterFocused mode also closes.
#[test]
fn f2_from_filter_mode_closes_modal() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    assert!(matches!(s.mode, SettingsModalMode::FilterFocused));
    let outcome = handle_settings_key(&mut s, &press(KeyCode::F(2)));
    assert!(matches!(outcome, SettingsKeyOutcome::Close));
}

/// "stamp" matches only `show_timestamps` — narrows to header + setting.
#[test]
fn filter_query_stamp_matches_show_timestamps_only() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    let filtered = s.filtered_indices();
    assert_eq!(
        filtered.len(),
        2,
        "expected exactly 2 visible rows (Appearance header + show_timestamps), \
         got {filtered:?}"
    );
    // Header must come before its child setting.
    match &s.rows[filtered[0]] {
        RowEntry::Header { category } => assert_eq!(*category, SettingCategory::Appearance),
        other => panic!("expected Header at filtered[0], got {other:?}"),
    }
    match &s.rows[filtered[1]] {
        RowEntry::Setting { key, .. } => assert_eq!(*key, "show_timestamps"),
        other => panic!("expected Setting at filtered[1], got {other:?}"),
    }
}

/// Non-matching query produces an empty filtered set.
#[test]
fn filter_query_nonexistent_shows_zero_settings() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "xyzzy-no-match".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    assert!(
        s.filtered_indices().is_empty(),
        "nonexistent query should produce empty filtered set, got {:?}",
        s.filtered_indices(),
    );
}

/// Empty query shows all rows.
#[test]
fn filter_empty_query_shows_all_rows() {
    let s = make_state();
    let filtered = s.filtered_indices();
    let expected: Vec<usize> = (0..s.rows.len()).collect();
    assert_eq!(
        filtered, expected,
        "empty-query filtered set must equal (0..rows.len())"
    );
}

/// Esc in filter mode clears the query and returns to Browse.
#[test]
fn filter_esc_clears_query_and_returns_to_browse() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    assert_eq!(s.query, "stamp");
    assert_eq!(s.filtered_indices().len(), 2);

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(matches!(s.mode, SettingsModalMode::Browse));
    assert_eq!(s.query, "", "Esc must clear the query");
    // Filter is inert again — full set restored in original order.
    let expected: Vec<usize> = (0..s.rows.len()).collect();
    assert_eq!(
        s.filtered_indices(),
        expected.as_slice(),
        "Esc must restore filtered_indices to (0..rows.len()) — full set in row-order"
    );
}

/// Navigation under filter walks only the filtered subset.
/// Up must not land on a header.
#[test]
fn filter_navigation_lands_on_filtered_subset_only() {
    let mut s = make_state();
    // Sanity check: initial selection is compact_mode (row 1).
    let compact_idx = row_idx_for(&s, "compact_mode");
    let show_ts_idx = row_idx_for(&s, "show_timestamps");
    assert_eq!(s.selected, compact_idx);

    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    // After filtering for "stamp", compact_mode is hidden — selection
    // snaps to the only remaining setting (show_timestamps).
    assert_eq!(
        s.selected, show_ts_idx,
        "selection should snap to show_timestamps when compact_mode is filtered out"
    );

    // Down arrow shouldn't move (only one setting in the filter).
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Down));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "Down at last visible setting should be Unchanged, got {outcome:?}"
    );
    assert_eq!(s.selected, show_ts_idx);

    // Up arrow shouldn't move either (only one setting in the filter,
    // and headers are not selectable).
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Up));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "Up at first visible setting should be Unchanged, got {outcome:?}"
    );
    assert_eq!(s.selected, show_ts_idx);

    // Up must not land on a header.
    let appearance_header_idx = s
        .rows
        .iter()
        .position(|r| {
            matches!(
                r,
                RowEntry::Header {
                    category: SettingCategory::Appearance
                }
            )
        })
        .expect("Appearance header must exist in default registry");
    assert_ne!(
        s.selected, appearance_header_idx,
        "Up arrow must NOT land on a header (regression: header became selectable)"
    );
}

/// Backspace pops a char and re-broadens the visible set.
#[test]
fn filter_backspace_broadens_visible_set() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    let narrowed = s.filtered_indices().len();
    assert_eq!(
        narrowed, 2,
        "expected narrowed filter (header + show_timestamps)"
    );

    // Pop trailing 'p' → "stam". Still matches only show_timestamps
    // (substring of "timestamps"); same 2 visible rows.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace));
    assert_eq!(s.query, "stam");
    assert_eq!(s.filtered_indices().len(), 2);

    // Empty the query out — now everything is visible. We pop one at
    // a time and check at each step that the cache regenerates
    // (rather than just shrinks).
    let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace)); // → "sta"
    assert_eq!(s.query, "sta");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace)); // → "st"
    assert_eq!(s.query, "st");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace)); // → "s"
    assert_eq!(s.query, "s");
    // "s" matches multiple settings (compact_mode via "messages" in
    // its description, show_timestamps via "show"/"timestamps",
    // simple_mode via "simple"/"ascii"). So the filtered set MUST be
    // larger than 2 here — proving each Backspace re-broadens.
    let setting_count_at_s = s
        .filtered_indices()
        .iter()
        .filter(|&&i| matches!(s.rows[i], RowEntry::Setting { .. }))
        .count();
    assert!(
        setting_count_at_s >= 2,
        "popping query from 'stam' to 's' must re-broaden — \
         expected >=2 visible settings, got {setting_count_at_s} \
         (filtered = {:?})",
        s.filtered_indices()
    );

    // Final pop → "". Filter inert, full set restored in order.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace));
    assert_eq!(s.query, "");
    let expected: Vec<usize> = (0..s.rows.len()).collect();
    assert_eq!(
        s.filtered_indices(),
        expected.as_slice(),
        "empty query must re-broaden to (0..rows.len()) in row order",
    );
}

/// Multi-keyword AND query narrows correctly.
#[test]
fn filter_with_multiple_matches_navigates_between_settings() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    // "compact" is a keyword on compact_mode; "simple" on simple_mode.
    // Use an OR-of-substrings approach: the query "mode" alone matches
    // too many things now (theme descriptions contain "mode"). We
    // instead use two distinct keyword matches separated by a single
    // word that's not in the theme catalog — testing the multi-word
    // AND behavior on a tight set.
    for c in "ascii minimal".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    // Both keywords are on simple_mode → exactly one setting matches.
    let filtered = s.filtered_indices();
    let setting_keys: Vec<&str> = filtered
        .iter()
        .filter_map(|&i| match &s.rows[i] {
            RowEntry::Setting { key, .. } => Some(*key),
            _ => None,
        })
        .collect();
    assert_eq!(setting_keys, vec!["simple_mode"]);

    // Selection should snap to the only visible setting.
    let simple_idx = row_idx_for(&s, "simple_mode");
    assert_eq!(s.selected, simple_idx);

    // Down at the only visible setting is Unchanged.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Down));
    assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
    assert_eq!(s.selected, simple_idx);

    // Up at the only visible setting is Unchanged (headers are not
    // selectable).
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Up));
    assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
    assert_eq!(s.selected, simple_idx);

    // Drop the second keyword (Backspace x8 to remove "minimal" — 7
    // chars + 1 space). Now "ascii" alone still matches only
    // simple_mode but we're back to a single-keyword filter that
    // doesn't ambiguously broaden. Asserts the "filter still narrows
    // correctly when one keyword drops out" property.
    for _ in 0..8 {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Backspace));
    }
    assert_eq!(s.query, "ascii");
    let filtered_after_pop = s.filtered_indices();
    let after_pop_keys: Vec<&str> = filtered_after_pop
        .iter()
        .filter_map(|&i| match &s.rows[i] {
            RowEntry::Setting { key, .. } => Some(*key),
            _ => None,
        })
        .collect();
    assert_eq!(after_pop_keys, vec!["simple_mode"]);
}

// ---------------------------------------------------------------------------
// Filter mode — Enter commits with preserved query
// ---------------------------------------------------------------------------

/// Enter in FilterFocused exits filter focus and preserves the query.
#[test]
fn filter_enter_commits_and_preserves_query() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    let show_ts_idx = row_idx_for(&s, "show_timestamps");
    assert!(matches!(s.mode, SettingsModalMode::FilterFocused));
    assert_eq!(s.query, "stamp");
    assert_eq!(s.selected, show_ts_idx);

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter must exit FilterFocused"
    );
    assert_eq!(s.query, "stamp", "Enter must PRESERVE the query (Issue 1)");
    assert_eq!(
        s.filtered_indices().len(),
        2,
        "filter must remain active after Enter-commit"
    );
    assert_eq!(s.selected, show_ts_idx);

    // And now the user can toggle directly without re-navigating.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "show_timestamps", false);
}

/// Backspace in Browse continues editing the preserved query.
#[test]
fn browse_backspace_pops_query_after_filter_commit() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    // Commit
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(matches!(s.mode, SettingsModalMode::Browse));
    assert_eq!(s.query, "stamp");

    // Backspace in Browse pops one char, stays in Browse, re-runs
    // invalidate_filter.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Backspace));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(matches!(s.mode, SettingsModalMode::Browse));
    assert_eq!(s.query, "stam");
    assert_eq!(s.filtered_indices().len(), 2);

    // Backspace on empty query is Unchanged (and the query stays "").
    s.query.clear();
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Backspace));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "Backspace on empty query must be Unchanged"
    );
}

/// PageDown/PageUp navigate in filter mode (advance x10 per press).
#[test]
fn filter_pageup_pagedown_navigates_in_filter_mode() {
    let mut s = make_state();
    // Enter filter mode without typing — filtered_cache stays full.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    assert!(matches!(s.mode, SettingsModalMode::FilterFocused));

    // PageDown from compact_mode advances toward the last row.
    let compact_idx = row_idx_for(&s, "compact_mode");
    let last_idx = s
        .rows
        .iter()
        .rposition(|r| matches!(r, RowEntry::Setting { .. }))
        .expect("default registry must contain at least one setting");
    assert_eq!(s.selected, compact_idx);

    // Compute how many PageDown presses are needed to reach the
    // last row (each press advances by 10 selectable rows; round
    // up so we definitely reach it). A single press isn't enough
    // once the registry exceeds ~10 settings.
    let total_selectable = s
        .rows
        .iter()
        .filter(|r| matches!(r, RowEntry::Setting { .. }))
        .count();
    let presses = total_selectable.div_ceil(10).max(1);
    for _ in 0..presses {
        let _ = handle_settings_key(&mut s, &press(KeyCode::PageDown));
    }
    assert_eq!(
        s.selected, last_idx,
        "PageDown × {presses} in filter must reach last selectable row",
    );

    // PageUp returns to the top.
    for _ in 0..presses {
        let _ = handle_settings_key(&mut s, &press(KeyCode::PageUp));
    }
    assert_eq!(s.selected, compact_idx);
}

// ---------------------------------------------------------------------------
// Filter mode — g/G filter-aware navigation
// ---------------------------------------------------------------------------

/// `g` lands on the first selectable row (not a header).
#[test]
fn g_jumps_to_first_visible_setting() {
    let mut s = make_state();
    // Move to the last row first so g actually has to navigate.
    navigate_to(&mut s, "simple_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char('g')));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    let first_setting_idx = row_idx_for(&s, "compact_mode");
    assert_eq!(
        s.selected, first_setting_idx,
        "`g` must land on first selectable row (compact_mode), not a header"
    );
}

/// `G` lands on the last selectable row.
#[test]
fn shift_g_jumps_to_last_visible_setting() {
    let mut s = make_state();
    // Default selection is compact_mode (first setting).
    let outcome = handle_settings_key(&mut s, &press_with(KeyCode::Char('G'), KeyModifiers::SHIFT));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    // Compute the last setting row in row-order at runtime.
    let last_setting_idx = s
        .rows
        .iter()
        .rposition(|r| matches!(r, RowEntry::Setting { .. }))
        .expect("default registry must contain at least one setting");
    assert_eq!(
        s.selected, last_setting_idx,
        "`G` must land on last selectable row in the registry"
    );
}

/// `g` respects the active filter — lands on the first visible setting.
#[test]
fn g_jumps_to_first_filtered_row_under_active_filter() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    // Commit filter into Browse with query preserved.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(matches!(s.mode, SettingsModalMode::Browse));

    // Selection is already on show_timestamps (snapped by
    // clamp_selected_to_visible). g should be a no-op (Unchanged)
    // because the first visible setting IS the current selection.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char('g')));
    let show_ts_idx = row_idx_for(&s, "show_timestamps");
    assert_eq!(s.selected, show_ts_idx);
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "g on the already-first-visible row should be Unchanged"
    );
}

/// `G` respects the active filter — lands on the last visible setting.
#[test]
fn shift_g_jumps_to_last_filtered_row_under_active_filter() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));

    let outcome = handle_settings_key(&mut s, &press_with(KeyCode::Char('G'), KeyModifiers::SHIFT));
    let show_ts_idx = row_idx_for(&s, "show_timestamps");
    assert_eq!(s.selected, show_ts_idx);
    // G must NOT land on compact_mode or simple_mode (both hidden).
    let compact_idx = row_idx_for(&s, "compact_mode");
    let simple_idx = row_idx_for(&s, "simple_mode");
    assert_ne!(s.selected, compact_idx, "G must not land on hidden row");
    assert_ne!(s.selected, simple_idx, "G must not land on hidden row");
    // Outcome is Unchanged because show_timestamps is already last.
    assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
}

// ---------------------------------------------------------------------------
// Selection stability
// ---------------------------------------------------------------------------

/// Filter keeps selection when the focused row remains visible.
#[test]
fn filter_keeps_selection_when_currently_selected_row_remains_visible() {
    let mut s = make_state();
    navigate_to(&mut s, "show_timestamps");
    let show_ts_idx = s.selected;

    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    assert_eq!(
        s.selected, show_ts_idx,
        "selection should NOT move — show_timestamps was already focused and matches the filter"
    );
}

// ---------------------------------------------------------------------------
// Multi-word AND semantics
// ---------------------------------------------------------------------------

/// One unmatched word in a multi-word query empties the result (AND).
#[test]
fn filter_multi_word_with_one_unmatched_word_shows_zero_settings() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    // "compact" alone matches compact_mode; appending an unmatched
    // word forces AND→empty.
    for c in "compact xyzzy".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    assert!(
        s.filtered_indices().is_empty(),
        "AND across words: one unmatched word should empty the result, got {:?}",
        s.filtered_indices()
    );
}

/// AND narrows strictly: adding an unmatched word yields 0 results.
#[test]
fn filter_and_semantics_narrow_strictly() {
    let reg = SettingsRegistry::defaults();
    // "ascii" matches only simple_mode (keyword).
    let single = reg.search("ascii");
    assert_eq!(single.len(), 1);
    assert_eq!(single[0].key, "simple_mode");
    // "ascii minimal" — both simple_mode keywords. Still 1 match.
    let conjunction = reg.search("ascii minimal");
    assert_eq!(conjunction.len(), 1);
    assert_eq!(conjunction[0].key, "simple_mode");
    // Adding an unmatched word must yield 0.
    let with_unmatched = reg.search("ascii xyzzy");
    assert!(
        with_unmatched.is_empty(),
        "AND with unmatched word must yield empty, got {:?}",
        with_unmatched.iter().map(|m| m.key).collect::<Vec<_>>()
    );
}

// ---------------------------------------------------------------------------
// Mouse parity under active filter
// ---------------------------------------------------------------------------

/// Lay out `row_rects` for filtered state — only visible rows get rects.
fn synth_rects_filtered(state: &mut SettingsModalState) {
    let filter: Vec<usize> = state.filtered_indices().to_vec();
    state.list_area = Rect {
        x: 0,
        y: 0,
        width: 80,
        height: filter.len() as u16,
    };
    state.row_rects = vec![Rect::default(); state.rows.len()];
    state.value_hit_rects = vec![Rect::default(); state.rows.len()];
    for (pos, &row_idx) in filter.iter().enumerate() {
        state.row_rects[row_idx] = Rect {
            x: 0,
            y: pos as u16,
            width: 80,
            height: 1,
        };
        state.value_hit_rects[row_idx] = Rect {
            x: 70,
            y: pos as u16,
            width: 8,
            height: 1,
        };
    }
}

/// Mouse click on a visible filtered row toggles it.
#[test]
fn mouse_click_on_visible_filtered_row_toggles() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    // Commit so we can click without typing.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    synth_rects_filtered(&mut s);
    // show_timestamps is selected (snapped). filtered layout:
    // pos 0 = Appearance header, pos 1 = show_timestamps.
    let y_in_filter = 1u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        y_in_filter,
    );
    assert_set_bool_action(outcome, "show_timestamps", false);
}

/// Click at a filtered-out row position is a no-op.
#[test]
fn mouse_click_at_filtered_out_row_position_is_no_op() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    synth_rects_filtered(&mut s);
    // y=2 would be compact_mode in an unfiltered layout but is outside
    // the filtered list_area (height=2). Click should be Unchanged.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        2,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "click outside filtered list_area must be Unchanged, got {outcome:?}"
    );
}

// ---------------------------------------------------------------------------
// Filter-rebuild timing — cache stability
// ---------------------------------------------------------------------------

/// `filtered_indices()` is a stable borrow, not regenerated per call.
#[test]
fn filter_cache_pointer_is_stable_across_reads() {
    let s = make_state();
    let ptr1 = s.filtered_indices().as_ptr();
    let ptr2 = s.filtered_indices().as_ptr();
    assert_eq!(
        ptr1, ptr2,
        "filtered_indices() should be a stable borrow, not a fresh allocation per read"
    );
}

/// `filtered_cache` is regenerated when the query mutates.
#[test]
fn filter_cache_pointer_changes_on_query_mutation() {
    let mut s = make_state();
    let ptr_before = s.filtered_indices().as_ptr();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    // Filter mode entry alone doesn't mutate query. Type a char to
    // force `invalidate_filter`.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('s')));
    let ptr_after = s.filtered_indices().as_ptr();
    assert_ne!(
        ptr_before, ptr_after,
        "filtered_cache must be regenerated on query mutation"
    );
}

// ---------------------------------------------------------------------------
// Render with filter — translation + "No matches"
// ---------------------------------------------------------------------------

/// `scroll_offset` stays within `filtered_indices()` bounds under filter.
#[test]
fn render_with_filter_active_and_small_viewport_clamps_scroll() {
    use ratatui::buffer::Buffer;
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "stamp".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    let area = Rect {
        x: 0,
        y: 0,
        width: 80,
        height: 12,
    };
    let mut buf = Buffer::empty(area);
    kigi_tui::views::settings_modal::render_settings_modal(&mut buf, area, &mut s, false, None);
    let visible = s.filtered_indices().len();
    assert!(
        s.scroll_offset <= visible.saturating_sub(1),
        "scroll_offset ({}) must be within filtered_indices bounds ({})",
        s.scroll_offset,
        visible
    );
}

/// "No matches" placeholder renders and echoes the query.
#[test]
fn render_no_matches_placeholder_includes_query() {
    use ratatui::buffer::Buffer;
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "xyzzy".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    assert!(s.filtered_indices().is_empty());
    // Larger area than the modal-min so the chrome + search bar +
    // footer all fit and the empty-state placeholder lands in the
    // remaining content area.
    let area = Rect {
        x: 0,
        y: 0,
        width: 120,
        height: 30,
    };
    let mut buf = Buffer::empty(area);
    kigi_tui::views::settings_modal::render_settings_modal(&mut buf, area, &mut s, false, None);
    // Scan all cells for the substring "No matches" and "xyzzy".
    let mut all_text = String::new();
    for y in 0..area.height {
        for x in 0..area.width {
            if let Some(cell) = buf.cell((x, y)) {
                all_text.push_str(cell.symbol());
            }
        }
        all_text.push('\n');
    }
    assert!(
        all_text.contains("No matches"),
        "rendered buffer must contain 'No matches' placeholder, got:\n{all_text}"
    );
    assert!(
        all_text.contains("xyzzy"),
        "rendered buffer must echo the query, got:\n{all_text}"
    );
}

// ---------------------------------------------------------------------------
// PickingEnum / EditingValue Esc routing
// ---------------------------------------------------------------------------

/// Esc in `PickingEnum` returns to Browse.
#[test]
fn esc_in_picking_enum_mode_returns_to_browse() {
    let mut s = make_state();
    s.mode = SettingsModalMode::PickingEnum {
        key: "compact_mode",
        choices_idx: 0,
        original_value: SettingValue::Bool(false),
        supports_preview: true,
    };
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

/// Esc in `EditingValue` returns to Browse.
#[test]
fn esc_in_editing_value_mode_returns_to_browse() {
    let mut s = make_state();
    s.mode = SettingsModalMode::EditingValue {
        key: "compact_mode",
        buffer: String::new(),
        cursor_byte: 0,
        validation_error: None,
    };
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

// ---------------------------------------------------------------------------
// Registry contracts
// ---------------------------------------------------------------------------

/// Pins which keys belong to each SettingKind.
#[test]
fn registry_kind_membership_through_pr_14() {
    let reg = SettingsRegistry::defaults();
    let mut by_kind: std::collections::BTreeMap<&str, Vec<&str>> =
        std::collections::BTreeMap::new();
    for meta in reg.all() {
        let kind_tag = match &meta.kind {
            SettingKind::Bool { .. } => "Bool",
            SettingKind::String { .. } => "String",
            SettingKind::Enum { .. } => "Enum",
            SettingKind::Int { .. } => "Int",

            SettingKind::DynamicEnum { .. } => "DynamicEnum",
            SettingKind::Group { .. } => "Group",
            other => panic!(
                "registry_kind_membership: setting `{}` has unknown kind {:?} — \
                 add an arm here AND a kind-membership assertion below",
                meta.key, other,
            ),
        };
        by_kind.entry(kind_tag).or_default().push(meta.key);
    }
    for keys in by_kind.values_mut() {
        keys.sort();
    }

    let bool_keys = by_kind.remove("Bool").unwrap_or_default();
    assert_eq!(
        bool_keys,
        vec![
            "compact_mode",
            "group_tool_verbs",
            "collapsed_edit_blocks",
            "invert_scroll",
            "display_refresh_auto_cadence",
            "multiline_mode",
            "prompt_suggestions",
            "respect_manual_folds",
            "show_thinking_blocks",
            "show_timeline",
            "show_timestamps",
            "simple_mode",
            "vim_mode",
            "remember_tool_approvals",
            "toolset.ask_user_question.timeout_enabled",
            "auto_update",
            "show_tips",
            // Per-tip contextual-hint children (hidden from the top-level list,
            // toggled inside the group sub-sheet) are still Bool settings.
            "contextual_hints.undo",
            "contextual_hints.plan_mode",
            "contextual_hints.image_input",
            "contextual_hints.send_now",
            "contextual_hints.small_screen",
            "contextual_hints.word_select",
        ]
        .into_iter()
        .collect::<std::collections::BTreeSet<_>>()
        .into_iter()
        .collect::<Vec<_>>(),
        "Bool kind membership drift",
    );

    let enum_keys = by_kind.remove("Enum").unwrap_or_default();
    assert_eq!(
        enum_keys,
        vec![
            "auto_dark_theme",
            "auto_light_theme",
            "default_selected_permission",
            "hunk_tracker_mode",
            "keep_text_selection",
            "permission_mode",
            "plan_mode",
            "render_mermaid",
            "screen_mode",
            "scroll_mode",
            "theme",
        ],
        "Enum kind membership drift",
    );

    let string_keys = by_kind.remove("String").unwrap_or_default();
    assert!(
        string_keys.is_empty(),
        "no String-kind settings should remain — `default_model` + `fork_secondary_model` \
         migrated to DynamicEnum; got: {string_keys:?}",
    );

    let dynamic_enum_keys = by_kind.remove("DynamicEnum").unwrap_or_default();
    assert_eq!(
        dynamic_enum_keys,
        vec!["default_model", "fork_secondary_model",],
        "DynamicEnum kind membership drift",
    );

    let int_keys = by_kind.remove("Int").unwrap_or_default();
    let mut sorted_int = int_keys.clone();
    sorted_int.sort();
    assert_eq!(
        sorted_int,
        vec!["max_thoughts_width", "scroll_lines", "scroll_speed"],
        "Int kind membership drift (PR 8)",
    );

    let group_keys = by_kind.remove("Group").unwrap_or_default();
    assert_eq!(
        group_keys,
        vec!["contextual_hints"],
        "Group kind membership drift",
    );

    // No unexpected kinds.
    assert!(
        by_kind.is_empty(),
        "registry contains unexpected SettingKind variants: {by_kind:?}",
    );
}

/// Pins the set of Enum-kind settings (sorted, order-independent).
#[test]
fn enum_settings_membership_through_pr_14() {
    let reg = SettingsRegistry::defaults();
    let mut enum_keys: Vec<&str> = reg
        .all()
        .iter()
        .filter(|m| matches!(m.kind, SettingKind::Enum { .. }))
        .map(|m| m.key)
        .collect();
    enum_keys.sort();
    assert_eq!(
        enum_keys,
        vec![
            "auto_dark_theme",
            "auto_light_theme",
            "default_selected_permission",
            "hunk_tracker_mode",
            "keep_text_selection",
            "permission_mode",
            "plan_mode",
            "render_mermaid",
            "screen_mode",
            "scroll_mode",
            "theme",
        ],
    );
}

/// `current_value_for` and `default_value_for` must agree at
/// `UiConfig::default()` with independently hard-coded expectations.
#[test]
fn defaults_round_trip_through_registry() {
    use kigi_tui::settings::{SettingValue, current_value_for};
    let reg = SettingsRegistry::defaults();
    let ui = UiConfig::default();
    let pager = PagerLocalSnapshot::default();

    // `current_value_for` for these keys reads process-wide caches, not `ui`.
    // Reset to defaults so a sibling test on this worker thread can't leak in.
    kigi_tui::appearance::cache::set_keep_text_selection(
        kigi_tui::appearance::TextSelection::Flash,
    );
    kigi_tui::appearance::cache::set_show_thinking_blocks(true);
    kigi_tui::appearance::cache::set_prompt_suggestions(true);
    kigi_tui::appearance::cache::set_group_tool_verbs(true);
    kigi_tui::appearance::cache::set_scroll_mode(kigi_tui::appearance::ScrollMode::Auto);
    kigi_tui::appearance::cache::set_invert_scroll(false);
    // 3 = the registry default shown while the profile is in charge.
    kigi_tui::appearance::cache::set_scroll_lines(3);

    // Hard-coded per-key expectations (independent of registry).
    let expected = |key: &str| -> SettingValue {
        match key {
            "compact_mode" => SettingValue::Bool(false),
            "screen_mode" => SettingValue::Enum("fullscreen"),
            "show_timestamps" => SettingValue::Bool(true),
            "show_timeline" => SettingValue::Bool(false),
            "simple_mode" => SettingValue::Bool(true),
            "vim_mode" => SettingValue::Bool(false),
            "remember_tool_approvals" => SettingValue::Bool(false),
            "toolset.ask_user_question.timeout_enabled" => SettingValue::Bool(true),
            "keep_text_selection" => SettingValue::Enum("flash"),
            "theme" => SettingValue::Enum("kiginight"),
            "auto_dark_theme" => SettingValue::Enum("kiginight"),
            "auto_light_theme" => SettingValue::Enum("kigiday"),
            "render_mermaid" => SettingValue::Enum("auto"),
            "multiline_mode" => SettingValue::Bool(false),
            "permission_mode" => SettingValue::Enum("ask"),
            "default_model" => SettingValue::String(String::new()),
            "max_thoughts_width" => SettingValue::Int(120),
            "scroll_speed" => SettingValue::Int(50),
            "scroll_mode" => SettingValue::Enum("auto"),
            "scroll_lines" => SettingValue::Int(3),
            "invert_scroll" => SettingValue::Bool(false),
            "display_refresh_auto_cadence" => SettingValue::Bool(false),
            "default_selected_permission" => SettingValue::Enum("always_allow_all_sessions"),
            "hunk_tracker_mode" => SettingValue::Enum("agent_only"),
            "plan_mode" => SettingValue::Enum("off"),
            "show_tips" => SettingValue::Bool(true),
            "auto_update" => SettingValue::Bool(true),
            "fork_secondary_model" => SettingValue::String(String::new()),
            "show_thinking_blocks" => SettingValue::Bool(true),
            "prompt_suggestions" => SettingValue::Bool(true),
            "group_tool_verbs" => SettingValue::Bool(true),
            "collapsed_edit_blocks" => SettingValue::Bool(false),
            "respect_manual_folds" => SettingValue::Bool(false),
            // Per-tip contextual-hint children default ON (inherit → true).
            "contextual_hints.undo" => SettingValue::Bool(true),
            "contextual_hints.plan_mode" => SettingValue::Bool(true),
            "contextual_hints.image_input" => SettingValue::Bool(true),
            "contextual_hints.send_now" => SettingValue::Bool(true),
            "contextual_hints.small_screen" => SettingValue::Bool(true),
            "contextual_hints.word_select" => SettingValue::Bool(true),
            other => panic!("test must list expected default for `{other}`"),
        }
    };

    for meta in reg.all() {
        // Group rows carry no scalar value/default to round-trip.
        if matches!(meta.kind, SettingKind::Group { .. }) {
            continue;
        }
        let live_value = current_value_for(meta.key, &ui, &pager)
            .unwrap_or_else(|| panic!("current_value_for(`{}`) returned None", meta.key));
        let default_value = kigi_tui::settings::default_value_for(meta);
        let expected_value = expected(meta.key);

        assert_eq!(
            live_value, expected_value,
            "current_value_for(`{}`) drifted from expected",
            meta.key
        );
        assert_eq!(
            default_value, expected_value,
            "default_value_for(`{}`) drifted from expected",
            meta.key
        );
    }
}

/// Initial modal-open state selects the first setting row.
#[test]
fn initial_state_selects_first_setting_row() {
    let s = make_state();
    match &s.rows[s.selected] {
        RowEntry::Setting { key, .. } => assert_eq!(*key, "compact_mode"),
        _ => panic!("initial selection must land on a setting row, not a header"),
    }
}

#[test]
fn settings_value_payload_matches_kind() {
    // Every Bool setting's typed setter carries a Bool value.
    let reg = SettingsRegistry::defaults();
    for meta in reg.all() {
        if !matches!(meta.kind, SettingKind::Bool { .. }) {
            continue;
        }
        // Group children are toggled inside the sub-sheet, not as top-level
        // rows (covered by the group sub-sheet e2e test), so skip navigation.
        if is_group_child(&reg, meta.key) {
            continue;
        }
        let mut state = make_state();
        navigate_to(&mut state, meta.key);
        let outcome = handle_settings_key(&mut state, &press(KeyCode::Char(' ')));
        match outcome {
            SettingsKeyOutcome::Action(Action::SetCompactMode(_))
            | SettingsKeyOutcome::Action(Action::SetTimestamps(_))
            | SettingsKeyOutcome::Action(Action::SetTimeline(_))
            | SettingsKeyOutcome::Action(Action::SetSimpleMode(_))
            | SettingsKeyOutcome::Action(Action::SetMultilineMode(_))
            | SettingsKeyOutcome::Action(Action::SetVimMode(_))
            | SettingsKeyOutcome::Action(Action::SetRememberToolApprovals(_))
            | SettingsKeyOutcome::Action(Action::SetAskUserQuestionTimeoutEnabled(_))
            | SettingsKeyOutcome::Action(Action::SetShowTips(_))
            | SettingsKeyOutcome::Action(Action::SetAutoUpdate(_))
            | SettingsKeyOutcome::Action(Action::SetRespectManualFolds(_))
            | SettingsKeyOutcome::Action(Action::SetShowThinkingBlocks(_))
            | SettingsKeyOutcome::Action(Action::SetPromptSuggestions(_))
            | SettingsKeyOutcome::Action(Action::SetGroupToolVerbs(_))
            | SettingsKeyOutcome::Action(Action::SetCollapsedEditBlocks(_))
            | SettingsKeyOutcome::Action(Action::SetInvertScroll(_))
            | SettingsKeyOutcome::Action(Action::SetDisplayRefreshAutoCadence(_)) => {}
            other => panic!(
                "expected a typed bool setter for `{}`, got {:?}",
                meta.key, other
            ),
        }
    }
}

/// `SettingValue` variants with same payload must not compare equal.
#[test]
fn setting_value_variants_are_distinct() {
    let b = SettingValue::Bool(true);
    let e = SettingValue::Enum("true");
    let s = SettingValue::String("true".to_string());
    let i = SettingValue::Int(1);
    assert_ne!(b, e);
    assert_ne!(b, s);
    assert_ne!(b, i);
    assert_ne!(e, s);
    assert_ne!(e, i);
    assert_ne!(s, i);
}

// ---------------------------------------------------------------------------
// KeyEventKind filtering
// ---------------------------------------------------------------------------

/// Release events are dropped (kitty-keyboard protocol parity).
#[test]
fn release_event_kind_is_dropped() {
    let mut s = make_state();
    let key = KeyEvent {
        code: KeyCode::Char(' '),
        modifiers: KeyModifiers::NONE,
        kind: KeyEventKind::Release,
        state: crossterm::event::KeyEventState::NONE,
    };
    let outcome = handle_settings_key(&mut s, &key);
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "Release event must produce Unchanged, got: {outcome:?}"
    );
}

/// Repeat Space is dropped (prevents disk thrash from held key).
#[test]
fn repeat_space_is_dropped_no_disk_thrash() {
    let mut s = make_state();
    let key = KeyEvent {
        code: KeyCode::Char(' '),
        modifiers: KeyModifiers::NONE,
        kind: KeyEventKind::Repeat,
        state: crossterm::event::KeyEventState::NONE,
    };
    let outcome = handle_settings_key(&mut s, &key);
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "Repeat on Space must not dispatch an Action, got: {outcome:?}"
    );
}

#[test]
fn repeat_enter_is_dropped() {
    let mut s = make_state();
    let key = KeyEvent {
        code: KeyCode::Enter,
        modifiers: KeyModifiers::NONE,
        kind: KeyEventKind::Repeat,
        state: crossterm::event::KeyEventState::NONE,
    };
    let outcome = handle_settings_key(&mut s, &key);
    assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
}

/// Repeat on j (navigation) is allowed — held arrow keys feel snappy.
#[test]
fn repeat_j_navigation_is_processed() {
    let mut s = make_state();
    let key = KeyEvent {
        code: KeyCode::Char('j'),
        modifiers: KeyModifiers::NONE,
        kind: KeyEventKind::Repeat,
        state: crossterm::event::KeyEventState::NONE,
    };
    let outcome = handle_settings_key(&mut s, &key);
    // From the initial state (compact_mode), Repeat j advances to the next
    // Appearance row: screen_mode.
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    match &s.rows[s.selected] {
        RowEntry::Setting { key, .. } => assert_eq!(*key, "screen_mode"),
        _ => panic!("expected setting row after Repeat j"),
    }
}

// ---------------------------------------------------------------------------
// d-key reset-to-default
// ---------------------------------------------------------------------------

/// `d` dispatches `Action::OpenResetConfirm` on the focused row.
#[test]
fn d_key_emits_open_reset_confirm_action_for_compact_mode() {
    let mut s = make_state();
    // Default selection lands on `compact_mode`.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char('d')));
    match outcome {
        SettingsKeyOutcome::Action(Action::OpenResetConfirm { key }) => {
            assert_eq!(key, "compact_mode");
        }
        other => panic!("expected Action::OpenResetConfirm, got {other:?}"),
    }
}

/// `d` fires `OpenResetConfirm` with the right key for every setting.
#[test]
fn d_key_emits_open_reset_confirm_for_every_setting() {
    let reg = SettingsRegistry::defaults();
    for meta in reg.all() {
        // Group rows have no scalar value to reset (consistent with the registry
        // reset-arm coverage test), and their children are hidden from the
        // top-level list — neither is `d`-resettable directly.
        if matches!(meta.kind, SettingKind::Group { .. }) || is_group_child(&reg, meta.key) {
            continue;
        }
        let mut s = make_state();
        // A row can be conditionally hidden from the top-level list. Skip
        // settings with no visible row; their reset path is covered by the
        // dispatch round-trip tests.
        let has_row = s
            .rows
            .iter()
            .any(|r| matches!(r, RowEntry::Setting { key, .. } if *key == meta.key));
        if !has_row {
            continue;
        }
        navigate_to(&mut s, meta.key);
        let outcome = handle_settings_key(&mut s, &press(KeyCode::Char('d')));
        match outcome {
            SettingsKeyOutcome::Action(Action::OpenResetConfirm { key }) => {
                assert_eq!(
                    key, meta.key,
                    "d on `{}` must target `{}`",
                    meta.key, meta.key
                );
            }
            other => panic!(
                "expected Action::OpenResetConfirm for `{}`, got {other:?}",
                meta.key
            ),
        }
    }
}

/// `d` on a header row is a no-op.
#[test]
fn d_key_on_header_row_is_unchanged() {
    let mut s = make_state();
    let header_idx = s
        .rows
        .iter()
        .position(|r| {
            matches!(
                r,
                RowEntry::Header {
                    category: SettingCategory::Appearance
                }
            )
        })
        .expect("Appearance header must exist");
    s.selected = header_idx;
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char('d')));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "d on header row must be Unchanged, got: {outcome:?}",
    );
}

// ---------------------------------------------------------------------------
// Mouse hit-rect edge cases
// ---------------------------------------------------------------------------

/// Click with empty `row_rects` (partial render) is a no-op.
#[test]
fn mouse_click_inside_list_with_empty_row_rects_is_no_op() {
    let mut s = make_state();
    s.list_area = Rect {
        x: 0,
        y: 0,
        width: 80,
        height: 10,
    };
    // No row_rects populated — simulates a partial render where the
    // chrome drew but row layout was aborted.
    s.row_rects.clear();
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        5,
        5,
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Unchanged));
}

/// Scroll-down at last row returns `Unchanged`.
#[test]
fn scroll_down_at_last_row_is_unchanged() {
    let mut s = make_state();
    synth_rects(&mut s);
    // Move to the last selectable row first.
    let last_idx = s
        .rows
        .iter()
        .rposition(|r| matches!(r, RowEntry::Setting { .. }))
        .unwrap();
    s.selected = last_idx;
    let outcome = handle_settings_mouse(&mut s, MouseEventKind::ScrollDown, 5, 1);
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "scroll-down at last row should be Unchanged, got: {outcome:?}"
    );
}

// ---------------------------------------------------------------------------
// Stub tests — `#[ignore] + unimplemented!()` until wired up.
// ---------------------------------------------------------------------------

/// Multi-word AND filter narrows to matching settings + section headers.
#[test]
fn pr2_filter_matches_multi_word_and() {
    let mut s = make_state();
    let _ = handle_settings_key(&mut s, &press(KeyCode::Char('/')));
    for c in "compact density".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(c)));
    }
    let filtered = s.filtered_indices();
    assert_eq!(
        filtered.len(),
        2,
        "expected exactly 2 visible rows (Appearance header + compact_mode), \
         got {filtered:?}"
    );
    // First entry: Appearance section header.
    match &s.rows[filtered[0]] {
        RowEntry::Header { category } => assert_eq!(*category, SettingCategory::Appearance),
        other => panic!("expected Header at filtered[0], got {other:?}"),
    }
    // Second entry: compact_mode setting.
    match &s.rows[filtered[1]] {
        RowEntry::Setting { key, .. } => assert_eq!(*key, "compact_mode"),
        other => panic!("expected Setting at filtered[1], got {other:?}"),
    }
}

/// Enum chooser sub-mode: Esc inside `PickingEnum` reverts
/// to the original value AND transitions back to Browse. This e2e
/// exercises the FULL production path:
///
///   Browse (synthetic Enum row focused)
///     → Enter      → try_enter_picking_enum() → PickingEnum
///     → Down       → choices_idx 0 → 1 (preview dispatch)
///     → Esc        → action_for_enum(key, original) → Browse
///
/// Unlike a version that set `state.mode = PickingEnum {...}`
/// directly, this proves the *production* entry path
/// (`handle_browse::Enter` → `try_enter_picking_enum`) — the only
/// place `try_enter_picking_enum` is reachable in production code.
///
/// This verifies the *structural* outcome (mode + Changed); the
/// Action variant assertion lands once
/// `action_for_enum("theme", _)` ships.
#[test]
fn pr3_esc_in_picker_reverts_to_original() {
    // Synthetic Enum registry — `action_for_enum` returns None for
    // this key, so the outcome is structural (Changed + Browse mode)
    // rather than an Action variant assertion.
    let registry = SettingsRegistry::from_entries(vec![SettingMeta {
        key: "test_enum",
        category: SettingCategory::Appearance,
        owner: SettingOwner::Shared,
        label: "Test enum",
        description: "Synthetic Enum entry for PR 3 picker revert path.",
        keywords: &["test"],
        kind: SettingKind::Enum {
            default: "first",
            choices: &[
                EnumChoice {
                    canonical: "first",
                    display: "First",
                    description: "First option.",
                },
                EnumChoice {
                    canonical: "second",
                    display: "Second",
                    description: "Second option.",
                },
            ],
            supports_preview: true,
        },
        restart_required: false,
        hidden_in_minimal: false,
    }]);
    let mut s = SettingsModalState::new(
        Arc::new(registry),
        UiConfig::default(),
        PagerLocalSnapshot::default(),
    );

    // Sanity: modal starts in Browse on the synthetic Enum row.
    assert!(matches!(s.mode, SettingsModalMode::Browse));
    match &s.rows[s.selected] {
        RowEntry::Setting { key, .. } => assert_eq!(*key, "test_enum"),
        _ => panic!("initial selection must be the synthetic Enum row"),
    }

    // Step 1: Enter on the Enum row → try_enter_picking_enum() fires
    // and seeds choices_idx + original_value from the row's current
    // value (None → fallback to first canonical).
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on Enum row should produce Changed, got {outcome:?}"
    );
    match s.mode {
        SettingsModalMode::PickingEnum {
            key,
            choices_idx,
            ref original_value,
            ..
        } => {
            assert_eq!(key, "test_enum");
            assert_eq!(choices_idx, 0);
            assert_eq!(original_value, &SettingValue::Enum("first"));
        }
        ref other => panic!("expected PickingEnum mode after Enter, got {other:?}"),
    }

    // Step 2: Down → preview-navigate to choice 1 (live preview
    // dispatch via action_for_enum, returns None here → Changed).
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Down));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    match s.mode {
        SettingsModalMode::PickingEnum { choices_idx, .. } => assert_eq!(choices_idx, 1),
        ref other => panic!("expected PickingEnum mode after Down, got {other:?}"),
    }

    // Step 3: Esc → revert. action_for_enum returns None (no Enum
    // arms yet), so the outcome is Changed. A later change will tighten
    // the assertion to `SettingsKeyOutcome::Action(Action::SetTheme("first"))`
    // once the theme arm ships.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Esc revert outcome should be Changed (or Action when arms exist), got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Esc must transition the modal back to Browse"
    );
}

/// Full theme picker e2e via the production entry path.
///
///   Browse (theme row focused)
///     → Enter      → try_enter_picking_enum() → PickingEnum
///     → Down       → choices_idx default → next, dispatches
///                    `Action::PreviewTheme(...)` (preview-only,
///                    no persist Effect, no toast)
///     → Up         → preview-revert
///     → Down       → preview to next
///     → Enter      → dispatches `Action::SetTheme(current)` COMMIT
///                    (single persist + toast per picker cycle)
///
/// The test (a) verifies the
/// PREVIEW vs COMMIT split (Up/Down emit Preview Actions, Enter
/// emits a Set/commit Action), and (b) derives expected canonicals
/// from the registry — a future catalog reorder doesn't break the
/// test for a non-bug reason.
#[test]
fn pr4_theme_preview_and_commit_e2e() {
    let reg = SettingsRegistry::defaults();
    let theme_meta = reg
        .find("theme")
        .expect("registry must contain `theme` for PR 4");
    let (default_canonical, default_idx, choices_count, next_canonical, next_idx) =
        match &theme_meta.kind {
            SettingKind::Enum {
                default, choices, ..
            } => {
                let default_idx = choices
                    .iter()
                    .position(|c| c.canonical == *default)
                    .expect("theme default must exist in choices");
                assert!(
                    default_idx + 1 < choices.len(),
                    "test requires at least one choice AFTER the default; reorder?"
                );
                let next = choices[default_idx + 1].canonical;
                (*default, default_idx, choices.len(), next, default_idx + 1)
            }
            other => panic!("expected Enum kind for `theme`, got {other:?}"),
        };
    assert!(
        choices_count >= 3,
        "PR 4 test requires ≥3 theme choices, got {choices_count}",
    );

    let mut s = make_state();
    navigate_to(&mut s, "theme");
    match &s.rows[s.selected] {
        RowEntry::Setting { key, .. } => assert_eq!(*key, "theme"),
        _ => panic!("expected to land on theme row"),
    }

    // Enter on Enum row → PickingEnum, seeded to the default.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on theme row should transition to PickingEnum, got {outcome:?}"
    );
    let original_canonical = match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            choices_idx,
            original_value,
            ..
        } => {
            assert_eq!(*key, "theme");
            // choices_idx points at the registry's default (derived
            // dynamically — no hardcoded "1").
            assert_eq!(*choices_idx, default_idx);
            match original_value {
                SettingValue::Enum(s) => *s,
                other => panic!("expected Enum original_value, got {other:?}"),
            }
        }
        ref other => panic!("expected PickingEnum mode, got {other:?}"),
    };
    assert_eq!(original_canonical, default_canonical);

    // Down → preview-navigate to next choice. The dispatched Action
    // is now a PREVIEW (no persist).
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Down));
    match outcome {
        SettingsKeyOutcome::Action(Action::PreviewTheme(name)) => {
            assert_eq!(
                name, next_canonical,
                "preview dispatch must carry the canonical of the new focused choice",
            );
        }
        other => panic!("expected Action::PreviewTheme(\"{next_canonical}\"), got {other:?}"),
    }
    match s.mode {
        SettingsModalMode::PickingEnum { choices_idx, .. } => assert_eq!(choices_idx, next_idx),
        ref other => panic!("expected PickingEnum after Down, got {other:?}"),
    }

    // Up → preview-revert to default.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Up));
    match outcome {
        SettingsKeyOutcome::Action(Action::PreviewTheme(name)) => {
            assert_eq!(name, default_canonical);
        }
        other => panic!("expected Action::PreviewTheme(\"{default_canonical}\"), got {other:?}"),
    }

    // Down again so commit lands on a non-default canonical.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));

    // Enter → COMMIT. Dispatches `Action::SetTheme(current_canonical)`
    // — a typed Action variant carrying the current preview value.
    // The dispatcher's `set_theme` emits Effect::PersistSetting +
    // toast (exercised by the strangler-fig e2e test below).
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetTheme(name)) => {
            assert_eq!(
                name, next_canonical,
                "Enter must commit the current preview, not the original"
            );
        }
        other => panic!("expected Action::SetTheme(\"{next_canonical}\") commit, got {other:?}"),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter commit must return to Browse"
    );
}

// Strangler-fig dispatch-layer tests for the typed Actions are in
// `crates/codegen/kigi-tui/src/app/dispatch.rs::tests` (next to
// the `set_compact_mode_emits_persist_setting_with_correct_payload`
// family) — see `set_theme_emits_persist_setting_with_correct_payload`
// and friends. The dispatch tests live there because the `AppView`
// test fixture (`test_app_with_agent`) isn't exported across the
// crate boundary.

/// Esc inside the theme picker dispatches a PREVIEW
/// Action (not a commit) — Esc revert
/// is a preview-style restore, not a re-persist.
#[test]
fn pr4_theme_picker_esc_dispatches_revert_action() {
    let reg = SettingsRegistry::defaults();
    let default_canonical = match &reg.find("theme").unwrap().kind {
        SettingKind::Enum { default, .. } => *default,
        _ => panic!("theme must be Enum"),
    };

    let mut s = make_state();
    navigate_to(&mut s, "theme");

    // Enter PickingEnum.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));

    // Preview-navigate so the original/current distinction is visible.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));

    // Esc → must dispatch PreviewTheme(original) AND return to Browse.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    match outcome {
        SettingsKeyOutcome::Action(Action::PreviewTheme(name)) => {
            assert_eq!(
                name, default_canonical,
                "Esc must carry the original canonical as a PREVIEW (not commit)",
            );
        }
        other => {
            panic!("expected Action::PreviewTheme(\"{default_canonical}\") on Esc, got {other:?}")
        }
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Esc must return to Browse"
    );
}

/// `action_for_enum` (preview) and
/// `action_for_enum_commit` map every theme-family key to the
/// matching typed Action variant:
/// parameterised across keys AND derives expected next-canonical
/// from the registry (catalog-reorder-resilient).
///
/// Also exercises EVERY choice (not just first Down), so
/// a refactor that routes correctly for choice 0 but breaks for
/// choice N>0 gets caught.
#[test]
fn pr4_picker_dispatches_each_theme_settings_action_variant() {
    let reg = SettingsRegistry::defaults();

    for key in &["theme", "auto_dark_theme", "auto_light_theme"] {
        let meta = reg
            .find(key)
            .unwrap_or_else(|| panic!("registry missing `{key}`"));
        let (default_idx, choices) = match &meta.kind {
            SettingKind::Enum {
                default, choices, ..
            } => {
                let i = choices
                    .iter()
                    .position(|c| c.canonical == *default)
                    .unwrap();
                (i, *choices)
            }
            _ => panic!("`{key}` must be Enum"),
        };

        let mut s = make_state();
        navigate_to(&mut s, key);
        let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));

        // Navigate forward through every remaining choice, asserting
        // the variant on each Preview dispatch.
        for (next_idx, choice) in choices.iter().enumerate().skip(default_idx + 1) {
            let expected = choice.canonical;
            let outcome = handle_settings_key(&mut s, &press(KeyCode::Down));
            match (*key, outcome) {
                ("theme", SettingsKeyOutcome::Action(Action::PreviewTheme(name))) => {
                    assert_eq!(name, expected, "theme preview at idx {next_idx}");
                }
                (
                    "auto_dark_theme",
                    SettingsKeyOutcome::Action(Action::PreviewAutoDarkTheme(name)),
                ) => {
                    assert_eq!(name, expected, "auto_dark preview at idx {next_idx}");
                }
                (
                    "auto_light_theme",
                    SettingsKeyOutcome::Action(Action::PreviewAutoLightTheme(name)),
                ) => {
                    assert_eq!(name, expected, "auto_light preview at idx {next_idx}");
                }
                (k, other) => panic!(
                    "Down on `{k}` at idx {next_idx} should dispatch matching Preview Action, got {other:?}"
                ),
            }
        }

        // Enter at the LAST choice → COMMIT Action variant for that
        // canonical.
        let last_canonical = choices.last().unwrap().canonical;
        let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
        match (*key, outcome) {
            ("theme", SettingsKeyOutcome::Action(Action::SetTheme(name))) => {
                assert_eq!(name, last_canonical);
            }
            ("auto_dark_theme", SettingsKeyOutcome::Action(Action::SetAutoDarkTheme(name))) => {
                assert_eq!(name, last_canonical);
            }
            ("auto_light_theme", SettingsKeyOutcome::Action(Action::SetAutoLightTheme(name))) => {
                assert_eq!(name, last_canonical);
            }
            (k, other) => panic!(
                "Enter on `{k}` last choice should dispatch matching commit Action, got {other:?}"
            ),
        }
    }
}

// ---------------------------------------------------------------------------
// Mouse-path coverage for the new Enum settings.
// The `every_registered_setting_is_exercised`
// test's docstring promises "keyboard test + mouse test" per
// registered key. Earlier only keyboard tests shipped for the 3 new
// enums; these tests close that gap.
// ---------------------------------------------------------------------------

/// Clicking on an Enum row in Browse mode selects it without firing
/// any Action — Enum rows require an explicit Enter to open the
/// picker (mouse picker-entry is deferred to a future change). The body
/// click outside the indicator hit-rect (cols 0-4) is a
/// select-only event.
#[test]
fn pr4_mouse_click_on_theme_row_selects_without_emitting_action() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "theme") as u16;
    // Body click (col 20 is well outside the 0-4 indicator hit-rect).
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        20,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on theme row should only select (no Action), got {outcome:?}",
    );
    assert_eq!(
        s.selected, row_y as usize,
        "selection must move to theme row"
    );
}

#[test]
fn pr4_mouse_click_on_auto_dark_theme_row_selects_without_emitting_action() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "auto_dark_theme") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        20,
        row_y,
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert_eq!(s.selected, row_y as usize);
}

#[test]
fn pr4_mouse_click_on_auto_light_theme_row_selects_without_emitting_action() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "auto_light_theme") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        20,
        row_y,
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert_eq!(s.selected, row_y as usize);
}

/// In picker mode (PickingEnum), mouse events are no-ops — clicks
/// don't select picker choices yet. Pins the contract so a
/// future change that adds click-to-pick must update this test
/// explicitly.
#[test]
fn pr4_mouse_click_in_theme_picker_is_no_op() {
    let mut s = make_state();
    s.mode = SettingsModalMode::PickingEnum {
        key: "theme",
        choices_idx: 0,
        original_value: SettingValue::Enum("kiginight"),
        supports_preview: true,
    };
    synth_rects(&mut s);
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        20,
        5,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "picker mode must ignore mouse clicks in PR 4, got {outcome:?}",
    );
    assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
}

// ---------------------------------------------------------------------------
// `multiline_mode` (first PAGER-owned setting)
//
// Unlike the SHARED bools (which round-trip through
// `Effect::PersistSetting` and the shell), `multiline_mode` is
// PAGER-owned: state lives on `AgentView.multiline_mode`, the modal
// reads from `PagerLocalSnapshot`, and the dispatcher's
// `set_multiline_mode` is the single mutation owner. No disk persist,
// no `Effect`, no toast on the no-op fast path.
//
// These tests mirror the keyboard + mouse coverage promised by
// `ALL_SETTINGS_EXERCISED` — same rigor as `compact_mode` et al.
// ---------------------------------------------------------------------------

/// Keyboard Space on the multiline row dispatches the typed setter
/// with the inverted snapshot value (default false → true). The modal
/// builds the bool from `PagerLocalSnapshot.multiline_mode` via the
/// `current_value_for` arm.
#[test]
fn pr5_space_on_multiline_mode_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "multiline_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "multiline_mode", true);
}

/// Enter on the multiline row also toggles (same Bool semantics as
/// compact_mode / show_timestamps / simple_mode). Pins the contract
/// that Bool row Enter and Space behave identically across both
/// SHELL/SHARED and PAGER-owned settings.
#[test]
fn pr5_enter_on_multiline_mode_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "multiline_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "multiline_mode", true);
}

/// Two-stage select-then-toggle for `multiline_mode` mouse path.
#[test]
fn pr5_mouse_click_on_multiline_mode_two_stage_toggles() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "multiline_mode") as u16;

    // First click: select-only (initial focus is on compact_mode).
    // Click at column=10 (outside the indicator hit-rect 0..5).
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );
    assert_eq!(
        s.selected, row_y as usize,
        "first click must move selection to multiline_mode row",
    );

    // Second click on the now-focused row: toggle.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_set_bool_action(outcome, "multiline_mode", true);
}

/// Value-column click on `multiline_mode` toggles in one click.
#[test]
fn pr5_mouse_click_on_multiline_indicator_toggles_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "multiline_mode") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert_set_bool_action(outcome, "multiline_mode", true);
}

/// Snapshot `multiline_mode: true` → Space dispatches `SetMultilineMode(false)`.
#[test]
fn pr5_snapshot_when_on_dispatches_off() {
    let snapshot = PagerLocalSnapshot {
        multiline_mode: true,
        yolo_mode: false,
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    navigate_to(&mut s, "multiline_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "multiline_mode", false);
}

/// `multiline_mode` lives under Editor, not Appearance.
#[test]
fn pr5_multiline_mode_renders_under_editor_category() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("multiline_mode")
        .expect("multiline_mode must be registered");
    assert_eq!(
        meta.category,
        SettingCategory::Editor,
        "multiline_mode must live under Editor"
    );
    assert_eq!(
        meta.owner,
        SettingOwner::Pager,
        "multiline_mode must be PAGER-owned"
    );
}

// ---------------------------------------------------------------------------
// permission_mode (security-relevant Enum, no preview)
// ---------------------------------------------------------------------------

/// `permission_mode` lives under the `Agent` section.
#[test]
fn pr6_permission_mode_renders_under_agent_category() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("permission_mode")
        .expect("permission_mode must be registered");
    assert_eq!(
        meta.category,
        SettingCategory::Agent,
        "permission_mode must live under Agent"
    );
    assert_eq!(
        meta.owner,
        SettingOwner::Shell,
        "permission_mode is SHELL-owned (persists via shell + ACP)"
    );
}

/// `permission_mode` must be `supports_preview: false` — toggling
/// drains the permission queue (irreversible side effect).
#[test]
fn pr6_permission_mode_does_not_support_preview() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("permission_mode")
        .expect("permission_mode must be registered");
    match &meta.kind {
        SettingKind::Enum {
            supports_preview, ..
        } => {
            assert!(
                !supports_preview,
                "permission_mode MUST be supports_preview: false — toggling YOLO has \
                 irreversible side effects (drains permission_queue) so per-keystroke \
                 preview is unsafe",
            );
        }
        other => panic!("expected Enum kind for permission_mode, got {other:?}"),
    }
}

/// `permission_mode` reads from pager snapshot, not `ui` (live state).
#[test]
fn pr6_current_value_for_reads_pager_snapshot() {
    use kigi_tui::settings::current_value_for;

    let ui = UiConfig::default();

    let off_snap = PagerLocalSnapshot {
        multiline_mode: false,
        yolo_mode: false,
        ..PagerLocalSnapshot::default()
    };
    let on_snap = PagerLocalSnapshot {
        multiline_mode: false,
        yolo_mode: true,
        ..PagerLocalSnapshot::default()
    };

    assert_eq!(
        current_value_for("permission_mode", &ui, &off_snap),
        Some(SettingValue::Enum("ask")),
        "yolo=false → 'ask'",
    );
    assert_eq!(
        current_value_for("permission_mode", &ui, &on_snap),
        Some(SettingValue::Enum("always-approve")),
        "yolo=true → 'always-approve'",
    );

    // Defensive: even when `ui.permission_mode` says one thing,
    // the snapshot wins. Pins the LIVE-state-over-disk contract.
    let conflicting_ui = UiConfig {
        permission_mode: Some("ask".into()),
        ..UiConfig::default()
    };
    assert_eq!(
        current_value_for("permission_mode", &conflicting_ui, &on_snap),
        Some(SettingValue::Enum("always-approve")),
        "snapshot must win over `ui.permission_mode` when they disagree — \
         the snapshot is the LIVE state, `ui` is the at-startup persisted state",
    );
}

/// Enter on `permission_mode` opens the picker seeded to current state.
#[test]
fn pr6_enter_on_permission_mode_row_enters_picking_enum() {
    let mut s = make_state();
    navigate_to(&mut s, "permission_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on permission_mode row must transition to PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            original_value,
            ..
        } => {
            assert_eq!(*key, "permission_mode");
            assert_eq!(
                original_value,
                &SettingValue::Enum("ask"),
                "default snapshot yolo_mode=false → original 'ask'"
            );
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

/// Nav in `permission_mode` picker must NOT dispatch preview Actions
/// (would drain permission queue on every keystroke).
#[test]
fn pr6_permission_mode_picker_nav_does_not_dispatch_preview() {
    // Two-key navigation: open the picker, then exercise both
    // "advance" keys (Down, j) and both "retreat" keys (Up, k).
    // We re-open the picker between key probes so each key starts
    // at a known-position. The test must
    // catch a hypothetical j/k path that bypasses set_picker_idx.
    for nav_key in &[
        KeyCode::Down,
        KeyCode::Char('j'),
        KeyCode::Up,
        KeyCode::Char('k'),
    ] {
        let mut s = make_state();
        navigate_to(&mut s, "permission_mode");
        let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));

        // For "retreat" keys (Up/k) at choices_idx=0, the outcome is
        // Unchanged (clamp at first). We pre-navigate down so retreat
        // keys have something to retreat from.
        if matches!(nav_key, KeyCode::Up | KeyCode::Char('k')) {
            let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
        }

        let outcome = handle_settings_key(&mut s, &press(*nav_key));
        assert!(
            matches!(outcome, SettingsKeyOutcome::Changed),
            "Nav key {nav_key:?} in permission_mode picker MUST NOT dispatch a preview \
             Action — that would persist on every keystroke and drain the \
             permission_queue. Got {outcome:?}",
        );
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
    }
}

/// Enter on "always-approve" commits `SetPermissionMode(AlwaysApprove)`.
#[test]
fn pr6_permission_mode_picker_enter_dispatches_set_permission_mode_commit() {
    use kigi_tui::app::actions::PermissionModeKind;
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("permission_mode").unwrap();
    let choices = match &meta.kind {
        SettingKind::Enum { choices, .. } => *choices,
        _ => panic!("permission_mode must be Enum"),
    };
    let always_idx = choices
        .iter()
        .position(|c| c.canonical == "always-approve")
        .expect("permission_mode must include the 'always-approve' choice");
    let default_canonical = match &meta.kind {
        SettingKind::Enum { default, .. } => *default,
        _ => unreachable!(),
    };
    let initial_idx = choices
        .iter()
        .position(|c| c.canonical == default_canonical)
        .expect("registered default must be present in catalog");

    let mut s = make_state();
    navigate_to(&mut s, "permission_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));

    assert!(
        matches!(s.mode, SettingsModalMode::PickingEnum { key, .. } if key == "permission_mode"),
        "Enter on permission_mode row must open the picker, got {:?}",
        s.mode,
    );
    // Navigate from the seeded default canonical to "always-approve".
    let steps = always_idx as isize - initial_idx as isize;
    let nav_key = if steps > 0 {
        KeyCode::Down
    } else {
        KeyCode::Up
    };
    for _ in 0..steps.unsigned_abs() {
        let _ = handle_settings_key(&mut s, &press(nav_key));
    }
    // Enter → commit.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetPermissionMode(
            PermissionModeKind::AlwaysApprove,
        )) => {}
        other => panic!(
            "Enter on 'always-approve' must commit Action::SetPermissionMode(AlwaysApprove), \
             got {other:?}"
        ),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter commit must return to Browse"
    );
}

/// Esc in non-preview picker returns to Browse without Action.
#[test]
fn pr6_permission_mode_picker_esc_does_not_dispatch_action() {
    let mut s = make_state();
    navigate_to(&mut s, "permission_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // Navigate so the original/current distinction would matter for
    // a preview-supporting Enum.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Esc on non-preview Enum picker must NOT emit an Action — \
         doing so would re-persist on every Esc. Got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Esc must return to Browse"
    );
}

/// Picker seeds at "always-approve" when `yolo_mode: true`.
#[test]
fn pr6_picker_seeds_choices_idx_from_pager_snapshot_yolo_true() {
    let snapshot = PagerLocalSnapshot {
        multiline_mode: false,
        yolo_mode: true,
        auto_mode_gate: true,
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    navigate_to(&mut s, "permission_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    let reg = SettingsRegistry::defaults();
    let always_idx = match &reg.find("permission_mode").unwrap().kind {
        SettingKind::Enum { choices, .. } => choices
            .iter()
            .position(|c| c.canonical == "always-approve")
            .expect("permission_mode must have 'always-approve' choice"),
        _ => panic!("permission_mode must be Enum"),
    };
    match s.mode {
        SettingsModalMode::PickingEnum {
            choices_idx,
            ref original_value,
            ..
        } => {
            assert_eq!(
                choices_idx, always_idx,
                "picker must seed at the 'always-approve' index when snapshot says yolo=true"
            );
            assert_eq!(
                original_value,
                &SettingValue::Enum("always-approve"),
                "original_value must match the live snapshot"
            );
        }
        ref other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

/// Exactly 4 canonical choices: {ask, auto, always-approve, default}.
#[test]
fn pr6_permission_mode_choices_use_canonical_strings() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("permission_mode").unwrap();
    let canonicals: Vec<&str> = match &meta.kind {
        SettingKind::Enum { choices, .. } => choices.iter().map(|c| c.canonical).collect(),
        _ => panic!("permission_mode must be Enum"),
    };
    assert_eq!(
        canonicals.len(),
        4,
        "permission_mode catalog must be exactly {{ask, auto, always-approve, default}} — adding a \
         choice requires updating action_for_enum_commit, apply_setting_rollback, \
         PermissionModeKind, AND load_permission_mode (PR 11 contract)",
    );
    assert!(
        canonicals.contains(&"auto"),
        "permission_mode must include 'auto' canonical (auto permission mode feature)"
    );
    assert!(
        canonicals.contains(&"ask"),
        "permission_mode must include 'ask' canonical (shell schema)"
    );
    assert!(
        canonicals.contains(&"always-approve"),
        "permission_mode must include 'always-approve' canonical (shell schema)"
    );
    assert!(
        canonicals.contains(&"default"),
        "permission_mode must include 'default' canonical (PR 11 — agent's \
         default permission behavior)"
    );
}

/// Search "yolo" finds exactly `permission_mode`.
#[test]
fn pr6_search_yolo_matches_permission_mode() {
    let reg = SettingsRegistry::defaults();
    let hits = reg.search("yolo");
    assert_eq!(
        hits.len(),
        1,
        "search('yolo') must return exactly one result (permission_mode) — \
         found {} results: {:?}",
        hits.len(),
        hits.iter().map(|m| m.key).collect::<Vec<_>>(),
    );
    assert_eq!(
        hits[0].key, "permission_mode",
        "search('yolo') unique result must be permission_mode"
    );
}

// ---------------------------------------------------------------------------
// Mouse path tests for permission_mode (keyboard ↔ mouse parity)
// ---------------------------------------------------------------------------

/// First click on unselected `permission_mode` row only selects.
#[test]
fn pr6_mouse_click_on_unselected_permission_mode_row_only_selects() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "permission_mode") as u16;

    // First click: selects only.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first body-click on unselected permission_mode row should only select, got: {outcome:?}",
    );
    assert_eq!(
        s.selected, row_y as usize,
        "selection must move to permission_mode row",
    );

    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "first body-click must NOT enter the picker (two-stage UX), mode is {:?}",
        s.mode,
    );
}

/// Second click on selected `permission_mode` opens the picker.
#[test]
fn pr6_mouse_click_on_selected_permission_mode_row_opens_picker() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "permission_mode") as u16;

    // First click: select.
    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_eq!(s.selected, row_y as usize);
    assert!(matches!(s.mode, SettingsModalMode::Browse));

    // Second click on the now-focused row: open the picker.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on focused Enum row must open picker (Changed), got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => {
            assert_eq!(*key, "permission_mode");
        }
        other => panic!(
            "second click on focused permission_mode row must enter PickingEnum, got {other:?}",
        ),
    }
}

/// Value-column click opens picker in one click.
#[test]
fn pr6_mouse_click_on_permission_mode_indicator_opens_picker_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "permission_mode") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "value click must open picker in one click, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => {
            assert_eq!(*key, "permission_mode");
        }
        other => {
            panic!("value click on permission_mode must enter PickingEnum, got {other:?}",)
        }
    }
}

// ---------------------------------------------------------------------------
// permission_mode 3-state tests (default/ask/always-approve)
// ---------------------------------------------------------------------------

/// Picking "Default" dispatches `SetPermissionMode(Default)`.
#[test]
fn pr11_picker_commit_for_default_dispatches_set_permission_mode_default() {
    use kigi_tui::app::actions::PermissionModeKind;
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("permission_mode").unwrap();
    let choices = match &meta.kind {
        SettingKind::Enum { choices, .. } => *choices,
        _ => panic!("permission_mode must be Enum"),
    };
    let default_idx = choices
        .iter()
        .position(|c| c.canonical == "default")
        .expect("permission_mode must include the 'default' choice (PR 11)");
    let initial_idx = choices
        .iter()
        .position(|c| c.canonical == "ask")
        .expect("'ask' canonical must be present");
    assert_ne!(
        default_idx, initial_idx,
        "test invariant: 'default' must be a distinct choice from 'ask'"
    );

    let mut s = make_state();
    navigate_to(&mut s, "permission_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(s.mode, SettingsModalMode::PickingEnum { key, .. } if key == "permission_mode"),
        "Enter on permission_mode row must open the picker, got {:?}",
        s.mode,
    );
    let steps = default_idx as isize - initial_idx as isize;
    let nav_key = if steps > 0 {
        KeyCode::Down
    } else {
        KeyCode::Up
    };
    for _ in 0..steps.unsigned_abs() {
        let _ = handle_settings_key(&mut s, &press(nav_key));
    }
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetPermissionMode(PermissionModeKind::Default)) => {}
        other => panic!(
            "Enter on 'default' must commit Action::SetPermissionMode(Default), got {other:?}"
        ),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter commit must return to Browse"
    );
}

/// Picking "Ask" dispatches `SetPermissionMode(Ask)`.
#[test]
fn pr11_picker_commit_for_ask_dispatches_set_permission_mode_ask() {
    use kigi_tui::app::actions::PermissionModeKind;
    // Set snapshot so the picker opens seeded at "always-approve",
    // then navigate to "ask" to commit a non-default selection.
    let snapshot = PagerLocalSnapshot {
        yolo_mode: true,
        auto_mode_gate: true,
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    navigate_to(&mut s, "permission_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(s.mode, SettingsModalMode::PickingEnum { key, .. } if key == "permission_mode"),
        "Enter on permission_mode row must open the picker, got {:?}",
        s.mode,
    );

    let reg = SettingsRegistry::defaults();
    let meta = reg.find("permission_mode").unwrap();
    let choices = match &meta.kind {
        SettingKind::Enum { choices, .. } => *choices,
        _ => panic!("permission_mode must be Enum"),
    };
    let always_idx = choices
        .iter()
        .position(|c| c.canonical == "always-approve")
        .unwrap();
    let ask_idx = choices.iter().position(|c| c.canonical == "ask").unwrap();
    let steps = ask_idx as isize - always_idx as isize;
    let nav_key = if steps > 0 {
        KeyCode::Down
    } else {
        KeyCode::Up
    };
    for _ in 0..steps.unsigned_abs() {
        let _ = handle_settings_key(&mut s, &press(nav_key));
    }
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetPermissionMode(PermissionModeKind::Ask)) => {}
        other => {
            panic!("Enter on 'ask' must commit Action::SetPermissionMode(Ask), got {other:?}")
        }
    }
}

/// Returns "default" when `ui.permission_mode == "default"` and yolo=false.
#[test]
fn pr11_current_value_for_returns_default_when_ui_says_default() {
    use kigi_tui::settings::current_value_for;
    let ui = UiConfig {
        permission_mode: Some("default".into()),
        ..UiConfig::default()
    };
    let pager = PagerLocalSnapshot {
        yolo_mode: false,
        ..PagerLocalSnapshot::default()
    };
    assert_eq!(
        current_value_for("permission_mode", &ui, &pager),
        Some(SettingValue::Enum("default")),
        "ui.permission_mode = 'default' + yolo=false → 'default'"
    );
}

/// Live yolo_mode=true overrides ui.permission_mode → "always-approve".
#[test]
fn pr11_current_value_for_pager_yolo_overrides_default_canonical() {
    use kigi_tui::settings::current_value_for;
    let ui = UiConfig {
        permission_mode: Some("default".into()),
        ..UiConfig::default()
    };
    let pager = PagerLocalSnapshot {
        yolo_mode: true,
        ..PagerLocalSnapshot::default()
    };
    assert_eq!(
        current_value_for("permission_mode", &ui, &pager),
        Some(SettingValue::Enum("always-approve")),
        "pager.yolo_mode=true wins over ui.permission_mode='default' — LIVE state precedence",
    );
}

/// yolo=true + ui=None → "always-approve" (--yolo startup baseline).
#[test]
fn pr11_current_value_for_yolo_true_with_ui_none_returns_always_approve() {
    use kigi_tui::settings::current_value_for;
    let ui = UiConfig {
        permission_mode: None,
        ..UiConfig::default()
    };
    let pager = PagerLocalSnapshot {
        yolo_mode: true,
        ..PagerLocalSnapshot::default()
    };
    assert_eq!(
        current_value_for("permission_mode", &ui, &pager),
        Some(SettingValue::Enum("always-approve")),
        "pager.yolo_mode=true + ui.permission_mode=None → 'always-approve' \
         (LIVE-state baseline for `--yolo` startup with no config setting)"
    );
}

/// Non-"default" values with yolo=false fall through to "ask".
#[test]
fn pr11_current_value_for_falls_through_to_ask() {
    use kigi_tui::settings::current_value_for;
    let pager = PagerLocalSnapshot {
        yolo_mode: false,
        ..PagerLocalSnapshot::default()
    };
    // Explicit "ask" → "ask"
    let ui_ask = UiConfig {
        permission_mode: Some("ask".into()),
        ..UiConfig::default()
    };
    assert_eq!(
        current_value_for("permission_mode", &ui_ask, &pager),
        Some(SettingValue::Enum("ask")),
        "explicit 'ask' → 'ask'"
    );
    // None → "ask" (the registry default)
    let ui_none = UiConfig {
        permission_mode: None,
        ..UiConfig::default()
    };
    assert_eq!(
        current_value_for("permission_mode", &ui_none, &pager),
        Some(SettingValue::Enum("ask")),
        "None → 'ask' (registry default)"
    );
    // Garbage → "ask" (defensive fallthrough)
    let ui_garbage = UiConfig {
        permission_mode: Some("nonexistent".into()),
        ..UiConfig::default()
    };
    assert_eq!(
        current_value_for("permission_mode", &ui_garbage, &pager),
        Some(SettingValue::Enum("ask")),
        "garbage canonical → 'ask' (defensive fallthrough)"
    );
}

/// `PermissionModeKind` canonical strings round-trip.
#[test]
fn pr11_permission_mode_kind_canonical_round_trip() {
    use kigi_tui::app::actions::PermissionModeKind;
    for kind in [
        PermissionModeKind::Default,
        PermissionModeKind::Ask,
        PermissionModeKind::AlwaysApprove,
    ] {
        let canonical = kind.as_canonical();
        let recovered = PermissionModeKind::from_canonical(canonical)
            .unwrap_or_else(|| panic!("from_canonical('{canonical}') must round-trip"));
        assert_eq!(
            recovered, kind,
            "PermissionModeKind::from_canonical(as_canonical({kind:?})) must round-trip"
        );
    }
    // Garbage input → None
    assert!(PermissionModeKind::from_canonical("nonexistent").is_none());
    assert!(PermissionModeKind::from_canonical("").is_none());
}

/// Catalog canonicals match `PermissionModeKind::as_canonical`.
#[test]
fn pr11_permission_mode_kind_canonical_strings_match_choices_catalog() {
    use kigi_tui::app::actions::PermissionModeKind;
    let catalog_canonicals: std::collections::HashSet<&str> = SettingsRegistry::defaults()
        .find("permission_mode")
        .and_then(|m| match &m.kind {
            SettingKind::Enum { choices, .. } => Some(*choices),
            _ => None,
        })
        .map(|c| c.iter().map(|c| c.canonical).collect())
        .expect("permission_mode must be registered");

    for kind in [
        PermissionModeKind::Default,
        PermissionModeKind::Ask,
        PermissionModeKind::Auto,
        PermissionModeKind::AlwaysApprove,
    ] {
        assert!(
            catalog_canonicals.contains(kind.as_canonical()),
            "catalog must contain `{}` (from PermissionModeKind::{kind:?})",
            kind.as_canonical(),
        );
    }
    assert_eq!(
        catalog_canonicals.len(),
        4,
        "catalog must be exactly {{ask, auto, always-approve, default}} — adding a fifth \
         choice requires adding a PermissionModeKind variant AND updating action_for_enum_commit \
         + apply_setting_rollback + load_permission_mode + this test (PR 11 contract)",
    );
}

/// Only `AlwaysApprove` projects to `true`.
#[test]
fn pr11_permission_mode_kind_is_always_approve_projection() {
    use kigi_tui::app::actions::PermissionModeKind;
    assert!(PermissionModeKind::AlwaysApprove.is_always_approve());
    assert!(!PermissionModeKind::Ask.is_always_approve());
    assert!(
        !PermissionModeKind::Default.is_always_approve(),
        "PR 11: Default must project onto yolo=false — it's an alias for Ask at runtime, \
         NOT an alias for AlwaysApprove"
    );
}

// cycle_mode delegation tests live in `dispatch.rs::tests`.

// The previous
// `pr7_d_key_opens_reset_confirmation_modal` was a duplicate of
// `d_key_emits_open_reset_confirm_action_for_compact_mode` at L1617.
// Removed to eliminate redundancy — the canonical d→OpenResetConfirm
// contract is asserted there + by the parameterised
// `d_key_emits_open_reset_confirm_for_every_setting` test.
//
// The full y/n-via-handle_modal_key dispatch path is exercised by
// the dispatch.rs::tests family
// (dispatch_confirm_reset_setting_reset_dispatches_typed_setter_for_*
// + dispatch_confirm_reset_setting_cancel_preserves_modal_state).

// ---------------------------------------------------------------------------
// Render-side tests for the reset-confirm overlay.
//
// These tests assert that the rendered buffer contains the
// confirmation prompt text + breadcrumb + y/n shortcuts. Without
// them, a future change that breaks the overlay's rendering layer
// would silently regress to "user can't see the dialog".
// ---------------------------------------------------------------------------

/// User-feedback follow-up: the reset-confirm overlay applies a
/// uniform "being reset" dim style to **every cell** of the focused
/// row's rect — label cells, value cells, AND description cells —
/// so the visual emphasis is the whole row about to be reset, not
/// biased toward the description column (which already had the
/// dimmest fg before the overlay's blend was applied).
#[test]
fn reset_overlay_dims_all_rows_except_target() {
    use kigi_tui::views::settings_modal::ResetConfirmOverlay;
    use ratatui::buffer::Buffer;
    use ratatui::style::Modifier;
    // Set up a state with at least 3 rows visible AND navigate to a
    // specific target (NOT the initially-selected row) so we can
    // assert dim-vs-full-intensity for both target and non-target
    // rows.
    let mut s = make_state();
    navigate_to(&mut s, "show_timestamps");
    let target_idx = s.selected;
    assert!(
        target_idx > 0,
        "test setup must select a non-first row so we can sample at least one earlier non-target row"
    );

    let area = Rect {
        x: 0,
        y: 0,
        width: 120,
        height: 30,
    };
    let mut buf = Buffer::empty(area);
    let overlay = ResetConfirmOverlay {
        prompt: "Reset 'Show timestamps' to default (on)?",
        breadcrumb_suffix: "Reset 'Show timestamps'",
    };
    kigi_tui::views::settings_modal::render_settings_modal(
        &mut buf,
        area,
        &mut s,
        false,
        Some(&overlay),
    );

    // Locate the target row and a non-target row from the rendered
    // row_rects. Pick the row before the target so it's clearly on a
    // different y-line.
    let target_rect = s.row_rects[target_idx];
    let non_target_idx = (0..target_idx)
        .rev()
        .find(|&i| {
            let r = s.row_rects[i];
            r.width > 0 && r.height > 0
        })
        .expect("at least one visible non-target row above the target");
    let non_target_rect = s.row_rects[non_target_idx];

    assert!(
        target_rect.width > 0 && target_rect.height > 0,
        "target row's hit-rect must be populated by the render path"
    );
    assert!(
        non_target_rect.width > 0 && non_target_rect.height > 0,
        "non-target row's hit-rect must be populated by the render path"
    );
    assert_ne!(
        target_rect.y, non_target_rect.y,
        "target + non-target rows must be on distinct y-lines"
    );

    // **Spotlight invariant.** The reset overlay applies
    // `Modifier::DIM` to every cell outside the target row's
    // y-range inside the list area, and leaves the target row at
    // full intensity. We assert both sides of that contract:
    //   - every cell in the non-target row's rect has DIM
    //   - no cell in the target row's rect has DIM
    let has_dim = |x: u16, y: u16| -> bool {
        buf.cell((x, y))
            .map(|c| c.modifier.contains(Modifier::DIM))
            .unwrap_or(false)
    };

    let mut non_target_dim_count = 0usize;
    for dx in 0..non_target_rect.width {
        if has_dim(non_target_rect.x + dx, non_target_rect.y) {
            non_target_dim_count += 1;
        }
    }
    assert_eq!(
        non_target_dim_count, non_target_rect.width as usize,
        "every cell in the non-target row's rect must carry Modifier::DIM \
         (got {} out of {})",
        non_target_dim_count, non_target_rect.width
    );

    let mut target_dim_count = 0usize;
    for dx in 0..target_rect.width {
        if has_dim(target_rect.x + dx, target_rect.y) {
            target_dim_count += 1;
        }
    }
    assert_eq!(
        target_dim_count, 0,
        "no cell in the target row's rect may carry Modifier::DIM \
         (got {} dimmed out of {})",
        target_dim_count, target_rect.width
    );

    // **Action-element invariant.** The prompt row
    // (rendered ABOVE the row list) and the y/n action footer
    // shortcuts must stay at full intensity — they're the entire
    // point of the overlay. Earlier revisions only asserted dim/no-dim
    // inside `list_area`, so a future refactor that widened the dim
    // sweep to include the prompt or the action footer would have
    // silently regressed without test feedback.
    //
    // The prompt row sits at `area.y` (line 0 of the modal's content
    // area). Sample multiple x-positions to defend against a
    // future regression that only dims a sub-region of the prompt
    // line.
    let prompt_y = area.y;
    let mut prompt_dim_count = 0usize;
    for dx in 0..area.width {
        if has_dim(area.x + dx, prompt_y) {
            prompt_dim_count += 1;
        }
    }
    assert_eq!(
        prompt_dim_count, 0,
        "no cell on the prompt row (y={prompt_y}) may carry Modifier::DIM — \
         the prompt is an action element and must stay at full intensity",
    );

    // The action footer's `y reset` / `n cancel` shortcuts render
    // at the modal's bottom edge. Locate them via the rendered row
    // text and assert no cell on those lines carries DIM.
    let find_row_y = |needle: &str| -> Option<u16> {
        for y in area.y..area.y + area.height {
            let mut row_text = String::new();
            for x in area.x..area.x + area.width {
                if let Some(cell) = buf.cell((x, y)) {
                    row_text.push_str(cell.symbol());
                }
            }
            if row_text.contains(needle) {
                return Some(y);
            }
        }
        None
    };
    let action_rows: Vec<u16> = ["reset", "cancel"]
        .iter()
        .filter_map(|n| find_row_y(n))
        .collect();
    assert!(
        !action_rows.is_empty(),
        "reset/cancel action footer must be visible — found neither row",
    );
    for action_y in action_rows {
        let mut action_dim_count = 0usize;
        for dx in 0..area.width {
            if has_dim(area.x + dx, action_y) {
                action_dim_count += 1;
            }
        }
        assert_eq!(
            action_dim_count, 0,
            "no cell on the action footer row (y={action_y}) may carry \
             Modifier::DIM — y/n shortcuts are action elements and must \
             stay at full intensity",
        );
    }
}

/// User-feedback follow-up: the settings modal renders a 1-line
/// "Ask Kigi" tip footer at the bottom of the content area in
/// Browse, FilterFocused, and PickingEnum modes (always-on tip).
/// The footer is suppressed in `EditingValue` because the editor
/// needs every line for input + validation. This pins the
/// discoverability contract.
#[test]
fn docs_footer_renders_for_browse_and_picker() {
    use ratatui::buffer::Buffer;
    let area = Rect {
        x: 0,
        y: 0,
        width: 120,
        height: 30,
    };
    for fixture_label in ["browse", "picker"] {
        let mut s = make_state();
        if fixture_label == "picker" {
            // Navigate to a row with an Enum kind (theme).
            navigate_to(&mut s, "theme");
            let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
            assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
        }
        let mut buf = Buffer::empty(area);
        kigi_tui::views::settings_modal::render_settings_modal(&mut buf, area, &mut s, false, None);
        let mut all_text = String::new();
        for y in 0..area.height {
            for x in 0..area.width {
                if let Some(cell) = buf.cell((x, y)) {
                    all_text.push_str(cell.symbol());
                }
            }
            all_text.push('\n');
        }
        assert!(
            all_text.contains("Ask Kigi"),
            "[{fixture_label}] docs footer (`Ask Kigi`) must appear in the rendered modal:\n\
             {all_text}"
        );
        assert!(
            all_text.contains("change theme to kigiday"),
            "[{fixture_label}] docs footer must include the example phrasing"
        );
    }
}

// ---------------------------------------------------------------------------
// User-feedback follow-up: expandable rows + restart pill on
// expand/edit.
//
// Right/`l` expands the focused row's description inline below the
// label line; Left/`h` collapses it. Multiple rows can be expanded
// simultaneously. The "restart" pill renders only while the row is
// expanded (change-time feedback is the toast's job).
// ---------------------------------------------------------------------------

/// Helper: render the modal into a sized buffer and return the full
/// rendered text as a single newline-joined string. Used by the
/// expand/collapse tests to detect description text in the buffer.
fn render_modal_to_string(s: &mut SettingsModalState, width: u16, height: u16) -> String {
    use ratatui::buffer::Buffer;
    let area = Rect {
        x: 0,
        y: 0,
        width,
        height,
    };
    let mut buf = Buffer::empty(area);
    kigi_tui::views::settings_modal::render_settings_modal(&mut buf, area, s, false, None);
    let mut out = String::new();
    for y in 0..area.height {
        for x in 0..area.width {
            if let Some(cell) = buf.cell((x, y)) {
                out.push_str(cell.symbol());
            }
        }
        out.push('\n');
    }
    out
}

#[test]
fn right_arrow_expands_focused_row() {
    let mut s = make_state();
    // The default focus is `compact_mode`. Press Right.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Right));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Right on focused row must transition to Changed (expanded), got {outcome:?}"
    );
    assert!(
        s.expanded_keys.contains("compact_mode"),
        "expanded_keys must contain `compact_mode` after Right, got {:?}",
        s.expanded_keys
    );

    let rendered = render_modal_to_string(&mut s, 120, 30);
    // `compact_mode`'s description starts with "Reduce padding".
    assert!(
        rendered.contains("Reduce padding"),
        "expanded row's description must appear in the rendered modal, got:\n{rendered}"
    );
}

#[test]
fn left_arrow_collapses_focused_row() {
    let mut s = make_state();
    // Pre-expand via Right.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Right));
    assert!(s.expanded_keys.contains("compact_mode"));

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Left));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Left on expanded row must transition to Changed (collapsed), got {outcome:?}"
    );
    assert!(
        !s.expanded_keys.contains("compact_mode"),
        "expanded_keys must NOT contain `compact_mode` after Left, got {:?}",
        s.expanded_keys
    );

    let rendered = render_modal_to_string(&mut s, 120, 30);
    assert!(
        !rendered.contains("Reduce padding"),
        "collapsed row's description must NOT appear in the rendered modal, got:\n{rendered}"
    );
}

/// Restart-required setting at its registered default, not expanded:
/// the pill is HIDDEN. User-feedback gate keeps the modal clean for
/// the common "browsing only" case.
#[test]
fn restart_pill_hidden_when_not_expanded_and_not_edited() {
    let mut s = make_state();
    // `show_tips` is restart_required: true and its registered
    // default is `true` (matches the snapshot's None → true fallback
    // in current_value_for). Not expanded → no pill.
    navigate_to(&mut s, "show_tips");
    assert!(!s.expanded_keys.contains("show_tips"));

    let rendered = render_modal_to_string(&mut s, 120, 30);
    let restart_lines: Vec<&str> = rendered
        .lines()
        .filter(|l| l.contains("Show tips") && l.contains("restart"))
        .collect();
    assert!(
        restart_lines.is_empty(),
        "restart pill must NOT render on at-default + collapsed restart_required row, \
         found: {restart_lines:?}"
    );
}

/// Same setting, after Right → expanded. Pill renders.
#[test]
fn restart_pill_visible_when_expanded() {
    let mut s = make_state();
    navigate_to(&mut s, "show_tips");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Right));
    assert!(s.expanded_keys.contains("show_tips"));

    let rendered = render_modal_to_string(&mut s, 120, 30);
    let restart_lines: Vec<&str> = rendered
        .lines()
        .filter(|l| l.contains("Show tips") && l.contains("restart"))
        .collect();
    assert!(
        !restart_lines.is_empty(),
        "restart pill must render on expanded restart_required row, got:\n{rendered}"
    );
}

/// Edited value (differs from registered default) but collapsed:
/// NO pill. A collapsed non-default row showing it forever misreads
/// as "restart pending" — the exact repro a user hit with a
/// previously-set Off value in a fresh session.
#[test]
fn restart_pill_hidden_when_edited_but_collapsed() {
    use kigi_shell::agent::config::UiConfig;
    use kigi_tui::settings::{PagerLocalSnapshot, SettingsRegistry};

    // Construct a state where `show_tips` is NOT at its registered
    // default of `true`.
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        PagerLocalSnapshot {
            show_tips: Some(false),
            ..PagerLocalSnapshot::default()
        },
    );
    navigate_to(&mut s, "show_tips");
    assert!(!s.expanded_keys.contains("show_tips"));

    let rendered = render_modal_to_string(&mut s, 120, 30);
    let restart_lines: Vec<&str> = rendered
        .lines()
        .filter(|l| l.contains("Show tips") && l.contains("restart"))
        .collect();
    assert!(
        restart_lines.is_empty(),
        "restart pill must NOT render on edited-but-collapsed row, found: {restart_lines:?}"
    );
}

/// Long descriptions wrap to the modal's content width on multiple
/// lines. The wrapped output covers the description verbatim
/// modulo whitespace normalization.
#[test]
fn expanded_description_wraps_to_modal_width() {
    let mut s = make_state();
    // `permission_mode`'s description is long enough to wrap at 80
    // cols. Expand and check that the entire description text is
    // present in the buffer.
    //
    // **Width.** The `→ expand` shortcut was added to the
    // Browse footer which can push the footer onto an extra line
    // at narrower widths; we render at 80 cols to keep the full
    // wrapped description visible.
    navigate_to(&mut s, "permission_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Right));

    // **Height bump.** Each non-first section header
    // earns a 1-line gap above it. With permission_mode focused
    // (Agent & Approval), two such gaps sit between Appearance
    // and the expanded row's wrapped description, which would
    // squeeze the 3rd wrapped line off the bottom at height=30.
    // Render at 34 lines so the existing assertion about
    // "automatically" still holds.
    let rendered = render_modal_to_string(&mut s, 80, 34);
    // Distinctive phrases from the description text:
    assert!(
        rendered.contains("Default") || rendered.contains("default"),
        "wrapped description must include the 'Default uses' phrase"
    );
    assert!(
        rendered.contains("Always") && rendered.contains("automatically"),
        "wrapped description must include the 'Always approve grants all \
         permissions automatically' phrase, got:\n{rendered}"
    );
}

/// Mouse click on the expand-triangle glyph (col 0 of a setting
/// row) toggles expansion — keyboard ↔ mouse parity for the
/// new expand affordance.
#[test]
fn click_on_expand_glyph_toggles_expansion() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "compact_mode") as u16;

    // First click on col 0 (triangle) — expand.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        0,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "triangle click must transition to Changed (expanded), got {outcome:?}"
    );
    assert!(
        s.expanded_keys.contains("compact_mode"),
        "triangle click on collapsed row must expand it"
    );

    // Second click on col 0 — collapse.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        0,
        row_y,
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(
        !s.expanded_keys.contains("compact_mode"),
        "triangle click on expanded row must collapse it"
    );
}

/// The `l` / `h` vim aliases mirror the Right / Left arrows.
#[test]
fn vim_l_h_keys_toggle_expansion() {
    let mut s = make_state();
    // l → expand.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char('l')));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(s.expanded_keys.contains("compact_mode"));

    // h → collapse.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char('h')));
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    assert!(!s.expanded_keys.contains("compact_mode"));
}

/// The confirmation overlay renders the prompt text inline at the
/// top of the content area — fixes the critical UX bug
/// where the dialog was invisible.
#[test]
fn reset_confirm_overlay_renders_prompt_with_setting_label_and_default() {
    use kigi_tui::views::settings_modal::ResetConfirmOverlay;
    use ratatui::buffer::Buffer;
    let mut s = make_state();
    let area = Rect {
        x: 0,
        y: 0,
        width: 120,
        height: 30,
    };
    let mut buf = Buffer::empty(area);
    let overlay = ResetConfirmOverlay {
        prompt: "Reset 'Compact mode' to default (off)?",
        breadcrumb_suffix: "Reset 'Compact mode'",
    };
    kigi_tui::views::settings_modal::render_settings_modal(
        &mut buf,
        area,
        &mut s,
        false,
        Some(&overlay),
    );
    let mut all_text = String::new();
    for y in 0..area.height {
        for x in 0..area.width {
            if let Some(cell) = buf.cell((x, y)) {
                all_text.push_str(cell.symbol());
            }
        }
        all_text.push('\n');
    }
    // The prompt text appears in the rendered buffer.
    assert!(
        all_text.contains("Reset 'Compact mode' to default (off)?"),
        "overlay prompt must appear in the rendered buffer:\n{all_text}"
    );
    // The chrome breadcrumb shows the abbreviated form.
    assert!(
        all_text.contains("Reset 'Compact mode'"),
        "breadcrumb suffix must appear in the chrome title:\n{all_text}"
    );
    // The confirmation footer shortcuts are visible.
    assert!(
        all_text.contains("y reset"),
        "y reset shortcut must appear in the rendered buffer:\n{all_text}",
    );
    assert!(
        all_text.contains("n cancel"),
        "n cancel shortcut must appear in the rendered buffer:\n{all_text}",
    );
}

/// Verify the modal helper `reset_confirm_prompt` constructs a
/// well-formed prompt for each registered setting. Catches a
/// formatter regression where a registry catalog reorder or a
/// missing display string would render an empty or garbled prompt.
#[test]
fn reset_confirm_prompt_helper_builds_well_formed_string_for_every_setting() {
    use kigi_shell::agent::config::UiConfig;
    use kigi_tui::settings::{PagerLocalSnapshot, SettingsRegistry};
    use kigi_tui::views::modal::{ActiveModal, ModalConfirmation, reset_confirm_prompt};
    let reg = SettingsRegistry::defaults();
    for meta in reg.all() {
        let state = Box::new(SettingsModalState::new(
            Arc::new(reg.clone()),
            UiConfig::default(),
            PagerLocalSnapshot::default(),
        ));
        let modal = ActiveModal::ResetSettingsConfirm {
            modal: ModalConfirmation::reset_settings(),
            key: meta.key,
            settings_state: state,
        };
        let prompt = reset_confirm_prompt(&modal).unwrap_or_else(|| {
            panic!(
                "reset_confirm_prompt returned None for `{}` — registry skew",
                meta.key
            )
        });
        assert!(
            prompt.contains(meta.label),
            "prompt for `{}` must contain its label `{}`, got: {prompt:?}",
            meta.key,
            meta.label,
        );
        assert!(
            prompt.starts_with("Reset"),
            "prompt for `{}` must start with 'Reset', got: {prompt:?}",
            meta.key,
        );
        assert!(
            prompt.ends_with("?"),
            "prompt for `{}` must be phrased as a question, got: {prompt:?}",
            meta.key,
        );
    }
}

// ---------------------------------------------------------------------------
// String + Int editor + validators
// ---------------------------------------------------------------------------

/// Int stepper: Enter opens, Up/Down/Left/Right step+clamp, Enter commits.
#[test]
fn pr15_int_stepper_commit_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "max_thoughts_width");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on Int row must transition to EditingValue, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => {
            assert_eq!(buffer, "120", "buffer must seed from default");
        }
        other => panic!("expected EditingValue mode, got {other:?}"),
    }

    // Stepper: Up = +5 → 125. Right = +10 → 135. Down x3 = -15
    // → 120. Up x16 = +80 → 200.
    for _ in 0..16 {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Up));
    }
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => assert_eq!(buffer, "200"),
        other => panic!("expected EditingValue, got {other:?}"),
    }

    // Enter commits at 200.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetMaxThoughtsWidth(200)) => {}
        other => panic!("expected SetMaxThoughtsWidth(200), got {other:?}"),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "successful commit must return to Browse"
    );
}

/// `default_model` DynamicEnum picker: Enter opens, catalog rows
/// dispatch `SetDefaultModel(<ModelId>)` resolved from snapshot.
#[test]
fn pr14_default_model_picker_commits_resolved_model_id() {
    let snapshot = PagerLocalSnapshot {
        available_models: vec![
            (
                "Kigi 4.5".to_string(),
                agent_client_protocol::ModelId::new(std::sync::Arc::from("kigi-4.5")),
            ),
            (
                "Kigi 3".to_string(),
                agent_client_protocol::ModelId::new(std::sync::Arc::from("kigi-3")),
            ),
        ],
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    navigate_to(&mut s, "default_model");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on DynamicEnum row must transition to PickingEnum, got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::PickingEnum { key, .. } if key == "default_model"),
        "Enter must transition to PickingEnum for default_model"
    );

    // Walk down past row 0 ("(no override)") to row 1 ("Kigi 4.5").
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Down));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Down on picker must move the focus, got {outcome:?}"
    );

    // Enter commits the selected model.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetDefaultModel(id)) => {
            assert_eq!(
                id.0.as_ref(),
                "kigi-4.5",
                "committed id must match snapshot"
            );
        }
        other => panic!("expected SetDefaultModel(<id>) on commit, got {other:?}"),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "successful commit must return to Browse"
    );
}

/// Row-0 "(no override)" dispatches `ClearDefaultModel`.
#[test]
fn pr14_default_model_picker_row_zero_commits_clear_action() {
    let snapshot = PagerLocalSnapshot {
        available_models: vec![(
            "Kigi 3".to_string(),
            agent_client_protocol::ModelId::new(std::sync::Arc::from("kigi-3")),
        )],
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    navigate_to(&mut s, "default_model");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // Picker opens with choices_idx at the snapshot's current model,
    // OR at 0 when current_model_name is None. The fixture above
    // leaves current_model_name as None → picker opens on row 0.
    match &s.mode {
        SettingsModalMode::PickingEnum { choices_idx, .. } => {
            assert_eq!(
                *choices_idx, 0,
                "picker must seed at row 0 when no current model is set"
            );
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
    // Enter on row 0 → ClearDefaultModel.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::ClearDefaultModel) => {}
        other => panic!("expected ClearDefaultModel from row-0 commit, got {other:?}"),
    }
}

/// Mouse click on `default_model` opens picker (keyboard ↔ mouse parity).
#[test]
fn pr14_mouse_click_on_dynamic_enum_row_opens_picker() {
    let snapshot = PagerLocalSnapshot {
        available_models: vec![(
            "Kigi 3".to_string(),
            agent_client_protocol::ModelId::new(std::sync::Arc::from("kigi-3")),
        )],
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    s.list_area = Rect {
        x: 0,
        y: 0,
        width: 80,
        height: 80,
    };
    s.row_rects.resize(s.rows.len(), Rect::default());
    let row_idx = row_idx_for(&s, "default_model");
    s.row_rects[row_idx] = Rect {
        x: 0,
        y: row_idx as u16,
        width: 80,
        height: 1,
    };
    // First click: select.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        20,
        row_idx as u16,
    );
    assert!(matches!(outcome, SettingsKeyOutcome::Changed));
    // Second click on the same row: opens picker.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        20,
        row_idx as u16,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on DynamicEnum row must open picker, got {outcome:?}",
    );
    assert!(
        matches!(s.mode, SettingsModalMode::PickingEnum { key, .. } if key == "default_model"),
        "second click on DynamicEnum row must transition to PickingEnum, got {:?}",
        s.mode,
    );
}

/// Mouse click on `max_thoughts_width` opens the inline editor.
#[test]
fn pr8_mouse_click_on_int_row_opens_editor() {
    let mut s = make_state();
    s.list_area = Rect {
        x: 0,
        y: 0,
        width: 80,
        height: 80,
    };
    s.row_rects.resize(s.rows.len(), Rect::default());
    let row_idx = row_idx_for(&s, "max_thoughts_width");
    s.row_rects[row_idx] = Rect {
        x: 0,
        y: row_idx as u16,
        width: 80,
        height: 1,
    };
    // First click selects.
    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        20,
        row_idx as u16,
    );
    // Second click opens editor.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        20,
        row_idx as u16,
    );

    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on Int row must be Changed, got {outcome:?}",
    );
    assert!(
        matches!(s.mode, SettingsModalMode::EditingValue { key, .. } if key == "max_thoughts_width"),
        "second click on Int row must transition to EditingValue, got {:?}",
        s.mode,
    );
}

/// Int stepper: Up/Down ±5, Left/Right ±10, clamped to [min, max].
#[test]
fn pr15_int_stepper_up_down_left_right_steps_and_clamps() {
    let mut s = make_state();
    navigate_to(&mut s, "max_thoughts_width");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // Default 120. Up: 120 + 5 = 125.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Up));
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => assert_eq!(buffer, "125"),
        _ => panic!("expected EditingValue"),
    }
    // Right: 125 + 10 = 135.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Right));
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => assert_eq!(buffer, "135"),
        _ => panic!("expected EditingValue"),
    }
    // Down 20x: 135 - 100 = 35 → clamps to min (40).
    for _ in 0..20 {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    }
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => {
            assert_eq!(buffer, "40", "must clamp to min");
        }
        _ => panic!("expected EditingValue"),
    }
    // Right 100x: 40 + 1000 = 1040 → clamps to max (500).
    for _ in 0..100 {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Right));
    }
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => {
            assert_eq!(buffer, "500", "must clamp to max");
        }
        _ => panic!("expected EditingValue"),
    }
    // Left 100x: 500 - 1000 = -500 → clamps to min (40).
    for _ in 0..100 {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Left));
    }
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => {
            assert_eq!(buffer, "40", "Left must clamp to min");
        }
        _ => panic!("expected EditingValue"),
    }
}

/// Int stepper rejects all text-input keys.
#[test]
fn pr15_int_stepper_rejects_text_input_keys() {
    let mut s = make_state();
    navigate_to(&mut s, "max_thoughts_width");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    let initial_buffer = match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => buffer.clone(),
        _ => panic!("expected EditingValue"),
    };
    assert_eq!(initial_buffer, "120", "buffer seeds from default");

    let reject_keys = &[
        KeyCode::Char('5'),
        KeyCode::Char('a'),
        KeyCode::Char('-'),
        // Extended reject-set.
        KeyCode::Char(' '), // Space (the Bool-toggle key in Browse mode)
        KeyCode::Char('+'), // Plus (would be a naïve numpad expectation)
        KeyCode::Char('.'), // Decimal point
        KeyCode::Backspace,
        KeyCode::Delete,
        KeyCode::Home,
        KeyCode::End,
        KeyCode::Insert,
        KeyCode::PageUp,
        KeyCode::PageDown,
    ];
    for k in reject_keys {
        let outcome = handle_settings_key(&mut s, &press(*k));
        assert!(
            matches!(outcome, SettingsKeyOutcome::Unchanged),
            "Int stepper must reject {k:?} (got {outcome:?})",
        );
        match &s.mode {
            SettingsModalMode::EditingValue { buffer, .. } => {
                assert_eq!(
                    buffer, &initial_buffer,
                    "buffer must stay at {initial_buffer:?} after {k:?}",
                );
            }
            _ => panic!("mode must remain EditingValue"),
        }
    }

    // Ctrl-modifier chords are also rejected.
    use crossterm::event::KeyModifiers;
    let press_with =
        |code: KeyCode, mods: KeyModifiers| crossterm::event::KeyEvent::new(code, mods);
    let outcome = handle_settings_key(
        &mut s,
        &press_with(KeyCode::Char('c'), KeyModifiers::CONTROL),
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Unchanged),
        "Int stepper must reject Ctrl+c (got {outcome:?})",
    );
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => {
            assert_eq!(
                buffer, &initial_buffer,
                "buffer must stay unchanged after Ctrl+c"
            );
        }
        _ => panic!("mode must remain EditingValue"),
    }
}

/// Esc in EditingValue returns to Browse without dispatching.
#[test]
fn pr8_esc_in_editing_value_cancels_without_dispatch() {
    let mut s = make_state();
    navigate_to(&mut s, "max_thoughts_width");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // Type some chars to dirty the buffer.
    for ch in "999".chars() {
        let _ = handle_settings_key(&mut s, &press(KeyCode::Char(ch)));
    }
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Esc in EditingValue must be Changed (mode swap), got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Esc must return to Browse"
    );
}

/// `default_model` and `max_thoughts_width` defaults round-trip
/// against hard-coded literals.
#[test]
fn pr8_default_model_and_max_thoughts_width_defaults_roundtrip() {
    use kigi_tui::settings::current_value_for;
    let reg = SettingsRegistry::defaults();
    let ui = UiConfig::default();
    let pager = PagerLocalSnapshot::default();

    // default_model: registered default is the empty-string sentinel
    // (no UiConfig mirror — cfg.models.default is resolved
    // dynamically). `current_value_for` reads from
    // `pager.current_model_name` which is None by default →
    // `unwrap_or_default()` produces the empty string. Both paths
    // converge on `SettingValue::String("")`.
    let dm_meta = reg.find("default_model").unwrap();
    assert_eq!(
        kigi_tui::settings::default_value_for(dm_meta),
        SettingValue::String(String::new()),
        "default_model registered default must be the empty string",
    );
    assert_eq!(
        current_value_for("default_model", &ui, &pager).unwrap(),
        SettingValue::String(String::new()),
        "default_model current_value_for with empty pager snapshot must be empty",
    );

    // max_thoughts_width: registered default is 120 (matches
    // UiConfig::default()'s DEFAULT_MAX_THOUGHTS_WIDTH constant).
    let mt_meta = reg.find("max_thoughts_width").unwrap();
    assert_eq!(
        kigi_tui::settings::default_value_for(mt_meta),
        SettingValue::Int(120),
        "max_thoughts_width registered default must be 120",
    );
    assert_eq!(
        current_value_for("max_thoughts_width", &ui, &pager).unwrap(),
        SettingValue::Int(120),
        "max_thoughts_width current_value_for must be 120 (UiConfig::default())",
    );
}

// ---------------------------------------------------------------------------
// default_selected_permission (Agent Enum, no preview — SHELL-owned, persists)
// ---------------------------------------------------------------------------

/// `default_selected_permission` lives under `Agent` and is SHELL-owned.
#[test]
fn default_selected_permission_renders_under_agent_category() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("default_selected_permission")
        .expect("default_selected_permission must be registered");
    assert_eq!(
        meta.category,
        SettingCategory::Agent,
        "default_selected_permission must live under Agent"
    );
    assert_eq!(
        meta.owner,
        SettingOwner::Shell,
        "default_selected_permission is SHELL-owned (persists to [ui].default_selected_permission)"
    );
}

/// `default_selected_permission` must be `supports_preview: false` —
/// permission prompts aren't open in the modal background, so there is
/// no live preview surface to drive.
#[test]
fn default_selected_permission_does_not_support_preview() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("default_selected_permission")
        .expect("default_selected_permission must be registered");
    match &meta.kind {
        SettingKind::Enum {
            supports_preview, ..
        } => {
            assert!(
                !supports_preview,
                "default_selected_permission MUST be supports_preview: false",
            );
        }
        other => panic!("expected Enum kind for default_selected_permission, got {other:?}"),
    }
}

/// `current_value_for` maps `UiConfig::default()` (None on disk) onto the
/// `always_allow_all_sessions` canonical (the effective default).
#[test]
fn default_selected_permission_current_value_defaults_to_always_allow_all_sessions() {
    use kigi_tui::settings::current_value_for;
    let ui = UiConfig::default();
    let pager = PagerLocalSnapshot::default();
    assert_eq!(
        current_value_for("default_selected_permission", &ui, &pager),
        Some(SettingValue::Enum("always_allow_all_sessions")),
        "None on disk → canonical 'always_allow_all_sessions' (effective default)",
    );
}

/// Enter opens the picker seeded to the current ("always_allow_all_sessions") value.
#[test]
fn default_selected_permission_enter_enters_picking_enum() {
    let mut s = make_state();
    navigate_to(&mut s, "default_selected_permission");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on default_selected_permission row must transition to PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            original_value,
            ..
        } => {
            assert_eq!(*key, "default_selected_permission");
            assert_eq!(
                original_value,
                &SettingValue::Enum("always_allow_all_sessions"),
                "default UiConfig → original 'always_allow_all_sessions'"
            );
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

/// Nav in the picker must NOT dispatch an Action — there is no preview.
#[test]
fn default_selected_permission_picker_nav_does_not_dispatch_preview() {
    for nav_key in &[
        KeyCode::Down,
        KeyCode::Char('j'),
        KeyCode::Up,
        KeyCode::Char('k'),
    ] {
        let mut s = make_state();
        navigate_to(&mut s, "default_selected_permission");
        let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));

        if matches!(nav_key, KeyCode::Up | KeyCode::Char('k')) {
            let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
        }

        let outcome = handle_settings_key(&mut s, &press(*nav_key));
        assert!(
            matches!(outcome, SettingsKeyOutcome::Changed),
            "Nav key {nav_key:?} in default_selected_permission picker MUST NOT dispatch an \
             Action (no preview). Got {outcome:?}",
        );
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
    }
}

/// Enter commits `SetDefaultSelectedPermission(canonical)` carrying the
/// selected choice's canonical string, then returns to Browse.
#[test]
fn default_selected_permission_picker_enter_dispatches_set_commit() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("default_selected_permission").unwrap();
    let (default_canonical, choices) = match &meta.kind {
        SettingKind::Enum {
            default, choices, ..
        } => (*default, *choices),
        _ => panic!("default_selected_permission must be Enum"),
    };
    // Picker seeds at the current value ("always_allow_all_sessions"); navigate one row down
    // and resolve that choice's canonical from the registry rather than
    // hardcoding it — robust against future catalog reordering.
    let seed_idx = choices
        .iter()
        .position(|c| c.canonical == default_canonical)
        .expect("default canonical must exist in choices");
    let target_canonical = choices
        .get(seed_idx + 1)
        .map(|c| c.canonical)
        .expect("default_selected_permission must have a choice after the default");

    let mut s = make_state();
    navigate_to(&mut s, "default_selected_permission");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // Nav to the next choice.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    // Enter → commit.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetDefaultSelectedPermission(canonical)) => {
            assert_eq!(
                canonical, target_canonical,
                "Enter must commit the selected canonical via SetDefaultSelectedPermission"
            );
        }
        other => panic!("expected Action::SetDefaultSelectedPermission commit, got {other:?}"),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter commit must return to Browse"
    );
}

/// Esc in the non-preview picker returns to Browse without an Action.
#[test]
fn default_selected_permission_picker_esc_does_not_dispatch_action() {
    let mut s = make_state();
    navigate_to(&mut s, "default_selected_permission");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Esc on non-preview Enum picker must NOT emit an Action. Got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Esc must return to Browse"
    );
}

// ---------------------------------------------------------------------------
// Mouse path tests for default_selected_permission
// ---------------------------------------------------------------------------

/// First click on unselected row only selects.
#[test]
fn default_selected_permission_mouse_click_on_unselected_row_only_selects() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "default_selected_permission") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first body-click on unselected default_selected_permission row should only select, got: {outcome:?}",
    );
    assert_eq!(s.selected, row_y as usize);
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

/// Second click on selected row opens picker.
#[test]
fn default_selected_permission_mouse_click_on_selected_row_opens_picker() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "default_selected_permission") as u16;

    // First click: select.
    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_eq!(s.selected, row_y as usize);

    // Second click on the focused row: open the picker.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on focused Enum row must open picker, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => {
            assert_eq!(*key, "default_selected_permission");
        }
        _ => {
            panic!("second click on focused default_selected_permission row must enter PickingEnum")
        }
    }
}

/// Value-column click opens picker in one click.
#[test]
fn default_selected_permission_mouse_click_on_indicator_opens_picker_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "default_selected_permission") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "value click must open picker in one click, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => {
            assert_eq!(*key, "default_selected_permission");
        }
        _ => {
            panic!("value click on default_selected_permission must enter PickingEnum")
        }
    }
}

// ---------------------------------------------------------------------------
// `plan_mode` (Agent-category Enum, PAGER-owned + ACP-mediated,
// supports_preview: false)
//
// Migrated from the per-Action `Action::EnterPlanMode` (no-description
// case) to the typed `Action::SetPlanMode(PlanModeKind)` going through
// the unified `set_plan_mode` dispatch path. The dispatcher is the
// single source of truth for idempotency, optimistic mutation
// (`plan_mode_pending`), modal-snapshot refresh, toast, and the
// `Effect::SetSessionMode` ACP emit.
//
// **Why `supports_preview: false`**: toggling fires an ACP
// `session/set_mode` request that mutates per-agent state and gates
// tool dispatch. Per-keystroke preview would either fire N round-trips
// per nav OR commit on every keystroke. Both are unacceptable.
// ---------------------------------------------------------------------------

/// `plan_mode` lives under the `Agent` section:
/// pins the category against drift.
#[test]
fn pr10_plan_mode_renders_under_agent_category() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("plan_mode").expect("plan_mode must be registered");
    assert_eq!(
        meta.category,
        SettingCategory::Agent,
        "plan_mode must live under Agent"
    );
    assert_eq!(
        meta.owner,
        SettingOwner::Pager,
        "plan_mode is PAGER-owned (per-session, NOT persisted to config.toml; \
         shell drives transitions via ACP CurrentModeUpdate)"
    );
}

/// `plan_mode` is an Enum with `supports_preview: false`. Toggling
/// fires an ACP `session/set_mode` request; per-keystroke preview
/// would either fire N round-trips OR commit on every nav.
#[test]
fn pr10_plan_mode_does_not_support_preview() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("plan_mode").expect("plan_mode must be registered");
    match &meta.kind {
        SettingKind::Enum {
            supports_preview, ..
        } => {
            assert!(
                !supports_preview,
                "plan_mode MUST be supports_preview: false — preview would \
                 fire an ACP round-trip per nav OR commit on every keystroke; \
                 both are unacceptable for an agent-mode transition",
            );
        }
        other => panic!("expected Enum kind for plan_mode, got {other:?}"),
    }
}

/// `current_value_for("plan_mode", _, pager)` reads from the pager
/// snapshot's `plan_mode_active` field (NOT from `UiConfig` — there
/// is no such UiConfig field by design; plan mode is per-session
/// only). Canonical mapping: `true → "on"`, `false → "off"`.
#[test]
fn pr10_current_value_for_reads_pager_snapshot() {
    use kigi_tui::settings::current_value_for;

    let ui = UiConfig::default();

    let off_snap = PagerLocalSnapshot {
        plan_mode_active: false,
        ..PagerLocalSnapshot::default()
    };
    let on_snap = PagerLocalSnapshot {
        plan_mode_active: true,
        ..PagerLocalSnapshot::default()
    };

    assert_eq!(
        current_value_for("plan_mode", &ui, &off_snap),
        Some(SettingValue::Enum("off")),
        "plan_mode_active=false → canonical 'off' (default state)",
    );
    assert_eq!(
        current_value_for("plan_mode", &ui, &on_snap),
        Some(SettingValue::Enum("on")),
        "plan_mode_active=true → canonical 'on'",
    );
}

/// Enter on the `plan_mode` row → PickingEnum mode (mirroring the
/// theme/permission_mode/coding_data_sharing picker), seeded to the
/// canonical of the current live state.
#[test]
fn pr10_enter_on_plan_mode_row_enters_picking_enum() {
    let mut s = make_state();
    navigate_to(&mut s, "plan_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on plan_mode row must transition to PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            original_value,
            ..
        } => {
            assert_eq!(*key, "plan_mode");
            assert_eq!(
                original_value,
                &SettingValue::Enum("off"),
                "default snapshot plan_mode_active=false → original 'off'"
            );
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

/// **Regression test.** Up/Down/j/k nav in the `plan_mode` picker
/// MUST NOT dispatch a preview Action — that would fire an ACP
/// round-trip per keystroke (the ACP path is eager). Mirror of
/// `pr6_permission_mode_picker_nav_does_not_dispatch_preview` and
/// `pr9_coding_data_sharing_picker_nav_does_not_dispatch_preview`.
#[test]
fn pr10_plan_mode_picker_nav_does_not_dispatch_preview() {
    for nav_key in &[
        KeyCode::Down,
        KeyCode::Char('j'),
        KeyCode::Up,
        KeyCode::Char('k'),
    ] {
        let mut s = make_state();
        navigate_to(&mut s, "plan_mode");
        let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));

        if matches!(nav_key, KeyCode::Up | KeyCode::Char('k')) {
            let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
        }

        let outcome = handle_settings_key(&mut s, &press(*nav_key));
        assert!(
            matches!(outcome, SettingsKeyOutcome::Changed),
            "Nav key {nav_key:?} in plan_mode picker MUST NOT dispatch a preview \
             Action — that would fire an ACP round-trip per keystroke. Got {outcome:?}",
        );
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
    }
}

/// Enter on the focused picker choice commits via
/// `Action::SetPlanMode(PlanModeKind)` — the typed setter, not a
/// preview variant. Pins the canonical-to-PlanModeKind mapping
/// (on→On, off→Off).
#[test]
fn pr10_plan_mode_picker_enter_dispatches_set_commit() {
    use kigi_tui::app::actions::PlanModeKind;

    let mut s = make_state();
    navigate_to(&mut s, "plan_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // Default snapshot has plan_mode_active=false → picker seeds at
    // "off". Down nav moves to "on".
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    // Enter → commit.
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetPlanMode(kind)) => {
            assert_eq!(
                kind,
                PlanModeKind::On,
                "Enter must commit `on` → SetPlanMode(PlanModeKind::On)"
            );
        }
        other => panic!("expected Action::SetPlanMode commit, got {other:?}"),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter commit must return to Browse"
    );
}

/// Esc inside the picker for a non-preview Enum returns to Browse
/// without dispatching any Action. Mirror of
/// `pr9_coding_data_sharing_picker_esc_does_not_dispatch_action`.
/// Since `plan_mode` has no preview, Esc must NOT re-persist.
#[test]
fn pr10_plan_mode_picker_esc_does_not_dispatch_action() {
    let mut s = make_state();
    navigate_to(&mut s, "plan_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Esc on non-preview Enum picker must NOT emit an Action — \
         doing so would fire an ACP round-trip on every Esc. Got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Esc must return to Browse"
    );
}

/// Snapshot-driven seeding: `PagerLocalSnapshot { plan_mode_active:
/// true }` makes Enter on the row open the picker seeded at the
/// "on" choice.
#[test]
fn pr10_picker_seeds_choices_idx_from_pager_snapshot_plan_mode_active() {
    let snapshot = PagerLocalSnapshot {
        plan_mode_active: true,
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    navigate_to(&mut s, "plan_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    let reg = SettingsRegistry::defaults();
    let on_idx = match &reg.find("plan_mode").unwrap().kind {
        SettingKind::Enum { choices, .. } => choices
            .iter()
            .position(|c| c.canonical == "on")
            .expect("plan_mode must have 'on' choice"),
        _ => panic!("plan_mode must be Enum"),
    };
    match s.mode {
        SettingsModalMode::PickingEnum {
            choices_idx,
            ref original_value,
            ..
        } => {
            assert_eq!(
                choices_idx, on_idx,
                "picker must seed at the 'on' index when snapshot says plan_mode_active=true"
            );
            assert_eq!(
                original_value,
                &SettingValue::Enum("on"),
                "original_value must match the live snapshot"
            );
        }
        ref other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

/// The choices catalog includes EXACTLY "off" and "on" with canonical
/// strings matching the dispatcher contract. Pins the canonical
/// contract — anything else here breaks the `action_for_enum_commit`
/// arm in `views/settings_modal.rs`.
#[test]
fn pr10_plan_mode_choices_use_canonical_strings() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("plan_mode").unwrap();
    let canonicals: Vec<&str> = match &meta.kind {
        SettingKind::Enum { choices, .. } => choices.iter().map(|c| c.canonical).collect(),
        _ => panic!("plan_mode must be Enum"),
    };
    assert_eq!(
        canonicals.len(),
        2,
        "plan_mode catalog must be exactly {{off, on}} — adding a choice requires \
         updating the action_for_enum_commit arm in views/settings_modal.rs AND \
         the action_for_reset arm in dispatch.rs AND PlanModeKind in actions.rs",
    );
    assert!(
        canonicals.contains(&"off"),
        "plan_mode must include 'off' canonical"
    );
    assert!(
        canonicals.contains(&"on"),
        "plan_mode must include 'on' canonical"
    );
}

// ---------------------------------------------------------------------------
// Mouse path tests for plan_mode (keyboard ↔ mouse parity).
//
// Mirrors the permission_mode / coding_data_sharing mouse tests. Every
// keyboard interaction has a mouse equivalent.
// ---------------------------------------------------------------------------

/// First mouse-click on a DIFFERENT (non-selected) `plan_mode` row
/// only SELECTS the row (no picker entry, no Action). Mirrors the
/// two-stage Bool-row select-then-toggle UX.
#[test]
fn pr10_mouse_click_on_unselected_plan_mode_row_only_selects() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "plan_mode") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first body-click on unselected plan_mode row should only select, got: {outcome:?}",
    );
    assert_eq!(s.selected, row_y as usize);
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

/// Second mouse-click on the selected row opens the picker for the
/// Enum row — mirroring the keyboard Enter path.
#[test]
fn pr10_mouse_click_on_selected_plan_mode_row_opens_picker() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "plan_mode") as u16;

    // First click: select.
    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_eq!(s.selected, row_y as usize);

    // Second click on the focused row: open the picker.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on focused Enum row must open picker, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => {
            assert_eq!(*key, "plan_mode");
        }
        _ => panic!("second click on focused plan_mode row must enter PickingEnum"),
    }
}

/// **Value-column layout.** Value-column click on the
/// plan_mode row opens the picker in ONE click — replaces the
/// previous left-edge indicator hit-rect.
#[test]
fn pr10_mouse_click_on_plan_mode_indicator_opens_picker_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "plan_mode") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "value click must open picker in one click, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => {
            assert_eq!(*key, "plan_mode");
        }
        _ => {
            panic!("value click on plan_mode must enter PickingEnum")
        }
    }
}

// ---------------------------------------------------------------------------
// `render_mermaid` (SHELL-owned Enum, Appearance).
//
// Unlike `plan_mode` (PAGER-owned, snapshot-seeded), `render_mermaid` is
// SHELL-owned: the live value comes from the process-wide cache mirror
// (`appearance::cache::load_render_mermaid`, default `auto`), mirroring how
// `vim_mode` reads its cache. The picker commits the typed
// `Action::SetRenderMermaid(RenderMermaid)` (canonical → enum via
// `RenderMermaid::from_canonical`). `supports_preview: false`, so picker nav
// and Esc must never dispatch an Action.
//
// These tests honor the `ALL_SETTINGS_EXERCISED` contract — keyboard AND
// mouse coverage, same rigor as `plan_mode` / `coding_data_sharing`.
// ---------------------------------------------------------------------------

/// `render_mermaid` lives under `Appearance` and is SHELL-owned (persisted to
/// `[ui].render_mermaid`). Pins the category + owner against drift.
#[test]
fn render_mermaid_renders_under_appearance_category() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("render_mermaid")
        .expect("render_mermaid must be registered");
    assert_eq!(
        meta.category,
        SettingCategory::Appearance,
        "render_mermaid must live under Appearance"
    );
    assert_eq!(
        meta.owner,
        SettingOwner::Shell,
        "render_mermaid is SHELL-owned (persisted to `[ui].render_mermaid`)"
    );
}

/// `render_mermaid` is an Enum with `supports_preview: false` — toggling
/// flips the render path for every Mermaid block, so per-keystroke preview
/// would thrash the scrollback. Commit-on-Enter only.
#[test]
fn render_mermaid_does_not_support_preview() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("render_mermaid")
        .expect("render_mermaid must be registered");
    match &meta.kind {
        SettingKind::Enum {
            supports_preview, ..
        } => assert!(
            !supports_preview,
            "render_mermaid MUST be supports_preview: false",
        ),
        other => panic!("expected Enum kind for render_mermaid, got {other:?}"),
    }
}

/// Enter on the `render_mermaid` row → PickingEnum, seeded to the live cache
/// value. With a fresh cache (no `[ui].render_mermaid` override) the default
/// is `auto`.
#[test]
fn enter_on_render_mermaid_row_enters_picking_enum() {
    let mut s = make_state();
    navigate_to(&mut s, "render_mermaid");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on render_mermaid row must transition to PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            original_value,
            ..
        } => {
            assert_eq!(*key, "render_mermaid");
            assert_eq!(
                original_value,
                &SettingValue::Enum("auto"),
                "default cache render_mermaid → original 'auto'"
            );
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

/// **Regression test.** Up/Down/j/k nav in the `render_mermaid` picker MUST
/// NOT dispatch a preview Action — `supports_preview: false`. Mirror of
/// `pr10_plan_mode_picker_nav_does_not_dispatch_preview`.
#[test]
fn render_mermaid_picker_nav_does_not_dispatch_preview() {
    for nav_key in &[
        KeyCode::Down,
        KeyCode::Char('j'),
        KeyCode::Up,
        KeyCode::Char('k'),
    ] {
        let mut s = make_state();
        navigate_to(&mut s, "render_mermaid");
        let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));

        if matches!(nav_key, KeyCode::Up | KeyCode::Char('k')) {
            let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
        }

        let outcome = handle_settings_key(&mut s, &press(*nav_key));
        assert!(
            matches!(outcome, SettingsKeyOutcome::Changed),
            "Nav key {nav_key:?} in render_mermaid picker MUST NOT dispatch a preview \
             Action. Got {outcome:?}",
        );
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
    }
}

/// Enter on the focused picker choice commits via
/// `Action::SetRenderMermaid(RenderMermaid)` — the typed setter. Default
/// seed is `auto` (index 0); one Down moves to `on` (index 1). Pins the
/// canonical-to-RenderMermaid mapping.
#[test]
fn render_mermaid_picker_enter_dispatches_set_commit() {
    use kigi_tui::appearance::RenderMermaid;

    let mut s = make_state();
    navigate_to(&mut s, "render_mermaid");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // Fresh cache seeds the picker at "auto"; Down moves to "on".
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetRenderMermaid(kind)) => {
            assert_eq!(
                kind,
                RenderMermaid::On,
                "Enter must commit `on` → SetRenderMermaid(RenderMermaid::On)"
            );
        }
        other => panic!("expected Action::SetRenderMermaid commit, got {other:?}"),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter commit must return to Browse"
    );
}

/// Esc inside the picker for a non-preview Enum returns to Browse without
/// dispatching any Action. Mirror of
/// `pr10_plan_mode_picker_esc_does_not_dispatch_action`.
#[test]
fn render_mermaid_picker_esc_does_not_dispatch_action() {
    let mut s = make_state();
    navigate_to(&mut s, "render_mermaid");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Esc on non-preview Enum picker must NOT emit an Action. Got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Esc must return to Browse"
    );
}

/// The choices catalog is EXACTLY {auto, on, off} with canonical strings
/// matching `RenderMermaid::as_canonical` and the `action_for_enum_commit`
/// arm in `views/settings_modal.rs`.
#[test]
fn render_mermaid_choices_use_canonical_strings() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("render_mermaid").unwrap();
    let canonicals: Vec<&str> = match &meta.kind {
        SettingKind::Enum { choices, .. } => choices.iter().map(|c| c.canonical).collect(),
        _ => panic!("render_mermaid must be Enum"),
    };
    assert_eq!(
        canonicals,
        vec!["auto", "on", "off"],
        "render_mermaid catalog must be exactly [auto, on, off] in order — \
         changing it requires updating RenderMermaid::from_canonical AND the \
         action_for_enum_commit arm in views/settings_modal.rs",
    );
}

// ---------------------------------------------------------------------------
// Mouse path tests for render_mermaid (keyboard ↔ mouse
// parity). Mirrors the plan_mode mouse tests.
// ---------------------------------------------------------------------------

/// First mouse-click on a DIFFERENT (non-selected) `render_mermaid` row only
/// SELECTS the row (no picker entry, no Action).
#[test]
fn mouse_click_on_unselected_render_mermaid_row_only_selects() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "render_mermaid") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first body-click on unselected render_mermaid row should only select, got: {outcome:?}",
    );
    assert_eq!(s.selected, row_y as usize);
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

/// Second mouse-click on the selected row opens the picker for the Enum row —
/// mirroring the keyboard Enter path.
#[test]
fn mouse_click_on_selected_render_mermaid_row_opens_picker() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "render_mermaid") as u16;

    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_eq!(s.selected, row_y as usize);

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on focused Enum row must open picker, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => assert_eq!(*key, "render_mermaid"),
        _ => panic!("second click on focused render_mermaid row must enter PickingEnum"),
    }
}

/// Value-column click on the render_mermaid row opens the picker in ONE click.
#[test]
fn mouse_click_on_render_mermaid_indicator_opens_picker_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "render_mermaid") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "value click must open picker in one click, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => assert_eq!(*key, "render_mermaid"),
        _ => panic!("value click on render_mermaid must enter PickingEnum"),
    }
}

// ---------------------------------------------------------------------------
// screen_mode (SHELL Enum, Appearance, restart_required, no preview).
// Catalog [fullscreen, minimal]; product default when unset is fullscreen.
// Session-only switches stay on /minimal and /fullscreen (do not write config).
// ---------------------------------------------------------------------------

/// Enter on the `screen_mode` row opens the picker seeded at the product
/// default `fullscreen` (UiConfig.screen_mode is None → canonical fullscreen).
#[test]
fn enter_on_screen_mode_row_enters_picking_enum() {
    let mut s = make_state();
    navigate_to(&mut s, "screen_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on screen_mode row must transition to PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            original_value,
            ..
        } => {
            assert_eq!(*key, "screen_mode");
            assert_eq!(
                original_value,
                &SettingValue::Enum("fullscreen"),
                "default UiConfig screen_mode=None → original 'fullscreen'"
            );
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

/// **Regression test.** Up/Down/j/k nav in the `screen_mode` picker MUST NOT
/// dispatch a preview Action — `supports_preview: false` (restart-required).
#[test]
fn screen_mode_picker_nav_does_not_dispatch_preview() {
    for nav_key in &[
        KeyCode::Down,
        KeyCode::Char('j'),
        KeyCode::Up,
        KeyCode::Char('k'),
    ] {
        let mut s = make_state();
        navigate_to(&mut s, "screen_mode");
        let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));

        if matches!(nav_key, KeyCode::Up | KeyCode::Char('k')) {
            let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
        }

        let outcome = handle_settings_key(&mut s, &press(*nav_key));
        assert!(
            matches!(outcome, SettingsKeyOutcome::Changed),
            "Nav key {nav_key:?} in screen_mode picker MUST NOT dispatch a preview \
             Action. Got {outcome:?}",
        );
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
    }
}

/// Enter on the focused picker choice commits via
/// `Action::SetScreenMode(String)` carrying the registry canonical. Seed is
/// `fullscreen` (index 0); one Down moves to `minimal` (index 1).
#[test]
fn screen_mode_picker_enter_dispatches_set_commit() {
    let mut s = make_state();
    navigate_to(&mut s, "screen_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetScreenMode(mode)) => {
            assert_eq!(
                mode, "minimal",
                "Enter must commit `minimal` → SetScreenMode(\"minimal\")"
            );
        }
        other => panic!("expected Action::SetScreenMode commit, got {other:?}"),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter commit must return to Browse"
    );
}

/// The choices catalog is EXACTLY {fullscreen, minimal} in order — contract
/// with `canonical_screen_mode` and the settings UI labels.
#[test]
fn screen_mode_choices_use_canonical_strings() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("screen_mode").unwrap();
    let canonicals: Vec<&str> = match &meta.kind {
        SettingKind::Enum { choices, .. } => choices.iter().map(|c| c.canonical).collect(),
        _ => panic!("screen_mode must be Enum"),
    };
    assert_eq!(
        canonicals,
        vec!["fullscreen", "minimal"],
        "screen_mode catalog must be exactly [fullscreen, minimal] in order — \
         changing it requires updating canonical_screen_mode and the chooser",
    );
    match &meta.kind {
        SettingKind::Enum {
            supports_preview, ..
        } => {
            assert!(
                !*supports_preview,
                "screen_mode is restart-required — no live preview"
            );
        }
        _ => unreachable!(),
    }
    assert!(meta.restart_required, "screen_mode requires restart");
}

/// Value-column click on the screen_mode row opens the picker in ONE click
/// (mouse ↔ keyboard parity).
#[test]
fn mouse_click_on_screen_mode_indicator_opens_picker_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "screen_mode") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "value click must open picker in one click, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => assert_eq!(*key, "screen_mode"),
        _ => panic!("value click on screen_mode must enter PickingEnum"),
    }
}

// ---------------------------------------------------------------------------
// hunk_tracker_mode (SHELL Enum, Advanced, restart_required, no preview).
// Catalog [agent_only, all_dirty, off]; `disabled` aliases `off` at parse
// time. Mirrors the render_mermaid enum tests (keyboard ↔ mouse parity).
// ---------------------------------------------------------------------------

/// Enter on the `hunk_tracker_mode` row opens the picker seeded at the
/// default `agent_only`.
#[test]
fn enter_on_hunk_tracker_mode_row_enters_picking_enum() {
    let mut s = make_state();
    navigate_to(&mut s, "hunk_tracker_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on hunk_tracker_mode row must transition to PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            original_value,
            ..
        } => {
            assert_eq!(*key, "hunk_tracker_mode");
            assert_eq!(
                original_value,
                &SettingValue::Enum("agent_only"),
                "default UiConfig hunk_tracker_mode → original 'agent_only'"
            );
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

/// **Regression test.** Up/Down/j/k nav in the `hunk_tracker_mode` picker MUST
/// NOT dispatch a preview Action — `supports_preview: false`. Mirror of
/// `render_mermaid_picker_nav_does_not_dispatch_preview`.
#[test]
fn hunk_tracker_mode_picker_nav_does_not_dispatch_preview() {
    for nav_key in &[
        KeyCode::Down,
        KeyCode::Char('j'),
        KeyCode::Up,
        KeyCode::Char('k'),
    ] {
        let mut s = make_state();
        navigate_to(&mut s, "hunk_tracker_mode");
        let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));

        if matches!(nav_key, KeyCode::Up | KeyCode::Char('k')) {
            let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
        }

        let outcome = handle_settings_key(&mut s, &press(*nav_key));
        assert!(
            matches!(outcome, SettingsKeyOutcome::Changed),
            "Nav key {nav_key:?} in hunk_tracker_mode picker MUST NOT dispatch a preview \
             Action. Got {outcome:?}",
        );
        assert!(matches!(s.mode, SettingsModalMode::PickingEnum { .. }));
    }
}

/// Enter on the focused picker choice commits via
/// `Action::SetHunkTrackerMode(String)` carrying the registry canonical. Seed
/// is `agent_only` (index 0); one Down moves to `all_dirty` (index 1). Pins
/// the canonical-string payload that `action_for_enum_commit` forwards.
#[test]
fn hunk_tracker_mode_picker_enter_dispatches_set_commit() {
    let mut s = make_state();
    navigate_to(&mut s, "hunk_tracker_mode");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // Fresh state seeds the picker at "agent_only"; Down moves to "all_dirty".
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetHunkTrackerMode(mode)) => {
            assert_eq!(
                mode, "all_dirty",
                "Enter must commit `all_dirty` → SetHunkTrackerMode(\"all_dirty\")"
            );
        }
        other => panic!("expected Action::SetHunkTrackerMode commit, got {other:?}"),
    }
    assert!(
        matches!(s.mode, SettingsModalMode::Browse),
        "Enter commit must return to Browse"
    );
}

/// The choices catalog is EXACTLY {agent_only, all_dirty, off} in order. The
/// commit forwards `choice.to_string()` verbatim, so these canonicals are the
/// contract with the shell-side `canonical_hunk_tracker_mode` parser.
#[test]
fn hunk_tracker_mode_choices_use_canonical_strings() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("hunk_tracker_mode").unwrap();
    let canonicals: Vec<&str> = match &meta.kind {
        SettingKind::Enum { choices, .. } => choices.iter().map(|c| c.canonical).collect(),
        _ => panic!("hunk_tracker_mode must be Enum"),
    };
    assert_eq!(
        canonicals,
        vec!["agent_only", "all_dirty", "off"],
        "hunk_tracker_mode catalog must be exactly [agent_only, all_dirty, off] in order — \
         changing it requires updating the shell-side canonical_hunk_tracker_mode parser",
    );
}

/// Value-column click on the hunk_tracker_mode row opens the picker in ONE
/// click (mouse ↔ keyboard parity).
#[test]
fn mouse_click_on_hunk_tracker_mode_indicator_opens_picker_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "hunk_tracker_mode") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "value click must open picker in one click, got: {outcome:?}",
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => assert_eq!(*key, "hunk_tracker_mode"),
        _ => panic!("value click on hunk_tracker_mode must enter PickingEnum"),
    }
}

// ---------------------------------------------------------------------------
// CLI batch: show_tips, auto_update (SHELL Bool, restart_required)
// ---------------------------------------------------------------------------

/// Space-toggle on `show_tips` dispatches typed setter.
#[test]
fn pr13_space_on_show_tips_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "show_tips");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "show_tips", false);
}

#[test]
fn pr13_space_on_auto_update_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "auto_update");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "auto_update", false);
}

/// Value-column click on `show_tips` toggles in one click.
#[test]
fn pr13_mouse_click_on_show_tips_indicator_toggles_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "show_tips") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        72,
        row_y,
    );
    assert_set_bool_action(outcome, "show_tips", false);
}

/// Two-stage select-then-toggle on `auto_update`.
#[test]
fn pr13_mouse_click_on_auto_update_two_stage_select_then_toggle() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "auto_update") as u16;

    // First click: select-only (the focused row was compact_mode).
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );

    // Second click on the SAME row should now toggle.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_set_bool_action(outcome, "auto_update", false);
}

/// CLI-batch settings are all `restart_required: true`.
#[test]
fn pr13_cli_batch_all_settings_are_restart_required() {
    let reg = SettingsRegistry::defaults();
    for key in ["show_tips", "auto_update"] {
        let meta = reg
            .find(key)
            .unwrap_or_else(|| panic!("registry must contain `{key}` (PR 13)"));
        assert!(
            meta.restart_required,
            "PR-13 setting `{key}` must have restart_required: true \
             (consumer reads value once at startup; the modal renders \
             the restart pill when this row is expanded)",
        );
        // Sanity: these CLI-batch settings are all SHELL-owned Bool.
        assert_eq!(meta.owner, SettingOwner::Shell);
        assert!(matches!(meta.kind, SettingKind::Bool { .. }));
    }
}

/// CLI-batch defaults round-trip through `current_value_for`.
#[test]
fn pr13_cli_batch_defaults_roundtrip_via_current_value_for() {
    use kigi_tui::settings::current_value_for;
    let ui = UiConfig::default();
    let pager = PagerLocalSnapshot::default();
    for (key, expected) in [("show_tips", true), ("auto_update", true)] {
        let value = current_value_for(key, &ui, &pager)
            .unwrap_or_else(|| panic!("current_value_for(`{key}`) must resolve"));
        assert_eq!(
            value,
            SettingValue::Bool(expected),
            "PR 13: `{key}` defaults to {expected} (matches the consumer's \
             .unwrap_or(...) at the original read site)",
        );
    }
}

/// CLI-batch settings are discoverable via search.
#[test]
fn pr13_cli_batch_settings_are_discoverable_via_search() {
    let reg = SettingsRegistry::defaults();
    let cases = [("tip", "show_tips"), ("auto-update", "auto_update")];
    for (query, expected_key) in cases {
        let hits = reg.search(query);
        assert!(
            hits.iter().any(|m| m.key == expected_key),
            "search(`{query}`) must include `{expected_key}` — hit keys: {:?}",
            hits.iter().map(|m| m.key).collect::<Vec<_>>(),
        );
    }
}

// ---------------------------------------------------------------------------
// fork_secondary_model (DynamicEnum, restart_required: false)
// ---------------------------------------------------------------------------

/// `fork_secondary_model` lives under Models.
#[test]
fn pr14_model_family_renders_under_models_category() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("fork_secondary_model")
        .expect("`fork_secondary_model` must be registered");
    assert_eq!(
        meta.category,
        SettingCategory::Models,
        "`fork_secondary_model` must live under Models"
    );
    assert_eq!(
        meta.owner,
        SettingOwner::Shell,
        "`fork_secondary_model` is SHELL-owned (persisted via util::config)"
    );
}

/// `fork_secondary_model` is `restart_required: false`.
#[test]
fn pr14_restart_required_split() {
    let reg = SettingsRegistry::defaults();
    let fork = reg.find("fork_secondary_model").unwrap();
    assert!(
        !fork.restart_required,
        "fork_secondary_model must be restart_required: false — the shell's \
         config_reloader rebroadcasts the new default on the next fork"
    );
}

/// Model settings use `DynamicEnum` with `ActiveModelCatalog`.
#[test]
fn pr14_string_settings_use_known_model_validator() {
    use kigi_tui::settings::DynamicEnumSource;
    let reg = SettingsRegistry::defaults();
    for key in ["default_model", "fork_secondary_model"] {
        let meta = reg
            .find(key)
            .unwrap_or_else(|| panic!("`{key}` must be registered"));
        match &meta.kind {
            SettingKind::DynamicEnum { source, .. } => {
                assert_eq!(
                    *source,
                    DynamicEnumSource::ActiveModelCatalog,
                    "`{key}` must pull choices from the active model catalog \
                     so the picker matches `/model`'s UX"
                );
            }
            other => panic!("expected DynamicEnum kind for `{key}`, got {other:?}"),
        }
    }
}

/// Defaults round-trip through `current_value_for`.
#[test]
fn pr14_model_family_defaults_roundtrip_via_current_value_for() {
    use kigi_tui::settings::current_value_for;
    let ui = UiConfig::default();
    let pager = PagerLocalSnapshot::default();

    // Baseline value folds to empty (no-opinion sentinel).
    let value = current_value_for("fork_secondary_model", &ui, &pager).unwrap();
    assert_eq!(
        value,
        SettingValue::String(String::new()),
        "PR 14: `fork_secondary_model` defaults to empty string (no-opinion sentinel)",
    );
}

/// Non-baseline `fork_secondary_model` surfaces verbatim.
#[test]
fn pr14_fork_secondary_model_reads_ui_config_non_baseline() {
    use kigi_tui::settings::current_value_for;
    let ui = UiConfig {
        fork_secondary_model: "Custom Fork Model".to_string(),
        ..UiConfig::default()
    };
    let pager = PagerLocalSnapshot::default();
    assert_eq!(
        current_value_for("fork_secondary_model", &ui, &pager),
        Some(SettingValue::String("Custom Fork Model".to_string())),
        "non-baseline `fork_secondary_model` must surface verbatim — the \
         baseline-equality fold only kicks in for the default value",
    );
}

/// Model-family settings are discoverable via search.
#[test]
fn pr14_model_family_settings_are_discoverable_via_search() {
    let reg = SettingsRegistry::defaults();
    let cases = [("fork", "fork_secondary_model")];
    for (query, expected_key) in cases {
        let hits = reg.search(query);
        assert!(
            hits.iter().any(|m| m.key == expected_key),
            "search(`{query}`) must include `{expected_key}` — hit keys: {:?}",
            hits.iter().map(|m| m.key).collect::<Vec<_>>(),
        );
    }
}

// ---------------------------------------------------------------------------
// vim_mode (scrollback navigation) — PAGER-owned, paired with simple_mode
// ---------------------------------------------------------------------------

/// Keyboard Space on the vim_mode row dispatches the typed setter
/// with the inverted snapshot value (default false → true). Same
/// shape as the `multiline_mode` test above; both rows are
/// PAGER-owned Bool settings.
#[test]
fn vim_mode_space_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "vim_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "vim_mode", true);
}

#[test]
fn vim_mode_enter_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "vim_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "vim_mode", true);
}

#[test]
fn vim_mode_mouse_click_two_stage_toggles() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "vim_mode") as u16;

    // First click: select-only.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );
    assert_eq!(s.selected, row_y as usize);

    // Second click: toggle.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_set_bool_action(outcome, "vim_mode", true);
}

#[test]
fn vim_mode_snapshot_on_dispatches_off() {
    let snapshot = PagerLocalSnapshot {
        vim_mode: true,
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    navigate_to(&mut s, "vim_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "vim_mode", false);
}

#[test]
fn vim_mode_renders_under_appearance_category_pager_owned() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("vim_mode").expect("vim_mode must be registered");
    assert_eq!(
        meta.category,
        SettingCategory::Appearance,
        "vim_mode lives under Appearance (paired with simple_mode)"
    );
    assert_eq!(meta.owner, SettingOwner::Shell, "vim_mode is SHELL-owned");
}

#[test]
fn simple_mode_label_distinguishes_input_from_scrollback() {
    // The pair (`simple_mode`, `vim_mode`) controls vim behaviour in
    // two different surfaces. The label rename in this rebase makes
    // that explicit so neither row is ambiguous when both are shown.
    let reg = SettingsRegistry::defaults();
    let simple = reg.find("simple_mode").expect("simple_mode registered");
    let vim = reg.find("vim_mode").expect("vim_mode registered");
    assert_eq!(simple.label, "Disable vim input mode");
    assert_eq!(vim.label, "Vim scrollback navigation");
    // Keyword sanity-check so search('vim') still finds both.
    assert!(simple.keywords.contains(&"vim"));
    assert!(vim.keywords.contains(&"vim"));
}

// ---------------------------------------------------------------------------
// keep_text_selection — SHELL-owned Mouse Enum (`flash` | `hold`)
//
// Mirrors `render_mermaid`: `supports_preview: false`, Enter opens picker,
// commit dispatches `Action::SetKeepTextSelection(TextSelection)`.
// ---------------------------------------------------------------------------

#[test]
fn keep_text_selection_renders_under_mouse_shell_owned() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("keep_text_selection")
        .expect("keep_text_selection must be registered");
    assert_eq!(meta.category, SettingCategory::Mouse);
    assert_eq!(meta.owner, SettingOwner::Shell);
    assert_eq!(meta.label, "Text selection");
    assert!(
        meta.description.contains("Shift"),
        "description should mention Shift-drag for native terminal copy"
    );
}

#[test]
fn keep_text_selection_does_not_support_preview() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("keep_text_selection")
        .expect("keep_text_selection must be registered");
    match &meta.kind {
        SettingKind::Enum {
            supports_preview, ..
        } => assert!(
            !supports_preview,
            "keep_text_selection MUST be supports_preview: false",
        ),
        other => panic!("expected Enum kind for keep_text_selection, got {other:?}"),
    }
}

#[test]
fn enter_on_keep_text_selection_row_enters_picking_enum() {
    // The picker's `original_value` is read from the process-wide cache; pin it
    // to the default so a sibling test's `set_keep_text_selection` can't leak in.
    kigi_tui::appearance::cache::set_keep_text_selection(
        kigi_tui::appearance::TextSelection::Flash,
    );
    let mut s = make_state();
    navigate_to(&mut s, "keep_text_selection");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on keep_text_selection row must transition to PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            original_value,
            ..
        } => {
            assert_eq!(*key, "keep_text_selection");
            assert_eq!(
                original_value,
                &SettingValue::Enum("flash"),
                "default keep_text_selection → original 'flash'"
            );
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
}

#[test]
fn keep_text_selection_picker_nav_does_not_dispatch_preview() {
    for nav_key in &[
        KeyCode::Down,
        KeyCode::Up,
        KeyCode::Char('j'),
        KeyCode::Char('k'),
    ] {
        let mut s = make_state();
        navigate_to(&mut s, "keep_text_selection");
        let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
        // Retreat keys at choices_idx=0 are clamped → Unchanged; step down first.
        if matches!(nav_key, KeyCode::Up | KeyCode::Char('k')) {
            let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
        }
        let outcome = handle_settings_key(&mut s, &press(*nav_key));
        assert!(
            matches!(outcome, SettingsKeyOutcome::Changed),
            "Nav key {nav_key:?} in keep_text_selection picker MUST NOT dispatch a preview \
             Action (supports_preview: false), got {outcome:?}"
        );
    }
}

#[test]
fn keep_text_selection_picker_enter_dispatches_set_commit() {
    use kigi_tui::appearance::TextSelection;

    // Pin the cache-backed live value so the picker seeds at flash (idx 0)
    // regardless of any sibling test that set hold/word_select on this thread.
    kigi_tui::appearance::cache::set_keep_text_selection(TextSelection::Flash);
    let mut s = make_state();
    navigate_to(&mut s, "keep_text_selection");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    // flash (idx 0); Down → hold (idx 1), Enter commits.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetKeepTextSelection(kind)) => {
            assert_eq!(kind, TextSelection::Hold);
        }
        other => panic!("expected SetKeepTextSelection(Hold), got {other:?}"),
    }
}

#[test]
fn keep_text_selection_picker_esc_does_not_dispatch_action() {
    let mut s = make_state();
    navigate_to(&mut s, "keep_text_selection");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Esc));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Esc with supports_preview: false must not dispatch Action, got {outcome:?}"
    );
}

#[test]
fn keep_text_selection_choices_use_canonical_strings() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("keep_text_selection").unwrap();
    let SettingKind::Enum { choices, .. } = &meta.kind else {
        panic!("keep_text_selection must be Enum");
    };
    let canonicals: Vec<_> = choices.iter().map(|c| c.canonical).collect();
    assert_eq!(
        canonicals,
        vec!["flash", "hold", "word_select"],
        "keep_text_selection catalog must be exactly [flash, hold, word_select] in order"
    );
}

#[test]
fn mouse_click_on_unselected_keep_text_selection_row_only_selects() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "keep_text_selection") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first body-click on unselected keep_text_selection row should only select, got: {outcome:?}",
    );
    assert_eq!(s.selected, row_y as usize);
}

#[test]
fn mouse_click_on_selected_keep_text_selection_row_opens_picker() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "keep_text_selection") as u16;
    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on focused keep_text_selection row must enter PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => assert_eq!(*key, "keep_text_selection"),
        _ => panic!("second click on focused keep_text_selection row must enter PickingEnum"),
    }
}

#[test]
fn mouse_click_on_keep_text_selection_indicator_opens_picker_in_one_click() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "keep_text_selection") as u16;
    // Value column is on the right; use a high x to hit the indicator.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        70,
        row_y,
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => assert_eq!(*key, "keep_text_selection"),
        _ => panic!("value click on keep_text_selection must enter PickingEnum, got {outcome:?}"),
    }
}

#[test]
fn keep_text_selection_hold_snapshot_seeds_picker_at_hold() {
    use kigi_tui::appearance::TextSelection;
    // Live value is the process-wide cache (like render_mermaid), not UiConfig alone.
    kigi_tui::appearance::cache::set_keep_text_selection(TextSelection::Hold);
    let ui = UiConfig {
        keep_text_selection: Some("hold".into()),
        ..UiConfig::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        ui,
        PagerLocalSnapshot::default(),
    );
    navigate_to(&mut s, "keep_text_selection");
    let _ = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match &s.mode {
        SettingsModalMode::PickingEnum {
            original_value,
            choices_idx,
            ..
        } => {
            assert_eq!(original_value, &SettingValue::Enum("hold"));
            assert_eq!(*choices_idx, 1, "hold is the second choice");
        }
        other => panic!("expected PickingEnum, got {other:?}"),
    }
}

// ---------------------------------------------------------------------------
// scroll_speed — SHELL-owned Int under Mouse, no preview
// ---------------------------------------------------------------------------

/// Int stepper open/step/commit for scroll_speed. Defaults to 50;
/// mid-range policy: Up/Down ±1, Left/Right ±5.
#[test]
fn scroll_speed_int_stepper_commit_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "scroll_speed");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on Int row must transition to EditingValue, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => {
            assert_eq!(buffer, "50", "buffer must seed from default 50");
        }
        other => panic!("expected EditingValue mode, got {other:?}"),
    }

    // Up = +1 → 51. Right = +5 → 56.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Up));
    let _ = handle_settings_key(&mut s, &press(KeyCode::Right));
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => assert_eq!(buffer, "56"),
        other => panic!("expected EditingValue, got {other:?}"),
    }

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetScrollSpeed(56)) => {}
        other => panic!("expected SetScrollSpeed(56), got {other:?}"),
    }
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

#[test]
fn scroll_speed_mouse_click_opens_editor() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "scroll_speed") as u16;

    // First click — select.
    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    // Second click on the focused row opens the editor.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on focused Int row must enter the editor, got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::EditingValue { key, .. } if key == "scroll_speed"),
        "mode must be EditingValue(scroll_speed) after Enter-equivalent click, got {:?}",
        s.mode,
    );
}

#[test]
fn scroll_speed_renders_under_mouse_shell_owned_bounds_1_to_100() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("scroll_speed")
        .expect("scroll_speed must be registered");
    assert_eq!(meta.category, SettingCategory::Mouse);
    assert_eq!(meta.owner, SettingOwner::Shell);
    match &meta.kind {
        SettingKind::Int { default, min, max } => {
            assert_eq!(*default, 50);
            assert_eq!(*min, 1);
            assert_eq!(*max, 100);
        }
        other => panic!("expected Int kind for scroll_speed, got {other:?}"),
    }
}

// ---------------------------------------------------------------------------
// scroll_mode — SHELL-owned Mouse Enum (`auto` | `wheel` | `trackpad`),
// no preview (mirrors keep_text_selection).
// ---------------------------------------------------------------------------

#[test]
fn scroll_mode_renders_under_mouse_shell_owned_no_preview() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("scroll_mode").expect("scroll_mode registered");
    assert_eq!(meta.category, SettingCategory::Mouse);
    assert_eq!(meta.owner, SettingOwner::Shell);
    match &meta.kind {
        SettingKind::Enum {
            default,
            choices,
            supports_preview,
        } => {
            assert_eq!(*default, "auto");
            assert!(!supports_preview, "scroll_mode must not preview");
            let canonicals: Vec<_> = choices.iter().map(|c| c.canonical).collect();
            assert_eq!(
                canonicals,
                vec!["auto", "wheel", "trackpad"],
                "catalog must match ScrollMode::as_canonical order"
            );
        }
        other => panic!("expected Enum kind for scroll_mode, got {other:?}"),
    }
}

#[test]
fn scroll_mode_picker_enter_dispatches_set_commit() {
    use kigi_tui::appearance::ScrollMode;

    // Pin the cache-backed live value so the picker seeds at auto (idx 0)
    // regardless of sibling tests on this thread.
    kigi_tui::appearance::cache::set_scroll_mode(ScrollMode::Auto);
    let mut s = make_state();
    navigate_to(&mut s, "scroll_mode");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on scroll_mode row must transition to PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum {
            key,
            original_value,
            ..
        } => {
            assert_eq!(*key, "scroll_mode");
            assert_eq!(original_value, &SettingValue::Enum("auto"));
        }
        other => panic!("expected PickingEnum mode, got {other:?}"),
    }
    // auto (idx 0); Down → wheel (idx 1), Enter commits the typed setter.
    let _ = handle_settings_key(&mut s, &press(KeyCode::Down));
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetScrollMode(mode)) => {
            assert_eq!(mode, ScrollMode::Wheel);
        }
        other => panic!("expected SetScrollMode(Wheel), got {other:?}"),
    }
}

#[test]
fn mouse_click_on_selected_scroll_mode_row_opens_picker() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "scroll_mode") as u16;
    // First click selects, second opens the picker.
    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on focused scroll_mode row must enter PickingEnum, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::PickingEnum { key, .. } => assert_eq!(*key, "scroll_mode"),
        _ => panic!("second click on focused scroll_mode row must enter PickingEnum"),
    }
}

// ---------------------------------------------------------------------------
// scroll_lines — SHELL-owned Int under Mouse (1-10), no preview
// ---------------------------------------------------------------------------

#[test]
fn scroll_lines_renders_under_mouse_shell_owned_bounds_1_to_10() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("scroll_lines").expect("scroll_lines registered");
    assert_eq!(meta.category, SettingCategory::Mouse);
    assert_eq!(meta.owner, SettingOwner::Shell);
    match &meta.kind {
        SettingKind::Int { default, min, max } => {
            assert_eq!(*default, 3);
            assert_eq!(*min, 1);
            assert_eq!(*max, 10);
        }
        other => panic!("expected Int kind for scroll_lines, got {other:?}"),
    }
}

/// Int stepper open/step/commit for scroll_lines (the scroll_speed pattern).
#[test]
fn scroll_lines_int_stepper_commit_dispatches_typed_setter() {
    // Pin the live cache so the buffer seeds at the default 3.
    kigi_tui::appearance::cache::set_scroll_lines(3);
    let mut s = make_state();
    navigate_to(&mut s, "scroll_lines");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "Enter on Int row must transition to EditingValue, got {outcome:?}"
    );
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => {
            assert_eq!(buffer, "3", "buffer must seed from default 3");
        }
        other => panic!("expected EditingValue mode, got {other:?}"),
    }

    // Narrow-range policy: Up = +1 → 4 (unit steps so every 1..=10 is reachable).
    let _ = handle_settings_key(&mut s, &press(KeyCode::Up));
    match &s.mode {
        SettingsModalMode::EditingValue { buffer, .. } => assert_eq!(buffer, "4"),
        other => panic!("expected EditingValue, got {other:?}"),
    }

    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    match outcome {
        SettingsKeyOutcome::Action(Action::SetScrollLines(4)) => {}
        other => panic!("expected SetScrollLines(4), got {other:?}"),
    }
    assert!(matches!(s.mode, SettingsModalMode::Browse));
}

#[test]
fn scroll_lines_mouse_click_opens_editor() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "scroll_lines") as u16;
    let _ = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "second click on focused Int row must enter the editor, got {outcome:?}"
    );
    assert!(
        matches!(s.mode, SettingsModalMode::EditingValue { key, .. } if key == "scroll_lines"),
        "mode must be EditingValue(scroll_lines), got {:?}",
        s.mode,
    );
}

// ---------------------------------------------------------------------------
// invert_scroll — SHELL-owned Bool (Mouse, default false)
// ---------------------------------------------------------------------------

#[test]
fn invert_scroll_space_dispatches_typed_setter() {
    kigi_tui::appearance::cache::set_invert_scroll(false);
    let mut s = make_state();
    navigate_to(&mut s, "invert_scroll");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "invert_scroll", true);
    kigi_tui::appearance::cache::set_invert_scroll(false);
}

#[test]
fn invert_scroll_enter_dispatches_typed_setter() {
    kigi_tui::appearance::cache::set_invert_scroll(true);
    let mut s = make_state();
    navigate_to(&mut s, "invert_scroll");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "invert_scroll", false);
    kigi_tui::appearance::cache::set_invert_scroll(false);
}

#[test]
fn invert_scroll_mouse_click_two_stage_toggles() {
    kigi_tui::appearance::cache::set_invert_scroll(false);
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "invert_scroll") as u16;
    // First click — select only.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first body-click should only select, got {outcome:?}"
    );
    // Second click — toggles.
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_set_bool_action(outcome, "invert_scroll", true);
    kigi_tui::appearance::cache::set_invert_scroll(false);
}

#[test]
fn invert_scroll_renders_under_mouse_shell_owned_default_false() {
    let reg = SettingsRegistry::defaults();
    let meta = reg.find("invert_scroll").expect("invert_scroll registered");
    assert_eq!(meta.category, SettingCategory::Mouse);
    assert_eq!(meta.owner, SettingOwner::Shell);
    match &meta.kind {
        SettingKind::Bool { default } => assert!(!default, "invert_scroll must default OFF"),
        other => panic!("expected Bool kind for invert_scroll, got {other:?}"),
    }
}

// ---------------------------------------------------------------------------
// display_refresh_auto_cadence — SHELL-owned Bool (Appearance, default false)
// ---------------------------------------------------------------------------

#[test]
fn display_refresh_auto_cadence_space_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "display_refresh_auto_cadence");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "display_refresh_auto_cadence", true);
}

#[test]
fn display_refresh_auto_cadence_enter_dispatches_typed_setter() {
    // Seed on so Enter toggles off.
    let mut ui = UiConfig::default();
    ui.display_refresh.auto_cadence_enabled = Some(true);
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        ui,
        PagerLocalSnapshot {
            auto_mode_gate: true,
            ..PagerLocalSnapshot::default()
        },
    );
    navigate_to(&mut s, "display_refresh_auto_cadence");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "display_refresh_auto_cadence", false);
}

#[test]
fn display_refresh_auto_cadence_mouse_click_two_stage_toggles() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "display_refresh_auto_cadence") as u16;
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first body-click should only select, got {outcome:?}"
    );
    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_set_bool_action(outcome, "display_refresh_auto_cadence", true);
}

#[test]
fn display_refresh_auto_cadence_meta_appearance_shell_restart_hidden_minimal() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("display_refresh_auto_cadence")
        .expect("display_refresh_auto_cadence registered");
    assert_eq!(meta.category, SettingCategory::Appearance);
    assert_eq!(meta.owner, SettingOwner::Shell);
    assert!(meta.restart_required);
    assert!(meta.hidden_in_minimal);
    assert_eq!(meta.label, "Match display refresh rate");
    match &meta.kind {
        SettingKind::Bool { default } => {
            assert!(!default, "display_refresh_auto_cadence must default OFF")
        }
        other => panic!("expected Bool kind for display_refresh_auto_cadence, got {other:?}"),
    }
}

#[test]
fn display_refresh_auto_cadence_defaults_roundtrip_via_current_value_for() {
    use kigi_tui::settings::current_value_for;
    let ui = UiConfig::default();
    let pager = PagerLocalSnapshot::default();
    let value = current_value_for("display_refresh_auto_cadence", &ui, &pager)
        .expect("current_value_for(display_refresh_auto_cadence) must resolve");
    assert_eq!(value, SettingValue::Bool(false));

    let mut ui_on = UiConfig::default();
    ui_on.display_refresh.auto_cadence_enabled = Some(true);
    let value = current_value_for("display_refresh_auto_cadence", &ui_on, &pager)
        .expect("current_value_for(display_refresh_auto_cadence) must resolve");
    assert_eq!(value, SettingValue::Bool(true));
}

// ---------------------------------------------------------------------------
// show_thinking_blocks — SHELL-owned Bool (Appearance, default true)
// ---------------------------------------------------------------------------

#[test]
fn show_thinking_blocks_space_dispatches_typed_setter() {
    // Pin off so space toggles to true.
    kigi_tui::appearance::cache::set_show_thinking_blocks(false);
    let mut s = make_state();
    navigate_to(&mut s, "show_thinking_blocks");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "show_thinking_blocks", true);
    kigi_tui::appearance::cache::set_show_thinking_blocks(true);
}

#[test]
fn show_thinking_blocks_enter_dispatches_typed_setter() {
    kigi_tui::appearance::cache::set_show_thinking_blocks(false);
    let mut s = make_state();
    navigate_to(&mut s, "show_thinking_blocks");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "show_thinking_blocks", true);
    kigi_tui::appearance::cache::set_show_thinking_blocks(true);
}

#[test]
fn show_thinking_blocks_mouse_click_two_stage_toggles() {
    kigi_tui::appearance::cache::set_show_thinking_blocks(false);
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "show_thinking_blocks") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );
    assert_eq!(s.selected, row_y as usize);

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    // Cache pinned off above → toggle dispatches true.
    assert_set_bool_action(outcome, "show_thinking_blocks", true);
    kigi_tui::appearance::cache::set_show_thinking_blocks(true);
}

#[test]
fn show_thinking_blocks_cache_on_dispatches_off() {
    // When the live cache is on, toggle should turn it off.
    kigi_tui::appearance::cache::set_show_thinking_blocks(true);
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        PagerLocalSnapshot::default(),
    );
    navigate_to(&mut s, "show_thinking_blocks");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "show_thinking_blocks", false);
    // Restore client default (on) for other tests that share the process cache.
    kigi_tui::appearance::cache::set_show_thinking_blocks(true);
}

#[test]
fn show_thinking_blocks_renders_under_appearance_category_shell_owned() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("show_thinking_blocks")
        .expect("show_thinking_blocks must be registered");
    assert_eq!(meta.category, SettingCategory::Appearance);
    assert_eq!(meta.owner, SettingOwner::Shell);
    match &meta.kind {
        SettingKind::Bool { default } => assert!(*default, "default must be true"),
        other => panic!("expected Bool kind for show_thinking_blocks, got {other:?}"),
    }
    // Must sit immediately above respect_manual_folds in the registry order.
    let keys: Vec<&str> = reg
        .all()
        .iter()
        .filter(|m| m.category == SettingCategory::Appearance)
        .map(|m| m.key)
        .collect();
    let show_idx = keys
        .iter()
        .position(|k| *k == "show_thinking_blocks")
        .expect("show_thinking_blocks in Appearance");
    let respect_idx = keys
        .iter()
        .position(|k| *k == "respect_manual_folds")
        .expect("respect_manual_folds in Appearance");
    assert_eq!(
        show_idx + 1,
        respect_idx,
        "show_thinking_blocks must be immediately above respect_manual_folds; \
         Appearance order: {keys:?}"
    );
}

// ---------------------------------------------------------------------------
// prompt_suggestions — SHELL-owned Bool (Editor, default true)
// ---------------------------------------------------------------------------

#[test]
fn prompt_suggestions_space_dispatches_typed_setter() {
    // Pin off so space toggles to true.
    kigi_tui::appearance::cache::set_prompt_suggestions(false);
    let mut s = make_state();
    navigate_to(&mut s, "prompt_suggestions");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "prompt_suggestions", true);
    kigi_tui::appearance::cache::set_prompt_suggestions(true);
}

#[test]
fn prompt_suggestions_enter_dispatches_typed_setter() {
    kigi_tui::appearance::cache::set_prompt_suggestions(false);
    let mut s = make_state();
    navigate_to(&mut s, "prompt_suggestions");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "prompt_suggestions", true);
    kigi_tui::appearance::cache::set_prompt_suggestions(true);
}

#[test]
fn prompt_suggestions_mouse_click_two_stage_toggles() {
    kigi_tui::appearance::cache::set_prompt_suggestions(false);
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "prompt_suggestions") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );
    assert_eq!(s.selected, row_y as usize);

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    // Cache pinned off above → toggle dispatches true.
    assert_set_bool_action(outcome, "prompt_suggestions", true);
    kigi_tui::appearance::cache::set_prompt_suggestions(true);
}

#[test]
fn prompt_suggestions_cache_on_dispatches_off() {
    // Default is on; when the live cache is on, toggle should turn it off.
    kigi_tui::appearance::cache::set_prompt_suggestions(true);
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        PagerLocalSnapshot::default(),
    );
    navigate_to(&mut s, "prompt_suggestions");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "prompt_suggestions", false);
    // Restore client default (on) for other tests that share the process cache.
    kigi_tui::appearance::cache::set_prompt_suggestions(true);
}

#[test]
fn prompt_suggestions_renders_under_editor_category_shell_owned() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("prompt_suggestions")
        .expect("prompt_suggestions must be registered");
    assert_eq!(meta.category, SettingCategory::Editor);
    assert_eq!(meta.owner, SettingOwner::Shell);
    match &meta.kind {
        SettingKind::Bool { default } => assert!(*default, "default must be true"),
        other => panic!("expected Bool kind for prompt_suggestions, got {other:?}"),
    }
    // Must sit immediately below multiline_mode in the registry order.
    let keys: Vec<&str> = reg
        .all()
        .iter()
        .filter(|m| m.category == SettingCategory::Editor)
        .map(|m| m.key)
        .collect();
    let multiline_idx = keys
        .iter()
        .position(|k| *k == "multiline_mode")
        .expect("multiline_mode in Editor");
    let prompt_idx = keys
        .iter()
        .position(|k| *k == "prompt_suggestions")
        .expect("prompt_suggestions in Editor");
    assert_eq!(
        multiline_idx + 1,
        prompt_idx,
        "prompt_suggestions must be immediately below multiline_mode; \
         Editor order: {keys:?}"
    );
}

// ---------------------------------------------------------------------------
// respect_manual_folds — PAGER-owned Bool
// ---------------------------------------------------------------------------

#[test]
fn respect_manual_folds_space_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "respect_manual_folds");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "respect_manual_folds", true);
}

#[test]
fn respect_manual_folds_enter_dispatches_typed_setter() {
    let mut s = make_state();
    navigate_to(&mut s, "respect_manual_folds");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "respect_manual_folds", true);
}

#[test]
fn respect_manual_folds_mouse_click_two_stage_toggles() {
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "respect_manual_folds") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );
    assert_eq!(s.selected, row_y as usize);

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert_set_bool_action(outcome, "respect_manual_folds", true);
}

#[test]
fn respect_manual_folds_snapshot_on_dispatches_off() {
    let snapshot = PagerLocalSnapshot {
        respect_manual_folds: true,
        ..PagerLocalSnapshot::default()
    };
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        snapshot,
    );
    navigate_to(&mut s, "respect_manual_folds");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "respect_manual_folds", false);
}

#[test]
fn respect_manual_folds_renders_under_appearance_category_pager_owned() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("respect_manual_folds")
        .expect("respect_manual_folds must be registered");
    assert_eq!(meta.category, SettingCategory::Appearance);
    assert_eq!(meta.owner, SettingOwner::Pager);
    match &meta.kind {
        SettingKind::Bool { default } => assert!(!default),
        other => panic!("expected Bool kind for respect_manual_folds, got {other:?}"),
    }
}

// ---------------------------------------------------------------------------
// group_tool_verbs — SHELL-owned Bool (Appearance, default true)
// ---------------------------------------------------------------------------

#[test]
fn group_tool_verbs_space_dispatches_typed_setter() {
    // Default is true; space toggles to false.
    kigi_tui::appearance::cache::set_group_tool_verbs(true);
    let mut s = make_state();
    navigate_to(&mut s, "group_tool_verbs");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "group_tool_verbs", false);
}

#[test]
fn group_tool_verbs_enter_dispatches_typed_setter() {
    kigi_tui::appearance::cache::set_group_tool_verbs(true);
    let mut s = make_state();
    navigate_to(&mut s, "group_tool_verbs");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "group_tool_verbs", false);
}

#[test]
fn group_tool_verbs_mouse_click_two_stage_toggles() {
    kigi_tui::appearance::cache::set_group_tool_verbs(true);
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "group_tool_verbs") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );
    assert_eq!(s.selected, row_y as usize);

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    // Default is true → toggle dispatches false.
    assert_set_bool_action(outcome, "group_tool_verbs", false);
}

#[test]
fn group_tool_verbs_cache_off_dispatches_on() {
    // When the live cache is off, toggle should turn it on.
    kigi_tui::appearance::cache::set_group_tool_verbs(false);
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        PagerLocalSnapshot::default(),
    );
    navigate_to(&mut s, "group_tool_verbs");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "group_tool_verbs", true);
    // Restore default (on) for other tests that share the process cache.
    kigi_tui::appearance::cache::set_group_tool_verbs(true);
}

#[test]
fn group_tool_verbs_renders_under_appearance_category_shell_owned() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("group_tool_verbs")
        .expect("group_tool_verbs must be registered");
    assert_eq!(meta.category, SettingCategory::Appearance);
    assert_eq!(meta.owner, SettingOwner::Shell);
    match &meta.kind {
        SettingKind::Bool { default } => assert!(*default, "default must be true"),
        other => panic!("expected Bool kind for group_tool_verbs, got {other:?}"),
    }
    // Must sit immediately below respect_manual_folds in the registry order.
    let keys: Vec<&str> = reg
        .all()
        .iter()
        .filter(|m| m.category == SettingCategory::Appearance)
        .map(|m| m.key)
        .collect();
    let respect_idx = keys
        .iter()
        .position(|k| *k == "respect_manual_folds")
        .expect("respect_manual_folds in Appearance");
    let group_idx = keys
        .iter()
        .position(|k| *k == "group_tool_verbs")
        .expect("group_tool_verbs in Appearance");
    assert_eq!(
        respect_idx + 1,
        group_idx,
        "group_tool_verbs must be immediately below respect_manual_folds; \
         Appearance order: {keys:?}"
    );
}

// ---------------------------------------------------------------------------
// collapsed_edit_blocks — SHELL-owned Bool (Appearance, default false)
// ---------------------------------------------------------------------------

#[test]
fn collapsed_edit_blocks_space_dispatches_typed_setter() {
    // Seed the live cache to the shipped default (bypasses the disk seed so a
    // host [ui] override can't flip the expected toggle direction).
    kigi_tui::appearance::cache::set_collapsed_edit_blocks(false);
    let mut s = make_state();
    navigate_to(&mut s, "collapsed_edit_blocks");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "collapsed_edit_blocks", true);
}

#[test]
fn collapsed_edit_blocks_enter_dispatches_typed_setter() {
    kigi_tui::appearance::cache::set_collapsed_edit_blocks(false);
    let mut s = make_state();
    navigate_to(&mut s, "collapsed_edit_blocks");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Enter));
    assert_set_bool_action(outcome, "collapsed_edit_blocks", true);
}

#[test]
fn collapsed_edit_blocks_mouse_click_two_stage_toggles() {
    kigi_tui::appearance::cache::set_collapsed_edit_blocks(false);
    let mut s = make_state();
    synth_rects(&mut s);
    let row_y = row_idx_for(&s, "collapsed_edit_blocks") as u16;

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    assert!(
        matches!(outcome, SettingsKeyOutcome::Changed),
        "first click on a different row body should only select, got: {outcome:?}"
    );
    assert_eq!(s.selected, row_y as usize);

    let outcome = handle_settings_mouse(
        &mut s,
        MouseEventKind::Down(crossterm::event::MouseButton::Left),
        10,
        row_y,
    );
    // Default is false → toggle dispatches true.
    assert_set_bool_action(outcome, "collapsed_edit_blocks", true);
}

#[test]
fn collapsed_edit_blocks_cache_on_dispatches_off() {
    // When the live cache is on (remote settings/team enable), toggle turns it off.
    kigi_tui::appearance::cache::set_collapsed_edit_blocks(true);
    let mut s = SettingsModalState::new(
        Arc::new(SettingsRegistry::defaults()),
        UiConfig::default(),
        PagerLocalSnapshot::default(),
    );
    navigate_to(&mut s, "collapsed_edit_blocks");
    let outcome = handle_settings_key(&mut s, &press(KeyCode::Char(' ')));
    assert_set_bool_action(outcome, "collapsed_edit_blocks", false);
    // Restore default (off) for other tests that share the process cache.
    kigi_tui::appearance::cache::set_collapsed_edit_blocks(false);
}

#[test]
fn collapsed_edit_blocks_renders_under_appearance_category_shell_owned() {
    let reg = SettingsRegistry::defaults();
    let meta = reg
        .find("collapsed_edit_blocks")
        .expect("collapsed_edit_blocks must be registered");
    assert_eq!(meta.category, SettingCategory::Appearance);
    assert_eq!(meta.owner, SettingOwner::Shell);
    match &meta.kind {
        SettingKind::Bool { default } => {
            assert!(!*default, "default must be false (rollout flag ships OFF)")
        }
        other => panic!("expected Bool kind for collapsed_edit_blocks, got {other:?}"),
    }
    // Must sit immediately below group_tool_verbs in the registry order.
    let keys: Vec<&str> = reg
        .all()
        .iter()
        .filter(|m| m.category == SettingCategory::Appearance)
        .map(|m| m.key)
        .collect();
    let group_idx = keys
        .iter()
        .position(|k| *k == "group_tool_verbs")
        .expect("group_tool_verbs in Appearance");
    let collapsed_idx = keys
        .iter()
        .position(|k| *k == "collapsed_edit_blocks")
        .expect("collapsed_edit_blocks in Appearance");
    assert_eq!(
        group_idx + 1,
        collapsed_idx,
        "collapsed_edit_blocks must be immediately below group_tool_verbs; \
         Appearance order: {keys:?}"
    );
}
