use super::*;
use crate::container::{Conditioning, Manifest, ModelRequirement, Prev};

fn env(cap: &str, refines: Option<(&[u8; 32], &[u8; 32], u8)>, derived: bool, payload: &[u8]) -> Vec<u8> {
    let mut m = Manifest::new("video", "replicate", ModelRequirement::new(cap), Conditioning::new("x"));
    if let Some((base, prev, layer)) = refines {
        m = m.with_refines(&Refines { base: hex(base), prev: Prev::One(hex(prev)), layer, span: None });
    }
    if derived {
        m = m.with_derived_from(&DerivedFrom { motif: "ab".repeat(32), motif_capability: "wai.video.int_motion".into(), renderer: "r@1".into(), settings: "cd".repeat(32) });
    }
    Wai::new(m, payload.to_vec()).to_bytes().unwrap()
}

/// An entry describing `bytes`.
#[allow(clippy::too_many_arguments)]
fn entry(bytes: &[u8], piece: u64, cap: u16, role: Role, layer: u8, refines: u32, tier: u8, flags: u8) -> EntryRec {
    let man = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
    EntryRec {
        content_hash: crate::jwp_hash::content_hash(bytes),
        byte_len: bytes.len() as u64,
        piece,
        refines,
        capability: cap,
        role,
        layer,
        prefix_tier: tier,
        flags,
        deadline_ms: if role == Role::Refinement { 1500 } else { 0 },
        prefix_digest: if tier == TIER_DECODE { [7u8; 32] } else { [0u8; 32] },
        sse: SSE_UNKNOWN,
        manifest_len: man,
        payload_len: bytes.len() as u64 - 12 - man as u64,
    }
}

/// Two pieces: the first a base, two refinements and a floor; the second the same base
/// again (a duplicate) and a floor. One class, `tv`, to layer 2 then 0.
fn fixture() -> (StageIndex, Vec<Vec<u8>>) {
    let base = env("wai.video.int_motion", None, false, b"base");
    let bh = crate::jwp_hash::content_hash(&base);
    let r1 = env("wai.video.int_refine", Some((&bh, &bh, 1)), false, b"r1");
    let r1h = crate::jwp_hash::content_hash(&r1);
    let r2 = env("wai.video.int_refine", Some((&bh, &r1h, 2)), false, b"r2");
    let f0 = env("wai.video.av1", None, true, b"floor-0");
    let f1 = env("wai.video.av1", None, true, b"floor-1");
    let objects = vec![base.clone(), r1.clone(), r2.clone(), f0.clone(), base.clone(), f1.clone()];
    let entries = vec![
        entry(&base, 0, 2, Role::Base, 0, NO_REFINES, TIER_DECODE, 0),
        entry(&r1, 0, 3, Role::Refinement, 1, 0, TIER_DECODE, 0),
        entry(&r2, 0, 3, Role::Refinement, 2, 1, TIER_DECODE, 0),
        entry(&f0, 0, 4, Role::Floor, 0, NO_REFINES, TIER_NONE, ENTRY_DERIVED_FROM),
        entry(&base, 1, 2, Role::Base, 0, NO_REFINES, TIER_DECODE, ENTRY_DUP),
        entry(&f1, 1, 4, Role::Floor, 0, NO_REFINES, TIER_NONE, ENTRY_DERIVED_FROM),
    ];
    let ix = StageIndex {
        header: Header {
            media_class: MediaClass::Video,
            tb_num: 25,
            tb_den: 1,
            policy_digest: [1u8; 32],
            prev_index_digest: [0u8; 32],
            first_piece: 0,
            first_entry: 0,
            plan_digest: plan_digest(&[0, 50]),
            component: 0,
        },
        pieces: vec![
            PieceRec { start: 0, duration: 50, first_entry: 0, n_entries: 4, kind: 0, flags: 0, rules: 0, moments: 0 },
            PieceRec { start: 50, duration: 25, first_entry: 4, n_entries: 2, kind: 1, flags: PIECE_SCENE_CUT, rules: 1, moments: 0 },
        ],
        entries,
        classes: vec![ClassRec { name: 1, max_layer: 2, flags: 0, max_kbps: 0 }],
        plan: vec![vec![2, 0]],
        strings: vec!["v".into(), "tv".into(), "wai.video.int_motion".into(), "wai.video.int_refine".into(), "wai.video.av1".into()],
    };
    (ix, objects)
}

