//! RED-pin for the loop-valued let-binder scope-close drop gap
//! (surfaced by the raw-buf fieldtest, refs #7).
//!
//! Property protected: under `--alloc=rc`, an owned `(RawBuf Int)`
//! whose `let`-binding value is a `(loop ...)` result, and which is
//! afterwards only borrow-read (`RawBuf.get`) — never consumed by an
//! `own`-taking callee — MUST be dropped at let-scope-close. The
//! runtime RC stats line at exit must report `live == 0`
//! (equivalently `allocs == frees`).
//!
//! Root cause: codegen's `is_rc_heap_allocated`
//! (`crates/ailang-codegen/src/drop.rs`) — the predicate the
//! `Term::Let` lowering uses to decide whether a let-binder owns a
//! fresh RC-heap allocation to `dec` at scope close — has no
//! `Term::Loop` arm. A loop-valued let-value falls through to
//! `_ => false`, so the binder is never flagged trackable and
//! `drop_symbol_for_binder` is never called for it. Compare the
//! direct-`(new ...)`-value path (`examples/raw_buf_drop_min.ail`)
//! which IS dropped, and the Own-returning `Term::App` arm which is.
//!
//! As of HEAD this test fails: stderr reports
//! `ailang_rc_stats: allocs=2 frees=1 live=1`. The owned RawBuf slab
//! threaded through the loop binder leaks.
//!
//! Scope note: this pins ONLY the scope-close drop gap. A distinct,
//! broader per-iteration recur-dec leak (superseded heap loop-binder
//! values not RC-dec'd inside the loop body) is filed separately and
//! is NOT covered here. RawBuf manifests only the scope-close gap
//! because `RawBuf.set` mutates in place (no per-iteration alloc).

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

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

#[test]
fn alloc_rc_loop_valued_rawbuf_binder_drops_at_scope_close() {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
    let src = workspace
        .join("examples")
        .join("raw_buf_loop_no_leak_pin.ail");
    let tmp = std::env::temp_dir().join(format!(
        "ailang_raw_buf_loop_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 raw_buf_loop_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,
        "an owned (RawBuf Int) bound to a (loop ...) result and only \
         borrow-read leaks {live} slab cell(s) (allocs={allocs} \
         frees={frees}); the loop-valued let-binder must drop at \
         scope close. `is_rc_heap_allocated` in \
         crates/ailang-codegen/src/drop.rs has no Term::Loop arm, so \
         the binder is never flagged trackable."
    );
    assert_eq!(
        allocs, frees,
        "alloc/free mismatch (allocs={allocs} frees={frees} live={live})"
    );
}
