//! RED for `bugfix-swarm-rc-alloc-undercount`.
//!
//! Named property protected: the `libailang_rt.a` archive that
//! `ail-embed` links (emitted by `build.rs` via
//! `ail build --emit=staticlib`) MUST be regenerated whenever
//! `runtime/rc.c` changes, so the global RC stats counter increment
//! in the linked binary is the atomic (`lock`-prefixed) form that
//! commit `7bfa11e` introduced. If the build does not track
//! `runtime/rc.c` as an input, a stale pre-`7bfa11e` archive is
//! linked whose `g_rc_alloc_count++` is the original non-atomic
//! racing increment — the exact swarm alloc-undercount
//! (`Σfrees` stable-exact, `Σallocs` short by a jittering
//! ~1600–57000 across runs) re-surfaces, frozen into a stale
//! build artefact even though `runtime/rc.c` itself is correct.
//!
//! Why this shape and not a swarm re-run: the symptom (jittering
//! `Σallocs`) is non-deterministic by construction (it depends on
//! thread interleaving on the racing `incq`). The *cause* is fully
//! deterministic: the linked archive's `ailang_rc_alloc` either has
//! the `lock`-prefixed atomic global increment (correct, from the
//! current `runtime/rc.c`) or a plain non-atomic `incq` (stale
//! pre-`7bfa11e`). This RED pins the cause directly: it asserts the
//! archive `build.rs` produced for THIS crate is built from a
//! `runtime/rc.c` whose `g_rc_alloc_count` increment is atomic.
//!
//! Not a methodology unsoundness: with the correct (current)
//! `runtime/rc.c` linked, the swarm's Σ-over-stat-lines balances
//! deterministically (`Σallocs == Σfrees == 12000003`, every run).
//! The Σ invariant is sound; only the *stale-artefact build gap*
//! breaks it. The GREEN fix is a build-dependency completeness fix
//! (`ail-embed/build.rs` must declare `runtime/rc.c` — and the rest
//! of the staticlib's runtime inputs — as `rerun-if-changed`), NOT
//! a runtime change: `runtime/rc.c` at HEAD is already correct.

use std::path::{Path, PathBuf};
use std::process::Command;

/// `<repo>` — the AILang workspace root, parent of this crate.
fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("ail-embed has a parent (the AILang repo root)")
        .to_path_buf()
}

/// The `libailang_rt.a` that `build.rs` emitted into this crate's
/// `OUT_DIR` and that `swarm_runner` / `lib.rs` link. There is exactly
/// one under `ail-embed/target/**/build/ail-embed-*/out/`.
fn linked_runtime_archive() -> PathBuf {
    let target = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target");
    let mut found: Vec<PathBuf> = Vec::new();
    fn walk(dir: &Path, acc: &mut Vec<PathBuf>) {
        let Ok(rd) = std::fs::read_dir(dir) else { return };
        for e in rd.flatten() {
            let p = e.path();
            if p.is_dir() {
                walk(&p, acc);
            } else if p.file_name().and_then(|s| s.to_str())
                == Some("libailang_rt.a")
            {
                acc.push(p);
            }
        }
    }
    walk(&target, &mut found);
    assert!(
        !found.is_empty(),
        "no libailang_rt.a under {} — build.rs must have emitted one \
         (build the crate first: it links this archive)",
        target.display()
    );
    // Newest by mtime — the one the current build links.
    found.sort_by_key(|p| {
        std::fs::metadata(p)
            .and_then(|m| m.modified())
            .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
    });
    found.pop().unwrap()
}

