//! CLI fixed cost: the spawn floor (`aura --help`, no fingerprint — nothing is
//! computed) and a minimal data-only project run whose fingerprint is the
//! FNV-1a hash of the run's single stdout JSON line. Fresh scratch project per
//! repetition, so store counters start identical.

use super::{median, RepOutcome};
use crate::child::run_timed;
use crate::surfaces::campaign::build_minimal_project;
use crate::synth::fnv1a;
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Instant;

/// Fingerprint of an `aura run` record line, invariant across rebuilds: the
/// line's `manifest.commit` is the aura binary's compile-time build sha
/// (crates/aura-cli/build.rs — the invariant-8 audit trail), so hashing the
/// raw line would flip the fingerprint on EVERY new commit and report a
/// phantom correctness change at each cycle close. Parse, blank that one
/// volatile field, re-serialize (serde_json's map is sorted — deterministic
/// bytes), then hash.
pub fn run_line_fingerprint(line: &str) -> Result<String, String> {
    let mut v: serde_json::Value =
        serde_json::from_str(line).map_err(|e| format!("run record line parses: {e}"))?;
    let commit = v
        .get_mut("manifest")
        .and_then(|m| m.get_mut("commit"))
        .ok_or("run record line carries manifest.commit")?;
    *commit = serde_json::Value::String(String::new());
    let canonical = serde_json::to_string(&v).expect("re-serializing a parsed Value cannot fail");
    Ok(format!("run_line_fnv={:016x}", fnv1a(canonical.as_bytes())))
}

/// Spawn samples folded into one repetition: single-digit-millisecond spawns
/// jitter well past the 10% NOTICE threshold, so each repetition reports the
/// median over a batch instead of one throw of the scheduler dice.
pub const HELP_SAMPLES: u32 = 20;
pub const RUN_SAMPLES: u32 = 10;

pub fn rep(bin: &Path) -> Result<RepOutcome, String> {
    // Spawn floor: median wall over a batch of `aura --help` spawns.
    let mut help_walls = Vec::new();
    for _ in 0..HELP_SAMPLES {
        let t = Instant::now();
        let help = std::process::Command::new(bin)
            .arg("--help")
            .output()
            .map_err(|e| format!("spawn aura --help: {e}"))?;
        help_walls.push(t.elapsed().as_secs_f64() * 1000.0);
        if !help.status.success() {
            return Err(format!("aura --help exited {:?}", help.status.code()));
        }
    }

    // Project-load + minimal run: one scratch project, a batch of runs, the
    // median wall. The fingerprint is the FIRST run's record line — later
    // runs append to the same store, so their lines legitimately differ (run
    // counters), while the first run of a fresh scratch is deterministic.
    let scratch = build_minimal_project()?;
    let mut run_walls = Vec::new();
    let mut first_line = None;
    for _ in 0..RUN_SAMPLES {
        let timed = run_timed(bin, &["run", "bench_sma_a.json"], &scratch.0)?;
        if timed.exit != Some(0) {
            return Err(format!("aura run exited {:?}: {}", timed.exit, timed.stdout));
        }
        run_walls.push(timed.wall_s * 1000.0);
        if first_line.is_none() {
            first_line = Some(
                timed
                    .stdout
                    .lines()
                    .next()
                    .ok_or("aura run must print its JSON record line")?
                    .to_string(),
            );
        }
    }
    let line = first_line.expect("RUN_SAMPLES >= 1 sets the first line");
    let mut metrics = BTreeMap::new();
    metrics.insert("help_ms".to_string(), median(help_walls));
    metrics.insert("run_ms".to_string(), median(run_walls));
    Ok(RepOutcome { metrics, fingerprint: run_line_fingerprint(&line)? })
}

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

    /// The symptom this fixes, pinned: two record lines that differ ONLY in
    /// `manifest.commit` (a rebuild at a new commit) must fingerprint
    /// identically — the raw-line hash flipped here and reported a phantom
    /// correctness change on the first post-commit bench run.
    #[test]
    fn run_line_fingerprint_is_invariant_across_build_commits() {
        let at_a = r#"{"manifest":{"commit":"aaaa","seed":0},"metrics":{"total_pips":0.5}}"#;
        let at_b = r#"{"manifest":{"commit":"bbbb","seed":0},"metrics":{"total_pips":0.5}}"#;
        assert_eq!(
            run_line_fingerprint(at_a).expect("parses"),
            run_line_fingerprint(at_b).expect("parses"),
        );
    }

    /// The fingerprint still catches what it exists for: a computed-result
    /// change (any non-provenance byte) yields a different hash.
    #[test]
    fn run_line_fingerprint_still_detects_result_changes() {
        let base = r#"{"manifest":{"commit":"aaaa","seed":0},"metrics":{"total_pips":0.5}}"#;
        let drifted = r#"{"manifest":{"commit":"aaaa","seed":0},"metrics":{"total_pips":0.6}}"#;
        assert_ne!(
            run_line_fingerprint(base).expect("parses"),
            run_line_fingerprint(drifted).expect("parses"),
        );
    }

    /// A line without the provenance field is malformed input, not a silent
    /// pass — the guard that keeps the blanking honest if the record schema
    /// ever moves.
    #[test]
    fn run_line_fingerprint_rejects_a_line_without_manifest_commit() {
        assert!(run_line_fingerprint(r#"{"metrics":{"total_pips":0.5}}"#).is_err());
    }
}
