use super::*;
use crate::stage_index::{ClassRec, EntryRec, Header, PieceRec, Role, StageIndex, NO_REFINES, SSE_UNKNOWN, TIER_DECODE};

fn key(s: u8) -> SigningKey {
    SigningKey::from_bytes(&[s; 32])
}

/// A one-piece index of component `id`, its base object named by `hash` at `len` bytes;
/// `prev` its previous window's digest.
fn index(id: &str, media: MediaClass, objects: &[([u8; 32], u64)], prev: [u8; 32]) -> Vec<u8> {
    let mut seen = Vec::new();
    let entries: Vec<EntryRec> = objects
        .iter()
        .enumerate()
        .map(|(i, (h, len))| {
            let dup = seen.contains(h);
            seen.push(*h);
            EntryRec {
                content_hash: *h,
                byte_len: *len,
                piece: i as u64,
                refines: NO_REFINES,
                capability: 1,
                role: Role::Base,
                layer: 0,
                prefix_tier: TIER_DECODE,
                flags: if dup { crate::stage_index::ENTRY_DUP } else { 0 },
                deadline_ms: 0,
                prefix_digest: [3; 32],
                sse: SSE_UNKNOWN,
                manifest_len: 10,
                payload_len: len - 22,
            }
        })
        .collect();
    StageIndex {
        header: Header {
            media_class: media,
            tb_num: 25,
            tb_den: 1,
            policy_digest: [5; 32],
            prev_index_digest: prev,
            first_piece: 0,
            first_entry: 0,
            plan_digest: [0; 32],
            component: 0,
        },
        pieces: (0..objects.len() as u64)
            .map(|i| PieceRec { start: 25 * i, duration: 25, first_entry: i as u32, n_entries: 1, kind: 0, flags: 0, rules: 0, moments: 0 })
            .collect(),
        entries,
        classes: vec![ClassRec { name: 2, max_layer: 0, flags: 0, max_kbps: 0 }],
        plan: vec![vec![0; objects.len()]],
        strings: vec![id.into(), "wai.video.int_motion".into(), "tv".into()],
    }
    .to_bytes()
}

/// Two components: `v` with a bumper twice (one stored copy), `a` repeating one of `v`'s
/// objects (a content hash shared across components is one stored copy too).
fn indexes(prev: [u8; 32]) -> Vec<(String, Vec<u8>)> {
    vec![
        ("v".into(), index("v", MediaClass::Video, &[([1; 32], 100), ([2; 32], 40), ([1; 32], 100)], prev)),
        ("a".into(), index("a", MediaClass::Audio, &[([2; 32], 40), ([3; 32], 30)], prev)),
    ]
}

fn seal(energy: (u64, Option<EnergyProvenance>), parent: Parent) -> (PackageReceiptV2, Vec<(String, Vec<u8>)>) {
    let prev = if matches!(parent, Parent::LiveWindow(_)) { [9; 32] } else { [0; 32] };
    let ix = indexes(prev);
    let r = PackageReceiptV2::seal(&key(1), "did:key:pkg", "asset-01", [5; 32], &ix, "builtin:wai-pack@0.1.0", energy, GrantRef::unbounded("pack"), parent).unwrap();
    (r, ix)
}

fn refs(ix: &[(String, Vec<u8>)]) -> Vec<&[u8]> {
    ix.iter().map(|(_, b)| b.as_slice()).collect()
}

#[test]
fn a_sealed_receipt_verifies_and_recomputes_its_counts() {
    let (r, ix) = seal((0, None), Parent::None);
    assert_eq!((r.total_entries, r.unique_objects, r.total_bytes, r.stored_bytes, r.work_units), (5, 3, 310, 170, 5));
    assert_eq!(r.components[0].media_class, MediaClass::Video);
    assert_eq!(r.components[1].index_file(), "index-a.wsix");
    r.verify_detailed(&refs(&ix)).unwrap();
    // A receipt is not a v1 payload: the domains differ from their first bytes on.
    assert!(r.signing_payload().starts_with(b"wai:pack-receipt\x01"));
}

