#![allow(clippy::print_stderr, clippy::print_stdout)]

use std::collections::BTreeSet;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

#[path = "../bin_support/rust_names.rs"]
mod rust_names;
#[path = "../bin_support/templates.rs"]
mod templates;

use rust_names::{module_name, rust_function_name, rust_string, rust_type_name};
use templates::{
    is_after_action, is_definitions_action, is_init_action, is_members_action, is_options_block,
    matching_action_brace, matching_template_close, named_action_templates,
    next_parser_action_block, next_predicate_action_block, next_template_block,
    parse_template_string, split_template_arguments, template_sequence_bodies,
};

const DESCRIPTOR_PATH: &str = "resources/org/antlr/v4/test/runtime/descriptors";
const ANTLR_JAR_ENV: &str = "ANTLR4_JAR";
const DESCRIPTORS_ENV: &str = "ANTLR4_RUNTIME_TESTSUITE";
const DEFAULT_ANTLR_JAR: &str = "/tmp/antlr-cleanroom/tools/antlr-4.13.2-complete.jar";
const DEFAULT_DESCRIPTORS: &str = "/tmp/antlr-cleanroom/antlr4-upstream/runtime-testsuite";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse()?;
    let descriptor_root = resolve_descriptor_root(&args.descriptors)?;
    let descriptors = load_descriptors(&descriptor_root, &args)?;
    let mut summary = Summary::default();

    if args.work_dir.exists() && !args.keep {
        fs::remove_dir_all(&args.work_dir)?;
    }
    fs::create_dir_all(&args.work_dir)?;

    for descriptor in descriptors {
        if let Some(reason) = unsupported_reason(&descriptor) {
            summary.skipped += 1;
            println!("skip {}: {reason}", descriptor.id());
            continue;
        }

        summary.ran += 1;
        match run_descriptor(&args, &descriptor) {
            Ok(result)
                if result.output == descriptor.output && result.errors == descriptor.errors =>
            {
                summary.passed += 1;
                println!("pass {}", descriptor.id());
                remove_descriptor_work_dir(&args, &descriptor)?;
            }
            Ok(result) => {
                summary.failed += 1;
                eprintln!(
                    "fail {}\nexpected stdout:\n{}\nactual stdout:\n{}\nexpected stderr:\n{}\nactual stderr:\n{}",
                    descriptor.id(),
                    descriptor.output,
                    result.output,
                    descriptor.errors,
                    result.errors
                );
            }
            Err(error) => {
                summary.failed += 1;
                eprintln!("fail {}: {error}", descriptor.id());
            }
        }

        if args.limit.is_some_and(|limit| summary.ran >= limit) {
            break;
        }
    }

    println!(
        "summary: {} passed, {} failed, {} skipped, {} run",
        summary.passed, summary.failed, summary.skipped, summary.ran
    );

    if summary.failed == 0 {
        Ok(())
    } else {
        Err(io::Error::other(format!(
            "{} runtime-testsuite case(s) failed",
            summary.failed
        ))
        .into())
    }
}

#[derive(Debug)]
struct Args {
    antlr_jar: PathBuf,
    descriptors: PathBuf,
    runtime_crate: PathBuf,
    work_dir: PathBuf,
    group: Option<String>,
    case_name: Option<String>,
    limit: Option<usize>,
    keep: bool,
}

impl Args {
    fn parse() -> Result<Self, String> {
        let mut antlr_jar = None;
        let mut descriptors = None;
        let mut runtime_crate = env::current_dir().map_err(|error| error.to_string())?;
        let mut work_dir = runtime_crate.join("target/antlr-runtime-testsuite");
        let mut group = None;
        let mut case_name = None;
        let mut limit = None;
        let mut keep = false;

        let mut iter = env::args().skip(1);
        while let Some(arg) = iter.next() {
            match arg.as_str() {
                "--antlr-jar" => {
                    antlr_jar = Some(PathBuf::from(next_arg(&mut iter, "--antlr-jar")?));
                }
                "--descriptors" => {
                    descriptors = Some(PathBuf::from(next_arg(&mut iter, "--descriptors")?));
                }
                "--runtime-crate" => {
                    runtime_crate = PathBuf::from(next_arg(&mut iter, "--runtime-crate")?);
                    work_dir = runtime_crate.join("target/antlr-runtime-testsuite");
                }
                "--work-dir" => work_dir = PathBuf::from(next_arg(&mut iter, "--work-dir")?),
                "--group" => group = Some(next_arg(&mut iter, "--group")?),
                "--case" => {
                    let value = next_arg(&mut iter, "--case")?;
                    if let Some((case_group, name)) = value.split_once('/') {
                        group = Some(case_group.to_owned());
                        case_name = Some(name.to_owned());
                    } else {
                        case_name = Some(value);
                    }
                }
                "--limit" => {
                    let value = next_arg(&mut iter, "--limit")?;
                    limit = Some(
                        value
                            .parse::<usize>()
                            .map_err(|error| format!("invalid --limit {value:?}: {error}"))?,
                    );
                }
                "--keep" => keep = true,
                "--help" | "-h" => return Err(usage()),
                other => return Err(format!("unknown argument {other}\n\n{}", usage())),
            }
        }

        let antlr_jar = resolve_path_argument(
            antlr_jar,
            ANTLR_JAR_ENV,
            vec![
                runtime_crate.join("tools/antlr-4.13.2-complete.jar"),
                runtime_crate.join("target/antlr-4.13.2-complete.jar"),
                PathBuf::from(DEFAULT_ANTLR_JAR),
            ],
            "--antlr-jar",
            "ANTLR tool jar",
        )?;
        let descriptors = resolve_path_argument(
            descriptors,
            DESCRIPTORS_ENV,
            vec![
                runtime_crate.join("target/antlr4/runtime-testsuite"),
                runtime_crate.join("../antlr4/runtime-testsuite"),
                PathBuf::from(DEFAULT_DESCRIPTORS),
            ],
            "--descriptors",
            "ANTLR runtime-testsuite descriptors",
        )?;

        Ok(Self {
            antlr_jar,
            descriptors,
            runtime_crate,
            work_dir,
            group,
            case_name,
            limit,
            keep,
        })
    }
}

/// Resolves an optional CLI path from, in order, the explicit flag, an
/// environment override, and known local checkout locations.
///
/// The bare `cargo run --bin antlr4-runtime-testsuite` workflow is meant for
/// the maintainer machine where the ANTLR jar and upstream checkout already
/// live under `/tmp/antlr-cleanroom`; fresh environments can still pass
/// explicit paths or set the documented environment variables.
fn resolve_path_argument(
    explicit: Option<PathBuf>,
    env_key: &str,
    candidates: Vec<PathBuf>,
    flag: &str,
    label: &str,
) -> Result<PathBuf, String> {
    if let Some(path) = explicit {
        return Ok(path);
    }
    if let Ok(value) = env::var(env_key) {
        if !value.is_empty() {
            return Ok(PathBuf::from(value));
        }
    }
    candidates.into_iter().find(|path| path.exists()).ok_or_else(|| {
        format!(
            "missing {label}; pass {flag}, set {env_key}, or create the default checkout under /tmp/antlr-cleanroom\n\n{}",
            usage()
        )
    })
}

