//! Conformance results (`extensions/conformance-results.md`): the record `conform-result/1`
//! (§2), its signing input and signature (§3), its verification in the order of §4 with the
//! codes of §5, and the corpus of §7 (`conform-result-conformance/`).
//!
//! `wai conform` writes records with [`sign`]; `wai verify` checks them with [`check`]. The
//! record is read with the crate's strict JSON reader (a member named twice, an unpaired
//! surrogate escape or bytes after the value are refused, and each number keeps its spelling),
//! so a number is an integer only when it is written as one.

use crate::jcs::{MAX_INTEGER, jcs};
use crate::strict_json::{self, Json};
use ed25519_dalek::{Signer, SigningKey};
use serde_json::{Map, Value, json};
use sha2::{Digest, Sha256};

/// The value of a record's `wai` member (§2).
pub const FORMAT: &str = "conform-result/1";

/// The domain the signing input starts with (§3).
pub const SIGN_DOMAIN: &[u8] = b"wai:conform-result\x01";

/// Why a record is refused (§5).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Reason {
    Malformed,
    UnknownFormat,
    SignatureInvalid,
    SummaryMismatch,
    ConformantMismatch,
}

impl Reason {
    pub const ALL: [Reason; 5] = [Reason::Malformed, Reason::UnknownFormat, Reason::SignatureInvalid, Reason::SummaryMismatch, Reason::ConformantMismatch];

    pub fn code(self) -> &'static str {
        match self {
            Reason::Malformed => "malformed",
            Reason::UnknownFormat => "unknown_format",
            Reason::SignatureInvalid => "signature_invalid",
            Reason::SummaryMismatch => "summary_mismatch",
            Reason::ConformantMismatch => "conformant_mismatch",
        }
    }
}

/// A refusal: the step of §4 that failed, its reason, and what was found.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Refusal {
    pub step: u8,
    pub reason: Reason,
    pub detail: String,
}

impl std::fmt::Display for Refusal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "step {} {}: {}", self.step, self.reason.code(), self.detail)
    }
}

/// What an accepted record states.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Accepted {
    pub conformant: bool,
    pub cases: u64,
    pub passed: u64,
    pub kind: String,
    pub suite: String,
    pub standard: String,
    pub corpus_digest: String,
    pub implementation: String,
    pub key: String,
    pub signer_pubkey: String,
}

fn refuse<T>(step: u8, reason: Reason, detail: impl Into<String>) -> Result<T, Refusal> {
    Err(Refusal { step, reason, detail: detail.into() })
}

fn is_hex(s: &str, n: usize) -> bool {
    s.len() == n && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}

fn unhex(s: &str) -> Vec<u8> {
    (0..s.len() / 2).map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap_or(0)).collect()
}

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

/// `YYYY-MM-DDTHH:MM:SSZ` with month 01–12, day 01–31, hour 00–23, minute 00–59 and second
/// 00–60 (§2).
pub fn is_utc_second(s: &str) -> bool {
    let b = s.as_bytes();
    if b.len() != 20 || b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' || b[19] != b'Z' {
        return false;
    }
    let num = |a: usize, n: usize| -> Option<u32> { s.get(a..a + n).filter(|t| t.bytes().all(|c| c.is_ascii_digit())).and_then(|t| t.parse().ok()) };
    matches!(
        (num(0, 4), num(5, 2), num(8, 2), num(11, 2), num(14, 2), num(17, 2)),
        (Some(_), Some(1..=12), Some(1..=31), Some(0..=23), Some(0..=59), Some(0..=60))
    )
}

/// The members of an object, each named once; `None` unless `v` is an object whose member
/// names are exactly `names`.
fn exactly<'a>(v: &'a Json, names: &[&str]) -> Option<Vec<&'a Json>> {
    let m = v.as_obj()?;
    if m.len() != names.len() {
        return None;
    }
    names.iter().map(|n| v.get(n)).collect()
}

fn int(v: &Json) -> Option<u64> {
    v.as_u64().filter(|n| *n <= MAX_INTEGER)
}

