//! The revenue-share corpus (`live-conformance/share/`, extensions/revenue-share.md §12).
//!
//! Its evidence comes from the ledger corpus (`live-conformance/ledger/`), each source taken whole
//! as that corpus states it: a room's break-form receipts under a policy that requires claims and
//! under one that does not, and three streams stitched by others. The tariffs, the invoices, the
//! channel's runs, the contracts and the statements are written here from fixed seeds. [`generate`]
//! reads the ledger corpus from a `wai/` directory; [`verify`] checks every vector against the
//! outcome it states.

use std::collections::BTreeMap;
use std::path::Path;

use ed25519_dalek::{Signer, SigningKey};
use serde_json::{Map, Value, json};

use super::*;
use crate::energy_class::EnergyProvenance;
use crate::quantum_ops::GrantRef;
use crate::receipt_ledger::{Invoice, Tariff};
use crate::wai_video_channel::ScheduleEntry;

/// 2026-10-01, 2026-11-01, 2026-12-01, 2027-01-01, 2027-03-01, 2027-04-01 (00:00:00Z).
const OCT: u64 = 1_790_812_800_000_000;
const NOV: u64 = 1_793_491_200_000_000;
const DEC: u64 = 1_796_083_200_000_000;
const JAN: u64 = 1_798_761_600_000_000;
const MAR: u64 = 1_803_859_200_000_000;
const APR: u64 = 1_806_537_600_000_000;
/// 2026-10-16T00:00:00Z: the ledger corpus's rooms' `t0`, and the first invoice's day.
const DAY16: u64 = 1_792_108_800_000_000;
const DAY_US: u64 = 86_400_000_000;
/// 2026-10-20 and 2026-10-25, whose noon the stitched streams' markers name.
const DAY20: u64 = DAY16 + 4 * DAY_US;
const DAY25: u64 = DAY16 + 9 * DAY_US;
/// Per presented and per billable impression, in millionths of a minor unit.
const PRICES: [u64; 2] = [1501, 903];
const PRICES_2: [u64; 2] = [2000, 1000];
/// The ledger corpus's sources this corpus shares.
const SOURCES: [&str; 5] = ["ads_live_break", "ads_presented", "stitched_hls", "stitched_form1", "stitched_squeezeback"];
const CHANNEL: &str = "fast-7";

/// A key of the corpus, from its fixed seed: `operator`, `owner-a`, `owner-b`, `channel`, `buyer`,
/// `buyer-2`, `stranger`. Test keys, never a deployment's.
pub fn key(label: &str) -> SigningKey {
    SigningKey::from_bytes(&sha256(&[b"wai:share-corpus\x01", label.as_bytes()]))
}

fn pk(label: &str) -> [u8; 32] {
    key(label).verifying_key().to_bytes()
}

fn read_json(wai: &Path, rel: &str) -> Result<Value, String> {
    let t = std::fs::read_to_string(wai.join(rel)).map_err(|e| format!("{rel}: {e}"))?;
    serde_json::from_str(&t).map_err(|e| format!("{rel}: {e}"))
}

fn named<'a>(list: &'a Value, name: &str) -> Result<&'a Value, String> {
    list.as_array().and_then(|l| l.iter().find(|v| v["name"] == name)).ok_or_else(|| format!("no ledger vector {name}"))
}

/// The ad deciders and stitchers a ledger vector's tariff bills (receipt-ledger §4.4, kind 4).
fn ad_scope(ledger_doc: &Value, vector: &str) -> Result<Vec<[u8; 32]>, String> {
    let t = named(&ledger_doc["vectors"], vector)?["tariff"].as_str().and_then(unhex).ok_or("a tariff")?;
    let t = Tariff::parse(&t).map_err(|e| format!("{vector}'s tariff: {e:?}"))?;
    Ok(t.scope.iter().filter(|(k, _)| *k == 4).map(|(_, id)| *id).collect())
}