fn next_arg(iter: &mut impl Iterator<Item = String>, flag: &str) -> Result<String, String> {
    iter.next()
        .ok_or_else(|| format!("{flag} requires a value\n\n{}", usage()))
}

fn usage() -> String {
    "usage: antlr4-runtime-testsuite [--antlr-jar ANTLR.jar] [--descriptors PATH] [--case Group/Name] [--group Group] [--limit N] [--keep]\n\nDefaults: ANTLR4_JAR or /tmp/antlr-cleanroom/tools/antlr-4.13.2-complete.jar; ANTLR4_RUNTIME_TESTSUITE or /tmp/antlr-cleanroom/antlr4-upstream/runtime-testsuite".to_owned()
}

#[derive(Debug, Default)]
struct Summary {
    ran: usize,
    passed: usize,
    failed: usize,
    skipped: usize,
}

#[derive(Clone, Debug)]
struct Descriptor {
    group: String,
    name: String,
    test_type: String,
    grammar_name: String,
    grammar: String,
    start_rule: String,
    input: String,
    output: String,
    errors: String,
    flags: String,
    slave_grammars: Vec<String>,
}

impl Descriptor {
    fn id(&self) -> String {
        format!("{}/{}", self.group, self.name)
    }

    fn is_parser(&self) -> bool {
        matches!(self.test_type.as_str(), "Parser" | "CompositeParser")
    }

    fn is_lexer(&self) -> bool {
        matches!(self.test_type.as_str(), "Lexer" | "CompositeLexer")
    }

    fn is_composite(&self) -> bool {
        matches!(
            self.test_type.as_str(),
            "CompositeParser" | "CompositeLexer"
        )
    }
}

#[derive(Debug)]
struct RunResult {
    output: String,
    errors: String,
}

/// Resolves either the upstream `runtime-testsuite` root or the descriptor
/// directory itself to the concrete descriptor directory.
fn resolve_descriptor_root(path: &Path) -> io::Result<PathBuf> {
    let direct = path.join(DESCRIPTOR_PATH);
    if direct.is_dir() {
        return Ok(direct);
    }
    if path.ends_with("descriptors") && path.is_dir() {
        return Ok(path.to_path_buf());
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        format!(
            "descriptor root not found under {}; pass runtime-testsuite root or descriptors directory",
            path.display()
        ),
    ))
}

/// Loads descriptor files in stable order and applies the CLI group/case
/// filters before parsing.
fn load_descriptors(root: &Path, args: &Args) -> io::Result<Vec<Descriptor>> {
    let mut descriptors = Vec::new();
    let mut group_dirs = sorted_children(root)?;
    group_dirs.retain(|entry| entry.path.is_dir());
    for group_dir in group_dirs {
        let group = group_dir.name;
        if args.group.as_ref().is_some_and(|wanted| wanted != &group) {
            continue;
        }

        let mut files = sorted_children(&group_dir.path)?;
        files.retain(|entry| entry.path.extension() == Some(OsStr::new("txt")));
        for file in files {
            let name = file.name.trim_end_matches(".txt").to_owned();
            if args
                .case_name
                .as_ref()
                .is_some_and(|wanted| wanted != &name)
            {
                continue;
            }
            let text = fs::read_to_string(&file.path)?;
            descriptors.push(parse_descriptor(group.clone(), name, &text)?);
        }
    }
    Ok(descriptors)
}

#[derive(Debug)]
struct DirEntryInfo {
    name: String,
    path: PathBuf,
}

fn sorted_children(path: &Path) -> io::Result<Vec<DirEntryInfo>> {
    let mut entries = Vec::new();
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let name = entry.file_name().to_string_lossy().into_owned();
        if name.starts_with('.') {
            continue;
        }
        entries.push(DirEntryInfo {
            name,
            path: entry.path(),
        });
    }
    entries.sort_by(|left, right| left.name.cmp(&right.name));
    Ok(entries)
}

/// Parses ANTLR runtime-testsuite descriptor text into the subset this harness
/// needs for execution and output comparison.
fn parse_descriptor(group: String, name: String, text: &str) -> io::Result<Descriptor> {
    let mut current_section: Option<String> = None;
    let mut current_value = String::new();
    let mut sections = Vec::new();

    for line in text.lines() {
        if let Some(section) = section_name(line) {
            if let Some(field) = current_section.replace(section.to_owned()) {
                sections.push((field, current_value.clone()));
                current_value.clear();
            }
        } else {
            current_value.push_str(line);
            current_value.push('\n');
        }
    }
    if let Some(field) = current_section {
        sections.push((field, current_value));
    }

    let mut descriptor = Descriptor {
        group,
        name,
        test_type: "Lexer".to_owned(),
        grammar_name: String::new(),
        grammar: String::new(),
        start_rule: String::new(),
        input: String::new(),
        output: String::new(),
        errors: String::new(),
        flags: String::new(),
        slave_grammars: Vec::new(),
    };

    for (section, value) in sections {
        let value = normalize_section_value(&value);
        match section.as_str() {
            "type" => descriptor.test_type = value,
            "grammar" => {
                let value = render_st_backslash_escapes(&value);
                descriptor.grammar_name = grammar_name(&value)?;
                descriptor.grammar = value;
            }
            "slaveGrammar" => descriptor
                .slave_grammars
                .push(render_st_backslash_escapes(&value)),
            "input" => descriptor.input = value,
            "output" => descriptor.output = value,
            "errors" => descriptor.errors = value,
            "flags" => descriptor.flags = value,
            "start" => descriptor.start_rule = value,
            "notes" | "skip" => {}
            other => {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("unknown descriptor section {other:?}"),
                ));
            }
        }
    }

    Ok(descriptor)
}

/// Returns a descriptor section name, deliberately excluding token display
/// output such as `[@0,...]`.
fn section_name(line: &str) -> Option<&str> {
    if line.starts_with('[') && line.ends_with(']') && line.len() > 2 {
        let name = &line[1..line.len() - 1];
        match name {
            "notes" | "type" | "grammar" | "slaveGrammar" | "start" | "input" | "output"
            | "errors" | "flags" | "skip" => Some(name),
            _ => None,
        }
    } else {
        None
    }
}

/// Mirrors the upstream descriptor parser's section trimming and triple-quote
/// handling so expected stdout/stderr bytes compare correctly.
fn normalize_section_value(value: &str) -> String {
    let trimmed = value.trim();
    if trimmed.starts_with("\"\"\"") {
        remove_marker(trimmed, "\"\"\"")
    } else if trimmed.contains('\n') {
        let mut out = trimmed.to_owned();
        out.push('\n');
        out
    } else {
        trimmed.to_owned()
    }
}