/// The linked runtime archive must be built from the current
/// `runtime/rc.c`, in which the global `g_rc_alloc_count` increment is
/// atomic. On x86-64 a relaxed `atomic_fetch_add_explicit` lowers to a
/// `lock`-prefixed RMW; the pre-`7bfa11e` non-atomic `g_rc_*count++`
/// emits none. A stale archive (build did not track `runtime/rc.c`)
/// therefore links zero `lock` instructions in `ailang_rc_alloc` and
/// re-introduces the swarm alloc-undercount race.
#[test]
fn linked_runtime_archive_has_atomic_global_rc_counters() {
    let archive = linked_runtime_archive();

    // Disassemble; count `lock`-prefixed instructions. The current
    // runtime emits exactly two (`lock incq` for g_rc_alloc_count in
    // ailang_rc_alloc and for g_rc_free_count in ailang_rc_dec). The
    // stale pre-7bfa11e archive emits zero (plain `incq`, or a
    // cmov-fused plain `incq`).
    let out = Command::new("objdump")
        .args(["-d", "--no-show-raw-insn"])
        .arg(&archive)
        .output()
        .expect("run objdump on libailang_rt.a");
    assert!(
        out.status.success(),
        "objdump failed on {}:\n{}",
        archive.display(),
        String::from_utf8_lossy(&out.stderr)
    );
    let disasm = String::from_utf8_lossy(&out.stdout);
    let lock_insns = disasm
        .lines()
        .filter(|l| {
            // Match the mnemonic column, not the symbol name / path.
            l.split_once('\t')
                .map(|(_, ins)| ins.trim_start().starts_with("lock "))
                .unwrap_or(false)
        })
        .count();

    assert!(
        lock_insns >= 2,
        "linked runtime archive `{}` has {lock_insns} lock-prefixed \
         instruction(s); the current runtime/rc.c emits an atomic \
         (lock-prefixed) global RC counter increment in both \
         ailang_rc_alloc and ailang_rc_dec (>=2 expected). Zero/one \
         means a STALE pre-7bfa11e archive is linked: build.rs did not \
         track runtime/rc.c as an input, so the swarm links the old \
         non-atomic g_rc_alloc_count++ and the alloc-undercount race \
         re-surfaces (Sfrees stable-exact, Sallocs short & jittering).",
        archive.display()
    );

    // Cross-check: a freshly-built archive from the SAME current
    // runtime/rc.c, via the same `ail build --emit=staticlib` path
    // build.rs uses, is atomic. If this differs from the linked one,
    // the linked one is provably stale.
    let outdir = std::env::temp_dir().join(format!(
        "ail-embed-rt-fresh-{}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&outdir);
    std::fs::create_dir_all(&outdir).unwrap();
    let repo = repo_root();
    let ail_bin = repo.join("target/debug/ail");
    assert!(
        ail_bin.exists(),
        "expected the `ail` CLI at {} (build.rs builds it; run \
         `cargo build -p ail` in the workspace first)",
        ail_bin.display()
    );
    let st = Command::new(&ail_bin)
        .arg("build")
        .arg(repo.join("examples/embed_backtest_step_tick.ail"))
        .args(["--emit=staticlib", "-o"])
        .arg(&outdir)
        .output()
        .expect("ail build --emit=staticlib (fresh runtime archive)");
    assert!(
        st.status.success(),
        "fresh `ail build --emit=staticlib` failed:\n{}",
        String::from_utf8_lossy(&st.stderr)
    );
    let fresh = outdir.join("libailang_rt.a");
    let fresh_disasm = Command::new("objdump")
        .args(["-d", "--no-show-raw-insn"])
        .arg(&fresh)
        .output()
        .expect("objdump fresh archive");
    let fresh_lock = String::from_utf8_lossy(&fresh_disasm.stdout)
        .lines()
        .filter(|l| {
            l.split_once('\t')
                .map(|(_, ins)| ins.trim_start().starts_with("lock "))
                .unwrap_or(false)
        })
        .count();
    let _ = std::fs::remove_dir_all(&outdir);
    assert_eq!(
        lock_insns, fresh_lock,
        "linked archive ({lock_insns} lock insns) disagrees with a \
         freshly-built archive from the current runtime/rc.c \
         ({fresh_lock} lock insns) — the linked archive is STALE: \
         ail-embed/build.rs did not regenerate libailang_rt.a after \
         runtime/rc.c changed (missing rerun-if-changed on the \
         staticlib runtime inputs)."
    );
}
