use super::*;
use crate::claim_receipt::verify_claim_json;

fn key(seed: u8) -> SigningKey {
    SigningKey::from_bytes(&[seed; 32])
}
fn pk(seed: u8) -> [u8; 32] {
    key(seed).verifying_key().to_bytes()
}

const P: &str = "https://publisher.example";

fn rev(root: u8, next: u8, parent: Option<&Revision>, at: &str, keys: Vec<KeyEntry>) -> Revision {
    Revision::seal(
        &key(root),
        Revision {
            origin: P.into(),
            seq: parent.map_or(0, |p| p.seq + 1),
            prev: parent.map(Revision::revision_hash),
            issued_at: at.into(),
            root: [0; 32],
            next_root: commitment(&pk(next)),
            keys,
            revoked_modules: vec![],
            grant_max_s: 3_600,
            status_max_age_s: 600,
            sig: [0; 64],
            hybrid: None,
        },
    )
    .unwrap()
}

/// Every claim the claim corpus accepts names a label: none is confirmed, and each stops
/// at step 1. The claim corpus needs no change for §7 to exist beside it.
#[test]
fn claim_corpus_signers_unconfirmed() {
    let doc: Value = serde_json::from_str(include_str!("../../../claim-conformance/vectors.json")).unwrap();
    let mut accepted = 0;
    for v in doc["vectors"].as_array().unwrap() {
        let Some(wire) = v.get("receipt").and_then(Value::as_str) else { continue };
        let Ok(c) = verify_claim_json(wire) else { continue };
        accepted += 1;
        let got = confirm(&Item::from_claim(&c), &[], &[], None);
        assert_eq!(got, Confirmation::Unconfirmed { step: "1", reason: Reason::SignerIdNotOriginBound }, "{}", v["name"]);
    }
    assert!(accepted > 0, "the claim corpus was not read");
}

#[test]
fn later_than_table() {
    let until = "2026-12-31T23:30:00Z";
    for (t, later) in [
        ("2026-12-31T23:30:00Z", false),
        ("2026-12-31T23:30:00.000Z", false),
        ("2026-12-31T23:30:00.001Z", true),
        ("2027-01-01T00:29:59+01:00", false), // 23:29:59Z
        ("2027-01-01T00:30:01+01:00", true),  // 23:30:01Z
        ("2026-12-31T23:00:00-00:30", false), // 23:30:00Z
        ("2026-12-31T23:59:59-23:59", true),  // next day
        ("2026-12-31T23:29:60Z", false),      // a leap second before the minute
        ("2026-12-31T23:30:00z", false),      // lower-case z is RFC 3339
        ("2026-12-31 23:30:01Z", true),       // not a date-time: later than every until
        ("2026-02-29T00:00:00Z", true),       // 2026 has no 29 February
    ] {
        assert_eq!(later_than(t, until), later, "{t}");
    }
    assert!(!later_than("2028-02-29T00:00:00Z", "2028-03-01T00:00:00Z"));
    assert!(!later_than("2026-09-20T00:00:00.5Z", "2026-09-20T00:00:00.50Z") && !later_than("2026-09-20T00:00:00.50Z", "2026-09-20T00:00:00.5Z"));
    assert!(is_log_time("2026-09-20T00:00:59Z") && !is_log_time("2026-09-20T00:00:60Z"));
    assert!(!is_log_time("2026-09-20t00:00:00Z") && !is_log_time("2026-09-20T00:00:00.0Z") && !is_log_time("2026-09-20T00:00:00+00:00"));
    assert_eq!(log_seconds("1970-01-01T00:00:00Z"), Some(0));
    assert_eq!(log_seconds("2000-03-01T00:00:00Z"), Some(951_868_800));
}

