use super::corpus::{content_key, kid, principal, T0};
use super::*;

fn list() -> CommitmentList {
    let keys = vec![
        ListKey { kid: kid("t/video"), scheme: Scheme::Cenc, key_commit: key_commitment(&content_key("t/video", 16)), period: None },
        ListKey {
            kid: kid("t/p1"),
            scheme: Scheme::Cbcs,
            key_commit: key_commitment(&content_key("t/p1", 16)),
            period: Some(Period { index: Some(1), window: Some((T0, T0 + 3600)) }),
        },
    ];
    CommitmentList::seal(&principal("key_holder"), "holder", "t-content", "svod:premium", Some([9; 32]), T0, keys)
}

fn ent(offline_s: u64, renewals: u32) -> Entitlement {
    Entitlement::seal(
        &principal("authority"),
        "authority",
        [1; 16],
        principal("device_a").verifying_key().to_bytes(),
        "svod:premium",
        T0,
        T0 + 7200,
        offline_s,
        renewals,
    )
}

fn req(kids: Vec<[u8; 16]>, kind: RequestKind, renews: Option<[u8; 32]>, t: u64) -> Request {
    let mut kids = kids;
    kids.sort_unstable();
    Request {
        content_id: "t-content".into(),
        delivery: Delivery { kind: DeliveryKind::Drm, system_id: Some([7; 16]) },
        kids,
        kind,
        renews,
        request_sha256: sha256(b"challenge"),
        request_t: t,
    }
}

fn authorities() -> [[u8; 32]; 1] {
    [principal("authority").verifying_key().to_bytes()]
}

/// Decide and seal one request, forwarded and answered 200 when granted.
fn seal(e: &Entitlement, r: &Request, issued: u64, log: &[ProxyReceipt]) -> ProxyReceipt {
    let b = e.to_bytes();
    let proof = prove(&principal("device_a"), &b, r);
    let l = list();
    let o = decide(&authorities(), Some(&b), Some(&proof), r, Some(&l), issued, log);
    let up = Upstream { status: 200, response_sha256: sha256(b"licence") };
    ProxyReceipt::assemble(&principal("proxy"), "proxy", r, Some(&l), Some(b), Some(proof), issued, &o, Some(up), 0, None)
}

#[test]
fn the_tables_round_trip() {
    for s in Scheme::ALL {
        assert_eq!(Scheme::from_code(s.code()), Some(s));
        assert_eq!(Scheme::from_label(s.label()), Some(s));
    }
    for d in Decision::ALL {
        assert_eq!(Decision::from_code(d.code()), Some(d));
        assert_eq!(Decision::from_label(d.label()), Some(d));
    }
    // Form 1's codes keep their meaning.
    assert_eq!(Decision::Granted.code(), 1);
    assert_eq!(Decision::DeniedNoEntitlement.code(), 2);
    assert_eq!(Decision::DeniedExpired.code(), 3);
    assert!(!DeliveryKind::HlsKeyUri.carries(Scheme::Cens) && !DeliveryKind::HlsKeyUri.carries(Scheme::Cbc1));
    assert!(!DeliveryKind::ClearKey.carries(Scheme::Aes128));
    assert!(Scheme::ALL.iter().all(|s| DeliveryKind::Drm.carries(*s)));
    assert_eq!(Scheme::Aes256Gcm.key_len(), 32);
}

#[test]
fn a_list_round_trips_and_its_rules_refuse() {
    let l = list();
    let b = l.to_bytes();
    assert_eq!(CommitmentList::parse(&b), Ok(l.clone()));
    assert!(l.keys.windows(2).all(|w| w[0].kid < w[1].kid), "sealed in KID order");
    let mut trailing = b.clone();
    trailing.push(0);
    assert_eq!(CommitmentList::parse(&trailing), Err(ListFault::Malformed));
    let mut sig = b.clone();
    let n = sig.len();
    sig[n - 2] ^= 1;
    assert_eq!(CommitmentList::parse(&sig), Err(ListFault::BadSignature));
    let reseal = |f: &dyn Fn(&mut CommitmentList)| {
        let mut m = l.clone();
        f(&mut m);
        m.sign(&principal("key_holder"));
        CommitmentList::parse(&m.to_bytes())
    };
    assert_eq!(reseal(&|m| m.keys.swap(0, 1)), Err(ListFault::Malformed), "KIDs out of order");
    assert_eq!(reseal(&|m| m.keys[0].key_commit = [0; 32]), Err(ListFault::Malformed));
    assert_eq!(reseal(&|m| m.keys[1].period = Some(Period { index: None, window: Some((5, 5)) })), Err(ListFault::Malformed));
    assert_eq!(reseal(&|m| m.content_id.clear()), Err(ListFault::Malformed));
    assert!(reseal(&|m| m.cpix = None).is_ok());
}

