//! Conformance corpus for the quantum-sim extension (`quantum-conformance/`).
//! statevector-equivalence: given the same `WQC` circuit, a conforming sink MUST
//! reconstruct the identical fixed-point statevector, hence the identical BLAKE3 —
//! hash equality, no tolerance. Each vector is a `.wqc` file + its expected circuit
//! hash and statevector hash (and, where a measurement is pinned, a shot-histogram
//! hash — shot-histogram-equivalence).
//!
//! The corpus has four trees:
//!
//! - `<dir>/`: `wai.quantum.circuit` (`WQC1`), `vectors/*.wqc` and
//!   `expected_hashes.json`. The four original vectors are frozen; three
//!   table-sensitive vectors follow them. An entry with no `capability` is a
//!   `wai.quantum.circuit` vector, as every entry of the original manifest is.
//! - `<dir>/v2/`: `wai.quantum.circuit2` (`WQC2`), the same layout and the same
//!   seven circuits, each entry naming its `capability`.
//! - `<dir>/reject/`: containers a conforming reader MUST refuse, and
//!   `reject.json`, which maps each file to the class it is refused as
//!   (`malformed`, `invalid` or `contract`).
//! - `<dir>/accept/`: containers a conforming reader MUST accept, at the edges
//!   of the reader's rules, and `accept.json`, which gives each one's reading:
//!   its capability, `n_qubits`, op count, `circuit_hash` and measurement.
//!
//! `verify` checks all four: each vector decodes to the capability its entry
//! names and reproduces its hashes, each reject file fails with its class, and
//! each accept file reads as listed.
//!
//! `verify <dir> --webgpu` (with the `quantum_fabric_webgpu` feature) runs the
//! same check once per WebGPU member this machine has, each vector's state
//! computed by the member instead of the native simulator. Each accept file is
//! also run on the member and must give what the native simulator gives: the
//! same state, or a refusal of the same class (the 255-qubit files are refused
//! as `too-many-qubits`). With no member it exits 2, listing the adapters
//! refused and why.
//!
//! `appendix` writes the phase tables of the extension's Appendix B, rendered
//! from the reference constants, into the generated part of the file it is
//! given (`extensions/quantum-sim.md`), so the tables are never typed by hand.
//!
//!   wai_quantum_vectors gen <dir>  |  verify <dir> [--webgpu]  |  appendix <quantum-sim.md>

use std::collections::BTreeSet;
use std::path::Path;
use wai::quantum::{accept_corpus, circuit_hash, from_wqc, reject_corpus, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure, StateVector, DYADIC_MAX};

/// The circuit2 tree, under the corpus directory.
const V2_DIR: &str = "v2";
/// The reject tree, under the corpus directory.
const REJECT_DIR: &str = "reject";
/// The reject tree's listing: file → class.
const REJECT_LISTING: &str = "reject.json";
/// The accept tree, under the corpus directory.
const ACCEPT_DIR: &str = "accept";
/// The accept tree's listing: file → reading.
const ACCEPT_LISTING: &str = "accept.json";

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)) => generate(Path::new(d)),
        (Some("verify"), Some(d)) => match a.get(3).map(String::as_str) {
            None => {
                if !summary(verify(Path::new(d))) {
                    std::process::exit(1);
                }
            }
            Some("--webgpu") => verify_webgpu(Path::new(d)),
            Some(_) => usage(),
        },
        (Some("appendix"), Some(f)) => write_appendix(Path::new(f)),
        _ => usage(),
    }
}

fn usage() -> ! {
    eprintln!("usage: wai_quantum_vectors gen <corpus-dir>  |  wai_quantum_vectors verify <corpus-dir> [--webgpu]  |  wai_quantum_vectors appendix <quantum-sim.md>");
    std::process::exit(2);
}

/// Print the outcome of a `verify`; whether it passed.
fn summary(fails: usize) -> bool {
    if fails > 0 {
        eprintln!("{fails} failed — NOT conformant");
        return false;
    }
    println!("all vectors pass: statevector-equivalence holds, every reject file is refused as listed, and every accept file reads as listed");
    true
}

