//! CLI-pipeline idempotency gate: every `examples/*.ail` survives
//! `ail parse | ail render | ail parse` with byte identity on the
//! canonical JSON output.
//!
//! This is the user-facing-surface defence line. Crate-internal
//! roundtrip tests in `ailang-surface` cover the same property
//! at the library level; this test additionally protects the
//! `ail parse` and `ail render` CLI wrappers from drift (output
//! formatting, exit codes, stdout framing).
//!
//! Pure reader: the test writes only to a `tempfile::TempDir`
//! that lives outside the repo and is cleaned up by Drop. No
//! committed content is mutated.
//!
//! Retired iter form-a.1 T9:
//! `cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`
//! (walked `.ail.json` corpus; replaced by the corpus-flipped
//! `cli_parse_then_render_then_parse_is_idempotent` below, added in
//! T1). The `list_json_fixtures` helper and `roundtrip_one` helper
//! were the only consumers of `.ail.json` walks and are retired
//! alongside.

use ailang_test_support::{examples_dir, list_ail_fixtures};
use std::process::Command;

fn ail_bin() -> &'static str {
    env!("CARGO_BIN_EXE_ail")
}

/// CLI-pipeline idempotency (post-form-a-default-authoring §C3): for
/// every `.ail` fixture, the user-facing pipeline
/// `ail parse <ail> | ail render | ail parse` is byte-identical
/// to direct `ail parse <ail>`. Pins drift internal tests cannot see.
#[test]
fn cli_parse_then_render_then_parse_is_idempotent() {
    let fixtures = list_ail_fixtures();
    assert!(
        !fixtures.is_empty(),
        "no .ail fixtures found under {}",
        examples_dir().display()
    );

    let tmpdir = tempfile::TempDir::new().expect("create tempdir");
    let mut checked = 0usize;
    let mut failures = Vec::<String>::new();

    for ail_path in &fixtures {
        // Direct parse → JSON tempfile.
        let json1 = tmpdir.path().join(format!("v1_{checked}.ail.json"));
        let parse1 = Command::new(ail_bin())
            .args([
                "parse",
                ail_path.to_str().unwrap(),
                "-o",
                json1.to_str().unwrap(),
            ])
            .output()
            .expect("spawn ail parse #1");
        if !parse1.status.success() {
            failures.push(format!(
                "{}: first ail parse exit={:?}\nstderr: {}",
                ail_path.display(),
                parse1.status.code(),
                String::from_utf8_lossy(&parse1.stderr),
            ));
            checked += 1;
            continue;
        }

        // Render that JSON → .ail tempfile.
        let ail_round = tmpdir.path().join(format!("round_{checked}.ail"));
        let rendered = Command::new(ail_bin())
            .args(["render", json1.to_str().unwrap()])
            .output()
            .expect("spawn ail render");
        if !rendered.status.success() {
            failures.push(format!(
                "{}: ail render exit={:?}\nstderr: {}",
                ail_path.display(),
                rendered.status.code(),
                String::from_utf8_lossy(&rendered.stderr),
            ));
            checked += 1;
            continue;
        }
        std::fs::write(&ail_round, &rendered.stdout)
            .expect("write ail_round tempfile");

        // Re-parse → second JSON tempfile.
        let json2 = tmpdir.path().join(format!("v2_{checked}.ail.json"));
        let parse2 = Command::new(ail_bin())
            .args([
                "parse",
                ail_round.to_str().unwrap(),
                "-o",
                json2.to_str().unwrap(),
            ])
            .output()
            .expect("spawn ail parse #2");
        if !parse2.status.success() {
            failures.push(format!(
                "{}: second ail parse exit={:?}\nstderr: {}",
                ail_path.display(),
                parse2.status.code(),
                String::from_utf8_lossy(&parse2.stderr),
            ));
            checked += 1;
            continue;
        }

        // Assert the two canonical JSONs are byte-equal.
        let b1 = std::fs::read(&json1).expect("read json1");
        let b2 = std::fs::read(&json2).expect("read json2");
        if b1 != b2 {
            let s1 = String::from_utf8_lossy(&b1).into_owned();
            let s2 = String::from_utf8_lossy(&b2).into_owned();
            failures.push(format!(
                "{}: CLI parse|render|parse non-idempotent.\n  v1: {s1}\n  v2: {s2}",
                ail_path.display(),
            ));
        }
        checked += 1;
    }

    if !failures.is_empty() {
        panic!(
            "CLI parse|render|parse non-idempotent on {} of {} fixtures:\n{}",
            failures.len(),
            checked,
            failures.join("\n\n")
        );
    }
    assert!(checked > 0, "no .ail fixtures found");
    eprintln!("CLI parse|render|parse idempotent for {checked} fixtures");
}
