//! Conformance corpus for the parameter-set pin (`pin-conformance/`, SPEC.md §3.1,
//! §4, §7, §7.1).
//!
//!   wai_pin_vectors verify   <corpus-dir>   every case gets its declared outcome, and
//!                                           every listing is byte for byte the listing
//!                                           of its set
//!   wai_pin_vectors generate <corpus-dir>   rewrite `listings/<name>.sha256` from
//!                                           `sets/<name>/`
//!
//! - `cases.json`: for a pin, a capability, the forms the sink reads and the
//!   artifacts it holds, the reason SPEC §3.1 names (or `ok`). Run through
//!   [`wai::pin::verify_bytes`], the in-memory core every sink in this tree uses.
//! - `selection.json`: for a `WAI2` envelope, the rendition SPEC §7.1 picks, the
//!   reason for each entry it skips, the companions applied with the pick, and the
//!   first component-role finding.
//! - `static.json`: the first finding a verifier makes without holding a parameter
//!   set ([`wai::container::Wai::check_pin`], [`wai::container::WaiMulti::check_pins`]).
//! - `unpinned.json`: the informative report `prior_unpinned` (SPEC §3.1, *Pin by
//!   default*; [`wai::container::Wai::check_unpinned`],
//!   [`wai::container::WaiMulti::check_unpinned`]).
//! - `listings/`: each `<name>.sha256` is the listing of `sets/<name>/`;
//!   `listings/bad/*` each break a listing rule (`set_malformed`).
//!
//! `pin-conformance/pin_verify.py` checks `cases.json`, `unpinned.json` and `listings/`
//! with code that shares nothing with this crate.

use std::collections::BTreeMap;
use std::path::Path;

use serde_json::Value;
use wai::container::{Manifest, ManifestV2, PinRefusal, Prior, Wai, WaiMulti};
use wai::pin::{listing_of_dir, parse_listing, verify_bytes, HeldBytes, READABLE_FORMS};

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

fn read_json(path: &Path) -> Result<Value, String> {
    let s = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
    serde_json::from_slice(&s).map_err(|e| format!("{}: {e}", path.display()))
}

/// The listings directly in `listings/` and the set directory each names.
fn listing_pairs(dir: &Path) -> Result<Vec<(String, std::path::PathBuf)>, String> {
    let mut out = Vec::new();
    let ld = dir.join("listings");
    for e in std::fs::read_dir(&ld).map_err(|e| format!("{}: {e}", ld.display()))? {
        let e = e.map_err(|e| e.to_string())?;
        let name = e.file_name().to_string_lossy().into_owned();
        if let Some(stem) = name.strip_suffix(".sha256") {
            if e.file_type().map_err(|e| e.to_string())?.is_file() {
                out.push((name.clone(), dir.join("sets").join(stem)));
            }
        }
    }
    out.sort();
    Ok(out)
}

fn generate(dir: &Path) -> i32 {
    let pairs = match listing_pairs(dir) {
        Ok(p) => p,
        Err(e) => {
            eprintln!("{e}");
            return 1;
        }
    };
    for (name, set) in pairs {
        match listing_of_dir(&set) {
            Ok(l) => {
                let p = dir.join("listings").join(&name);
                if let Err(e) = std::fs::write(&p, &l) {
                    eprintln!("write {}: {e}", p.display());
                    return 1;
                }
                println!("wrote {} ({} B, sha256 {})", p.display(), l.len(), wai::pin::sha256_hex(&l));
            }
            Err(e) => {
                eprintln!("{}: {e}", set.display());
                return 1;
            }
        }
    }
    0
}