/// Test 6 (the receipt rules) and the rest of verification, each with its reason.
#[test]
fn every_receipt_mutation_names_its_reason() {
    let (r, ix) = seal((0, None), Parent::None);
    let resign = |mut r: PackageReceiptV2| {
        r.sig = key(1).sign(&r.signing_payload()).to_bytes();
        r
    };
    let code = |r: &PackageReceiptV2, ix: &[&[u8]]| r.verify_detailed(ix).map(|_| "ok").unwrap_or_else(|e| e.code);
    let good = refs(&ix);
    // The counts lie, signed: the indexes give others.
    let mut lie = r.clone();
    lie.unique_objects = 2;
    lie.stored_bytes = 100;
    assert_eq!(code(&resign(lie), &good), "receipt-counts");
    let mut lie = r.clone();
    lie.work_units = 4;
    assert_eq!(code(&resign(lie), &good), "receipt-counts");
    // A live window's parent kind over indexes with no previous window.
    let mut wrong = r.clone();
    wrong.parent = Parent::LiveWindow([4; 32]);
    assert_eq!(code(&resign(wrong), &good), "receipt-parent-kind");
    // A re-pack is fine with no previous window.
    let mut repack = r.clone();
    repack.parent = Parent::RePack([4; 32]);
    assert_eq!(code(&resign(repack), &good), "ok");
    // And a package with no parent over a live window's indexes.
    let live = indexes([9; 32]);
    let mut over_live = r.clone();
    for (c, (_, b)) in over_live.components.iter_mut().zip(&live) {
        c.index_digest = crate::stage_index::digest_of(b);
        c.stage_root = StageIndex::parse(b).unwrap().stage_root();
    }
    assert_eq!(code(&resign(over_live), &refs(&live)), "receipt-parent-kind");
    assert_eq!(code(&r, &good[..1]), "receipt-index-count");
    // A receipt over no component at all, signed: refused, whatever its parent.
    for parent in [Parent::None, Parent::LiveWindow([4; 32])] {
        let mut none = r.clone();
        none.components.clear();
        none.parent = parent;
        none.total_entries = 0;
        none.unique_objects = 0;
        none.total_bytes = 0;
        none.stored_bytes = 0;
        none.work_units = 0;
        assert_eq!(code(&resign(none), &[]), "receipt-index-count");
    }
    assert_eq!(code(&r, &[good[1], good[0]]), "receipt-index-digest");
    let mut root = r.clone();
    root.components[0].stage_root = [0; 32];
    assert_eq!(code(&resign(root), &good), "receipt-stage-root");
    let mut media = r.clone();
    media.components[1].media_class = MediaClass::Video;
    assert_eq!(code(&resign(media), &good), "receipt-component");
    let mut policy = r.clone();
    policy.policy_digest = [6; 32];
    assert_eq!(code(&resign(policy), &good), "receipt-policy-digest");
    let mut energy = r.clone();
    energy.joules_micro = 12;
    assert_eq!(code(&resign(energy), &good), "receipt-energy");
    let mut sig = r.clone();
    sig.sig[0] ^= 1;
    assert_eq!(code(&sig, &good), "receipt-signature");
    let mut field = r.clone();
    field.asset_id.push('x');
    assert_eq!(code(&field, &good), "receipt-signature");
    // An index that breaks its own rules names the index's reason.
    let mut bad = StageIndex::parse(&ix[0].1).unwrap();
    bad.entries[2].flags = 0;
    let bad = bad.to_bytes();
    assert_eq!(code(&r, &[&bad, good[1]]), "dup-flag");
}

