//! The conformance corpus of extensions/ad-presentation.md §12: `live-conformance/presentation/`.
//!
//! [`generate`] writes it from fixed seeds, as the live-ads corpus is written (its rooms are
//! built by [`super::corpus`]'s helpers): every session key is SHA-256 of this corpus's label
//! and an index, every media object's content hash SHA-256 of a label and two numbers,
//! every token's nonce and salt likewise, and Ed25519 and the RSASSA-PSS of RFC 9578 §6 over
//! a given salt are deterministic, so every file is the same bytes on every machine. The
//! attestation issuer's key is the published test key of RFC 9578 Appendix A.2, test vector
//! 1, whose own token the corpus also carries, so that a verifier checks its RSASSA-PSS
//! against the RFC's vector before it reads any token of the corpus.
//!
//! Each vector states the outcome it was written to have, and the generator asserts that
//! this module reaches it, that each claim gets the verdict it was written to get, and that
//! the corpus gives every reason of §11.
//!
//! [`verify`] checks a corpus directory's files against this module.
//! `live-conformance/ad_presentation_verify.py` checks the same files from the extension's
//! text, sharing no code with this module.

use std::collections::{BTreeMap, BTreeSet};

use serde_json::{json, Map, Value};

use super::corpus::{self as base, policy, room, seal, Room, Seal, Spec, PRODUCER, S, SPEC};
use super::presentation::{self as pr, geometry, require, rule, span_flag, token_type, Claim, ClaimFacts, Creative, CreativeObject, Issuer, Requirement, Span};
use super::*;

/// RFC 9578 Appendix A.2, its first test vector: the issuer's public key (`pkI`), a DER
/// SubjectPublicKeyInfo of id-RSASSA-PSS.
pub const RFC9578_PKI: &str = "30820152303d06092a864886f70d01010a3030a00d300b0609608648016503040202a11a301806092a864886f70d010108300b0609608648016503040202a2030201300382010f003082010a0282010100cb1aed6b6a95f5b1ce013a4cfcab25b94b2e64a23034e4250a7eab43c0df3a8c12993af12b111908d4b471bec31d4b6c9ad9cdda90612a2ee903523e6de5a224d6b02f09e5c374d0cfe01d8f529c500a78a2f67908fa682b5a2b430c81eaf1af72d7b5e794fc98a3139276879757ce453b526ef9bf6ceb99979b8423b90f4461a22af37aab0cf5733f7597abe44d31c732db68a181c6cbbe607d8c0e52e0655fd9996dc584eca0be87afbcd78a337d17b1dba9e828bbd81e291317144e7ff89f55619709b096cbb9ea474cead264c2073fe49740c01f00e109106066983d21e5f83f086e2e823c879cd43cef700d2a352a9babd612d03cad02db134b7e225a5f0203010001";
/// The same vector's TokenChallenge.
pub const RFC9578_CHALLENGE: &str = "0002000e6973737565722e6578616d706c65208e7acc900e393381e8810b7c9e4a68b5163f1f880ab6688a6ffe780923609e88000e6f726967696e2e6578616d706c65";
/// The same vector's token.
pub const RFC9578_TOKEN: &str = "0002aa72019d1f951df197021ce63876fe8b0a02dc1c31a12b0a2dd1508d07827f055969f643b4cfda5196d4aa86aeb5368834f4f06de46950ed435b3b81bd036d44ca572f8982a9ca248a3056186322d93ca147266121ddeb5632c07f1f71cd2708bc6a21b533d07294b5e900faf5537dd3eb33cee4e08c9670d1e5358fd184b0e00c637174f5206b14c7bb0e724ebf6b56271e5aa2ed94c051c4a433d302b23bc52460810d489fb050f9de5c868c6c1b06e3849fd087629f704cc724bc0d0984d5c339686fcdd75f9a9cdd25f37f855f6f4c584d84f716864f546b696d620c5bd41a811498de84ff9740ba3003ba2422d26b91eb745c084758974642a42078201543246ddb58030ea8e722376aa82484dca9610a8fb7e018e396165462e17a03e40ea7e128c090a911ecc708066cb201833010c1ebd4e910fc8e27a1be467f78671836a508257123a45e4e0ae2180a434bd1037713466347a8ebe46439d3da1970";
/// The modulus of that issuer key, big-endian.
pub(crate) const ISSUER_N: &str = "cb1aed6b6a95f5b1ce013a4cfcab25b94b2e64a23034e4250a7eab43c0df3a8c12993af12b111908d4b471bec31d4b6c9ad9cdda90612a2ee903523e6de5a224d6b02f09e5c374d0cfe01d8f529c500a78a2f67908fa682b5a2b430c81eaf1af72d7b5e794fc98a3139276879757ce453b526ef9bf6ceb99979b8423b90f4461a22af37aab0cf5733f7597abe44d31c732db68a181c6cbbe607d8c0e52e0655fd9996dc584eca0be87afbcd78a337d17b1dba9e828bbd81e291317144e7ff89f55619709b096cbb9ea474cead264c2073fe49740c01f00e109106066983d21e5f83f086e2e823c879cd43cef700d2a352a9babd612d03cad02db134b7e225a5f";
/// Its private exponent, big-endian: a test key the RFC publishes, used here to sign the
/// corpus's tokens.
pub(crate) const ISSUER_D: &str = "2f309b7738b58cf779f0c915f822dfc9f490dbeadf1e7adfac578ffd5703c6bba2da9d5a49725889b7ba112f8c6ff30551d9473e000bc35c50e167ab7fa73a0c2b21965c4b59257c1ac06cbdcf28e863f6718ea8c29043c1c6da84629490fd0491bf52d172c959c1921b96949c725747b8a7c7871995fa0b9eb1107ba854c6c32a6f4747c5d1c304c7467d55b080aa233a3a52ffdf26114cbb541ffa6b696b7a096f8dcff69ca08ab58efb0fb753aa5185c73001696bbce8c099e3f435dcbbfe05e7dbaad23db2a659c23add521407f150b1af2d9054c4164229d6840b7a32a4f8481493ae8959cf0aea8def0c3c9f75b9a34c75718b7072c8dbab17cefeca49";
/// The name the corpus's ad policies give the issuer.
pub(crate) const ISSUER_NAME: &[u8] = b"issuer.example";