fn code(ix: &StageIndex) -> &'static str {
    match ix.check() {
        Ok(()) => "ok",
        Err(r) => r.code,
    }
}

#[test]
fn an_index_round_trips_and_checks() {
    let (ix, objects) = fixture();
    let b = ix.to_bytes();
    assert_eq!(b.len() % 8, 0);
    assert_eq!(b.len(), HEADER_LEN + 2 * PIECE_LEN + 6 * ENTRY_LEN + CLASS_LEN + 8 + 72);
    let back = StageIndex::parse(&b).unwrap();
    assert_eq!(back, ix);
    assert_eq!(back.to_bytes(), b);
    assert_eq!(code(&ix), "ok");
    assert_eq!(ix.digest(), digest_of(&b));
    assert_eq!(ix.leaves().len(), 6);
    assert_eq!(ix.stage_root(), crate::world::receipt::merkle_root(&ix.leaves()));
    for (k, o) in objects.iter().enumerate() {
        check_object(&ix, k, o).unwrap_or_else(|r| panic!("entry {k}: {r}"));
    }
}

/// A piece of floors alone, a floor-only component's (packaging §7.8): its floors carry
/// no derived_from, which they would only as the presentation of a base, and one that does
/// is refused; such a piece's kind is `none`, which nothing classifies.
#[test]
fn a_piece_of_floors_alone_presents_no_base() {
    let (mut ix, _) = fixture();
    ix.entries.remove(4);
    ix.pieces[1].n_entries = 1;
    assert_eq!(code(&ix), "floor-derived-from", "a floor with derived_from in a piece with no base");
    ix.entries[4].flags = 0;
    ix.pieces[1].kind = PIECE_KIND_NONE;
    assert_eq!(code(&ix), "ok");
    let back = StageIndex::parse(&ix.to_bytes()).unwrap();
    assert_eq!(back.pieces[1].kind, PIECE_KIND_NONE);
}

#[test]
fn the_plan_digest_is_over_the_starts() {
    let mut want = blake3::Hasher::new();
    want.update(b"wai:pack-plan\x01");
    want.update(&2u64.to_le_bytes());
    want.update(&0u64.to_le_bytes());
    want.update(&50u64.to_le_bytes());
    assert_eq!(plan_digest(&[0, 50]), *want.finalize().as_bytes());
    assert_ne!(plan_digest(&[0, 50]), plan_digest(&[0, 51]));
}