/// The head does not depend on the order revisions arrive in, with orphans, strangers,
/// a thief's revision and the owner's rotation among them.
#[test]
fn head_is_order_independent() {
    let r0 = rev(0x61, 0x62, None, "2026-09-01T00:00:00Z", vec![KeyEntry::active("d1", pk(0x63), &["delivery"])]);
    let r1 = rev(0x61, 0x62, Some(&r0), "2026-09-02T00:00:00Z", r0.keys.clone());
    let mut tk = r1.keys.clone();
    tk.push(KeyEntry::active("x", pk(0x6a), &["delivery"]));
    let thief = rev(0x61, 0x62, Some(&r1), "2026-09-03T00:00:00Z", tk);
    let owner = rev(0x62, 0x68, Some(&r1), "2026-09-04T00:00:00Z", r1.keys.clone());
    let owner2 = rev(0x62, 0x68, Some(&owner), "2026-09-05T00:00:00Z", r1.keys.clone());
    let orphan = rev(0x62, 0x68, Some(&owner2), "2026-09-06T00:00:00Z", r1.keys.clone());
    let stranger = Revision::seal_unchecked(
        &key(0x6f),
        Revision { next_root: commitment(&pk(0x67)), ..rev(0x61, 0x62, Some(&r1), "2026-09-03T00:00:00Z", r1.keys.clone()) },
    );
    let pool = [r0, r1.clone(), thief, owner.clone(), stranger, orphan];
    let anchor = r1.revision_hash();
    let want = head(&pool, &anchor).unwrap();
    assert_eq!(want.hash, owner.revision_hash());
    // Every rotation of the pool, forwards and backwards.
    for i in 0..pool.len() {
        let mut p = pool.to_vec();
        p.rotate_left(i);
        assert_eq!(head(&p, &anchor).unwrap().hash, want.hash);
        p.reverse();
        assert_eq!(head(&p, &anchor).unwrap().hash, want.hash);
    }
    let mut with_parent = pool.to_vec();
    with_parent.push(owner2.clone());
    assert_eq!(head(&with_parent, &anchor).unwrap().hash, pool[5].revision_hash());
}

#[test]
fn seal_refuses_rule1_breaks() {
    let base = || Revision {
        origin: P.into(),
        seq: 0,
        prev: None,
        issued_at: "2026-09-01T00:00:00Z".into(),
        root: [0; 32],
        next_root: commitment(&pk(0x62)),
        keys: vec![KeyEntry::active("d1", pk(0x63), &["delivery"])],
        revoked_modules: vec![],
        grant_max_s: 60,
        status_max_age_s: 60,
        sig: [0; 64],
        hybrid: None,
    };
    assert!(Revision::seal(&key(0x61), base()).is_ok());
    type Break = Box<dyn Fn(&mut Revision)>;
    let breaks: Vec<Break> = vec![
        Box::new(|r| r.seq = 1),
        Box::new(|r| r.issued_at = "2026-09-01T00:00:00.5Z".into()),
        Box::new(|r| r.next_root = commitment(&pk(0x61))),
        Box::new(|r| r.keys.push(KeyEntry::active("d1", pk(0x64), &["delivery"]))),
        Box::new(|r| r.keys.push(KeyEntry::active("d2", pk(0x63), &["delivery"]))),
        Box::new(|r| r.keys.push(KeyEntry::active("r", pk(0x61), &["delivery"]))),
        Box::new(|r| r.keys.push(KeyEntry::active("n", pk(0x62), &["delivery"]))),
        Box::new(|r| r.keys[0].may_sign.clear()),
        Box::new(|r| r.keys[0].may_sign.push("delivery".into())),
        Box::new(|r| r.keys[0].may_sign.push("energy//x".into())),
        Box::new(|r| r.keys[0].id = "a b".into()),
        Box::new(|r| r.keys[0].until = Some("2026-09-02T00:00:00Z".into())),
        Box::new(|r| r.keys[0].status = EntryStatus::Retired),
        Box::new(|r| r.keys[0].status = EntryStatus::Revoked),
        Box::new(|r| r.revoked_modules = vec![[1; 32], [1; 32]]),
        Box::new(|r| r.grant_max_s = 0),
        Box::new(|r| r.grant_max_s = module_grant_ceiling_s() + 1),
        Box::new(|r| r.status_max_age_s = status_ceiling_s() + 1),
    ];
    for (i, b) in breaks.iter().enumerate() {
        let mut r = base();
        b(&mut r);
        assert_eq!(Revision::seal(&key(0x61), r), Err(Reason::KeyLogMalformed), "break {i}");
    }
    // A key of small order is not a key §3.1 step 3 accepts.
    let mut r = base();
    let mut identity = [0u8; 32];
    identity[0] = 1;
    r.keys[0].key = identity;
    assert_eq!(Revision::seal(&key(0x61), r), Err(Reason::KeyLogMalformed));
}

/// Ed25519 did:key spellings of the claim corpus's publisher and meter keys.
#[test]
fn did_key_literals_from_seeds() {
    assert_eq!(signer_id::did_key(&pk(0x11)), "did:key:z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S");
    assert_eq!(signer_id::did_key(&pk(0x22)), "did:key:z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK");
}

