use crate::error::{Error, Result};

use super::entries::TmbEntry;

pub const TMDH_CHUNK_SIZE: usize = 16;

pub const TMPP_CHUNK_SIZE: usize = 12;

pub const TMAL_CHUNK_SIZE: usize = 16;

pub const TMAC_CHUNK_SIZE: usize = 28;

pub const TMTR_CHUNK_SIZE: usize = 24;

/// A single chunk from a `.tmb` file.
///
/// Known structural tags decode to typed variants; `C*` tags become
/// [`TmbChunk::Entry`]; everything else becomes [`TmbChunk::Opaque`]
/// for forward compatibility. Use [`TmbChunk::tag`] and
/// [`TmbChunk::start_offset`] for uniform access across all variants.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum TmbChunk<'a> {
    /// Timeline header (`TMDH`).
    Tmdh(TmdhChunk),
    /// Path pointer (`TMPP`).
    Tmpp(TmppChunk),
    /// Actor list (`TMAL`).
    Tmal(TmalChunk),
    /// Actor entry (`TMAC`).
    Tmac(TmacChunk),
    /// Track entry (`TMTR`).
    Tmtr(TmtrChunk),
    /// A typed action entry (tags starting with `C`).
    Entry(TmbEntry<'a>),
    /// An unrecognized chunk tag, surfaced as raw bytes for forward compat.
    Opaque {
        tag: [u8; 4],
        bytes: &'a [u8],
        start_offset: usize,
    },
}

impl<'a> TmbChunk<'a> {
    pub(crate) fn parse(tag: [u8; 4], body: &'a [u8], start_offset: usize) -> Result<Self> {
        match &tag {
            b"TMDH" => Ok(TmbChunk::Tmdh(TmdhChunk::parse(body, start_offset)?)),
            b"TMPP" => Ok(TmbChunk::Tmpp(TmppChunk::parse(body, start_offset)?)),
            b"TMAL" => Ok(TmbChunk::Tmal(TmalChunk::parse(body, start_offset)?)),
            b"TMAC" => Ok(TmbChunk::Tmac(TmacChunk::parse(body, start_offset)?)),
            b"TMTR" => Ok(TmbChunk::Tmtr(TmtrChunk::parse(body, start_offset)?)),
            _ if tag.first().is_some_and(|&b| b == b'C') => {
                Ok(TmbChunk::Entry(TmbEntry::parse(tag, body, start_offset)?))
            }
            _ => {
                debug_assert!(
                    body.len() >= 8,
                    "walk_one_chunk pre-validated chunk size >= 8",
                );
                Ok(TmbChunk::Opaque {
                    tag,
                    bytes: body,
                    start_offset,
                })
            }
        }
    }

    /// Byte offset of this chunk's first byte within the original input slice.
    #[must_use]
    pub fn start_offset(&self) -> usize {
        match self {
            TmbChunk::Tmdh(c) => c.start_offset,
            TmbChunk::Tmpp(c) => c.start_offset,
            TmbChunk::Tmal(c) => c.start_offset,
            TmbChunk::Tmac(c) => c.start_offset,
            TmbChunk::Tmtr(c) => c.start_offset,
            TmbChunk::Entry(e) => e.start_offset(),
            TmbChunk::Opaque { start_offset, .. } => *start_offset,
        }
    }

    /// Four-byte tag identifying this chunk type.
    #[must_use]
    pub fn tag(&self) -> [u8; 4] {
        match self {
            TmbChunk::Tmdh(_) => *b"TMDH",
            TmbChunk::Tmpp(_) => *b"TMPP",
            TmbChunk::Tmal(_) => *b"TMAL",
            TmbChunk::Tmac(_) => *b"TMAC",
            TmbChunk::Tmtr(_) => *b"TMTR",
            TmbChunk::Entry(entry) => entry.tag(),
            TmbChunk::Opaque { tag, .. } => *tag,
        }
    }
}

/// Timeline header chunk (`TMDH`).
///
/// Describes the overall duration and identity of the timeline.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TmdhChunk {
    id: i16,
    unknown_1: i16,
    length: i16,
    unknown_3: i16,
    start_offset: usize,
}

