//! Schema-coverage audit: every AST enum variant of `Def`, `Term`,
//! `Pattern`, `Literal`, `Type`, `ParamMode` is exercised by at
//! least one fixture under `examples/`.
//!
//! Why this exists: the round-trip tests in `ailang-surface` are
//! only as strong as the fixture corpus that drives them. A schema
//! variant with zero fixture coverage round-trips trivially — there
//! is nothing to round-trip. This test makes the corpus's coverage
//! observable, so a gap surfaces as a fail-loud test, not as silent
//! confidence.
//!
//! How it stays in sync with the AST: every match arm below is
//! exhaustive (no `_ => ...` wildcard). When a new AST variant
//! lands, the compiler rejects this file until the visitor and the
//! `VariantTag` enum are extended in lockstep.
//!
//! Pure reader: this test loads fixtures but does not modify any
//! committed content. A failure means a fixture must be added (in
//! a separate iteration); the test must not be relaxed.

use std::collections::HashSet;

use ailang_test_support::{examples_dir, list_ail_fixtures};

use ailang_core::ast::{
    Def, InstanceMethod, Literal, Module, NewArg, ParamMode, Pattern, Term, Type,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum VariantTag {
    // Def
    DefFn,
    DefConst,
    DefType,
    DefClass,
    DefInstance,
    // Term
    TermLit,
    TermVar,
    TermApp,
    TermLet,
    TermLetRec,
    TermIf,
    TermDo,
    TermCtor,
    TermMatch,
    TermLam,
    TermSeq,
    TermClone,
    TermReuseAs,
    TermLoop,
    TermRecur,
    TermNew,
    TermIntrinsic,
    // Pattern
    PatternWild,
    PatternVar,
    PatternLit,
    PatternCtor,
    // Literal
    LiteralInt,
    LiteralBool,
    LiteralStr,
    LiteralUnit,
    LiteralFloat,
    // Type
    TypeCon,
    TypeFn,
    TypeVar,
    TypeForall,
    // ParamMode
    ParamModeOwn,
    ParamModeBorrow,
}

/// Lists every expected variant. Stored as a const array so that
/// adding a new `VariantTag` here and forgetting to add the
/// corresponding match arm in the visitor is a Rust compile error
/// (and vice versa).
const EXPECTED_VARIANTS: &[VariantTag] = &[
    VariantTag::DefFn,
    VariantTag::DefConst,
    VariantTag::DefType,
    VariantTag::DefClass,
    VariantTag::DefInstance,
    VariantTag::TermLit,
    VariantTag::TermVar,
    VariantTag::TermApp,
    VariantTag::TermLet,
    VariantTag::TermLetRec,
    VariantTag::TermIf,
    VariantTag::TermDo,
    VariantTag::TermCtor,
    VariantTag::TermMatch,
    VariantTag::TermLam,
    VariantTag::TermSeq,
    VariantTag::TermClone,
    VariantTag::TermReuseAs,
    VariantTag::TermLoop,
    VariantTag::TermRecur,
    VariantTag::TermNew,
    VariantTag::TermIntrinsic,
    VariantTag::PatternWild,
    VariantTag::PatternVar,
    VariantTag::PatternLit,
    VariantTag::PatternCtor,
    VariantTag::LiteralInt,
    VariantTag::LiteralBool,
    VariantTag::LiteralStr,
    VariantTag::LiteralUnit,
    VariantTag::LiteralFloat,
    VariantTag::TypeCon,
    VariantTag::TypeFn,
    VariantTag::TypeVar,
    VariantTag::TypeForall,
    VariantTag::ParamModeOwn,
    VariantTag::ParamModeBorrow,
];

fn visit_def(def: &Def, observed: &mut HashSet<VariantTag>) {
    match def {
        Def::Fn(fd) => {
            observed.insert(VariantTag::DefFn);
            visit_type(&fd.ty, observed);
            visit_term(&fd.body, observed);
        }
        Def::Const(cd) => {
            observed.insert(VariantTag::DefConst);
            visit_type(&cd.ty, observed);
            visit_term(&cd.value, observed);
        }
        Def::Type(td) => {
            observed.insert(VariantTag::DefType);
            for ctor in &td.ctors {
                for field_ty in &ctor.fields {
                    visit_type(field_ty, observed);
                }
            }
        }
        Def::Class(cd) => {
            observed.insert(VariantTag::DefClass);
            for m in &cd.methods {
                visit_type(&m.ty, observed);
                if let Some(body) = &m.default {
                    visit_term(body, observed);
                }
            }
        }
        Def::Instance(id) => {
            observed.insert(VariantTag::DefInstance);
            visit_type(&id.type_, observed);
            for m in &id.methods {
                visit_instance_method(m, observed);
            }
        }
    }
}

fn visit_instance_method(m: &InstanceMethod, observed: &mut HashSet<VariantTag>) {
    visit_term(&m.body, observed);
}

fn visit_term(t: &Term, observed: &mut HashSet<VariantTag>) {
    match t {
        Term::Lit { lit } => {
            observed.insert(VariantTag::TermLit);
            visit_literal(lit, observed);
        }
        Term::Var { .. } => {
            observed.insert(VariantTag::TermVar);
        }
        Term::App { callee, args, .. } => {
            observed.insert(VariantTag::TermApp);
            visit_term(callee, observed);
            for a in args {
                visit_term(a, observed);
            }
        }
        Term::Let { value, body, .. } => {
            observed.insert(VariantTag::TermLet);
            visit_term(value, observed);
            visit_term(body, observed);
        }
        Term::LetRec { ty, body, in_term, .. } => {
            observed.insert(VariantTag::TermLetRec);
            visit_type(ty, observed);
            visit_term(body, observed);
            visit_term(in_term, observed);
        }
        Term::If { cond, then, else_ } => {
            observed.insert(VariantTag::TermIf);
            visit_term(cond, observed);
            visit_term(then, observed);
            visit_term(else_, observed);
        }
        Term::Do { args, .. } => {
            observed.insert(VariantTag::TermDo);
            for a in args {
                visit_term(a, observed);
            }
        }
        Term::Ctor { args, .. } => {
            observed.insert(VariantTag::TermCtor);
            for a in args {
                visit_term(a, observed);
            }
        }
        Term::Match { scrutinee, arms } => {
            observed.insert(VariantTag::TermMatch);
            visit_term(scrutinee, observed);
            for arm in arms {
                visit_pattern(&arm.pat, observed);
                visit_term(&arm.body, observed);
            }
        }
        Term::Lam { param_tys, ret_ty, body, .. } => {
            observed.insert(VariantTag::TermLam);
            for ty in param_tys {
                visit_type(ty, observed);
            }
            visit_type(ret_ty, observed);
            visit_term(body, observed);
        }
        Term::Seq { lhs, rhs } => {
            observed.insert(VariantTag::TermSeq);
            visit_term(lhs, observed);
            visit_term(rhs, observed);
        }
        Term::Clone { value } => {
            observed.insert(VariantTag::TermClone);
            visit_term(value, observed);
        }
        Term::ReuseAs { source, body } => {
            observed.insert(VariantTag::TermReuseAs);
            visit_term(source, observed);
            visit_term(body, observed);
        }
        Term::Loop { binders, body } => {
            observed.insert(VariantTag::TermLoop);
            for b in binders {
                visit_type(&b.ty, observed);
                visit_term(&b.init, observed);
            }
            visit_term(body, observed);
        }
        Term::Recur { args } => {
            observed.insert(VariantTag::TermRecur);
            for a in args {
                visit_term(a, observed);
            }
        }
        Term::New { args, .. } => {
            // Functional construction. Exercised by the `new_*` /
            // `raw_buf_*` fixtures under examples/.
            observed.insert(VariantTag::TermNew);
            for arg in args {
                match arg {
                    NewArg::Type(t) => visit_type(t, observed),
                    NewArg::Value(v) => visit_term(v, observed),
                }
            }
        }
        Term::Intrinsic => {
            observed.insert(VariantTag::TermIntrinsic);
        }
    }
}

fn visit_pattern(p: &Pattern, observed: &mut HashSet<VariantTag>) {
    match p {
        Pattern::Wild => {
            observed.insert(VariantTag::PatternWild);
        }
        Pattern::Var { .. } => {
            observed.insert(VariantTag::PatternVar);
        }
        Pattern::Lit { lit } => {
            observed.insert(VariantTag::PatternLit);
            visit_literal(lit, observed);
        }
        Pattern::Ctor { fields, .. } => {
            observed.insert(VariantTag::PatternCtor);
            for f in fields {
                visit_pattern(f, observed);
            }
        }
    }
}

fn visit_literal(l: &Literal, observed: &mut HashSet<VariantTag>) {
    match l {
        Literal::Int { .. } => {
            observed.insert(VariantTag::LiteralInt);
        }
        Literal::Bool { .. } => {
            observed.insert(VariantTag::LiteralBool);
        }
        Literal::Str { .. } => {
            observed.insert(VariantTag::LiteralStr);
        }
        Literal::Unit => {
            observed.insert(VariantTag::LiteralUnit);
        }
        Literal::Float { .. } => {
            observed.insert(VariantTag::LiteralFloat);
        }
    }
}

fn visit_type(t: &Type, observed: &mut HashSet<VariantTag>) {
    match t {
        Type::Con { args, .. } => {
            observed.insert(VariantTag::TypeCon);
            for a in args {
                visit_type(a, observed);
            }
        }
        Type::Fn { params, param_modes, ret, ret_mode, .. } => {
            observed.insert(VariantTag::TypeFn);
            for p in params {
                visit_type(p, observed);
            }
            for m in param_modes {
                visit_param_mode(m, observed);
            }
            visit_type(ret, observed);
            visit_param_mode(ret_mode, observed);
        }
        Type::Var { .. } => {
            observed.insert(VariantTag::TypeVar);
        }
        Type::Forall { body, .. } => {
            observed.insert(VariantTag::TypeForall);
            visit_type(body, observed);
        }
    }
}

fn visit_param_mode(m: &ParamMode, observed: &mut HashSet<VariantTag>) {
    match m {
        ParamMode::Own => {
            observed.insert(VariantTag::ParamModeOwn);
        }
        ParamMode::Borrow => {
            observed.insert(VariantTag::ParamModeBorrow);
        }
    }
}

fn visit_module(m: &Module, observed: &mut HashSet<VariantTag>) {
    for def in &m.defs {
        visit_def(def, observed);
    }
}

#[test]
fn every_ast_variant_is_observed_in_the_fixture_corpus() {
    let fixtures = list_ail_fixtures();
    assert!(
        !fixtures.is_empty(),
        "no .ail fixtures found under {}",
        examples_dir().display()
    );

    let mut observed: HashSet<VariantTag> = HashSet::new();
    let mut load_failures = Vec::<String>::new();

    for path in &fixtures {
        match ailang_surface::load_module(path) {
            Ok(m) => visit_module(&m, &mut observed),
            Err(e) => load_failures.push(format!("{}: {e}", path.display())),
        }
    }

    // Load failures fail the test on their own — a fixture that
    // does not load is a corpus bug, surface it loud.
    if !load_failures.is_empty() {
        panic!(
            "{} fixture(s) failed to load during coverage scan:\n{}",
            load_failures.len(),
            load_failures.join("\n")
        );
    }

    let missing: Vec<VariantTag> = EXPECTED_VARIANTS
        .iter()
        .copied()
        .filter(|v| !observed.contains(v))
        .collect();

    if !missing.is_empty() {
        let names: Vec<String> = missing.iter().map(|v| format!("{v:?}")).collect();
        panic!(
            "{} AST variant(s) have NO fixture coverage in {}:\n  {}\n\n\
             every variant must be exercised by at least one fixture; \
             add a fixture that uses each missing variant (do NOT \
             weaken this test).",
            missing.len(),
            examples_dir().display(),
            names.join("\n  ")
        );
    }

    eprintln!(
        "schema coverage ok: {} variants observed across {} fixtures",
        observed.len(),
        fixtures.len()
    );
}
