//! RED for `bugfix-rc-global-stats-race`.
//!
//! Named property protected: under a multi-threaded host that uses the
//! NULL-ctx GLOBAL RC-stats fallback path (`__ail_tls_ctx == NULL` ->
//! `g_rc_alloc_count` / `g_rc_free_count`, runtime/rc.c:90-91,161,212),
//! the `AILANG_RC_STATS` atexit Σ is EXACT: it counts every
//! `ailang_rc_alloc` and every to-zero `ailang_rc_dec`, with no lost
//! increments. The two global counters are plain `static uint64_t`
//! incremented by a non-atomic `++`; concurrent workers race on the
//! read-modify-write and silently drop updates, so the printed total
//! under-counts non-deterministically (M5 iter 2's symbol-fan swarm:
//! allocs jitter 5993321..5999318, never the true 6_000_000).
//!
//! This is NOT a memory bug: no box crosses a thread (alloc + dec in
//! the same loop body), the per-object refcount header op is correct,
//! and the program is bit-exact every run. The ONLY defect is the
//! under-counted statistics Σ — exactly the deferred atomic-counter
//! decision rc.c's own header (rc.c:44-49,85) names, with M5 the first
//! concurrent consumer.
//!
//! The existing per-ctx swarm/accounting harnesses
//! (embed_swarm_tsan.rs, embed_rc_accounting_tsan.rs) always set
//! `__ail_tls_ctx = ctx`, so they exercise the per-thread (zero-
//! contention) counters and never the global fallback — that is why
//! the suite is green while this bug ships.
//!
//! Build/link mechanism mirrors `embed_swarm_tsan.rs`: a staticlib
//! build of any kernel emits `libailang_rt.a`; the C host links the
//! runtime archive directly and never calls a kernel.

use std::process::Command;

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

/// 8 threads * 2_000_000 cycles — must match the `#define`s in
/// `crates/ail/tests/embed/rc_global_stats_race.c`.
const NTHREADS: u64 = 8;
const NCYCLES: u64 = 2_000_000;
const EXPECTED: u64 = NTHREADS * NCYCLES;

#[test]
fn global_rc_stats_counters_are_exact_under_thread_contention() {
    let outdir = std::env::temp_dir().join(format!(
        "ail-rc-global-stats-race-{}", std::process::id()));
    std::fs::create_dir_all(&outdir).unwrap();
    let root = ailang_test_support::canonical_workspace_root();

    // Any kernel staticlib build emits libailang_rt.a alongside it.
    let st = Command::new(ail_bin())
        .args(["build", "examples/embed_backtest_step_tick.ail",
               "--emit=staticlib", "--alloc=rc",
               "-o", outdir.to_str().unwrap()])
        .current_dir(&root).status().expect("ail build");
    assert!(st.success(), "ail build --emit=staticlib failed");

    let rt_archive = outdir.join("libailang_rt.a");
    assert!(rt_archive.exists(), "libailang_rt.a not emitted");

    let host_c = root.join("crates/ail/tests/embed/rc_global_stats_race.c");
    let bin = outdir.join("rc_global_stats_race");
    let st = Command::new("cc")
        .args([
            "-O2", "-pthread",
            host_c.to_str().unwrap(),
            rt_archive.to_str().unwrap(),
            "-o", bin.to_str().unwrap(),
        ])
        .status().expect("cc");
    assert!(st.success(), "linking rc_global_stats_race.c failed");

    let out = Command::new(&bin)
        .env("AILANG_RC_STATS", "1")
        .output().expect("run rc_global_stats_race");
    assert!(out.status.success(),
        "host must exit 0; stderr:\n{}",
        String::from_utf8_lossy(&out.stderr));

    let stderr = String::from_utf8_lossy(&out.stderr);
    let line = stderr.lines()
        .find(|l| l.starts_with("ailang_rc_stats:"))
        .unwrap_or_else(|| panic!(
            "missing ailang_rc_stats line; stderr was:\n{stderr}"));

    let mut allocs: Option<u64> = None;
    let mut frees: Option<u64> = None;
    for tok in line.split_whitespace() {
        if let Some(v) = tok.strip_prefix("allocs=") {
            allocs = Some(v.parse().expect("allocs= u64"));
        } else if let Some(v) = tok.strip_prefix("frees=") {
            frees = Some(v.parse().expect("frees= u64"));
        }
    }
    let allocs = allocs.expect("allocs= present");
    let frees = frees.expect("frees= present");

    // Pre-fix: the non-atomic `g_rc_*count++` loses updates under
    // contention, so allocs/frees are < EXPECTED and jitter run-to-run.
    // Post-fix (atomic global counters): exact equality, deterministic.
    assert_eq!(allocs, EXPECTED,
        "global RC alloc counter must count every ailang_rc_alloc on \
         the NULL-ctx fallback path with no lost increments \
         (allocs={allocs}, expected {EXPECTED}); line: {line:?}");
    assert_eq!(frees, EXPECTED,
        "global RC free counter must count every to-zero ailang_rc_dec \
         on the NULL-ctx fallback path with no lost increments \
         (frees={frees}, expected {EXPECTED}); line: {line:?}");
}