/// A tariff of the operator's with a buyer, for the contract's period.
fn tariff(buyer: &str, currency: [u8; 3], ads: [u64; 2], scope: &[[u8; 32]]) -> Tariff {
    let mut scope: Vec<(u8, [u8; 32])> = scope.iter().map(|id| (4, *id)).collect();
    scope.sort();
    scope.dedup();
    Tariff {
        version: ledger::VERSION,
        seller: pk("operator"),
        buyer: pk(buyer),
        start: OCT,
        end: JAN,
        currency,
        room: [0; ledger::ROOM_PRICES],
        delivery: [0; 4],
        package: [0; 3],
        ads,
        job: vec![],
        scope,
        seller_sig: [0; 64],
        buyer_sig: [0; 64],
    }
    .sign(&key("operator"), &key(buyer))
}

/// A ledger document as the corpus holds it: its sources by name.
fn held(t: &Tariff, i: &Invoice, previous: Option<&Invoice>, sources: &[&str]) -> Value {
    json!({"tariff": hex(&t.to_bytes()), "invoice": hex(&i.to_bytes()), "previous": previous.map(|p| hex(&p.to_bytes())), "sources": sources})
}

fn invoice(pool: &BTreeMap<String, Value>, t: &Tariff, previous: Option<&Invoice>, start: u64, end: u64, sources: &[&str]) -> Result<Invoice, String> {
    let s: Vec<Value> = sources.iter().map(|n| pool[*n].clone()).collect();
    ledger::make_invoice(&key("operator"), t, previous, start, end, &s).map_err(|e| format!("an invoice of {sources:?}: {e:?}"))
}

fn entry(asset: &str, start_ms: u64, duration_ms: u64, kind: EntryKind) -> ScheduleEntry {
    ScheduleEntry { asset_id: asset.into(), start_ms, duration_ms, kind }
}

/// A run of `channel`, signed by the channel signer.
fn run(channel: &str, schedule: Vec<ScheduleEntry>, window: (u64, u64), joules: u64, class: Option<EnergyProvenance>, parent: Option<[u8; 32]>) -> ChannelReceipt {
    ChannelReceipt::seal(&key("channel"), "did:key:channel-signer", channel, schedule, window.0, window.1, joules, class, GrantRef::unbounded("video.channel"), parent)
}

/// The channel's two windows: the 16th's first five minutes, and the 25th's noon half hour.
fn windows() -> (ChannelReceipt, ChannelReceipt, ChannelReceipt) {
    let a = DAY16 / 1000;
    let r1 = run(
        CHANNEL,
        vec![
            entry("a/ep-101", a, 90_000, EntryKind::Program),
            entry("slate-15", a + 90_000, 15_000, EntryKind::Ad),
            entry("b/doc-7", a + 105_000, 195_000, EntryKind::Program),
        ],
        (a, a + 300_000),
        4_000,
        Some(EnergyProvenance::ModelBased),
        None,
    );
    let noon = DAY25 / 1000 + 43_200_000;
    let second = |last: &str| {
        vec![
            entry("a/ep-102", noon - 120_000, 250_000, EntryKind::Program),
            entry("mid-1", noon + 130_000, 30_000, EntryKind::Ad),
            entry(last, noon + 160_000, 1_640_000, EntryKind::Program),
        ]
    };
    let r2 = run(CHANNEL, second("b/doc-8"), (noon, noon + 1_800_000), 0, None, Some(r1.receipt_hash()));
    let r2_unmapped = run(CHANNEL, second("c/film-9"), (noon, noon + 1_800_000), 0, None, Some(r1.receipt_hash()));
    (r1, r2, r2_unmapped)
}

/// The contract's terms: two owners, a split each and a between split, under `rule`.
fn terms_of(room: [u8; 32], stitchers: &[[u8; 32]], rule_: u8, period: (u64, u64)) -> Contract {
    let mut owners = vec![pk("owner-a"), pk("owner-b")];
    owners.sort();
    let ia = owners.iter().position(|k| *k == pk("owner-a")).expect("listed") as u8;
    let ib = 1 - ia;
    let mut carriers: Vec<(u8, [u8; 32])> = vec![(carrier::ROOM, room)];
    carriers.extend(stitchers.iter().map(|s| (carrier::STITCHER, *s)));
    carriers.sort();
    let mut halves = vec![(ia, 500_000), (ib, 500_000)];
    halves.sort();
    Contract {
        operator: pk("operator"),
        start: period.0,
        end: period.1,
        currency: *b"EUR",
        channel: CHANNEL.as_bytes().to_vec(),
        channel_signer: pk("channel"),
        rule: rule_,
        between: 2,
        carriers,
        owners,
        splits: vec![
            Split { operator_ppm: 666_667, shares: vec![(ia, 333_333)] },
            Split { operator_ppm: 400_000, shares: vec![(ib, 600_000)] },
            Split { operator_ppm: 0, shares: halves },
        ],
        assets: ["a/ep-101", "a/ep-102", "a/ep-103", "b/doc-7", "b/doc-8"].iter().map(|a| (a.as_bytes().to_vec(), if a.starts_with('a') { 0 } else { 1 })).collect(),
        operator_sig: [0; 64],
        countersigs: Vec::new(),
    }
}

