use crate::{DiagnosticStage, SourceId, parse_source};

#[test]
#[allow(clippy::disallowed_methods)] // `insta` assertion macros unwrap internal I/O.
fn frontend_tool_syntax_cases_match_upstream_outcomes() {
    let accepted = [
        ("testA/parser-no-rules", "grammar A;\n"),
        ("testA/lexer-no-rules", "lexer grammar A;\n"),
        (
            "testEmptyGrammarOptions",
            "grammar A;\noptions {}\na : 'x' ;\n",
        ),
        ("testEmptyRuleOptions", "grammar A;\na options{} : 'x' ;\n"),
        (
            "testEmptyBlockOptions",
            "grammar A;\na : (options{} : 'x') ;\n",
        ),
        ("testEmptyTokensBlock", "grammar A;\ntokens {}\na : 'x' ;\n"),
    ];
    for (name, source) in accepted {
        parse_source(SourceId::new(0), name, source)
            .unwrap_or_else(|error| panic!("{name}: {:?}", error.diagnostics()));
    }

    let rejected = [
        (
            "testA/missing-grammar-keyword",
            "A;",
            DiagnosticStage::Parser,
        ),
        (
            "testA/missing-grammar-name",
            "grammar ;",
            DiagnosticStage::Parser,
        ),
        (
            "testA/missing-grammar-semi",
            "grammar A\na : ID ;\n",
            DiagnosticStage::Parser,
        ),
        (
            "testA/extra-rule-semi",
            "grammar A;\na : ID ;;\nb : B ;",
            DiagnosticStage::Parser,
        ),
        (
            "testA/extra-grammar-semi",
            "grammar A;;\na : ID ;\n",
            DiagnosticStage::Parser,
        ),
        (
            "testA/missing-rule-action",
            "grammar A;\na @init : ID ;\n",
            DiagnosticStage::Parser,
        ),
        (
            "testA/malformed-rule-prequel",
            "grammar A;\na  ( A | B ) D ;\nb : B ;",
            DiagnosticStage::Parser,
        ),
        (
            "testExtraColon",
            "grammar A;\na : : A ;\nb : B ;",
            DiagnosticStage::Parser,
        ),
        (
            "testMissingRuleSemi",
            "grammar A;\na : A \nb : B ;",
            DiagnosticStage::Parser,
        ),
        (
            "testMissingRuleSemi2",
            "lexer grammar A;\nA : 'a' \nB : 'b' ;",
            DiagnosticStage::Parser,
        ),
        (
            "testMissingRuleSemi3",
            "grammar A;\na : A \nb[int i] returns [int y] : B ;",
            DiagnosticStage::Parser,
        ),
        (
            "testMissingRuleSemi4",
            "grammar A;\na : b \n  catch [Exception e] {...}\nb : B ;\n",
            DiagnosticStage::Parser,
        ),
        (
            "testMissingRuleSemi5",
            "grammar A;\na : A \n  catch [Exception e] {...}\n",
            DiagnosticStage::Parser,
        ),
        (
            "testBadRulePrequelStart",
            "grammar A;\na @ options {k=1;} : A ;\nb : B ;",
            DiagnosticStage::Parser,
        ),
        (
            "testBadRulePrequelStart2",
            "grammar A;\na } : A ;\nb : B ;",
            DiagnosticStage::Parser,
        ),
        (
            "testUnterminatedStringLiteral",
            "grammar A;\na : 'x\n  ;\n",
            DiagnosticStage::Lexer,
        ),
        (
            "testParserRuleNameStartingWithUnderscore",
            "grammar A;\n_a : 'x' ;\n",
            DiagnosticStage::Lexer,
        ),
    ];
    let mut observed = Vec::new();
    for (name, source, expected_stage) in rejected {
        let error = parse_source(SourceId::new(0), name, source)
            .expect_err("invalid grammar must not return a CST");
        assert!(
            error
                .diagnostics()
                .iter()
                .any(|diagnostic| diagnostic.stage == expected_stage),
            "{name}: {:?}",
            error.diagnostics()
        );
        observed.push((name, error.diagnostics().to_vec()));
    }

    // The per-case assert only checks that *some* diagnostic carries the expected stage; snapshot
    // the full (code, stage, span, message) for every rejected case so drift in any of those
    // upstream-matched fields is caught, not just the stage.
    insta::assert_debug_snapshot!(
        "frontend_tool_syntax_cases_match_upstream_outcomes",
        observed
    );
}