/// An authority registered for pre-emption is confirmed only when the pinned revision
/// itself lists the key with it, not a later revision taken from anywhere.
#[test]
fn preemption_requires_the_pinned_revision() {
    let r0 = rev(0x61, 0x62, None, "2026-09-01T00:00:00Z", vec![KeyEntry::active("d1", pk(0x63), &["delivery"])]);
    let mut k1 = r0.keys.clone();
    k1.push(KeyEntry::active("pe", pk(0x64), &["test/preempt"]));
    let r1 = rev(0x61, 0x62, Some(&r0), "2026-09-02T00:00:00Z", k1);
    let item = Item {
        signer_pubkey: pk(0x64),
        signer_id: "did:web:publisher.example#pe".into(),
        authorities: vec!["test/preempt".into()],
        time: None,
        pq_key: None,
    };
    let pinned_at = |r: &Revision| vec![Anchor::pinned(P, r.revision_hash())];
    let held = [r0.clone(), r1.clone()];
    let got = confirm_with(&item, &held, &pinned_at(&r0), None, &["test/preempt"]);
    assert_eq!(got, Confirmation::Unconfirmed { step: "6a", reason: Reason::AnchorNotPinned });
    let got = confirm_with(&item, &held, &pinned_at(&r1), None, &["test/preempt"]);
    assert_eq!(got.level(), Level::Confirmed);
    // Without the registration, the same item confirms from the later revision.
    assert_eq!(confirm(&item, &held, &pinned_at(&r0), None).level(), Level::Confirmed);
}

#[test]
fn published_test_keys_cover_the_tree_seeds() {
    for seed in [0x01u8, 0x09, 0x11, 0x22, 0x33, 0x44, 0x55, 0x61, 0x6d, 0x6f, 0x70, 0x71, 0x76] {
        assert!(is_published_test_key(&pk(seed)), "{seed:#x}");
    }
    let mut other = [0x5au8; 32];
    other[0] = 0x5b;
    assert!(!is_published_test_key(&SigningKey::from_bytes(&other).verifying_key().to_bytes()));
}

#[test]
fn canonical_form_holds() {
    assert_eq!(check_key_log_canonical_form(), Vec::<String>::new());
}

#[test]
fn corpus_is_the_committed_file_and_passes() {
    let pinned = include_str!("../../../signer-conformance/vectors.json").replace("\r\n", "\n");
    assert!(pinned == conformance_vectors(), "signer-conformance/vectors.json does not regenerate");
    let results = run_corpus(&pinned).unwrap();
    assert_eq!(results.len(), corpus_vector_count());
    let failed: Vec<_> = results.iter().filter(|r| !r.pass).map(|r| format!("{}: {} != {}", r.name, r.got, r.expected)).collect();
    assert!(failed.is_empty(), "{failed:#?}");
}

#[test]
fn claim_authorities_follow_the_table() {
    let doc: Value = serde_json::from_str(include_str!("../../../claim-conformance/vectors.json")).unwrap();
    let mut seen = std::collections::BTreeSet::new();
    for v in doc["vectors"].as_array().unwrap() {
        let Some(wire) = v.get("receipt").and_then(Value::as_str) else { continue };
        let Ok(c) = verify_claim_json(wire) else { continue };
        let a = c.authorities();
        let body = serde_json::from_str::<Value>(wire).unwrap()["body"].clone();
        let provenance = body.get("energy_provenance").and_then(Value::as_str).map(|p| format!("energy/{p}"));
        let mut want = match c.class().label() {
            "energy" => vec![provenance.clone().unwrap()],
            "lawful-basis" => vec![format!("lawful-basis/{}", body["record"].as_str().unwrap())],
            other => vec![other.to_owned()],
        };
        // Any body that restates a figure with its acquisition class needs that class's
        // authority too (§7.5), whatever its class.
        if let Some(p) = provenance
            && !want.contains(&p)
        {
            want.push(p);
        }
        assert_eq!(a, want);
        seen.insert(a[0].split('/').next().unwrap().to_owned());
    }
    for class in ["energy", "lawful-basis", "measurement", "operating-point", "stage", "delivery-gap", "reconstruct-session"] {
        assert!(seen.contains(class), "the claim corpus holds no confirmable {class} claim: {seen:?}");
    }
}

