//! RED-pin for the 2026-05-14 Cat-A heap-Str leak in `print`
//! (uncovered during the per-type-print-op retirement).
//!
//! Property protected: under `--alloc=rc`, evaluating the trivial
//! program `(body (app print 42))` does NOT leak the heap-Str
//! allocated inside `print`'s body. `print`'s body is the explicit-
//! let `let s = show x in do io/print_str s` (see iter 24.3, pinned
//! structurally by `print_mono_body_shape.rs`); `show` at primitive
//! type calls `int_to_str` which returns a fresh heap-Str (slab in
//! `runtime/str.c`). `io/print_str` borrows the Str. At let-scope-
//! close, the binder `s` is the only owner — codegen must emit
//! `ailang_rc_dec(s)`. The runtime stats line at exit must report
//! `live == 0` (equivalently `allocs == frees`).
//!
//! Codegen's `is_rc_heap_allocated` App-arm reads the callee's
//! `Type::Fn.ret_mode` via `synth_callee_ret_mode` and tracks the
//! let-binder iff it is `Own`. The mono'ed `show__Int` inherits its
//! `Type::Fn` from the `Show` class method declaration in
//! `examples/prelude.ail.json`, whose `show` carries `ret_mode: own`
//! (every fn-type slot is moded — spec 0062). So `show__Int` returns
//! `Own`, the let-binder for the show-result is trackable, and
//! `drop_symbol_for_binder` emits the scope-close dec — `live == 0`.

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

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

#[test]
fn alloc_rc_print_int_does_not_leak_show_result_str() {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
    let src = workspace
        .join("examples")
        .join("print_int_no_leak_pin.ail");
    let tmp = std::env::temp_dir().join(format!(
        "ailang_print_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 print_int_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,
        "`(app print 42)` under --alloc=rc leaks {live} heap-Str cell(s) \
         (allocs={allocs} frees={frees}); print's let-binder for the \
         show-result must drop at let-scope-close. The Show class method \
         `show` carries `ret_mode: own`, so mono-synthesised `show__Int` \
         is `Own`-returning and `is_rc_heap_allocated` tracks the binder."
    );
    assert_eq!(
        allocs, frees,
        "alloc/free mismatch (allocs={allocs} frees={frees} live={live})"
    );
}