fn unhex(s: &str) -> Vec<u8> {
    base::unhex(s).expect("a hexadecimal constant")
}

fn sha(parts: &[&[u8]]) -> [u8; 32] {
    base::sha(parts)
}

/// Session key `i` (ad-presentation §3).
fn player(i: u8) -> SigningKey {
    SigningKey::from_bytes(&sha(&[b"wai:ad-presentation/corpus/player\x00", &[i]]))
}

fn player_pk(i: u8) -> [u8; 32] {
    player(i).verifying_key().to_bytes()
}

/// A creative of objects of these durations; object `j`'s bytes are named by SHA-256 of the
/// label, the creative's number and `j`.
fn creative(n: u8, durations: &[u32]) -> Creative {
    Creative {
        objects: durations
            .iter()
            .enumerate()
            .map(|(j, &d)| CreativeObject { content_hash: sha(&[b"wai:ad-presentation/corpus/object\x00", &[n, j as u8]]), duration_ms: d })
            .collect(),
    }
}

fn creative_x() -> Creative {
    creative(1, &[4_000, 4_000, 4_000, 3_000])
}
fn creative_y() -> Creative {
    creative(2, &[5_000, 5_000, 5_000])
}
fn creative_z() -> Creative {
    creative(3, &[5_000, 5_000])
}
fn creative_w() -> Creative {
    creative(4, &[3_000, 3_000, 3_000, 3_000, 3_000])
}

fn ad(id: &str, c: &Creative, price_micros: u64) -> AdCandidate {
    AdCandidate { ad_id: id.into(), creative_hash: c.digest(), duration_ms: c.duration_ms(), price_micros }
}

/// Three ads over two slots and a 30 s avail: ad-x and ad-y fill it.
fn pod() -> Vec<AdCandidate> {
    vec![ad("ad-x", &creative_x(), 40_000), ad("ad-y", &creative_y(), 30_000), ad("ad-z", &creative_z(), 25_000)]
}

fn creatives() -> Vec<Creative> {
    vec![creative_x(), creative_y(), creative_z(), creative_w()]
}

fn issuers() -> Vec<Issuer> {
    vec![Issuer { name: ISSUER_NAME.to_vec(), token_type: token_type::BLIND_RSA, keys: vec![unhex(RFC9578_PKI)] }]
}

fn span(start_ms: u32, length_ms: u32, visible_bp: u16, flags: u8) -> Span {
    Span { start_ms, length_ms, visible_bp, flags }
}

/// One span over a whole creative, every pixel in view, audible.
fn whole(c: &Creative) -> Vec<Span> {
    vec![span(0, c.duration_ms() as u32, 10_000, span_flag::AUDIBLE)]
}

