//! The conformance corpus of [platform-encode] §10.1, `encode-conformance/`.
//!
//! The streams under `streams/` were made by encoders through each interface, from
//! [`source`] with the plan [`PLAN`], and are fixtures: no encoder is a pure function, so
//! they are pinned by their SHA-256 in `streams.json` and never regenerated. Everything
//! else is generated here, byte for byte, from them and from a fixed-seed key: the source,
//! the plan, the mutated streams under `mutated/`, the records under `records/`, and
//! `cases.json`, which gives every case its outcome.
//!
//! [platform-encode]: ../../../../extensions/platform-encode.md

use super::record::{self, Reason};
use super::{Codec, annexb, sha256_hex, y4m};
use ed25519_dalek::SigningKey;
use serde_json::{Value, json};
use std::collections::BTreeMap;

/// The source's size, frame count and rate.
pub const W: u32 = 256;
pub const H: u32 = 144;
pub const FRAMES: u64 = 12;
pub const FPS: (u32, u32) = (30, 1);
/// The IDR plan every fixture stream was made with.
pub const PLAN: [u64; 3] = [0, 5, 9];
/// The rate every fixture stream was asked for.
pub const KBPS: u32 = 400;
/// The corpus signer's seed.
pub const SEED: [u8; 32] = [0x45; 32];
/// Every record's `issued_at`.
pub const ISSUED_AT: &str = "2026-10-25T12:00:00Z";

/// A frame of the source: a diagonal gradient sliding right, a square moving down, and
/// low-amplitude noise from a fixed seed; chroma ramps that move with time.
pub fn frame(w: usize, h: usize, t: usize) -> Vec<u8> {
    let mut f = vec![0u8; w * h + 2 * (w / 2) * (h / 2)];
    let mut s = 0x9e37_79b9_7f4a_7c15u64 ^ (t as u64).wrapping_mul(0xbf58_476d_1ce4_e5b9);
    for y in 0..h {
        for x in 0..w {
            s ^= s << 13;
            s ^= s >> 7;
            s ^= s << 17;
            let g = ((x + y + 3 * t) % 256) as i32;
            let sq = (x / 64 + (y + 4 * t) / 64).is_multiple_of(2);
            let v = (g / 2 + if sq { 96 } else { 32 } + (s % 9) as i32 - 4).clamp(16, 235);
            f[y * w + x] = v as u8;
        }
    }
    let c = (w / 2) * (h / 2);
    for y in 0..h / 2 {
        for x in 0..w / 2 {
            f[w * h + y * (w / 2) + x] = (128 + ((x + t) % 64) as i32 - 32) as u8;
            f[w * h + c + y * (w / 2) + x] = (128 + ((y + 2 * t) % 64) as i32 - 32) as u8;
        }
    }
    f
}

/// The source, `source.y4m`.
pub fn source() -> Vec<u8> {
    let frames: Vec<Vec<u8>> = (0..FRAMES as usize).map(|t| frame(W as usize, H as usize, t)).collect();
    y4m::write(W, H, FPS.0, FPS.1, &frames)
}

/// The plan, `plan.txt`: one frame index per line.
pub fn plan_text() -> String {
    PLAN.iter().map(|p| format!("{p}\n")).collect()
}

/// A fixture stream: its file name, codec and the identity its records state. The
/// identities are the corpus's, not the encoders': public text names no product.
pub struct Fixture {
    pub file: &'static str,
    pub codec: Codec,
    pub api: &'static str,
    pub hardware: bool,
    /// The rung's size where it is not the source's (resampled by two-tap).
    pub size: Option<(u32, u32)>,
}

pub const FIXTURES: &[Fixture] = &[
    Fixture { file: "platform-avc.h264", codec: Codec::Avc, api: "platform", hardware: true, size: None },
    Fixture { file: "platform-hevc.h265", codec: Codec::Hevc, api: "platform", hardware: true, size: None },
    Fixture { file: "va-1-avc.h264", codec: Codec::Avc, api: "va", hardware: true, size: None },
    Fixture { file: "va-1-hevc.h265", codec: Codec::Hevc, api: "va", hardware: true, size: None },
    Fixture { file: "va-2-avc.h264", codec: Codec::Avc, api: "va", hardware: true, size: None },
    Fixture { file: "va-2-hevc.h265", codec: Codec::Hevc, api: "va", hardware: true, size: None },
    Fixture { file: "gpu-driver-avc.h264", codec: Codec::Avc, api: "gpu-driver", hardware: true, size: None },
    Fixture { file: "gpu-driver-hevc.h265", codec: Codec::Hevc, api: "gpu-driver", hardware: true, size: None },
    Fixture { file: "software-avc.h264", codec: Codec::Avc, api: "software", hardware: false, size: None },
    Fixture { file: "software-avc-128x72.h264", codec: Codec::Avc, api: "software", hardware: false, size: Some((128, 72)) },
];