/// `verify <dir> --webgpu`: the whole check once per WebGPU member, each state
/// computed by the member. Exits 2 when there is no member, 1 when any member
/// fails.
#[cfg(all(feature = "quantum_fabric_webgpu", not(target_arch = "wasm32")))]
fn verify_webgpu(dir: &Path) {
    use wai::quantum_fabric_webgpu::{blocking, WebGpuError};
    let d = blocking::discover().unwrap_or_else(|e| {
        eprintln!("verify --webgpu: adapter discovery did not resolve: {e}");
        std::process::exit(2);
    });
    for r in &d.refused {
        eprintln!("verify --webgpu: adapter {} ({}) refused: {}", r.key, r.adapter, r.error);
    }
    if d.found.is_empty() {
        eprintln!("verify --webgpu: no WebGPU member on this machine");
        std::process::exit(2);
    }
    let mut failed = 0;
    for m in &d.found {
        println!("verify --webgpu: member {}", m.id());
        let run = |c: &Circuit| {
            blocking::simulate_from(m, c, 0).map_err(|e| match e {
                WebGpuError::Circuit(q) => (q.kind(), q.to_string()),
                other => ("member", other.to_string()),
            })
        };
        if !summary(verify_with(dir, Some(&run))) {
            failed += 1;
        }
    }
    if failed > 0 {
        eprintln!("verify --webgpu: {failed} of {} members failed", d.found.len());
        std::process::exit(1);
    }
}

/// Without the WebGPU member in this build, `--webgpu` is an error.
#[cfg(not(all(feature = "quantum_fabric_webgpu", not(target_arch = "wasm32"))))]
fn verify_webgpu(_dir: &Path) {
    eprintln!("verify --webgpu: this build has no WebGPU member; build wai_quantum_vectors natively with --features quantum_fabric_webgpu");
    std::process::exit(2);
}

fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}

/// The canonical vectors, in the gateset `gs`.
///
/// The first four: a Bell pair, a 3-qubit GHZ, and QFTs of 3 and 5 qubits (the
/// "quantum FFT" — Hadamard + controlled-phase ladder + swaps). The Bell pair
/// also pins a measurement so the corpus exercises shot-histogram-equivalence.
/// Every controlled phase of a QFT from |0…0⟩ acts while its control is |0⟩, so
/// these four see `H` but no `P(k)`.
///
/// The three that follow see the phase table. `qft5_x22` and `qft8_ones` run
/// the QFT from a basis state with set bits. `dyadic_ladder` sets qubit 0,
/// applies Y, Z, S, S†, T, T† and I to qubit 1, and then every `P(1..=32)`,
/// twice, to qubit 1 under qubit 0 as control, so a change of ±1 to either
/// component of any table entry changes its statevector hash.
fn vectors(gs: Gateset) -> Vec<(&'static str, Circuit, Option<Measure>)> {
    let mut bell = Circuit::with_gateset(2, gs);
    bell.h(0).cx(0, 1);

    let mut ghz = Circuit::with_gateset(3, gs);
    ghz.h(0).cx(0, 1).cx(1, 2);

    let qft = |n: u8| {
        let mut c = Circuit::qft(n);
        c.gateset = gs;
        c
    };
    let x_then_qft = |n: u8, xs: &[u8]| {
        let mut c = Circuit::with_gateset(n, gs);
        for &q in xs {
            c.x(q);
        }
        c.ops.extend(Circuit::qft(n).ops);
        c
    };

    let mut ladder = Circuit::with_gateset(2, gs);
    ladder.x(0).y(1).z(1).s(1);
    let one = |base| Gate { base, controls: vec![], target: 1, param: 0 };
    ladder.ops.push(one(BaseGate::Sdg));
    ladder.t(1);
    ladder.ops.push(one(BaseGate::Tdg));
    ladder.ops.push(one(BaseGate::I));
    for _ in 0..2 {
        for k in 1..=32 {
            ladder.cp(k, 0, 1);
        }
    }

    vec![
        ("bell", bell, Some(Measure { seed: 0xC0FFEE, shots: 100_000 })),
        ("ghz3", ghz, None),
        ("qft3", qft(3), None),
        ("qft5", qft(5), None),
        ("qft5_x22", x_then_qft(5, &[1, 2, 4]), None),
        ("dyadic_ladder", ladder, None),
        ("qft8_ones", x_then_qft(8, &[0, 1, 2, 3, 4, 5, 6, 7]), None),
    ]
}