/// A session's energy figure is an item of its own (§7.5): `reconstruct-session` and the
/// energy authority of the class its `energy` writes, read from the nested member; an
/// unmetered session, and every other class, has none.
#[test]
fn a_session_figure_is_its_own_item() {
    let doc: Value = serde_json::from_str(include_str!("../../../claim-conformance/vectors.json")).unwrap();
    let (mut metered, mut unmetered) = (0, 0);
    for v in doc["vectors"].as_array().unwrap() {
        let Some(wire) = v.get("receipt").and_then(Value::as_str) else { continue };
        let Ok(c) = verify_claim_json(wire) else { continue };
        let body = serde_json::from_str::<Value>(wire).unwrap()["body"].clone();
        let want = match (c.class().label(), body.get("energy")) {
            ("reconstruct-session", Some(Value::Object(e))) => {
                metered += 1;
                Some(vec!["reconstruct-session".to_owned(), format!("energy/{}", e["energy_provenance"].as_str().unwrap())])
            }
            ("reconstruct-session", _) => {
                unmetered += 1;
                None
            }
            _ => None,
        };
        assert_eq!(Item::figure_of_claim(&c).map(|i| i.authorities().to_vec()), want);
        assert_eq!(c.figure_authorities(), want);
    }
    assert!(metered > 0 && unmetered > 0, "the claim corpus holds metered and unmetered sessions: {metered} {unmetered}");
}

/// A pinned record moved forward by a retrieval keeps the revision that was pinned. A key
/// a later, fetched revision adds with a pre-emption authority is not confirmed, though
/// the record stays pinned for the hardware and lawful-basis authorities.
#[test]
fn a_forward_move_keeps_the_pinned_revision_for_preemption() {
    let r0 = rev(0x61, 0x62, None, "2026-09-01T00:00:00Z", vec![KeyEntry::active("d1", pk(0x63), &["delivery"])]);
    let mut k1 = r0.keys.clone();
    k1.push(KeyEntry::active("pe", pk(0x64), &["test/preempt"]));
    k1.push(KeyEntry::active("m2", pk(0x69), &["energy/CalibratedInstrument"]));
    let r1 = rev(0x61, 0x62, Some(&r0), "2026-09-02T00:00:00Z", k1);
    let Anchoring::Anchored(pinned) = anchor_log(P, &log_json(std::slice::from_ref(&r0)), AnchorPath::Pinned, None) else {
        panic!("pinning r0 anchors")
    };
    let fetched = AnchorPath::Fetched { acquired_at: "2026-09-03T00:00:00Z".into() };
    let held0 = [r0.clone()];
    let Anchoring::Anchored(moved) = anchor_log(P, &log_json(&[r0.clone(), r1.clone()]), fetched, Some((&pinned, &held0))) else {
        panic!("a retrieval that contains the anchor moves it forward")
    };
    assert_eq!(moved.path, AnchorPath::Pinned);
    assert_eq!(moved.revision, r1.revision_hash());
    assert_eq!(moved.pinned_revision, Some(r0.revision_hash()));
    let held = [r0.clone(), r1.clone()];
    let preempt = Item {
        signer_pubkey: pk(0x64),
        signer_id: "did:web:publisher.example#pe".into(),
        authorities: vec!["test/preempt".into()],
        time: None,
        pq_key: None,
    };
    let got = confirm_with(&preempt, &held, std::slice::from_ref(&moved), None, &["test/preempt"]);
    assert_eq!(got, Confirmation::Unconfirmed { step: "6a", reason: Reason::AnchorNotPinned });
    let hw = Item::for_family(pk(0x69), "did:web:publisher.example#m2", &["CalibratedInstrument"], None);
    let hw = Item { authorities: vec!["energy/CalibratedInstrument".into()], ..hw };
    assert_eq!(confirm(&hw, &held, std::slice::from_ref(&moved), None).level(), Level::Confirmed);
    // A record that says pinned but names no pinned revision is not pinned for step 6a.
    let bare = Anchor { pinned_revision: None, ..moved.clone() };
    assert_eq!(
        confirm(&hw, &held, &[bare], None),
        Confirmation::Unconfirmed { step: "6a", reason: Reason::AnchorNotPinned }
    );
}