fn remove_marker(value: &str, marker: &str) -> String {
    let mut out = String::new();
    let mut rest = value;
    while let Some(index) = rest.find(marker) {
        out.push_str(&rest[..index]);
        rest = &rest[index + marker.len()..];
    }
    out.push_str(rest);
    out
}

/// Applies the `StringTemplate` backslash collapse used by the upstream Java
/// harness when descriptor grammars are rendered as templates.
fn render_st_backslash_escapes(value: &str) -> String {
    let mut out = String::with_capacity(value.len());
    let mut chars = value.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '\\' {
            match chars.peek() {
                Some('\\') => {
                    chars.next();
                    out.push('\\');
                }
                Some('<' | '>') => {}
                _ => out.push(ch),
            }
        } else {
            out.push(ch);
        }
    }
    out
}

fn grammar_name(grammar: &str) -> io::Result<String> {
    let first_line = grammar.lines().next().unwrap_or_default();
    let Some(start) = first_line.find("grammar ") else {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("missing grammar declaration in {first_line:?}"),
        ));
    };
    let Some(stop) = first_line.find(';') else {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("missing grammar declaration semicolon in {first_line:?}"),
        ));
    };
    Ok(first_line[start + "grammar ".len()..stop].to_owned())
}

/// Classifies descriptors that the current metadata-first harness cannot run
/// yet while keeping them visible in summaries.
fn unsupported_reason(descriptor: &Descriptor) -> Option<&'static str> {
    if !descriptor.slave_grammars.is_empty() && !descriptor.is_composite() {
        return Some("composite grammars are not wired into the metadata harness yet");
    }
    if descriptor.is_composite() && !composite_grammar_supported(descriptor) {
        return Some("composite grammar shape is not wired into the metadata harness yet");
    }
    if !descriptor.flags.is_empty() && !runtime_flags_supported(descriptor) {
        return Some("diagnostic/profile/DFA flags are not implemented in the Rust harness yet");
    }
    let grammar = combined_grammar_source(descriptor);
    if has_target_template(&grammar) && !target_templates_supported(descriptor, &grammar) {
        return Some("target-template semantic actions are not rendered by this harness yet");
    }
    if descriptor.is_parser() {
        if !descriptor.output.is_empty() {
            if !target_templates_supported(descriptor, &grammar) {
                return Some(
                    "parser target actions/listeners are not wired into the Rust harness yet",
                );
            }
        }
        if !descriptor.errors.is_empty() && !parser_error_diagnostics_supported(descriptor) {
            return Some(
                "parser error recovery diagnostics are not wired into the Rust harness yet",
            );
        }
        return None;
    }
    if !descriptor.is_lexer() {
        return Some("descriptor type is not supported by the metadata harness yet");
    }
    None
}

/// Identifies descriptor runtime flags whose behavior is already represented by
/// the current Rust harness without extra setup.
fn runtime_flags_supported(descriptor: &Descriptor) -> bool {
    matches!(
        descriptor.flags.trim(),
        "notBuildParseTree" | "predictionMode=LL" | "predictionMode=SLL"
    ) || (descriptor.flags.trim() == "showDFA"
        && matches!(
            descriptor.id().as_str(),
            "SemPredEvalLexer/DisableRule"
                | "SemPredEvalLexer/EnumNotID"
                | "SemPredEvalLexer/IDnotEnum"
                | "SemPredEvalLexer/IDvsEnum"
                | "SemPredEvalLexer/Indent"
                | "SemPredEvalLexer/LexerInputPositionSensitivePredicates"
        ))
        || (descriptor.flags.trim() == "showDiagnosticErrors"
            && matches!(
                descriptor.id().as_str(),
                "SemPredEvalParser/TwoUnpredicatedAlts"
                    | "SemPredEvalParser/TwoUnpredicatedAltsAndOneOrthogonalAlt"
            ))
        || (descriptor.flags.trim() == "showDiagnosticErrors"
            && descriptor.group == "FullContextParsing")
}

/// Whitelists composite descriptors whose import and action shapes are modeled by
/// the current metadata harness.
fn composite_grammar_supported(descriptor: &Descriptor) -> bool {
    matches!(
        descriptor.id().as_str(),
        "CompositeLexers/LexerDelegatorInvokesDelegateRule"
            | "CompositeLexers/LexerDelegatorRuleOverridesDelegate"
            | "CompositeParsers/BringInLiteralsFromDelegate"
            | "CompositeParsers/CombinedImportsCombined"
            | "CompositeParsers/DelegatesSeeSameTokenType"
            | "CompositeParsers/DelegatorAccessesDelegateMembers"
            | "CompositeParsers/DelegatorInvokesDelegateRule"
            | "CompositeParsers/DelegatorInvokesDelegateRuleWithArgs"
            | "CompositeParsers/DelegatorInvokesDelegateRuleWithReturnStruct"
            | "CompositeParsers/DelegatorInvokesFirstVersionOfDelegateRule"
            | "CompositeParsers/DelegatorRuleOverridesDelegate"
            | "CompositeParsers/DelegatorRuleOverridesDelegates"
            | "CompositeParsers/DelegatorRuleOverridesLookaheadInDelegate"
            | "CompositeParsers/ImportedGrammarWithEmptyOptions"
            | "CompositeParsers/ImportedRuleWithAction"
            | "CompositeParsers/ImportLexerWithOnlyFragmentRules"
            | "CompositeParsers/KeywordVSIDOrder"
    )
}

/// Admits only parser-error descriptors covered by the current mismatch and
/// single-token recovery diagnostics, leaving mixed lexer/parser diagnostic
/// ordering cases skipped.
fn parser_error_diagnostics_supported(descriptor: &Descriptor) -> bool {
    if runtime_flags_supported(descriptor) && descriptor.flags.trim() == "showDiagnosticErrors" {
        return true;
    }
    matches!(
        descriptor.name.as_str(),
        "ConjuringUpToken"
            | "ConjuringUpTokenFromSet"
            | "ComplementSet"
            | "ExtraToken"
            | "ExtraTokensAndAltLabels"
            | "ExtraneousInput"
            | "InvalidEmptyInput"
            | "LL2"
            | "LL3"
            | "LLStar"
            | "MultiTokenDeletionBeforeLoop"
            | "MultiTokenDeletionBeforeLoop2"
            | "MultiTokenDeletionDuringLoop"
            | "MultiTokenDeletionDuringLoop2"
            | "NoTruePredsThrowsNoViableAlt"
            | "NoViableAlt"
            | "NoViableAltAvoidance"
            | "PredFromAltTestedInLoopBack_1"
            | "PredTestedEvenWhenUnAmbig_2"
            | "PredictionMode_SLL"
            | "SingleSetInsertion"
            | "SingleSetInsertionConsumption"
            | "SingleTokenDeletion"
            | "SingleTokenDeletionBeforeAlt"
            | "SingleTokenDeletionBeforeLoop"
            | "SingleTokenDeletionBeforeLoop2"
            | "SingleTokenDeletionBeforePredict"
            | "SingleTokenDeletionConsumption"
            | "SingleTokenDeletionDuringLoop"
            | "SingleTokenDeletionDuringLoop2"
            | "SingleTokenDeletionExpectingSet"
            | "SingleTokenInsertion"
            | "SemPredFailOption"
            | "SimpleValidate"
            | "SimpleValidate2"
            | "Sync"
            | "TokenMismatch"
            | "TokenMismatch2"
            | "TokenMismatch3"
            | "ValidateInDFA"
            | "UnicodeEscapedSMPRangeSetMismatch"
    )
}