/// One vector tree: `vectors/<name>.wqc` for each vector and its
/// `expected_hashes.json`, as (path under `prefix`, bytes). With
/// `name_capability`, each entry names its capability first.
fn tree_files(prefix: &str, gs: Gateset, name_capability: bool) -> Vec<(String, Vec<u8>)> {
    let mut files = Vec::new();
    let mut manifest = serde_json::Map::new();
    for (name, circuit, measure) in vectors(gs) {
        let bytes = to_wqc(&circuit, measure).expect("a corpus circuit is valid");
        files.push((format!("{prefix}vectors/{name}.wqc"), bytes));
        let sv = circuit.simulate().expect("simulate");
        let mut entry = serde_json::Map::new();
        if name_capability {
            entry.insert("capability".into(), gs.capability().into());
        }
        entry.insert("n_qubits".into(), circuit.n_qubits.into());
        entry.insert("n_ops".into(), circuit.ops.len().into());
        entry.insert("circuit_hash".into(), hex(&circuit_hash(&circuit).expect("a corpus circuit is valid")).into());
        entry.insert("statevector_hash".into(), hex(&sv.statevector_hash()).into());
        if let Some(m) = measure {
            entry.insert(
                "measure".into(),
                serde_json::json!({
                    "seed": m.seed,
                    "shots": m.shots,
                    "histogram_hash": hex(&sv.histogram_hash(m.seed, m.shots)),
                }),
            );
        }
        manifest.insert(format!("{name}.wqc"), entry.into());
    }
    let text = serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n";
    files.push((format!("{prefix}expected_hashes.json"), text.into_bytes()));
    files
}

/// The reject tree: `<name>.wqc` for each case and the listing, as (path under
/// `prefix`, bytes). Each case is checked here to fail with its class, so `gen`
/// never writes a reject file the reader accepts.
fn reject_files(prefix: &str) -> Vec<(String, Vec<u8>)> {
    let mut files = Vec::new();
    let mut listing = serde_json::Map::new();
    for (name, bytes, kind) in reject_corpus() {
        match from_wqc(&bytes) {
            Ok(_) => panic!("reject case {name} is accepted"),
            Err(e) => assert_eq!(e.kind(), kind, "reject case {name}: {e}"),
        }
        listing.insert(format!("{name}.wqc"), kind.into());
        files.push((format!("{prefix}{name}.wqc"), bytes));
    }
    let text = serde_json::to_string_pretty(&serde_json::Value::Object(listing)).unwrap() + "\n";
    files.push((format!("{prefix}{REJECT_LISTING}"), text.into_bytes()));
    files
}

/// How a container reads, as an accept listing states it.
fn reading(c: &Circuit, m: Option<Measure>) -> serde_json::Value {
    serde_json::json!({
        "capability": c.gateset.capability(),
        "n_qubits": c.n_qubits,
        "n_ops": c.ops.len(),
        "circuit_hash": hex(&circuit_hash(c).expect("an accepted circuit hashes")),
        "measure": m.map(|m| serde_json::json!({ "seed": m.seed, "shots": m.shots })),
    })
}

/// The accept tree: `<name>.wqc` for each case and the listing of its reading,
/// as (path under `prefix`, bytes). The listing is the reading the case
/// states, and each case is checked here to read so, so `gen` never writes an
/// accept file the reader refuses or misreads.
fn accept_files(prefix: &str) -> Vec<(String, Vec<u8>)> {
    let mut files = Vec::new();
    let mut listing = serde_json::Map::new();
    for (name, bytes, circuit, measure) in accept_corpus() {
        match from_wqc(&bytes) {
            Ok(got) => assert_eq!(got, (circuit.clone(), measure), "accept case {name}"),
            Err(e) => panic!("accept case {name} is refused: {e}"),
        }
        listing.insert(format!("{name}.wqc"), reading(&circuit, measure));
        files.push((format!("{prefix}{name}.wqc"), bytes));
    }
    let text = serde_json::to_string_pretty(&serde_json::Value::Object(listing)).unwrap() + "\n";
    files.push((format!("{prefix}{ACCEPT_LISTING}"), text.into_bytes()));
    files
}