/// The record body of fixture `f` over `stream`, unsigned.
fn body(f: &Fixture, stream: &[u8], src_sha: &str) -> Value {
    let checked = annexb::check_stream(f.codec, stream, FRAMES, &PLAN).expect("a fixture stream holds to §4.3");
    let (w, h) = f.size.unwrap_or((W, H));
    json!({
        "kind": record::KIND,
        "capability": f.codec.capability(),
        "encoder": {
            "api": f.api,
            "name": format!("corpus {} encoder ({})", f.api, f.file),
            "version": "1.0",
            "hardware": if f.hardware { "yes" } else { "no" },
            "device": "corpus device",
            "driver": "corpus driver 1",
            "library_sha256": if f.api == "software" { json!(sha256_hex(b"corpus software library")) } else { Value::Null },
        },
        "settings": {"width": w, "height": h, "fps_num": FPS.0, "fps_den": FPS.1, "kbps": KBPS, "range": "video",
                     "resample": if f.size.is_some() { super::scale::NAME } else { "none" }},
        "source": {"frames": FRAMES, "width": W, "height": H, "sha256": src_sha},
        "idr": PLAN,
        "stream": {"sha256": sha256_hex(stream), "bytes": stream.len(), "units": checked.units.len(),
                   "profile_idc": checked.sps.profile_idc, "level_idc": checked.sps.level_idc,
                   "tier": if checked.sps.high_tier { "high" } else { "main" }},
        "energy": Value::Null,
        "wall_us": 1000,
        "package": Value::Null,
        "issued_at": ISSUED_AT,
    })
}

fn energy(joules_micro: u64, fabrics: &[&str], coverage: &str) -> Value {
    json!({"joules_micro": joules_micro, "energy_provenance": "OnChipCounter",
           "energy_uncertainty": {"relative_ppm": 50_000, "window_us": 400_000},
           "energy_counter_filtering": "Unknown", "energy_calibration_ref": Value::Null,
           "coverage": coverage, "counter": "task", "attribution": "task", "fabrics": fabrics})
}

/// One case: its record's text, the stream it is checked with (a file under `streams/`
/// or `mutated/`), whether the source is held, and the outcome.
pub struct Case {
    pub name: String,
    pub record: String,
    pub stream: Option<String>,
    pub source: bool,
    pub expect: Option<Reason>,
}

/// The NAL units of each access unit of a checked stream, as owned bytes.
fn split(codec: Codec, s: &[u8]) -> Vec<Vec<Vec<u8>>> {
    let nals = annexb::nal_units(codec, s).expect("a fixture parses");
    let aus = annexb::access_units(codec, s, &nals).expect("a fixture parses");
    aus.iter().map(|au| nals[au.clone()].iter().map(|n| s[n.range.clone()].to_vec()).collect()).collect()
}

fn join(aus: &[Vec<Vec<u8>>]) -> Vec<u8> {
    let mut out = Vec::new();
    for au in aus {
        for n in au {
            out.extend_from_slice(&[0, 0, 0, 1]);
            out.extend_from_slice(n);
        }
    }
    out
}

