//! The surface framework: a surface is a closure the harness calls once per
//! repetition; `run_reps` applies the measurement discipline (1 discarded
//! warmup + N measured reps, median per metric) and enforces cross-rep
//! fingerprint identity — a surface disagreeing with itself is nondeterminism,
//! reported as an infra error, never averaged away.

pub mod campaign;
pub mod engine;
pub mod fixed_cost;
pub mod ingest;

use crate::baseline::Measured;
use std::collections::BTreeMap;

/// Workload sizing: `quick` is the test/dev mode (tiny inputs, mechanics
/// only); the committed baselines are always full-size.
#[derive(Clone, Copy, Debug)]
pub struct Sizing {
    pub quick: bool,
}

/// One repetition's outcome.
pub struct RepOutcome {
    pub metrics: BTreeMap<String, f64>,
    pub fingerprint: String,
}

pub fn median(mut xs: Vec<f64>) -> f64 {
    xs.sort_by(|a, b| a.partial_cmp(b).expect("no NaN metrics"));
    xs[xs.len() / 2]
}

/// 1 discarded warmup + `reps` measured runs; median per metric key;
/// cross-rep fingerprint identity enforced.
pub fn run_reps(
    surface: &str,
    reps: u32,
    mut rep: impl FnMut() -> Result<RepOutcome, String>,
) -> Result<Measured, String> {
    let reps = reps.max(1);
    let _warmup = rep()?; // page/alloc warmup, discarded
    let mut runs = Vec::new();
    for _ in 0..reps {
        runs.push(rep()?);
    }
    let fingerprint = runs[0].fingerprint.clone();
    for r in &runs[1..] {
        if r.fingerprint != fingerprint {
            return Err(format!(
                "surface {surface} is nondeterministic across repetitions: {fingerprint:?} vs {:?}",
                r.fingerprint
            ));
        }
    }
    let mut metrics = BTreeMap::new();
    for key in runs[0].metrics.keys() {
        let vals: Vec<f64> = runs.iter().filter_map(|r| r.metrics.get(key).copied()).collect();
        metrics.insert(key.clone(), median(vals));
    }
    Ok(Measured { surface: surface.to_string(), metrics, fingerprint })
}

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

    #[test]
    fn median_picks_the_middle() {
        assert_eq!(median(vec![3.0, 1.0, 2.0]), 2.0);
        assert_eq!(median(vec![1.0, 2.0]), 2.0);
    }

    /// The engine surface's actual deliverable: `run_reps` composed with the
    /// real engine surface (a re-bootstrapped SMA-cross harness per call),
    /// not a mock closure. Protects the wiring end to end — a warmup plus
    /// three fresh bootstraps must agree on the determinism fingerprint and
    /// the returned metrics map must carry both keys a `BaselineDoc` for
    /// this surface needs (`wall_s`, `bars_per_s`), positive and finite.
    #[test]
    fn run_reps_drives_the_real_engine_surface_deterministically() {
        let measured = run_reps("engine", 3, || engine::rep(Sizing { quick: true }))
            .expect("the real engine surface must agree with itself across repeated bootstraps");
        assert_eq!(measured.surface, "engine");
        let bars_per_s = *measured.metrics.get("bars_per_s").expect("bars_per_s metric present");
        assert!(bars_per_s.is_finite() && bars_per_s > 0.0, "bars_per_s: {bars_per_s}");
        assert!(measured.metrics.contains_key("wall_s"));
    }

    /// The ingest surface's actual deliverable: `run_reps` composed with the
    /// real ingest surface (a fresh `DataServer` re-opened and re-drained
    /// per call).
    /// Protects that a warmup plus three fresh drains of the same synthetic
    /// archive agree on the fingerprint, and that the metrics map carries
    /// the schema the ingest `BaselineDoc` needs.
    #[test]
    fn run_reps_drives_the_real_ingest_surface_deterministically() {
        let fixture = ingest::setup(Sizing { quick: true }).expect("ingest fixture must write");
        let measured = run_reps("ingest", 3, || ingest::rep(&fixture))
            .expect("the real ingest surface must agree with itself across repeated drains");
        assert_eq!(measured.surface, "ingest");
        let bars_per_s = *measured.metrics.get("bars_per_s").expect("bars_per_s metric present");
        assert!(bars_per_s.is_finite() && bars_per_s > 0.0, "bars_per_s: {bars_per_s}");
        assert!(measured.metrics.contains_key("wall_s"));
    }

    /// `run_reps` composed with the real campaign sweep surface — a fresh
    /// scratch project, a freshly seeded blueprint, and a genuine
    /// `aura campaign run` child process spawned per repetition (not an
    /// in-process shortcut). Protects that two independently built-and-run
    /// scratch campaigns agree on the winner-ordinal fingerprint extracted
    /// from the child's stdout — C1's determinism carried through the
    /// child-process boundary, which is the whole reason the campaign
    /// surfaces exist as a distinct measurement path from the in-process
    /// engine/ingest surfaces above — and that the metrics map carries the
    /// wall-clock key the campaign `BaselineDoc` needs.
    ///
    /// Ignored in the default suite: it builds the release `aura` binary and
    /// spawns real campaign child runs (minutes cold) — the suite-wallclock
    /// discipline forbids that cost on every `cargo test`. Run it explicitly:
    /// `cargo test -p aura-bench -- --ignored`.
    #[test]
    #[ignore = "builds the release aura binary + real campaign scratch runs; run via -- --ignored"]
    fn run_reps_drives_the_real_campaign_sweep_surface_deterministically() {
        let bin = crate::child::resolve_aura_bin(&crate::child::workspace_root())
            .expect("the release aura binary must build");
        let measured = run_reps("campaign_sweep", 2, || campaign::sweep_rep(&bin, Sizing { quick: true }))
            .expect("the real campaign sweep surface must agree with itself across independent scratch runs");
        assert_eq!(measured.surface, "campaign_sweep");
        assert!(measured.metrics.contains_key("wall_s"));
        assert!(
            measured.fingerprint.starts_with("cells="),
            "fingerprint: {}",
            measured.fingerprint
        );
    }

    /// `run_reps` composed with the real CLI fixed-cost surface — a genuine
    /// `aura --help` spawn-floor wall plus a fresh scratch project and a real
    /// `aura run` child process per repetition (not an in-process shortcut).
    /// Protects that two independent scratch runs agree on the run-line FNV
    /// fingerprint extracted from the child's stdout, and that the metrics
    /// map carries the wall-clock keys the fixed_cost `BaselineDoc` needs.
    ///
    /// Ignored in the default suite: it builds the release `aura` binary and
    /// spawns real child processes — the suite-wallclock discipline forbids
    /// that cost on every `cargo test`. Run it explicitly:
    /// `cargo test -p aura-bench -- --ignored`.
    #[test]
    #[ignore = "builds the release aura binary + real fixed-cost child runs; run via -- --ignored"]
    fn run_reps_drives_the_real_fixed_cost_surface_deterministically() {
        let bin = crate::child::resolve_aura_bin(&crate::child::workspace_root())
            .expect("the release aura binary must build");
        let measured = run_reps("cli_fixed_cost", 2, || fixed_cost::rep(&bin))
            .expect("the real fixed_cost surface must agree with itself across independent scratch runs");
        assert_eq!(measured.surface, "cli_fixed_cost");
        assert!(measured.metrics.contains_key("help_ms"));
        assert!(measured.metrics.contains_key("run_ms"));
        assert!(
            measured.fingerprint.starts_with("run_line_fnv="),
            "fingerprint: {}",
            measured.fingerprint
        );
    }

    #[test]
    fn run_reps_rejects_cross_rep_fingerprint_drift() {
        let mut n = 0;
        let r = run_reps("t", 2, move || {
            n += 1;
            Ok(RepOutcome { metrics: BTreeMap::new(), fingerprint: format!("fp{n}") })
        });
        assert!(r.is_err(), "differing fingerprints must be an error");
    }

    /// The measurement discipline itself: the first (warmup) call is never
    /// mixed into the reported metric — its outlier value would otherwise
    /// skew the median — and the median is computed independently per
    /// metric key across only the measured reps.
    #[test]
    fn run_reps_discards_warmup_and_medians_only_measured_reps() {
        let mut n = 0;
        let measured = run_reps("t", 3, move || {
            n += 1;
            // Call 1 is the discarded warmup: an outlier that must never
            // enter the median. Calls 2..=4 (the measured reps) report
            // 1.0, 2.0, 3.0, whose median is 2.0.
            let v = if n == 1 { 1_000_000.0 } else { (n - 1) as f64 };
            let mut metrics = BTreeMap::new();
            metrics.insert("m".to_string(), v);
            Ok(RepOutcome { metrics, fingerprint: "fp".to_string() })
        })
        .expect("agreeing fingerprints must succeed");
        assert_eq!(measured.surface, "t");
        assert_eq!(measured.metrics.get("m"), Some(&2.0), "warmup outlier must not pollute the median");
        assert_eq!(measured.fingerprint, "fp");
    }
}