/// What a claim is written from.
#[derive(Clone)]
struct Draft {
    room: [u8; 32],
    cue: [u8; 32],
    /// The session key whose session the claim names.
    session_of: u8,
    slot: u8,
    creative: Creative,
    geometry: u8,
    frames: u32,
    spans: Vec<Span>,
    /// The key that signs it: `session_of`'s unless a control says otherwise.
    signer: Option<u8>,
    /// A token for this claim's own challenge, by this index (its nonce and salt).
    token: Option<u8>,
}

impl Draft {
    fn new(room: &Room, cue: [u8; 32], session_of: u8, slot: u8, creative: Creative, spans: Vec<Span>) -> Draft {
        Draft { room: room.id, cue, session_of, slot, creative, geometry: geometry::AD, frames: pr::NOT_EXPOSED, spans, signer: None, token: None }
    }

    fn unsigned(&self) -> Claim {
        let signer = self.signer.unwrap_or(self.session_of);
        Claim {
            room: self.room,
            cue: self.cue,
            session: pr::session_of(&self.room, &player_pk(self.session_of)),
            slot: self.slot,
            creative_hash: self.creative.digest(),
            player: player_pk(signer),
            geometry: self.geometry,
            frames: self.frames,
            objects: self.creative.touched(&self.spans),
            spans: self.spans.clone(),
            attestation: vec![],
            sig: [0; 64],
        }
    }

    fn claim(&self) -> Claim {
        let mut c = self.unsigned();
        if let Some(i) = self.token {
            c.attestation = token_for(&c, i);
        }
        c.sign(&player(self.signer.unwrap_or(self.session_of)))
    }
}

/// A token of type 0x0002 for a claim's own challenge (ad-presentation §7.2), signed with the
/// RFC's test issuer key, its nonce and salt by index `i`.
fn token_for(c: &Claim, i: u8) -> Vec<u8> {
    let challenge = pr::token_challenge(token_type::BLIND_RSA, ISSUER_NAME, &pr::redemption_context(c));
    let mut t = token_type::BLIND_RSA.to_be_bytes().to_vec();
    t.extend_from_slice(&sha(&[b"wai:ad-presentation/corpus/nonce\x00", &[i]]));
    t.extend_from_slice(&sha(&[&challenge]));
    t.extend_from_slice(&sha(&[&unhex(RFC9578_PKI)]));
    let mut salt = [0u8; pr::PSS_SALT];
    salt.copy_from_slice(&sha2::Sha384::digest([b"wai:ad-presentation/corpus/salt\x00".as_slice(), &[i]].concat()));
    let auth = pr::pss_sign(&unhex(ISSUER_N), &unhex(ISSUER_D), &t, &salt);
    t.extend_from_slice(&auth);
    t
}

/// A receipt sealed for session key `p`'s session.
fn sealed(room: &Room, p: &AdPolicy, cue: [u8; 32], player: u8, format: u8, pod: Vec<AdCandidate>, quartiles: &[u8]) -> SsaiBreakReceipt {
    let mut s = Seal::new(cue, 30_000, 0, format, pod, quartiles);
    s.session_bytes = Some(pr::session_of(&room.id, &player_pk(player)));
    seal(room, p, &s)
}

