//! Handler for `mcptest baseline`.
//!
//! v1 ships `update`, which folds a saved canonical run report into
//! the timing baseline file the `mcptest run --check-baseline` gate
//! reads. The user workflow is two commands so a CI workflow can
//! capture the report once and feed it to the baseline updater
//! without re-running the suite:
//!
//! ```sh
//! mcptest run --reporter json --output run.json
//! mcptest baseline update --from-report run.json \
//!     mcptest.timing-baseline.yml
//! ```

use std::fs;

use anyhow::{Context, Result};
use mcptest_core::security::ServerFingerprint;

use crate::cli::args::{BaselineArgs, BaselineCommand, BaselineUpdateArgs};

/// Dispatch the `mcptest baseline` subcommand.
pub fn baseline_command(args: BaselineArgs) -> Result<i32> {
    match args.command {
        BaselineCommand::Update(update) => update_command(update),
    }
}

fn update_command(args: BaselineUpdateArgs) -> Result<i32> {
    let report_bytes = fs::read(&args.from_report)
        .with_context(|| format!("failed to read run report `{}`", args.from_report.display()))?;
    let run: mcptest_core::report::Report = serde_json::from_slice(&report_bytes)
        .with_context(|| {
            format!(
                "failed to parse run report `{}` (expected the canonical JSON shape from `mcptest run --reporter json`)",
                args.from_report.display(),
            )
        })?;

    let now = now_rfc3339();
    let mcptest_version = env!("CARGO_PKG_VERSION").to_string();

    let mut updated = match mcptest_core::baseline::load(&args.baseline_path) {
        Ok(existing) => mcptest_core::baseline::fold_run_into_baseline(
            &existing,
            &run,
            args.samples,
            &now,
            &mcptest_version,
        ),
        Err(mcptest_core::baseline::BaselineError::Io(e))
            if e.kind() == std::io::ErrorKind::NotFound =>
        {
            mcptest_core::baseline::bootstrap_from_run(&run, &now, &mcptest_version)
        }
        Err(e) => {
            return Err(anyhow::anyhow!(
                "failed to load baseline `{}`: {e}",
                args.baseline_path.display()
            ));
        }
    };

    // Capture the server launch identity for the SEC-065 binary-pinning lane
    // when the user named a target. A timing-only update leaves any existing
    // fingerprint in place (fold_run_into_baseline already preserved it).
    if let Some(fingerprint) = server_fingerprint(&args) {
        updated.server_fingerprint = Some(fingerprint);
    }

    let existed = args.baseline_path.exists();
    mcptest_core::baseline::write(&updated, &args.baseline_path).with_context(|| {
        format!(
            "failed to write baseline `{}`",
            args.baseline_path.display()
        )
    })?;

    println!(
        "wrote {} baseline {} ({} test{}{})",
        if existed { "updated" } else { "new" },
        args.baseline_path.display(),
        updated.tests.len(),
        if updated.tests.len() == 1 { "" } else { "s" },
        if updated.server_fingerprint.is_some() {
            ", server fingerprint pinned"
        } else {
            ""
        },
    );
    Ok(0)
}

/// Build the server fingerprint to pin from the `--server-command` or
/// `--server-url` flag, or `None` when neither was passed.
///
/// `--server-command` is the resolved stdio launch (first token is the command,
/// the rest are args); `--server-url` is a URL endpoint. The two flags are
/// mutually exclusive at the clap layer, so this only reads one. An empty
/// command vec is treated as absent so a stray `--server-command` with no value
/// does not pin a nameless binary.
fn server_fingerprint(args: &BaselineUpdateArgs) -> Option<ServerFingerprint> {
    if let Some(url) = &args.server_url {
        return Some(ServerFingerprint::url(url));
    }
    let (command, rest) = args.server_command.split_first()?;
    Some(ServerFingerprint::stdio(command, rest))
}

/// Render a unix timestamp as RFC 3339 UTC without pulling chrono or
/// time. The baseline schema only wants a string the user can read,
/// not a typed value, so a hand-rolled formatter keeps the dep tree
/// small and matches the existing baseline write path.
fn now_rfc3339() -> String {
    use std::time::SystemTime;
    let secs = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    let mut days = secs / 86_400;
    let secs_of_day = secs % 86_400;
    let h = secs_of_day / 3600;
    let m = (secs_of_day % 3600) / 60;
    let s = secs_of_day % 60;
    let mut year: u64 = 1970;
    loop {
        let dy = if is_leap_year(year) { 366 } else { 365 };
        if days >= dy {
            days -= dy;
            year += 1;
        } else {
            break;
        }
    }
    let months_table = if is_leap_year(year) {
        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    } else {
        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    };
    let mut month: usize = 0;
    while month < 12 && days >= months_table[month] {
        days -= months_table[month];
        month += 1;
    }
    let day = days + 1;
    let month_human = month + 1;
    format!("{year:04}-{month_human:02}-{day:02}T{h:02}:{m:02}:{s:02}Z")
}

fn is_leap_year(y: u64) -> bool {
    (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
}

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

    #[test]
    fn now_rfc3339_renders_a_sortable_iso8601_string() {
        let s = now_rfc3339();
        assert_eq!(s.len(), 20, "expected 20-char RFC 3339, got: {s}");
        assert!(s.ends_with('Z'), "expected trailing Z, got: {s}");
        let year: u32 = s[0..4].parse().unwrap();
        assert!(year >= 2025, "expected current-era year, got: {year}");
    }

    #[test]
    fn is_leap_year_table() {
        assert!(is_leap_year(2000));
        assert!(!is_leap_year(1900));
        assert!(is_leap_year(2024));
        assert!(!is_leap_year(2023));
    }

    #[test]
    fn server_fingerprint_is_none_without_a_target_flag() {
        let args = BaselineUpdateArgs::default();
        assert!(super::server_fingerprint(&args).is_none());
    }

    #[test]
    fn server_fingerprint_reads_a_url_target() {
        let args = BaselineUpdateArgs {
            server_url: Some("https://api.example.com/mcp".into()),
            ..Default::default()
        };
        match super::server_fingerprint(&args) {
            Some(ServerFingerprint::Url { host, .. }) => {
                assert_eq!(host.as_deref(), Some("api.example.com"));
            }
            other => panic!("expected a URL fingerprint, got {other:?}"),
        }
    }

    #[test]
    fn server_fingerprint_reads_a_stdio_command_and_args() {
        let args = BaselineUpdateArgs {
            server_command: vec!["uvx".into(), "mcp-server".into(), "--flag".into()],
            ..Default::default()
        };
        match super::server_fingerprint(&args) {
            Some(ServerFingerprint::Stdio { command, args, .. }) => {
                assert_eq!(command, "uvx");
                assert_eq!(args, vec!["mcp-server".to_string(), "--flag".to_string()]);
            }
            other => panic!("expected a stdio fingerprint, got {other:?}"),
        }
    }

    #[test]
    fn server_fingerprint_treats_an_empty_command_as_absent() {
        let args = BaselineUpdateArgs {
            server_command: vec![],
            ..Default::default()
        };
        assert!(super::server_fingerprint(&args).is_none());
    }
}
