//! RED-pin for leg (C) of the drop-soundness bug family the
//! Implicit-cutover (#55) surfaced: a polymorphic ctor's per-type
//! drop fn rc_dec's a type-parameter field even when the field is
//! monomorphised at a value type (Int/Bool/Float/Unit).
//!
//! Property protected: under `--alloc=rc`, dropping a value of a
//! polymorphic ADT whose ctor field is monomorphised at a value type
//! must NOT emit `ailang_rc_dec` on that field. The field is stored
//! inline as a raw scalar (an i64 `7`, not a heap pointer), so an
//! `rc_dec` dereferences the scalar-as-pointer and crashes. The
//! binary must run to completion, print the inner value, and the
//! runtime RC stats line must report `live == 0` (the single ADT
//! slab is freed exactly once, with no spurious dec of the inline
//! value field).
//!
//! Root cause: `crates/ailang-codegen/src/drop.rs`
//! `emit_drop_fn_for_type` (mirrored in `emit_iterative_drop_fn_for_type`
//! and `emit_partial_drop_fn_for_type`) emits the per-type drop fn ONCE
//! from the polymorphic `Def::Type` declaration — never per-monomorph.
//! For each ctor field it computes `llvm_type(fty)` where `fty` is the
//! declared field type. For a type-parameter field that type is a
//! `Type::Var`, which `llvm_type` rejects (synth.rs returns `Err`); the
//! `.unwrap_or_else(|_| "ptr".into())` at the field loop then silently
//! treats the var-typed field as a boxed pointer and emits `rc_dec`.
//! At the `Box Int` monomorph the field is an inline `i64`, so the
//! emitted `rc_dec` dereferences the integer value -> SIGSEGV.
//!
//! As of HEAD (the in-flight Implicit cutover) this test fails: the
//! binary exits 139 (SIGSEGV) before printing anything. Pre-cutover
//! the bug was latent — Implicit scrutinees skipped the drop path
//! entirely, so the mistyped drop never ran (it leaked instead of
//! crashing).
//!
//! A single-field `Box a` at `Int` is the minimal trigger. The fix
//! must still dec genuinely-heap fields (e.g. a `Box Str` field must
//! free its slab exactly once) and must hold for the partial-drop and
//! nested-monomorph variants — see the debugger handoff for the fix
//! locus and soundness reasoning.

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

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

#[test]
fn alloc_rc_value_type_field_is_not_rc_dec_dropped() {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
    let src = workspace
        .join("examples")
        .join("drop_value_field_no_segfault_pin.ail");
    let tmp = std::env::temp_dir().join(format!(
        "ailang_drop_value_field_no_segfault_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 drop_value_field_no_segfault_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 {:?}): the polymorphic drop fn \
         drop_<m>_Box rc_dec's the type-parameter field, which at the \
         Box Int monomorph is the inline i64 `7` (not a heap pointer), \
         so rc_dec(7) dereferences the scalar-as-pointer and SIGSEGVs. \
         Fix in crates/ailang-codegen/src/drop.rs: do not dec a field \
         whose monomorphised type is a value type.",
        output.status
    );
    let stdout = String::from_utf8(output.stdout).expect("stdout utf8");
    assert_eq!(
        stdout.trim_end(),
        "7",
        "unboxing MkBox 7 at Int must print 7; got {stdout:?}"
    );
    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,
        "RC imbalance after dropping a value-type-field ADT \
         (allocs={allocs} frees={frees} live={live}); the Box slab \
         must be freed exactly once and the inline Int field must \
         never be dec'd."
    );
    assert_eq!(
        allocs, frees,
        "alloc/free mismatch (allocs={allocs} frees={frees} live={live})"
    );
}