fn string_or_null(v: &Json) -> bool {
    matches!(v, Json::Str(_) | Json::Null)
}

/// Step 3: the members of §2, each of its type and form. `Err` names the first one that is not.
fn members(r: &Json) -> Result<(), String> {
    const TOP: [&str; 14] = ["wai", "kind", "suite", "standard", "toolchain", "corpus", "implementation", "started_at", "cases", "summary", "conformant", "key", "signer_pubkey", "sig"];
    let m = exactly(r, &TOP).ok_or("the record's members are not exactly those of §2")?;
    let s = |i: usize| m[i].as_str();
    for (i, name) in [(1, "kind"), (2, "suite"), (3, "standard")] {
        s(i).ok_or(format!("{name} is not a string"))?;
    }
    let t = exactly(m[4], &["name", "version"]).ok_or("toolchain is not an object of name and version")?;
    if !t.iter().all(|x| x.as_str().is_some()) {
        return Err("toolchain's name and version are not strings".into());
    }
    let c = exactly(m[5], &["dir", "files", "digest"]).ok_or("corpus is not an object of dir, files and digest")?;
    if c[0].as_str().is_none() || int(c[1]).is_none() {
        return Err("corpus's dir is not a string, or its files not an integer".into());
    }
    if !c[2].as_str().and_then(|d| d.strip_prefix("sha256:")).is_some_and(|h| is_hex(h, 64)) {
        return Err("corpus's digest is not sha256: and 64 lowercase hexadecimal digits".into());
    }
    let im = exactly(m[6], &["adapter", "impl", "name", "version", "files"]).ok_or("implementation is not an object of adapter, impl, name, version and files")?;
    if !matches!(im[0].as_str(), Some("command" | "http")) {
        return Err("implementation's adapter is neither command nor http".into());
    }
    if im[1].as_str().is_none() || !string_or_null(im[2]) || !string_or_null(im[3]) {
        return Err("implementation's impl is not a string, or its name or version neither a string nor null".into());
    }
    for f in im[4].as_arr().ok_or("implementation's files is not an array")? {
        let f = exactly(f, &["path", "sha256"]).ok_or("an implementation file is not an object of path and sha256")?;
        if f[0].as_str().is_none() || !f[1].as_str().is_some_and(|h| is_hex(h, 64)) {
            return Err("an implementation file's path is not a string, or its sha256 not 64 lowercase hexadecimal digits".into());
        }
    }
    if !s(7).is_some_and(is_utc_second) {
        return Err("started_at is not YYYY-MM-DDTHH:MM:SSZ".into());
    }
    for case in m[8].as_arr().ok_or("cases is not an array")? {
        let c = exactly(case, &["name", "pass", "expected", "got"]).ok_or("a case is not an object of name, pass, expected and got")?;
        if c[0].as_str().is_none() || !matches!(c[1], Json::Bool(_)) || c[2].as_str().is_none() || c[3].as_str().is_none() {
            return Err("a case's name, expected or got is not a string, or its pass not a boolean".into());
        }
    }
    let sm = exactly(m[9], &["cases", "passed", "failed"]).ok_or("summary is not an object of cases, passed and failed")?;
    if !sm.iter().all(|x| int(x).is_some()) {
        return Err("summary's counts are not integers from 0 to 2^53 − 1".into());
    }
    if !matches!(m[10], Json::Bool(_)) {
        return Err("conformant is not a boolean".into());
    }
    if !matches!(s(11), Some("ephemeral" | "supplied")) {
        return Err("key is neither ephemeral nor supplied".into());
    }
    if !s(12).is_some_and(|h| is_hex(h, 64)) || !s(13).is_some_and(|h| is_hex(h, 128)) {
        return Err("signer_pubkey or sig is not lowercase hexadecimal of its length".into());
    }
    Ok(())
}