/// The mutated streams (`mutated/`), each derived from a fixture by one edit, with the
/// reason §4.3 gives it.
pub fn mutations(streams: &BTreeMap<String, Vec<u8>>) -> Vec<(String, &'static str, Codec, Vec<u8>, Reason)> {
    let avc = &streams["platform-avc.h264"];
    let hevc = &streams["platform-hevc.h265"];
    let a = split(Codec::Avc, avc);
    let h = split(Codec::Hevc, hevc);
    let mut out = Vec::new();
    // §4.3 item 1: `00 00 02` inside the last NAL unit.
    let mut m = avc.clone();
    let at = m.len() - 2;
    m.splice(at..at, [0u8, 0, 2]);
    out.push(("avc-zero-zero-two.h264".to_string(), "platform-avc.h264", Codec::Avc, m, Reason::StreamMalformed));
    // item 2: the last access unit dropped.
    out.push(("avc-one-short.h264".to_string(), "platform-avc.h264", Codec::Avc, join(&a[..a.len() - 1]), Reason::FrameCount));
    // item 3: the second IDR's slices made non-IDR (nal_unit_type 5 → 1).
    let mut b = a.clone();
    for n in &mut b[PLAN[1] as usize] {
        if n[0] & 0x1f == 5 {
            n[0] = (n[0] & 0xe0) | 1;
        }
    }
    out.push(("avc-idr-missing.h264".to_string(), "platform-avc.h264", Codec::Avc, join(&b), Reason::KeyframeMismatch));
    // item 4: the second IDR access unit without its sequence parameter set.
    let mut b = a.clone();
    b[PLAN[1] as usize].retain(|n| n[0] & 0x1f != 7);
    out.push(("avc-sps-missing.h264".to_string(), "platform-avc.h264", Codec::Avc, join(&b), Reason::ParameterSets));
    // item 4: no picture parameter set in any access unit, so every IDR is alike and only the
    // rule that an IDR carries every parameter set refuses it.
    let mut b = a.clone();
    for au in &mut b {
        au.retain(|n| n[0] & 0x1f != 8);
    }
    out.push(("avc-no-pps.h264".to_string(), "platform-avc.h264", Codec::Avc, join(&b), Reason::ParameterSets));
    // item 5: a slice of the second picture given nuh_layer_id 32.
    let mut b = h.clone();
    for n in &mut b[1] {
        if (n[0] >> 1) & 0x3f < 32 {
            n[0] |= 1;
        }
    }
    out.push(("hevc-layer.h265".to_string(), "platform-hevc.h265", Codec::Hevc, join(&b), Reason::StreamUnsupported));
    // item 6: two pictures inside a piece swapped, so their order count runs backwards.
    let mut b = a.clone();
    b.swap(2, 3);
    out.push(("avc-swapped.h264".to_string(), "platform-avc.h264", Codec::Avc, join(&b), Reason::StreamReordered));
    // item 3, HEVC: a CRA picture (21) where the plan has none.
    let mut b = h.clone();
    for n in &mut b[2] {
        if (n[0] >> 1) & 0x3f < 32 {
            n[0] = (n[0] & 0x81) | (21 << 1);
        }
    }
    out.push(("hevc-cra.h265".to_string(), "platform-hevc.h265", Codec::Hevc, join(&b), Reason::KeyframeMismatch));
    // item 3: the first IDR access unit given a second slice, not the picture's first
    // (first_slice_segment_in_pic_flag cleared), of a trailing type (1).
    let mut b = h.clone();
    let slice = b[0].iter().find(|n| matches!((n[0] >> 1) & 0x3f, 19 | 20)).expect("an IDR slice").clone();
    let mut extra = slice;
    extra[0] = (extra[0] & 0x81) | (1 << 1);
    extra[2] &= 0x7f;
    b[0].push(extra);
    out.push(("hevc-mixed-idr.h265".to_string(), "platform-hevc.h265", Codec::Hevc, join(&b), Reason::StreamMalformed));
    // item 2: NAL units after the last picture (an SEI) that begin an access unit with no slice.
    let mut m = avc.clone();
    m.extend_from_slice(&[0, 0, 0, 1, 0x06, 0x05, 0x01, 0x00, 0x80]);
    out.push(("avc-trailing-sei.h264".to_string(), "platform-avc.h264", Codec::Avc, m, Reason::StreamMalformed));
    // item 4: every IDR access unit carrying a second, other sequence parameter set (its
    // level_idc changed), the same bytes each time.
    let mut b = a.clone();
    for &i in &PLAN {
        let au = &mut b[i as usize];
        let at = au.iter().position(|n| n[0] & 0x1f == 7).expect("an SPS");
        let mut other = au[at].clone();
        other[3] ^= 1;
        au.insert(at + 1, other);
    }
    out.push(("avc-two-sps.h264".to_string(), "platform-avc.h264", Codec::Avc, join(&b), Reason::ParameterSets));
    // item 2: a middle access unit dropped.
    let mut b = a.clone();
    b.remove(2);
    out.push(("avc-middle-dropped.h264".to_string(), "platform-avc.h264", Codec::Avc, join(&b), Reason::FrameCount));
    out
}