#[test]
fn seal_refuses_what_it_cannot_sign() {
    let ix = indexes([0; 32]);
    let s = |ix: &[(String, Vec<u8>)], energy, grant, parent| {
        PackageReceiptV2::seal(&key(1), "o", "a", [5; 32], ix, "p", energy, grant, parent).map(|_| "ok").unwrap_or_else(|e| e.code)
    };
    let g = || GrantRef::unbounded("pack");
    assert_eq!(s(&ix, (0, None), g(), Parent::None), "ok");
    assert_eq!(s(&[], (0, None), g(), Parent::LiveWindow([1; 32])), "receipt-index-count");
    assert_eq!(s(&[ix[0].clone(), ("v".into(), ix[0].1.clone())], (0, None), g(), Parent::None), "receipt-component");
    assert_eq!(s(&[("w".into(), ix[0].1.clone())], (0, None), g(), Parent::None), "receipt-component");
    assert_eq!(s(&ix, (0, None), g(), Parent::LiveWindow([1; 32])), "receipt-parent-kind");
    assert_eq!(s(&ix, (5, None), g(), Parent::None), "receipt-energy");
    #[allow(deprecated)]
    let legacy = s(&ix, (5, Some(EnergyProvenance::HwShunt)), g(), Parent::None);
    assert_eq!(legacy, "receipt-energy", "a version-2 receipt takes no legacy label");
    let mut big = g();
    big.joule_ceiling_micro = 1 << 60;
    assert_eq!(s(&ix, (0, None), big, Parent::None), "receipt-json");
    let mut wrong_policy = StageIndex::parse(&ix[0].1).unwrap();
    wrong_policy.header.policy_digest = [8; 32];
    assert_eq!(s(&[("v".into(), wrong_policy.to_bytes())], (0, None), g(), Parent::None), "receipt-policy-digest");
}

/// Test 18: the JSON is RFC 8785 canonical, reads back to the receipt, and a labelled
/// receipt carries its class and evidence.
#[test]
fn the_v2_json_is_canonical_and_round_trips() {
    for (energy, parent) in [
        ((0, None), Parent::None),
        ((25_000, Some(EnergyProvenance::ModelBased)), Parent::RePack([7; 32])),
        ((25_000, Some(crate::energy_class::test_on_chip())), Parent::LiveWindow([7; 32])),
    ] {
        let (r, ix) = seal(energy, parent);
        let j = r.to_json();
        let v: Value = serde_json::from_str(&j).unwrap();
        assert_eq!(jcs(&v).unwrap(), j, "the JSON is its own canonical form");
        assert_eq!(v["version"], 2);
        assert_eq!(v["kind"], "package");
        assert_eq!(v["receipt_hash"], hex(&r.receipt_hash()));
        assert_eq!(v["components"][0]["index"], "index-v.wsix");
        assert_eq!(v["grant"]["joule_ceiling_micro"], Value::Null);
        assert_eq!(v["energy_provenance"].is_null(), r.energy_provenance.is_none(), "always present, null when unmetered");
        let back = PackageReceiptV2::from_json(&j).unwrap();
        assert_eq!(back, r);
        back.verify_detailed(&refs(&ix)).unwrap();
        assert_eq!(AnyPackageReceipt::from_json(&j).unwrap(), AnyPackageReceipt::V2(r));
    }
    // Its key order is code-unit order.
    let (r, _) = seal((0, None), Parent::None);
    let j = r.to_json();
    assert!(j.starts_with("{\"asset_id\":\"asset-01\",\"components\":[{\"id\":\"v\",\"index\":\"index-v.wsix\""), "{j}");
}