/// `energy/<class>` is read from any `energy_provenance` a body carries, whatever its
/// class, beside the class's own authority, and is never listed twice.
#[test]
fn energy_provenance_adds_its_authority_whatever_the_class() {
    use crate::claim_receipt::claim_authorities;
    let body = serde_json::json!({ "energy_provenance": "OnChipCounter", "joules_micro": 1 });
    assert_eq!(claim_authorities("measurement", &body), vec!["measurement", "energy/OnChipCounter"]);
    assert_eq!(claim_authorities("energy", &body), vec!["energy/OnChipCounter"]);
    assert_eq!(claim_authorities("soft-binding", &serde_json::json!({ "alg": "x" })), vec!["soft-binding"]);
    assert_eq!(claim_authorities("lawful-basis", &serde_json::json!({ "record": "decision" })), vec!["lawful-basis/decision"]);
}

/// An item read from a verified JWP group needs `delivery` and the energy authority of
/// each class its labelled leaves and its labelled total carry; the same item as the
/// corpus describes it. A group that does not verify, or an object not included under it,
/// gives no item.
#[test]
fn from_group_reads_the_classes_a_verified_group_carries() {
    use crate::energy_class::{CounterFiltering, EnergyProvenance, Uncertainty};
    use crate::jwp_delivery::{GroupBuilder, ObjectLabel};
    let chip = EnergyProvenance::OnChipCounter {
        uncertainty: Uncertainty { relative_ppm: 20_000, window_us: 1_000_000 },
        filtering: CounterFiltering::Unknown,
    };
    let sid = "did:web:publisher.example#d2";
    let build = |labels: &[Option<ObjectLabel>], labelled: bool| {
        let mut b = GroupBuilder::default();
        for (i, l) in labels.iter().enumerate() {
            b.add(crate::jwp_hash::content_hash(&[i as u8]), 12_000, "origin.example", l.clone());
        }
        b.finish(&key(0x66), sid, 7, 3, "2026-09-18T11:00:00Z", "2026-09-18T12:00:00Z", None, labelled).expect("composes")
    };
    let lab = Some(ObjectLabel::Class(chip.clone()));

    // A legacy group of unlabelled objects: delivery alone.
    let (g, objs) = build(&[None, None], false);
    let refs: Vec<_> = objs.iter().collect();
    let it = Item::from_group(&g, &refs).unwrap();
    assert_eq!(it.authorities(), ["delivery"]);
    assert_eq!(it, Item::for_group(pk(0x66), sid, &[], "2026-09-18T12:00:00Z"));

    // Labelled leaves under a labelled total: the class once, beside delivery.
    let (g, objs) = build(&[lab.clone(), lab.clone()], true);
    assert!(g.label.as_ref().and_then(|l| l.class.as_ref()).is_some(), "the total carries the class");
    let refs: Vec<_> = objs.iter().collect();
    let it = Item::from_group(&g, &refs).unwrap();
    assert_eq!(it.authorities(), ["delivery", "energy/OnChipCounter"]);
    assert_eq!(it, Item::for_group(pk(0x66), sid, &["OnChipCounter", "OnChipCounter"], "2026-09-18T12:00:00Z"));
    // The labelled total alone carries the class, objects or none.
    assert_eq!(Item::from_group(&g, &[]).unwrap().authorities(), ["delivery", "energy/OnChipCounter"]);

    // A labelled leaf under a legacy payload still adds its class.
    let (g, objs) = build(&[lab.clone(), None], false);
    assert!(g.label.is_none());
    let it = Item::from_group(&g, &[&objs[0]]).unwrap();
    assert_eq!(it.authorities(), ["delivery", "energy/OnChipCounter"]);
    assert_eq!(Item::from_group(&g, &[&objs[1]]).unwrap().authorities(), ["delivery"]);

    // A group whose signature fails, and an object from another group, give no item.
    let mut bad = g.clone();
    bad.group_end = "2026-09-18T12:00:01Z".into();
    assert!(Item::from_group(&bad, &[]).is_err());
    let (other, other_objs) = build(&[lab.clone(), lab], true);
    assert_ne!(other.root_hash, g.root_hash);
    assert!(Item::from_group(&g, &[&other_objs[1]]).is_err());
}

