//! RED-pin for the per-iteration heap loop-binder leak across `recur`
//! (bug #49).
//!
//! Property protected: under `--alloc=rc`, when a `(loop ...)` carries a
//! heap-valued loop binder (here a boxed-ADT `Cell` accumulator) and a
//! `recur` REPLACES that binder with a fresh heap value, the superseded
//! prior value MUST be RC-decremented at the `recur` store. The runtime
//! RC stats line at exit must report `live == 0` (equivalently
//! `allocs == frees`) even though stdout is already correct.
//!
//! Root cause: codegen's `Term::Recur` arm in
//! `crates/ailang-codegen/src/lib.rs` (~line 2126) stores each new arg
//! value into the binder alloca with a plain `store` and never emits an
//! RC-dec for the heap value already in that slot. Each `recur` over a
//! heap binder therefore leaks the value it overwrites. This is DISTINCT
//! from the scope-close drop path in `crates/ailang-codegen/src/drop.rs`
//! (commit 8bcdae1), which drops only the loop's FINAL result — the
//! intermediate per-iteration values it never sees.
//!
//! A boxed ADT (not a `Str`) is the deliberate trigger: every ADT value
//! is heap-allocated, so dec'ing the superseded value is unambiguously
//! UB-free. `Str` literals lower to static globals, so a `Str` loop seed
//! must NOT be dec'd; the Str accumulator case is the separate
//! static-vs-heap-Str representation question (the obstacle behind the
//! deliberate Str exclusion in commit 8bcdae1), out of scope for this pin.
//!
//! The fixture threads a `Cell` through three `recur`s. As of HEAD this
//! test fails: stderr reports `ailang_rc_stats: allocs=4 frees=1 live=3`
//! — the three superseded `Cell` slabs leak. RawBuf loop binders do NOT
//! exhibit this leak because `RawBuf.set` mutates in place (own->own, no
//! per-iteration alloc), so their binder is never replaced; the trigger
//! requires a binder that a `recur` rebinds to a *fresh* heap allocation.

use std::path::Path;
use std::process::Command;

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

#[test]
fn alloc_rc_heap_loop_binder_replaced_by_recur_does_not_leak() {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
    let src = workspace
        .join("examples")
        .join("loop_recur_heap_binder_no_leak_pin.ail");
    let tmp = std::env::temp_dir().join(format!(
        "ailang_loop_recur_heap_binder_no_leak_pin_{}",
        std::process::id()
    ));
    std::fs::create_dir_all(&tmp).unwrap();
    let out = tmp.join("bin");
    let status = Command::new(ail_bin())
        .args(["build", src.to_str().unwrap(), "--alloc=rc", "-o"])
        .arg(&out)
        .status()
        .expect("ail build failed to run");
    assert!(
        status.success(),
        "ail build --alloc=rc failed for loop_recur_heap_binder_no_leak_pin.ail"
    );
    let output = Command::new(&out)
        .env("AILANG_RC_STATS", "1")
        .output()
        .expect("execute binary");
    assert!(
        output.status.success(),
        "binary exited non-zero: status {:?}",
        output.status
    );
    let stderr = String::from_utf8(output.stderr).expect("stderr utf8");
    let stats_line = stderr
        .lines()
        .find(|l| l.starts_with("ailang_rc_stats:"))
        .unwrap_or_else(|| {
            panic!("missing ailang_rc_stats line in stderr; stderr was:\n{stderr}")
        });
    let mut allocs: Option<u64> = None;
    let mut frees: Option<u64> = None;
    let mut live: Option<i64> = None;
    for tok in stats_line.split_whitespace() {
        if let Some(v) = tok.strip_prefix("allocs=") {
            allocs = v.parse().ok();
        } else if let Some(v) = tok.strip_prefix("frees=") {
            frees = v.parse().ok();
        } else if let Some(v) = tok.strip_prefix("live=") {
            live = v.parse().ok();
        }
    }
    let allocs = allocs.expect("missing allocs= field");
    let frees = frees.expect("missing frees= field");
    let live = live.expect("missing live= field");
    assert_eq!(
        live, 0,
        "a heap (boxed-ADT) loop binder replaced by `recur` leaks {live} \
         superseded slab(s) (allocs={allocs} frees={frees}); the \
         Term::Recur arm in crates/ailang-codegen/src/lib.rs (~line \
         2126) stores the new binder value without RC-dec'ing the prior \
         heap value it overwrites."
    );
    assert_eq!(
        allocs, frees,
        "alloc/free mismatch (allocs={allocs} frees={frees} live={live})"
    );
}