fn in_force(c: Contract) -> Contract {
    c.offer(&key("operator")).countersign(&key("owner-a")).expect("an owner").countersign(&key("owner-b")).expect("an owner")
}

/// A statement changed and signed again by the operator.
fn resealed(s: &Statement, f: impl FnOnce(&mut Statement)) -> Vec<u8> {
    let mut s = s.clone();
    f(&mut s);
    s.sign(&key("operator")).to_bytes()
}

/// One vector: what it names, by name.
struct Vector {
    name: &'static str,
    about: &'static str,
    contract: &'static str,
    contracts: Vec<&'static str>,
    statement: Vec<u8>,
    runs: Option<Vec<&'static str>>,
    ledgers: Vec<&'static str>,
}

/// The pools the vectors name.
struct Pools {
    sources: BTreeMap<String, Value>,
    ledgers: BTreeMap<String, Value>,
    runs: BTreeMap<String, Value>,
    contracts: BTreeMap<String, Value>,
}

impl Pools {
    fn ledger(&self, name: &str) -> Value {
        let l = &self.ledgers[name];
        let sources: Vec<Value> = l["sources"].as_array().expect("names").iter().map(|n| self.sources[n.as_str().expect("a name")].clone()).collect();
        json!({"tariff": l["tariff"], "invoice": l["invoice"], "previous": l["previous"], "sources": sources})
    }

    /// The verification document of named parts, with a statement or without one.
    fn document(&self, contract: &str, contracts: &[&str], statement: Option<&[u8]>, runs: Option<&[&str]>, ledgers: &[&str]) -> Value {
        let mut d = Map::new();
        d.insert("contract".into(), self.contracts[contract].clone());
        d.insert("contracts".into(), Value::Array(contracts.iter().map(|n| self.contracts[*n].clone()).collect()));
        if let Some(s) = statement {
            d.insert("statement".into(), json!(hex(s)));
        }
        d.insert("runs".into(), runs.map_or(Value::Null, |r| Value::Array(r.iter().map(|n| self.runs[*n].clone()).collect())));
        d.insert("ledgers".into(), Value::Array(ledgers.iter().map(|n| self.ledger(n)).collect()));
        Value::Object(d)
    }
}

