//! Settings registry — pure-metadata data model.
//!
//! See the module-level docs in `mod.rs` for the architectural rationale.

use agent_client_protocol as acp;
use kigi_shell::agent::config::UiConfig;
use kigi_tools::implementations::kigi::ask_user_question;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// Stable identity for a setting. The string id matches the `UiConfig`
/// serde field name (for SHELL/SHARED settings) and is the canonical key
/// referenced by tests, telemetry, and registry lookups.
///
/// We deliberately do NOT use a `SettingId` enum: enum renames would
/// ripple through call sites, while `&'static str` ties the registry's
/// vocabulary directly to the shell schema.
pub type SettingKey = &'static str;

/// Ownership class for a setting — the pager vs. shell ownership taxonomy.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingOwner {
    /// In-memory pager state; no disk write (e.g. `multiline_mode`).
    Pager,
    /// Shell schema, shell-mediated write, no pager-side cache.
    Shell,
    /// Shell schema + pager-side thread-local cache for render hot path.
    Shared,
}

/// Categorization mirroring `shortcuts_help::CATEGORY_ORDER` —
/// the settings modal renders one section per category in `ALL` order.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingCategory {
    Appearance,
    Mouse,
    Editor,
    Agent,
    Models,
    Session,
    Advanced,
}

impl SettingCategory {
    /// Render order — Appearance first (most-touched), then Mouse, then the rest.
    pub const ALL: &'static [Self] = &[
        Self::Appearance,
        Self::Mouse,
        Self::Editor,
        Self::Agent,
        Self::Models,
        Self::Session,
        Self::Advanced,
    ];

    /// Section-header label as rendered in the modal.
    pub fn label(&self) -> &'static str {
        match self {
            Self::Appearance => "Appearance",
            Self::Mouse => "Mouse",
            Self::Editor => "Editor & Input",
            Self::Agent => "Agent & Approval",
            Self::Models => "Models",
            Self::Session => "Session",
            Self::Advanced => "Advanced",
        }
    }
}

/// One choice in an `Enum` setting.
#[derive(Debug, Clone, Copy)]
pub struct EnumChoice {
    /// Canonical persisted value (e.g. `"kiginight"`).
    pub canonical: &'static str,
    /// Display label shown in the chooser (e.g. `"Kigi Night"`).
    pub display: &'static str,
    /// Sub-text shown in the chooser sheet (e.g. `"Dark + magenta accent"`).
    pub description: &'static str,
}

/// Runtime-built enum choice for `SettingKind::DynamicEnum` settings
/// whose choices come from a `PagerLocalSnapshot` at picker-open time.
/// Owned `String` fields because values are sourced from runtime catalogs.
#[derive(Debug, Clone)]
pub struct OwnedEnumChoice {
    pub canonical: String,
    pub display: String,
    pub description: String,
}

/// Source of runtime choices for a `SettingKind::DynamicEnum`.
/// `#[non_exhaustive]` allows adding new sources without breaking matches.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DynamicEnumSource {
    /// Models from the active session's catalog. Prepends a
    /// `"(no override)"` sentinel so the user can clear the setting.
    ActiveModelCatalog,
}

/// Build the owned choice list for a `DynamicEnum` at picker-open time.
/// `ActiveModelCatalog` prepends an empty-canonical "(no override)"
/// choice at index 0 for clearing the setting.
pub fn dynamic_enum_choices(
    source: DynamicEnumSource,
    snapshot: &PagerLocalSnapshot,
) -> Vec<OwnedEnumChoice> {
    match source {
        DynamicEnumSource::ActiveModelCatalog => {
            let mut out = Vec::with_capacity(snapshot.available_models.len() + 1);
            out.push(OwnedEnumChoice {
                canonical: String::new(),
                display: "(no override)".to_string(),
                description: "Inherit the default model (no per-user override).".to_string(),
            });
            for (name, _id) in &snapshot.available_models {
                out.push(OwnedEnumChoice {
                    canonical: name.clone(),
                    display: name.clone(),
                    description: String::new(),
                });
            }
            out
        }
    }
}

/// String validator applied at write time.
///
/// **SECURITY:** The editor's char filter rejects both Cc and Cf
/// Unicode categories to prevent Trojan-Source visual spoofing.
/// New input surfaces (e.g. paste) must re-apply this filter.
#[derive(Debug, Clone, Copy)]
pub enum StringValidator {
    /// Non-empty, no whitespace. Used for model ids.
    NonEmptyToken,
    /// Validated against the live model catalog at commit time.
    /// Empty input is accepted as a "clear-default" sentinel.
    KnownModel,
    /// No constraint (any UTF-8 accepted).
    Any,
}

/// Value-kind metadata, including per-kind defaults.
/// `#[non_exhaustive]` allows adding new kinds without breaking matches.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SettingKind {
    Bool {
        default: bool,
    },
    /// Free-form (or validated) string.
    String {
        default: &'static str,
        validator: StringValidator,
    },
    /// Stringly-typed choice from a static catalog.
    Enum {
        default: &'static str,
        choices: &'static [EnumChoice],
        /// When `true`, navigating the chooser previews changes live;
        /// Esc reverts to the original value.
        supports_preview: bool,
    },
    /// Bounded integer. Modal stepper step sizes are derived from
    /// `(max - min)` (see `int_step_sizes` in the settings modal).
    Int {
        default: i64,
        min: i64,
        max: i64,
    },
    /// Choice from a runtime-resolved catalog (e.g. available models).
    /// Persists as `SettingValue::String(canonical)`.
    DynamicEnum {
        default: &'static str,
        source: DynamicEnumSource,
        supports_preview: bool,
    },
    /// A navigational row that opens a sub-sheet of `children` (other
    /// registered settings, by key). Carries no scalar value of its own:
    /// `current_value_for`/`default_value_for` skip it and the modal renders
    /// it as a chevron row whose Enter opens the sub-sheet. Children are
    /// hidden from the top-level list (rendered only inside the sub-sheet).
    Group {
        children: &'static [SettingKey],
    },
}