/// A record read by the strict reader, as the serde value the signing input is written from.
/// Every number in it is a plain integer by step 3.
fn to_value(j: &Json) -> Value {
    match j {
        Json::Null => Value::Null,
        Json::Bool(b) => Value::Bool(*b),
        Json::Num(n) => n.parse::<u64>().map(Value::from).unwrap_or(Value::Null),
        Json::Str(s) => Value::String(s.clone()),
        Json::Arr(a) => Value::Array(a.iter().map(to_value).collect()),
        Json::Obj(m) => Value::Object(m.iter().map(|(k, v)| (k.clone(), to_value(v))).collect()),
    }
}

/// The signing input of §3 for `record`: the domain, then the record without `sig` in RFC 8785
/// form. `None` when the record is outside the signed subset.
pub fn signing_input(record: &Map<String, Value>) -> Option<Vec<u8>> {
    let mut r = record.clone();
    r.remove("sig");
    let mut m = SIGN_DOMAIN.to_vec();
    m.extend_from_slice(jcs(&Value::Object(r)).ok()?.as_bytes());
    Some(m)
}

/// `record` signed with `key`: `signer_pubkey` set to the key, `sig` to the signature.
pub fn sign(mut record: Map<String, Value>, key: &SigningKey) -> Result<Map<String, Value>, String> {
    record.insert("signer_pubkey".into(), json!(hex(key.verifying_key().as_bytes())));
    record.remove("sig");
    let m = signing_input(&record).ok_or("the record is outside the signed JSON subset (an integer past 2^53 − 1, or a number that is not one)")?;
    record.insert("sig".into(), json!(hex(&key.sign(&m).to_bytes())));
    Ok(record)
}

/// A record checked in the order of §4.
pub fn check(bytes: &[u8]) -> Result<Accepted, Refusal> {
    // 1. It reads.
    let r = match strict_json::read(bytes) {
        Ok(j @ Json::Obj(_)) => j,
        Ok(_) => return refuse(1, Reason::Malformed, "the value is not an object"),
        Err(e) => return refuse(1, Reason::Malformed, e),
    };
    // 2. It is this format.
    if r.get("wai").and_then(Json::as_str) != Some(FORMAT) {
        return refuse(2, Reason::UnknownFormat, format!("wai is not {FORMAT}"));
    }
    // 3. Its members.
    if let Err(e) = members(&r) {
        return refuse(3, Reason::Malformed, e);
    }
    let v = to_value(&r);
    let o = v.as_object().expect("an object");
    let str_of = |k: &str| o.get(k).and_then(Value::as_str).unwrap_or_default().to_string();
    // 4. Its signature.
    let pk: [u8; 32] = unhex(&str_of("signer_pubkey")).try_into().expect("64 hex digits by step 3");
    let sig: [u8; 64] = unhex(&str_of("sig")).try_into().expect("128 hex digits by step 3");
    let Some(m) = signing_input(o) else {
        return refuse(3, Reason::Malformed, "the record is outside the signed JSON subset");
    };
    if crate::sig::verify_strict(&pk, &m, &sig).is_err() {
        return refuse(4, Reason::SignatureInvalid, "the signature does not verify over the record by signer_pubkey");
    }
    // 5. Its summary.
    let cases = o["cases"].as_array().expect("an array by step 3");
    let passed = cases.iter().filter(|c| c["pass"] == Value::Bool(true)).count() as u64;
    let n = cases.len() as u64;
    let s = &o["summary"];
    if s["cases"].as_u64() != Some(n) || s["passed"].as_u64() != Some(passed) || s["failed"].as_u64() != Some(n - passed) {
        return refuse(5, Reason::SummaryMismatch, format!("the cases count {n}, {passed} passed and {} failed", n - passed));
    }
    // 6. Its verdict.
    let conformant = n > 0 && passed == n;
    if o["conformant"].as_bool() != Some(conformant) {
        return refuse(6, Reason::ConformantMismatch, format!("the cases make the run {}conformant", if conformant { "" } else { "not " }));
    }
    Ok(Accepted {
        conformant,
        cases: n,
        passed,
        kind: str_of("kind"),
        suite: str_of("suite"),
        standard: str_of("standard"),
        corpus_digest: o["corpus"]["digest"].as_str().unwrap_or_default().to_string(),
        implementation: o["implementation"]["impl"].as_str().unwrap_or_default().to_string(),
        key: str_of("key"),
        signer_pubkey: str_of("signer_pubkey"),
    })
}