/// Builds the grammar text passed to the Rust generator for action extraction.
///
/// ANTLR's metadata output for imported grammars is flattened into the delegator
/// `.interp` file, so action templates from imported rules must be visible to the
/// Rust generator as well. Delegates are ordered by the delegator's `import`
/// clause so rule overrides pick the same first definition ANTLR keeps.
fn combined_grammar_source(descriptor: &Descriptor) -> String {
    let mut out = String::new();
    let mut seen = BTreeSet::new();
    push_grammar_source(&mut out, &descriptor.grammar);
    append_imported_grammar_sources(&descriptor.grammar, descriptor, &mut seen, &mut out);
    for grammar in &descriptor.slave_grammars {
        if let Ok(name) = grammar_name(grammar) {
            if seen.insert(name) {
                push_grammar_source(&mut out, grammar);
            }
        }
    }
    out
}

fn append_imported_grammar_sources(
    grammar: &str,
    descriptor: &Descriptor,
    seen: &mut BTreeSet<String>,
    out: &mut String,
) {
    for import in imported_grammar_names(grammar) {
        if !seen.insert(import.clone()) {
            continue;
        }
        let Some(slave) = slave_grammar_by_name(descriptor, &import) else {
            continue;
        };
        push_grammar_source(out, slave);
        append_imported_grammar_sources(slave, descriptor, seen, out);
    }
}

fn slave_grammar_by_name<'a>(descriptor: &'a Descriptor, name: &str) -> Option<&'a str> {
    descriptor.slave_grammars.iter().find_map(|grammar| {
        grammar_name(grammar)
            .ok()
            .filter(|grammar_name| grammar_name == name)
            .map(|_| grammar.as_str())
    })
}

fn push_grammar_source(out: &mut String, grammar: &str) {
    if !out.is_empty() && !out.ends_with('\n') {
        out.push('\n');
    }
    out.push_str(grammar);
    if !out.ends_with('\n') {
        out.push('\n');
    }
}

/// Extracts direct `import A, B;` dependencies from a grammar header.
fn imported_grammar_names(grammar: &str) -> Vec<String> {
    let mut names = Vec::new();
    for line in grammar.lines() {
        let line = line.split("//").next().unwrap_or_default().trim();
        let Some(imports) = line
            .strip_prefix("import ")
            .and_then(|value| value.strip_suffix(';'))
        else {
            continue;
        };
        names.extend(
            imports
                .split(',')
                .map(str::trim)
                .filter(|name| !name.is_empty())
                .map(ToOwned::to_owned),
        );
    }
    names
}

fn has_target_template(grammar: &str) -> bool {
    next_template_block(grammar, 0).is_some()
        || grammar.contains("{<")
        || grammar.contains("<BailErrorStrategy")
        || grammar.contains("<ImportRuleInvocationStack")
        || grammar.contains("<writeln")
        || grammar.contains("<write")
        || grammar.contains("<InputText")
        || grammar.contains("<LANotEquals")
        || grammar.contains("@members")
        || grammar.contains("@definitions")
}

fn target_templates_supported(descriptor: &Descriptor, grammar: &str) -> bool {
    if descriptor.is_lexer() {
        return lexer_target_templates_supported(descriptor, grammar);
    }
    if !descriptor.is_parser() {
        return false;
    }
    if unsupported_members_templates(grammar)
        || grammar.contains("@definitions")
        || !supported_signature_templates(grammar)
    {
        return false;
    }
    if grammar.contains("@init") && !supported_init_action_templates(grammar) {
        return false;
    }
    if grammar.contains("@after") && !supported_after_action_templates(grammar) {
        return false;
    }
    supported_action_templates(grammar)
}

fn lexer_target_templates_supported(descriptor: &Descriptor, grammar: &str) -> bool {
    if descriptor.name == "PositionAdjustingLexer" {
        return grammar.contains("<PositionAdjustingLexer")
            && supported_lexer_predicate_templates(grammar)
            && supported_action_templates(grammar);
    }
    if grammar.contains("@members")
        || grammar.contains("@definitions")
        || grammar.contains("<PositionAdjustingLexer")
    {
        return false;
    }
    supported_lexer_predicate_templates(grammar) && supported_action_templates(grammar)
}

fn supported_action_templates(grammar: &str) -> bool {
    let mut offset = 0;
    while let Some(block) = next_parser_action_block(grammar, offset, is_int_return_assignment) {
        offset = block.after_brace;
        if block.predicate
            || is_after_action(grammar, block.open_brace)
            || is_init_action(grammar, block.open_brace)
            || is_definitions_action(grammar, block.open_brace)
            || is_members_action(grammar, block.open_brace)
            || is_options_block(grammar, block.open_brace)
        {
            continue;
        }
        if !is_supported_action_block(block.body) {
            return false;
        }
    }
    true
}

fn is_supported_action_block(body: &str) -> bool {
    body.trim().is_empty()
        || is_supported_action_template_sequence(body)
        || is_int_return_assignment(body)
}

/// Allows upstream parser setup actions that are either implemented directly by
/// the smoke harness or irrelevant to metadata-driven recognition.
fn supported_init_action_templates(grammar: &str) -> bool {
    let mut saw_init_action = false;
    let mut offset = 0;
    while let Some(block) = next_template_block(grammar, offset) {
        offset = block.after_brace;
        if block.predicate || !is_init_action(grammar, block.open_brace) {
            continue;
        }
        saw_init_action = true;
        if !matches!(
            block.body.trim(),
            "BuildParseTrees()"
                | "BailErrorStrategy()"
                | "GetExpectedTokenNames():writeln()"
                | "LL_EXACT_AMBIG_DETECTION()"
        ) {
            return false;
        }
    }
    saw_init_action
}

fn supported_after_action_templates(grammar: &str) -> bool {
    let mut saw_after_action = false;
    let listener_kind = listener_template_kind(grammar);
    for block in named_action_templates(grammar, "@after") {
        saw_after_action = true;
        let body = block.body.trim();
        if is_string_tree_label_template(body)
            || is_context_member_string_tree_template(body)
            || (listener_kind.is_some() && is_context_member_walk_listener_template(body))
        {
            continue;
        }
        if !is_supported_action_template(body) {
            return false;
        }
    }
    saw_after_action
}

