//! Surface-side loader: dispatches by file extension.
//!
//! `.ail` → `surface::parse` (Form A); `.ail.json` → `core::load_module`
//! (Form B). Workspace import resolution prefers `.ail` over `.ail.json`.

use ailang_surface::{load_module, load_workspace};
use std::fs;

/// Property: `load_module` on a `.ail` (Form A) source file dispatches
/// to `ailang_surface::parse` and returns the parsed module. Guards
/// against the dispatch silently falling through to the JSON loader
/// (which would fail on Form-A bytes with the misleading
/// `json: expected value at line 1 column 1` error the rest of this
/// iter exists to eliminate).
#[test]
fn load_module_dispatches_ail_to_surface_parse() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let path = tmp.path().join("hello.ail");
    // Smallest valid Form-A module: header + no defs.
    fs::write(&path, "(module hello)\n").expect("write");

    let module = load_module(&path).expect("surface parses .ail");
    assert_eq!(module.name, "hello");
}

/// Property: `load_module` on a `.ail.json` (Form B) source file
/// delegates to `ailang_core::load_module` and produces the same
/// `Module` value the core entry point would. Guards against the
/// surface loader silently shadowing or rewriting the JSON path.
#[test]
fn load_module_dispatches_ail_json_to_core_loader() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let path = tmp.path().join("dummy.ail.json");
    fs::write(
        &path,
        r#"{"schema":"ailang/v0","name":"dummy","imports":[],"defs":[]}"#,
    )
    .expect("write");

    let surface_loaded = load_module(&path).expect("surface dispatches .ail.json to core");
    let core_loaded = ailang_core::load_module(&path).expect("core loads directly");

    assert_eq!(surface_loaded.name, core_loaded.name);
    assert_eq!(surface_loaded.schema, core_loaded.schema);
}

/// Property: workspace import resolution accepts mixed extensions —
/// a `.ail` entry module can import a sibling provided as `.ail.json`,
/// and both modules end up in the loaded workspace. Guards against
/// the `.ail`-first / `.ail.json`-fallback precedence rule in
/// `workspace::visit` regressing back to JSON-only resolution.
#[test]
fn load_workspace_resolves_ail_imports_first_then_ail_json() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let entry = tmp.path().join("main.ail");
    // Surface grammar uses one `(import <name>)` clause per import.
    fs::write(&entry, "(module main (import leaf))\n").expect("write entry");

    let leaf = tmp.path().join("leaf.ail.json");
    fs::write(
        &leaf,
        r#"{"schema":"ailang/v0","name":"leaf","imports":[],"defs":[]}"#,
    )
    .expect("write leaf");

    let ws = load_workspace(&entry).expect("workspace loads with mixed extensions");
    assert!(ws.modules.contains_key("main"));
    assert!(ws.modules.contains_key("leaf"));
}

/// Property: when BOTH `<module>.ail` (Form A) and `<module>.ail.json`
/// (Form B) exist in the workspace root, the `.ail` sibling wins —
/// the loaded module's content reflects Form A, not Form B. Pins the
/// `surface_path.is_file()` precedence branch in
/// `workspace::visit`'s import-path construction. Regression would
/// look like the loader silently shadowing a freshly-edited `.ail`
/// with a stale `.ail.json` snapshot of the same module name.
#[test]
fn load_workspace_prefers_ail_when_both_extensions_exist() {
    let tmp = tempfile::tempdir().expect("tempdir");

    // Entry imports `leaf`.
    let entry = tmp.path().join("main.ail");
    fs::write(&entry, "(module main (import leaf))\n").expect("write entry");

    // Both leaf.ail and leaf.ail.json exist, with different in-source content.
    // The `.ail` carries a `(const winner ...)`; the `.ail.json` carries a
    // `(const loser ...)`. The loader must pick `winner`.
    let leaf_ail = tmp.path().join("leaf.ail");
    fs::write(
        &leaf_ail,
        "(module leaf (const winner (type (con Int)) (body 1)))\n",
    )
    .expect("write leaf.ail");

    let leaf_json = tmp.path().join("leaf.ail.json");
    fs::write(
        &leaf_json,
        r#"{"schema":"ailang/v0","name":"leaf","imports":[],"defs":[{"kind":"const","name":"loser","type":{"k":"con","name":"Int"},"value":{"t":"lit","lit":{"kind":"int","value":2}}}]}"#,
    )
    .expect("write leaf.ail.json");

    let ws = load_workspace(&entry).expect("workspace loads with both extensions present");
    let leaf = &ws.modules["leaf"];
    let names: Vec<&str> = leaf
        .defs
        .iter()
        .map(ailang_core::def_name)
        .collect();
    assert!(
        names.contains(&"winner"),
        "expected `.ail` content (def `winner`); got defs: {names:?}"
    );
    assert!(
        !names.contains(&"loser"),
        "must NOT have loaded `.ail.json` content (def `loser`); got defs: {names:?}"
    );
}
