//! Slash commands and the autocomplete menu shown above the composer.
//!
//! The command table is static; [`filter`] turns the current composer line into
//! a (possibly empty) menu when it begins with `/`, and the binary dispatches
//! the chosen command in `app::dispatch_slash`. Pure and unit-tested — no
//! terminal or `App` dependency here.

/// One slash command in the autocomplete menu.
#[derive(Debug, Clone, Copy)]
pub struct SlashCommand {
    /// The command word, without the leading `/`.
    pub name: &'static str,
    /// One-line description shown in the popup.
    pub description: &'static str,
    /// Whether the command takes an argument typed after it (e.g. `/model
    /// gpt-5.2`). Arg commands prefill the composer on accept rather than
    /// executing immediately.
    pub needs_arg: bool,
}

/// The available commands, in display order.
pub const COMMANDS: &[SlashCommand] = &[
    SlashCommand {
        name: "resume",
        description: "resume a previous conversation",
        needs_arg: false,
    },
    SlashCommand {
        name: "rewind",
        description: "jump back to an earlier point in this conversation",
        needs_arg: false,
    },
    SlashCommand {
        name: "clear",
        description: "clear the transcript and start a new conversation",
        needs_arg: false,
    },
    SlashCommand {
        name: "model",
        description: "pick a model (lists the server's models; or /model <name>)",
        needs_arg: false,
    },
    SlashCommand {
        name: "effort",
        description: "pick the reasoning effort level",
        needs_arg: false,
    },
    SlashCommand {
        name: "server",
        description: "change the server / base URL",
        needs_arg: true,
    },
    SlashCommand {
        name: "settings",
        description: "open config.toml in $EDITOR",
        needs_arg: false,
    },
    SlashCommand {
        name: "bootstrap",
        description: "re-upload the file-editing skill (optionally delete the old)",
        needs_arg: false,
    },
    SlashCommand {
        name: "mcp",
        description: "manage MCP servers (opens a menu; or /mcp list|login|logout|add)",
        needs_arg: false,
    },
    SlashCommand {
        name: "reasoning",
        description: "toggle showing reasoning",
        needs_arg: false,
    },
    SlashCommand {
        name: "output",
        description: "toggle showing tool/shell output",
        needs_arg: false,
    },
];

/// The open autocomplete menu: the filtered commands plus the highlighted row.
#[derive(Debug, Clone)]
pub struct SlashMenu {
    pub items: Vec<&'static SlashCommand>,
    pub selected: usize,
}

impl SlashMenu {
    /// The currently highlighted command.
    pub fn selected(&self) -> &'static SlashCommand {
        self.items[self.selected]
    }

    /// Move the highlight up (wrapping).
    pub fn up(&mut self) {
        if self.items.is_empty() {
            return;
        }
        self.selected = (self.selected + self.items.len() - 1) % self.items.len();
    }

    /// Move the highlight down (wrapping).
    pub fn down(&mut self) {
        if self.items.is_empty() {
            return;
        }
        self.selected = (self.selected + 1) % self.items.len();
    }
}

/// Build the menu for the composer's first line, or `None` if no menu should
/// show. The menu appears only while typing the command *name*: the line must
/// start with `/` and contain no space yet (a space means the user has
/// committed to a command and is typing its argument). Returns `None` when
/// nothing matches the typed prefix so the popup simply closes.
pub fn filter(first_line: &str) -> Option<SlashMenu> {
    let rest = first_line.strip_prefix('/')?;
    if rest.contains(char::is_whitespace) {
        return None;
    }
    let prefix = rest.to_ascii_lowercase();
    let items: Vec<&'static SlashCommand> = COMMANDS
        .iter()
        .filter(|c| c.name.starts_with(&prefix))
        .collect();
    if items.is_empty() {
        return None;
    }
    Some(SlashMenu { items, selected: 0 })
}

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

    #[test]
    fn slash_alone_lists_all_commands() {
        let m = filter("/").expect("menu for '/'");
        assert_eq!(m.items.len(), COMMANDS.len());
        assert_eq!(m.selected, 0);
    }

    #[test]
    fn filters_by_prefix_case_insensitively() {
        let m = filter("/re").expect("menu for '/re'");
        let names: Vec<_> = m.items.iter().map(|c| c.name).collect();
        assert_eq!(names, vec!["resume", "rewind", "reasoning"]);
        // Case-insensitive.
        assert_eq!(filter("/RE").unwrap().items.len(), 3);
        // Exact match still lists it.
        assert_eq!(filter("/model").unwrap().items[0].name, "model");
    }

    #[test]
    fn no_menu_without_slash_or_once_typing_an_arg() {
        assert!(filter("hello").is_none());
        assert!(filter("").is_none());
        // A space means an argument is being typed -> menu closes.
        assert!(filter("/model gpt-5.2").is_none());
        // Unknown prefix -> no menu.
        assert!(filter("/zzz").is_none());
    }

    #[test]
    fn navigation_wraps() {
        let mut m = filter("/").unwrap();
        m.up(); // wrap to last
        assert_eq!(m.selected, COMMANDS.len() - 1);
        m.down(); // wrap to first
        assert_eq!(m.selected, 0);
        m.down();
        assert_eq!(m.selected, 1);
    }

    #[test]
    fn arg_commands_are_flagged() {
        let needs_arg = |name| {
            COMMANDS
                .iter()
                .find(|c| c.name == name)
                .map(|c| c.needs_arg)
        };
        // /server prefills its arg; /model and /resume open pickers on accept.
        assert_eq!(needs_arg("server"), Some(true));
        assert_eq!(needs_arg("model"), Some(false));
        assert_eq!(needs_arg("resume"), Some(false));
    }
}