/// A verifier's outcome as §4 writes it.
pub fn verdict(bytes: &[u8]) -> String {
    match check(bytes) {
        Ok(a) => format!("accept conformant={}", a.conformant),
        Err(r) => format!("reject step={} reason={}", r.step, r.reason.code()),
    }
}

/// The corpus digest of §2.1 over `(path under wai/, the file's bytes)`.
pub fn corpus_digest<'a>(files: impl IntoIterator<Item = (&'a str, &'a [u8])>) -> String {
    let mut lines: Vec<String> = files.into_iter().map(|(p, b)| format!("{}  {}\n", hex(&Sha256::digest(b)), p.replace('\\', "/"))).collect();
    lines.sort();
    lines.dedup();
    format!("sha256:{}", hex(&Sha256::digest(lines.concat().as_bytes())))
}

// ─────────────────────────────── the corpus (§7) ───────────────────────────────

/// The corpus key's seed: 32 bytes of `0x43`. A published test key; it attests nothing.
pub const CORPUS_SEED: [u8; 32] = [0x43; 32];

/// The seed of the second key a vector re-signs with: 32 bytes of `0x44`.
pub const CORPUS_SEED_SUPPLIED: [u8; 32] = [0x44; 32];

fn sha(b: &[u8]) -> String {
    hex(&Sha256::digest(b))
}

/// The record every vector starts from: a verifier run, three cases, every one passed.
fn base(kind: &str, adapter: &str, key: &str, cases: &[(&str, bool)]) -> Map<String, Value> {
    let passed = cases.iter().filter(|c| c.1).count();
    let digest = corpus_digest([("claim-conformance/vectors.json", b"an example corpus file" as &[u8])]);
    let (imp, files) = if adapter == "http" {
        (json!("http://127.0.0.1:8380/verify"), json!([]))
    } else {
        (json!("example-verifier verdicts {corpus}"), json!([{"path": "/opt/example/bin/example-verifier", "sha256": sha(b"an example program")}]))
    };
    let mut r = Map::new();
    r.insert("wai".into(), json!(FORMAT));
    r.insert("kind".into(), json!(kind));
    r.insert("suite".into(), json!("claim"));
    r.insert("standard".into(), json!("v0.4.0"));
    r.insert("toolchain".into(), json!({"name": "wai", "version": "0.3.0"}));
    r.insert("corpus".into(), json!({"dir": "claim-conformance", "files": 1, "digest": digest}));
    r.insert(
        "implementation".into(),
        json!({"adapter": adapter, "impl": imp, "name": if key == "supplied" { json!("an example verifier") } else { Value::Null }, "version": if key == "supplied" { json!("1.0") } else { Value::Null }, "files": files}),
    );
    r.insert("started_at".into(), json!("2026-10-27T12:00:00Z"));
    r.insert(
        "cases".into(),
        Value::Array(
            cases
                .iter()
                .map(|(n, p)| {
                    let expected = format!("reject step=3 reason={n}");
                    json!({"name": n, "pass": p, "expected": expected, "got": if *p { expected.clone() } else { "accept".to_string() }})
                })
                .collect(),
        ),
    );
    r.insert("summary".into(), json!({"cases": cases.len(), "passed": passed, "failed": cases.len() - passed}));
    r.insert("conformant".into(), json!(passed == cases.len() && !cases.is_empty()));
    r.insert("key".into(), json!(key));
    r
}

const THREE: [(&str, bool); 3] = [("signature_invalid", true), ("bad_signer_key", true), ("field_type", true)];
const ONE_FAILED: [(&str, bool); 3] = [("signature_invalid", true), ("bad_signer_key", false), ("field_type", true)];

fn text(r: &Map<String, Value>) -> String {
    serde_json::to_string(&Value::Object(r.clone())).expect("a record serialises")
}