#[test]
fn an_entitlement_holds_only_as_signed() {
    let e = ent(0, 0);
    let b = e.to_bytes();
    assert_eq!(Entitlement::read(&b), Some(e.clone()));
    assert!(e.holds());
    let mut m = e.clone();
    m.not_after += 1;
    assert!(!m.holds(), "a changed window does not verify");
    let mut m = e.clone();
    m.renewals = 1;
    m.sign(&principal("authority"));
    assert!(!m.holds(), "renewals with no offline allowance");
    let mut m = e;
    m.not_before = m.not_after + 1;
    m.sign(&principal("authority"));
    assert!(!m.holds());
    let mut t = b.clone();
    t.push(0);
    assert_eq!(Entitlement::read(&t), None);
}

#[test]
fn each_check_decides_in_its_order() {
    use Decision::*;
    let l = list();
    let e = ent(3600, 1);
    let b = e.to_bytes();
    let dev = principal("device_a");
    let auth = authorities();
    let video = kid("t/video");
    let run = |r: &Request, ent: Option<&[u8]>, proof: Option<[u8; 64]>, issued: u64, log: &[ProxyReceipt]| {
        decide(&auth, ent, proof.as_ref(), r, Some(&l), issued, log).decision
    };
    let r = req(vec![video], RequestKind::Streaming, None, T0 + 10);
    let p = prove(&dev, &b, &r);
    assert_eq!(run(&r, None, None, T0 + 10, &[]), DeniedEntitlementInvalid);
    assert_eq!(run(&r, Some(&b), Some(prove(&principal("device_b"), &b, &r)), T0 + 10, &[]), DeniedEntitlementInvalid);
    assert_eq!(decide(&[[3; 32]], Some(&b), Some(&p), &r, Some(&l), T0 + 10, &[] as &[ProxyReceipt]).decision, DeniedEntitlementInvalid);
    assert_eq!(run(&r, Some(&b), Some(p), T0 + 10 + FRESHNESS_S + 1, &[]), DeniedStale);
    assert_eq!(run(&r, Some(&b), Some(p), T0 + 10 + FRESHNESS_S, &[]), Granted, "the freshness bound is inclusive");
    let granted = seal(&e, &r, T0 + 10, &[]);
    assert_eq!(run(&r, Some(&b), Some(p), T0 + 11, std::slice::from_ref(&granted)), DeniedReplayed);
    let unknown = req(vec![video, [0xEE; 16]], RequestKind::Streaming, None, T0 + 10);
    assert_eq!(run(&unknown, Some(&b), Some(prove(&dev, &b, &unknown)), T0 + 10, &[]), DeniedUnknownKey);
    assert_eq!(decide(&auth, Some(&b), Some(&p), &r, None, T0 + 10, &[] as &[ProxyReceipt]).decision, DeniedUnknownKey);
    let mut basic = e.clone();
    basic.entitlement = "svod:basic".into();
    basic.sign(&principal("authority"));
    let bb = basic.to_bytes();
    assert_eq!(run(&r, Some(&bb), Some(prove(&dev, &bb, &r)), T0 + 10, &[]), DeniedNoEntitlement);
    let early = req(vec![video], RequestKind::Streaming, None, T0 - 10);
    assert_eq!(run(&early, Some(&b), Some(prove(&dev, &b, &early)), T0 - 10, &[]), DeniedNotYetValid);
    let late = req(vec![video], RequestKind::Streaming, None, T0 + 7201);
    assert_eq!(run(&late, Some(&b), Some(prove(&dev, &b, &late)), T0 + 7201, &[]), DeniedExpired);
    let at_end = req(vec![video], RequestKind::Streaming, None, T0 + 7200);
    assert_eq!(run(&at_end, Some(&b), Some(prove(&dev, &b, &at_end)), T0 + 7200, &[]), Granted, "not_after is included");
    // A period that ends before the entitlement starts.
    let mut later = e.clone();
    later.not_before = T0 + 3600;
    later.sign(&principal("authority"));
    let lb = later.to_bytes();
    let p1 = req(vec![kid("t/p1")], RequestKind::Streaming, None, T0 + 3700);
    assert_eq!(run(&p1, Some(&lb), Some(prove(&dev, &lb, &p1)), T0 + 3700, &[]), DeniedPeriodOutside, "end at most not_before");
    let pre = ent(0, 0).to_bytes();
    let offline = req(vec![video], RequestKind::Offline, None, T0 + 10);
    assert_eq!(run(&offline, Some(&pre), Some(prove(&dev, &pre, &offline)), T0 + 10, &[]), DeniedOfflineNotEntitled);
}