/// Write the corpus, from fixed seeds and the ledger corpus under `wai`.
pub fn generate(wai: &Path) -> Result<String, String> {
    let ledger_doc = read_json(wai, "live-conformance/ledger/vectors.json")?;
    let mut sources = BTreeMap::new();
    for n in SOURCES {
        sources.insert(n.to_string(), ledger_doc["sources"].get(n).cloned().ok_or_else(|| format!("the ledger corpus has no source {n}"))?);
    }
    let mut scope = ad_scope(&ledger_doc, "a_day")?;
    let stitchers = ad_scope(&ledger_doc, "streams_stitched_by_others")?;
    scope.extend(&stitchers);
    let room = match ledger::verify_source(&sources["ads_presented"], &[]).map_err(|e| format!("ads_presented: {e:?}"))? {
        Facts::Ads(a) => a.room,
        _ => return Err("ads_presented is an ads source".into()),
    };

    // The invoices: a chain of two under the first buyer's tariff, and three under a second's.
    let t1 = tariff("buyer", *b"EUR", PRICES, &scope);
    let t2 = tariff("buyer-2", *b"EUR", PRICES_2, &scope);
    let i1 = invoice(&sources, &t1, None, DAY16, DAY16 + DAY_US, &["ads_live_break", "ads_presented"])?;
    let stitched = ["stitched_hls", "stitched_form1", "stitched_squeezeback"];
    let i2 = invoice(&sources, &t1, Some(&i1), DAY25, DAY25 + DAY_US, &stitched)?;
    let i_dup = invoice(&sources, &t2, None, DAY16, DAY16 + DAY_US, &["ads_live_break"])?;
    let i_early = invoice(&sources, &t2, None, DAY20, DAY20 + DAY_US, &stitched)?;
    let mut ledgers = BTreeMap::new();
    ledgers.insert("day_16".to_string(), held(&t1, &i1, None, &["ads_live_break", "ads_presented"]));
    ledgers.insert("day_25".to_string(), held(&t1, &i2, Some(&i1), &stitched));
    ledgers.insert("day_16_rebilled".to_string(), held(&t2, &i_dup, None, &["ads_live_break"]));
    ledgers.insert("day_20".to_string(), held(&t2, &i_early, None, &stitched));
    let mut flipped = ledgers["day_16"].clone();
    let mut ib = i1.to_bytes();
    let last = ib.len() - 1;
    ib[last] ^= 0x01;
    flipped["invoice"] = json!(hex(&ib));
    ledgers.insert("day_16_invoice_flipped".to_string(), flipped);
    let foreign = named(&ledger_doc["vectors"], "streams_stitched_by_others")?;
    ledgers.insert("another_seller".to_string(), json!({"tariff": foreign["tariff"], "invoice": foreign["invoice"], "previous": foreign["previous"], "sources": foreign["sources"]}));

    // The runs.
    let (r1, r2, r2_unmapped) = windows();
    let mut runs = BTreeMap::new();
    runs.insert("oct_16".to_string(), r1.to_json());
    runs.insert("oct_25".to_string(), r2.to_json());
    runs.insert("oct_25_unmapped".to_string(), r2_unmapped.to_json());
    let mut sigless = r1.to_json();
    sigless.as_object_mut().expect("an object").remove("sig");
    runs.insert("oct_16_no_sig".to_string(), sigless);
    let mut tampered = r1.clone();
    tampered.aired = vec![0];
    tampered.sig = key("channel").sign(&tampered.payload()).to_bytes();
    runs.insert("oct_16_aired_misstated".to_string(), tampered.to_json());
    let other = ChannelReceipt::seal(&key("channel"), "did:key:channel-signer", "fast-8", r1.schedule.clone(), r1.window_start_ms, r1.window_end_ms, 0, None, GrantRef::unbounded("video.channel"), None);
    runs.insert("fast_8".to_string(), other.to_json());
    let late = run(CHANNEL, r1.schedule.clone(), (r1.window_start_ms + 240_000, r1.window_start_ms + 600_000), 0, None, None);
    runs.insert("oct_16_overlapping".to_string(), late.to_json());

    // The contracts.
    let c = in_force(terms_of(room, &stitchers, rule::PRECEDING, (OCT, JAN)));
    let c_following = in_force(terms_of(room, &stitchers, rule::FOLLOWING, (OCT, JAN)));
    let c_between = in_force(terms_of(room, &stitchers, rule::BETWEEN, (OCT, JAN)));
    let c_next = in_force(terms_of(room, &stitchers, rule::PRECEDING, (JAN, APR)));
    let c_overlap = in_force(terms_of(room, &stitchers, rule::PRECEDING, (DEC, MAR)));
    let ib_owner = c.owners.iter().position(|k| *k == pk("owner-b")).expect("listed");
    let ia_owner = 1 - ib_owner;
    let c_offered = terms_of(room, &stitchers, rule::PRECEDING, (OCT, JAN)).offer(&key("operator")).countersign(&key("owner-a")).expect("an owner");
    let mut c_owner_sig = c.clone();
    c_owner_sig.countersigs[ia_owner][0] ^= 0x01;
    let mut unwhole = terms_of(room, &stitchers, rule::PRECEDING, (OCT, JAN));
    unwhole.splits[0].operator_ppm -= 1;
    let unwhole = in_force(unwhole);
    let mut unordered = terms_of(room, &stitchers, rule::PRECEDING, (OCT, JAN));
    unordered.assets.reverse();
    let unordered = in_force(unordered);
    let mut small = c.clone();
    small.operator = crate::sig::IDENTITY;
    let cb = c.to_bytes();
    let mut contracts = BTreeMap::new();
    let mut put = |n: &str, b: Vec<u8>| {
        contracts.insert(n.to_string(), json!(hex(&b)));
    };
    put("preceding", cb.clone());
    put("following", c_following.to_bytes());
    put("between", c_between.to_bytes());
    put("next_quarter", c_next.to_bytes());
    put("overlapping", c_overlap.to_bytes());
    put("offered", c_offered.to_bytes());
    put("owner_signature_flipped", c_owner_sig.to_bytes());
    put("split_short", unwhole.to_bytes());
    put("assets_unordered", unordered.to_bytes());
    put("operator_key_small_order", small.to_bytes());
    put("truncated", cb[..3].to_vec());
    put("magic", [b"WRC2".as_slice(), &cb[4..]].concat());
    put("version_2", [&cb[..4], &[2u8][..], &cb[5..]].concat());
    put("trailing_byte", [cb.as_slice(), &[0u8][..]].concat());
    let mut flip = cb.clone();
    flip[c.body().len()] ^= 0x01;
    put("operator_signature_flipped", flip);

    let pools = Pools { sources, ledgers, runs, contracts };

    // The accepted statements.
    let main_runs = ["oct_16", "oct_25"];
    let main_ledgers = ["day_16", "day_25"];
    let statement = |contract: &str, party: [u8; 32], ledgers_: &[&str]| -> Result<Statement, String> {
        let inputs = pools.document(contract, &["next_quarter", contract], None, Some(&main_runs[..]), ledgers_);
        super::create(&inputs, &key("operator"), party, OCT, NOV).map(|(s, _)| s).map_err(|e| format!("the statement of {contract}: {e:?}"))
    };
    let s_a = statement("preceding", pk("owner-a"), &main_ledgers)?;
    let s_b = statement("preceding", pk("owner-b"), &main_ledgers)?;
    let s_o = statement("preceding", pk("operator"), &main_ledgers)?;
    let s_b_following = statement("following", pk("owner-b"), &main_ledgers)?;
    let s_a_between = statement("between", pk("owner-a"), &main_ledgers)?;
    let s_a_nothing = statement("preceding", pk("owner-a"), &[])?;
    let sa = s_a.to_bytes();

    let main = |name: &'static str, about: &'static str, statement: Vec<u8>| Vector {
        name,
        about,
        contract: "preceding",
        contracts: vec!["next_quarter", "preceding"],
        statement,
        runs: Some(main_runs.to_vec()),
        ledgers: main_ledgers.to_vec(),
    };
    let with_contract = |name: &'static str, about: &'static str, contract: &'static str| Vector { contract, contracts: vec![], ..main(name, about, sa.clone()) };
    let with_runs = |name: &'static str, about: &'static str, runs: Vec<&'static str>| Vector { runs: Some(runs), ..main(name, about, sa.clone()) };
    let with_ledgers = |name: &'static str, about: &'static str, ledgers: Vec<&'static str>, statement: Vec<u8>| Vector { ledgers, ..main(name, about, statement) };
    let line_b = s_b.lines[0];

    let vectors = vec![
        // Accepted.
        main("owner_a_october", "owner A's October: the beacon break inside its programme on the 16th, its programme's stitched ads on the 25th and the ad slot after it by the rule preceding, and its half of the unplaced presentations", sa.clone()),
        main("owner_b_october", "owner B's October: the presented break inside its programme on the 16th, the stitched ad inside its programme on the 25th, and its half of the unplaced presentations", s_b.to_bytes()),
        main("operator_october", "the operator's October: its part of each owner's split, and no line of the between split, in which it holds no share", s_o.to_bytes()),
        Vector { contract: "following", contracts: vec![], ..main("owner_b_following", "owner B's October under the rule following: the ad slot on the 25th is its programme's", s_b_following.to_bytes()) },
        Vector { contract: "between", contracts: vec![], ..main("owner_a_between", "owner A's October under the rule between: the ad slot on the 25th goes to the between split", s_a_between.to_bytes()) },
        with_ledgers("owner_a_nothing_billed", "a statement of a period with no invoice: no line, a total of zero", vec![], s_a_nothing.to_bytes()),
        // The document and the contract.
        Vector { runs: None, ..main("document_malformed", "a verification document whose runs are not a list", sa.clone()) },
        with_contract("contract_truncated", "a contract of three bytes", "truncated"),
        with_contract("contract_magic", "a contract whose magic is WRC2", "magic"),
        with_contract("contract_version_2", "a contract of version 2", "version_2"),
        with_contract("contract_trailing_byte", "a contract with a byte after its last countersignature", "trailing_byte"),
        with_contract("contract_assets_unordered", "a contract whose assets are not in ascending order", "assets_unordered"),
        with_contract("contract_split_short", "a contract one of whose splits sums to one millionth short of the whole", "split_short"),
        with_contract("contract_operator_key_small_order", "a contract whose operator key is of small order", "operator_key_small_order"),
        with_contract("contract_operator_signature", "a contract whose operator signature has a byte flipped", "operator_signature_flipped"),
        with_contract("contract_offered", "a contract owner B has not countersigned", "offered"),
        with_contract("contract_owner_signature", "a contract whose owner A countersignature has a byte flipped", "owner_signature_flipped"),
        Vector { contracts: vec!["next_quarter", "overlapping"], ..main("contract_overlap", "another contract of the channel whose period overlaps the contract's last month", sa.clone()) },
        Vector { contracts: vec!["offered"], ..main("other_contract_offered", "another contract given that is not in force", sa.clone()) },
        // The statement.
        main("statement_truncated", "a statement of ten bytes", sa[..10].to_vec()),
        main("statement_lines_unordered", "a statement whose lines are not in ascending order", resealed(&s_a, |s| s.lines.reverse())),
        main("statement_of_another_contract", "a statement naming the contract under the rule following", resealed(&s_a, |s| s.contract = c_following.digest())),
        main("statement_of_a_stranger", "a statement for a key the contract does not name", resealed(&s_a, |s| s.party = pk("stranger"))),
        main("statement_past_the_contract", "a statement whose period ends a day after the contract's", resealed(&s_a, |s| s.end = JAN + DAY_US)),
        main("statement_signature", "a statement whose signature has a byte flipped", {
            let mut b = sa.clone();
            let n = b.len();
            b[n - 40] ^= 0x01;
            b
        }),
        // The runs.
        with_runs("run_without_signature", "a run whose carriage has no sig", vec!["oct_16_no_sig", "oct_25"]),
        with_runs("run_aired_misstated", "a run whose aired set the window does not give, signed again", vec!["oct_16_aired_misstated", "oct_25"]),
        with_runs("run_of_another_channel", "a run of another channel, by the same signer", vec!["oct_16", "fast_8"]),
        with_runs("runs_overlapping", "a third run whose window overlaps the first's", vec!["oct_16", "oct_25", "oct_16_overlapping"]),
        // The ledger documents.
        with_ledgers("ledger_refused", "a ledger document whose invoice signature has a byte flipped, refused by the receipt ledger at its place", vec!["day_16_invoice_flipped", "day_25"], sa.clone()),
        with_ledgers("invoice_of_another_seller", "a ledger document of the ledger corpus, whose seller is not the operator", vec!["day_16", "day_25", "another_seller"], sa.clone()),
        main("invoice_outside_the_period", "a statement of the 1st to the 20th, given the invoice of the 25th", resealed(&s_a, |s| s.end = DAY20)),
        with_ledgers("impression_billed_twice", "a second buyer's invoice billing the beacon break of the 16th again", vec!["day_16", "day_25", "day_16_rebilled"], sa.clone()),
        // The impressions.
        with_ledgers("impression_outside_the_period", "a statement of the 20th whose invoice of that day bills the stitched presentations of the 25th", vec!["day_20"], resealed(&s_a, |s| (s.start, s.end) = (DAY20, DAY20 + DAY_US))),
        with_runs("impression_outside_every_window", "the stitched presentations of the 25th with only the run of the 16th", vec!["oct_16"]),
        with_runs("asset_unmapped", "a run of the 25th whose last programme the contract does not map", vec!["oct_16", "oct_25_unmapped"]),
        // The figures.
        main("line_of_another_split", "owner A's statement with a line of owner B's split", resealed(&s_a, |s| {
            s.lines.insert(2, Line { ppm: 1, amount: 0, ..line_b });
        })),
        main("line_impressions_raised", "a line stating one impression more than its split and class give", resealed(&s_a, |s| s.lines[0].impressions += 1)),
        main("line_citations_changed", "a line whose citation digest has a byte changed", resealed(&s_a, |s| s.lines[0].cites[0] ^= 0x01)),
        main("line_amount_raised", "a line whose amount is one more than its part, and the total with it", resealed(&s_a, |s| {
            s.lines[0].amount += 1;
            s.total += 1;
        })),
        main("line_missing", "a statement that leaves its last line out, and its amount from the total", resealed(&s_a, |s| {
            let l = s.lines.pop().expect("a line");
            s.total -= l.amount;
        })),
        main("runs_listed_short", "a statement listing one of the two runs it is given", resealed(&s_a, |s| {
            s.runs.retain(|r| *r != r2.receipt_hash());
        })),
    ];

    let mut out = Vec::with_capacity(vectors.len());
    for v in &vectors {
        let doc = pools.document(v.contract, &v.contracts, Some(&v.statement), v.runs.as_deref(), &v.ledgers);
        let outcome = outcome_json(&super::verify(&doc));
        out.push(json!({
            "name": v.name, "about": v.about, "contract": v.contract, "contracts": v.contracts, "statement": hex(&v.statement),
            "runs": v.runs, "ledgers": v.ledgers, "outcome": outcome,
        }));
    }
    let keys = json!({
        "operator": hex(&pk("operator")), "owner_a": hex(&pk("owner-a")), "owner_b": hex(&pk("owner-b")), "channel_signer": hex(&pk("channel")),
        "buyer": hex(&pk("buyer")), "buyer_2": hex(&pk("buyer-2")), "stranger": hex(&pk("stranger")),
    });
    let to_map = |m: BTreeMap<String, Value>| Value::Object(m.into_iter().collect());
    let doc = json!({
        "corpus": CAPABILITY,
        "extension": "extensions/revenue-share.md",
        "keys": keys,
        "sources": to_map(pools.sources),
        "ledgers": to_map(pools.ledgers),
        "runs": to_map(pools.runs),
        "contracts": to_map(pools.contracts),
        "vectors": out,
    });
    Ok(serde_json::to_string_pretty(&doc).expect("JSON") + "\n")
}