/// Replace `from` (which occurs once) in `s`.
fn once(s: &str, from: &str, to: &str) -> String {
    assert_eq!(s.matches(from).count(), 1, "{from} occurs once in {s}");
    s.replace(from, to)
}

/// Every vector of §7: a name, what it is, the record's bytes, and the outcome §4 gives it,
/// stated here from the text and never taken from [`check`]. The reference must reach each
/// stated outcome, or nothing is written.
pub fn vectors() -> Vec<(String, String, String, String)> {
    let key = SigningKey::from_bytes(&CORPUS_SEED);
    let other = SigningKey::from_bytes(&CORPUS_SEED_SUPPLIED);
    let signed = |r: Map<String, Value>, k: &SigningKey| sign(r, k).expect("signs");
    let mut v: Vec<(String, String, String, String)> = Vec::new();
    let mut add = |name: &str, expected: &str, about: &str, bytes: String| v.push((name.to_string(), about.to_string(), bytes, expected.to_string()));

    let valid = signed(base("verifier", "command", "ephemeral", &THREE), &key);
    add("valid", "accept conformant=true", "a conformant run, every case passed, from the command adapter with a key made for the run", text(&valid));
    let failed = signed(base("verifier", "command", "ephemeral", &ONE_FAILED), &key);
    add("valid_not_conformant", "accept conformant=false", "a run with one failed case: a genuine record of a run that did not conform", text(&failed));
    add("valid_no_cases", "accept conformant=false", "a run with no case: not conformant", text(&signed(base("verifier", "command", "ephemeral", &[]), &key)));
    add("valid_http_supplied_key", "accept conformant=true",
        "a conformant run of a decoder from the HTTP adapter, signed with a key the runner holds, its name and version given",
        text(&signed(base("decoder", "http", "supplied", &THREE), &other)),
    );

    // Changed after signing: the signature no longer covers what the record says.
    let mut flipped = failed.clone();
    flipped["cases"][1]["pass"] = json!(true);
    flipped["summary"] = json!({"cases": 3, "passed": 3, "failed": 0});
    flipped["conformant"] = json!(true);
    add("flipped_case", "reject step=4 reason=signature_invalid", "the failed case turned into a pass after signing, its summary and verdict made to agree", text(&flipped));
    let mut digest = valid.clone();
    let d = digest["corpus"]["digest"].as_str().unwrap().to_string();
    let last = if d.ends_with('0') { "1" } else { "0" };
    digest["corpus"]["digest"] = json!(format!("{}{last}", &d[..d.len() - 1]));
    add("corpus_digest_changed", "reject step=4 reason=signature_invalid", "the corpus digest's last digit changed after signing", text(&digest));
    let mut summary = failed.clone();
    summary["summary"]["failed"] = json!(0);
    add("summary_changed", "reject step=4 reason=signature_invalid", "the summary alone changed after signing", text(&summary));
    let mut bad = valid.clone();
    let s = bad["sig"].as_str().unwrap().to_string();
    let flip = format!("{:02x}", u8::from_str_radix(&s[..2], 16).unwrap() ^ 1);
    bad["sig"] = json!(format!("{flip}{}", &s[2..]));
    add("bad_signature", "reject step=4 reason=signature_invalid", "one bit of the signature changed", text(&bad));
    // S + L: the same signature's S past the group order, which the strict rule refuses.
    let mut big = valid.clone();
    let sb = unhex(big["sig"].as_str().unwrap());
    let l: [u8; 32] = [
        0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10,
    ];
    let mut s_plus_l = [0u8; 32];
    let mut carry = 0u16;
    for i in 0..32 {
        let t = sb[32 + i] as u16 + l[i] as u16 + carry;
        s_plus_l[i] = t as u8;
        carry = t >> 8;
    }
    big["sig"] = json!(format!("{}{}", hex(&sb[..32]), hex(&s_plus_l)));
    add("signature_s_not_reduced", "reject step=4 reason=signature_invalid", "the signature's S replaced by S + L, which verifies under the cofactorless equation reduced mod L but not under the strict rule", text(&big));
    // The identity as the key, (R, S) = (B, 1): verifies over any message without a private key.
    let mut identity = valid.clone();
    identity["signer_pubkey"] = json!(hex(&crate::sig::IDENTITY));
    let mut one = [0u8; 32];
    one[0] = 1;
    identity["sig"] = json!(format!("{}{}", hex(&crate::sig::BASE_POINT), hex(&one)));
    add("key_small_order", "reject step=4 reason=signature_invalid", "the identity point as the key with (R, S) = (B, 1), which the cofactorless equation accepts over any message", text(&identity));

    // Signed, and still refused.
    let mut sm = base("verifier", "command", "ephemeral", &ONE_FAILED);
    sm["summary"] = json!({"cases": 3, "passed": 3, "failed": 0});
    add("summary_mismatch_signed", "reject step=5 reason=summary_mismatch", "signed as it is: a summary that does not count its cases", text(&signed(sm, &key)));
    // Each count wrong alone, the other two right, so each of the three is checked.
    for (name, summary, about) in [
        ("summary_cases_mismatch_signed", json!({"cases": 4, "passed": 2, "failed": 1}), "signed as it is: a case count one more than its cases, its passed and failed right"),
        ("summary_passed_mismatch_signed", json!({"cases": 3, "passed": 3, "failed": 1}), "signed as it is: a passed count one more than its cases pass, its other counts right"),
        ("summary_failed_mismatch_signed", json!({"cases": 3, "passed": 2, "failed": 0}), "signed as it is: a failed count one less than its cases fail, its other counts right"),
    ] {
        let mut r = base("verifier", "command", "ephemeral", &ONE_FAILED);
        r["summary"] = summary;
        add(name, "reject step=5 reason=summary_mismatch", about, text(&signed(r, &key)));
    }
    let mut cm = base("verifier", "command", "ephemeral", &ONE_FAILED);
    cm["conformant"] = json!(true);
    add("conformant_mismatch_signed", "reject step=6 reason=conformant_mismatch", "signed as it is: conformant though a case failed", text(&signed(cm, &key)));
    let mut cn = base("verifier", "command", "ephemeral", &[]);
    cn["conformant"] = json!(true);
    add("conformant_no_cases_signed", "reject step=6 reason=conformant_mismatch", "signed as it is: conformant with no case", text(&signed(cn, &key)));
    let mut uf = base("verifier", "command", "ephemeral", &THREE);
    uf["wai"] = json!("conform-result/2");
    add("unknown_format", "reject step=2 reason=unknown_format", "signed as it is: a version this revision does not define", text(&signed(uf, &key)));
    let mut missing = base("verifier", "command", "ephemeral", &THREE);
    missing.remove("corpus");
    add("missing_member", "reject step=3 reason=malformed", "signed as it is: no corpus member", text(&signed(missing, &key)));
    let mut extra = base("verifier", "command", "ephemeral", &THREE);
    extra.insert("note".into(), json!("a member §2 does not list"));
    add("extra_member", "reject step=3 reason=malformed", "signed as it is: a member §2 does not list", text(&signed(extra, &key)));

    // Bytes a strict reader refuses, or numbers that are not integers.
    let vt = text(&valid);
    add("duplicate_member", "reject step=1 reason=malformed", "the valid record with its verdict named twice, the second time true", once(&vt, "\"conformant\":true,", "\"conformant\":false,\"conformant\":true,"));
    add("fraction_count", "reject step=3 reason=malformed", "the valid record with a count written with a fraction (3.0)", once(&vt, "\"summary\":{\"cases\":3,", "\"summary\":{\"cases\":3.0,"));
    add("digest_upper_case", "reject step=3 reason=malformed", "the valid record with its corpus digest in upper-case hexadecimal", once(&vt, &d, &format!("sha256:{}", d[7..].to_uppercase())));
    add("not_json", "reject step=1 reason=malformed", "bytes that are not a JSON text", vt[..vt.len() - 1].to_string());

    for (n, _, b, want) in &v {
        let got = verdict(b.as_bytes());
        assert_eq!(&got, want, "{n}: the reference reaches {got}, the text says {want}");
    }
    v
}