/// Every file `gen` writes, as (path under the corpus directory, bytes).
fn corpus_files() -> Vec<(String, Vec<u8>)> {
    let mut files = tree_files("", Gateset::V1, false);
    files.extend(tree_files(&format!("{V2_DIR}/"), Gateset::V2, true));
    files.extend(reject_files(&format!("{REJECT_DIR}/")));
    files.extend(accept_files(&format!("{ACCEPT_DIR}/")));
    files
}

fn generate(dir: &Path) {
    for (rel, bytes) in corpus_files() {
        let path = dir.join(&rel);
        std::fs::create_dir_all(path.parent().expect("a file has a directory")).expect("mkdir");
        std::fs::write(&path, &bytes).expect("write");
    }
    println!("generated quantum vectors into {}", dir.display());
}

// ---- Appendix B of the extension ---------------------------------------------------

/// The markers around the generated part of the extension's Appendix B.
const APPENDIX_BEGIN: &str = "<!-- generated: wai_quantum_vectors appendix; do not edit by hand -->";
const APPENDIX_END: &str = "<!-- end generated -->";

/// The phase tables of Appendix B, rendered from the reference constants: for
/// each gateset, oldest first, a subsection `B.<n>` named by its capability,
/// `P(1..=DYADIC_MAX)` as `(re, im)`, and the table's digest.
fn appendix_tables() -> String {
    let mut o = String::from("\n");
    for (i, gs) in Gateset::ALL.into_iter().enumerate() {
        o.push_str(&format!("### B.{} `{}`\n\n", i + 1, gs.capability()));
        o.push_str("| k | re P(k) | im P(k) |\n|---:|---:|---:|\n");
        for k in 1..=DYADIC_MAX {
            let p = gs.phase(k);
            o.push_str(&format!("| {k} | {} | {} |\n", p.re, p.im));
        }
        o.push_str(&format!("\nDigest: `{}`\n\n", hex(&gs.table_digest())));
    }
    o
}

/// `text` with the generated part of Appendix B replaced by
/// [`appendix_tables`], or `None` when it lacks the markers.
fn splice_appendix(text: &str) -> Option<String> {
    let b = text.find(APPENDIX_BEGIN)? + APPENDIX_BEGIN.len();
    let e = text[b..].find(APPENDIX_END)? + b;
    Some(format!("{}\n{}{}", &text[..b], appendix_tables(), &text[e..]))
}

fn write_appendix(path: &Path) {
    let text = std::fs::read_to_string(path).unwrap_or_else(|e| {
        eprintln!("{}: {e}", path.display());
        std::process::exit(1);
    });
    let Some(new) = splice_appendix(&text) else {
        eprintln!("{}: no generated Appendix B markers ({APPENDIX_BEGIN} … {APPENDIX_END})", path.display());
        std::process::exit(1);
    };
    if new == text {
        println!("{}: Appendix B is current", path.display());
    } else {
        std::fs::write(path, new).expect("write");
        println!("{}: Appendix B rewritten from the phase tables", path.display());
    }
}

// ---- Verification ----------------------------------------------------------------

/// A member's run of a circuit: its state, or a refusal as (class, message).
/// The class of a refusal the circuit itself earns is its `QuantumError`'s
/// kind; a refusal of the member's own (device, width, range) is `"member"`.
type Simulate<'a> = dyn Fn(&Circuit) -> Result<StateVector, (&'static str, String)> + 'a;

/// Verify all four trees under `dir`; the number of failures.
fn verify(dir: &Path) -> usize {
    verify_with(dir, None)
}