fn verify(dir: &Path) -> i32 {
    let mut failures = Vec::new();
    let mut checked = 0usize;
    let mut fail = |what: String| failures.push(what);

    // listings/: byte equality with the set, and the bad ones are malformed.
    match listing_pairs(dir) {
        Ok(pairs) => {
            for (name, set) in pairs {
                checked += 1;
                let committed = std::fs::read(dir.join("listings").join(&name)).unwrap_or_default();
                match listing_of_dir(&set) {
                    Ok(l) if l == committed => {}
                    Ok(_) => fail(format!("listings/{name}: differs from the listing of {}", set.display())),
                    Err(e) => fail(format!("listings/{name}: {e}")),
                }
            }
        }
        Err(e) => fail(e),
    }
    let bad = dir.join("listings/bad");
    if let Ok(rd) = std::fs::read_dir(&bad) {
        for e in rd.flatten() {
            checked += 1;
            let b = std::fs::read(e.path()).unwrap_or_default();
            match parse_listing(&b) {
                Err(PinRefusal::SetMalformed { .. }) => {}
                other => fail(format!("listings/bad/{}: expected set_malformed, got {other:?}",
                                      e.file_name().to_string_lossy())),
            }
        }
    }

    // cases.json
    match read_json(&dir.join("cases.json")) {
        Ok(doc) => {
            for c in doc["cases"].as_array().into_iter().flatten() {
                checked += 1;
                let name = c["name"].as_str().unwrap_or("?");
                let got = run_case(dir, c).unwrap_or_else(|e| format!("error: {e}"));
                let want = c["expect"].as_str().unwrap_or("?");
                if got != want {
                    fail(format!("cases.json {name}: expected {want}, got {got}"));
                }
            }
        }
        Err(e) => fail(e),
    }

    // selection.json
    match read_json(&dir.join("selection.json")) {
        Ok(doc) => {
            for c in doc["cases"].as_array().into_iter().flatten() {
                checked += 1;
                let name = c["name"].as_str().unwrap_or("?");
                match run_selection(dir, c) {
                    Ok(got) if got == c["expect"] => {}
                    Ok(got) => fail(format!("selection.json {name}: expected {}, got {got}", c["expect"])),
                    Err(e) => fail(format!("selection.json {name}: {e}")),
                }
            }
        }
        Err(e) => fail(e),
    }

    // static.json
    match read_json(&dir.join("static.json")) {
        Ok(doc) => {
            for c in doc["cases"].as_array().into_iter().flatten() {
                checked += 1;
                let name = c["name"].as_str().unwrap_or("?");
                match run_static(&c["manifest"]) {
                    Ok(got) if got == c["expect"] => {}
                    Ok(got) => fail(format!("static.json {name}: expected {}, got {got}", c["expect"])),
                    Err(e) => fail(format!("static.json {name}: {e}")),
                }
            }
        }
        Err(e) => fail(e),
    }

    // unpinned.json
    match read_json(&dir.join("unpinned.json")) {
        Ok(doc) => {
            for c in doc["cases"].as_array().into_iter().flatten() {
                checked += 1;
                let name = c["name"].as_str().unwrap_or("?");
                match run_unpinned(&c["manifest"]) {
                    Ok(got) if got == c["expect"] => {}
                    Ok(got) => fail(format!("unpinned.json {name}: expected {}, got {got}", c["expect"])),
                    Err(e) => fail(format!("unpinned.json {name}: {e}")),
                }
            }
        }
        Err(e) => fail(e),
    }

    if failures.is_empty() {
        println!("pin-conformance: {checked} checks, all as declared");
        0
    } else {
        for f in &failures {
            eprintln!("FAIL {f}");
        }
        eprintln!("pin-conformance: {} of {checked} checks failed", failures.len());
        1
    }
}

/// The held artifacts of a case, read into memory: (bytes, files below the root).
type Loaded = Vec<(Vec<u8>, Option<BTreeMap<String, Vec<u8>>>)>;

fn load_held(dir: &Path, held: &Value) -> Result<Loaded, String> {
    let mut out = Vec::new();
    for h in held.as_array().into_iter().flatten() {
        if let Some(f) = h["file"].as_str() {
            out.push((std::fs::read(dir.join(f)).map_err(|e| format!("{f}: {e}"))?, None));
        } else if let Some(l) = h["listing"].as_str() {
            let bytes = std::fs::read(dir.join(l)).map_err(|e| format!("{l}: {e}"))?;
            let root = dir.join(h["root"].as_str().ok_or("a held listing names its root")?);
            // The files the listing names that exist below the root; a listing that
            // does not parse names none.
            let mut files = BTreeMap::new();
            if let Ok(lines) = parse_listing(&bytes) {
                for (p, _) in lines {
                    if let Ok(b) = std::fs::read(root.join(&p)) {
                        files.insert(p, b);
                    }
                }
            }
            out.push((bytes, Some(files)));
        } else {
            return Err(format!("held entry {h} is neither a file nor a listing"));
        }
    }
    Ok(out)
}