/// An item read from another family's receipt takes the key that signed it and every
/// class its figures carry; a receipt that does not verify, and a JWP claim, give none.
#[test]
fn from_family_reads_a_verified_receipt() {
    use crate::energy_class::{CounterFiltering, EnergyProvenance, Uncertainty};
    use crate::provenance::{AssetReceipt, ProvStep};
    let chip = EnergyProvenance::OnChipCounter {
        uncertainty: Uncertainty { relative_ppm: 20_000, window_us: 1_000_000 },
        filtering: CounterFiltering::Unknown,
    };
    let sid = "did:web:publisher.example#d1";
    let a = [1u8; 32];
    let b = [2u8; 32];
    let mut labelled = ProvStep::new("capture", [0; 32], a, 100, 12_000);
    labelled.energy_provenance = Some(chip);
    let mut model = ProvStep::new("encode.jpeg", a, b, 100, 3_000);
    model.energy_provenance = Some(EnergyProvenance::ModelBased);
    let r = AssetReceipt::build(&key(0x63), sid, b, "image", "wai.image.jpeg", 1, vec![labelled, model], None);
    let it = Item::from_family(&r, sid, None).expect("a verified provenance receipt");
    assert_eq!(it.signer_pubkey(), &pk(0x63));
    assert_eq!(it.authorities(), ["receipt", "energy/OnChipCounter", "energy/ModelBased"]);

    let plain = AssetReceipt::build(&key(0x63), sid, a, "image", "wai.image.jpeg", 1, vec![ProvStep::new("capture", [0; 32], a, 100, 0)], None);
    let it = Item::from_family(&plain, sid, Some("2026-09-18T12:00:00Z")).unwrap();
    assert_eq!(it, Item::for_family(pk(0x63), sid, &[], Some("2026-09-18T12:00:00Z")));

    let mut forged = r.clone();
    forged.steps[0].joules_micro += 1;
    assert_eq!(Item::from_family(&forged, sid, None), None, "a receipt that does not verify gives no item");

    let claim = ClaimReceipt::seal(
        &key(0x64),
        "did:web:publisher.example#m1",
        crate::claim_receipt::SubjectType::Object,
        crate::jwp_hash::content_hash(b"subject"),
        0,
        "2026-09-18T12:00:00Z",
        crate::claim_receipt::ClaimBody::SoftBinding(crate::claim_receipt::SoftBindingClaim {
            alg: "org.example.watermark.1".into(),
            registry: "https://registry.example.org/soft-binding-algorithms".into(),
            binding_type: crate::claim_receipt::BindingType::Watermark,
            value: vec![1, 2, 3],
            resolution_endpoint: "https://resolver.example.org/matches".into(),
        }),
        None,
    )
    .unwrap();
    assert_eq!(Item::from_family(&claim, "did:web:publisher.example#m1", None), None, "a claim is read by from_claim");
}

/// SPEC §3.1 check 5 through the reference dispatcher: a sink configured with a key log
/// that holds a capability's pinned parameter sets to jwp-receipts §7.9.
mod pinned_sets_through_dispatch {
    use super::*;
    use crate::codecs::CAP_NEURAL_INT_MLICPP as MLIC;
    use crate::container::{Conditioning, Inert, Manifest, ModelRequirement, PinRefusal, Prior, Skip, Wai, WaiMulti};
    use crate::pin::{self, HeldBytes, PinAuthority, READABLE_FORMS};

    const NOW: &str = "2026-09-20T06:00:00Z";
    const PNG: &str = "wai.image.png";

    fn vectors() -> std::path::PathBuf {
        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/int_mlicpp")
    }

    /// The committed int_mlicpp set: its listing and its one file.
    fn set() -> (Vec<u8>, Vec<u8>) {
        (std::fs::read(vectors().join("model.sha256")).unwrap(), std::fs::read(vectors().join("model.wmb")).unwrap())
    }

    /// The pin the committed set-form envelope carries.
    fn pin() -> Prior {
        let w = Wai::read(vectors().join("stream_set.wai")).unwrap();
        let p = w.prior().expect("stream_set.wai pins its set").clone();
        assert_eq!(p.form(), Some(crate::codecs::CAP_PRIOR_SET));
        p
    }

    fn envelope(fallback: Option<&str>) -> Wai {
        let mut req = ModelRequirement::new(MLIC);
        req.fallback = fallback.map(String::from);
        let mut c = Conditioning::new("int_mlicpp");
        c.prior = Some(pin());
        Wai::new(Manifest::new("image", "decode", req, c), b"payload".to_vec())
    }

    fn digest() -> [u8; 32] {
        pin::sha256(&set().0)
    }