/// Verify all four trees under `dir`, each vector's state computed by `member`
/// when one is given and by the native simulator otherwise; with a member,
/// each accept file must also run on it as it does natively. The number of
/// failures.
fn verify_with(dir: &Path, member: Option<&Simulate>) -> usize {
    verify_tree(dir, "", member)
        + verify_tree(&dir.join(V2_DIR), &format!("{V2_DIR}/"), member)
        + verify_rejects(&dir.join(REJECT_DIR), &format!("{REJECT_DIR}/"))
        + verify_accepts(&dir.join(ACCEPT_DIR), &format!("{ACCEPT_DIR}/"), member)
}

/// The `.wqc` file names in `dir`.
fn wqc_names(dir: &Path) -> Result<BTreeSet<String>, String> {
    let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
    let mut names = BTreeSet::new();
    for e in entries {
        let name = e.map_err(|e| e.to_string())?.file_name().to_string_lossy().into_owned();
        if name.ends_with(".wqc") {
            names.insert(name);
        }
    }
    Ok(names)
}

/// A JSON object read from `path`.
fn read_object(path: &Path) -> Result<serde_json::Map<String, serde_json::Value>, String> {
    let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
    match serde_json::from_slice(&bytes).map_err(|e| format!("{}: {e}", path.display()))? {
        serde_json::Value::Object(o) => Ok(o),
        _ => Err(format!("{}: not a JSON object", path.display())),
    }
}

/// Report one check: `PASS` or `FAIL` with the reason; 1 for a failure.
fn report(label: &str, r: Result<String, String>) -> usize {
    match r {
        Ok(note) => {
            println!("PASS {label}{note}");
            0
        }
        Err(why) => {
            println!("FAIL {label}: {why}");
            1
        }
    }
}

/// Each listed file and only those are present.
fn same_files(listed: BTreeSet<String>, present: Result<BTreeSet<String>, String>) -> Result<String, String> {
    let present = present?;
    let missing: Vec<_> = listed.difference(&present).collect();
    let unlisted: Vec<_> = present.difference(&listed).collect();
    if missing.is_empty() && unlisted.is_empty() {
        Ok(String::new())
    } else {
        Err(format!("listed but missing {missing:?}, present but unlisted {unlisted:?}"))
    }
}