/// The corpus as `vectors.json` writes it.
pub fn corpus_document() -> String {
    let vs: Vec<Value> = vectors().into_iter().map(|(name, about, record, expected)| json!({"name": name, "about": about, "record": record, "expected": expected})).collect();
    let doc = json!({
        "about": "conformance-results §7: records of conform-result/1, each with the outcome §4 gives it. Signed with the published test keys of seeds 32 bytes of 0x43 and of 0x44, which attest nothing; every run, program and path in them is an example.",
        "public_keys": [hex(SigningKey::from_bytes(&CORPUS_SEED).verifying_key().as_bytes()), hex(SigningKey::from_bytes(&CORPUS_SEED_SUPPLIED).verifying_key().as_bytes())],
        "vectors": vs,
    });
    format!("{}\n", serde_json::to_string_pretty(&doc).expect("serialises"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use ed25519_dalek::Verifier;

    #[test]
    fn a_signed_record_is_accepted_and_every_member_is_covered() {
        let key = SigningKey::from_bytes(&CORPUS_SEED);
        let r = sign(base("verifier", "command", "ephemeral", &THREE), &key).unwrap();
        let a = check(text(&r).as_bytes()).unwrap();
        assert!(a.conformant);
        assert_eq!((a.cases, a.passed), (3, 3));
        // Every member changed after signing is caught by the signature.
        for k in r.keys().filter(|k| *k != "sig" && *k != "signer_pubkey" && *k != "wai") {
            let mut t = r.clone();
            t[k] = match &t[k] {
                Value::String(s) => json!(format!("{s}x")),
                Value::Bool(b) => json!(!b),
                Value::Object(_) | Value::Array(_) => continue,
                other => other.clone(),
            };
            let got = check(text(&t).as_bytes());
            assert!(got.is_err(), "{k} changed is refused");
        }
    }

    /// Under the identity key, (R, S) = (B, 1) satisfies the cofactorless equation over any
    /// message: RFC 8032's check accepts it (so `BASE_POINT` is B), and WAI's strict rule refuses it.
    #[test]
    fn the_small_order_forgery_is_one_and_is_refused() {
        let mut sig = [0u8; 64];
        sig[..32].copy_from_slice(&crate::sig::BASE_POINT);
        sig[32] = 1;
        let identity = ed25519_dalek::VerifyingKey::from_bytes(&crate::sig::IDENTITY).expect("the identity decodes");
        assert!(identity.verify(b"any message", &ed25519_dalek::Signature::from_bytes(&sig)).is_ok());
        assert!(crate::sig::verify_strict(&crate::sig::IDENTITY, b"any message", &sig).is_err());
    }

    /// The record's signature site refuses every forgery the strict rule exists for
    /// (`sig::forgeries`), set into a signed record.
    #[test]
    fn the_record_refuses_every_forgery() {
        let good = sign(base("verifier", "command", "ephemeral", &THREE), &SigningKey::from_bytes(&CORPUS_SEED)).unwrap();
        crate::sig::assert_refuses_forgeries(
            &good,
            |r, k, s| {
                r.insert("signer_pubkey".into(), json!(hex(&k)));
                r.insert("sig".into(), json!(hex(&s)));
            },
            |r| signing_input(r).expect("in the subset"),
            |r| check(text(r).as_bytes()).is_ok(),
        );
    }

    #[test]
    fn utc_seconds() {
        assert!(is_utc_second("2026-10-27T12:00:00Z"));
        for s in ["2026-13-27T12:00:00Z", "2026-10-27T24:00:00Z", "2026-10-27T12:00:00+00:00", "2026-10-27 12:00:00Z", "2026-10-27T12:00:00.5Z"] {
            assert!(!is_utc_second(s), "{s}");
        }
    }

    #[test]
    fn every_reason_has_a_vector() {
        let vs = vectors();
        for r in Reason::ALL {
            assert!(vs.iter().any(|v| v.3.ends_with(&format!("reason={}", r.code()))), "{} has a vector", r.code());
        }
    }
}
