//! Slash-command registry and palette filter state. The palette UI lives
//! in `widgets::slash_palette`; key handling in `update::handle_palette_key`.

/// One row in the command palette.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlashCommand {
    pub name: String,
    pub description: String,
    /// Whether selecting the command does anything. All v0.10 commands are enabled.
    pub enabled: bool,
}

/// The full v0.10 command set — every command is wired and enabled.
pub fn builtin_commands() -> Vec<SlashCommand> {
    [
        ("help", "Show commands and key bindings"),
        ("quit", "Quit capo"),
        ("compact", "Compact the conversation context now"),
        ("fork", "Branch from an earlier message"),
        ("tree", "View the session branch tree"),
        ("new", "Start a fresh session"),
        ("model", "Switch model"),
        ("resume", "Resume a previous session"),
        ("clone", "Copy this session to a new independent file"),
        ("extensions", "List loaded extensions and their health"),
        ("settings", "Edit ~/.capo/agent/settings.toml"),
    ]
    .into_iter()
    .map(|(name, description)| SlashCommand {
        name: name.into(),
        description: description.into(),
        enabled: true,
    })
    .collect()
}

/// Known Anthropic model ids offered by the `/model` picker. The picker
/// also accepts a free-typed id, so this list need not be exhaustive —
/// it is a convenience, not a constraint. No per-release maintenance
/// burden: an unknown new model is still reachable by typing it.
pub const KNOWN_MODELS: &[&str] = &["claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5"];

/// Palette state: the text typed after `/`, and the cursor into the
/// filtered list.
#[derive(Debug, Default)]
pub struct PaletteState {
    /// Raw filter text (what the user typed after the leading `/`).
    pub filter: String,
    /// Index into `filtered()` of the highlighted row.
    pub selected: usize,
}

impl PaletteState {
    /// Commands whose name starts with the current filter (case-insensitive).
    /// When the filter is empty, every command matches.
    pub fn filtered(&self, extension_commands: &[String]) -> Vec<SlashCommand> {
        let f = self.filter.to_ascii_lowercase();
        let mut out: Vec<SlashCommand> = builtin_commands()
            .into_iter()
            .filter(|c| c.name.starts_with(&f))
            .collect();
        for ext_cmd in extension_commands {
            if ext_cmd.starts_with(&f) {
                out.push(SlashCommand {
                    name: ext_cmd.clone(),
                    description: "(extension)".into(),
                    enabled: true,
                });
            }
        }
        out
    }

    /// Clamp `selected` into range after the filtered list changes.
    pub fn clamp_selection(&mut self, extension_commands: &[String]) {
        let len = self.filtered(extension_commands).len();
        if len == 0 {
            self.selected = 0;
        } else if self.selected >= len {
            self.selected = len - 1;
        }
    }
}

#[cfg(test)]
mod model_tests {
    use super::KNOWN_MODELS;

    #[test]
    fn known_models_nonempty_and_unique() {
        assert!(!KNOWN_MODELS.is_empty());
        let mut sorted = KNOWN_MODELS.to_vec();
        sorted.sort_unstable();
        sorted.dedup();
        assert_eq!(sorted.len(), KNOWN_MODELS.len(), "duplicate model id");
    }
}

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

    #[test]
    fn empty_filter_matches_all() {
        let p = PaletteState::default();
        assert_eq!(p.filtered(&[]).len(), builtin_commands().len());
    }

    #[test]
    fn empty_filter_includes_extension_commands() {
        let p = PaletteState::default();
        let ext = vec!["todo".into()];
        let filtered = p.filtered(&ext);
        assert!(filtered.iter().any(|c| c.name == "todo"));
    }

    #[test]
    fn filter_narrows_by_prefix() {
        let p = PaletteState {
            filter: "co".into(),
            selected: 0,
        };
        let f = p.filtered(&[]);
        assert_eq!(f.len(), 1);
        assert_eq!(f[0].name, "compact");
    }

    #[test]
    fn filter_is_case_insensitive() {
        let p = PaletteState {
            filter: "HE".into(),
            selected: 0,
        };
        assert_eq!(
            p.filtered(&[])
                .iter()
                .map(|c| c.name.as_str())
                .collect::<Vec<_>>(),
            ["help"]
        );
    }

    #[test]
    fn clamp_selection_pulls_into_range() {
        let mut p = PaletteState {
            filter: "help".into(),
            selected: 5,
        };
        p.clamp_selection(&[]);
        assert_eq!(p.selected, 0); // only one match → index 0
    }

    #[test]
    fn all_v0_3_commands_are_enabled() {
        for c in builtin_commands() {
            assert!(c.enabled, "/{} should be enabled", c.name);
        }
    }
}