/// What a vector was written to do.
#[derive(Clone, Copy, Debug)]
enum Intent {
    Accept,
    Refuse(&'static str, Reason),
}

struct Vector {
    name: &'static str,
    about: &'static str,
    room: Room,
    policy: Vec<u8>,
    receipts: Vec<Value>,
    pods: Vec<Vec<AdCandidate>>,
    /// Each claim's bytes, what it is, and the verdict it was written to get.
    claims: Vec<(Vec<u8>, &'static str, Result<(), pr::Reason>)>,
    intent: Intent,
}

impl Vector {
    fn run(&self) -> Run {
        let chains = self.room.chains();
        let claims: Vec<Vec<u8>> = self.claims.iter().map(|c| c.0.clone()).collect();
        let (cr, is) = (creatives(), issuers());
        super::verify(&Input {
            descriptor: &self.room.descriptor,
            grants: &self.room.grants,
            head: self.room.head.as_deref(),
            chains: &chains,
            policy: &self.policy,
            pt0_ms: 0,
            receipts: &self.receipts,
            pods: &self.pods,
            claims: &claims,
            creatives: &cr,
            issuers: &is,
            supply_files: &[],
        })
    }
}

fn requirement(require: u8, rule: u8, attest: bool) -> Requirement {
    Requirement { require, rule, token_type: if attest { token_type::BLIND_RSA } else { 0 }, issuer: if attest { ISSUER_NAME.to_vec() } else { vec![] } }
}

/// A room with one 30 s mid-roll at 120 s, under a policy of version 2.
fn mid_roll(spec: Spec, req: Requirement) -> (Room, AdPolicy, [u8; 32]) {
    let mut r = room(spec);
    let b = r.cue(PRODUCER, 120 * S, cue_type::BREAK, 21, 30_000);
    r.annotate(PRODUCER, 120 * S, b, placement::MID);
    let p = policy(&r, |p| {
        p.open_avail_ms = 0;
        p.preload_ms = 0;
        p.presentation = Some(req);
    });
    (r, p, b)
}

fn vectors() -> Vec<Vector> {
    let mut out = Vec::new();
    let (x, y) = (creative_x(), creative_y());
    let ok = Ok(());
    let no = Err;

    // The video rule: good impressions, and a control of each claim check.
    let (r, p, b) = mid_roll(SPEC, requirement(require::CLAIM, rule::VIDEO, false));
    let fmt = asset_format::HLS;
    let receipts = [
        sealed(&r, &p, b, 1, fmt, pod(), &[5, 4]),
        sealed(&r, &p, b, 2, asset_format::DASH, pod(), &[5, 2]),
        sealed(&r, &p, b, 3, fmt, pod(), &[5, 5]),
        sealed(&r, &p, b, 5, fmt, pod(), &[5, 5]),
        sealed(&r, &p, b, 7, fmt, pod(), &[5, 5]),
    ];
    let d = |pl: u8, slot: u8, c: &Creative, spans: Vec<Span>| Draft::new(&r, b, pl, slot, c.clone(), spans);
    let good = d(3, 0, &x, whole(&x)).claim();
    let mut swapped = d(3, 1, &y, whole(&y)).claim();
    swapped.objects[1] = sha(&[b"wai:ad-presentation/corpus/object\x00", &[9, 9]]);
    let swapped = swapped.sign(&player(3));
    let unbound = Draft { signer: Some(4), ..d(3, 1, &y, whole(&y)) }.claim();
    let mut forged = d(5, 1, &y, whole(&y)).claim();
    forged.sig[20] ^= 0x01;
    let screen = Draft { geometry: geometry::SCREEN, ..d(7, 0, &x, whole(&x)) }.claim();
    let player_area = Draft { geometry: geometry::PLAYER, frames: 450, ..d(7, 1, &y, vec![span(0, 15_000, 6_000, span_flag::AUDIBLE)]) }.claim();
    let truncated = {
        let b = d(6, 0, &x, whole(&x)).claim().to_bytes();
        b[..b.len() - 1].to_vec()
    };
    let claims = vec![
        (d(1, 0, &x, whole(&x)).claim().to_bytes(), "viewer 1, ad-x: in view and audible from start to end", ok),
        (
            d(1, 1, &y, vec![span(0, 4_000, 3_000, 1), span(4_000, 3_000, 6_000, 1), span(7_000, 5_000, 2_000, 1)]).claim().to_bytes(),
            "viewer 1, ad-y: scrolled partly out of view; 3 s at 60% of the pixels qualify, and 12 s of 15 reach the third quartile",
            ok,
        ),
        (d(2, 0, &x, vec![span(0, 15_000, 4_000, 1)]).claim().to_bytes(), "viewer 2, ad-x: 40% of the pixels in view throughout, below the video rule's half", no(pr::Reason::ClaimNotViewable)),
        (
            d(2, 1, &y, vec![span(0, 1_500, 10_000, 1), span(1_500, 1_500, 10_000, 3), span(3_000, 1_000, 1_000, 1)]).claim().to_bytes(),
            "viewer 2, ad-y: two stretches of 1.5 s in view with a pause between them, which is not 2 continuous seconds",
            no(pr::Reason::ClaimNotViewable),
        ),
        (good.to_bytes(), "viewer 3, ad-x: in view and audible from start to end", ok),
        (good.to_bytes(), "the same claim given a second time", no(pr::Reason::ClaimDuplicate)),
        (unbound.to_bytes(), "a claim for viewer 3's ad-y signed by viewer 4's session key, from which viewer 3's session is not derived", no(pr::Reason::ClaimKeyUnbound)),
        (swapped.to_bytes(), "viewer 3, ad-y: a presented object's hash is not the creative's second object", no(pr::Reason::ClaimAssetMismatch)),
        (
            d(5, 0, &x, vec![span(0, 8_000, 10_000, 1)]).claim().to_bytes(),
            "viewer 5, ad-x: 8 s of 15 presented, the midpoint, where the receipt states it complete",
            no(pr::Reason::ClaimQuartileMismatch),
        ),
        (forged.to_bytes(), "viewer 5, ad-y: one bit of the signature flipped, so the impression has no claim", no(pr::Reason::BadSignature)),
        (
            d(5, 1, &y, vec![span(0, 4_000, 10_000, 1), span(8_000, 7_000, 10_000, 1)]).claim().to_bytes(),
            "viewer 5, ad-y, signed: a skip from 4 s to 8 s with no resumption stated, which breaks §5.4 rule 6",
            no(pr::Reason::ClaimMalformed),
        ),
        (d(6, 0, &x, whole(&x)).claim().to_bytes(), "viewer 6, whose session has no receipt for the break", no(pr::Reason::ClaimNoDecision)),
        (truncated, "a claim short of its last byte", no(pr::Reason::ClaimMalformed)),
        (screen.to_bytes(), "viewer 7, ad-x: geometry stated as a full-screen television output, which the video rule does not admit", no(pr::Reason::ClaimNotViewable)),
        (player_area.to_bytes(), "viewer 7, ad-y: 60% of the player's area in view, measured over the player and stated so, with a count of rendered frames", ok),
    ];
    out.push(Vector {
        name: "video_rule",
        about: "a 30 s mid-roll under a policy that requires a claim for each impression and the video rule: five viewers, each filled with ad-x and ad-y; each claim is good or a control of one check, and each receipt's spend counts only the impressions whose claim verifies",
        room: r.clone(),
        policy: p.to_bytes(),
        receipts: receipts.iter().map(SsaiBreakReceipt::to_json).collect(),
        pods: vec![pod(); 5],
        claims,
        intent: Intent::Accept,
    });

    // The cross-media rule with attestation and audibility, over an out and its in.
    let mut r = room(SPEC);
    let out_ = r.cue(PRODUCER, 60 * S, cue_type::OUT, 31, 0);
    r.cue(PRODUCER, 90 * S, cue_type::IN, 31, 30_000);
    let p = policy(&r, |p| p.presentation = Some(requirement(require::CLAIM | require::ATTEST | require::AUDIBLE, rule::CROSS_MEDIA, true)));
    let receipts = [
        sealed(&r, &p, out_, 11, asset_format::HLS, pod(), &[5, 5]),
        sealed(&r, &p, out_, 12, asset_format::HLS, pod(), &[5, 5]),
        sealed(&r, &p, out_, 13, asset_format::DASH, pod(), &[5, 5]),
        sealed(&r, &p, out_, 14, asset_format::DASH, pod(), &[5, 5]),
    ];
    let d = |pl: u8, slot: u8, c: &Creative, spans: Vec<Span>| Draft::new(&r, out_, pl, slot, c.clone(), spans);
    let tv = |pl: u8, slot: u8, c: &Creative, t: Option<u8>| Draft { geometry: geometry::SCREEN, token: t, ..d(pl, slot, c, whole(c)) };
    let first = tv(11, 0, &x, Some(1)).claim();
    let lifted = {
        let mut c = tv(12, 0, &x, None).unsigned();
        c.attestation = first.attestation.clone();
        c.sign(&player(12))
    };
    let broken = {
        let mut c = tv(12, 1, &y, None).unsigned();
        let mut t = token_for(&c, 2);
        let n = t.len();
        t[n - 7] ^= 0x01;
        c.attestation = t;
        c.sign(&player(12))
    };
    let claims = vec![
        (first.to_bytes(), "viewer 11, ad-x, on a television: full screen, audible throughout, with a device attestation bound to this impression", ok),
        (tv(11, 1, &y, None).claim().to_bytes(), "viewer 11, ad-y: the same, with no attestation", no(pr::Reason::AttestationAbsent)),
        (lifted.to_bytes(), "viewer 12, ad-x: carrying viewer 11's token, whose challenge is another impression's", no(pr::Reason::AttestationInvalid)),
        (broken.to_bytes(), "viewer 12, ad-y: a token for its own challenge with one bit of its authenticator flipped", no(pr::Reason::AttestationInvalid)),
        (
            Draft { token: Some(3), ..d(13, 0, &x, vec![span(0, 15_000, 10_000, 0)]) }.claim().to_bytes(),
            "viewer 13, ad-x: in view throughout and muted, where the policy requires the run to be audible",
            no(pr::Reason::ClaimNotViewable),
        ),
        (
            Draft { token: Some(4), ..d(13, 1, &y, vec![span(0, 15_000, 9_000, 1)]) }.claim().to_bytes(),
            "viewer 13, ad-y: 90% of the pixels measured in view, where the cross-media rule requires all of them",
            no(pr::Reason::ClaimNotViewable),
        ),
        (tv(14, 0, &x, Some(5)).claim().to_bytes(), "viewer 14, ad-x: full screen, audible, attested", ok),
        (tv(14, 1, &y, Some(6)).claim().to_bytes(), "viewer 14, ad-y: full screen, audible, attested", ok),
    ];
    out.push(Vector {
        name: "cross_media_attested",
        about: "a live break, an out and its in 30 s later, under a policy that requires a claim, a device attestation of RFC 9578's publicly verifiable type from one issuer, the cross-media rule and an audible run: four viewers on television outputs and screens",
        room: r.clone(),
        policy: p.to_bytes(),
        receipts: receipts.iter().map(SsaiBreakReceipt::to_json).collect(),
        pods: vec![pod(); 4],
        claims,
        intent: Intent::Accept,
    });

    // The ceiling holds the presented spend.
    let w = creative_w();
    let costly = vec![ad("ad-w", &w, 600_000)];
    let one_minor = Spec { budget_minor: 1, ..SPEC };
    for (name, about, both, intent) in [
        (
            "ceiling_counts_presented",
            "a decider grant of 1 minor unit and two receipts each billing 600 000 µ-units: together they pass the ceiling, but only the first has a claim, so the presented spend, 600 000, is within it",
            false,
            Intent::Accept,
        ),
        (
            "ceiling_exceeded_by_presented",
            "the same two receipts, each with a claim that verifies: the presented spend of the second takes the room past the ceiling",
            true,
            Intent::Refuse("receipt", Reason::FundsExceeded),
        ),
    ] {
        let (r, p, b) = mid_roll(one_minor, requirement(require::CLAIM, rule::VIDEO, false));
        let mk = |pl: u8| {
            let mut s = Seal::new(b, 30_000, 0, asset_format::HLS, costly.clone(), &[5]);
            s.session_bytes = Some(pr::session_of(&r.id, &player_pk(pl)));
            s.funds = 1_000_000;
            seal(&r, &p, &s)
        };
        let mut claims = vec![(Draft::new(&r, b, 21, 0, w.clone(), whole(&w)).claim().to_bytes(), "viewer 21, ad-w: in view and audible from start to end", Ok(()))];
        if both {
            claims.push((Draft::new(&r, b, 22, 0, w.clone(), whole(&w)).claim().to_bytes(), "viewer 22, ad-w: in view and audible from start to end", Ok(())));
        }
        out.push(Vector { name, about, room: r.clone(), policy: p.to_bytes(), receipts: vec![mk(21).to_json(), mk(22).to_json()], pods: vec![costly.clone(); 2], claims, intent });
    }

    // A policy whose presentation fields break §8.
    let (r, _, _) = mid_roll(SPEC, requirement(require::CLAIM, rule::VIDEO, false));
    let bad = policy(&r, |p| p.presentation = Some(Requirement { require: require::ATTEST, rule: rule::NONE, token_type: token_type::BLIND_RSA, issuer: ISSUER_NAME.to_vec() }));
    out.push(Vector {
        name: "policy_attestation_without_claim",
        about: "a policy of version 2 requiring an attestation and no claim to carry it",
        room: r,
        policy: bad.to_bytes(),
        receipts: vec![],
        pods: vec![],
        claims: vec![],
        intent: Intent::Refuse("policy", Reason::AdPolicyMalformed),
    });
    out
}

fn build() -> Vec<(Vector, Run)> {
    let mut out = Vec::new();
    for v in vectors() {
        let run = v.run();
        let ok = match (v.intent, run.outcome()) {
            (Intent::Accept, Outcome::Accept) => true,
            (Intent::Refuse(place, reason), Outcome::Refuse { at, reason: got }) => {
                *got == reason
                    && place
                        == match at {
                            At::Room(_) => "room",
                            At::Policy => "policy",
                            At::Record { .. } => "record",
                            At::Receipt(_) => "receipt",
                        }
            }
            _ => false,
        };
        assert!(ok, "vector {} was written to {:?} and gives {:?}", v.name, v.intent, run.outcome());
        if matches!(run.outcome(), Outcome::Accept) {
            for (k, ((_, about, want), got)) in v.claims.iter().zip(&run.claims).enumerate() {
                assert_eq!(&got.verdict, want, "vector {} claim {k} ({about})", v.name);
            }
        }
        out.push((v, run));
    }
    out
}

// ---- JSON ----

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

fn verdict(v: &Result<(), pr::Reason>) -> Value {
    match v {
        Ok(()) => json!("presented"),
        Err(r) => json!(r.code()),
    }
}

fn claim_json(f: &ClaimFacts) -> Value {
    json!({
        "digest": f.digest.map(|d| hex(&d)),
        "verdict": verdict(&f.verdict),
        "receipt": f.receipt,
        "slot": f.slot,
        "quartile": f.quartile,
        "run_ms": f.run_ms,
        "viewable_ms": f.viewable_ms,
    })
}

/// What a vector states of its run: live-ads §9.4, and ad-presentation §9.4.
fn run_json(name: &str, run: &Run) -> Map<String, Value> {
    let mut o = base::run_json(name, run);
    if !matches!(run.outcome(), Outcome::Accept) {
        return o;
    }
    if let Some(Value::Array(facts)) = o.get_mut("receipt_facts") {
        for (f, r) in facts.iter_mut().zip(&run.receipts) {
            let m = f.as_object_mut().expect("an object");
            m.insert("presented_spend_micros".into(), json!(r.presented_spend_micros));
            m.insert(
                "impressions".into(),
                Value::Array(r.impressions.iter().map(|i| json!({"slot": i.slot, "claim": i.claim, "verdict": verdict(&i.verdict)})).collect()),
            );
        }
    }
    o.insert("claim_facts".into(), Value::Array(run.claims.iter().map(claim_json).collect()));
    o
}

fn creative_json(c: &Creative) -> Value {
    json!({"objects": c.objects.iter().map(|o| json!({"content_hash": hex(&o.content_hash), "duration_ms": o.duration_ms})).collect::<Vec<_>>()})
}

fn issuer_json(i: &Issuer) -> Value {
    json!({"name": String::from_utf8(i.name.clone()).expect("an ASCII name"), "token_type": i.token_type, "keys": i.keys.iter().map(|k| hex(k)).collect::<Vec<_>>()})
}

fn vector_json(v: &Vector, run: &Run) -> Value {
    let chains: Vec<Vec<String>> = v.room.chains().iter().map(|c| base::hexes(c)).collect();
    let mut o = Map::new();
    o.insert("name".into(), json!(v.name));
    o.insert("about".into(), json!(v.about));
    o.insert("descriptor".into(), json!(hex(&v.room.descriptor)));
    o.insert("grants".into(), json!(base::hexes(&v.room.grants)));
    o.insert("key_log".into(), json!(v.room.head));
    o.insert("chains".into(), json!(chains));
    o.insert("policy".into(), json!(hex(&v.policy)));
    o.insert("pt0_ms".into(), json!(0));
    o.insert("receipts".into(), Value::Array(v.receipts.clone()));
    o.insert("pods".into(), Value::Array(v.pods.iter().map(|p| base::pod_json(p)).collect()));
    o.insert("claims".into(), json!(v.claims.iter().map(|c| hex(&c.0)).collect::<Vec<_>>()));
    o.insert("claim_about".into(), json!(v.claims.iter().map(|c| c.1).collect::<Vec<_>>()));
    o.extend(run_json(v.name, run));
    Value::Object(o)
}

/// Write the corpus: `vectors.json` and each accepted room's HLS tags.
pub fn generate() -> Vec<(String, String)> {
    // The RFC's own token verifies under its key; the corpus's tokens are signed with it.
    let (pki, challenge, token) = (unhex(RFC9578_PKI), unhex(RFC9578_CHALLENGE), unhex(RFC9578_TOKEN));
    let t = pr::read_token(&token).expect("the RFC's token reads");
    assert!(t.challenge_digest == sha(&[&challenge]) && t.key_id == sha(&[&pki]) && pr::pss_verify(&pki, t.input, t.authenticator), "RFC 9578 A.2's first test vector");
    let rooms = build();
    let mut seen: BTreeSet<&str> = BTreeSet::new();
    for (_, run) in &rooms {
        seen.extend(run.claims.iter().filter_map(|c| c.verdict.err().map(pr::Reason::code)));
        seen.extend(run.receipts.iter().flat_map(|r| r.impressions.iter().filter_map(|i| i.verdict.err().map(pr::Reason::code))));
    }
    for r in pr::Reason::ALL {
        assert!(seen.contains(r.code()), "no claim or impression gives {}", r.code());
    }
    let mut files = Vec::new();
    for (v, run) in &rooms {
        if matches!(run.outcome(), Outcome::Accept) {
            files.push((base::hls_file(v.name), base::hls_body(run)));
        }
    }
    let players: Map<String, Value> = [1u8, 2, 3, 4, 5, 6, 7, 11, 12, 13, 14, 21, 22].iter().map(|&i| (i.to_string(), json!(hex(&player_pk(i))))).collect();
    let doc = json!({
        "about": "extensions/ad-presentation.md §12: rooms of live-ads breaks under ad policies that require presentation claims, each viewer's break-form wai.video.ssai receipt, the claims its player signed, the creatives and the attestation issuer the verifier holds, each claim's verdict and each receipt's presented spend against the decider grant's ceiling",
        "rfc9578_a2_vector1": {"pkI": RFC9578_PKI, "token_challenge": RFC9578_CHALLENGE, "token": RFC9578_TOKEN},
        "creatives": creatives().iter().map(creative_json).collect::<Vec<_>>(),
        "issuers": issuers().iter().map(issuer_json).collect::<Vec<_>>(),
        "players": players,
        "vectors": rooms.iter().map(|(v, run)| vector_json(v, run)).collect::<Vec<_>>(),
    });
    let mut body = serde_json::to_string_pretty(&doc).expect("JSON");
    body.push('\n');
    files.push(("vectors.json".into(), body));
    files.sort();
    files
}

fn creative_of(v: &Value) -> Option<Creative> {
    Some(Creative {
        objects: v["objects"]
            .as_array()?
            .iter()
            .map(|o| Some(CreativeObject { content_hash: base::unhex(o["content_hash"].as_str()?)?.try_into().ok()?, duration_ms: u32::try_from(o["duration_ms"].as_u64()?).ok()? }))
            .collect::<Option<_>>()?,
    })
}

fn issuer_of(v: &Value) -> Option<Issuer> {
    Some(Issuer {
        name: v["name"].as_str()?.as_bytes().to_vec(),
        token_type: u16::try_from(v["token_type"].as_u64()?).ok()?,
        keys: v["keys"].as_array()?.iter().map(|k| base::unhex(k.as_str()?)).collect::<Option<_>>()?,
    })
}

/// Check a corpus's files against this module: every room gets the outcome and everything
/// else it states, and every tag file is the run's. Returns the failures.
pub fn verify(files: &BTreeMap<String, String>) -> Vec<String> {
    let mut fails = Vec::new();
    let Some(doc) = files.get("vectors.json").and_then(|s| serde_json::from_str::<Value>(s).ok()) else {
        return vec!["vectors.json does not read".into()];
    };
    let held = (|| Some((doc["creatives"].as_array()?.iter().map(creative_of).collect::<Option<Vec<_>>>()?, doc["issuers"].as_array()?.iter().map(issuer_of).collect::<Option<Vec<_>>>()?)))();
    let Some((creatives, issuers)) = held else {
        return vec!["the creatives or issuers do not read".into()];
    };
    for v in doc["vectors"].as_array().into_iter().flatten() {
        let name = v["name"].as_str().unwrap_or("?").to_string();
        let parsed = (|| {
            let chains: Vec<Vec<Vec<u8>>> = v["chains"].as_array()?.iter().map(base::hex_list).collect::<Option<_>>()?;
            let pods: Vec<Vec<AdCandidate>> = v["pods"].as_array()?.iter().map(base::pod_of).collect::<Option<_>>()?;
            Some((
                base::unhex(v["descriptor"].as_str()?)?,
                base::hex_list(&v["grants"])?,
                v["key_log"].as_str().map(String::from),
                chains,
                base::unhex(v["policy"].as_str()?)?,
                v["pt0_ms"].as_u64()?,
                v["receipts"].as_array()?.clone(),
                pods,
                base::hex_list(&v["claims"])?,
            ))
        })();
        let Some((descriptor, grants, head, chains, policy, pt0_ms, receipts, pods, claims)) = parsed else {
            fails.push(format!("vector {name}: its inputs do not read"));
            continue;
        };
        let run = super::verify(&Input {
            descriptor: &descriptor,
            grants: &grants,
            head: head.as_deref(),
            chains: &chains,
            policy: &policy,
            pt0_ms,
            receipts: &receipts,
            pods: &pods,
            claims: &claims,
            creatives: &creatives,
            issuers: &issuers,
            supply_files: &[],
        });
        for (k, w) in &run_json(&name, &run) {
            if v.get(k) != Some(w) {
                fails.push(format!("vector {name}: {k} is {w}, the corpus states {}", v.get(k).unwrap_or(&Value::Null)));
            }
        }
        if matches!(run.outcome(), Outcome::Accept) && files.get(&base::hls_file(&name)) != Some(&base::hls_body(&run)) {
            fails.push(format!("vector {name}: {} differs from the tags the room writes", base::hls_file(&name)));
        }
    }
    fails
}