impl TmdhChunk {
    fn parse(body: &[u8], start_offset: usize) -> Result<Self> {
        if body.len() < TMDH_CHUNK_SIZE {
            return Err(Error::InvalidTmbEntryLength {
                tag: *b"TMDH",
                entry_size: u32::try_from(body.len()).unwrap_or(u32::MAX),
                expected_or_remaining: TMDH_CHUNK_SIZE as u64,
            });
        }
        let id = i16::from_le_bytes([body[8], body[9]]);
        let unknown_1 = i16::from_le_bytes([body[10], body[11]]);
        let length = i16::from_le_bytes([body[12], body[13]]);
        let unknown_3 = i16::from_le_bytes([body[14], body[15]]);
        Ok(Self {
            id,
            unknown_1,
            length,
            unknown_3,
            start_offset,
        })
    }

    /// Timeline identifier.
    #[must_use]
    pub fn id(&self) -> i16 {
        self.id
    }

    #[must_use]
    pub fn unknown_1(&self) -> i16 {
        self.unknown_1
    }

    /// Timeline duration in ticks.
    #[must_use]
    pub fn length(&self) -> i16 {
        self.length
    }

    #[must_use]
    pub fn unknown_3(&self) -> i16 {
        self.unknown_3
    }

    /// Byte offset of this chunk's first byte within the input slice.
    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

/// Path pointer chunk (`TMPP`): holds an offset into a path-string table.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TmppChunk {
    path_offset: u32,
    start_offset: usize,
}

impl TmppChunk {
    fn parse(body: &[u8], start_offset: usize) -> Result<Self> {
        if body.len() < TMPP_CHUNK_SIZE {
            return Err(Error::InvalidTmbEntryLength {
                tag: *b"TMPP",
                entry_size: u32::try_from(body.len()).unwrap_or(u32::MAX),
                expected_or_remaining: TMPP_CHUNK_SIZE as u64,
            });
        }
        let path_offset = u32::from_le_bytes([body[8], body[9], body[10], body[11]]);
        Ok(Self {
            path_offset,
            start_offset,
        })
    }

    /// Offset into the path-string table for this entry.
    #[must_use]
    pub fn path_offset(&self) -> u32 {
        self.path_offset
    }

    /// Byte offset of this chunk within the input slice.
    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

/// Actor list chunk (`TMAL`): an offset + count pointing to an array of actor IDs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TmalChunk {
    actor_ids_offset: u32,
    actor_ids_count: u32,
    start_offset: usize,
}

impl TmalChunk {
    fn parse(body: &[u8], start_offset: usize) -> Result<Self> {
        if body.len() < TMAL_CHUNK_SIZE {
            return Err(Error::InvalidTmbEntryLength {
                tag: *b"TMAL",
                entry_size: u32::try_from(body.len()).unwrap_or(u32::MAX),
                expected_or_remaining: TMAL_CHUNK_SIZE as u64,
            });
        }
        let actor_ids_offset = u32::from_le_bytes([body[8], body[9], body[10], body[11]]);
        let actor_ids_count = u32::from_le_bytes([body[12], body[13], body[14], body[15]]);
        Ok(Self {
            actor_ids_offset,
            actor_ids_count,
            start_offset,
        })
    }

    /// Offset of the actor ID array.
    #[must_use]
    pub fn actor_ids_offset(&self) -> u32 {
        self.actor_ids_offset
    }

    /// Number of actor IDs in the array.
    #[must_use]
    pub fn actor_ids_count(&self) -> u32 {
        self.actor_ids_count
    }

    /// Byte offset of this chunk within the input slice.
    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

/// Actor entry chunk (`TMAC`): describes a single actor's tracks and timing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TmacChunk {
    id: i16,
    time: i16,
    ability_delay: i32,
    unknown_2: i32,
    track_ids_offset: u32,
    track_ids_count: u32,
    start_offset: usize,
}

