//! RED-pin for leg 3 of bug #63 — the per-branch-vs-per-fn consume-
//! accounting leak at an `if`-branch fall-through (the `if` half of the
//! branch-consume-split class; the `match` half is
//! match_arm_consume_split_no_leak_pin.rs).
//!
//! Property protected: under `--alloc=rc`, when an `(own ...)` heap
//! param is consumed on ONE `if` branch but is LIVE on the sibling
//! branch, it MUST be dropped on the live branch before the fn returns.
//! `run_if b 0` takes the `then` branch (`ge 0 0` is true) where `b` is
//! live; the `else` branch (`sink b`) consumes it. As of HEAD this fails:
//! the binary reports `live=1` because the per-fn aggregate consume of
//! `b` is `>= 1` (the `else` branch consumes it) so the fn-return dec
//! skips `b` and nothing drops it on the `then` path.
//!
//! `if` is a first-class `MTerm::If` (it does NOT desugar to `match`),
//! so it needs the per-branch drop in its own codegen node. Goes green
//! with the #63-leg-3 fix.

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

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

#[test]
fn alloc_rc_owned_param_consumed_on_one_if_branch_live_on_sibling_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("if_branch_consume_split_no_leak_pin.ail");
    let tmp = std::env::temp_dir().join(format!(
        "ailang_if_branch_consume_split_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 if_branch_consume_split_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 (own) heap param consumed on one if branch but live on the \
         sibling branch leaks {live} slab(s) (allocs={allocs} frees={frees}): \
         the per-branch live param is never dropped because the fn-return \
         dec gates on the per-fn aggregate consume (>= 1 from the consuming \
         branch) instead of the per-branch consume."
    );
    assert_eq!(
        allocs, frees,
        "alloc/free mismatch (allocs={allocs} frees={frees} live={live})"
    );
}