#[test]
fn an_offline_licence_renews_once_and_never_outlives_the_entitlement() {
    use Decision::*;
    let e = ent(3600, 1);
    let video = kid("t/video");
    let o0 = seal(&e, &req(vec![video], RequestKind::Offline, None, T0 + 10), T0 + 10, &[]);
    assert_eq!((o0.decision, o0.licence_expiry, o0.renewal), (Granted, Some(T0 + 3610), Some(0)));
    let log = vec![o0.clone()];
    let o1 = seal(&e, &req(vec![video], RequestKind::Renewal, Some(o0.identity()), T0 + 3000), T0 + 3000, &log);
    assert_eq!((o1.decision, o1.licence_expiry, o1.renewal), (Granted, Some(T0 + 6600), Some(1)));
    let log = vec![o0.clone(), o1.clone()];
    let o2 = seal(&e, &req(vec![video], RequestKind::Renewal, Some(o1.identity()), T0 + 4000), T0 + 4000, &log);
    assert_eq!(o2.decision, DeniedRenewalsExhausted);
    let lapsed = seal(&e, &req(vec![video], RequestKind::Renewal, Some(o0.identity()), T0 + 3611), T0 + 3611, std::slice::from_ref(&o0));
    assert_eq!(lapsed.decision, DeniedRenewalLapsed);
    let other = seal(&e, &req(vec![video, kid("t/p1")], RequestKind::Renewal, Some(o0.identity()), T0 + 100), T0 + 100, std::slice::from_ref(&o0));
    assert_eq!(other.decision, DeniedRenewalUnknown, "a renewal names the same keys");
    let clamp = seal(&e, &req(vec![video], RequestKind::Offline, None, T0 + 7000), T0 + 7000, &[]);
    assert_eq!(clamp.licence_expiry, Some(T0 + 7200));
    for r in [&o0, &o1, &o2, &lapsed, &other, &clamp] {
        assert_eq!(r.check(), Ok(()));
    }
}

#[test]
fn a_receipt_round_trips_and_verifies_strictly() {
    let r = seal(&ent(0, 0), &req(vec![kid("t/video")], RequestKind::Streaming, None, T0 + 10), T0 + 10, &[]);
    assert!(r.released && r.keys[0].key_commit == Some(key_commitment(&content_key("t/video", 16))));
    let back = ProxyReceipt::from_json(&r.to_json()).expect("its own JSON reads");
    assert_eq!(back, r);
    assert_eq!(back.identity(), r.identity());
    let mut j = r.to_json();
    j["energy_provenance"] = serde_json::json!("HwShunt");
    assert_eq!(ProxyReceipt::from_json(&j), None, "the legacy label does not read in this form");
    let mut j = r.to_json();
    j.as_object_mut().unwrap().insert("extra".into(), serde_json::json!(1));
    assert_eq!(ProxyReceipt::from_json(&j), None);
    crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.check().is_ok());
}

