//! Drift detection between ast.rs and `docs/DESIGN.md` §"Data model".
//!
//! DESIGN.md is the canonical schema source-of-truth. Every AST enum
//! (`Term`, `Pattern`, `Type`, `Def`, `Literal`, `ParamMode`) must have a
//! JSON-schema anchor (e.g. `"t": "lit"`, `"k": "fn"`) present in that
//! document. These tests enforce the property.
//!
//! The exhaustive `match` per enum is the load-bearing mechanism: adding a
//! new variant without a matching arm fails compilation before the test runs.
//! Once the variant is matched, the test asserts the anchor is present in
//! DESIGN.md.

use ailang_core::ast::{
    ClassDef, ClassMethod, Constraint, ConstDef, Ctor, Def, FnDef, InstanceDef,
    InstanceMethod, Literal, Pattern, ParamMode, Suppress, Term, Type, TypeDef,
};

const DESIGN_MD: &str = include_str!("../../../docs/DESIGN.md");

/// Every `Term` variant must have its JSON-schema anchor present in
/// DESIGN.md §"Data model". An LLM author cannot produce a term variant
/// whose `"t"` tag is absent from the canonical schema document.
#[test]
fn design_md_anchors_every_term_variant() {
    let exemplars: Vec<(&str, Term)> = vec![
        (
            r#""t": "lit""#,
            Term::Lit { lit: Literal::Unit },
        ),
        (
            r#""t": "var""#,
            Term::Var { name: "x".into() },
        ),
        (
            r#""t": "app""#,
            Term::App {
                callee: Box::new(Term::Var { name: "f".into() }),
                args: vec![],
                tail: false,
            },
        ),
        (
            r#""t": "let""#,
            Term::Let {
                name: "x".into(),
                value: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
                body: Box::new(Term::Var { name: "x".into() }),
            },
        ),
        (
            r#""t": "letrec""#,
            Term::LetRec {
                name: "f".into(),
                ty: Type::fn_implicit(vec![], Type::int(), vec![]),
                params: vec![],
                body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
                in_term: Box::new(Term::Var { name: "f".into() }),
            },
        ),
        (
            r#""t": "if""#,
            Term::If {
                cond: Box::new(Term::Lit { lit: Literal::Bool { value: true } }),
                then: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }),
                else_: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
            },
        ),
        (
            r#""t": "do""#,
            Term::Do {
                op: "io/print_int".into(),
                args: vec![],
                tail: false,
            },
        ),
        (
            r#""t": "ctor""#,
            Term::Ctor {
                type_name: "List".into(),
                ctor: "Nil".into(),
                args: vec![],
            },
        ),
        (
            r#""t": "match""#,
            Term::Match {
                scrutinee: Box::new(Term::Var { name: "x".into() }),
                arms: vec![],
            },
        ),
        (
            r#""t": "lam""#,
            Term::Lam {
                params: vec![],
                param_tys: vec![],
                ret_ty: Box::new(Type::int()),
                effects: vec![],
                body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
            },
        ),
        (
            r#""t": "seq""#,
            Term::Seq {
                lhs: Box::new(Term::Var { name: "a".into() }),
                rhs: Box::new(Term::Var { name: "b".into() }),
            },
        ),
        (
            r#""t": "clone""#,
            Term::Clone {
                value: Box::new(Term::Var { name: "x".into() }),
            },
        ),
        (
            r#""t": "reuse-as""#,
            Term::ReuseAs {
                source: Box::new(Term::Var { name: "x".into() }),
                body: Box::new(Term::Var { name: "y".into() }),
            },
        ),
    ];

    for (anchor, term) in exemplars {
        // Exhaustive match: compiler rejects this file if a new Term
        // variant lacks an arm, catching drift at compile time.
        let _: &'static str = match term {
            Term::Lit { .. } => "lit",
            Term::Var { .. } => "var",
            Term::App { .. } => "app",
            Term::Let { .. } => "let",
            Term::LetRec { .. } => "letrec",
            Term::If { .. } => "if",
            Term::Do { .. } => "do",
            Term::Ctor { .. } => "ctor",
            Term::Match { .. } => "match",
            Term::Lam { .. } => "lam",
            Term::Seq { .. } => "seq",
            Term::Clone { .. } => "clone",
            Term::ReuseAs { .. } => "reuse-as",
        };
        assert!(
            DESIGN_MD.contains(anchor),
            "DESIGN.md §Data-model is missing anchor `{anchor}` for a Term variant — \
             add it to docs/DESIGN.md"
        );
    }
}

