//! Conformance corpus for OER/2 receipts (`oer-conformance/`).
//!
//! reason-set-equivalence: for every published vector, a conforming verifier MUST
//! report exactly the set of reasons the vector declares — the valid vectors none,
//! each tampered twin exactly its own. A verifier that fails a vector for the wrong
//! reason does not conform.
//!
//! The corpus holds vector files from two independent producers — `vectors.json`
//! from the reference generator (Python) and `firmware_vectors.json` from the
//! portable C kernel in `settle/` — plus `edge_vectors.json`, one hand-declared case
//! for each reason and number shape the upstream nine never exercise, and
//! `job_vectors.json`, the hand-declared cases of minor version 1's job receipts (the
//! unmetered marker and the acquisition class, §3.1). This binary is the third
//! implementation and checks all three files, plus the two fixtures — canonical
//! form and stage list — the vectors cannot check.
//!
//!   wai_oer_vectors verify <corpus-dir>
//!
//! There is no `gen`: the vectors are produced by the reference generator and the C
//! kernel, which is the point — a verifier that also wrote its own vectors would be
//! checking itself.

use std::path::Path;
use wai::oer::{check_canonical_form, load, run};

/// Each file the corpus must hold, and how many vectors it must contain, so a wrong
/// path or a truncated file fails instead of passing on nothing.
const FILES: &[(&str, usize)] = &[
    ("vectors.json", 9),          // the upstream draft's vectors (reference generator)
    ("firmware_vectors.json", 1), // the portable C kernel's output
    ("edge_vectors.json", 99),    // the reasons and number shapes the upstream nine never exercise
    ("job_vectors.json", 38),     // minor version 1's job receipts: the unmetered marker and the class (§3.1)
];

/// Documents a verifier must refuse whole (§4), in `unreadable/`: numbers whose
/// nearest binary64 is infinite, `NaN` and `Infinity`, an unpaired surrogate, nesting
/// past 64, and a repeated member name. Pinned by name, so a missing, renamed or
/// stray file fails rather than passes.
const UNREADABLE: &[&str] = &[
    "u01-midpoint-float.json",
    "u02-midpoint-integer.json",
    "u03-beyond-binary64.json",
    "u04-nan.json",
    "u05-infinity.json",
    "u06-negative-infinity.json",
    "u07-unpaired-surrogate.json",
    "u08-nested-65.json",
    "u09-duplicate-member.json",
    "u10-duplicate-figure-in-a-receipt.json",
    "u11-nested-5000.json",
];

fn main() {
    let a: Vec<String> = std::env::args().collect();
    match (a.get(1).map(String::as_str), a.get(2)) {
        (Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))),
        _ => {
            eprintln!("usage: wai_oer_vectors verify <corpus-dir>");
            std::process::exit(2);
        }
    }
}

fn verify(dir: &Path) -> i32 {
    let mut fail = 0usize;

    let cf = check_canonical_form();
    println!(
        "{} canonical form and stage list pinned to their fixtures  expected=  got={}",
        if cf.is_empty() { "PASS" } else { "FAIL" },
        cf.join(",")
    );
    fail += (!cf.is_empty()) as usize;

    for (file, want) in FILES {
        let path = dir.join(file);
        let doc: serde_json::Value = match std::fs::read(&path)
            .map_err(|e| e.to_string())
            .and_then(|b| load(&b))
        {
            Ok(d) => d,
            Err(e) => {
                println!("FAIL {file}: {e}");
                fail += 1;
                continue;
            }
        };
        match run(&doc) {
            Ok(results) => {
                if results.len() != *want {
                    println!("FAIL {file}: {} vectors, expected {want}", results.len());
                    fail += 1;
                }
                for r in results {
                    println!(
                        "{} {}  expected={}  got={}",
                        if r.pass { "PASS" } else { "FAIL" },
                        r.name,
                        r.expected.join(","),
                        r.got.join(",")
                    );
                    fail += (!r.pass) as usize;
                }
            }
            Err(e) => {
                println!("FAIL {file}: {e}");
                fail += 1;
            }
        }
    }

    // Only regular `.json` files are documents; the set present must be exactly the
    // pinned one, and a file that cannot be read is a failure, never a refusal.
    let listed = std::fs::read_dir(dir.join("unreadable")).map(|d| {
        let mut v: Vec<String> = d
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().is_ok_and(|t| t.is_file()))
            .map(|e| e.file_name().to_string_lossy().into_owned())
            .filter(|n| n.ends_with(".json"))
            .collect();
        v.sort();
        v
    });
    match listed {
        Err(e) => {
            println!("FAIL unreadable/: {e}");
            fail += 1;
        }
        Ok(names) if names != UNREADABLE => {
            println!("FAIL unreadable/ holds {names:?}, expected {UNREADABLE:?}");
            fail += 1;
        }
        Ok(_) => {}
    }
    for name in UNREADABLE {
        match std::fs::read(dir.join("unreadable").join(name)) {
            Err(e) => {
                println!("FAIL unreadable/{name}: {e}");
                fail += 1;
            }
            Ok(b) => match load(&b) {
                Err(e) => println!("PASS unreadable/{name} refused  ({e})"),
                Ok(_) => {
                    println!("FAIL unreadable/{name} was read; it must be refused whole");
                    fail += 1;
                }
            },
        }
    }

    if fail == 0 {
        println!("all vectors pass: reason-set-equivalence holds");
        0
    } else {
        println!("{fail} failure(s)");
        1
    }
}