    /// r0 lists a module/decoder key; r1, its child, revokes `revoked`.
    fn log(revoked: &[[u8; 32]]) -> (Revision, Revision) {
        let r0 = rev(0x61, 0x62, None, "2026-09-01T00:00:00Z", vec![KeyEntry::active("modd", pk(0x6b), &["module/decoder"])]);
        let mut r1 = Revision { seq: 1, prev: Some(r0.revision_hash()), issued_at: "2026-09-10T00:00:00Z".into(), ..r0.clone() };
        r1.revoked_modules = revoked.to_vec();
        let r1 = Revision::seal(&key(0x61), r1).unwrap();
        (r0, r1)
    }

    /// A sink holding `log`, anchored at its first revision by pinning, with a fresh
    /// status for the head, a grant for the committed set, and int_mlicpp held to §7.9.
    fn sink(revoked: &[[u8; 32]]) -> SinkKeyLog {
        let (r0, r1) = log(revoked);
        let grant = ModuleGrant::seal(
            &key(0x6b),
            "did:web:publisher.example#modd",
            P,
            digest(),
            ModuleForm::Decoder,
            &[MLIC],
            "2026-09-20T05:30:00Z",
            "2026-09-20T06:30:00Z",
        )
        .unwrap()
        .to_json();
        let status = KeyStatus::seal(&key(0x61), &r1, "2026-09-20T05:55:00Z").to_json();
        SinkKeyLog::new(vec![r0.clone(), r1], vec![Anchor::pinned(P, r0.revision_hash())])
            .with_statuses(vec![status])
            .with_grants(vec![grant])
            .hold_pinned_sets(MLIC, &[P])
            .at(NOW)
    }