/// One row in the registry. Pure metadata — no function pointers, no
/// closures, no heap allocations beyond the static `keywords` slice.
#[derive(Debug, Clone)]
pub struct SettingMeta {
    /// Stable id; also the `UiConfig` serde field name and TOML key for
    /// SHELL/SHARED settings.
    pub key: SettingKey,
    pub category: SettingCategory,
    pub owner: SettingOwner,
    pub label: &'static str,
    pub description: &'static str,
    /// Free-form keywords for the search/filter. All lowercase,
    /// no empty strings — `keywords_lowercase_and_non_empty` enforces.
    pub keywords: &'static [&'static str],
    pub kind: SettingKind,
    /// When `true`, the value takes effect only on next session start.
    /// Renders a "restart" pill on the row while it is expanded.
    pub restart_required: bool,
    /// When `true`, the row is hidden in minimal mode (the setting still
    /// exists and applies to the full TUI).
    pub hidden_in_minimal: bool,
}

/// A typed value carried by `Action::Set*` payloads, modal preview state,
/// and the rollback path on persist failure.
///
/// Each variant aligns 1:1 with a `SettingKind` variant.
#[derive(Debug, Clone, PartialEq)]
pub enum SettingValue {
    Bool(bool),
    String(String),
    Enum(&'static str),
    Int(i64),
}

/// Snapshot of pager-local state captured when the modal opens.
/// Used by `current_value_for` to render against LIVE state rather
/// than the on-disk `UiConfig`. Refreshed by
/// `refresh_open_settings_modals` after every mutation.
#[derive(Debug, Clone)]
pub struct PagerLocalSnapshot {
    /// Whether multiline input mode is active.
    pub multiline_mode: bool,
    /// Whether YOLO mode (always-approve) is active on the active agent.
    pub yolo_mode: bool,
    /// Whether Auto (LLM classifier) mode is active on the active agent.
    /// Mutually exclusive with `yolo_mode` in practice (yolo wins); read by
    /// `/auto` so it can toggle off when already on.
    pub auto_mode: bool,
    /// Currently-selected model's display name, or `None` if no catalog
    /// has loaded yet.
    pub current_model_name: Option<String>,
    /// `(display_name, ModelId)` pairs from the active session's catalog.
    /// Cloned into the snapshot so the modal's validator/resolver is
    /// self-contained (the modal outlives the borrow on `app.agents`).
    pub available_models: Vec<(String, acp::ModelId)>,
    /// Whether plan mode is active. Uses effective state
    /// (`pending.unwrap_or(active)`) so rapid toggles don't double-send.
    /// Refreshed on all mutation paths including ACP `CurrentModeUpdate`.
    pub plan_mode_active: bool,
    /// `[cli].show_tips` mirror. `None` = no TOML override → default `true`.
    pub show_tips: Option<bool>,
    /// `[cli].auto_update` mirror. `None` = no TOML override → default `true`.
    pub auto_update: Option<bool>,
    /// Process-wide vim-mode scrollback flag. Mirrors
    /// `appearance::cache::load_vim_mode()` at snapshot time.
    pub vim_mode: bool,
    /// Process-wide mouse-wheel scroll speed (1-100). Mirrors
    /// `appearance::cache::load_scroll_speed()` at snapshot time.
    pub scroll_speed: u8,
    /// Mirrors `AppView::appearance.scrollback.scroll.respect_manual_folds`
    /// at snapshot time.
    pub respect_manual_folds: bool,
    /// Mirrors `AppView::auto_mode_gate` at snapshot time. When false the
    /// permission-mode picker hides the "Auto" choice (matches the Shift+Tab
    /// cycle, which skips Auto when the feature gate is off).
    pub auto_mode_gate: bool,
    /// `[toolset.ask_user_question].timeout_enabled` mirror (effective TOML
    /// merge, like `show_tips`). `None` = unset in TOML → default `true`.
    pub ask_user_question_timeout_enabled: Option<bool>,
}

impl Default for PagerLocalSnapshot {
    fn default() -> Self {
        Self {
            multiline_mode: false,
            yolo_mode: false,
            auto_mode: false,
            current_model_name: None,
            available_models: Vec::new(),
            plan_mode_active: false,
            show_tips: None,
            auto_update: None,
            vim_mode: false,
            // Matches the registry default and
            // `appearance::cache::SCROLL_SPEED_DEFAULT`. Bare `u8::default()`
            // would be `0` (out-of-range) so we override.
            scroll_speed: 50,
            respect_manual_folds: crate::appearance::ScrollConfig::default().respect_manual_folds,
            auto_mode_gate: false,
            ask_user_question_timeout_enabled: None,
        }
    }
}

/// Canonicalize a raw hunk-tracker mode to a registry choice. Case-insensitive
/// and trimmed; `disabled` aliases `off`; unknown/blank/`None` → `agent_only`.
pub fn canonical_hunk_tracker_mode(value: Option<&str>) -> &'static str {
    let raw = value.unwrap_or_default().trim();
    if raw.eq_ignore_ascii_case("all_dirty") {
        "all_dirty"
    } else if raw.eq_ignore_ascii_case("off") || raw.eq_ignore_ascii_case("disabled") {
        "off"
    } else {
        "agent_only"
    }
}

/// `minimal` stays; everything else (including unset / legacy `default`) → `fullscreen`.
pub fn canonical_screen_mode(value: Option<&str>) -> &'static str {
    let raw = value.unwrap_or_default().trim();
    if raw.eq_ignore_ascii_case("minimal") {
        "minimal"
    } else {
        "fullscreen"
    }
}

impl PagerLocalSnapshot {
    /// Iterate over just the display names. Convenience helper for
    /// validator paths that don't need the ids.
    pub fn available_model_names(&self) -> impl Iterator<Item = &str> {
        self.available_models.iter().map(|(name, _)| name.as_str())
    }

    /// Resolve a user-supplied name to a `ModelId` via the snapshot.
    /// Case-insensitive ASCII match against display names only (ids
    /// aren't carried in the snapshot's primary key — callers needing
    /// id-based resolution should reach for `ModelState::resolve_by_name_or_id`).
    pub fn resolve_model_name(&self, query: &str) -> Option<acp::ModelId> {
        self.available_models.iter().find_map(|(name, id)| {
            if name.eq_ignore_ascii_case(query) {
                Some(id.clone())
            } else {
                None
            }
        })
    }
}

// ---------------------------------------------------------------------------
// Registry
// ---------------------------------------------------------------------------

/// Process-wide settings registry. Built in `main` and stored on
/// `AppView::settings_registry: Arc<SettingsRegistry>`.
#[derive(Debug, Clone)]
pub struct SettingsRegistry {
    entries: Vec<SettingMeta>,
}

impl SettingsRegistry {
    /// Build the default registry from `crate::settings::defs::default_settings()`.
    pub fn defaults() -> Self {
        let entries = crate::settings::defs::default_settings();
        assert_unique_keys(&entries);
        Self { entries }
    }