#[test]
fn the_v2_json_reader_refuses_what_it_does_not_define() {
    let (r, _) = seal((0, None), Parent::None);
    let j = r.to_json();
    let code = |t: &str| PackageReceiptV2::from_json(t).map(|_| "ok").unwrap_or_else(|e| e.code);
    assert_eq!(code(&j), "ok");
    assert_eq!(code(&j.replacen("{", "{\"extra\":1,", 1)), "receipt-json");
    assert_eq!(code(&j.replace("\"index\":\"index-v.wsix\"", "\"index\":\"other.wsix\"")), "receipt-json");
    assert_eq!(code(&j.replace("\"parent_kind\":\"none\"", "\"parent_kind\":\"re-pack\"")), "receipt-json");
    assert_eq!(code(&j.replace("\"total_bytes\":310", "\"total_bytes\":311")), "receipt-json", "receipt_hash no longer matches");
    assert_eq!(code(&j.replace("\"version\":2", "\"version\":3")), "receipt-json");
    assert_eq!(AnyPackageReceipt::from_json(&j.replace("\"version\":2", "\"version\":3")).unwrap_err().code, "receipt-version");
    // §9.1: a member named twice, at any depth, whichever value comes first.
    assert_eq!(code(&j.replacen("{\"asset_id\"", "{\"asset_id\":\"asset-01\",\"asset_id\"", 1)), "receipt-json");
    assert_eq!(code(&j.replacen("{\"asset_id\"", "{\"asset_id\":\"forged\",\"asset_id\"", 1)), "receipt-json");
    assert_eq!(code(&j.replacen("\"grant\":{\"capability\"", "\"grant\":{\"capability\":\"x\",\"capability\"", 1)), "receipt-json");
    assert_eq!(AnyPackageReceipt::from_json(&j.replacen("\"version\":2", "\"version\":3,\"version\":2", 1)).unwrap_err().code, "receipt-json", "before the version is read");
    // -0 is not an integer of the form.
    assert_eq!(code(&j.replace("\"joules_micro\":0", "\"joules_micro\":-0")), "receipt-json");
    // energy_provenance is always present: null when unmetered, never left out.
    assert_eq!(code(&j.replace("\"energy_provenance\":null,", "")), "receipt-json");
    // Evidence only as the class carries it, never null in its place.
    assert_eq!(code(&j.replace("\"energy_provenance\":null,", "\"energy_provenance\":null,\"energy_uncertainty\":null,")), "receipt-json");
    let (lab, _) = seal((25_000, Some(crate::energy_class::test_on_chip())), Parent::None);
    let lj = lab.to_json();
    assert_eq!(code(&lj), "ok");
    assert_eq!(code(&lj.replace(",\"energy_counter_filtering\":\"Unknown\"", "")), "receipt-json", "an on-chip counter's filtering is part of its evidence");
}

/// §9.3: a version-1 receipt keeps verifying when its JSON leaves the class out (it was
/// issued unmetered) or writes evidence as null, and names no member twice.
#[test]
fn the_v1_reader_reads_issued_receipts() {
    use crate::wai_video_package::{segment, PackageReceipt};
    let src: Vec<(&[u8], u64)> = vec![(b"a", 2000), (b"b", 2000)];
    let r = PackageReceipt::seal(&key(2), "o", "asset", segment(&src, 2000), 0, None, GrantRef::unbounded("video.package"), None);
    let j = r.to_json().unwrap();
    let code = |t: &str| PackageReceipt::from_json(t).map(|_| "ok").unwrap_or_else(|e| e.code);
    assert_eq!(code(&j), "ok");
    assert_eq!(code(&j.replace("\"energy_provenance\":null,", "")), "ok");
    assert_eq!(code(&j.replace("\"energy_provenance\":null,", "\"energy_provenance\":null,\"energy_uncertainty\":null,")), "ok");
    assert_eq!(code(&j.replacen("{\"asset_id\"", "{\"asset_id\":\"x\",\"asset_id\"", 1)), "receipt-json");
}

/// Test 18: version 1 gets a JSON form; it keeps its signed bytes (the legacy pin) and
/// reads back to itself, labelled or not, with or without a `"version"` member.
#[test]
#[allow(deprecated)] // the legacy HwShunt receipt is the point
fn the_v1_json_round_trips_and_keeps_the_legacy_pin() {
    use crate::wai_video_package::{segment, PackageReceipt};
    let src: Vec<(&[u8], u64)> = vec![(b"content-gop-0000", 2000), (b"AD-creative-0001", 2000), (b"content-gop-0002", 2000), (b"AD-creative-0001", 2000)];
    let g = GrantRef::unbounded("video.package");
    let legacy = PackageReceipt::seal(&key(1), "did:key:pkg", "asset-abc", segment(&src, 2000), 12_000, Some(EnergyProvenance::HwShunt), g.clone(), None);
    assert_eq!(hex(&legacy.receipt_hash()), "71a9da59a3202abb2c3fd22ee692b1485a9c674bb8c311f4c82455e6be29ae2b");
    let j = legacy.to_json().unwrap();
    assert_eq!(jcs(&serde_json::from_str(&j).unwrap()).unwrap(), j);
    assert!(j.contains("\"energy_provenance\":\"HwShunt\"") && j.contains("\"version\":1"), "{j}");
    let back = PackageReceipt::from_json(&j).unwrap();
    assert_eq!(back, legacy);
    assert!(back.verify());
    assert_eq!(AnyPackageReceipt::from_json(&j).unwrap(), AnyPackageReceipt::V1(legacy.clone()));
    // Absent "version" reads as version 1.
    let unversioned = j.replace(",\"version\":1", "");
    assert_eq!(PackageReceipt::from_json(&unversioned).unwrap(), legacy);
    assert!(matches!(AnyPackageReceipt::from_json(&unversioned).unwrap(), AnyPackageReceipt::V1(_)));
    let unmetered = PackageReceipt::seal(&key(2), "o", "asset", segment(&src, 2000), 0, None, g.clone(), Some([3; 32]));
    let j = unmetered.to_json().unwrap();
    assert!(j.contains("\"energy_provenance\":null") && j.contains("\"parent_receipt_hash\":\"0303"), "{j}");
    assert_eq!(PackageReceipt::from_json(&j).unwrap(), unmetered);
    let refined = PackageReceipt::seal(&key(2), "o", "asset", segment(&src, 2000), 9_000, Some(crate::energy_class::test_on_chip()), g, None);
    assert_eq!(PackageReceipt::from_json(&refined.to_json().unwrap()).unwrap(), refined);
}