    fn dispatch<'a>(w: &'a Wai, key_log: Option<&dyn PinAuthority>) -> Result<&'a str, Inert> {
        let (listing, model) = set();
        let files: std::collections::BTreeMap<String, &[u8]> = [("model.wmb".to_owned(), model.as_slice())].into_iter().collect();
        let held = [HeldBytes::Set { listing: &listing, files: &files }];
        // In the manifest's order, as a deployer may configure: these tests are about the
        // pin's check 5, which the energy order would not reach while the classical
        // fallback is advertised (SPEC §4, *Energy order*).
        w.dispatch_with(&crate::container::DispatchPolicy::manifest_order(), |_| true, |c, p| pin::verify_bytes_with(c, p, &held, READABLE_FORMS, key_log).map(|_| ()))
            .map(|d| d.capability)
    }

    #[test]
    fn a_held_grant_lets_the_pinned_set_decode() {
        let w = envelope(Some(PNG));
        assert_eq!(dispatch(&w, Some(&sink(&[]))), Ok(MLIC));
    }

    /// The set's digest revoked in the head: the pin is refused at SPEC §3.1's check 5
    /// with `prior_revoked`, and dispatch takes the fallback; with no fallback the
    /// envelope is inert and reports it.
    #[test]
    fn a_revoked_parameter_set_is_refused_through_dispatch() {
        let k = sink(&[digest()]);
        assert_eq!(dispatch(&envelope(Some(PNG)), Some(&k)), Ok(PNG));
        let e = dispatch(&envelope(None), Some(&k)).unwrap_err();
        assert_eq!(e.code(), "prior_revoked");
        assert!(
            matches!(e, Inert::PinRefused { ref capability, refusal: PinRefusal::Revoked { revoked_by: None, .. } } if capability == MLIC),
            "{e:?}"
        );
        // Another digest revoked leaves this set held.
        assert_eq!(dispatch(&envelope(None), Some(&sink(&[[7; 32]]))), Ok(MLIC));
    }

    /// Check 5 runs before check 6: a revoked set is refused whether or not the sink
    /// holds its bytes, and a sink that does not hold the set reads none of it.
    #[test]
    fn revocation_is_check_5_before_prior_not_held() {
        let k = sink(&[digest()]);
        assert_eq!(pin::verify_bytes_with(MLIC, &pin(), &[], READABLE_FORMS, Some(&k)).unwrap_err().code(), "prior_revoked");
        assert_eq!(pin::verify_bytes_with(MLIC, &pin(), &[], READABLE_FORMS, None).unwrap_err().code(), "prior_not_held");
        // On disk, through the path the neural sink's ModelRegistry takes.
        let held = [pin::Held::Listing(vectors().join("model.sha256"))];
        assert_eq!(pin::verify_with(MLIC, &pin(), &held, READABLE_FORMS, Some(&k)).unwrap_err().code(), "prior_revoked");
        assert!(pin::verify_with(MLIC, &pin(), &held, READABLE_FORMS, Some(&sink(&[]))).is_ok());
    }

    /// Fail closed: offline (no status), no trusted clock, no grant, and a grant from an
    /// origin that is not a module origin all refuse with `prior_not_authorised`, naming
    /// §7.9's check, and dispatch falls back.
    #[test]
    fn offline_and_unauthorised_sets_fall_back() {
        let offline = sink(&[]).with_statuses(vec![]);
        let no_clock = SinkKeyLog { now: None, ..sink(&[]) };
        let no_grant = sink(&[]).with_grants(vec![]);
        let other_origin = SinkKeyLog { held_to: BTreeMap::new(), ..sink(&[]) }.hold_pinned_sets(MLIC, &["https://other.example"]);
        for (what, k, want) in [
            ("offline", &offline, Some(("4", "module_status_stale"))),
            ("no clock", &no_clock, Some(("4", "module_status_stale"))),
            ("no grant", &no_grant, None),
            ("other origin", &other_origin, Some(("3", "origin_not_expected"))),
        ] {
            assert_eq!(dispatch(&envelope(Some(PNG)), Some(k)), Ok(PNG), "{what}");
            let e = dispatch(&envelope(None), Some(k)).unwrap_err();
            assert_eq!(e.code(), "prior_not_authorised", "{what}");
            let Inert::PinRefused { refusal: PinRefusal::NotAuthorised { check, reason, .. }, .. } = e else { panic!("{what}: {e:?}") };
            assert_eq!(check.zip(reason), want, "{what}");
        }
        // The subject's origin is a module origin too.
        let subject = SinkKeyLog { held_to: BTreeMap::new(), ..sink(&[]) }.hold_pinned_sets(MLIC, &[]).with_subject_origin(P);
        assert_eq!(dispatch(&envelope(None), Some(&subject)), Ok(MLIC));
    }

    /// A sink with no key log does not run §7.9, and one whose relying party does not
    /// hold the capability to §7.9 skips check 5 for it: a key log refuses a set it revokes, held to §7.9 or not.
    #[test]
    fn no_key_log_runs_no_section_7_9() {
        assert_eq!(dispatch(&envelope(None), None), Ok(MLIC));
        let not_held_to = SinkKeyLog { held_to: BTreeMap::new(), ..sink(&[digest()]) };
        assert_eq!(dispatch(&envelope(None), Some(&not_held_to)), Err(crate::container::Inert::PinRefused {
            capability: MLIC.to_owned(),
            refusal: crate::container::PinRefusal::Revoked { capability: MLIC.to_owned(), revoked_by: Some("https://publisher.example".to_owned()) },
        }));
    }

    /// WAI2 selection skips a rendition whose pinned set is revoked and picks the next.
    #[test]
    fn wai2_selection_skips_a_revoked_rendition() {
        use crate::container::Rendition;
        let (listing, model) = set();
        let files: std::collections::BTreeMap<String, &[u8]> = [("model.wmb".to_owned(), model.as_slice())].into_iter().collect();
        let held = [HeldBytes::Set { listing: &listing, files: &files }];
        let m = WaiMulti::new(
            "image",
            "decode",
            Value::Null,
            vec![Rendition::media(MLIC, "int_mlicpp", b"a".to_vec()).with_prior(pin()), Rendition::media(PNG, "png", b"b".to_vec())],
        );
        let k = sink(&[digest()]);
        // In the table's order, as a deployer may configure (by default the classical png
        // is walked first, and the pinned entry's check 5 is not reached).
        let table = crate::container::DispatchPolicy::manifest_order();
        let p = m.pick_with(&table, |_| true, |c, p| pin::verify_bytes_with(c, p, &held, READABLE_FORMS, Some(&k)).map(|_| ())).unwrap();
        assert_eq!(p.index, 1);
        assert!(matches!(p.skipped.as_slice(), [(0, Skip::Pin(PinRefusal::Revoked { .. }))]), "{:?}", p.skipped);
        let p = m.pick_with(&table, |_| true, |c, p| pin::verify_bytes_with(c, p, &held, READABLE_FORMS, Some(&sink(&[]))).map(|_| ())).unwrap();
        assert_eq!(p.index, 0);
    }
}