    /// Build a registry from a caller-supplied list of `SettingMeta`.
    /// Test seam — `#[doc(hidden)]` to discourage production use.
    /// Panics on duplicate keys.
    #[doc(hidden)]
    pub fn from_entries(entries: Vec<SettingMeta>) -> Self {
        assert_unique_keys(&entries);
        Self { entries }
    }

    /// All registered settings, in declaration order.
    pub fn all(&self) -> &[SettingMeta] {
        &self.entries
    }

    /// Look up a setting by key.
    pub fn find(&self, key: SettingKey) -> Option<&SettingMeta> {
        self.entries.iter().find(|m| m.key == key)
    }

    /// Iterate the settings in a given category, preserving declaration
    /// order.
    pub fn by_category(&self, cat: SettingCategory) -> impl Iterator<Item = &SettingMeta> {
        self.entries.iter().filter(move |m| m.category == cat)
    }

    /// Multi-word AND match against label, description, key, and keywords.
    pub fn search(&self, query: &str) -> Vec<&SettingMeta> {
        let q = query.to_lowercase();
        let words: Vec<&str> = q.split_whitespace().collect();
        if words.is_empty() {
            return self.entries.iter().collect();
        }
        self.entries
            .iter()
            .filter(|m| {
                let haystack = build_search_haystack(m);
                words.iter().all(|w| haystack.contains(w))
            })
            .collect()
    }
}

/// Panic if `entries` contains duplicate keys. Called from both
/// `defaults()` and `from_entries()` — downstream string-equality
/// gates rely on each key mapping to exactly one entry.
fn assert_unique_keys(entries: &[SettingMeta]) {
    use std::collections::HashSet;
    let mut seen: HashSet<&str> = HashSet::with_capacity(entries.len());
    let mut dupes: Vec<&str> = Vec::new();
    for m in entries {
        if !seen.insert(m.key) {
            dupes.push(m.key);
        }
    }
    assert!(
        dupes.is_empty(),
        "duplicate setting keys in registry: {dupes:?} — every \
         registered key must be globally unique so string-equality \
         gates in the view layer match at most one entry",
    );
}

fn build_search_haystack(m: &SettingMeta) -> String {
    let mut s = String::new();
    s.push_str(&m.label.to_lowercase());
    s.push(' ');
    s.push_str(&m.description.to_lowercase());
    s.push(' ');
    s.push_str(m.key);
    for kw in m.keywords {
        s.push(' ');
        s.push_str(kw);
    }
    s
}

// ---------------------------------------------------------------------------
// Snapshot reads — the one place that maps SettingKey → live field.
// ---------------------------------------------------------------------------

