//! Pure argument parsing for bot commands.
//!
//! Every function in this module is synchronous and free of I/O. All
//! filesystem, network, and agent-pool access belongs in `io.rs`.

// ── Parsed argument types ─────────────────────────────────────────────────────

/// Parsed arguments for the `assign` command.
#[derive(Debug)]
pub struct AssignArgs {
    /// The numeric story identifier (as a string, e.g. `"42"`).
    pub number: String,
    /// The model / agent name (e.g. `"opus"`, `"coder-sonnet"`).
    pub model: String,
}

/// Parsed arguments for the `start` command.
#[derive(Debug)]
pub struct StartArgs {
    /// The numeric story identifier.
    pub number: String,
    /// Optional model hint (e.g. `"opus"` → resolved to `"coder-opus"`).
    pub hint: Option<String>,
}

// ── Parsing functions ─────────────────────────────────────────────────────────

/// Parse `assign` arguments: `<number> <model>`.
///
/// Returns `Err` with a user-visible usage string if the arguments are missing
/// or invalid (non-numeric number, empty model).
pub fn parse_assign(args: &str) -> Result<AssignArgs, String> {
    let mut parts = args.splitn(2, char::is_whitespace);
    let number = parts.next().unwrap_or("").trim().to_string();
    let model = parts.next().unwrap_or("").trim().to_string();

    if number.is_empty() || !number.chars().all(|c| c.is_ascii_digit()) || model.is_empty() {
        return Err("Usage: `/assign <number> <model>` (e.g. `/assign 42 opus`)".to_string());
    }

    Ok(AssignArgs { number, model })
}

/// Parse `start` arguments: `<number>` or `<number> <model_hint>`.
///
/// Returns `Err` with a user-visible usage string if the number is missing
/// or non-numeric.
pub fn parse_start(args: &str) -> Result<StartArgs, String> {
    let mut parts = args.splitn(2, char::is_whitespace);
    let number = parts.next().unwrap_or("").trim().to_string();
    let hint_str = parts.next().unwrap_or("").trim();

    if number.is_empty() || !number.chars().all(|c| c.is_ascii_digit()) {
        return Err(
            "Usage: `/start <number>` or `/start <number> <model>` (e.g. `/start 42 opus`)"
                .to_string(),
        );
    }

    let hint = if hint_str.is_empty() {
        None
    } else {
        Some(hint_str.to_string())
    };

    Ok(StartArgs { number, hint })
}

/// Parse a single numeric argument for commands like `delete` and `rmtree`.
///
/// `cmd_name` is used only in the error message (e.g. `"delete"` or `"rmtree"`).
/// Returns `Err` with a user-visible usage string if the argument is missing
/// or non-numeric.
pub fn parse_number(cmd_name: &str, args: &str) -> Result<String, String> {
    let number = args.trim().to_string();
    if number.is_empty() || !number.chars().all(|c| c.is_ascii_digit()) {
        return Err(format!(
            "Usage: `/{cmd_name} <number>` (e.g. `/{cmd_name} 42`)"
        ));
    }
    Ok(number)
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    // -- parse_assign ----------------------------------------------------------

    #[test]
    fn assign_valid() {
        let r = parse_assign("42 opus").unwrap();
        assert_eq!(r.number, "42");
        assert_eq!(r.model, "opus");
    }

    #[test]
    fn assign_valid_model_with_spaces() {
        // splitn(2): everything after first whitespace goes into `model`.
        let r = parse_assign("42 claude-opus-4").unwrap();
        assert_eq!(r.number, "42");
        assert_eq!(r.model, "claude-opus-4");
    }

    #[test]
    fn assign_missing_all_args() {
        assert!(parse_assign("").is_err());
    }

    #[test]
    fn assign_missing_model() {
        let err = parse_assign("42").unwrap_err();
        assert!(
            err.contains("Usage"),
            "error should contain usage hint: {err}"
        );
    }

    #[test]
    fn assign_non_numeric_number() {
        let err = parse_assign("foo opus").unwrap_err();
        assert!(
            err.contains("Usage"),
            "error should contain usage hint: {err}"
        );
    }

    #[test]
    fn assign_number_with_letters_is_invalid() {
        assert!(parse_assign("42x opus").is_err());
    }

    // -- parse_start -----------------------------------------------------------

    #[test]
    fn start_valid_number_only() {
        let r = parse_start("42").unwrap();
        assert_eq!(r.number, "42");
        assert!(r.hint.is_none());
    }

    #[test]
    fn start_valid_with_hint() {
        let r = parse_start("42 opus").unwrap();
        assert_eq!(r.number, "42");
        assert_eq!(r.hint.as_deref(), Some("opus"));
    }

    #[test]
    fn start_missing_number() {
        let err = parse_start("").unwrap_err();
        assert!(
            err.contains("Usage"),
            "error should contain usage hint: {err}"
        );
    }

    #[test]
    fn start_non_numeric_number() {
        let err = parse_start("foo").unwrap_err();
        assert!(
            err.contains("Usage"),
            "error should contain usage hint: {err}"
        );
    }

    #[test]
    fn start_non_numeric_with_hint() {
        assert!(parse_start("foo opus").is_err());
    }

    // -- parse_number ----------------------------------------------------------

    #[test]
    fn number_valid() {
        assert_eq!(parse_number("delete", "99").unwrap(), "99");
    }

    #[test]
    fn number_missing() {
        let err = parse_number("delete", "").unwrap_err();
        assert!(
            err.contains("Usage"),
            "error should contain usage hint: {err}"
        );
        assert!(
            err.contains("delete"),
            "error should mention the command: {err}"
        );
    }

    #[test]
    fn number_non_numeric() {
        let err = parse_number("delete", "abc").unwrap_err();
        assert!(
            err.contains("Usage"),
            "error should contain usage hint: {err}"
        );
    }

    #[test]
    fn number_usage_contains_cmd_name() {
        let err = parse_number("rmtree", "").unwrap_err();
        assert!(
            err.contains("rmtree"),
            "usage should mention the command: {err}"
        );
    }

    #[test]
    fn number_whitespace_only_is_invalid() {
        assert!(parse_number("delete", "   ").is_err());
    }
}