/// Test 6 (the index rules): each mutation is refused with its named reason.
#[test]
fn every_index_mutation_names_its_reason() {
    type Mutation = fn(&mut StageIndex);
    let cases: &[(&str, Mutation)] = &[
        // Layer 2 refines the floor, a later entry of its piece.
        ("refines-mismatch", |ix| ix.entries[2].refines = 3),
        // Layer 2 refines the base: a layer skipped.
        ("layer-gap", |ix| ix.entries[2].refines = 0),
        ("refines-outside-window", |ix| ix.entries[2].refines = 99),
        ("tier-entropy-passthrough", |ix| {
            ix.strings[2] = "wai.image.jpegai".into();
            ix.entries[0].prefix_tier = TIER_ENTROPY;
            ix.entries[0].prefix_digest = [0; 32];
            ix.entries[4].prefix_tier = TIER_ENTROPY;
            ix.entries[4].prefix_digest = [0; 32];
            ix.entries[1].prefix_tier = TIER_ENTROPY;
            ix.entries[1].prefix_digest = [0; 32];
        }),
        ("tier-weakest", |ix| {
            ix.strings[2] = "wai.image.jpegai".into();
            ix.entries[0].prefix_tier = TIER_ENTROPY;
            ix.entries[0].prefix_digest = [0; 32];
            ix.entries[4].prefix_tier = TIER_ENTROPY;
            ix.entries[4].prefix_digest = [0; 32];
        }),
        ("tier-mismatch", |ix| {
            ix.entries[3].prefix_tier = TIER_DECODE;
            ix.entries[3].prefix_digest = [7; 32];
        }),
        ("digest-without-tier", |ix| ix.entries[3].prefix_digest = [9; 32]),
        ("dup-flag", |ix| ix.entries[4].flags = 0),
        ("dup-flag", |ix| ix.entries[5].flags |= ENTRY_DUP),
        ("dup-length", |ix| ix.entries[4].byte_len += 1),
        ("layer-role", |ix| ix.entries[3].layer = 1),
        ("layer-role", |ix| ix.entries[0].refines = 1),
        ("entry-order", |ix| ix.entries.swap(2, 3)),
        ("deadline-role", |ix| ix.entries[0].deadline_ms = 5),
        ("floor-derived-from", |ix| ix.entries[3].flags = 0),
        ("entry-flags", |ix| ix.entries[0].flags |= 0x80),
        ("piece-order", |ix| ix.pieces[1].start = 51),
        ("piece-order", |ix| ix.pieces[0].duration = 0),
        ("piece-entries", |ix| ix.pieces[1].first_entry = 3),
        ("piece-entries", |ix| ix.entries[5].piece = 0),
        ("piece-kind", |ix| ix.pieces[0].kind = 9),
        ("piece-kind", |ix| ix.pieces[0].kind = PIECE_KIND_NONE + 1),
        ("class-plan", |ix| ix.plan[0][1] = 1),
        ("class-plan", |ix| ix.plan[0][0] = 3),
        ("class-plan", |ix| {
            ix.classes[0].flags = CLASS_FLOOR_ONLY;
        }),
        ("class-flags", |ix| ix.classes[0].flags = 2),
        ("index-timebase", |ix| ix.header.tb_den = 0),
        // An on-demand index (no previous window) opens at piece 0, entry 0, time 0.
        ("window-start", |ix| ix.header.first_piece = 1),
        ("window-start", |ix| ix.header.first_entry = 1),
        ("window-start", |ix| {
            for p in ix.pieces.iter_mut() {
                p.start += 50;
            }
        }),
    ];
    for (want, m) in cases {
        let (mut ix, _) = fixture();
        m(&mut ix);
        // Through the bytes, as a verifier reads the file.
        let back = StageIndex::parse(&ix.to_bytes()).unwrap_or_else(|r| panic!("{want}: the mutation does not parse: {r}"));
        assert_eq!(code(&back), *want, "{want}");
    }
}