/// Test 17's library half: the version-2 receipt shows its figure under the version-1
/// profile, only with the indexes it verifies against, and a reference meter's
/// measurement claim about it checks against that view.
#[cfg(feature = "receipt_claims")]
#[test]
fn the_v2_figure_view_and_its_measurement_claim() {
    use crate::energy_figure::{meter_report, EnergyFigures};
    let (r, ix) = seal((25_000, Some(EnergyProvenance::ModelBased)), Parent::None);
    let good = refs(&ix);
    let view = (&r, good.as_slice()).figure_view().unwrap();
    assert_eq!(view.profile, "wai:video-package-receipt-id");
    assert_eq!(view.identity, r.receipt_hash());
    assert_eq!(view.figure(None).unwrap().joules_micro, 25_000);
    assert!(crate::energy_figure::profile_name_of_domain(DOMAIN_RECEIPT_ID) == Some("wai:video-package-receipt-id"));
    let none: Vec<&[u8]> = Vec::new();
    assert!((&r, none.as_slice()).figure_view().is_none(), "no view without the indexes it verifies against");
    let claim = meter_report::emit(&view, None, None, crate::energy_report::BuildRecord::of_this_build(), "model:500mW", &key(1), "did:key:pkg", None, "2026-10-06T00:00:00Z").unwrap();
    claim.check_figure_against(Some(&view)).unwrap();
    // Another key may not report the figure.
    assert!(meter_report::emit(&view, None, None, crate::energy_report::BuildRecord::of_this_build(), "model", &key(2), "x", None, "2026-10-06T00:00:00Z").is_err());
    // An unmetered receipt gets no report.
    let (u, uix) = seal((0, None), Parent::None);
    let ug = refs(&uix);
    let uview = (&u, ug.as_slice()).figure_view().unwrap();
    assert!(meter_report::emit(&uview, None, None, crate::energy_report::BuildRecord::of_this_build(), "model", &key(1), "x", None, "2026-10-06T00:00:00Z").is_err());
}

/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
/// non-canonical spelling of one, and an R of small order are each `receipt-signature`,
/// though a lenient check accepts each.
#[test]
fn the_signature_is_verified_strictly() {
    let (r, ix) = seal((0, None), Parent::None);
    let good = refs(&ix);
    let set = |r: &mut PackageReceiptV2, k, s| (r.signer_pubkey, r.sig) = (k, s);
    crate::sig::assert_refuses_forgeries(&r, set, |r| r.signing_payload(), |r| r.verify_detailed(&good).is_ok());
    for f in crate::sig::forgeries(|k| {
        let mut g = r.clone();
        set(&mut g, *k, [0; 64]);
        g.signing_payload()
    }) {
        let mut g = r.clone();
        set(&mut g, f.key, f.sig);
        assert_eq!(g.verify_detailed(&good).map_err(|e| e.code), Err("receipt-signature"), "{}", f.name);
    }
}
