//! Pin tests that the loop/recur typecheck fixtures produce the
//! expected diagnostic codes (or zero diagnostics for the positive
//! `sum_to`-class case). Mirrors `mut_typecheck_pin.rs`; negative
//! fixtures live as `.ail.json` so the diagnostic code is the
//! load-bearing assertion, not the surface form.

use ailang_check::check_workspace;
use ailang_surface::load_workspace;
use ailang_test_support::examples_dir;

/// Load the named fixture under `examples/`, run check_workspace, and
/// return the diagnostic code list (one entry per Diagnostic).
fn check_fixture(fixture_name: &str) -> Vec<String> {
    let path = examples_dir().join(fixture_name);
    let ws = load_workspace(&path)
        .unwrap_or_else(|e| panic!("workspace `{fixture_name}` must load: {e:?}"));
    let diags = check_workspace(&ws);
    diags.iter().map(|d| d.code.clone()).collect()
}

#[test]
fn loop_sum_to_typechecks_clean() {
    // `examples/loop_sum_to.ail` doubles as the round-trip fixture and
    // the positive typecheck pin: it must typecheck without any
    // diagnostic. One fixture, two properties — no near-duplicate
    // corpus file.
    let codes = check_fixture("loop_sum_to.ail");
    assert!(codes.is_empty(), "expected zero diagnostics, got {codes:?}");
}

#[test]
fn recur_not_in_tail_position_emits_recur_not_in_tail_position() {
    let codes = check_fixture("test_recur_not_in_tail_position.ail.json");
    assert_eq!(codes, vec!["recur-not-in-tail-position".to_string()]);
}

#[test]
fn recur_outside_loop_emits_recur_outside_loop() {
    let codes = check_fixture("test_recur_outside_loop.ail.json");
    assert_eq!(codes, vec!["recur-outside-loop".to_string()]);
}

#[test]
fn recur_arity_mismatch_emits_recur_arity_mismatch() {
    let codes = check_fixture("test_recur_arity_mismatch.ail.json");
    assert_eq!(codes, vec!["recur-arity-mismatch".to_string()]);
}

#[test]
fn recur_type_mismatch_emits_recur_type_mismatch() {
    let codes = check_fixture("test_recur_type_mismatch.ail.json");
    assert_eq!(codes, vec!["recur-type-mismatch".to_string()]);
}

#[test]
fn infinite_loop_typechecks_clean_no_termination_claim() {
    let codes = check_fixture("loop_forever.ail");
    assert!(codes.is_empty(), "infinite loop must typecheck (no totality claim), got {codes:?}");
}

/// Property: a `Term::Lam` whose body captures a `Term::Loop` binder
/// is rejected at `ail check` with the dedicated
/// `loop-binder-captured-by-lambda` diagnostic. Loop binders are
/// alloca-resident and cannot escape their loop frame (codegen
/// registers them in the `binder_allocas` registry,
/// `std::mem::take`-emptied at the lambda boundary); the `Term::Lam`
/// escape guard rejects the capture at typecheck rather than letting
/// codegen panic on type-correct input.
#[test]
fn lambda_capturing_loop_binder_emits_loop_binder_captured_by_lambda() {
    let codes = check_fixture("test_loop_binder_captured_by_lambda.ail.json");
    assert_eq!(codes, vec!["loop-binder-captured-by-lambda".to_string()]);
}