/// A file that is not an index's layout is refused before any rule is applied.
#[test]
fn a_malformed_file_is_refused_with_its_reason() {
    let (ix, _) = fixture();
    let b = ix.to_bytes();
    // Every strict prefix is refused: one too short to hold the magic has none; any
    // other is truncated (the header, a table, a padding).
    for n in 0..b.len() {
        let r = StageIndex::parse(&b[..n]).unwrap_err();
        assert_eq!(r.code, if n < 4 { "index-magic" } else { "index-truncated" }, "prefix {n}: {r}");
    }
    // The refusals come in §6.2's order: a reserved byte before a media class, a media
    // class before a role.
    let mut two = b.clone();
    two[6] = 7;
    two[150] = 1;
    assert_eq!(StageIndex::parse(&two).unwrap_err().code, "index-reserved");
    let mut two = b.clone();
    two[6] = 7;
    two[HEADER_LEN + 2 * PIECE_LEN + 54] = 7;
    assert_eq!(StageIndex::parse(&two).unwrap_err().code, "index-media-class");
    let mut t = b.clone();
    t.extend_from_slice(&[0u8; 8]);
    assert_eq!(StageIndex::parse(&t).unwrap_err().code, "index-trailing");
    let mutate = |at: usize, v: u8| {
        let mut m = b.clone();
        m[at] = v;
        StageIndex::parse(&m).map(|_| "ok").unwrap_or_else(|r| r.code)
    };
    assert_eq!(mutate(0, b'X'), "index-magic");
    assert_eq!(mutate(4, 2), "index-version");
    assert_eq!(mutate(6, 7), "index-media-class");
    assert_eq!(mutate(150, 1), "index-reserved");
    assert_eq!(mutate(HEADER_LEN + 28, 1), "index-reserved");
    let e0 = HEADER_LEN + 2 * PIECE_LEN;
    assert_eq!(mutate(e0 + 54, 7), "index-role");
    assert_eq!(mutate(e0 + 56, 3), "index-tier");
    assert_eq!(mutate(e0 + 58, 1), "index-reserved");
    assert_eq!(mutate(e0 + 52, 40), "index-string");
    assert_eq!(mutate(144, 9), "index-string");
    // A count the file cannot hold is refused before anything is sized from it.
    let mut big = b.clone();
    big[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
    assert_eq!(StageIndex::parse(&big).unwrap_err().code, "index-truncated");
}

/// Test 6 (the object rules): an entry naming a file that is not a WAI envelope (a
/// fragmented MP4 file), and an object that is not what its entry says.
#[test]
fn every_object_mutation_names_its_reason() {
    let (ix, objects) = fixture();
    let cmaf = b"\x00\x00\x00\x18ftypcmfc\x00\x00\x00\x00cmfciso6".to_vec();
    let r = |k: usize, bytes: &[u8], f: &dyn Fn(&mut StageIndex)| {
        let mut ix = ix.clone();
        f(&mut ix);
        check_object(&ix, k, bytes).map(|_| "ok").unwrap_or_else(|r| r.code)
    };
    assert_eq!(r(3, &cmaf, &|ix| {
        ix.entries[3].content_hash = crate::jwp_hash::content_hash(&cmaf);
        ix.entries[3].byte_len = cmaf.len() as u64;
    }), "non-envelope-entry");
    assert_eq!(r(0, &objects[0], &|_| {}), "ok");
    assert_eq!(r(0, &objects[1], &|_| {}), "object-length");
    let mut flipped = objects[0].clone();
    *flipped.last_mut().unwrap() ^= 1;
    assert_eq!(r(0, &flipped, &|_| {}), "object-hash");
    let mut trailing = objects[0].clone();
    trailing.push(0);
    assert_eq!(r(0, &trailing, &|ix| {
        ix.entries[0].byte_len += 1;
        ix.entries[0].content_hash = crate::jwp_hash::content_hash(&trailing);
    }), "non-envelope-entry");
    assert_eq!(r(0, &objects[0], &|ix| ix.entries[0].payload_len += 1), "object-lengths");
    assert_eq!(r(0, &objects[0], &|ix| ix.entries[0].capability = 4), "object-capability");
    // Layer 2's refines names the base and layer 1; read under layer 1's entry, its
    // producer is not the entry below.
    assert_eq!(r(2, &objects[2], &|ix| ix.entries[2].refines = 0), "object-refines");
    assert_eq!(r(1, &objects[2], &|ix| {
        ix.entries[1].content_hash = ix.entries[2].content_hash;
        ix.entries[1].byte_len = ix.entries[2].byte_len;
        ix.entries[1].manifest_len = ix.entries[2].manifest_len;
        ix.entries[1].payload_len = ix.entries[2].payload_len;
    }), "object-refines");
    assert_eq!(r(3, &objects[3], &|ix| ix.entries[3].flags = 0), "object-derived-from");
    assert_eq!(r(0, &objects[0], &|ix| ix.entries[0].flags = ENTRY_DERIVED_FROM), "object-derived-from");
}

#[test]
fn a_capability_s_tier_is_its_registry_class_s() {
    assert_eq!(capability_prefix_tier("wai.video.int_motion"), TIER_DECODE);
    assert_eq!(capability_prefix_tier("wai.image.png"), TIER_DECODE);
    assert_eq!(capability_prefix_tier("wai.video.int_refine"), TIER_DECODE);
    assert_eq!(capability_prefix_tier("wai.image.jpegai"), TIER_ENTROPY);
    assert_eq!(capability_prefix_tier("wai.video.av1"), TIER_NONE);
    assert_eq!(capability_prefix_tier("wai.video.nope"), TIER_NONE);
}