impl TmacChunk {
    fn parse(body: &[u8], start_offset: usize) -> Result<Self> {
        if body.len() < TMAC_CHUNK_SIZE {
            return Err(Error::InvalidTmbEntryLength {
                tag: *b"TMAC",
                entry_size: u32::try_from(body.len()).unwrap_or(u32::MAX),
                expected_or_remaining: TMAC_CHUNK_SIZE as u64,
            });
        }
        let id = i16::from_le_bytes([body[8], body[9]]);
        let time = i16::from_le_bytes([body[10], body[11]]);
        let ability_delay = i32::from_le_bytes([body[12], body[13], body[14], body[15]]);
        let unknown_2 = i32::from_le_bytes([body[16], body[17], body[18], body[19]]);
        let track_ids_offset = u32::from_le_bytes([body[20], body[21], body[22], body[23]]);
        let track_ids_count = u32::from_le_bytes([body[24], body[25], body[26], body[27]]);
        Ok(Self {
            id,
            time,
            ability_delay,
            unknown_2,
            track_ids_offset,
            track_ids_count,
            start_offset,
        })
    }

    /// Actor identifier.
    #[must_use]
    pub fn id(&self) -> i16 {
        self.id
    }

    /// Start time in ticks.
    #[must_use]
    pub fn time(&self) -> i16 {
        self.time
    }

    /// Ability activation delay in ticks.
    #[must_use]
    pub fn ability_delay(&self) -> i32 {
        self.ability_delay
    }

    #[must_use]
    pub fn unknown_2(&self) -> i32 {
        self.unknown_2
    }

    /// Offset of the track ID array.
    #[must_use]
    pub fn track_ids_offset(&self) -> u32 {
        self.track_ids_offset
    }

    /// Number of track IDs in the array.
    #[must_use]
    pub fn track_ids_count(&self) -> u32 {
        self.track_ids_count
    }

    /// Byte offset of this chunk within the input slice.
    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

/// Track chunk (`TMTR`): an ordered list of action entries for one actor track.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TmtrChunk {
    id: i16,
    time: i16,
    entry_ids_offset: u32,
    entry_ids_count: u32,
    lua_offset: u32,
    start_offset: usize,
}

impl TmtrChunk {
    fn parse(body: &[u8], start_offset: usize) -> Result<Self> {
        if body.len() < TMTR_CHUNK_SIZE {
            return Err(Error::InvalidTmbEntryLength {
                tag: *b"TMTR",
                entry_size: u32::try_from(body.len()).unwrap_or(u32::MAX),
                expected_or_remaining: TMTR_CHUNK_SIZE as u64,
            });
        }
        let id = i16::from_le_bytes([body[8], body[9]]);
        let time = i16::from_le_bytes([body[10], body[11]]);
        let entry_ids_offset = u32::from_le_bytes([body[12], body[13], body[14], body[15]]);
        let entry_ids_count = u32::from_le_bytes([body[16], body[17], body[18], body[19]]);
        let lua_offset = u32::from_le_bytes([body[20], body[21], body[22], body[23]]);
        Ok(Self {
            id,
            time,
            entry_ids_offset,
            entry_ids_count,
            lua_offset,
            start_offset,
        })
    }

    /// Track identifier.
    #[must_use]
    pub fn id(&self) -> i16 {
        self.id
    }

    /// Start time in ticks.
    #[must_use]
    pub fn time(&self) -> i16 {
        self.time
    }

    /// Offset of the entry ID array.
    #[must_use]
    pub fn entry_ids_offset(&self) -> u32 {
        self.entry_ids_offset
    }

    /// Number of entry IDs in the array.
    #[must_use]
    pub fn entry_ids_count(&self) -> u32 {
        self.entry_ids_count
    }

    /// Offset of the Lua condition script, or 0 if none.
    #[must_use]
    pub fn lua_offset(&self) -> u32 {
        self.lua_offset
    }

    /// Returns `true` if this track has an associated Lua condition.
    #[must_use]
    pub fn has_lua_condition(&self) -> bool {
        self.lua_offset != 0
    }

    /// Byte offset of this chunk within the input slice.
    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn tmdh_bytes(id: i16, u1: i16, length: i16, u3: i16) -> Vec<u8> {
        let mut buf = Vec::with_capacity(TMDH_CHUNK_SIZE);
        buf.extend_from_slice(b"TMDH");
        buf.extend_from_slice(
            &u32::try_from(TMDH_CHUNK_SIZE)
                .expect("size fits u32")
                .to_le_bytes(),
        );
        buf.extend_from_slice(&id.to_le_bytes());
        buf.extend_from_slice(&u1.to_le_bytes());
        buf.extend_from_slice(&length.to_le_bytes());
        buf.extend_from_slice(&u3.to_le_bytes());
        buf
    }

