//! Conformance corpus for conformance results (`conform-result-conformance/`,
//! extensions/conformance-results.md §7).
//!
//! Every vector is one record's bytes and the outcome §4 gives it: `accept conformant=<bool>`,
//! or `reject step=<n> reason=<code>`. A verifier that reaches another outcome does not
//! conform.
//!
//!   wai_conform_result_vectors gen    <corpus-dir>   write vectors.json
//!   wai_conform_result_vectors verify <corpus-dir>   vectors.json regenerates byte for byte, no
//!                                                    other file but the README and the second
//!                                                    implementation is there, and every vector
//!                                                    reaches its stated outcome
//!   wai_conform_result_vectors verdicts <file>       one `name<TAB>outcome` line per vector, any
//!                                                    stated outcome ignored
//!
//! The generator and this verifier are one implementation. The corpus's second
//! implementation is `conform-result-conformance/conform_result_verify.py`, written from the
//! extension's text and sharing no code with this binary.

use serde_json::Value;
use std::path::Path;
use wai::conform_result::{corpus_document, verdict};

fn main() {
    let a: Vec<String> = std::env::args().collect();
    match (a.get(1).map(String::as_str), a.get(2)) {
        (Some("gen"), Some(d)) => {
            let p = Path::new(d).join("vectors.json");
            if let Err(e) = std::fs::create_dir_all(d).and_then(|_| std::fs::write(&p, corpus_document())) {
                eprintln!("write {}: {e}", p.display());
                std::process::exit(1);
            }
            println!("wrote {}", p.display());
        }
        (Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))),
        (Some("verdicts"), Some(f)) => std::process::exit(verdicts(Path::new(f))),
        _ => {
            eprintln!("usage: wai_conform_result_vectors gen|verify <corpus-dir> | verdicts <file>");
            std::process::exit(2);
        }
    }
}

fn read_vectors(file: &Path) -> Result<Vec<Value>, String> {
    let text = std::fs::read_to_string(file).map_err(|e| format!("read {}: {e}", file.display()))?;
    let doc: Value = serde_json::from_str(&text).map_err(|e| format!("{}: {e}", file.display()))?;
    doc.get("vectors").and_then(Value::as_array).cloned().ok_or_else(|| format!("{}: no vectors", file.display()))
}

/// `name<TAB>outcome` for every vector of a corpus-format file.
fn verdicts(file: &Path) -> i32 {
    match read_vectors(file) {
        Ok(vs) => {
            for v in vs {
                let name = v.get("name").and_then(Value::as_str).unwrap_or("?");
                let record = v.get("record").and_then(Value::as_str).unwrap_or("");
                println!("{name}\t{}", verdict(record.as_bytes()));
            }
            0
        }
        Err(e) => {
            eprintln!("{e}");
            1
        }
    }
}

fn verify(dir: &Path) -> i32 {
    let mut fail = 0usize;
    let path = dir.join("vectors.json");
    let committed = std::fs::read(&path).unwrap_or_default();
    let same = committed == corpus_document().as_bytes();
    println!("{} vectors.json regenerates byte for byte", if same { "PASS" } else { "FAIL" });
    fail += !same as usize;
    let allowed = ["README.md", "vectors.json", "conform_result_verify.py", "__pycache__"];
    for e in std::fs::read_dir(dir).map(|r| r.flatten().collect::<Vec<_>>()).unwrap_or_default() {
        let n = e.file_name().to_string_lossy().into_owned();
        if !allowed.contains(&n.as_str()) {
            println!("FAIL {n}: a file the corpus does not hold");
            fail += 1;
        }
    }
    match read_vectors(&path) {
        Err(e) => {
            println!("FAIL {e}");
            fail += 1;
        }
        Ok(vs) => {
            for v in &vs {
                let name = v.get("name").and_then(Value::as_str).unwrap_or("?");
                let want = v.get("expected").and_then(Value::as_str).unwrap_or("");
                let got = verdict(v.get("record").and_then(Value::as_str).unwrap_or("").as_bytes());
                let ok = got == want;
                fail += !ok as usize;
                println!("{} {name}  expected={want}  got={got}", if ok { "PASS" } else { "FAIL" });
            }
            println!("{} vectors, {fail} failure(s)", vs.len());
        }
    }
    (fail > 0) as i32
}
