//! Tiny human-duration parser for agent caps.
//!
//! Why hand-rolled: the only duration strings agent caps need are simple
//! single-unit forms like `"30s"` or `"2m"`, and the workspace carries no
//! duration-parsing crate. A short parser avoids a new dependency and a
//! NOTICE entry for a one-string-shape need. Accepts an integer count
//! followed by a single unit suffix: `ms`, `s`, `m`, or `h`.

use std::time::Duration;

use thiserror::Error;

/// Why a duration string failed to parse. Carries the offending input so the
/// suite loader can point the user at the exact YAML value that did not parse.
#[derive(Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum DurationParseError {
    /// The string was empty or had no numeric part.
    #[error("empty or non-numeric duration: `{0}`")]
    Empty(String),
    /// The numeric part did not fit a `u64`.
    #[error("duration count out of range: `{0}`")]
    Overflow(String),
    /// The unit suffix was not one of `ms`, `s`, `m`, `h`.
    #[error("unknown duration unit in `{0}` (use ms, s, m, or h)")]
    UnknownUnit(String),
}

/// Parse a human duration like `"30s"`, `"500ms"`, `"2m"`, `"1h"` into a
/// [`Duration`]. Whitespace around the value is tolerated; a bare unit or a
/// bare number is rejected so a typo cannot silently become zero.
pub fn parse_human_duration(input: &str) -> Result<Duration, DurationParseError> {
    let trimmed = input.trim();
    let split = trimmed
        .find(|c: char| !c.is_ascii_digit())
        .ok_or_else(|| DurationParseError::UnknownUnit(input.to_string()))?;
    if split == 0 {
        return Err(DurationParseError::Empty(input.to_string()));
    }
    let (digits, unit) = trimmed.split_at(split);
    let count: u64 = digits
        .parse()
        .map_err(|_| DurationParseError::Overflow(input.to_string()))?;
    match unit.trim() {
        "ms" => Ok(Duration::from_millis(count)),
        "s" => Ok(Duration::from_secs(count)),
        "m" => Ok(Duration::from_secs(count.saturating_mul(60))),
        "h" => Ok(Duration::from_secs(count.saturating_mul(3600))),
        _ => Err(DurationParseError::UnknownUnit(input.to_string())),
    }
}

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

    #[test]
    fn parses_seconds() {
        assert_eq!(
            parse_human_duration("30s").unwrap(),
            Duration::from_secs(30)
        );
    }

    #[test]
    fn parses_minutes() {
        assert_eq!(
            parse_human_duration("2m").unwrap(),
            Duration::from_secs(120)
        );
    }

    #[test]
    fn parses_hours() {
        assert_eq!(
            parse_human_duration("1h").unwrap(),
            Duration::from_secs(3600)
        );
    }

    #[test]
    fn parses_milliseconds() {
        assert_eq!(
            parse_human_duration("500ms").unwrap(),
            Duration::from_millis(500)
        );
    }

    #[test]
    fn tolerates_surrounding_whitespace() {
        assert_eq!(
            parse_human_duration("  45s  ").unwrap(),
            Duration::from_secs(45)
        );
    }

    #[test]
    fn rejects_bare_number() {
        assert!(matches!(
            parse_human_duration("30"),
            Err(DurationParseError::UnknownUnit(_))
        ));
    }

    #[test]
    fn rejects_bare_unit() {
        assert!(matches!(
            parse_human_duration("s"),
            Err(DurationParseError::Empty(_))
        ));
    }

    #[test]
    fn rejects_unknown_unit() {
        assert!(matches!(
            parse_human_duration("5d"),
            Err(DurationParseError::UnknownUnit(_))
        ));
    }

    #[test]
    fn rejects_empty() {
        assert!(matches!(
            parse_human_duration(""),
            Err(DurationParseError::UnknownUnit(_))
        ));
    }
}