fn supported_lexer_predicate_templates(grammar: &str) -> bool {
    let mut offset = 0;
    while let Some(block) = next_predicate_action_block(grammar, offset) {
        offset = block.after_brace;
        if block.body.contains('<') && !is_supported_lexer_predicate_template(block.body.trim()) {
            return false;
        }
    }
    true
}

fn is_supported_lexer_predicate_template(body: &str) -> bool {
    if let Some(inner) = single_template_body(body) {
        return is_supported_lexer_predicate_template(inner);
    }
    matches!(body, "True()" | "False()")
        || body == r#"<Column()> \< 2"#
        || body == "<Column()> < 2"
        || body == "<Column()> >= 2"
        || body
            .strip_prefix("TextEquals(")
            .and_then(|value| value.strip_suffix(')'))
            .is_some_and(|argument| parse_template_string(argument).is_some())
        || body
            .strip_prefix("TokenStartColumnEquals(")
            .and_then(|value| value.strip_suffix(')'))
            .is_some_and(|argument| {
                parse_template_string(argument).is_some_and(|value| value.parse::<usize>().is_ok())
            })
}

fn single_template_body(body: &str) -> Option<&str> {
    let body = body.trim();
    if body.as_bytes().first() != Some(&b'<') {
        return None;
    }
    let close = matching_template_close(body, 1)?;
    (close + 1 == body.len()).then_some(&body[1..close])
}

/// Mirrors the generator's currently supported action-template subset so the
/// harness runs only descriptors it can translate faithfully.
fn is_supported_action_template(body: &str) -> bool {
    matches!(
        body,
        r#"writeln("$text")"#
            | r#"write("$text")"#
            | "InputText():writeln()"
            | "Text():writeln()"
            | "Text():write()"
            | "RuleInvocationStack():writeln()"
            | "RuleInvocationStack():write()"
            | "Pass()"
            | "LL_EXACT_AMBIG_DETECTION()"
            | "DumpDFA()"
            | r#"ToStringTree("$ctx"):writeln()"#
            | r#"ToStringTree("$ctx"):write()"#
            | "Invoke_foo()"
    ) || body.starts_with("writeln(\"\\\"")
        || body.starts_with("write(\"\\\"")
        || is_string_tree_label_template(body)
        || is_noop_action_template(body)
        || is_append_str_token_text_template(body)
        || is_token_text_template(body)
        || is_token_display_template(body)
        || is_add_member_template(body)
        || is_member_value_template(body)
        || is_rule_value_template(body)
        || (body.starts_with("PlusText(\"") && body.ends_with("):writeln()"))
        || (body.starts_with("PlusText(\"") && body.ends_with("):write()"))
}

fn is_supported_action_template_sequence(body: &str) -> bool {
    template_sequence_bodies(body).is_some_and(|templates| {
        templates
            .into_iter()
            .all(|template| is_supported_action_template(template.trim()))
    })
}

fn is_add_member_template(body: &str) -> bool {
    body.strip_prefix("AddMember(")
        .and_then(|value| value.strip_suffix(')'))
        .map(split_template_arguments)
        .is_some_and(|arguments| {
            let [member, value] = arguments.as_slice() else {
                return false;
            };
            parse_template_string(member).is_some()
                && parse_template_string(value).is_some_and(|value| value.parse::<i64>().is_ok())
        })
}

fn is_member_value_template(body: &str) -> bool {
    let argument = body
        .strip_prefix("writeln(GetMember(")
        .and_then(|value| value.strip_suffix("))"))
        .or_else(|| {
            body.strip_prefix("write(GetMember(")
                .and_then(|value| value.strip_suffix("))"))
        });
    argument.is_some_and(|argument| parse_template_string(argument).is_some())
}

fn supported_signature_templates(grammar: &str) -> bool {
    grammar.lines().all(|line| {
        supported_signature_template_on_line(line, "returns [")
            && supported_signature_template_on_line(line, "locals [")
    })
}

/// Checks one `returns [...]` or `locals [...]` clause for target-template
/// signatures the generator can erase or model in the runtime-test harness.
fn supported_signature_template_on_line(line: &str, marker: &str) -> bool {
    let Some(marker_start) = line.find(marker) else {
        return true;
    };
    let after_marker = marker_start + marker.len();
    let leading_whitespace = line[after_marker..].len() - line[after_marker..].trim_start().len();
    let template_start = after_marker + leading_whitespace;
    if line.as_bytes().get(template_start) != Some(&b'<') {
        return true;
    }
    let Some(close_angle) = matching_template_close(line, template_start + 1) else {
        return false;
    };
    let body = &line[template_start + 1..close_angle];
    (body.starts_with("IntArg(") && body.ends_with(')'))
        || matches!(body, "StringType()" | "StringList()")
}

/// Allows only member templates that are no-op scaffolding for this metadata
/// harness; real listener/member customizations stay skipped.
fn unsupported_members_templates(grammar: &str) -> bool {
    if !(grammar.contains("@members") || grammar.contains("@parser::members")) {
        return false;
    }
    let mut saw_supported = false;
    let mut offset = 0;
    while let Some(block) = next_template_block(grammar, offset) {
        offset = block.after_brace;
        if !is_members_action(grammar, block.open_brace) {
            continue;
        }
        if !is_supported_members_template(block.body.trim()) {
            return true;
        }
        saw_supported = true;
    }
    !saw_supported
}

fn is_supported_members_template(body: &str) -> bool {
    body == "DeclareContextListGettersFunction()"
        || body == "Declare_foo()"
        || body == "Declare_pred()"
        || (body.starts_with("InitBooleanMember(") && body.ends_with(",True())"))
        || (body.starts_with("InitIntMember(") && body.ends_with(')'))
}

fn listener_template_kind(grammar: &str) -> Option<&'static str> {
    grammar
        .lines()
        .find_map(|line| listener_line_kind(line.trim()))
}

fn listener_line_kind(trimmed: &str) -> Option<&'static str> {
    if trimmed.starts_with("<BasicListener(") {
        Some("basic")
    } else if trimmed.starts_with("<TokenGetterListener(") {
        Some("token-getter")
    } else if trimmed.starts_with("<RuleGetterListener(") {
        Some("rule-getter")
    } else if trimmed.starts_with("<LRListener(") {
        Some("left-recursive")
    } else if trimmed.starts_with("<LRWithLabelsListener(") {
        Some("left-recursive-labels")
    } else {
        None
    }
}

fn is_noop_action_template(body: &str) -> bool {
    (body.starts_with("AssignLocal(")
        || body.starts_with("AssertIsList(")
        || body.starts_with("InitIntVar(")
        || body.starts_with("IntArg(")
        || body.starts_with("Production(")
        || body.starts_with("Result(")
        || body.starts_with("SetMember("))
        && body.ends_with(')')
}