/// Read the current value of `key` from `UiConfig` (SHELL/SHARED) or
/// pager snapshot (PAGER-owned). Returns `None` for unknown keys.
/// Adding a new Bool setting requires arms here, in `action_for_bool`,
/// a `Action::SetX` variant, a shell helper, and an e2e test row.
pub fn current_value_for(
    key: SettingKey,
    ui: &UiConfig,
    pager: &PagerLocalSnapshot,
) -> Option<SettingValue> {
    match key {
        // SHARED — UiConfig source of truth, pager keeps a cache.
        "compact_mode" => Some(SettingValue::Bool(ui.compact_mode)),
        "show_timestamps" => Some(SettingValue::Bool(ui.show_timestamps.unwrap_or(true))),
        "show_timeline" => Some(SettingValue::Bool(ui.show_timeline_enabled())),
        "simple_mode" => Some(SettingValue::Bool(ui.simple_mode.unwrap_or(true))),
        // Per-tip contextual hints — `None` (inherit) reads as the default ON.
        "contextual_hints.undo" => {
            Some(SettingValue::Bool(ui.contextual_hints.undo.unwrap_or(true)))
        }
        "contextual_hints.plan_mode" => Some(SettingValue::Bool(
            ui.contextual_hints.plan_mode.unwrap_or(true),
        )),
        "contextual_hints.image_input" => Some(SettingValue::Bool(
            ui.contextual_hints.image_input.unwrap_or(true),
        )),
        "contextual_hints.send_now" => Some(SettingValue::Bool(
            ui.contextual_hints.send_now.unwrap_or(true),
        )),
        "contextual_hints.small_screen" => Some(SettingValue::Bool(
            ui.contextual_hints.small_screen.unwrap_or(true),
        )),
        "contextual_hints.word_select" => Some(SettingValue::Bool(
            ui.contextual_hints.word_select.unwrap_or(true),
        )),
        "keep_text_selection" => Some(SettingValue::Enum(
            crate::appearance::cache::load_keep_text_selection().as_canonical(),
        )),
        // PAGER — read from snapshot.
        "multiline_mode" => Some(SettingValue::Bool(pager.multiline_mode)),
        // PAGER — read from process-wide cache (snapshot mirror keeps
        // the modal in sync with the live cache value).
        "vim_mode" => Some(SettingValue::Bool(pager.vim_mode)),
        "scroll_speed" => Some(SettingValue::Int(pager.scroll_speed as i64)),
        // Live caches (like `group_tool_verbs`); scroll_lines shows the
        // registry default 3 while unset (profile-default state).
        "scroll_mode" => Some(SettingValue::Enum(
            crate::appearance::cache::load_scroll_mode().as_canonical(),
        )),
        "invert_scroll" => Some(SettingValue::Bool(
            crate::appearance::cache::load_invert_scroll(),
        )),
        // Nested `[ui.display_refresh].auto_cadence_enabled`; None → default false.
        "display_refresh_auto_cadence" => Some(SettingValue::Bool(
            ui.display_refresh.auto_cadence_enabled.unwrap_or(false),
        )),
        "scroll_lines" => Some(SettingValue::Int(
            crate::appearance::cache::load_scroll_lines()
                .map(i64::from)
                .unwrap_or(3),
        )),
        // Live cache (like `render_mermaid`).
        "show_thinking_blocks" => Some(SettingValue::Bool(
            crate::appearance::cache::load_show_thinking_blocks(),
        )),
        // Live cache (like `show_thinking_blocks`).
        "group_tool_verbs" => Some(SettingValue::Bool(
            crate::appearance::cache::load_group_tool_verbs(),
        )),
        // Live cache (like `group_tool_verbs`).
        "collapsed_edit_blocks" => Some(SettingValue::Bool(
            crate::appearance::cache::load_collapsed_edit_blocks(),
        )),
        // Live cache; `KIGI_PROMPT_SUGGESTIONS` env overrides at the gate.
        "prompt_suggestions" => Some(SettingValue::Bool(
            crate::appearance::cache::load_prompt_suggestions(),
        )),
        "respect_manual_folds" => Some(SettingValue::Bool(pager.respect_manual_folds)),
        // SHELL — canonicalized from `[ui].hunk_tracker_mode`.
        "hunk_tracker_mode" => Some(SettingValue::Enum(canonical_hunk_tracker_mode(
            ui.hunk_tracker_mode.as_deref(),
        ))),
        "screen_mode" => Some(SettingValue::Enum(canonical_screen_mode(
            ui.screen_mode.as_deref(),
        ))),
        // Theme: unknown disk values fall through to canonical default.
        // auto_dark/light additionally filter out "auto" (circular ref).
        "theme" => Some(SettingValue::Enum(
            ui.theme
                .as_deref()
                .and_then(crate::theme::canonical_name)
                .unwrap_or("kiginight"),
        )),
        "auto_dark_theme" => Some(SettingValue::Enum(
            ui.auto_dark_theme
                .as_deref()
                .and_then(crate::theme::canonical_name)
                .filter(|s| *s != "auto")
                .unwrap_or("kiginight"),
        )),
        "auto_light_theme" => Some(SettingValue::Enum(
            ui.auto_light_theme
                .as_deref()
                .and_then(crate::theme::canonical_name)
                .filter(|s| *s != "auto")
                .unwrap_or("kigiday"),
        )),
        // render_mermaid: SHELL-owned (persisted to `[ui].render_mermaid`).
        // Read from the process-wide cache mirror, which reflects the live value
        // the render path uses — the `vim_mode` snapshot field plays the same
        // role for its setting.
        "render_mermaid" => Some(SettingValue::Enum(
            crate::appearance::cache::load_render_mermaid().as_canonical(),
        )),
        // permission_mode: live snapshot wins over on-disk value.
        // yolo=true → "always-approve"; else honor ui ("auto" / "default" / "ask").
        "permission_mode" => Some(SettingValue::Enum(if pager.yolo_mode {
            "always-approve"
        } else if matches!(ui.permission_mode.as_deref(), Some("auto")) {
            "auto"
        } else if matches!(ui.permission_mode.as_deref(), Some("default")) {
            "default"
        } else {
            "ask"
        })),
        // remember_tool_approvals: reflects the user-config layer the modal
        // toggles (other layers feed the effective gate at spawn). None → false.
        "remember_tool_approvals" => Some(SettingValue::Bool(
            ui.remember_tool_approvals.unwrap_or(false),
        )),
        // ask_user_question timeout: reflects the effective TOML merge; the
        // toggle writes the user layer, and env/remote settings tiers feed the
        // final gate at agent build. None → the resolver-shared default (ON).
        "toolset.ask_user_question.timeout_enabled" => Some(SettingValue::Bool(
            pager
                .ask_user_question_timeout_enabled
                .unwrap_or(ask_user_question::DEFAULT_ASK_USER_QUESTION_TIMEOUT_ENABLED),
        )),
        // default_selected_permission: maps `[ui].default_selected_permission`
        // onto one of the four registry canonicals. `None` / unrecognised on
        // disk → `always_allow_all_sessions` (the effective default — the
        // cursor lands on the "Always allow on all sessions" row, picked
        // explicitly in `enqueue_permission`).
        "default_selected_permission" => Some(SettingValue::Enum(
            crate::appearance::permission_cursor::DefaultSelectedPermission::from_config_value(
                ui.default_selected_permission
                    .as_deref()
                    .unwrap_or_default(),
            )
            .as_canonical(),
        )),
        // default_model: reads from pager snapshot (not UiConfig).
        // None (no catalog yet) → empty string.
        "default_model" => Some(SettingValue::String(
            pager.current_model_name.clone().unwrap_or_default(),
        )),
        // max_thoughts_width: `u16` widened to `i64`.
        "max_thoughts_width" => Some(SettingValue::Int(ui.max_thoughts_width as i64)),
        // plan_mode: canonical via `PlanModeKind::from_bool().as_canonical()`.
        "plan_mode" => Some(SettingValue::Enum(
            crate::app::actions::PlanModeKind::from_bool(pager.plan_mode_active).as_canonical(),
        )),
        // CLI batch: snapshot mirrors; `None` → effective default `true`.
        "show_tips" => Some(SettingValue::Bool(pager.show_tips.unwrap_or(true))),
        "auto_update" => Some(SettingValue::Bool(pager.auto_update.unwrap_or(true))),
        // fork_secondary_model: baseline value folds to empty string.
        "fork_secondary_model" => Some(SettingValue::String({
            let baseline = kigi_shell::models::default_model();
            if ui.fork_secondary_model == baseline {
                String::new()
            } else {
                ui.fork_secondary_model.clone()
            }
        })),

        _ => None,
    }
}

