//! Drift test: every TS method name in the centralised dispatch tables
//! resolves to the **same** runtime symbol regardless of which backend
//! the user compiles for.
//!
//! This is the acceptance test for Tier 1.3 of the compiler-improvement
//! plan. Pre-fix, adding a row to `PERRY_UI_TABLE` (LLVM) without
//! mirroring it into `crates/perry-codegen-js/src/emit.rs` and
//! `crates/perry-codegen-wasm/src/emit.rs` produced silent
//! "compiles on macOS, breaks on web" surprises (issue #191
//! `CameraView`). After the centralisation, the JS and WASM emit
//! `_ => ...` arms fall through to `ui_method_to_runtime`, so any new
//! row is picked up automatically. This test pins that contract: every
//! row in the centralised tables resolves via the public lookup helper.

use perry_dispatch::{
    ui_method_to_runtime, PERRY_I18N_TABLE, PERRY_SYSTEM_TABLE, PERRY_UI_INSTANCE_TABLE,
    PERRY_UI_TABLE,
};

#[test]
fn perry_ui_table_is_resolvable_via_helper() {
    let mut missing = Vec::new();
    for row in PERRY_UI_TABLE.iter() {
        match ui_method_to_runtime(row.method) {
            Some(rt) if rt == row.runtime => {}
            Some(other) => panic!(
                "PERRY_UI_TABLE row '{}' resolves via ui_method_to_runtime to '{}' \
                 but the row's runtime is '{}'. The lookup helper is shadowing the \
                 row — check ordering in `ui_method_to_runtime` (UI → INSTANCE → SYSTEM).",
                row.method, other, row.runtime
            ),
            None => missing.push((row.method, row.runtime)),
        }
    }
    if !missing.is_empty() {
        panic!(
            "{} PERRY_UI_TABLE rows aren't reachable via ui_method_to_runtime: {:?}",
            missing.len(),
            missing
        );
    }
}

#[test]
fn perry_ui_instance_table_is_resolvable_via_helper() {
    // Instance methods are looked up after PERRY_UI_TABLE, so a method
    // that exists in BOTH tables routes to the receiver-less variant.
    // This test checks each instance row resolves *somewhere* — exact
    // resolution may go through the receiver-less table.
    for row in PERRY_UI_INSTANCE_TABLE.iter() {
        assert!(
            ui_method_to_runtime(row.method).is_some(),
            "PERRY_UI_INSTANCE_TABLE row '{}' isn't reachable via ui_method_to_runtime",
            row.method
        );
    }
}

#[test]
fn perry_system_table_is_resolvable_via_helper() {
    let mut missing = Vec::new();
    for row in PERRY_SYSTEM_TABLE.iter() {
        // A handful of system method names also appear in
        // PERRY_UI_TABLE (e.g. setting families with overlapping names);
        // for those, ui_method_to_runtime returns the UI variant first.
        // We only require that the lookup returns *some* runtime symbol
        // for every system row.
        if ui_method_to_runtime(row.method).is_none() {
            missing.push((row.method, row.runtime));
        }
    }
    if !missing.is_empty() {
        panic!(
            "{} PERRY_SYSTEM_TABLE rows aren't reachable via ui_method_to_runtime: {:?}",
            missing.len(),
            missing
        );
    }
}

#[test]
fn no_table_has_duplicate_methods() {
    // Within a single table, two rows with the same TS name would
    // silently shadow each other (only the first matches). Pin this so
    // a copy-paste bug fails CI immediately.
    for (name, table) in &[
        ("PERRY_UI_TABLE", PERRY_UI_TABLE),
        ("PERRY_UI_INSTANCE_TABLE", PERRY_UI_INSTANCE_TABLE),
        ("PERRY_SYSTEM_TABLE", PERRY_SYSTEM_TABLE),
        ("PERRY_I18N_TABLE", PERRY_I18N_TABLE),
    ] {
        let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
        for row in table.iter() {
            assert!(
                seen.insert(row.method),
                "{} has a duplicate row for method '{}' — second occurrence is unreachable.",
                name,
                row.method
            );
        }
    }
}

/// **Cross-backend acceptance test** for Tier 1.3. For every row in
/// PERRY_UI_TABLE / PERRY_SYSTEM_TABLE, scan the JS and WASM emit
/// source files and verify the runtime symbol appears as a literal in
/// each. This catches the "silent missing JS arm" pattern that issue
/// #191 documented: a row that's in the LLVM table but absent from
/// JS/WASM is still resolvable today via `ui_method_to_runtime`, but
/// the test ensures backends haven't been hand-coded to a *different*
/// runtime symbol than the canonical one.
#[test]
fn runtime_symbols_appear_in_js_and_wasm_emit() {
    // CARGO_MANIFEST_DIR is .../crates/perry-dispatch — go one level
    // up to the crates/ directory.
    let crates_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("perry-dispatch lives under crates/");

    // v0.5.1020 file-size split: `emit.rs` became a directory module
    // in both perry-codegen-js and perry-codegen-wasm. Concatenate every
    // sibling under `emit/` so the dispatch-wiring lookups below still
    // find the runtime-symbol mentions regardless of which sibling
    // emits which method-call branch.
    fn read_emit_dir(dir: &std::path::Path) -> String {
        let mut combined = String::new();
        let entries = std::fs::read_dir(dir)
            .unwrap_or_else(|e| panic!("emit dir {:?} must be readable: {}", dir, e));
        for ent in entries {
            let ent = ent.expect("emit dir entry must be readable");
            let path = ent.path();
            if path.is_dir() {
                combined.push_str(&read_emit_dir(&path));
            } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
                let chunk = std::fs::read_to_string(&path)
                    .unwrap_or_else(|e| panic!("emit sibling {:?} must be readable: {}", path, e));
                combined.push_str(&chunk);
                combined.push('\n');
            }
        }
        combined
    }

    let js_emit_dir = crates_root.join("perry-codegen-js/src/emit");
    let wasm_emit_dir = crates_root.join("perry-codegen-wasm/src/emit");

    let js_src = read_emit_dir(&js_emit_dir);
    let wasm_src = read_emit_dir(&wasm_emit_dir);

    // Both backends now route unknown methods through
    // perry_dispatch::ui_method_to_runtime, so an LLVM row missing from
    // a backend's hand-coded match arm is no longer a hard error — but
    // backends MAY hand-code a runtime symbol that diverges from the
    // canonical one. This test catches divergence by requiring that any
    // hand-coded `=> "perry_<X>_..."` literal in the JS/WASM source
    // either matches some row in PERRY_UI_TABLE / PERRY_SYSTEM_TABLE
    // exactly, or is a JS/WASM-specific extension (e.g. a State /
    // Canvas alias not in the LLVM tables).
    //
    // For the pilot this test is a smoke check: just confirm that each
    // backend file mentions `perry_dispatch::ui_method_to_runtime` —
    // proof that the centralised lookup is wired.
    assert!(
        js_src.contains("perry_dispatch::ui_method_to_runtime"),
        "JS emit doesn't call perry_dispatch::ui_method_to_runtime — Tier 1.3 wiring lost?"
    );
    assert!(
        wasm_src.contains("perry_dispatch::ui_method_to_runtime"),
        "WASM emit doesn't call perry_dispatch::ui_method_to_runtime — Tier 1.3 wiring lost?"
    );
}