/// A vector tree: `expected_hashes.json` and `vectors/`.
fn verify_tree(dir: &Path, label: &str, member: Option<&Simulate>) -> usize {
    let m = match read_object(&dir.join("expected_hashes.json")) {
        Ok(m) => m,
        Err(why) => return report(&format!("{label}expected_hashes.json"), Err(why)),
    };
    let v = dir.join("vectors");
    let mut fails = report(&format!("{label}vectors/ holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(&v)));
    for (file, spec) in &m {
        fails += report(&format!("{label}{file}"), check_vector(&v.join(file), spec, member));
    }
    fails
}

/// One vector against its manifest entry, its state computed by `member` when
/// one is given.
fn check_vector(path: &Path, spec: &serde_json::Value, member: Option<&Simulate>) -> Result<String, String> {
    let field = |k: &str| spec.get(k).ok_or(format!("the entry has no {k}"));
    let text = |k: &str| field(k).and_then(|v| v.as_str().ok_or(format!("{k} is not a string")));
    let number = |v: &serde_json::Value, k: &str| v.get(k).and_then(|x| x.as_u64()).ok_or(format!("{k} is not an unsigned integer"));
    // The original manifest names no capability: its entries are circuit/1.
    let capability = match spec.get("capability") {
        None => Gateset::V1.capability(),
        Some(c) => c.as_str().ok_or("capability is not a string")?,
    };
    let gs = Gateset::from_capability(capability).ok_or(format!("unknown capability {capability:?}"))?;
    let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
    let (circuit, measure) = from_wqc(&bytes).map_err(|e| e.to_string())?;
    if circuit.gateset != gs {
        return Err(format!("the file is {}, the entry names {capability}", circuit.gateset.capability()));
    }
    if u64::from(circuit.n_qubits) != number(spec, "n_qubits")? || circuit.ops.len() as u64 != number(spec, "n_ops")? {
        return Err("n_qubits or n_ops differ from the entry".into());
    }
    let sv = match member {
        None => circuit.simulate().map_err(|e| e.to_string())?,
        Some(run) => run(&circuit).map_err(|(_, why)| format!("the member refused it: {why}"))?,
    };
    let mut bad = Vec::new();
    if hex(&circuit_hash(&circuit).map_err(|e| e.to_string())?) != text("circuit_hash")? {
        bad.push("circuit_hash");
    }
    if hex(&sv.statevector_hash()) != text("statevector_hash")? {
        bad.push("statevector_hash");
    }
    match (measure, spec.get("measure")) {
        (Some(mm), Some(ms)) => {
            let hist = ms.get("histogram_hash").and_then(|h| h.as_str()).ok_or("histogram_hash is not a string")?;
            if mm.seed != number(ms, "seed")? || mm.shots != number(ms, "shots")? {
                bad.push("measure seed or shots");
            }
            if hex(&sv.histogram_hash(mm.seed, mm.shots)) != hist {
                bad.push("histogram_hash");
            }
        }
        (None, None) => {}
        _ => bad.push("measure (present in only one of the file and the entry)"),
    }
    if bad.is_empty() { Ok(String::new()) } else { Err(format!("{} differ", bad.join(", "))) }
}

/// The reject tree: every listed file fails with its listed class.
fn verify_rejects(dir: &Path, label: &str) -> usize {
    let m = match read_object(&dir.join(REJECT_LISTING)) {
        Ok(m) => m,
        Err(why) => return report(&format!("{label}{REJECT_LISTING}"), Err(why)),
    };
    let mut fails = report(&format!("{label} holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(dir)));
    for (file, kind) in &m {
        let r = (|| -> Result<String, String> {
            let kind = kind.as_str().ok_or("the class is not a string")?;
            let bytes = std::fs::read(dir.join(file)).map_err(|e| e.to_string())?;
            match from_wqc(&bytes) {
                Ok(_) => Err(format!("accepted; it must be refused as {kind}")),
                Err(e) if e.kind() == kind => Ok(format!(" (refused: {kind})")),
                Err(e) => Err(format!("refused as {}, not {kind}: {e}", e.kind())),
            }
        })();
        fails += report(&format!("{label}{file}"), r);
    }
    fails
}

/// The accept tree: every listed file is accepted and reads as listed; with a
/// member, each also runs on it as it does natively.
fn verify_accepts(dir: &Path, label: &str, member: Option<&Simulate>) -> usize {
    let m = match read_object(&dir.join(ACCEPT_LISTING)) {
        Ok(m) => m,
        Err(why) => return report(&format!("{label}{ACCEPT_LISTING}"), Err(why)),
    };
    let mut fails = report(&format!("{label} holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(dir)));
    for (file, want) in &m {
        let r = (|| -> Result<String, String> {
            let bytes = std::fs::read(dir.join(file)).map_err(|e| e.to_string())?;
            let (c, measure) = from_wqc(&bytes).map_err(|e| format!("refused as {}: {e}", e.kind()))?;
            let got = reading(&c, measure);
            if &got != want {
                return Err(format!("read as {got}, the listing says {want}"));
            }
            match member {
                None => Ok(" (accepted, read as listed)".into()),
                Some(run) => runs_as_native(&c, run).map(|how| format!(" (accepted, read as listed; {how})")),
            }
        })();
        fails += report(&format!("{label}{file}"), r);
    }
    fails
}

/// `c` on the member gives what it gives natively: the same state, or a
/// refusal of the same class.
fn runs_as_native(c: &Circuit, run: &Simulate) -> Result<String, String> {
    match (c.simulate(), run(c)) {
        (Ok(native), Ok(on)) if native == on => Ok("the member's state is the native one".into()),
        (Ok(_), Ok(_)) => Err("the member's state differs from the native one".into()),
        (Err(e), Err((class, _))) if e.kind() == class => Ok(format!("refused as {class} natively and by the member")),
        (Err(e), Err((class, why))) => Err(format!("refused natively as {}, by the member as {class}: {why}", e.kind())),
        (Ok(_), Err((_, why))) => Err(format!("the member refused it: {why}")),
        (Err(e), Ok(_)) => Err(format!("the member ran it; natively it is refused: {e}")),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;
    use wai::quantum::MAX_QUBITS;

    fn corpus() -> PathBuf {
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance")
    }

    /// A scratch copy of what `gen` writes, removed when dropped.
    struct Scratch(PathBuf);
    impl Scratch {
        fn new(name: &str) -> Scratch {
            let dir = std::env::temp_dir().join(format!("wai_quantum_vectors-{name}-{}", std::process::id()));
            let _ = std::fs::remove_dir_all(&dir);
            generate(&dir);
            Scratch(dir)
        }
    }
    impl Drop for Scratch {
        fn drop(&mut self) {
            let _ = std::fs::remove_dir_all(&self.0);
        }
    }

    /// The committed corpus is what `gen` writes today, file for file and byte
    /// for byte, and it verifies.
    #[test]
    fn the_committed_corpus_is_what_gen_writes() {
        let dir = corpus();
        let files = corpus_files();
        // Two trees of seven vectors and a manifest, and the reject and accept
        // trees: a file per case and the listing.
        assert_eq!(files.len(), 2 * 8 + reject_corpus().len() + 1 + accept_corpus().len() + 1);
        for (rel, want) in files {
            let mut got = std::fs::read(dir.join(&rel)).unwrap_or_else(|e| panic!("{rel}: {e}"));
            if rel.ends_with(".json") {
                // A checkout may convert line endings of text files.
                got = String::from_utf8(got).unwrap().replace("\r\n", "\n").into_bytes();
            }
            assert!(got == want, "{rel} differs from what gen writes");
        }
        assert_eq!(verify(&dir), 0);
    }

    /// The extension's Appendix B is what the reference constants render, and
    /// each table's digest, which §3.1 and §3.2 also state, appears there and
    /// in the appendix and nowhere else.
    #[test]
    fn the_extension_appendix_is_the_tables() {
        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions/quantum-sim.md");
        // A checkout may convert line endings of text files.
        let text = std::fs::read_to_string(&path).unwrap().replace("\r\n", "\n");
        let want = splice_appendix(&text).expect("the extension has the generated markers");
        assert!(
            text == want,
            "Appendix B of extensions/quantum-sim.md differs from the phase tables: \
             run `wai_quantum_vectors appendix ../extensions/quantum-sim.md` from wai/wai-rs"
        );
        for gs in Gateset::ALL {
            let digest = hex(&gs.table_digest());
            assert_eq!(text.matches(digest.as_str()).count(), 2, "{} digest {digest}", gs.capability());
        }
        let tables = appendix_tables();
        assert!(tables.contains("### B.1 `wai.quantum.circuit`\n"));
        assert!(tables.contains("### B.2 `wai.quantum.circuit2`\n"));
        assert!(tables.contains("| 3 | 759250124 | 759250124 |\n"));
        assert!(tables.contains("| 22 | 1073741824 | 1608 |\n"));
        let rows = tables.lines().filter(|l| l.starts_with("| ") && l.as_bytes()[2].is_ascii_digit()).count();
        assert_eq!(rows, Gateset::ALL.len() * DYADIC_MAX);
    }

    /// `verify` fails a corpus whose v2 entry names the other capability, whose
    /// reject listing names an accepted file or the wrong class, whose accept
    /// listing gives another reading or names a refused file, or that holds a
    /// file its listing does not.
    #[test]
    fn verify_fails_what_it_should() {
        let s = Scratch::new("verify");
        assert_eq!(verify(&s.0), 0);
        let edit = |rel: &str, from: &str, to: &str| {
            let p = s.0.join(rel);
            let text = std::fs::read_to_string(&p).unwrap();
            assert!(text.contains(from), "{from}");
            std::fs::write(&p, text.replacen(from, to, 1)).unwrap();
        };
        // A circuit2 file under a circuit/1 entry.
        edit("v2/expected_hashes.json", "\"wai.quantum.circuit2\"", "\"wai.quantum.circuit\"");
        assert_eq!(verify(&s.0), 1);
        edit("v2/expected_hashes.json", "\"wai.quantum.circuit\"", "\"wai.quantum.circuit2\"");
        // A refused file listed under another class.
        edit("reject/reject.json", "\"v2_param_on_h.wqc\": \"invalid\"", "\"v2_param_on_h.wqc\": \"malformed\"");
        assert_eq!(verify(&s.0), 1);
        edit("reject/reject.json", "\"v2_param_on_h.wqc\": \"malformed\"", "\"v2_param_on_h.wqc\": \"invalid\"");
        // An accept entry with another reading.
        edit("accept/accept.json", "\"seed\": 1,", "\"seed\": 2,");
        assert_eq!(verify(&s.0), 1);
        edit("accept/accept.json", "\"seed\": 2,", "\"seed\": 1,");
        // A refused file in the accept tree.
        std::fs::copy(s.0.join("reject/v2_param_on_h.wqc"), s.0.join("accept/v1_whitespace.wqc")).unwrap();
        assert_eq!(verify(&s.0), 1);
        std::fs::copy(s.0.join("vectors/bell.wqc"), s.0.join("accept/v1_whitespace.wqc")).unwrap();
        assert_eq!(verify(&s.0), 1, "bell has a measurement; v1_whitespace has none");
        std::fs::remove_file(s.0.join("accept/v1_whitespace.wqc")).unwrap();
        generate(&s.0);
        assert_eq!(verify(&s.0), 0);
        // An accepted file in the reject tree.
        std::fs::copy(s.0.join("v2/vectors/bell.wqc"), s.0.join("reject/v2_param_on_h.wqc")).unwrap();
        assert_eq!(verify(&s.0), 1);
        // A file no listing names, in the reject tree and in a vector tree.
        std::fs::write(s.0.join("reject/stray.wqc"), b"WQC2").unwrap();
        std::fs::copy(s.0.join("vectors/bell.wqc"), s.0.join("v2/vectors/stray.wqc")).unwrap();
        assert_eq!(verify(&s.0), 3);
        // A missing tree.
        std::fs::remove_dir_all(s.0.join("v2")).unwrap();
        assert_eq!(verify(&s.0), 3);
    }

    /// With a member, `verify` passes when the member gives the native states
    /// and refusals, and fails each vector and accept file where it does not:
    /// the 255-qubit accept files must be refused as too-many-qubits.
    #[test]
    fn verify_with_a_member_holds_it_to_the_native_simulator() {
        let dir = corpus();
        let native = |c: &Circuit| c.simulate().map_err(|e| (e.kind(), e.to_string()));
        assert_eq!(verify_with(&dir, Some(&native)), 0);
        let accepts = accept_corpus();
        let wide = accepts.iter().filter(|(_, _, c, _)| c.simulate().is_err()).count();
        assert!(wide > 0, "an accept file is refused at simulate");
        assert!(
            accepts.iter().all(|(_, _, c, _)| c.simulate().is_err() == (c.n_qubits > MAX_QUBITS)),
            "the accept files refused at simulate are the ones too wide to simulate"
        );
        let n_vectors = 2 * vectors(Gateset::V1).len();
        // A member that changes one amplitude: every vector, and every accept
        // file but the ones both refuse.
        let off_by_one = |c: &Circuit| {
            native(c).map(|mut s| {
                s.amps[0].re += 1;
                s
            })
        };
        assert_eq!(verify_with(&dir, Some(&off_by_one)), n_vectors + accepts.len() - wide);
        // A member that refuses everything as its own: every vector and every
        // accept file, the 255-qubit ones included (its refusal is not the
        // circuit's class).
        let refuses = |_: &Circuit| -> Result<StateVector, (&'static str, String)> { Err(("member", "no device".to_string())) };
        assert_eq!(verify_with(&dir, Some(&refuses)), n_vectors + accepts.len());
    }
}