/// Default value for `key`, derived from the registry metadata.
pub fn default_value_for(meta: &SettingMeta) -> SettingValue {
    match &meta.kind {
        SettingKind::Bool { default } => SettingValue::Bool(*default),
        SettingKind::String { default, .. } => SettingValue::String((*default).to_string()),
        SettingKind::Enum { default, .. } => SettingValue::Enum(default),
        SettingKind::Int { default, .. } => SettingValue::Int(*default),
        // `DynamicEnum` widens to `String` for runtime catalog values.
        SettingKind::DynamicEnum { default, .. } => SettingValue::String((*default).to_string()),
        // Group rows carry no scalar value; the render/reset paths special-case
        // them before calling this, so the returned value is never observed.
        SettingKind::Group { .. } => SettingValue::Bool(false),
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    /// Every SHELL/SHARED setting's default must match `UiConfig::default()`.
    /// PAGER-owned settings are covered by `defaults_match_pager_state`.
    #[test]
    fn defaults_match_ui_config_default() {
        let reg = SettingsRegistry::defaults();
        let ui = UiConfig::default();
        for meta in reg.all() {
            if meta.owner == SettingOwner::Pager {
                continue;
            }
            // Group rows have no scalar default to compare.
            if matches!(meta.kind, SettingKind::Group { .. }) {
                continue;
            }
            match (meta.key, &meta.kind) {
                ("compact_mode", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default, ui.compact_mode,
                        "compact_mode default drifts from UiConfig::default()"
                    );
                }
                // Per-tip contextual hints: `None` (inherit) → default ON.
                ("contextual_hints.undo", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.contextual_hints.undo.unwrap_or(true),
                        "contextual_hints.undo default drifts from UiConfig::default()"
                    );
                }
                ("contextual_hints.plan_mode", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.contextual_hints.plan_mode.unwrap_or(true),
                        "contextual_hints.plan_mode default drifts from UiConfig::default()"
                    );
                }
                ("contextual_hints.image_input", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.contextual_hints.image_input.unwrap_or(true),
                        "contextual_hints.image_input default drifts from UiConfig::default()"
                    );
                }
                ("contextual_hints.send_now", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.contextual_hints.send_now.unwrap_or(true),
                        "contextual_hints.send_now default drifts from UiConfig::default()"
                    );
                }
                ("contextual_hints.small_screen", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.contextual_hints.small_screen.unwrap_or(true),
                        "contextual_hints.small_screen default drifts from UiConfig::default()"
                    );
                }
                ("contextual_hints.word_select", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.contextual_hints.word_select.unwrap_or(true),
                        "contextual_hints.word_select default drifts from UiConfig::default()"
                    );
                }
                ("show_timestamps", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.show_timestamps.unwrap_or(true),
                        "show_timestamps default drifts from UiConfig::default()"
                    );
                }
                ("show_timeline", SettingKind::Bool { default }) => {
                    // Single-sourced via UiConfig::show_timeline_enabled(); this
                    // guards that defs.rs wired the resolver, not a stray literal.
                    assert_eq!(
                        *default,
                        ui.show_timeline_enabled(),
                        "show_timeline default drifts from UiConfig::default()"
                    );
                }
                ("simple_mode", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.simple_mode.unwrap_or(true),
                        "simple_mode default drifts from UiConfig::default()"
                    );
                }
                ("theme", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.theme, None,
                        "test assumes UiConfig::default().theme is None",
                    );
                    let expected = ui
                        .theme
                        .as_deref()
                        .and_then(crate::theme::canonical_name)
                        .unwrap_or("kiginight");
                    assert_eq!(
                        *default, expected,
                        "theme default drifts from UiConfig::default()",
                    );
                }
                ("auto_dark_theme", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.auto_dark_theme, None,
                        "test assumes UiConfig::default().auto_dark_theme is None",
                    );
                    let expected = ui
                        .auto_dark_theme
                        .as_deref()
                        .and_then(crate::theme::canonical_name)
                        .filter(|s| *s != "auto")
                        .unwrap_or("kiginight");
                    assert_eq!(
                        *default, expected,
                        "auto_dark_theme default drifts from UiConfig::default()",
                    );
                }
                ("auto_light_theme", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.auto_light_theme, None,
                        "test assumes UiConfig::default().auto_light_theme is None",
                    );
                    let expected = ui
                        .auto_light_theme
                        .as_deref()
                        .and_then(crate::theme::canonical_name)
                        .filter(|s| *s != "auto")
                        .unwrap_or("kigiday");
                    assert_eq!(
                        *default, expected,
                        "auto_light_theme default drifts from UiConfig::default()",
                    );
                }
                // permission_mode: None on disk → "ask" fallback.
                ("permission_mode", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.permission_mode, None,
                        "test assumes UiConfig::default().permission_mode is None",
                    );
                    let expected = "ask";
                    assert_eq!(
                        *default, expected,
                        "permission_mode default drifts from UiConfig::default()'s \
                         None → 'ask' fallback (load_permission_mode contract)",
                    );
                }
                // default_model: no UiConfig mirror, resolved dynamically.
                // Registry default is empty string ("no opinion").
                ("default_model", SettingKind::DynamicEnum { default, .. }) => {
                    assert_eq!(
                        *default, "",
                        "default_model registry default must be empty string — \
                         the live default is resolved dynamically from \
                         cfg.models.default at session start",
                    );
                }
                // max_thoughts_width: `u16` widened to `i64`.
                ("max_thoughts_width", SettingKind::Int { default, .. }) => {
                    assert_eq!(
                        *default, ui.max_thoughts_width as i64,
                        "max_thoughts_width default drifts from UiConfig::default()",
                    );
                }
                // CLI batch: fields live on CliConfig, not UiConfig.
                // Defaults pinned literally.
                ("show_tips", SettingKind::Bool { default }) => {
                    assert!(*default, "show_tips registry default must be true");
                }
                ("auto_update", SettingKind::Bool { default }) => {
                    assert!(
                        *default,
                        "auto_update registry default must be true \
                         (matches auto_update.rs's `.unwrap_or(true)`)"
                    );
                }
                // vim_mode: Option<bool>; None → false.
                ("vim_mode", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.vim_mode.unwrap_or(false),
                        "vim_mode default drifts from UiConfig::default()"
                    );
                }
                // remember_tool_approvals: Option<bool>; None → false.
                ("remember_tool_approvals", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.remember_tool_approvals.unwrap_or(false),
                        "remember_tool_approvals default drifts from UiConfig::default()"
                    );
                }
                // ask_user_question timeout: no UiConfig mirror (lives under
                // `[toolset]`); default anchored on the resolver-shared const.
                ("toolset.ask_user_question.timeout_enabled", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ask_user_question::DEFAULT_ASK_USER_QUESTION_TIMEOUT_ENABLED,
                        "toolset.ask_user_question.timeout_enabled default drifts from the \
                         shared resolver const in kigi-tools"
                    );
                }
                // show_thinking_blocks: Option<bool>; None → true (client default).
                ("show_thinking_blocks", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.show_thinking_blocks.unwrap_or(true),
                        "show_thinking_blocks default drifts from UiConfig::default()"
                    );
                }
                // group_tool_verbs: Option<bool>; None → true (client default).
                ("group_tool_verbs", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.group_tool_verbs.unwrap_or(true),
                        "group_tool_verbs default drifts from UiConfig::default()"
                    );
                }
                // collapsed_edit_blocks: Option<bool>; None → false (rollout
                // flag ships OFF; the cache const is pinned in cache.rs).
                ("collapsed_edit_blocks", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.collapsed_edit_blocks.unwrap_or(false),
                        "collapsed_edit_blocks default drifts from UiConfig::default()"
                    );
                    assert!(!*default, "collapsed_edit_blocks must default OFF");
                }
                // prompt_suggestions: Option<bool>; None → true (client default).
                ("prompt_suggestions", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.prompt_suggestions.unwrap_or(true),
                        "prompt_suggestions default drifts from UiConfig::default()"
                    );
                }
                ("keep_text_selection", SettingKind::Enum { default, .. }) => {
                    let expected = if ui.keep_text_selection_enabled() {
                        "hold"
                    } else {
                        "flash"
                    };
                    assert_eq!(*default, expected);
                }
                // hunk_tracker_mode: Option<String>; None → "agent_only".
                ("hunk_tracker_mode", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.hunk_tracker_mode, None,
                        "test assumes UiConfig::default().hunk_tracker_mode is None",
                    );
                    assert_eq!(
                        *default,
                        canonical_hunk_tracker_mode(ui.hunk_tracker_mode.as_deref()),
                        "hunk_tracker_mode default drifts from UiConfig::default()",
                    );
                }
                ("screen_mode", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.screen_mode, None,
                        "test assumes UiConfig::default().screen_mode is None",
                    );
                    assert_eq!(
                        *default,
                        canonical_screen_mode(ui.screen_mode.as_deref()),
                        "screen_mode default drifts from UiConfig::default()",
                    );
                    assert_eq!(*default, "fullscreen");
                }
                // render_mermaid: Option<String>; None → "auto".
                ("render_mermaid", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.render_mermaid, None,
                        "test assumes UiConfig::default().render_mermaid is None",
                    );
                    let expected = ui
                        .render_mermaid
                        .as_deref()
                        .and_then(crate::appearance::RenderMermaid::from_canonical)
                        .map(|k| k.as_canonical())
                        .unwrap_or("auto");
                    assert_eq!(
                        *default, expected,
                        "render_mermaid default drifts from UiConfig::default()",
                    );
                }
                // scroll_speed: Option<u8>; None → 50.
                ("scroll_speed", SettingKind::Int { default, .. }) => {
                    assert_eq!(
                        *default,
                        ui.scroll_speed.unwrap_or(50) as i64,
                        "scroll_speed default drifts from UiConfig::default()"
                    );
                }
                // scroll_mode: Option<String>; None → "auto".
                ("scroll_mode", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.scroll_mode, None,
                        "test assumes UiConfig::default().scroll_mode is None",
                    );
                    assert_eq!(
                        *default,
                        crate::appearance::ScrollMode::default().as_canonical(),
                        "scroll_mode default drifts from UiConfig::default()",
                    );
                }
                // invert_scroll: Option<bool>; None → false.
                ("invert_scroll", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.invert_scroll.unwrap_or(false),
                        "invert_scroll default drifts from UiConfig::default()"
                    );
                }
                // display_refresh.auto_cadence_enabled: Option<bool>; None → false.
                ("display_refresh_auto_cadence", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default,
                        ui.display_refresh.auto_cadence_enabled.unwrap_or(false),
                        "display_refresh_auto_cadence default drifts from UiConfig::default()"
                    );
                }
                // scroll_lines: Option<u8>; None → registry default 3 (the
                // display value while the per-terminal profile is in charge).
                ("scroll_lines", SettingKind::Int { default, .. }) => {
                    assert_eq!(
                        *default,
                        ui.scroll_lines.map(i64::from).unwrap_or(3),
                        "scroll_lines default drifts from UiConfig::default()"
                    );
                }
                // default_selected_permission: Option<String>; None →
                // "always_allow_all_sessions" (the effective default; first
                // prompt's cursor lands on the "Always allow on all sessions"
                // row, picked explicitly in `enqueue_permission`).
                ("default_selected_permission", SettingKind::Enum { default, .. }) => {
                    assert_eq!(
                        ui.default_selected_permission, None,
                        "test assumes UiConfig::default().default_selected_permission is None",
                    );
                    assert_eq!(
                        *default,
                        crate::appearance::permission_cursor::DefaultSelectedPermission::AlwaysAllowAllSessions
                            .as_canonical(),
                        "default_selected_permission registry default must be \
                         `always_allow_all_sessions` — the on-disk source of truth is \
                         `UiConfig::default_selected_permission: Option<String>` (defaults to \
                         None, mapped to the `always_allow_all_sessions` canonical)",
                    );
                }
                // fork_secondary_model: empty-string default = "no opinion".
                ("fork_secondary_model", SettingKind::DynamicEnum { default, .. }) => {
                    assert_eq!(
                        *default, "",
                        "fork_secondary_model registry default must be empty string — \
                         the live default is `crate::models::default_model()` and the \
                         current_value_for arm folds matching values to the empty sentinel",
                    );
                    // Cross-check: the UiConfig field IS the built-in default.
                    assert_eq!(
                        ui.fork_secondary_model,
                        kigi_shell::models::default_model(),
                        "UiConfig::default().fork_secondary_model must equal \
                         models::default_model() — drift here breaks the empty-fold contract",
                    );
                }

                _ => panic!(
                    "settings::defs::default_settings() contains entry `{}` with no \
                     matching arm in defaults_match_ui_config_default. Add an arm.",
                    meta.key
                ),
            }
        }
    }

    /// Every PAGER-owned setting's default must match
    /// `PagerLocalSnapshot::default()`. Three-way alignment (registry
    /// → snapshot → `AgentView::new`) is enforced across multiple tests.
    #[test]
    fn defaults_match_pager_state() {
        let reg = SettingsRegistry::defaults();
        let pager = PagerLocalSnapshot::default();
        for meta in reg.all() {
            if meta.owner != SettingOwner::Pager {
                continue;
            }
            match (meta.key, &meta.kind) {
                ("multiline_mode", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default, pager.multiline_mode,
                        "multiline_mode default drifts from PagerLocalSnapshot::default()"
                    );
                }
                ("respect_manual_folds", SettingKind::Bool { default }) => {
                    assert_eq!(
                        *default, pager.respect_manual_folds,
                        "respect_manual_folds default drifts from PagerLocalSnapshot::default()"
                    );
                    assert_eq!(
                        *default,
                        crate::appearance::ScrollConfig::default().respect_manual_folds,
                        "respect_manual_folds default drifts from ScrollConfig::default() — \
                         the appearance config is the source of truth"
                    );
                }
                // plan_mode: per-session, not persisted.
                ("plan_mode", SettingKind::Enum { default, .. }) => {
                    let expected = if pager.plan_mode_active { "on" } else { "off" };
                    assert_eq!(
                        *default, expected,
                        "plan_mode default `{default}` drifts from PagerLocalSnapshot::default() \
                         (plan_mode_active: {})",
                        pager.plan_mode_active,
                    );
                }
                _ => panic!(
                    "settings::defs::default_settings() contains PAGER entry `{}` with no \
                     matching arm in defaults_match_pager_state. Add an arm.",
                    meta.key
                ),
            }
        }
    }

    /// Every registered setting must resolve to `Some(_)` with a
    /// variant matching its `SettingKind`. Catches registry/dispatch skew.
    #[test]
    fn every_setting_has_dispatch_arm() {
        let reg = SettingsRegistry::defaults();
        let ui = UiConfig::default();
        let pager = PagerLocalSnapshot::default();
        for meta in reg.all() {
            // Group rows carry no scalar value; they are not read via this path.
            if matches!(meta.kind, SettingKind::Group { .. }) {
                continue;
            }
            let value = current_value_for(meta.key, &ui, &pager).unwrap_or_else(|| {
                panic!(
                    "current_value_for(`{}`) returned None. Add a match arm.",
                    meta.key
                )
            });
            let kind_matches = matches!(
                (&meta.kind, &value),
                (SettingKind::Bool { .. }, SettingValue::Bool(_))
                    | (SettingKind::String { .. }, SettingValue::String(_))
                    | (SettingKind::Enum { .. }, SettingValue::Enum(_))
                    | (SettingKind::Int { .. }, SettingValue::Int(_))
                    // `DynamicEnum` uses `SettingValue::String`.
                    | (SettingKind::DynamicEnum { .. }, SettingValue::String(_))
            );
            assert!(
                kind_matches,
                "current_value_for(`{}`) returned {value:?} which doesn't match the \
                 registered SettingKind. Wrong variant in the match arm.",
                meta.key
            );
        }
    }

    /// Unregistered keys return `None`.
    #[test]
    fn unmapped_key_returns_none() {
        let ui = UiConfig::default();
        let pager = PagerLocalSnapshot::default();
        assert!(current_value_for("never-registered-key-xyzzy", &ui, &pager).is_none());
    }

    #[test]
    fn canonical_hunk_tracker_mode_maps_aliases_and_unknowns() {
        assert_eq!(
            canonical_hunk_tracker_mode(Some("agent_only")),
            "agent_only"
        );
        assert_eq!(canonical_hunk_tracker_mode(Some("all_dirty")), "all_dirty");
        assert_eq!(canonical_hunk_tracker_mode(Some("off")), "off");
        // `disabled` is an accepted alias for `off`.
        assert_eq!(canonical_hunk_tracker_mode(Some("disabled")), "off");
        // Case-insensitive + whitespace-tolerant.
        assert_eq!(canonical_hunk_tracker_mode(Some("  OFF  ")), "off");
        assert_eq!(canonical_hunk_tracker_mode(Some("Disabled")), "off");
        assert_eq!(canonical_hunk_tracker_mode(Some("All_Dirty")), "all_dirty");
        // Unknown / blank / absent → the `agent_only` default.
        assert_eq!(canonical_hunk_tracker_mode(Some("bogus")), "agent_only");
        assert_eq!(canonical_hunk_tracker_mode(Some("")), "agent_only");
        assert_eq!(canonical_hunk_tracker_mode(None), "agent_only");
    }

    #[test]
    fn canonical_screen_mode_maps_aliases_and_unknowns() {
        assert_eq!(canonical_screen_mode(Some("minimal")), "minimal");
        assert_eq!(canonical_screen_mode(Some("fullscreen")), "fullscreen");
        assert_eq!(canonical_screen_mode(Some("full")), "fullscreen");
        assert_eq!(canonical_screen_mode(Some("  MINIMAL ")), "minimal");
        assert_eq!(canonical_screen_mode(Some("default")), "fullscreen");
        assert_eq!(canonical_screen_mode(Some("auto")), "fullscreen");
        assert_eq!(canonical_screen_mode(Some("bogus")), "fullscreen");
        assert_eq!(canonical_screen_mode(Some("")), "fullscreen");
        assert_eq!(canonical_screen_mode(None), "fullscreen");
    }

    /// Corrupted `auto_dark_theme = "auto"` (would cause circular ref)
    /// falls back to canonical default.
    #[test]
    fn current_value_for_auto_dark_theme_filters_auto_value() {
        let ui = UiConfig {
            auto_dark_theme: Some("auto".into()),
            ..UiConfig::default()
        };
        let pager = PagerLocalSnapshot::default();
        let value = current_value_for("auto_dark_theme", &ui, &pager).expect("must resolve");
        assert_eq!(
            value,
            SettingValue::Enum("kiginight"),
            "corrupted `auto_dark_theme = \"auto\"` must fall back to canonical default",
        );
    }

    #[test]
    fn current_value_for_auto_light_theme_filters_auto_value() {
        let ui = UiConfig {
            auto_light_theme: Some("auto".into()),
            ..UiConfig::default()
        };
        let pager = PagerLocalSnapshot::default();
        let value = current_value_for("auto_light_theme", &ui, &pager).expect("must resolve");
        assert_eq!(
            value,
            SettingValue::Enum("kigiday"),
            "corrupted `auto_light_theme = \"auto\"` must fall back to canonical default",
        );
    }

    /// Unknown values in `auto_dark_theme` fall back to default.
    #[test]
    fn current_value_for_auto_dark_theme_unknown_falls_back() {
        let ui = UiConfig {
            auto_dark_theme: Some("nonexistent-theme".into()),
            ..UiConfig::default()
        };
        let pager = PagerLocalSnapshot::default();
        let value = current_value_for("auto_dark_theme", &ui, &pager).expect("must resolve");
        assert_eq!(value, SettingValue::Enum("kiginight"));
    }

    /// Keywords must be lowercase and non-empty.
    #[test]
    fn keywords_lowercase_and_non_empty() {
        let reg = SettingsRegistry::defaults();
        for meta in reg.all() {
            for kw in meta.keywords {
                assert!(
                    !kw.is_empty(),
                    "setting `{}` has an empty keyword",
                    meta.key
                );
                assert_eq!(
                    *kw,
                    kw.to_lowercase(),
                    "setting `{}` keyword `{kw}` is not lowercase",
                    meta.key
                );
            }
        }
    }

    /// Keys are globally unique (Vec preserves declaration order).
    #[test]
    fn unique_keys() {
        use std::collections::HashMap;
        let reg = SettingsRegistry::defaults();
        let mut counts: HashMap<&str, usize> = HashMap::new();
        for m in reg.all() {
            *counts.entry(m.key).or_insert(0) += 1;
        }
        let dupes: Vec<&&str> = counts
            .iter()
            .filter(|(_, n)| **n > 1)
            .map(|(k, _)| k)
            .collect();
        assert!(
            dupes.is_empty(),
            "duplicate setting keys in default_settings(): {dupes:?}",
        );
    }

    /// `from_entries` panics on duplicate keys.
    #[test]
    #[should_panic(expected = "duplicate setting keys in registry")]
    fn from_entries_panics_on_duplicate_keys() {
        let dup_meta = SettingMeta {
            key: "synthetic_int",
            category: SettingCategory::Advanced,
            owner: SettingOwner::Shared,
            label: "Synthetic Int",
            description: "Test fixture.",
            keywords: &["test"],
            kind: SettingKind::Int {
                default: 50,
                min: 0,
                max: 200,
            },
            restart_required: false,
            hidden_in_minimal: false,
        };
        // Same key registered twice → panic.
        let _ = SettingsRegistry::from_entries(vec![dup_meta.clone(), dup_meta]);
    }

    /// Search is a literal substring multi-word AND match.
    #[test]
    fn search_multi_word_and() {
        let reg = SettingsRegistry::defaults();
        let hits = reg.search("compact density");
        assert_eq!(hits.len(), 1, "expected 1 match for 'compact density'");
        assert_eq!(hits[0].key, "compact_mode");

        let empty = reg.search("xyzzy-no-match");
        assert!(empty.is_empty(), "expected no match for 'xyzzy-no-match'");

        // Empty query returns everything.
        let all = reg.search("");
        assert_eq!(all.len(), reg.all().len());
    }

    /// `default_value_for` returns the registered default verbatim.
    #[test]
    fn default_value_for_returns_kind_default() {
        let reg = SettingsRegistry::defaults();
        for meta in reg.all() {
            // Group rows have no meaningful scalar default (sentinel only).
            if matches!(meta.kind, SettingKind::Group { .. }) {
                continue;
            }
            let v = default_value_for(meta);
            match (&meta.kind, &v) {
                (SettingKind::Bool { default }, SettingValue::Bool(b)) => assert_eq!(b, default),
                (SettingKind::String { default, .. }, SettingValue::String(s)) => {
                    assert_eq!(s, default);
                }
                (SettingKind::Enum { default, .. }, SettingValue::Enum(e)) => {
                    assert_eq!(e, default);
                }
                (SettingKind::Int { default, .. }, SettingValue::Int(i)) => {
                    assert_eq!(i, default);
                }
                // `DynamicEnum` widens to `String`.
                (SettingKind::DynamicEnum { default, .. }, SettingValue::String(s)) => {
                    assert_eq!(s, default);
                }
                _ => panic!("default_value_for kind mismatch for `{}`", meta.key),
            }
        }
    }

    #[test]
    fn category_label_round_trip() {
        // Sanity check on the canonical labels (used as section headers
        // in the modal and as filter keywords).
        for cat in SettingCategory::ALL {
            let label = cat.label();
            assert!(!label.is_empty(), "category label cannot be empty");
        }
    }

    /// The contextual-hints group is registered with its child Bool settings,
    /// each defaulting ON, and the children read from `[ui.contextual_hints]`
    /// (None inherits → ON; a user opt-out flips it).
    #[test]
    fn contextual_hints_group_and_children_registered() {
        let reg = SettingsRegistry::defaults();
        let group = reg.find("contextual_hints").expect("group registered");
        let SettingKind::Group { children } = &group.kind else {
            panic!("contextual_hints must be a Group");
        };
        assert_eq!(
            *children,
            &[
                "contextual_hints.undo",
                "contextual_hints.plan_mode",
                "contextual_hints.image_input",
                "contextual_hints.send_now",
                "contextual_hints.small_screen",
                "contextual_hints.word_select",
            ],
        );
        for &key in *children {
            let child = reg
                .find(key)
                .unwrap_or_else(|| panic!("child `{key}` must be registered"));
            assert!(
                matches!(child.kind, SettingKind::Bool { default: true }),
                "child `{key}` must be a Bool defaulting ON",
            );
        }

        let pager = PagerLocalSnapshot::default();
        // Default (inherit) reads as ON.
        assert_eq!(
            current_value_for("contextual_hints.undo", &UiConfig::default(), &pager),
            Some(SettingValue::Bool(true)),
        );
        // A user opt-out flips the read for that tip only.
        let ui = UiConfig {
            contextual_hints: kigi_shell::agent::config::ContextualHints {
                undo: Some(false),
                ..Default::default()
            },
            ..UiConfig::default()
        };
        assert_eq!(
            current_value_for("contextual_hints.undo", &ui, &pager),
            Some(SettingValue::Bool(false)),
        );
        assert_eq!(
            current_value_for("contextual_hints.plan_mode", &ui, &pager),
            Some(SettingValue::Bool(true)),
        );
    }

    /// The `compact_mode` description's row count must track the auto-compact
    /// threshold (`AUTO_COMPACT_MAX_ROWS`). `description` is a static string,
    /// so a threshold move would otherwise silently lie in the settings copy —
    /// same drift class the default-drift assertions above guard.
    #[test]
    fn compact_mode_description_tracks_auto_compact_threshold() {
        let reg = SettingsRegistry::defaults();
        let meta = reg.find("compact_mode").expect("compact_mode registered");
        let expected = format!("{} rows", crate::views::agent::AUTO_COMPACT_MAX_ROWS);
        assert!(
            meta.description.contains(&expected),
            "compact_mode description must mention the `{expected}` auto-compact \
             threshold; got {:?}",
            meta.description
        );
    }

    /// Every static Enum's choice count must stay within `MAX_PICKER_CHOICES`
    /// (product guard; the chooser already scrolls within the viewport).
    #[test]
    fn enum_choice_counts_are_bounded() {
        let reg = SettingsRegistry::defaults();
        for meta in reg.all() {
            let SettingKind::Enum { choices, .. } = &meta.kind else {
                continue;
            };
            assert!(
                choices.len() <= crate::views::settings_modal::MAX_PICKER_CHOICES,
                "Enum setting `{}` has {} choices, exceeds MAX_PICKER_CHOICES \
                 ({}). Raise the cap deliberately for a larger curated catalog \
                 (picker already scrolls).",
                meta.key,
                choices.len(),
                crate::views::settings_modal::MAX_PICKER_CHOICES,
            );
        }
    }
}