fn sign(v: Value) -> String {
    record::to_text(&record::sign(v, &SigningKey::from_bytes(&SEED)))
}

/// Every case, in order: one accepted per fixture; the energy cases; the record's own
/// negatives; then one per mutated stream (each with a record that states it, so the
/// stream's shape is what refuses it).
pub fn cases(streams: &BTreeMap<String, Vec<u8>>) -> Vec<Case> {
    let src = source();
    let (_, frames) = y4m::read_all(&src).expect("the corpus source reads");
    let src_sha = sha256_hex(&frames.concat());
    let mut out = Vec::new();
    let mut push = |name: &str, record: String, stream: Option<&str>, source: bool, expect: Option<Reason>| {
        out.push(Case { name: name.to_string(), record, stream: stream.map(|s| format!("streams/{s}")), source, expect });
    };
    for f in FIXTURES {
        let b = body(f, &streams[f.file], &src_sha);
        push(&format!("accept-{}", f.file.rsplit_once('.').expect("an extension").0), sign(b), Some(f.file), true, None);
    }
    let f = &FIXTURES[0];
    let base = || body(f, &streams[f.file], &src_sha);
    let edit = |p: &[&str], v: Value| {
        let mut b = base();
        let mut at = &mut b;
        for k in &p[..p.len() - 1] {
            at = &mut at[*k];
        }
        at[p[p.len() - 1]] = v;
        b
    };
    // The record alone (stream not held), accepted, and with an energy figure.
    push("accept-record-only", sign(base()), None, false, None);
    push("accept-energy-partial", sign(edit(&["energy"], energy(213_000, &["cpu"], "Partial"))), Some(f.file), false, None);
    push("accept-energy-whole-engine", sign(edit(&["energy"], energy(213_000, &["cpu", "media"], "Whole"))), Some(f.file), false, None);
    // §7 step 1.
    let good = sign(base());
    push("malformed-extra-member", good.replacen("{\"capability\"", "{\"added\":1,\"capability\"", 1), None, false, Some(Reason::RecordMalformed));
    push("malformed-duplicate-member", good.replacen("{\"capability\"", "{\"kind\":\"wai-encode\",\"capability\"", 1), None, false, Some(Reason::RecordMalformed));
    push("malformed-negative", sign(edit(&["wall_us"], json!(1000))).replacen("\"wall_us\":1000", "\"wall_us\":-1", 1), None, false, Some(Reason::RecordMalformed));
    push("malformed-boolean", sign(edit(&["encoder", "hardware"], json!(true))), None, false, Some(Reason::RecordMalformed));
    push("malformed-fraction", good.replacen("\"wall_us\":1000", "\"wall_us\":1000.0", 1), None, false, Some(Reason::RecordMalformed));
    push("malformed-date-time", sign(edit(&["issued_at"], json!("2026-10-25 12:00:00"))), None, false, Some(Reason::RecordMalformed));
    // step 2.
    let bytes_for = |key: &[u8; 32]| {
        let mut b = base();
        b["signer_pubkey"] = json!(super::hex(key));
        record::signing_bytes(&b)
    };
    let forged = crate::sig::forgeries(bytes_for);
    for fg in forged.iter().take(1) {
        let mut b = base();
        b["signer_pubkey"] = json!(super::hex(&fg.key));
        b["sig"] = json!(super::hex(&fg.sig));
        push("bad-key-identity", record::to_text(&b), None, false, Some(Reason::BadKey));
    }
    let mut v: Value = serde_json::from_str(&good).expect("a record");
    let mut sig = v["sig"].as_str().expect("a sig").to_string();
    let flipped = if sig.as_bytes()[0] == b'0' { '1' } else { '0' };
    sig.replace_range(0..1, &flipped.to_string());
    v["sig"] = json!(sig);
    push("bad-signature", record::to_text(&v), None, false, Some(Reason::BadSignature));
    // step 3 to 7.
    push("capability-av1", sign(edit(&["capability"], json!("wai.video.av1"))), None, false, Some(Reason::Capability));
    push("identity-blank-device", sign(edit(&["encoder", "device"], json!(" \t"))), None, false, Some(Reason::IdentityIncomplete));
    push("identity-va-software", sign(edit(&["encoder"], json!({"api": "va", "name": "n", "version": "1", "hardware": "no", "device": "d", "driver": "r", "library_sha256": null}))), None, false, Some(Reason::IdentityInconsistent));
    push("identity-platform-library", sign(edit(&["encoder", "library_sha256"], json!(sha256_hex(b"x")))), None, false, Some(Reason::IdentityInconsistent));
    push("identity-unknown-api", sign(edit(&["encoder", "api"], json!("other"))), None, false, Some(Reason::IdentityInconsistent));
    let software = |hardware: &str| json!({"api": "software", "name": "n", "version": "1", "hardware": hardware, "device": "d", "driver": "r", "library_sha256": sha256_hex(b"corpus software library")});
    push("identity-software-hardware", sign(edit(&["encoder"], software("yes"))), None, false, Some(Reason::IdentityInconsistent));
    let mut b = edit(&["encoder"], software("no"));
    b["capability"] = json!("wai.video.hevc");
    push("identity-software-hevc", sign(b), None, false, Some(Reason::IdentityInconsistent));
    // An odd width alone (resampled, so the resample rule holds).
    let mut b = edit(&["settings", "width"], json!(255));
    b["settings"]["resample"] = json!(super::scale::NAME);
    push("settings-odd-width", sign(b), None, false, Some(Reason::SettingsInvalid));
    push("settings-fps-zero", sign(edit(&["settings", "fps_den"], json!(0))), None, false, Some(Reason::SettingsInvalid));
    push("settings-no-frames", sign(edit(&["source", "frames"], json!(0))), None, false, Some(Reason::SettingsInvalid));
    push("settings-resample-none-resized", sign(edit(&["settings", "width"], json!(128))), None, false, Some(Reason::SettingsInvalid));
    push("settings-kbps-zero", sign(edit(&["settings", "kbps"], json!(0))), None, false, Some(Reason::SettingsInvalid));
    push("plan-not-from-zero", sign(edit(&["idr"], json!([1, 5, 9]))), None, false, Some(Reason::PlanInvalid));
    push("plan-past-the-end", sign(edit(&["idr"], json!([0, 5, 12]))), None, false, Some(Reason::PlanInvalid));
    // A package's pieces that are not the plan.
    let piece = |start: u64, frames: u64| json!({"start": start, "frames": frames, "motif": sha256_hex(b"base"), "object": sha256_hex(b"object")});
    push("plan-pieces-other", sign(edit(&["package"], json!({"receipt_sha256": sha256_hex(b"receipt"), "rung": "r", "pieces": [piece(0, 4), piece(4, 5), piece(9, 3)]}))), None, false, Some(Reason::PlanInvalid));
    // A zero figure of a class that carries no evidence, so only rule 1 refuses it.
    push("energy-zero", sign(edit(&["energy"], {
        let mut e = energy(0, &["cpu"], "Partial");
        e["energy_provenance"] = json!("ModelBased");
        e["energy_uncertainty"] = Value::Null;
        e["energy_counter_filtering"] = Value::Null;
        e
    })), None, false, Some(Reason::EnergyInvalid));
    push("energy-below-floor", sign(edit(&["energy"], {
        let mut e = energy(5, &["cpu"], "Partial");
        e["energy_uncertainty"]["relative_ppm"] = json!(1000);
        e
    })), None, false, Some(Reason::EnergyInvalid));
    push("energy-legacy-class", sign(edit(&["energy"], {
        let mut e = energy(213_000, &["cpu"], "Partial");
        e["energy_provenance"] = json!("HwShunt");
        e["energy_uncertainty"] = Value::Null;
        e["energy_counter_filtering"] = Value::Null;
        e
    })), None, false, Some(Reason::EnergyInvalid));
    push("energy-coverage-value", sign(edit(&["energy"], energy(213_000, &["cpu"], "Most"))), None, false, Some(Reason::EnergyInvalid));
    push("energy-attribution-value", sign(edit(&["energy"], {
        let mut e = energy(213_000, &["cpu"], "Partial");
        e["attribution"] = json!("guess");
        e
    })), None, false, Some(Reason::EnergyInvalid));
    push("energy-fabrics-out-of-order", sign(edit(&["energy"], energy(213_000, &["media", "cpu"], "Partial"))), None, false, Some(Reason::EnergyInvalid));
    push("energy-whole-without-engine", sign(edit(&["energy"], energy(213_000, &["cpu"], "Whole"))), None, false, Some(Reason::EnergyCoverage));
    // step 8 and 9.
    push("stream-other-bytes", sign(base()), Some("platform-hevc.h265"), false, Some(Reason::StreamMismatch));
    // A rung of another size (resampled, so §7 step 5 holds) over the source-sized stream.
    let mut b = edit(&["settings", "width"], json!(254));
    b["settings"]["resample"] = json!(super::scale::NAME);
    push("sps-other-width", sign(b), Some(f.file), false, Some(Reason::SpsMismatch));
    push("sps-other-level", sign(edit(&["stream", "level_idc"], json!(1))), Some(f.file), false, Some(Reason::SpsMismatch));
    push("source-other-digest", sign(edit(&["source", "sha256"], json!(sha256_hex(b"another source")))), None, true, Some(Reason::SourceMismatch));
    // The same bytes, a record of another frame count: the held source's header decides.
    let mut b = edit(&["source", "frames"], json!(FRAMES + 1));
    b["stream"]["units"] = json!(FRAMES + 1);
    push("source-other-frames", sign(b), None, true, Some(Reason::SourceMismatch));
    // The source's rate is 30:1 as its header writes it; 60:2 is another rate as written.
    let mut b = edit(&["settings", "fps_num"], json!(60));
    b["settings"]["fps_den"] = json!(2);
    push("source-other-rate", sign(b), None, true, Some(Reason::SourceMismatch));
    push("source-other-range", sign(edit(&["settings", "range"], json!("full"))), None, true, Some(Reason::SourceMismatch));
    for (name, from, codec, m, reason) in mutations(streams) {
        let fx = FIXTURES.iter().find(|x| x.file == from).expect("a fixture");
        let mut b = body(fx, &streams[from], &src_sha);
        b["stream"]["sha256"] = json!(sha256_hex(&m));
        b["stream"]["bytes"] = json!(m.len());
        let _ = codec;
        out.push(Case { name: format!("stream-{}", name.rsplit_once('.').expect("an extension").0), record: sign(b), stream: Some(format!("mutated/{name}")), source: false, expect: Some(reason) });
    }
    out
}

