//! IR-shape pin for milestone 24.3's `print` polymorphic free fn.
//!
//! Property protected: the post-mono `print__Int` `Def::Fn.body` is
//! structurally `Term::Let { name: "s", value: App(show__Int, [x]),
//! body: Term::Do { op: "io/print_str", args: [s] } }`. The explicit
//! let-binder around `show__Int x` is load-bearing for the heap-Str RC
//! discipline (the heap-Str Str carve-out at
//! `drop_symbol_for_binder` requires
//! a let-binder to attach the rc-dec to). If a future codegen / mono
//! refactor inlines the let-binder away, this pin fires and surfaces
//! the regression BEFORE the E2E runtime stats produce a confusing
//! "memory leak" diagnostic.
//!
//! Note: `FnDef.body: Term` is the *inner* function body — top-level
//! fns carry their parameter list separately on `FnDef.params`, so no
//! outer `Term::Lam` wraps the body. This contrasts with instance
//! method bodies (which carry a `Term::Lam` because they are anonymous
//! function values inside `InstanceMethod.body`).

use ailang_core::ast::{Def, Term};
use ailang_surface::load_workspace;
use std::path::PathBuf;

fn fixture_path() -> PathBuf {
    let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    d.pop();
    d.pop();
    d.join("examples").join("show_print_smoke.ail")
}

#[test]
fn print_int_body_preserves_explicit_let_binder() {
    let ws = load_workspace(&fixture_path()).expect("workspace loads");
    let diags = ailang_check::check_workspace(&ws);
    assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
    let post_mono = ailang_check::monomorphise_workspace(&ws).expect("mono green");

    let prelude_mod = post_mono
        .modules
        .get("prelude")
        .expect("prelude module present");

    let print_int = prelude_mod
        .defs
        .iter()
        .find_map(|d| match d {
            Def::Fn(f) if f.name == "print__Int" => Some(f),
            _ => None,
        })
        .expect("print__Int mono symbol not found in prelude module");

    // Body shape: Term::Let { name: "s",
    //   value: Term::App(show__Int, [x]),
    //   body: Term::Do { op: "io/print_str", args: [s] } }
    let (let_value, let_body) = match &print_int.body {
        Term::Let { name, value, body } => {
            assert_eq!(name, "s", "let-binder name expected 's', got {name:?}");
            (value.as_ref(), body.as_ref())
        }
        other => panic!(
            "print__Int.body is not Term::Let — let-binder was optimised away or never inserted. Got: {other:?}"
        ),
    };

    // Inner App should reference show__Int (post-mono symbol).
    let app_fn = match let_value {
        Term::App { callee, .. } => callee.as_ref(),
        other => panic!("print__Int let-value is not Term::App, got: {other:?}"),
    };
    let app_fn_name = match app_fn {
        Term::Var { name } => name,
        other => panic!("let-value's fn position is not Term::Var, got: {other:?}"),
    };
    assert_eq!(
        app_fn_name, "show__Int",
        "post-mono let-value should call show__Int, got: {app_fn_name}"
    );

    // Inner body should be a Do invoking io/print_str.
    match let_body {
        Term::Do { op, args, .. } => {
            assert_eq!(op, "io/print_str", "let-body Do op expected 'io/print_str', got {op:?}");
            assert_eq!(args.len(), 1, "io/print_str expects 1 arg, got {}", args.len());
        }
        other => panic!("print__Int let-body is not Term::Do, got: {other:?}"),
    }
}