#[test]
fn each_own_rule_refuses() {
    let r = seal(&ent(3600, 1), &req(vec![kid("t/video")], RequestKind::Offline, None, T0 + 10), T0 + 10, &[]);
    assert_eq!(r.check(), Ok(()));
    let reseal = |f: &dyn Fn(&mut ProxyReceipt)| {
        let mut m = r.clone();
        f(&mut m);
        m.sign(&principal("proxy"));
        m.check()
    };
    use Reason::*;
    assert_eq!(reseal(&|m| m.keys.push(m.keys[0].clone())), Err(ReceiptInvalid), "rule 2");
    assert_eq!(reseal(&|m| m.delivery.system_id = None), Err(ReceiptInvalid), "rule 3");
    assert_eq!(reseal(&|m| m.delivery.system_id = Some([0; 16])), Err(ReceiptInvalid), "rule 3");
    assert_eq!(
        reseal(&|m| {
            m.delivery = Delivery { kind: DeliveryKind::ClearKey, system_id: None };
            m.keys[0].scheme = Some(Scheme::Aes128);
            m.upstream_status = None;
            m.response_sha256 = None;
        }),
        Err(SchemeDeliveryMismatch),
        "rule 4"
    );
    assert_eq!(reseal(&|m| m.required_entitlement = None), Err(ReceiptInvalid), "rule 5");
    assert_eq!(reseal(&|m| m.proof = None), Err(ReceiptInvalid), "rule 6");
    assert_eq!(reseal(&|m| m.renews = Some([1; 32])), Err(ReceiptInvalid), "rule 6");
    assert_eq!(reseal(&|m| m.decision = Decision::DeniedStale), Err(ReceiptInvalid), "rule 7");
    assert_eq!(reseal(&|m| m.keys[0].key_commit = None), Err(ReceiptInvalid), "rule 7");
    assert_eq!(reseal(&|m| m.upstream_status = Some(500)), Err(ReceiptInvalid), "rule 8");
    assert_eq!(reseal(&|m| m.response_sha256 = None), Err(ReceiptInvalid), "rule 8");
    assert_eq!(reseal(&|m| m.licence_expiry = None), Err(ReceiptInvalid), "rule 9");
    assert_eq!(reseal(&|m| m.renewal = Some(1)), Err(ReceiptInvalid), "rule 9");
    assert_eq!(reseal(&|m| m.licence_expiry = Some(m.issued - 1)), Err(ReceiptInvalid), "rule 9");
    assert_eq!(reseal(&|m| m.joules_micro = 5), Err(ReceiptInvalid), "rule 10: a figure with no class");
    assert_eq!(
        reseal(&|m| {
            m.joules_micro = 1;
            m.energy_provenance = Some(EnergyProvenance::OnChipCounter {
                uncertainty: Uncertainty { relative_ppm: 1_000, window_us: 10 },
                filtering: CounterFiltering::Unknown,
            });
        }),
        Err(ReceiptInvalid),
        "rule 10: below the resolution floor"
    );
}

#[test]
fn a_ticket_follows_its_receipt() {
    let r0 = seal(&ent(0, 0), &req(vec![kid("t/video")], RequestKind::Streaming, None, T0 + 10), T0 + 10, &[]);
    let mut r = r0.clone();
    r.delivery = Delivery { kind: DeliveryKind::ClearKey, system_id: None };
    r.upstream_status = None;
    r.response_sha256 = None;
    r.sign(&principal("proxy"));
    assert_eq!(r.check(), Ok(()));
    assert!(Ticket::for_receipt(&principal("proxy"), &r0, 60).is_none(), "a wrapped delivery has no ticket");
    assert!(Ticket::for_receipt(&principal("proxy"), &r, 301).is_none());
    let t = Ticket::for_receipt(&principal("proxy"), &r, 60).unwrap();
    let back = Ticket::parse(&t.to_bytes()).expect("reads");
    assert_eq!(back, t);
    assert!(t.follows(&r));
    let mut other = t.clone();
    other.session_type = 2;
    other.sign(&principal("proxy"));
    assert!(!other.follows(&r));
    let mut b = t.to_bytes();
    b[8] ^= 1;
    assert!(Ticket::parse(&b).is_none(), "a changed byte does not verify");
}

