//! Round-trip gate for the form-(A) projection.
//!
//! For every `examples/*.ail.json` fixture, this test:
//!
//! 1. Loads the original module via `ailang_core::load_module`.
//! 2. Prints it with `ailang_surface::print`.
//! 3. Re-parses the printed text with `ailang_surface::parse`.
//! 4. Canonicalises both modules and asserts byte-equal canonical JSON.
//!
//! In addition, the three hand-written exhibits (`hello.ailx`,
//! `box.ailx`, `list_map_poly.ailx`) are parsed and asserted to produce
//! canonical JSON identical to their corresponding `.ail.json`
//! fixtures. This is the human/AI authoring ground-truth check that
//! the form is writeable without going through the printer.

use std::path::{Path, PathBuf};

fn examples_dir() -> PathBuf {
    // `CARGO_MANIFEST_DIR` is the surface crate root; examples live two
    // levels up.
    let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    crate_dir.parent().unwrap().parent().unwrap().join("examples")
}

fn list_json_fixtures() -> Vec<PathBuf> {
    let dir = examples_dir();
    let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
        .unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()))
        .filter_map(|entry| entry.ok())
        .map(|e| e.path())
        .filter(|p| {
            p.file_name()
                .and_then(|n| n.to_str())
                .map(|n| {
                    // Round-trip every fixture. The 22b.1/22b.2/22b.3 filter
                    // was retired in 22b.4a once the Form-A parser+printer
                    // arms for ClassDef / InstanceDef landed.
                    n.ends_with(".ail.json")
                })
                .unwrap_or(false)
        })
        .collect();
    paths.sort();
    paths
}

#[test]
fn print_then_parse_round_trips_every_fixture() {
    let fixtures = list_json_fixtures();
    assert!(
        !fixtures.is_empty(),
        "no .ail.json fixtures found under {}",
        examples_dir().display()
    );

    let mut failures = Vec::<String>::new();
    let mut passed = 0usize;
    for path in &fixtures {
        match round_trip_one(path) {
            Ok(()) => passed += 1,
            Err(msg) => failures.push(format!("{}: {msg}", path.display())),
        }
    }
    if !failures.is_empty() {
        panic!(
            "round-trip failed for {} of {} fixtures (passed: {}):\n{}",
            failures.len(),
            fixtures.len(),
            passed,
            failures.join("\n")
        );
    }
    eprintln!("round-trip ok for {passed} fixtures");
}

fn round_trip_one(path: &Path) -> Result<(), String> {
    let original = ailang_core::load_module(path).map_err(|e| format!("load: {e}"))?;
    let text = ailang_surface::print(&original);
    let parsed = ailang_surface::parse(&text)
        .map_err(|e| format!("re-parse failed: {e}\n--- printed text ---\n{text}"))?;
    let bytes_orig = ailang_core::canonical::to_bytes(&original);
    let bytes_round = ailang_core::canonical::to_bytes(&parsed);
    if bytes_orig != bytes_round {
        let s_orig = String::from_utf8_lossy(&bytes_orig).into_owned();
        let s_round = String::from_utf8_lossy(&bytes_round).into_owned();
        return Err(format!(
            "canonical bytes differ.\noriginal: {s_orig}\nround:    {s_round}"
        ));
    }
    Ok(())
}

#[test]
fn handwritten_exhibits_match_json_fixtures() {
    let dir = examples_dir();
    let pairs: &[(&str, &str)] = &[
        ("hello.ailx", "hello.ail.json"),
        ("box.ailx", "box.ail.json"),
        ("list_map_poly.ailx", "list_map_poly.ail.json"),
    ];
    let mut failures = Vec::new();
    for (ailx, json) in pairs {
        let ailx_path = dir.join(ailx);
        let json_path = dir.join(json);
        let text = std::fs::read_to_string(&ailx_path)
            .unwrap_or_else(|e| panic!("read {}: {e}", ailx_path.display()));
        let parsed = match ailang_surface::parse(&text) {
            Ok(m) => m,
            Err(e) => {
                failures.push(format!("{ailx}: parse failed: {e}"));
                continue;
            }
        };
        let original = ailang_core::load_module(&json_path)
            .unwrap_or_else(|e| panic!("load {}: {e}", json_path.display()));
        let bytes_orig = ailang_core::canonical::to_bytes(&original);
        let bytes_parsed = ailang_core::canonical::to_bytes(&parsed);
        if bytes_orig != bytes_parsed {
            let s_orig = String::from_utf8_lossy(&bytes_orig).into_owned();
            let s_round = String::from_utf8_lossy(&bytes_parsed).into_owned();
            failures.push(format!(
                "{ailx} vs {json}: canonical bytes differ.\noriginal: {s_orig}\nparsed:   {s_round}"
            ));
        }
    }
    if !failures.is_empty() {
        panic!(
            "{} hand-written exhibit(s) failed:\n{}",
            failures.len(),
            failures.join("\n\n")
        );
    }
}