fn with_held<T>(loaded: &Loaded, f: impl FnOnce(&[HeldBytes<'_>]) -> T) -> T {
    let views: Vec<Option<BTreeMap<String, &[u8]>>> = loaded.iter()
        .map(|(_, files)| files.as_ref().map(|m| m.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect()))
        .collect();
    let held: Vec<HeldBytes<'_>> = loaded.iter().zip(views.iter())
        .map(|((bytes, _), v)| match v {
            Some(files) => HeldBytes::Set { listing: bytes, files },
            None => HeldBytes::File(bytes),
        })
        .collect();
    f(&held)
}

fn run_case(dir: &Path, c: &Value) -> Result<String, String> {
    let cap = c["capability"].as_str().ok_or("no capability")?;
    let prior: Prior = serde_json::from_value(c["pin"].clone()).map_err(|e| format!("pin: {e}"))?;
    let readable: Vec<String> = match c.get("readable") {
        Some(r) => serde_json::from_value(r.clone()).map_err(|e| e.to_string())?,
        None => READABLE_FORMS.iter().map(|s| s.to_string()).collect(),
    };
    let readable: Vec<&str> = readable.iter().map(String::as_str).collect();
    let loaded = load_held(dir, &c["held"])?;
    Ok(with_held(&loaded, |held| match verify_bytes(cap, &prior, held, &readable) {
        Ok(_) => "ok".to_string(),
        Err(r) => r.code().to_string(),
    }))
}

fn run_selection(dir: &Path, c: &Value) -> Result<Value, String> {
    let manifest: ManifestV2 = serde_json::from_value(c["manifest"].clone()).map_err(|e| format!("manifest: {e}"))?;
    let payloads: Vec<Vec<u8>> = c["payloads"].as_array().ok_or("no payloads")?
        .iter().map(|p| p.as_str().unwrap_or_default().as_bytes().to_vec()).collect();
    let m = WaiMulti::from_parts(manifest, payloads).map_err(|e| e.to_string())?;
    let advertised: Option<Vec<String>> = c["advertised"].as_array()
        .map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect());
    let held: Vec<Vec<u8>> = c["held"].as_array().into_iter().flatten()
        .map(|f| std::fs::read(dir.join(f.as_str().unwrap_or_default())).map_err(|e| format!("{f}: {e}")))
        .collect::<Result<_, _>>()?;
    let verify = |cap: &str, p: &Prior| {
        let hb: Vec<HeldBytes<'_>> = held.iter().map(|b| HeldBytes::File(b)).collect();
        verify_bytes(cap, p, &hb, READABLE_FORMS).map(|_| ())
    };
    let skips = |s: &[(usize, wai::container::Skip)]| -> Value {
        Value::Array(s.iter().map(|(i, k)| serde_json::json!([i, k.code()])).collect())
    };
    let mut out = serde_json::Map::new();
    // In the table's order, a deployer policy SPEC §7.1 step 2 allows: the cases' subject is
    // the pin, which the default energy order (SPEC §4) would not reach while a classical
    // entry is advertised before the learned ones (pin-conformance/README.md).
    let table = wai::container::DispatchPolicy::manifest_order();
    match m.pick_with(&table, |cap| advertised.as_ref().is_none_or(|a| a.iter().any(|x| x == cap)), verify) {
        Ok(p) => {
            out.insert("picked".into(), p.index.into());
            out.insert("skipped".into(), skips(&p.skipped));
            if c["expect"].get("companions").is_some() {
                let comps: Vec<usize> = m.companions_of_index(p.index).into_iter().map(|(i, _, _)| i).collect();
                out.insert("companions".into(), serde_json::json!(comps));
            }
        }
        Err(s) => {
            out.insert("inert".into(), skips(&s));
        }
    }
    if c["expect"].get("roles").is_some() {
        let roles = match m.check_component_roles() {
            Ok(()) => "ok".to_string(),
            Err(e) => format!("{e:?}").split([' ', '{']).next().unwrap_or_default().to_string(),
        };
        out.insert("roles".into(), roles.into());
    }
    Ok(Value::Object(out))
}

/// The report `prior_unpinned` (SPEC §3.1, *Pin by default*): for a `WAI1` manifest,
/// `prior_unpinned` or `none`; for a `WAI2` manifest, the index of every entry it is made
/// about.
fn run_unpinned(manifest: &Value) -> Result<Value, String> {
    match manifest["wai"].as_str() {
        Some("1.0") => {
            let m: Manifest = serde_json::from_value(manifest.clone()).map_err(|e| format!("manifest: {e}"))?;
            Ok(Wai::new(m, Vec::new()).check_unpinned().map_or("none", |f| f.code).into())
        }
        Some("1.1") => {
            let m: ManifestV2 = serde_json::from_value(manifest.clone()).map_err(|e| format!("manifest: {e}"))?;
            let n = m.renditions.len();
            let w = WaiMulti::from_parts(m, vec![Vec::new(); n]).map_err(|e| e.to_string())?;
            Ok(serde_json::json!(w.check_unpinned().iter().map(|(i, _)| *i).collect::<Vec<_>>()))
        }
        other => Err(format!("unknown wai version {other:?}")),
    }
}

fn run_static(manifest: &Value) -> Result<Value, String> {
    match manifest["wai"].as_str() {
        Some("1.0") => {
            let m: Manifest = serde_json::from_value(manifest.clone()).map_err(|e| format!("manifest: {e}"))?;
            Ok(match Wai::new(m, Vec::new()).check_pin() {
                Ok(()) => "ok".into(),
                Err(f) => f.code.into(),
            })
        }
        Some("1.1") => {
            let m: ManifestV2 = serde_json::from_value(manifest.clone()).map_err(|e| format!("manifest: {e}"))?;
            let n = m.renditions.len();
            let w = WaiMulti::from_parts(m, vec![Vec::new(); n]).map_err(|e| e.to_string())?;
            Ok(match w.check_pins() {
                Ok(()) => "ok".into(),
                Err((i, f)) => serde_json::json!([i, f.code]),
            })
        }
        other => Err(format!("unknown wai version {other:?}")),
    }
}
