//! Fieldtest c0010 #1 — `aura run` as a downstream CLI consumer (axis a).
//!
//! Question under test: invoking the REAL `aura` binary the way the deferred
//! C18 registry or a shell/jq pipeline would — capture stdout, treat it as the
//! single-line canonical JSON report (manifest + metrics) — does the documented
//! C14 machine-readable face hold? Specifically:
//!   - `aura run`            -> exit 0, one line of JSON on stdout, nothing on stderr;
//!   - the JSON parses as an object with {manifest, metrics} nesting (rustdoc:
//!     "prints the run's metrics + manifest as canonical JSON to stdout");
//!   - two runs are byte-identical (C1 determinism, pinned on the surface);
//!   - missing / unknown subcommand -> exit 2 + `aura: usage: aura run` on
//!     stderr, NOTHING on stdout (cycle_scope bad-args contract).
//!
//! This consumer does NOT read crates/*/src. It only runs the binary and
//! inspects observable behaviour. The binary path comes from $AURA_BIN (a CI
//! consumer would point this at the built artefact); we default to the
//! sibling workspace target/debug/aura so `cargo run` works out of the box.

use std::process::Command;

fn aura_bin() -> String {
    if let Ok(p) = std::env::var("AURA_BIN") {
        return p;
    }
    // Default: the workspace debug artefact, two levels up from this crate.
    let here = env!("CARGO_MANIFEST_DIR");
    format!("{here}/../../target/debug/aura")
}

/// A bare-bones JSON value walker — enough for a consumer to confirm the
/// documented {manifest, metrics} shape WITHOUT a serde dependency (the
/// workspace is deliberately zero-external-dependency; a downstream consumer
/// would reach for serde_json or jq, but here we only assert structural facts
/// the rustdoc promises). Returns the substring value following `"key":`.
fn raw_value_after<'a>(json: &'a str, key: &str) -> Option<&'a str> {
    let needle = format!("\"{key}\":");
    let idx = json.find(&needle)? + needle.len();
    Some(&json[idx..])
}

fn run_capture(args: &[&str]) -> (String, String, i32) {
    let out = Command::new(aura_bin())
        .args(args)
        .output()
        .unwrap_or_else(|e| panic!("failed to spawn {}: {e}", aura_bin()));
    (
        String::from_utf8_lossy(&out.stdout).to_string(),
        String::from_utf8_lossy(&out.stderr).to_string(),
        out.status.code().unwrap_or(-1),
    )
}

fn main() {
    println!("using binary: {}", aura_bin());

    // --- `aura run`: exit 0, one JSON line on stdout, clean stderr ---
    let (stdout, stderr, code) = run_capture(&["run"]);
    println!("`aura run` exit={code}");
    println!("`aura run` stdout = {stdout:?}");
    assert_eq!(code, 0, "aura run must exit 0");
    assert!(stderr.is_empty(), "aura run must not write stderr, got {stderr:?}");

    let line = stdout.trim_end_matches('\n');
    assert!(!line.contains('\n'), "report must be a single line of JSON");
    assert!(line.starts_with('{') && line.ends_with('}'), "report is a JSON object");

    // Documented nesting: a `manifest` object and a `metrics` object.
    assert!(raw_value_after(line, "manifest").is_some(), "manifest key present");
    assert!(raw_value_after(line, "metrics").is_some(), "metrics key present");
    // Documented manifest fields (RunManifest rustdoc: commit/params/window/seed/broker).
    for k in ["commit", "params", "window", "seed", "broker"] {
        assert!(raw_value_after(line, k).is_some(), "manifest.{k} key present");
    }
    // Documented metrics fields (RunMetrics rustdoc).
    for k in ["total_pips", "max_drawdown", "exposure_sign_flips"] {
        assert!(raw_value_after(line, k).is_some(), "metrics.{k} key present");
    }

    // --- determinism: two runs byte-identical (C1) ---
    let (stdout2, _, _) = run_capture(&["run"]);
    assert_eq!(stdout, stdout2, "two runs must be byte-identical (C1)");
    println!("determinism: two runs byte-identical OK");

    // --- bad-args contract: missing subcommand ---
    let (out, err, code) = run_capture(&[]);
    println!("`aura` (no args) exit={code} stderr={err:?}");
    assert_eq!(code, 2, "no subcommand must exit 2");
    assert!(out.is_empty(), "no subcommand must print nothing on stdout, got {out:?}");
    assert_eq!(err.trim_end(), "aura: usage: aura run", "exact usage line on stderr");

    // --- bad-args contract: unknown subcommand ---
    let (out, err, code) = run_capture(&["play"]);
    println!("`aura play` exit={code} stderr={err:?}");
    assert_eq!(code, 2, "unknown subcommand must exit 2");
    assert!(out.is_empty(), "unknown subcommand must print nothing on stdout");
    assert_eq!(err.trim_end(), "aura: usage: aura run", "exact usage line on stderr");

    // --- probe: trailing junk after `run`. The cycle_scope says "Any other or
    // missing subcommand prints usage and exits 2"; `run extra` is neither a
    // clean `run` nor a different subcommand. What does the binary do? ---
    let (out, err, code) = run_capture(&["run", "extra-garbage"]);
    println!("`aura run extra-garbage` exit={code} stdout_len={} stderr={err:?}", out.len());
    // We RECORD the behaviour; the reading we assert is the lenient one the
    // binary actually exhibits, and flag the spec ambiguity in the report.
    if code == 0 {
        println!("NOTE: `aura run <extra>` was ACCEPTED (exit 0) — trailing args ignored.");
    } else {
        println!("NOTE: `aura run <extra>` was REJECTED (exit {code}).");
    }

    println!("c0010_1 OK: aura run -> JSON report; determinism + bad-args contract observed");
}