/// Every `Pattern` variant must have its JSON-schema anchor present in
/// DESIGN.md §"Data model". Missing anchors mean an LLM cannot produce
/// the corresponding pattern form.
#[test]
fn design_md_anchors_every_pattern_variant() {
    let exemplars: Vec<(&str, Pattern)> = vec![
        (r#""p": "wild""#, Pattern::Wild),
        (r#""p": "var""#, Pattern::Var { name: "x".into() }),
        (r#""p": "lit""#, Pattern::Lit { lit: Literal::Int { value: 0 } }),
        (
            r#""p": "ctor""#,
            Pattern::Ctor { ctor: "Nil".into(), fields: vec![] },
        ),
    ];

    for (anchor, pat) in exemplars {
        let _: &'static str = match pat {
            Pattern::Wild => "wild",
            Pattern::Var { .. } => "var",
            Pattern::Lit { .. } => "lit",
            Pattern::Ctor { .. } => "ctor",
        };
        assert!(
            DESIGN_MD.contains(anchor),
            "DESIGN.md §Data-model is missing anchor `{anchor}` for a Pattern variant"
        );
    }
}

/// Every `Type` variant must have its JSON-schema anchor present in
/// DESIGN.md §"Data model". The `"k"` discriminator is load-bearing for
/// the codegen and typechecker; an LLM must know all four forms.
#[test]
fn design_md_anchors_every_type_variant() {
    let exemplars: Vec<(&str, Type)> = vec![
        (r#""k": "con""#, Type::int()),
        (r#""k": "fn""#, Type::fn_implicit(vec![], Type::unit(), vec![])),
        (r#""k": "var""#, Type::Var { name: "a".into() }),
        (
            r#""k": "forall""#,
            Type::Forall {
                vars: vec!["a".into()],
                constraints: vec![],
                body: Box::new(Type::Var { name: "a".into() }),
            },
        ),
    ];

    for (anchor, ty) in exemplars {
        let _: &'static str = match ty {
            Type::Con { .. } => "con",
            Type::Fn { .. } => "fn",
            Type::Var { .. } => "var",
            Type::Forall { .. } => "forall",
        };
        assert!(
            DESIGN_MD.contains(anchor),
            "DESIGN.md §Data-model is missing anchor `{anchor}` for a Type variant"
        );
    }
}

/// Every `Literal` variant must have its JSON-schema anchor present in
/// DESIGN.md §"Data model". The `"kind"` discriminator identifies the
/// literal type; an LLM cannot produce a literal it hasn't seen in the
/// schema.
#[test]
fn design_md_anchors_every_literal_variant() {
    let exemplars: Vec<(&str, Literal)> = vec![
        (r#""kind": "int""#, Literal::Int { value: 0 }),
        (r#""kind": "bool""#, Literal::Bool { value: true }),
        (r#""kind": "str""#, Literal::Str { value: "x".into() }),
        (r#""kind": "unit""#, Literal::Unit),
        (r#""kind": "float""#, Literal::Float { bits: 0 }),
    ];

    for (anchor, lit) in exemplars {
        let _: &'static str = match lit {
            Literal::Int { .. } => "int",
            Literal::Bool { .. } => "bool",
            Literal::Str { .. } => "str",
            Literal::Unit => "unit",
            Literal::Float { .. } => "float",
        };
        assert!(
            DESIGN_MD.contains(anchor),
            "DESIGN.md §Data-model is missing anchor `{anchor}` for a Literal variant"
        );
    }
}

/// Every `Def` kind must have its JSON-schema anchor present in
/// DESIGN.md §"Data model". All five kinds (`fn`, `const`, `type`,
/// `class`, `instance`) must be documented so an LLM can write
/// any kind of top-level definition.
#[test]
fn design_md_anchors_every_def_kind() {
    let fn_def = FnDef {
        name: "f".into(),
        doc: None,
        suppress: vec![],
        ty: Type::fn_implicit(vec![], Type::int(), vec![]),
        params: vec![],
        body: Term::Lit { lit: Literal::Int { value: 0 } },
    };
    let const_def = ConstDef {
        name: "k".into(),
        doc: None,
        ty: Type::int(),
        value: Term::Lit { lit: Literal::Int { value: 0 } },
    };
    let type_def = TypeDef {
        name: "T".into(),
        doc: None,
        vars: vec![],
        ctors: vec![Ctor { name: "C".into(), fields: vec![] }],
        drop_iterative: false,
    };
    let class_def = ClassDef {
        name: "Show".into(),
        param: "a".into(),
        superclass: None,
        methods: vec![ClassMethod {
            name: "show".into(),
            ty: Type::fn_implicit(vec![Type::Var { name: "a".into() }], Type::str_(), vec![]),
            default: None,
        }],
        doc: None,
    };
    let instance_def = InstanceDef {
        class: "Show".into(),
        type_: Type::int(),
        methods: vec![InstanceMethod {
            name: "show".into(),
            body: Term::Lit { lit: Literal::Str { value: "0".into() } },
        }],
        doc: None,
    };

    let exemplars: Vec<(&str, Def)> = vec![
        (r#""kind": "fn""#, Def::Fn(fn_def)),
        (r#""kind": "const""#, Def::Const(const_def)),
        (r#""kind": "type""#, Def::Type(type_def)),
        (r#""kind": "class""#, Def::Class(class_def)),
        (r#""kind": "instance""#, Def::Instance(instance_def)),
    ];

    for (anchor, def) in exemplars {
        let _: &'static str = match def {
            Def::Fn(_) => "fn",
            Def::Const(_) => "const",
            Def::Type(_) => "type",
            Def::Class(_) => "class",
            Def::Instance(_) => "instance",
        };
        assert!(
            DESIGN_MD.contains(anchor),
            "DESIGN.md §Data-model is missing anchor `{anchor}` for a Def kind"
        );
    }
}

/// Every `ParamMode` variant must have its serialized string form present
/// in DESIGN.md §"Data model". The mode annotations are load-bearing for
/// ownership checking; an LLM author must know all three forms.
#[test]
fn design_md_anchors_every_parammode_variant() {
    let exemplars: Vec<(&str, ParamMode)> = vec![
        (r#""implicit""#, ParamMode::Implicit),
        (r#""own""#, ParamMode::Own),
        (r#""borrow""#, ParamMode::Borrow),
    ];

    for (anchor, mode) in exemplars {
        let _: &'static str = match mode {
            ParamMode::Implicit => "implicit",
            ParamMode::Own => "own",
            ParamMode::Borrow => "borrow",
        };
        assert!(
            DESIGN_MD.contains(anchor),
            "DESIGN.md §Data-model is missing anchor `{anchor}` for a ParamMode variant"
        );
    }
}

/// Nested struct key anchors must be present in DESIGN.md §"Data model".
/// These keys appear inside `Suppress`, `ClassMethod`, `InstanceMethod`,
/// and `Type::Forall` — they are not discriminators but they ARE part
/// of the canonical JSON schema and must remain documented.
#[test]
fn design_md_anchors_nested_struct_keys() {
    // Constructors exercised here ensure ast.rs field names are correct.
    let _ = Suppress { code: "x".into(), because: "y".into() };
    let _ = ClassMethod {
        name: "m".into(),
        ty: Type::fn_implicit(vec![], Type::int(), vec![]),
        default: None,
    };
    let _ = InstanceMethod {
        name: "m".into(),
        body: Term::Lit { lit: Literal::Unit },
    };
    let _ = Constraint {
        class: "Show".into(),
        type_: Type::int(),
    };

    let anchors = [
        r#""code""#,
        r#""because""#,
        r#""methods""#,
        r#""constraints""#,
    ];

    for anchor in anchors {
        assert!(
            DESIGN_MD.contains(anchor),
            "DESIGN.md §Data-model is missing nested-struct-key anchor `{anchor}`"
        );
    }
}
