//! Child-process measurement: resolve the release `aura` binary via cargo's
//! JSON messages, run it under `/usr/bin/time -v`, and parse wall/CPU/RSS
//! from the stderr report. `/usr/bin/time` missing is an infra error — the
//! campaign surfaces have no fallback RSS probe by design (one tool, one
//! parse, the practice proven in the #277 measurement session).

use std::path::{Path, PathBuf};
use std::time::Instant;

/// Workspace root: two levels above this crate's manifest dir.
pub fn workspace_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .nth(2)
        .expect("crates/aura-bench sits two levels under the workspace root")
        .to_path_buf()
}

/// Build (if needed) and locate the release `aura` binary. Unmeasured setup.
pub fn resolve_aura_bin(ws_root: &Path) -> Result<PathBuf, String> {
    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
    let out = std::process::Command::new(cargo)
        .args(["build", "--release", "-p", "aura-cli", "--message-format=json"])
        .current_dir(ws_root)
        .output()
        .map_err(|e| format!("spawn cargo build: {e}"))?;
    if !out.status.success() {
        return Err(format!(
            "cargo build --release -p aura-cli failed:\n{}",
            String::from_utf8_lossy(&out.stderr)
        ));
    }
    parse_aura_executable(&String::from_utf8_lossy(&out.stdout))
        .ok_or_else(|| "no compiler-artifact message named `aura` with an executable".to_string())
}

/// Extract the `aura` executable path from `--message-format=json` lines.
pub fn parse_aura_executable(stdout: &str) -> Option<PathBuf> {
    for line in stdout.lines() {
        let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else { continue };
        if v["reason"] == "compiler-artifact"
            && v["target"]["name"] == "aura"
            && v["executable"].is_string()
        {
            return Some(PathBuf::from(v["executable"].as_str().expect("checked string")));
        }
    }
    None
}

pub struct TimedChild {
    pub wall_s: f64,
    pub cpu_percent: Option<f64>,
    pub peak_rss_mb: Option<f64>,
    pub stdout: String,
    pub stderr: String,
    pub exit: Option<i32>,
}

/// Run `argv` in `cwd` under `/usr/bin/time -v`, measuring wall in-process.
pub fn run_timed(bin: &Path, args: &[&str], cwd: &Path) -> Result<TimedChild, String> {
    if !Path::new("/usr/bin/time").exists() {
        return Err("/usr/bin/time is required for child-process surfaces".to_string());
    }
    let t = Instant::now();
    let out = std::process::Command::new("/usr/bin/time")
        .arg("-v")
        .arg(bin)
        .args(args)
        .current_dir(cwd)
        .output()
        .map_err(|e| format!("spawn {}: {e}", bin.display()))?;
    let wall_s = t.elapsed().as_secs_f64();
    let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
    let (cpu_percent, peak_rss_mb) = parse_time_v(&stderr);
    Ok(TimedChild {
        wall_s,
        cpu_percent,
        peak_rss_mb,
        stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
        stderr,
        exit: out.status.code(),
    })
}

/// Parse `Percent of CPU this job got` and `Maximum resident set size` out of
/// a `/usr/bin/time -v` stderr report.
pub fn parse_time_v(stderr: &str) -> (Option<f64>, Option<f64>) {
    let mut cpu = None;
    let mut rss = None;
    for line in stderr.lines() {
        let line = line.trim();
        if let Some(v) = line.strip_prefix("Percent of CPU this job got: ") {
            cpu = v.trim_end_matches('%').parse::<f64>().ok();
        } else if let Some(v) = line.strip_prefix("Maximum resident set size (kbytes): ") {
            rss = v.parse::<f64>().ok().map(|kb| kb / 1024.0);
        }
    }
    (cpu, rss)
}

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

    /// `resolve_aura_bin` hands its `cargo build --release -p aura-cli`
    /// invocation a `current_dir`, and that invocation only produces the
    /// right binary if `workspace_root` actually resolves to the real
    /// workspace root — the `.ancestors().nth(2)` computation is a
    /// hardcoded distance that would silently point at the wrong directory
    /// (and build nothing, or the wrong crate graph) if `aura-bench` were
    /// ever relocated a level deeper or shallower. Pins that it lands on
    /// the directory holding the actual workspace manifest, not merely
    /// some ancestor.
    #[test]
    fn workspace_root_resolves_the_real_cargo_workspace() {
        let root = workspace_root();
        let manifest = std::fs::read_to_string(root.join("Cargo.toml")).unwrap_or_else(|e| {
            panic!("workspace_root ({}) must contain Cargo.toml: {e}", root.display())
        });
        assert!(manifest.contains("[workspace]"), "not the workspace root: {}", root.display());
    }

    #[test]
    fn parse_time_v_extracts_cpu_and_rss() {
        let s = "\tCommand being timed: \"aura --help\"\n\
                 \tPercent of CPU this job got: 2270%\n\
                 \tMaximum resident set size (kbytes): 1048576\n";
        let (cpu, rss) = parse_time_v(s);
        assert_eq!(cpu, Some(2270.0));
        assert_eq!(rss, Some(1024.0));
    }

    #[test]
    fn parse_aura_executable_reads_compiler_artifact_lines() {
        let lines = concat!(
            "{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"aura-bench\"},\"executable\":\"/x/aura-bench\"}\n",
            "{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"aura\"},\"executable\":\"/x/release/aura\"}\n",
            "{\"reason\":\"build-finished\",\"success\":true}\n",
        );
        assert_eq!(parse_aura_executable(lines), Some(PathBuf::from("/x/release/aura")));
    }

    #[test]
    fn parse_aura_executable_ignores_null_executables() {
        let lines = "{\"reason\":\"compiler-artifact\",\"target\":{\"name\":\"aura\"},\"executable\":null}\n";
        assert_eq!(parse_aura_executable(lines), None);
    }
}
