//! The conformance corpus of `extensions/scene-carriage.md` §9: `live-conformance/scene/`.
//!
//! [`generate`] writes it from worlds and documents it builds in code and from the audio scenes
//! and worlds other corpora commit, each pinned here by its SHA-256 ([`INPUT_FILES`]). Every
//! value is a constant of this module or a SHA-256 output of a label, so the corpus is the same
//! bytes on every machine. Each vector states its outcome; the generator asserts that this
//! module reaches it before writing it, and that the corpus gives every code of §8.
//!
//! [`verify`] checks a corpus directory against this module.
//! `live-conformance/scene_verify.py` checks the same files from the extension's text, sharing
//! no code with this module.

use std::collections::BTreeMap;

use serde_json::{json, Value};

use super::{gltf, hex, kinematic_state, usda, world_bytes, Bundle, ImportParams, Imported, MediaItem, CODES};
use crate::world::fixed::Fx;

/// The files of other corpora the corpus reads: `(path from the corpus directory, SHA-256)`.
pub const INPUT_FILES: &[(&str, &str)] = &[
    ("../../world-conformance/vectors/scene.wwld", "41b89729f87c308653d4359522280d7255c36d5b2d5300eafa8b1b7f7b76b3f0"),
    ("../../world-conformance/vectors/basic_kinematics.wwld", "c90b8a03ea11ab87428b2255f402c17eb26a89c625b59d8d753afad143707491"),
    ("../../audio-conformance/vectors/moving.wscn", "f799afa2ade71684f0fb1bf5eeb6413950228906551ba75be477b24ace125010"),
    ("../../audio-conformance/vectors/distance.wscn", "fcb946a54ef3b79d7f7ab14b98777800ff90e06309f7f2a5f9422bfc32f13d0b"),
    ("../../audio-conformance/vectors/stereo_pair.wscn", "1ac272f61ce80b691de745bd9af94db2aaab20087cade54128c8886a53cd7185"),
];

/// The world contract every world the corpus builds carries (interactive-worlds §6).
pub fn contract(sim: &str) -> Vec<u8> {
    format!(
        r#"{{"checkpoint_every":600,"numeric":"wai.det.fixed64","resolve":{{"default":"wai.world.resolve.lww"}},"rng":{{"alg":"xoshiro256++","seed":"{}"}},"sim":"{sim}","tick_rate":60}}"#,
        "00".repeat(32)
    )
    .into_bytes()
}

/// The audio contract the corpus's imports carry (spatial-audio §2.1).
pub const AUDIO_CONTRACT: &[u8] = br#"{"distance":"wai.audio.dist.inverse_clamped","duration":48000,"floor_layout":"stereo","numeric":"wai.det.fixed64","pan_law":"wai.audio.pan.sqrt_constpower","sample_rate":48000}"#;

fn label(s: &str) -> [u8; 32] {
    crate::pin::sha256(format!("wai:scene/corpus\x00{s}").as_bytes())
}

fn fx(v: f64) -> i64 {
    Fx::from_bits((v * 4294967296.0) as i64).to_bits()
}

fn node_id(i: u64) -> [u8; 16] {
    crate::world::mint_node_id(&label("publisher"), i)
}

fn state(vals: &[i64]) -> Vec<u8> {
    vals.iter().flat_map(|v| v.to_le_bytes()).collect()
}