fn is_token_text_template(body: &str) -> bool {
    let Some(argument) = body
        .strip_prefix("writeln(\"$")
        .and_then(|value| value.strip_suffix(".text\")"))
        .or_else(|| {
            body.strip_prefix("write(\"$")
                .and_then(|value| value.strip_suffix(".text\")"))
        })
    else {
        return false;
    };
    argument
        .chars()
        .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}

/// Recognizes the simple `$rule.v` and `$rule.result` print helpers that the
/// generator can evaluate from the parse tree for left-recursion fixtures.
fn is_rule_value_template(body: &str) -> bool {
    let Some(argument) = body
        .strip_prefix("writeln(\"$")
        .and_then(|value| value.strip_suffix("\")"))
        .or_else(|| {
            body.strip_prefix("write(\"$")
                .and_then(|value| value.strip_suffix("\")"))
        })
    else {
        return false;
    };
    let Some((rule_name, value_name)) = argument.split_once('.') else {
        return false;
    };
    is_antlr_identifier(rule_name) && is_antlr_identifier(value_name) && value_name != "text"
}

/// Recognizes simple raw return assignments that ANTLR lowers to action
/// transitions and the Rust generator captures as rule-context return slots.
fn is_int_return_assignment(body: &str) -> bool {
    let body = body.trim();
    let Some((name, value)) = body
        .strip_prefix('$')
        .and_then(|body| body.strip_suffix(';'))
        .and_then(|body| body.split_once('='))
    else {
        return false;
    };
    is_antlr_identifier(name.trim()) && value.trim().parse::<i64>().is_ok()
}

/// Mirrors the generator's `AppendStr` subset: a literal prefix plus either the
/// current rule text or a `$label.text` payload.
fn is_append_str_token_text_template(body: &str) -> bool {
    append_str_arguments(body)
        .map(split_template_arguments)
        .is_some_and(|arguments| {
            let [prefix, value] = arguments.as_slice() else {
                return false;
            };
            parse_template_string(prefix).is_some()
                && parse_template_string(value).is_some_and(|value| {
                    value == "$text"
                        || value
                            .strip_prefix('$')
                            .and_then(|label| label.strip_suffix(".text"))
                            .is_some_and(is_antlr_identifier)
                })
        })
}

/// Extracts the comma-separated arguments from the fluent
/// `AppendStr(...):write[ln]()` forms used by runtime descriptors.
fn append_str_arguments(body: &str) -> Option<&str> {
    if let Some(arguments) = body
        .strip_prefix("AppendStr(")
        .and_then(|value| value.strip_suffix("):writeln()"))
    {
        return Some(arguments);
    }
    body.strip_prefix("AppendStr(")
        .and_then(|value| value.strip_suffix("):write()"))
}

fn is_token_display_template(body: &str) -> bool {
    append_arguments(body)
        .map(split_template_arguments)
        .is_some_and(|arguments| {
            let [prefix, value] = arguments.as_slice() else {
                return false;
            };
            parse_template_string(prefix).is_some()
                && parse_template_string(value).is_some_and(|value| {
                    value.strip_prefix('$').is_some_and(|name| {
                        is_antlr_identifier(name.strip_suffix(".stop").unwrap_or(name))
                    })
                })
        })
}

fn append_arguments(body: &str) -> Option<&str> {
    if let Some(arguments) = body
        .strip_prefix("Append(")
        .and_then(|value| value.strip_suffix("):writeln()"))
    {
        return Some(arguments);
    }
    if let Some(arguments) = body
        .strip_prefix("Append(")
        .and_then(|value| value.strip_suffix("):write()"))
    {
        return Some(arguments);
    }
    if let Some(arguments) = body
        .strip_prefix("writeln(Append(")
        .and_then(|value| value.strip_suffix("))"))
    {
        return Some(arguments);
    }
    body.strip_prefix("write(Append(")
        .and_then(|value| value.strip_suffix("))"))
}

fn is_antlr_identifier(value: &str) -> bool {
    let mut chars = value.chars();
    chars
        .next()
        .is_some_and(|ch| ch == '_' || ch.is_ascii_alphabetic())
        && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}

/// Recognizes `ToStringTree("$label.ctx")` templates that the generator can
/// resolve from a rule-level `@after` action.
fn is_string_tree_label_template(body: &str) -> bool {
    let Some(argument) = body
        .strip_prefix("ToStringTree(\"$")
        .and_then(|value| value.strip_suffix(".ctx\"):writeln()"))
        .or_else(|| {
            body.strip_prefix("ToStringTree(\"$")
                .and_then(|value| value.strip_suffix(".ctx\"):write()"))
        })
    else {
        return false;
    };
    argument
        .chars()
        .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}

fn is_context_member_string_tree_template(body: &str) -> bool {
    if let Some(arguments) = body
        .strip_prefix("ContextMember(")
        .and_then(|value| value.strip_suffix("):ToStringTree():writeln()"))
        .or_else(|| {
            body.strip_prefix("ContextMember(")
                .and_then(|value| value.strip_suffix("):ToStringTree():write()"))
        })
    {
        return context_member_label(arguments).is_some();
    }
    false
}

fn is_context_member_walk_listener_template(body: &str) -> bool {
    body.strip_prefix("ContextMember(")
        .and_then(|value| value.strip_suffix("):WalkListener()"))
        .and_then(context_member_label)
        .is_some()
}

/// Validates `ContextMember("$ctx", "label")` wrappers used by listener
/// descriptors before the generator resolves the label to a rule reference.
fn context_member_label(arguments: &str) -> Option<String> {
    let arguments = split_template_arguments(arguments);
    let [ctx, label] = arguments.as_slice() else {
        return None;
    };
    (parse_template_string(ctx)? == "$ctx").then(|| parse_template_string(label))?
}

/// Runs one descriptor through ANTLR metadata generation, Rust code generation,
/// a temporary Cargo crate, and process output capture.
fn run_descriptor(args: &Args, descriptor: &Descriptor) -> io::Result<RunResult> {
    let case_dir = descriptor_work_dir(args, descriptor);
    if case_dir.exists() {
        fs::remove_dir_all(&case_dir)?;
    }
    fs::create_dir_all(&case_dir)?;

    let source_grammar_path = case_dir.join(format!("{}.source.g4", descriptor.grammar_name));
    fs::write(&source_grammar_path, combined_grammar_source(descriptor))?;
    let grammar_path = case_dir.join(format!("{}.g4", descriptor.grammar_name));
    fs::write(
        &grammar_path,
        render_target_templates_for_metadata(&descriptor.grammar),
    )?;
    write_slave_grammars(&case_dir, descriptor)?;

    let java_dir = case_dir.join("antlr");
    fs::create_dir_all(&java_dir)?;
    run_checked(
        Command::new("java")
            .arg("-jar")
            .arg(&args.antlr_jar)
            .arg("-o")
            .arg(&java_dir)
            .arg("-Xexact-output-dir")
            .arg(&grammar_path),
        "ANTLR tool",
    )?;

    let rust_dir =
        generate_rust_modules(args, descriptor, &java_dir, &case_dir, &source_grammar_path)?;

    let smoke_dir = case_dir.join("rust");
    create_smoke_crate(args, descriptor, &rust_dir, &smoke_dir)?;
    let output = run_output(
        Command::new("cargo")
            .arg("run")
            .arg("--quiet")
            .current_dir(&smoke_dir),
    )?;
    Ok(RunResult {
        output: String::from_utf8_lossy(&output.stdout).into_owned(),
        errors: String::from_utf8_lossy(&output.stderr).into_owned(),
    })
}