/// A vector of the corpus as a verification document (§8.1), its parts resolved by name.
pub fn document(doc: &Value, v: &Value) -> Option<Value> {
    let ledger = |n: &Value| -> Option<Value> {
        let l = doc["ledgers"].get(n.as_str()?)?;
        let sources: Vec<Value> = l["sources"].as_array()?.iter().map(|s| doc["sources"].get(s.as_str()?).cloned()).collect::<Option<_>>()?;
        Some(json!({"tariff": l["tariff"], "invoice": l["invoice"], "previous": l["previous"], "sources": sources}))
    };
    let mut d = Map::new();
    d.insert("contract".into(), doc["contracts"].get(v["contract"].as_str()?)?.clone());
    d.insert("contracts".into(), Value::Array(v["contracts"].as_array()?.iter().map(|n| doc["contracts"].get(n.as_str()?).cloned()).collect::<Option<_>>()?));
    d.insert("statement".into(), v["statement"].clone());
    let runs = match &v["runs"] {
        Value::Null => Value::Null,
        r => Value::Array(r.as_array()?.iter().map(|n| doc["runs"].get(n.as_str()?).cloned()).collect::<Option<_>>()?),
    };
    d.insert("runs".into(), runs);
    d.insert("ledgers".into(), Value::Array(v["ledgers"].as_array()?.iter().map(ledger).collect::<Option<_>>()?));
    Some(Value::Object(d))
}

/// The failures: every vector whose stated outcome is not the one [`super::verify`] gives.
pub fn verify(text: &str) -> Vec<String> {
    let doc: Value = match serde_json::from_str(text) {
        Ok(d) => d,
        Err(e) => return vec![format!("vectors.json: {e}")],
    };
    let mut fails = Vec::new();
    for v in doc["vectors"].as_array().into_iter().flatten() {
        let name = v["name"].as_str().unwrap_or("?");
        let Some(d) = document(&doc, v) else {
            fails.push(format!("{name}: the vector does not read"));
            continue;
        };
        let got = outcome_json(&super::verify(&d));
        if got != v["outcome"] {
            fails.push(format!("{name}: the outcome is {got}, the corpus states {}", v["outcome"]));
        }
    }
    fails
}