/// The worlds the corpus builds: kinematic0, ballistic0 and truss0 nodes at tick 0.
fn built_worlds() -> Vec<(&'static str, Vec<u8>)> {
    let mesh = label("asset/cube.glb");
    let amb = label("asset/ambience.flac");
    let assets = vec![(mesh.to_vec(), 0u8, "cube.glb".to_string()), (amb.to_vec(), 1u8, "ambience.flac".to_string())];
    let mut k = vec![
        (node_id(1), kinematic_state([fx(1.0), fx(2.0), fx(3.0)])),
        (node_id(2), kinematic_state([fx(0.5), fx(-2.25), 0])),
        (node_id(3), state(&[1431655765, fx(10.0), fx(-7.75), fx(1.0), 0, 0, 7])),
    ];
    let mut b = vec![
        (node_id(4), state(&[fx(0.0), fx(5.0), fx(0.0), 0, 0, 0, fx(0.5), fx(0.75)])),
        (node_id(5), state(&[fx(-3.0), fx(1.5), fx(2.0), fx(1.0), 0, 0, fx(0.25), fx(0.5)])),
    ];
    let p1 = node_id(6);
    let p2 = node_id(7);
    let mut member = vec![1u8];
    member.extend_from_slice(&p1);
    member.extend_from_slice(&p2);
    member.extend(state(&[fx(2.0), fx(0.125)]));
    let particle = |p: [i64; 3]| {
        let mut v = vec![0u8];
        v.extend(state(&[p[0], p[1], p[2], 0, 0, 0, fx(1.0)]));
        v
    };
    let mut t = vec![(p1, particle([0, fx(2.0), 0])), (p2, particle([fx(2.0), fx(2.0), 0])), (node_id(8), member)];
    // Positions at the edges of §3: JSON's switch to exponent form (2^-20 below it, 2^-17
    // above), USD's (2^-23), binary32 ties in both directions (2^24 + 1, 2^24 + 3), binary64
    // ties (2^53 + 1, 2^53 + 3), the least step and the largest value.
    let mut e = vec![
        (node_id(9), kinematic_state([1 << 12, 1 << 15, 1 << 9])),
        (node_id(10), kinematic_state([(1 << 24) + 1, (1 << 24) + 3, -((1 << 24) + 1)])),
        (node_id(11), kinematic_state([(1 << 53) + 1, (1 << 53) + 3, fx(1.0)])),
        (node_id(12), kinematic_state([1, -1, i64::MAX])),
    ];
    vec![
        ("edges", world_bytes(&contract("wai.world.sim.kinematic0"), &mut e, &[])),
        ("kinematic", world_bytes(&contract("wai.world.sim.kinematic0"), &mut k, &assets)),
        ("ballistic", world_bytes(&contract("wai.world.sim.ballistic0"), &mut b, &[])),
        ("truss", world_bytes(&contract("wai.world.sim.truss0"), &mut t, &[])),
    ]
}

fn media(name: &str, asset: [u8; 32], cap: &str, size: Option<(u32, u32)>) -> MediaItem {
    MediaItem { name: name.into(), asset, capability: cap.into(), size }
}

fn media_v(m: &[MediaItem]) -> Value {
    Value::Array(
        m.iter()
            .map(|x| {
                let mut o = json!({"name": x.name, "asset": hex(&x.asset), "capability": x.capability});
                if let Some((w, h)) = x.size {
                    o["width"] = json!(w);
                    o["height"] = json!(h);
                }
                o
            })
            .collect(),
    )
}

/// The hash of the source of the committed `moving.wscn`.
fn first_source(wscn: &[u8]) -> [u8; 32] {
    let v = super::read_audio(wscn).expect("a committed audio scene");
    v.objects[0].source
}

fn params_v(p: &Option<ImportParams>) -> Value {
    match p {
        None => Value::Null,
        Some(p) => json!({
            "actor": hex(&p.actor),
            "world_contract": p.world_contract.as_ref().map(|c| String::from_utf8(c.clone()).unwrap()),
            "audio_contract": p.audio_contract.as_ref().map(|c| String::from_utf8(c.clone()).unwrap()),
        }),
    }
}