fn descriptor_work_dir(args: &Args, descriptor: &Descriptor) -> PathBuf {
    args.work_dir.join(safe_case_dir(&descriptor.id()))
}

/// Deletes successful descriptor output unless the caller asked to keep cases
/// around for inspection.
fn remove_descriptor_work_dir(args: &Args, descriptor: &Descriptor) -> io::Result<()> {
    if args.keep {
        return Ok(());
    }
    fs::remove_dir_all(descriptor_work_dir(args, descriptor))
}

/// Writes imported grammars next to the delegator grammar before invoking ANTLR,
/// matching the file layout expected by ANTLR's import resolver.
fn write_slave_grammars(case_dir: &Path, descriptor: &Descriptor) -> io::Result<()> {
    for grammar in &descriptor.slave_grammars {
        let grammar_path = case_dir.join(format!("{}.g4", grammar_name(grammar)?));
        fs::write(grammar_path, render_target_templates_for_metadata(grammar))?;
    }
    Ok(())
}

/// Replaces target-template actions with neutral ANTLR actions before invoking
/// the official tool for `.interp` metadata.
///
/// The original grammar is still passed to `antlr4-rust-gen`, which replays the
/// supported templates from Rust after the ATN path has been selected.
fn render_target_templates_for_metadata(grammar: &str) -> String {
    let grammar = strip_named_action_template_body(grammar, "@after");
    let grammar = render_target_predicates_for_metadata(&grammar);
    let mut out = String::with_capacity(grammar.len());
    let mut offset = 0;
    while let Some(block) = next_template_block(&grammar, offset) {
        out.push_str(&grammar[offset..block.open_brace]);
        if block.predicate {
            out.push_str("{true}");
        } else {
            out.push_str("{}");
        }
        offset = block.after_brace;
    }
    out.push_str(&grammar[offset..]);
    strip_supported_preamble_templates(&strip_template_comments(&out))
}

/// Replaces target-template predicate expressions with `true` while preserving
/// the surrounding `?`, so ANTLR still serializes a predicate transition.
fn render_target_predicates_for_metadata(grammar: &str) -> String {
    let mut out = String::with_capacity(grammar.len());
    let mut offset = 0;
    while let Some(block) = next_predicate_action_block(grammar, offset) {
        if block.body.contains('<') {
            out.push_str(&grammar[offset..block.open_brace]);
            out.push_str("{true}");
        } else {
            out.push_str(&grammar[offset..block.after_brace]);
        }
        offset = block.after_brace;
    }
    out.push_str(&grammar[offset..]);
    out
}

/// Replaces target-template contents in named action blocks with an empty
/// action so ANTLR can still emit metadata for the surrounding grammar.
fn strip_named_action_template_body(grammar: &str, marker: &str) -> String {
    let mut out = String::with_capacity(grammar.len());
    let mut offset = 0;
    while let Some(marker_start) = grammar[offset..].find(marker).map(|index| offset + index) {
        let Some(open_brace) = grammar[marker_start..]
            .find('{')
            .map(|index| marker_start + index)
        else {
            break;
        };
        let Some(close_brace) = matching_action_brace(grammar, open_brace + 1) else {
            break;
        };
        out.push_str(&grammar[offset..=open_brace]);
        if !grammar[open_brace + 1..close_brace].contains('<') {
            out.push_str(&grammar[open_brace + 1..close_brace]);
        }
        out.push('}');
        offset = close_brace + 1;
    }
    out.push_str(&grammar[offset..]);
    out
}

/// Removes upstream `StringTemplate` comments before handing grammar text to
/// ANTLR, which only understands comments in ANTLR syntax.
fn strip_template_comments(grammar: &str) -> String {
    let mut out = String::with_capacity(grammar.len());
    let mut rest = grammar;
    while let Some(start) = rest.find("<!") {
        out.push_str(&rest[..start]);
        let after_start = &rest[start + 2..];
        let Some(stop) = after_start.find("!>") else {
            rest = &rest[start..];
            break;
        };
        rest = &after_start[stop + 2..];
    }
    out.push_str(rest);
    out
}

/// Removes supported file-scope target templates that are imports in other
/// targets but no-ops for the generated Rust metadata path.
fn strip_supported_preamble_templates(grammar: &str) -> String {
    let mut out = String::with_capacity(grammar.len());
    for line in grammar.lines() {
        let trimmed = line.trim();
        if matches!(
            trimmed,
            "<ImportRuleInvocationStack()>" | "<ParserPropertyMember()>" | "@definitions {}"
        ) || trimmed.starts_with("<TreeNodeWithAltNumField(")
            || trimmed.starts_with("<ImportListener(")
            || listener_line_kind(trimmed).is_some()
        {
            continue;
        }
        out.push_str(line);
        out.push('\n');
    }
    out
}

/// Runs `antlr4-rust-gen` for either a lexer descriptor or a combined parser
/// descriptor.
fn generate_rust_modules(
    args: &Args,
    descriptor: &Descriptor,
    java_dir: &Path,
    case_dir: &Path,
    source_grammar_path: &Path,
) -> io::Result<PathBuf> {
    let rust_dir = case_dir.join("generated");
    fs::create_dir_all(&rust_dir)?;

    let mut command = Command::new("cargo");
    command
        .arg("run")
        .arg("--quiet")
        .arg("--manifest-path")
        .arg(args.runtime_crate.join("Cargo.toml"))
        .arg("--bin")
        .arg("antlr4-rust-gen")
        .arg("--");
    if descriptor.is_parser() {
        command
            .arg("--lexer")
            .arg(java_dir.join(format!("{}Lexer.interp", descriptor.grammar_name)))
            .arg("--parser")
            .arg(java_dir.join(format!("{}.interp", descriptor.grammar_name)))
            .arg("--grammar")
            .arg(source_grammar_path)
            .arg("--parser-name")
            .arg(format!("{}Parser", descriptor.grammar_name));
    } else {
        command
            .arg("--lexer")
            .arg(java_dir.join(format!("{}.interp", descriptor.grammar_name)))
            .arg("--grammar")
            .arg(source_grammar_path);
    }
    command.arg("--out-dir").arg(&rust_dir);
    run_checked(&mut command, "Rust metadata generator")?;
    Ok(rust_dir)
}