    fn tmpp_bytes(path_offset: u32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(TMPP_CHUNK_SIZE);
        buf.extend_from_slice(b"TMPP");
        buf.extend_from_slice(
            &u32::try_from(TMPP_CHUNK_SIZE)
                .expect("size fits u32")
                .to_le_bytes(),
        );
        buf.extend_from_slice(&path_offset.to_le_bytes());
        buf
    }

    fn tmal_bytes(offset: u32, count: u32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(TMAL_CHUNK_SIZE);
        buf.extend_from_slice(b"TMAL");
        buf.extend_from_slice(
            &u32::try_from(TMAL_CHUNK_SIZE)
                .expect("size fits u32")
                .to_le_bytes(),
        );
        buf.extend_from_slice(&offset.to_le_bytes());
        buf.extend_from_slice(&count.to_le_bytes());
        buf
    }

    fn tmac_bytes(
        id: i16,
        time: i16,
        ability_delay: i32,
        unknown_2: i32,
        track_offset: u32,
        track_count: u32,
    ) -> Vec<u8> {
        let mut buf = Vec::with_capacity(TMAC_CHUNK_SIZE);
        buf.extend_from_slice(b"TMAC");
        buf.extend_from_slice(
            &u32::try_from(TMAC_CHUNK_SIZE)
                .expect("size fits u32")
                .to_le_bytes(),
        );
        buf.extend_from_slice(&id.to_le_bytes());
        buf.extend_from_slice(&time.to_le_bytes());
        buf.extend_from_slice(&ability_delay.to_le_bytes());
        buf.extend_from_slice(&unknown_2.to_le_bytes());
        buf.extend_from_slice(&track_offset.to_le_bytes());
        buf.extend_from_slice(&track_count.to_le_bytes());
        buf
    }

    fn tmtr_bytes(
        id: i16,
        time: i16,
        entry_offset: u32,
        entry_count: u32,
        lua_offset: u32,
    ) -> Vec<u8> {
        let mut buf = Vec::with_capacity(TMTR_CHUNK_SIZE);
        buf.extend_from_slice(b"TMTR");
        buf.extend_from_slice(
            &u32::try_from(TMTR_CHUNK_SIZE)
                .expect("size fits u32")
                .to_le_bytes(),
        );
        buf.extend_from_slice(&id.to_le_bytes());
        buf.extend_from_slice(&time.to_le_bytes());
        buf.extend_from_slice(&entry_offset.to_le_bytes());
        buf.extend_from_slice(&entry_count.to_le_bytes());
        buf.extend_from_slice(&lua_offset.to_le_bytes());
        buf
    }

    #[test]
    fn tmdh_chunk_round_trip_all_fields() {
        let bytes = tmdh_bytes(1234, 5, 99, -7);
        let tmdh = TmdhChunk::parse(&bytes, 0x40).expect("parse");
        assert_eq!(tmdh.id(), 1234);
        assert_eq!(tmdh.unknown_1(), 5);
        assert_eq!(tmdh.length(), 99);
        assert_eq!(tmdh.unknown_3(), -7);
        assert_eq!(tmdh.start_offset(), 0x40);
    }

    #[test]
    fn tmpp_chunk_round_trip_all_fields() {
        let bytes = tmpp_bytes(0xCAFE_BABE);
        let tmpp = TmppChunk::parse(&bytes, 0x80).expect("parse");
        assert_eq!(tmpp.path_offset(), 0xCAFE_BABE);
        assert_eq!(tmpp.start_offset(), 0x80);
    }

    #[test]
    fn tmal_chunk_round_trip_all_fields() {
        let bytes = tmal_bytes(0x1234, 7);
        let tmal = TmalChunk::parse(&bytes, 0x100).expect("parse");
        assert_eq!(tmal.actor_ids_offset(), 0x1234);
        assert_eq!(tmal.actor_ids_count(), 7);
        assert_eq!(tmal.start_offset(), 0x100);
    }