#[test]
fn the_encodings_are_canonical() {
    for n in 0..40u8 {
        let v: Vec<u8> = (0..n).map(|i| i.wrapping_mul(37)).collect();
        assert_eq!(unb64url(&b64url(&v)), Some(v.clone()));
        assert_eq!(unb64(&b64(&v)), Some(v));
    }
    assert_eq!(unb64("AAE="), Some(vec![0, 1]));
    assert_eq!(unb64("AAF="), None, "non-zero unused bits");
    assert_eq!(unb64("AAE"), None, "padding is required");
    assert_eq!(unb64(" AA\nE= "), Some(vec![0, 1]), "white space is removed");
    assert_eq!(unb64url("AAE="), None);
    assert_eq!(parse_kids(&format!("{},{}", hex(&[2; 16]), hex(&[1; 16]))), Some(vec![[1; 16], [2; 16]]));
    assert_eq!(parse_kids(&format!("{0},{0}", hex(&[2; 16]))), None, "a KID twice");
    assert_eq!(parse_kids(&hex(&[0xAB; 16]).to_uppercase()), None, "lowercase only");
    assert_eq!(parse_kids(""), None);
}

#[test]
fn the_cpix_documents_derive_their_lists() {
    let docs = corpus::documents();
    let film = cpix::derive(docs["film.cpix.xml"].as_bytes()).expect("the film document derives");
    assert_eq!(film.content_id, "film-0001");
    assert_eq!(film.keys.len(), 2);
    assert!(film.keys.iter().all(|k| k.key == content_key(if k.kid == kid("film/video") { "film/video" } else { "film/audio" }, 16)));
    let channel = cpix::derive(docs["channel.cpix.xml"].as_bytes()).unwrap();
    assert!(channel.keys.iter().all(|k| k.period.is_some_and(|p| p.index.is_some() && p.window.is_some())));
    assert_eq!(cpix::derive(docs["encrypted.cpix.xml"].as_bytes()), Err(cpix::CpixError::Unsupported));
    assert_eq!(cpix::derive(docs["broken.cpix.xml"].as_bytes()), Err(cpix::CpixError::Malformed));
    let f = &docs["film.cpix.xml"];
    for (from, to, want) in [
        (" contentId=\"film-0001\"", "", cpix::CpixError::Unsupported),
        ("commonEncryptionScheme=\"cenc\"", "commonEncryptionScheme=\"cbc2\"", cpix::CpixError::Unsupported),
        ("<cpix:ContentKey kid=", "<cpix:ContentKey dependsOnKey=\"00000000-0000-0000-0000-000000000000\" kid=", cpix::CpixError::Unsupported),
        ("<pskc:PlainValue>", "<pskc:PlainValue>AAAA", cpix::CpixError::Malformed),
        ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE x>\n", cpix::CpixError::Malformed),
    ] {
        assert!(f.contains(from), "{from}");
        assert_eq!(cpix::derive(f.replacen(from, to, 1).as_bytes()), Err(want), "{from} -> {to}");
    }
    let c = &docs["channel.cpix.xml"];
    let dup = c.replacen("periodId=\"p2\"", "periodId=\"p1\"", 1).replacen("<cpix:KeyPeriodFilter periodId=\"p3\"/>", "<cpix:KeyPeriodFilter periodId=\"p3\"/><cpix:KeyPeriodFilter periodId=\"p1\"/>", 1);
    assert_eq!(cpix::derive(dup.as_bytes()), Err(cpix::CpixError::Unsupported), "a key for two periods");
    assert_eq!(cpix::derive(c.replacen("periodId=\"p2\"", "periodId=\"p9\"", 1).as_bytes()), Err(cpix::CpixError::Malformed));
    assert_eq!(cpix::derive(c.replacen("index=\"1\"", "index=\"4294967296\"", 1).as_bytes()), Err(cpix::CpixError::Unsupported));
}