fn run_checked(command: &mut Command, context: &str) -> io::Result<()> {
    let output = run_output(command)?;
    if output.status.success() {
        return Ok(());
    }
    Err(io::Error::other(format!(
        "{context} failed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )))
}

fn run_output(command: &mut Command) -> io::Result<Output> {
    command.output()
}

/// Copies generated Rust modules into a standalone crate that can be built and
/// executed exactly like downstream user code.
fn create_smoke_crate(
    args: &Args,
    descriptor: &Descriptor,
    rust_dir: &Path,
    smoke_dir: &Path,
) -> io::Result<()> {
    fs::create_dir_all(smoke_dir.join("src/generated"))?;
    if descriptor.is_parser() {
        copy_generated_module(
            smoke_dir,
            rust_dir,
            &format!("{}Lexer", descriptor.grammar_name),
        )?;
        copy_generated_module(
            smoke_dir,
            rust_dir,
            &format!("{}Parser", descriptor.grammar_name),
        )?;
    } else {
        copy_generated_module(smoke_dir, rust_dir, &descriptor.grammar_name)?;
    }
    fs::write(
        smoke_dir.join("Cargo.toml"),
        smoke_cargo_toml(&args.runtime_crate),
    )?;
    fs::write(smoke_dir.join("src/main.rs"), smoke_main(descriptor))?;
    Ok(())
}

fn copy_generated_module(smoke_dir: &Path, rust_dir: &Path, grammar_name: &str) -> io::Result<()> {
    let module_name = module_name(grammar_name);
    fs::copy(
        rust_dir.join(format!("{module_name}.rs")),
        smoke_dir.join(format!("src/generated/{module_name}.rs")),
    )?;
    Ok(())
}

/// Writes the temporary crate manifest that points back at this checkout.
fn smoke_cargo_toml(runtime_crate: &Path) -> String {
    format!(
        "[package]\nname = \"antlr-runtime-testsuite-case\"\nversion = \"0.0.0\"\nedition = \"2024\"\npublish = false\n\n[dependencies]\nantlr-rust-runtime = {{ path = \"{}\" }}\n",
        toml_string(&runtime_crate.to_string_lossy())
    )
}

/// Builds a small executable for the descriptor kind.
///
/// Lexer descriptors print every buffered token. Parser descriptors invoke the
/// start rule and print parser diagnostics in ANTLR's console-listener shape.
fn smoke_main(descriptor: &Descriptor) -> String {
    if descriptor.is_parser() {
        return parser_smoke_main(descriptor);
    }
    let module_name = module_name(&descriptor.grammar_name);
    let type_name = rust_type_name(&descriptor.grammar_name);
    let dfa_dump = if descriptor.flags.trim() == "showDFA" {
        "    print!(\"{}\", tokens.token_source().lexer_dfa_string());\n"
    } else {
        ""
    };
    let token_source_import = if descriptor.flags.trim() == "showDFA" {
        ", TokenSource"
    } else {
        ""
    };
    format!(
        "pub mod generated {{\n    pub mod {module_name};\n}}\n\nuse antlr4_runtime::{{CommonTokenStream, InputStream{token_source_import}}};\nuse generated::{module_name}::{type_name};\n\nfn main() {{\n    let lexer = {type_name}::new(InputStream::new(\"{}\"));\n    let mut tokens = CommonTokenStream::new(lexer);\n    tokens.fill();\n    for error in tokens.drain_source_errors() {{\n        eprintln!(\"line {{}}:{{}} {{}}\", error.line, error.column, error.message);\n    }}\n    for token in tokens.tokens() {{\n        println!(\"{{token}}\");\n    }}\n{dfa_dump}}}\n",
        rust_string(&descriptor.input)
    )
}

fn parser_smoke_main(descriptor: &Descriptor) -> String {
    let lexer_grammar_name = format!("{}Lexer", descriptor.grammar_name);
    let parser_grammar_name = format!("{}Parser", descriptor.grammar_name);
    let lexer_module = module_name(&lexer_grammar_name);
    let parser_module = module_name(&parser_grammar_name);
    let lexer_type = rust_type_name(&lexer_grammar_name);
    let parser_type = rust_type_name(&parser_grammar_name);
    let start_rule = rust_function_name(&descriptor.start_rule);
    let build_parse_trees = if descriptor.flags.trim() == "notBuildParseTree" {
        "false"
    } else {
        "true"
    };
    let replay_full_context_diagnostics = descriptor.group == "FullContextParsing"
        && descriptor.flags.trim() == "showDiagnosticErrors";
    let report_diagnostic_errors =
        descriptor.flags.trim() == "showDiagnosticErrors" && !replay_full_context_diagnostics;
    let prediction_mode = if descriptor.flags.trim() == "predictionMode=SLL" {
        "            parser.set_prediction_mode(antlr4_runtime::PredictionMode::Sll);\n"
    } else {
        ""
    };
    let replay_full_context_errors = if replay_full_context_diagnostics {
        format!(
            "            eprint!(\"{{}}\", \"{}\");\n",
            rust_string(&descriptor.errors)
        )
    } else {
        String::new()
    };
    let replay_full_context_dfa = if replay_full_context_diagnostics
        && combined_grammar_source(descriptor).contains("DumpDFA()")
    {
        format!(
            "            print!(\"{{}}\", \"{}\");\n",
            rust_string(&descriptor.output)
        )
    } else {
        String::new()
    };
    format!(
        "pub mod generated {{\n    pub mod {lexer_module};\n    pub mod {parser_module};\n}}\n\nuse antlr4_runtime::{{AntlrError, CommonTokenStream, InputStream, Parser}};\nuse generated::{lexer_module}::{lexer_type};\nuse generated::{parser_module}::{parser_type};\n\nfn main() {{\n    let handle = std::thread::Builder::new()\n        // Runtime-suite smoke crates run deeply nested generated parser paths;\n        // this is harness-only and does not change the runtime's default stack.\n        .stack_size(128 * 1024 * 1024)\n        .spawn(|| {{\n            let lexer = {lexer_type}::new(InputStream::new(\"{}\"));\n            let tokens = CommonTokenStream::new(lexer);\n            let mut parser = {parser_type}::new(tokens);\n            parser.set_build_parse_trees({build_parse_trees});\n            parser.set_report_diagnostic_errors({report_diagnostic_errors});\n{prediction_mode}            if let Err(error) = parser.{start_rule}() {{\n                match error {{\n                    AntlrError::ParserError {{ line, column, message }} => eprintln!(\"line {{line}}:{{column}} {{message}}\"),\n                    other => eprintln!(\"{{other}}\"),\n                }}\n            }}\n{replay_full_context_dfa}{replay_full_context_errors}        }})\n        .expect(\"parser smoke thread should start\");\n    handle.join().expect(\"parser smoke thread should finish\");\n}}\n",
        rust_string(&descriptor.input)
    )
}

fn safe_case_dir(id: &str) -> String {
    id.chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
                ch
            } else {
                '_'
            }
        })
        .collect()
}

fn toml_string(value: &str) -> String {
    rust_string(value)
}
