use super::*;

#[test]
fn dates_read_as_a_player_reads_them() {
    assert_eq!(date_ms("1970-01-01T00:00:00Z"), Some(0));
    assert_eq!(date_ms("2026-10-25T00:00:00.000Z"), Some(1_792_886_400_000));
    assert_eq!(date_ms("2026-10-25T00:00:03.4Z"), Some(1_792_886_403_400));
    assert_eq!(date_ms("2026-10-25T00:00:03.4567Z"), Some(1_792_886_403_456));
    assert_eq!(date_ms("2026-10-25T02:00:00+02:00"), Some(1_792_886_400_000));
    assert_eq!(date_ms("2024-02-29T00:00:00Z"), Some(1_709_164_800_000));
    for bad in ["2023-02-29T00:00:00Z", "2026-10-25 00:00:00Z", "2026-10-25T00:00:00", "2026-10-25T24:00:00Z", "2026-10-25T00:00:00.Z", "2026-13-01T00:00:00Z"] {
        assert_eq!(date_ms(bad), None, "{bad}");
    }
}

#[test]
fn durations_read_exactly_half_up() {
    assert_eq!(seconds_ms("2.400,"), Some(2400));
    assert_eq!(seconds_ms("2.0005"), Some(2001));
    assert_eq!(seconds_ms("2.0004999"), Some(2000));
    assert_eq!(seconds_ms("4"), Some(4000));
    assert_eq!(seconds_ms("4.,title"), Some(4000));
    assert_eq!(seconds_ms(".5"), Some(500));
    assert_eq!(seconds_ms("x"), None);
}

#[test]
fn attributes_read_quoted_and_bare() {
    let a = attributes("ID=\"break-1\",START-DATE=\"2026-10-25T00:00:03.400Z\",DURATION=3.0,SCTE35-OUT=0xFC30,X-COM=\"a, b\"");
    assert_eq!(a["ID"], "break-1");
    assert_eq!(a["DURATION"], "3.0");
    assert_eq!(a["SCTE35-OUT"], "0xFC30");
    assert_eq!(a["X-COM"], "a, b");
}

#[test]
fn origins_serialize_as_rfc6454_does() {
    assert_eq!(origin_of("https://Media.Example/a/b?x=1").as_deref(), Some("https://media.example"));
    assert_eq!(origin_of("https://media.example:443/a").as_deref(), Some("https://media.example"));
    assert_eq!(origin_of("http://media.example:8080/a").as_deref(), Some("http://media.example:8080"));
    assert_eq!(origin_of("https://user:pw@media.example/a").as_deref(), Some("https://media.example"));
    assert_eq!(origin_of("https://[::1]:8443/a").as_deref(), Some("https://[::1]:8443"));
    assert_eq!(origin_of("not a url"), None);
}

#[test]
fn names_state_content_hashes_only_in_their_forms() {
    let h = "420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a";
    assert!(stated_sha256(&format!("https://x.example/f/{h}.m4s")).is_some());
    assert!(stated_sha256(&format!("https://x.example/f/{h}.m4s?t=1")).is_some());
    assert!(stated_sha256(&format!("https://x.example/f/{h}.wai")).is_none());
    assert!(stated_sha256(&format!("https://x.example/f/{}.m4s", &h[..63])).is_none());
    assert!(stated_sha256(&format!("https://x.example/f/{}.m4s", h.to_uppercase())).is_none());
    assert!(stated_cid(&format!("https://x.example/o/{h}.wai")).is_some());
    assert!(stated_cid(&format!("live/1.wai#n=3&cid={h}")).is_some());
    assert!(stated_cid("live/1.wai").is_none());
}

/// The acknowledgements a session signs from the hashes and lengths it kept are the ones
/// stream-audit's reference signs from the objects' bytes (stream-audit §4.2).
#[test]
fn acknowledgements_match_stream_audits_reference() {
    let key = SigningKey::from_bytes(&[7u8; 32]);
    let start = 1_792_886_400_000_000u64;
    let cfg = Config::from_json(&json!({"channel": "film-library", "owner": hex(&[9u8; 32]), "window_us": 2_000_000})).unwrap();
    let mut sink = Sink::open(cfg, key.clone(), start);
    let mut refs = Vec::new();
    for i in 0..12u64 {
        // Objects 0 and 1, and 4 and 5, are one body from one origin in one window: repeats.
        let origin = if i < 6 { "https://a.example" } else { "https://b.example" };
        let body = vec![(i / 2) as u8; 100 + (i as usize / 2) * 10];
        let at = start + i * 700_000;
        let media = i % 4 != 0;
        sink.object(&ObjectMeta { url: format!("{origin}/x/{}", i % 5), at, media: Some(media), status: 200, ..Default::default() }, &body).unwrap();
        refs.push(audit::Received { at, origin: origin.into(), media, bytes: body });
    }
    let end = start + 9_000_000;
    let (mine, ro, rb) = sink.acknowledgements(audit::presentation::ON_DEMAND, end);
    let theirs = audit::acknowledgements(&key, audit::presentation::ON_DEMAND, start, end, 2_000_000, &refs);
    assert_eq!(mine, theirs.acks);
    assert_eq!((ro, rb), (theirs.repeat_objects, theirs.repeat_bytes));
    assert!(ro > 0);
}

/// A session's bundle verifies as the owner's collector verifies it, and refuses under any other
/// owner or channel.
#[test]
fn a_session_bundle_verifies() {
    let start = 1_792_886_400_000_000u64;
    let owner = SigningKey::from_bytes(&[1u8; 32]).verifying_key().to_bytes();
    let cfg = Config::from_json(&json!({"channel": "news-east", "owner": hex(&owner)})).unwrap();
    let mut sink = Sink::open(cfg, SigningKey::from_bytes(&[2u8; 32]), start);
    sink.object(&ObjectMeta { url: "https://cdn.example/s/1.m4s".into(), at: start + 10, media: Some(true), status: 200, ..Default::default() }, b"segment").unwrap();
    for k in 0..20u64 {
        let o = Observation { at: start + 100_000 + k * 200_000, playing: true, visible_bp: 10_000, audible: true, program_ms: None, seeked: false, interaction: false };
        assert_eq!(sink.observe(&o).unwrap(), None);
    }
    let b = sink.end(audit::ended::ENDED, start + 5_000_000).unwrap();
    let bytes = bundle_bytes(&b);
    let ok = audit::verify_bundle(&bytes, &owner, &["news-east".to_string()]).to_json();
    assert_eq!(ok["outcome"], json!("accept"), "{ok}");
    let other = audit::verify_bundle(&bytes, &[3u8; 32], &["news-east".to_string()]).to_json();
    assert_eq!(other["reason"], json!("audit_wrong_owner"), "{other}");
    let ch = audit::verify_bundle(&bytes, &owner, &["film-library".to_string()]).to_json();
    assert_eq!(ch["reason"], json!("audit_unknown_channel"), "{ch}");
    // The session takes nothing after its end.
    assert_eq!(sink.event("present", start + 6_000_000), Err(Reason::SessionEnded));
}

#[test]
fn the_corpus_reaches_what_it_states() {
    let doc = corpus::generate();
    corpus::verify(&doc).unwrap();
}