    #[test]
    fn tmac_chunk_round_trip_all_fields() {
        let bytes = tmac_bytes(42, 5, 99, -3, 0xABCD, 12);
        let tmac = TmacChunk::parse(&bytes, 0x200).expect("parse");
        assert_eq!(tmac.id(), 42);
        assert_eq!(tmac.time(), 5);
        assert_eq!(tmac.ability_delay(), 99);
        assert_eq!(tmac.unknown_2(), -3);
        assert_eq!(tmac.track_ids_offset(), 0xABCD);
        assert_eq!(tmac.track_ids_count(), 12);
        assert_eq!(tmac.start_offset(), 0x200);
    }

    #[test]
    fn tmtr_chunk_round_trip_all_fields() {
        let bytes = tmtr_bytes(3, 100, 0x1000, 8, 0xDEAD);
        let tmtr = TmtrChunk::parse(&bytes, 0x400).expect("parse");
        assert_eq!(tmtr.id(), 3);
        assert_eq!(tmtr.time(), 100);
        assert_eq!(tmtr.entry_ids_offset(), 0x1000);
        assert_eq!(tmtr.entry_ids_count(), 8);
        assert_eq!(tmtr.lua_offset(), 0xDEAD);
        assert!(tmtr.has_lua_condition());
        assert_eq!(tmtr.start_offset(), 0x400);
    }

    #[test]
    fn tmtr_chunk_no_lua_when_offset_zero() {
        let bytes = tmtr_bytes(1, 0, 0, 0, 0);
        let tmtr = TmtrChunk::parse(&bytes, 0).expect("parse");
        assert!(!tmtr.has_lua_condition());
    }

    #[test]
    fn dispatch_routes_tmdh_to_typed_variant() {
        let bytes = tmdh_bytes(0, 0, 0, 0);
        let chunk = TmbChunk::parse(*b"TMDH", &bytes, 0).expect("parse");
        assert!(matches!(chunk, TmbChunk::Tmdh(_)));
        assert_eq!(chunk.tag(), *b"TMDH");
    }

    #[test]
    fn dispatch_routes_tmpp_to_typed_variant() {
        let bytes = tmpp_bytes(0);
        let chunk = TmbChunk::parse(*b"TMPP", &bytes, 0).expect("parse");
        assert!(matches!(chunk, TmbChunk::Tmpp(_)));
        assert_eq!(chunk.tag(), *b"TMPP");
    }

    #[test]
    fn dispatch_routes_tmal_to_typed_variant() {
        let bytes = tmal_bytes(0, 0);
        let chunk = TmbChunk::parse(*b"TMAL", &bytes, 0).expect("parse");
        assert!(matches!(chunk, TmbChunk::Tmal(_)));
    }

    #[test]
    fn dispatch_routes_tmac_to_typed_variant() {
        let bytes = tmac_bytes(0, 0, 0, 0, 0, 0);
        let chunk = TmbChunk::parse(*b"TMAC", &bytes, 0).expect("parse");
        assert!(matches!(chunk, TmbChunk::Tmac(_)));
    }

    #[test]
    fn dispatch_routes_tmtr_to_typed_variant() {
        let bytes = tmtr_bytes(0, 0, 0, 0, 0);
        let chunk = TmbChunk::parse(*b"TMTR", &bytes, 0).expect("parse");
        assert!(matches!(chunk, TmbChunk::Tmtr(_)));
    }