/// The foreign glTF documents: `(name, text)`.
fn foreign_gltf(src: [u8; 32], video: [u8; 32]) -> Vec<(&'static str, String)> {
    let urn = |h: &[u8; 32]| format!("urn:wai:asset:{}", hex(h));
    let nodes = r#"{"asset":{"version":"2.0"},"scene":0,"scenes":[{"nodes":[0,3]}],"nodes":[{"name":"base","translation":[1,0,-2],"children":[1,2]},{"name":"lamp","translation":[0.5,2,0]},{"name":"crate","mesh":0,"translation":[-1.25,0,0.75]},{"name":"eye","camera":0,"translation":[0,1.5,4]}],"meshes":[{"primitives":[{"attributes":{"POSITION":0}}]}],"accessors":[{"componentType":5126,"count":3,"type":"VEC3","min":[0,0,0],"max":[1,1,0]}],"cameras":[{"type":"perspective","perspective":{"yfov":1,"znear":0.1}}]}"#.to_string();
    // Times 0, 0.5, 1 s at 48 kHz: samples 0, 24000, 48000. Positions -2, 0 and 2 on x.
    let times: Vec<f32> = vec![0.0, 0.5, 1.0];
    let poss: Vec<[f32; 3]> = vec![[-2.0, 0.0, -1.0], [0.0, 0.0, -1.0], [2.0, 0.0, -1.0]];
    let mut data: Vec<u8> = times.iter().flat_map(|t| t.to_le_bytes()).collect();
    for p in &poss {
        for x in p {
            data.extend_from_slice(&x.to_le_bytes());
        }
    }
    let b64 = gltf::base64(&data);
    let audio = |src_uri: &str, mime: &str, interp: &str, t1: &str, pregain: &str, ty: &str| {
        let times_b64 = if t1 == "0.5" {
            b64.clone()
        } else {
            let mut d = data.clone();
            d[4..8].copy_from_slice(&0.1f32.to_le_bytes());
            gltf::base64(&d)
        };
        format!(
            r#"{{"asset":{{"version":"2.0"}},"extensionsUsed":["MPEG_accessor_timed","MPEG_audio_spatial","MPEG_buffer_circular","MPEG_media","MPEG_texture_video"],"extensions":{{"MPEG_media":{{"media":[{{"name":"voice","autoplay":true,"alternatives":[{{"uri":"{src_uri}","mimeType":"{mime}"}}]}},{{"name":"screen","startTime":0,"alternatives":[{{"uri":"https://example.invalid/screen.mp4","mimeType":"video/mp4"}},{{"uri":"{v}","mimeType":"application/wai; codecs=\"wai1.video.av1\""}}]}}]}}}},"scene":0,"scenes":[{{"nodes":[0]}}],"nodes":[{{"name":"room","translation":[0,1,0],"children":[1]}},{{"name":"speaker","translation":[-2,0,-1],"extensions":{{"MPEG_audio_spatial":{{"sources":[{{"id":0,"type":"{ty}","accessors":[2]{pregain}}}]}}}}}}],"buffers":[{{"byteLength":{len},"uri":"data:application/octet-stream;base64,{times_b64}"}},{{"byteLength":4096,"extensions":{{"MPEG_buffer_circular":{{"media":0}}}}}},{{"byteLength":6912,"extensions":{{"MPEG_buffer_circular":{{"media":1}}}}}}],"bufferViews":[{{"buffer":0,"byteLength":12}},{{"buffer":0,"byteOffset":12,"byteLength":36}},{{"buffer":1,"byteLength":4096}},{{"buffer":2,"byteLength":6912}}],"accessors":[{{"bufferView":0,"componentType":5126,"count":3,"type":"SCALAR","min":[0],"max":[1]}},{{"bufferView":1,"componentType":5126,"count":3,"type":"VEC3"}},{{"bufferView":2,"componentType":5126,"count":1024,"type":"SCALAR","extensions":{{"MPEG_accessor_timed":{{"immutable":true}}}}}},{{"bufferView":3,"componentType":5121,"normalized":true,"count":2304,"type":"VEC3","extensions":{{"MPEG_accessor_timed":{{"immutable":true}}}}}}],"animations":[{{"channels":[{{"sampler":0,"target":{{"node":1,"path":"translation"}}}}],"samplers":[{{"input":0,"interpolation":"{interp}","output":1}}]}}],"textures":[{{"extensions":{{"MPEG_texture_video":{{"accessor":3,"width":48,"height":16,"format":"RGB"}}}}}}]}}"#,
            v = urn(&video),
            len = data.len(),
        )
    };
    let wai_mime = r#"application/wai; codecs=\"wai1.audio.opus\""#;
    vec![
        ("nodes", nodes.clone()),
        ("audio", audio(&urn(&src), wai_mime, "LINEAR", "0.5", "", "Object")),
        ("rotation", nodes.replacen(r#""name":"lamp","translation":[0.5,2,0]"#, r#""name":"lamp","translation":[0.5,2,0],"rotation":[0,0.7071068,0,0.7071068]"#, 1)),
        ("inexact", nodes.replacen("[0.5,2,0]", "[0.5,1e-10,0]", 1)),
        ("step", audio(&urn(&src), wai_mime, "STEP", "0.5", "", "Object")),
        ("time", audio(&urn(&src), wai_mime, "LINEAR", "0.1", "", "Object")),
        ("pregain", audio(&urn(&src), wai_mime, "LINEAR", "0.5", r#","pregain":6"#, "Object")),
        ("hoa", audio(&urn(&src), wai_mime, "LINEAR", "0.5", "", "HOA")),
        ("https", audio("https://example.invalid/voice.ogg", "audio/ogg", "LINEAR", "0.5", "", "Object")),
        ("not_json", nodes[..nodes.len() / 2].to_string()),
        ("not_gltf2", nodes.replacen(r#""version":"2.0""#, r#""version":"1.0""#, 1)),
    ]
}

/// The foreign USDA layers: `(name, text)`.
fn foreign_usda(mesh: [u8; 32]) -> Vec<(&'static str, String)> {
    let layer = format!(
        r#"#usda 1.0
(
    "A layer written outside WAI"
    defaultPrim = "Set"
    metersPerUnit = 1
    upAxis = "Y"
)

def Xform "Set" (
    kind = "group"
)
{{
    double3 xformOp:translate = (10, 0, 0)
    uniform token[] xformOpOrder = ["xformOp:translate"]

    def Xform "Pillar"   # a pillar
    {{
        double3 xformOp:translate = (0.5, 3, -0.25)
        uniform token[] xformOpOrder = ["xformOp:translate"]

        def Xform "Cap"
        {{
            double3 xformOp:translate = (0, 1.5, 0)
        }}
    }}

    def Mesh "Floor"
    {{
        int[] faceVertexCounts = [3]
        int[] faceVertexIndices = [0, 1, 2]
        point3f[] points = [(0, 0, 0), (1, 0, 0), (0, 0, 1)]
    }}

    /* an asset by its content hash */
    def "Statue" (
        prepend references = @urn:wai:asset:{}@
    )
    {{
    }}
}}
"#,
        hex(&mesh)
    );
    vec![
        ("layer", layer.clone()),
        ("rotate", layer.replacen("        uniform token[] xformOpOrder = [\"xformOp:translate\"]\n\n        def Xform \"Cap\"", "        float3 xformOp:rotateXYZ = (0, 90, 0)\n        uniform token[] xformOpOrder = [\"xformOp:translate\", \"xformOp:rotateXYZ\"]\n\n        def Xform \"Cap\"", 1)),
        ("rotate_unlisted", layer.replacen("            double3 xformOp:translate = (0, 1.5, 0)\n", "            double3 xformOp:translate = (0, 1.5, 0)\n            float3 xformOp:rotateXYZ = (0, 90, 0)\n", 1)),
        ("inexact", layer.replacen("(0, 1.5, 0)", "(0, 0.1, 0)", 1)),
        ("asset_path", layer.replacen(&format!("@urn:wai:asset:{}@", hex(&mesh)), "@./statue.usda@", 1)),
        ("variant", layer.replacen("    def Mesh \"Floor\"", "    variantSet \"look\" = {\n        \"plain\" {\n        }\n    }\n\n    def Mesh \"Floor\"", 1)),
        ("over", layer.replacen("    def Mesh \"Floor\"", "    over \"Floor\"", 1)),
        ("broken", layer[..layer.len() - 3].to_string()),
    ]
}

/// The inputs whose bytes are missing or do not match their pins.
pub fn check_inputs(inputs: &BTreeMap<String, Vec<u8>>) -> Vec<String> {
    INPUT_FILES.iter().filter(|(p, d)| inputs.get(*p).map(|b| crate::pin::sha256_hex(b)).as_deref() != Some(*d)).map(|(p, _)| p.to_string()).collect()
}

/// Generate the corpus from `inputs` (by the paths of [`INPUT_FILES`]): file name → bytes.
pub fn generate(inputs: &BTreeMap<String, Vec<u8>>) -> BTreeMap<String, Vec<u8>> {
    assert!(check_inputs(inputs).is_empty(), "the inputs must match their pins");
    let mut files: BTreeMap<String, Vec<u8>> = BTreeMap::new();
    let mut vectors: Vec<Value> = Vec::new();
    let mut seen: std::collections::BTreeSet<String> = Default::default();
    let path_of = |name: &str| -> String { INPUT_FILES.iter().find(|(p, _)| p.ends_with(name)).expect("an input").0.to_string() };
    for (name, bytes) in built_worlds() {
        files.insert(format!("inputs/{name}.wwld"), bytes);
    }
    let get = |files: &BTreeMap<String, Vec<u8>>, p: &str| -> Vec<u8> {
        files.get(p).cloned().or_else(|| inputs.get(p).cloned()).unwrap_or_else(|| panic!("{p}"))
    };
    let src = first_source(&inputs[&path_of("moving.wscn")]);
    let video = label("asset/screen.av1");
    let amb = label("asset/ambience.flac");
    let screen = media("screen", video, "wai.video.av1", Some((64, 36)));
    type Spec<'a> = (&'a str, Option<String>, Option<String>, Vec<MediaItem>);
    let bundles: Vec<Spec<'_>> = vec![
        ("kinematic", Some("inputs/kinematic.wwld".into()), None, vec![screen.clone(), media("ambience", amb, "wai.audio.flac", None)]),
        ("ballistic", Some("inputs/ballistic.wwld".into()), None, vec![]),
        ("edges", Some("inputs/edges.wwld".into()), None, vec![]),
        ("truss", Some("inputs/truss.wwld".into()), None, vec![]),
        ("empty_scene", Some(path_of("scene.wwld")), None, vec![]),
        ("replay", Some(path_of("basic_kinematics.wwld")), None, vec![]),
        ("moving", None, Some(path_of("moving.wscn")), vec![media("tone", src, "wai.audio.flac", None)]),
        ("distance", None, Some(path_of("distance.wscn")), vec![]),
        ("stereo_pair", None, Some(path_of("stereo_pair.wscn")), vec![]),
        ("full", Some("inputs/kinematic.wwld".into()), Some(path_of("moving.wscn")), vec![screen.clone(), media("tone", src, "wai.audio.flac", None)]),
    ];
    let bundle_v = |w: &Option<String>, a: &Option<String>, m: &[MediaItem]| json!({"world": w, "audio": a, "media": media_v(m)});
    let mut push = |v: Value, seen: &mut std::collections::BTreeSet<String>| {
        if let Some(c) = v.get("code").and_then(Value::as_str) {
            seen.insert(c.to_string());
        }
        vectors.push(v);
    };
    // Exports and their round trips.
    for (name, w, a, m) in &bundles {
        let b = Bundle { world: w.as_ref().map(|p| get(&files, p)), audio: a.as_ref().map(|p| get(&files, p)), media: m.clone() };
        for fmt in ["gltf", "usda"] {
            let out = if fmt == "gltf" { gltf::export(&b) } else { usda::export(&b) }.unwrap_or_else(|e| panic!("{name}: {e}"));
            let back = if fmt == "gltf" { gltf::import(&out, &ImportParams::default()) } else { usda::import(&out, &ImportParams::default()) }.unwrap();
            assert_eq!(back, Imported { bundle: b.clone(), dropped: vec![] }, "{name} {fmt}");
            let p = format!("out/{name}.{fmt}");
            files.insert(p.clone(), out);
            push(json!({"name": format!("{name}_{fmt}_export"), "op": "export", "format": fmt, "bundle": bundle_v(w, a, m), "outcome": "accept", "file": p}), &mut seen);
            push(json!({"name": format!("{name}_{fmt}_import"), "op": "import", "format": fmt, "file": p, "params": Value::Null, "outcome": "accept", "bundle": bundle_v(w, a, m), "dropped": []}), &mut seen);
        }
    }
    // Bundles refused on export.
    let mut bad_world = get(&files, "inputs/kinematic.wwld");
    {
        // Swap the first two node ids of the graph: ascending order broken.
        let secs = super::sections(&bad_world, b"WWLD", "world_malformed").unwrap();
        let g = &secs[&2];
        let at = bad_world.windows(g.len()).position(|w| w == g.as_slice()).unwrap();
        let first = at + 4;
        let second = first + 16 + 4 + 56;
        let (a, b): (Vec<u8>, Vec<u8>) = (bad_world[first..first + 16].to_vec(), bad_world[second..second + 16].to_vec());
        bad_world[first..first + 16].copy_from_slice(&b);
        bad_world[second..second + 16].copy_from_slice(&a);
    }
    files.insert("inputs/neg/unordered.wwld".into(), bad_world);
    let moving = get(&files, &path_of("moving.wscn"));
    let mut truncated = moving.clone();
    {
        let secs = super::sections(&moving, b"WSCN", "audio_malformed").unwrap();
        let t = &secs[&2];
        let at = moving.windows(t.len()).position(|w| w == t.as_slice()).unwrap();
        // Claim one more key for the first object than the table holds.
        let nk = u16::from_le_bytes([moving[at + 44], moving[at + 45]]);
        truncated[at + 44..at + 46].copy_from_slice(&(nk + 1).to_le_bytes());
    }
    files.insert("inputs/neg/truncated.wscn".into(), truncated);
    let descending = {
        let v = super::read_audio(&moving).unwrap();
        let keys: Vec<(u64, [i64; 3])> = v.objects[0].keys.iter().rev().map(|k| (k.sample, k.pos)).collect();
        super::audio_bytes(&super::sections(&moving, b"WSCN", "audio_malformed").unwrap()[&1], &[(v.objects[0].source, keys)], &[])
    };
    files.insert("inputs/neg/descending.wscn".into(), descending);
    type NegSpec<'a> = (&'a str, Option<&'a str>, Option<&'a str>, Vec<MediaItem>);
    let neg_bundles: Vec<NegSpec<'_>> = vec![
        ("unordered_world", Some("inputs/neg/unordered.wwld"), None, vec![]),
        ("truncated_audio", None, Some("inputs/neg/truncated.wscn"), vec![]),
        ("descending_keys", None, Some("inputs/neg/descending.wscn"), vec![]),
        ("world_capability", None, None, vec![media("thing", video, "wai.world.scene", None)]),
        ("video_without_size", None, None, vec![media("screen", video, "wai.video.av1", None)]),
    ];
    for (name, w, a, m) in &neg_bundles {
        let b = Bundle { world: w.map(|p| get(&files, p)), audio: a.map(|p| get(&files, p)), media: m.clone() };
        for fmt in ["gltf", "usda"] {
            let r = if fmt == "gltf" { gltf::export(&b) } else { usda::export(&b) };
            let code = r.expect_err(name).code;
            push(json!({"name": format!("{name}_{fmt}"), "op": "export", "format": fmt, "bundle": bundle_v(&w.map(str::to_string), &a.map(str::to_string), m), "outcome": "refuse", "code": code}), &mut seen);
        }
    }
    // Documents: WAI-written ones altered, and foreign ones.
    let actor = label("actor");
    let full = |wc: Option<Vec<u8>>, ac: Option<Vec<u8>>| Some(ImportParams { actor, world_contract: wc, audio_contract: ac });
    let kin = contract("wai.world.sim.kinematic0");
    let both = full(Some(kin.clone()), Some(AUDIO_CONTRACT.to_vec()));
    let mut docs: Vec<(String, &str, Vec<u8>, Option<ImportParams>)> = Vec::new();
    let kg = files["out/kinematic.gltf"].clone();
    let ku = files["out/kinematic.usda"].clone();
    let s = String::from_utf8(kg.clone()).unwrap();
    docs.push(("changed".into(), "gltf", s.replacen("\"translation\":[1,2,3]", "\"translation\":[1,2,4]", 1).into_bytes(), None));
    let wpos = s.find("\"world\":\"").unwrap() + 9;
    let mut bh = kg.clone();
    bh[wpos] = b'z';
    docs.push(("carried_bad_hex".into(), "gltf", bh, None));
    let su = String::from_utf8(ku.clone()).unwrap();
    docs.push(("changed".into(), "usda", su.replacen("(1, 2, 3)", "(1, 2, 4)", 1).into_bytes(), None));
    docs.push(("carried_bad_profile".into(), "usda", su.replacen("\"wai-scene/1\"", "\"wai-scene/9\"", 1).into_bytes(), None));
    for (n, t) in foreign_gltf(src, video) {
        let p = if matches!(n, "nodes" | "rotation" | "inexact") { full(Some(kin.clone()), None) } else { both.clone() };
        docs.push((format!("foreign_{n}"), "gltf", t.into_bytes(), p));
    }
    let nodes_doc = docs.iter().find(|d| d.0 == "foreign_nodes").unwrap().2.clone();
    docs.push(("foreign_nodes_no_contract".into(), "gltf", nodes_doc.clone(), full(None, None)));
    docs.push(("foreign_nodes_ballistic".into(), "gltf", nodes_doc, full(Some(contract("wai.world.sim.ballistic0")), None)));
    for (n, t) in foreign_usda(label("asset/statue.usd")) {
        docs.push((format!("foreign_{n}"), "usda", t.into_bytes(), full(Some(kin.clone()), None)));
    }
    let layer_doc = docs.iter().find(|d| d.0 == "foreign_layer").unwrap().2.clone();
    docs.push(("foreign_layer_no_contract".into(), "usda", layer_doc, full(None, None)));
    for (name, fmt, bytes, params) in docs {
        let p = format!("docs/{name}.{fmt}");
        let r = if fmt == "gltf" { gltf::import(&bytes, &params.clone().unwrap_or_default()) } else { usda::import(&bytes, &params.clone().unwrap_or_default()) };
        files.insert(p.clone(), bytes);
        let v = match r {
            Ok(imp) => {
                let w = imp.bundle.world.map(|b| {
                    let q = format!("imported/{name}_{fmt}.wwld");
                    files.insert(q.clone(), b);
                    q
                });
                let a = imp.bundle.audio.map(|b| {
                    let q = format!("imported/{name}_{fmt}.wscn");
                    files.insert(q.clone(), b);
                    q
                });
                json!({"name": format!("{name}_{fmt}"), "op": "import", "format": fmt, "file": p, "params": params_v(&params), "outcome": "accept", "bundle": bundle_v(&w, &a, &imp.bundle.media), "dropped": imp.dropped})
            }
            Err(e) => json!({"name": format!("{name}_{fmt}"), "op": "import", "format": fmt, "file": p, "params": params_v(&params), "outcome": "refuse", "code": e.code}),
        };
        push(v, &mut seen);
    }
    let missing: Vec<&&str> = CODES.iter().filter(|c| !seen.contains(**c)).collect();
    assert!(missing.is_empty(), "the corpus gives no vector the codes {missing:?}");
    let inputs_v: Vec<Value> = INPUT_FILES.iter().map(|(p, d)| json!({"path": p, "sha256": d})).collect();
    let files_v: Vec<Value> = files.iter().map(|(p, b)| json!({"file": p, "sha256": crate::pin::sha256_hex(b)})).collect();
    let doc = json!({
        "about": "the conformance corpus of extensions/scene-carriage.md §9, written by wai_scene_vectors gen",
        "inputs": inputs_v,
        "files": files_v,
        "vectors": vectors,
    });
    let mut text = serde_json::to_string_pretty(&doc).expect("json");
    text.push('\n');
    files.insert("vectors.json".into(), text.into_bytes());
    files
}

/// Check a corpus directory's files against this module: every file regenerates byte for
/// byte and nothing else is there. Returns the failures.
pub fn verify(corpus: &BTreeMap<String, Vec<u8>>, inputs: &BTreeMap<String, Vec<u8>>) -> Vec<String> {
    let missing = check_inputs(inputs);
    if !missing.is_empty() {
        return vec![format!("inputs missing or not matching their pins: {missing:?}")];
    }
    let want = generate(inputs);
    let mut bad = Vec::new();
    for (p, b) in &want {
        match corpus.get(p) {
            Some(x) if x == b => {}
            Some(_) => bad.push(format!("{p} does not regenerate byte for byte")),
            None => bad.push(format!("{p} is missing")),
        }
    }
    for p in corpus.keys() {
        if !want.contains_key(p) {
            bad.push(format!("{p} is not a file of the corpus"));
        }
    }
    bad
}