/// `cases.json`: every case's name, files and outcome.
pub fn cases_json(cases: &[Case]) -> String {
    let v: Vec<Value> = cases
        .iter()
        .map(|c| json!({"name": c.name, "record": format!("records/{}.json", c.name), "stream": c.stream, "source": if c.source { json!("source.y4m") } else { Value::Null }, "expect": c.expect.map_or("accept", Reason::code)}))
        .collect();
    serde_json::to_string_pretty(&json!({"cases": v})).expect("JSON") + "\n"
}

/// Every generated file, by its path under `encode-conformance/`, from the fixture streams.
pub fn generate(streams: &BTreeMap<String, Vec<u8>>) -> BTreeMap<String, Vec<u8>> {
    let mut files = BTreeMap::new();
    files.insert("source.y4m".to_string(), source());
    files.insert("plan.txt".to_string(), plan_text().into_bytes());
    for (name, _, _, m, _) in mutations(streams) {
        files.insert(format!("mutated/{name}"), m);
    }
    let cs = cases(streams);
    for c in &cs {
        files.insert(format!("records/{}.json", c.name), c.record.clone().into_bytes());
    }
    files.insert("cases.json".to_string(), cases_json(&cs).into_bytes());
    files
}

/// Run every case: its outcome, or a description of the first that disagrees.
pub fn check(dir: &std::path::Path) -> Result<usize, String> {
    let read = |p: &str| std::fs::read(dir.join(p)).map_err(|e| format!("{p}: {e}"));
    let index: Value = serde_json::from_slice(&read("cases.json")?).map_err(|e| e.to_string())?;
    let (hd, frames) = y4m::read_all(&read("source.y4m")?).map_err(|e| e.to_string())?;
    let planes = frames.concat();
    let mut n = 0;
    for c in index["cases"].as_array().ok_or("no cases")? {
        let name = c["name"].as_str().unwrap_or_default();
        let text = String::from_utf8(read(c["record"].as_str().unwrap_or_default())?).map_err(|e| e.to_string())?;
        let stream = c["stream"].as_str().map(read).transpose()?;
        let held = record::Held { stream: stream.as_deref(), source: c["source"].as_str().map(|_| (&hd, planes.as_slice())) };
        let got = record::verify(&text, &held).err().map_or("accept", Reason::code);
        if got != c["expect"].as_str().unwrap_or_default() {
            return Err(format!("{name}: expected {}, got {got}", c["expect"]));
        }
        n += 1;
    }
    Ok(n)
}