    #[test]
    fn dispatch_routes_unknown_tag_to_opaque() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(b"WXYZ");
        bytes.extend_from_slice(&12u32.to_le_bytes());
        bytes.extend_from_slice(&[0u8; 4]);
        let chunk = TmbChunk::parse(*b"WXYZ", &bytes, 0x123).expect("parse");
        match chunk {
            TmbChunk::Opaque {
                tag,
                bytes,
                start_offset,
            } => {
                assert_eq!(tag, *b"WXYZ");
                assert_eq!(bytes.len(), 12);
                assert_eq!(start_offset, 0x123);
            }
            other => panic!("expected Opaque, got {other:?}"),
        }
    }

    #[test]
    fn tmdh_rejects_short_body() {
        let bytes = vec![0u8; 8];
        let err = TmdhChunk::parse(&bytes, 0).unwrap_err();
        match err {
            Error::InvalidTmbEntryLength { tag, .. } => assert_eq!(tag, *b"TMDH"),
            other => panic!("expected InvalidTmbEntryLength, got {other:?}"),
        }
    }

    #[test]
    fn tmpp_rejects_short_body() {
        let bytes = vec![0u8; 8];
        assert!(matches!(
            TmppChunk::parse(&bytes, 0),
            Err(Error::InvalidTmbEntryLength { tag, .. }) if tag == *b"TMPP"
        ));
    }

    #[test]
    fn tmal_rejects_short_body() {
        let bytes = vec![0u8; 8];
        assert!(matches!(
            TmalChunk::parse(&bytes, 0),
            Err(Error::InvalidTmbEntryLength { tag, .. }) if tag == *b"TMAL"
        ));
    }

    #[test]
    fn tmac_rejects_short_body() {
        let bytes = vec![0u8; 8];
        assert!(matches!(
            TmacChunk::parse(&bytes, 0),
            Err(Error::InvalidTmbEntryLength { tag, .. }) if tag == *b"TMAC"
        ));
    }

    #[test]
    fn tmtr_rejects_short_body() {
        let bytes = vec![0u8; 8];
        assert!(matches!(
            TmtrChunk::parse(&bytes, 0),
            Err(Error::InvalidTmbEntryLength { tag, .. }) if tag == *b"TMTR"
        ));
    }

    #[test]
    fn start_offset_returns_stored_value_for_typed() {
        let chunks = [
            (tmdh_bytes(0, 0, 0, 0), *b"TMDH", 0x100usize),
            (tmpp_bytes(0), *b"TMPP", 0x200usize),
            (tmal_bytes(0, 0), *b"TMAL", 0x300usize),
            (tmac_bytes(0, 0, 0, 0, 0, 0), *b"TMAC", 0x400usize),
            (tmtr_bytes(0, 0, 0, 0, 0), *b"TMTR", 0x500usize),
        ];
        for (bytes, tag, expected_offset) in chunks {
            let chunk = TmbChunk::parse(tag, &bytes, expected_offset).expect("parse");
            assert_eq!(
                chunk.start_offset(),
                expected_offset,
                "start_offset mismatch for tag {tag:?}",
            );
        }
    }

    #[test]
    fn start_offset_returns_stored_value_for_opaque() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(b"ZZZZ");
        bytes.extend_from_slice(&12u32.to_le_bytes());
        bytes.extend_from_slice(&[0u8; 4]);
        let chunk = TmbChunk::parse(*b"ZZZZ", &bytes, 0x600).expect("parse");
        assert_eq!(chunk.start_offset(), 0x600);
    }

    #[test]
    fn tag_returns_correct_value_for_entry_and_opaque_variants() {
        use crate::tmb::entries::TmbEntry;

        let mut c131 = Vec::with_capacity(20);
        c131.extend_from_slice(b"C131");
        c131.extend_from_slice(&20u32.to_le_bytes());
        c131.extend_from_slice(&0i16.to_le_bytes());
        c131.extend_from_slice(&0i16.to_le_bytes());
        c131.extend_from_slice(&0i32.to_le_bytes());
        c131.extend_from_slice(&0i32.to_le_bytes());
        let entry_chunk = TmbChunk::parse(*b"C131", &c131, 0).expect("parse");
        assert!(
            matches!(entry_chunk, TmbChunk::Entry(TmbEntry::C131(_))),
            "expected TmbChunk::Entry(TmbEntry::C131), got {entry_chunk:?}",
        );
        assert_eq!(
            entry_chunk.tag(),
            *b"C131",
            "TmbChunk::Entry.tag() must delegate to TmbEntry::tag()",
        );

        let mut opaque = Vec::new();
        opaque.extend_from_slice(b"WXYZ");
        opaque.extend_from_slice(&12u32.to_le_bytes());
        opaque.extend_from_slice(&[0u8; 4]);
        let opaque_chunk = TmbChunk::parse(*b"WXYZ", &opaque, 0).expect("parse");
        assert!(
            matches!(opaque_chunk, TmbChunk::Opaque { .. }),
            "expected TmbChunk::Opaque, got {opaque_chunk:?}",
        );
        assert_eq!(
            opaque_chunk.tag(),
            *b"WXYZ",
            "TmbChunk::Opaque.tag() must return stored raw tag",
        );
    }
}
