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

/// Byte length of the base chunk header shared by all entries: `tag` + `size`.
pub const TMB_ITEM_BASE_HEADER_SIZE: usize = 8;

/// Byte length of a chunk header that includes an `id` field.
pub const TMB_ITEM_WITH_ID_HEADER_SIZE: usize = 10;

/// Byte length of a chunk header that includes both `id` and `time` fields.
pub const TMB_ITEM_WITH_TIME_HEADER_SIZE: usize = 12;

pub const C006_ENTRY_SIZE: usize = 0x18;

pub const C010_ENTRY_SIZE: usize = 0x28;

pub const C014_ENTRY_SIZE: usize = 0x1C;

pub const C063_ENTRY_SIZE: usize = 0x20;

pub const C131_ENTRY_SIZE: usize = 0x14;

/// A typed action entry from a `.tmb` file.
///
/// Known `C0xx` tags decode to their respective typed variants. Any other
/// `C*` tag becomes [`TmbEntry::Opaque`], preserving the raw payload for
/// forward compatibility.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum TmbEntry<'a> {
    /// Visibility control entry (`C006`).
    C006(C006Entry),
    /// Animation clip reference entry (`C010`).
    C010(C010Entry),
    /// Object visibility/control entry (`C014`).
    C014(C014Entry),
    /// Sound playback entry (`C063`).
    C063(C063Entry),
    /// Enabled-flag pair entry (`C131`).
    C131(C131Entry),
    /// An unrecognized `C*` entry, surfaced as raw bytes.
    Opaque {
        tag: [u8; 4],
        id: i16,
        time: i16,
        bytes: &'a [u8],
    },
}

impl<'a> TmbEntry<'a> {
    pub(crate) fn parse(tag: [u8; 4], body: &'a [u8], start_offset: usize) -> Result<Self> {
        if body.len() < TMB_ITEM_WITH_TIME_HEADER_SIZE {
            return Err(Error::InvalidTmbEntryLength {
                tag,
                entry_size: u32::try_from(body.len()).unwrap_or(u32::MAX),
                expected_or_remaining: TMB_ITEM_WITH_TIME_HEADER_SIZE as u64,
            });
        }
        let id = i16::from_le_bytes([body[8], body[9]]);
        let time = i16::from_le_bytes([body[10], body[11]]);

        match &tag {
            b"C006" => parse_c006(body, id, time, start_offset),
            b"C010" => parse_c010(body, id, time, start_offset),
            b"C014" => parse_c014(body, id, time, start_offset),
            b"C063" => parse_c063(body, id, time, start_offset),
            b"C131" => parse_c131(body, id, time, start_offset),
            _ => Ok(TmbEntry::Opaque {
                tag,
                id,
                time,
                bytes: &body[TMB_ITEM_WITH_TIME_HEADER_SIZE..],
            }),
        }
    }

    /// Four-byte tag for this entry.
    #[must_use]
    pub fn tag(&self) -> [u8; 4] {
        match self {
            TmbEntry::C006(_) => *b"C006",
            TmbEntry::C010(_) => *b"C010",
            TmbEntry::C014(_) => *b"C014",
            TmbEntry::C063(_) => *b"C063",
            TmbEntry::C131(_) => *b"C131",
            TmbEntry::Opaque { tag, .. } => *tag,
        }
    }

    /// Entry identifier, uniform across all variants.
    #[must_use]
    pub fn id(&self) -> i16 {
        match self {
            TmbEntry::C006(e) => e.id,
            TmbEntry::C010(e) => e.id,
            TmbEntry::C014(e) => e.id,
            TmbEntry::C063(e) => e.id,
            TmbEntry::C131(e) => e.id,
            TmbEntry::Opaque { id, .. } => *id,
        }
    }

    /// Start time in ticks, uniform across all variants.
    #[must_use]
    pub fn time(&self) -> i16 {
        match self {
            TmbEntry::C006(e) => e.time,
            TmbEntry::C010(e) => e.time,
            TmbEntry::C014(e) => e.time,
            TmbEntry::C063(e) => e.time,
            TmbEntry::C131(e) => e.time,
            TmbEntry::Opaque { time, .. } => *time,
        }
    }

    /// Byte offset of this entry's first byte within the input slice.
    ///
    /// Returns 0 for [`TmbEntry::Opaque`] entries.
    #[must_use]
    pub fn start_offset(&self) -> usize {
        match self {
            TmbEntry::C006(e) => e.start_offset,
            TmbEntry::C010(e) => e.start_offset,
            TmbEntry::C014(e) => e.start_offset,
            TmbEntry::C063(e) => e.start_offset,
            TmbEntry::C131(e) => e.start_offset,
            TmbEntry::Opaque { .. } => 0,
        }
    }

    /// Returns `true` if this entry is an unrecognized [`TmbEntry::Opaque`] variant.
    #[must_use]
    pub fn is_opaque(&self) -> bool {
        matches!(self, TmbEntry::Opaque { .. })
    }
}

fn require_size(tag: [u8; 4], found: usize, expected: usize) -> Result<()> {
    if found == expected {
        Ok(())
    } else {
        Err(Error::InvalidTmbEntryLength {
            tag,
            entry_size: u32::try_from(found).unwrap_or(u32::MAX),
            expected_or_remaining: expected as u64,
        })
    }
}

fn parse_c006(body: &[u8], id: i16, time: i16, start_offset: usize) -> Result<TmbEntry<'_>> {
    require_size(*b"C006", body.len(), C006_ENTRY_SIZE)?;
    let enabled = 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 unknown_3 = i32::from_le_bytes([body[20], body[21], body[22], body[23]]);
    Ok(TmbEntry::C006(C006Entry {
        id,
        time,
        enabled,
        unknown_2,
        unknown_3,
        start_offset,
    }))
}

fn parse_c010(body: &[u8], id: i16, time: i16, start_offset: usize) -> Result<TmbEntry<'_>> {
    require_size(*b"C010", body.len(), C010_ENTRY_SIZE)?;
    let duration = i32::from_le_bytes([body[12], body[13], body[14], body[15]]);
    let unknown_1 = i32::from_le_bytes([body[16], body[17], body[18], body[19]]);
    let flags = u32::from_le_bytes([body[20], body[21], body[22], body[23]]);
    let animation_start = f32::from_le_bytes([body[24], body[25], body[26], body[27]]);
    let animation_end = f32::from_le_bytes([body[28], body[29], body[30], body[31]]);
    let path_offset = u32::from_le_bytes([body[32], body[33], body[34], body[35]]);
    let unknown_2 = i32::from_le_bytes([body[36], body[37], body[38], body[39]]);
    Ok(TmbEntry::C010(C010Entry {
        id,
        time,
        duration,
        unknown_1,
        flags,
        animation_start,
        animation_end,
        path_offset,
        unknown_2,
        start_offset,
    }))
}

fn parse_c014(body: &[u8], id: i16, time: i16, start_offset: usize) -> Result<TmbEntry<'_>> {
    require_size(*b"C014", body.len(), C014_ENTRY_SIZE)?;
    let enabled = 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 object_position = i32::from_le_bytes([body[20], body[21], body[22], body[23]]);
    let object_control = i32::from_le_bytes([body[24], body[25], body[26], body[27]]);
    Ok(TmbEntry::C014(C014Entry {
        id,
        time,
        enabled,
        unknown_2,
        object_position,
        object_control,
        start_offset,
    }))
}

fn parse_c063(body: &[u8], id: i16, time: i16, start_offset: usize) -> Result<TmbEntry<'_>> {
    require_size(*b"C063", body.len(), C063_ENTRY_SIZE)?;
    let loop_count = i32::from_le_bytes([body[12], body[13], body[14], body[15]]);
    let interrupt = i32::from_le_bytes([body[16], body[17], body[18], body[19]]);
    let path_offset = u32::from_le_bytes([body[20], body[21], body[22], body[23]]);
    let sound_index = i32::from_le_bytes([body[24], body[25], body[26], body[27]]);
    let sound_position = body[28];
    let bind_id = body[29];
    let unknown_1 = i16::from_le_bytes([body[30], body[31]]);
    Ok(TmbEntry::C063(C063Entry {
        id,
        time,
        loop_count,
        interrupt,
        path_offset,
        sound_index,
        sound_position,
        bind_id,
        unknown_1,
        start_offset,
    }))
}

fn parse_c131(body: &[u8], id: i16, time: i16, start_offset: usize) -> Result<TmbEntry<'_>> {
    require_size(*b"C131", body.len(), C131_ENTRY_SIZE)?;
    let enabled = 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]]);
    Ok(TmbEntry::C131(C131Entry {
        id,
        time,
        enabled,
        unknown_2,
        start_offset,
    }))
}

/// Visibility control timeline entry (`C006`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct C006Entry {
    id: i16,
    time: i16,
    enabled: i32,
    unknown_2: i32,
    unknown_3: i32,
    start_offset: usize,
}

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

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

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

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

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

    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

/// Animation clip reference entry (`C010`).
///
/// References an animation path string and specifies playback range and flags.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct C010Entry {
    id: i16,
    time: i16,
    duration: i32,
    unknown_1: i32,
    flags: u32,
    animation_start: f32,
    animation_end: f32,
    path_offset: u32,
    unknown_2: i32,
    start_offset: usize,
}

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

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

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

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

    #[must_use]
    pub fn flags(&self) -> u32 {
        self.flags
    }

    #[must_use]
    pub fn animation_start(&self) -> f32 {
        self.animation_start
    }

    #[must_use]
    pub fn animation_end(&self) -> f32 {
        self.animation_end
    }

    #[must_use]
    pub fn path_offset(&self) -> u32 {
        self.path_offset
    }

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

    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

/// Object visibility/control entry (`C014`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct C014Entry {
    id: i16,
    time: i16,
    enabled: i32,
    unknown_2: i32,
    object_position: i32,
    object_control: i32,
    start_offset: usize,
}

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

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

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

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

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

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

    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

/// Sound playback entry (`C063`).
///
/// References a sound asset path and specifies loop count, interrupt behavior,
/// and audio channel binding.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct C063Entry {
    id: i16,
    time: i16,
    loop_count: i32,
    interrupt: i32,
    path_offset: u32,
    sound_index: i32,
    sound_position: u8,
    bind_id: u8,
    unknown_1: i16,
    start_offset: usize,
}

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

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

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

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

    #[must_use]
    pub fn path_offset(&self) -> u32 {
        self.path_offset
    }

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

    #[must_use]
    pub fn sound_position(&self) -> u8 {
        self.sound_position
    }

    #[must_use]
    pub fn bind_id(&self) -> u8 {
        self.bind_id
    }

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

    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

/// Enabled-flag pair entry (`C131`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct C131Entry {
    id: i16,
    time: i16,
    enabled: i32,
    unknown_2: i32,
    start_offset: usize,
}

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

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

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

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

    #[must_use]
    pub fn start_offset(&self) -> usize {
        self.start_offset
    }
}

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

    fn c006_bytes(id: i16, time: i16, enabled: i32, u2: i32, u3: i32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(C006_ENTRY_SIZE);
        buf.extend_from_slice(b"C006");
        buf.extend_from_slice(
            &u32::try_from(C006_ENTRY_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(&enabled.to_le_bytes());
        buf.extend_from_slice(&u2.to_le_bytes());
        buf.extend_from_slice(&u3.to_le_bytes());
        buf
    }

    #[allow(clippy::too_many_arguments)]
    fn c010_bytes(
        id: i16,
        time: i16,
        duration: i32,
        unknown_1: i32,
        flags: u32,
        animation_start: f32,
        animation_end: f32,
        path_offset: u32,
        unknown_2: i32,
    ) -> Vec<u8> {
        let mut buf = Vec::with_capacity(C010_ENTRY_SIZE);
        buf.extend_from_slice(b"C010");
        buf.extend_from_slice(
            &u32::try_from(C010_ENTRY_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(&duration.to_le_bytes());
        buf.extend_from_slice(&unknown_1.to_le_bytes());
        buf.extend_from_slice(&flags.to_le_bytes());
        buf.extend_from_slice(&animation_start.to_le_bytes());
        buf.extend_from_slice(&animation_end.to_le_bytes());
        buf.extend_from_slice(&path_offset.to_le_bytes());
        buf.extend_from_slice(&unknown_2.to_le_bytes());
        buf
    }

    fn c014_bytes(id: i16, time: i16, enabled: i32, u2: i32, pos: i32, ctrl: i32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(C014_ENTRY_SIZE);
        buf.extend_from_slice(b"C014");
        buf.extend_from_slice(
            &u32::try_from(C014_ENTRY_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(&enabled.to_le_bytes());
        buf.extend_from_slice(&u2.to_le_bytes());
        buf.extend_from_slice(&pos.to_le_bytes());
        buf.extend_from_slice(&ctrl.to_le_bytes());
        buf
    }

    #[allow(clippy::too_many_arguments)]
    fn c063_bytes(
        id: i16,
        time: i16,
        loop_count: i32,
        interrupt: i32,
        path_offset: u32,
        sound_index: i32,
        sound_position: u8,
        bind_id: u8,
        unknown_1: i16,
    ) -> Vec<u8> {
        let mut buf = Vec::with_capacity(C063_ENTRY_SIZE);
        buf.extend_from_slice(b"C063");
        buf.extend_from_slice(
            &u32::try_from(C063_ENTRY_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(&loop_count.to_le_bytes());
        buf.extend_from_slice(&interrupt.to_le_bytes());
        buf.extend_from_slice(&path_offset.to_le_bytes());
        buf.extend_from_slice(&sound_index.to_le_bytes());
        buf.push(sound_position);
        buf.push(bind_id);
        buf.extend_from_slice(&unknown_1.to_le_bytes());
        buf
    }

    fn c131_bytes(id: i16, time: i16, enabled: i32, u2: i32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(C131_ENTRY_SIZE);
        buf.extend_from_slice(b"C131");
        buf.extend_from_slice(
            &u32::try_from(C131_ENTRY_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(&enabled.to_le_bytes());
        buf.extend_from_slice(&u2.to_le_bytes());
        buf
    }

    #[test]
    fn c006_round_trip_all_fields() {
        let bytes = c006_bytes(10, 20, 1, 0x4242, -1);
        let entry = TmbEntry::parse(*b"C006", &bytes, 0x100).expect("parse");
        match entry {
            TmbEntry::C006(c) => {
                assert_eq!(c.id(), 10);
                assert_eq!(c.time(), 20);
                assert_eq!(c.enabled(), 1);
                assert_eq!(c.unknown_2(), 0x4242);
                assert_eq!(c.unknown_3(), -1);
                assert_eq!(c.start_offset(), 0x100);
            }
            other => panic!("expected C006, got {other:?}"),
        }
    }

    #[test]
    fn c010_round_trip_all_fields() {
        let bytes = c010_bytes(7, 30, 50, 1, 0x0F, 2.5, 7.25, 0x40, -99);
        let entry = TmbEntry::parse(*b"C010", &bytes, 0x200).expect("parse");
        match entry {
            TmbEntry::C010(c) => {
                assert_eq!(c.id(), 7);
                assert_eq!(c.time(), 30);
                assert_eq!(c.duration(), 50);
                assert_eq!(c.unknown_1(), 1);
                assert_eq!(c.flags(), 0x0F);
                assert!((c.animation_start() - 2.5).abs() < f32::EPSILON);
                assert!((c.animation_end() - 7.25).abs() < f32::EPSILON);
                assert_eq!(c.path_offset(), 0x40);
                assert_eq!(c.unknown_2(), -99);
                assert_eq!(c.start_offset(), 0x200);
            }
            other => panic!("expected C010, got {other:?}"),
        }
    }

    #[test]
    fn c014_round_trip_all_fields() {
        let bytes = c014_bytes(3, 8, 1, 2, 3, 4);
        let entry = TmbEntry::parse(*b"C014", &bytes, 0x300).expect("parse");
        match entry {
            TmbEntry::C014(c) => {
                assert_eq!(c.id(), 3);
                assert_eq!(c.time(), 8);
                assert_eq!(c.enabled(), 1);
                assert_eq!(c.unknown_2(), 2);
                assert_eq!(c.object_position(), 3);
                assert_eq!(c.object_control(), 4);
                assert_eq!(c.start_offset(), 0x300);
            }
            other => panic!("expected C014, got {other:?}"),
        }
    }

    #[test]
    fn c063_round_trip_all_fields() {
        let bytes = c063_bytes(11, 15, 1, 0, 0x60, 5, 1, 2, 42);
        let entry = TmbEntry::parse(*b"C063", &bytes, 0x400).expect("parse");
        match entry {
            TmbEntry::C063(c) => {
                assert_eq!(c.id(), 11);
                assert_eq!(c.time(), 15);
                assert_eq!(c.loop_count(), 1);
                assert_eq!(c.interrupt(), 0);
                assert_eq!(c.path_offset(), 0x60);
                assert_eq!(c.sound_index(), 5);
                assert_eq!(c.sound_position(), 1);
                assert_eq!(c.bind_id(), 2);
                assert_eq!(c.unknown_1(), 42);
                assert_eq!(c.start_offset(), 0x400);
            }
            other => panic!("expected C063, got {other:?}"),
        }
    }

    #[test]
    fn c131_round_trip_all_fields() {
        let bytes = c131_bytes(1, 0, 1, 7);
        let entry = TmbEntry::parse(*b"C131", &bytes, 0x500).expect("parse");
        match entry {
            TmbEntry::C131(c) => {
                assert_eq!(c.id(), 1);
                assert_eq!(c.time(), 0);
                assert_eq!(c.enabled(), 1);
                assert_eq!(c.unknown_2(), 7);
                assert_eq!(c.start_offset(), 0x500);
            }
            other => panic!("expected C131, got {other:?}"),
        }
    }

    #[test]
    fn unknown_c0xx_entry_surfaces_as_opaque() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(b"C999");
        bytes.extend_from_slice(&20u32.to_le_bytes());
        bytes.extend_from_slice(&5i16.to_le_bytes());
        bytes.extend_from_slice(&9i16.to_le_bytes());
        bytes.extend_from_slice(&[0xAA; 8]);
        let entry = TmbEntry::parse(*b"C999", &bytes, 0).expect("parse");
        match entry {
            TmbEntry::Opaque {
                tag,
                id,
                time,
                bytes: payload,
            } => {
                assert_eq!(tag, *b"C999");
                assert_eq!(id, 5);
                assert_eq!(time, 9);
                assert_eq!(payload.len(), 8);
                assert!(payload.iter().all(|&b| b == 0xAA));
            }
            other => panic!("expected Opaque, got {other:?}"),
        }
    }

    #[test]
    fn opaque_with_no_payload_returns_empty_slice() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(b"C888");
        bytes.extend_from_slice(&12u32.to_le_bytes());
        bytes.extend_from_slice(&0i16.to_le_bytes());
        bytes.extend_from_slice(&0i16.to_le_bytes());
        let entry = TmbEntry::parse(*b"C888", &bytes, 0).expect("parse");
        match entry {
            TmbEntry::Opaque { bytes, .. } => assert_eq!(bytes.len(), 0),
            other => panic!("expected Opaque, got {other:?}"),
        }
    }

    #[test]
    fn rejects_c006_with_wrong_size() {
        let mut bytes = c006_bytes(0, 0, 0, 0, 0);
        bytes.extend_from_slice(&[0u8; 4]);
        bytes[4..8].copy_from_slice(&28u32.to_le_bytes());
        let err = TmbEntry::parse(*b"C006", &bytes, 0).unwrap_err();
        match err {
            Error::InvalidTmbEntryLength { tag, .. } => assert_eq!(tag, *b"C006"),
            other => panic!("expected InvalidTmbEntryLength, got {other:?}"),
        }
    }

    #[test]
    fn rejects_entry_with_short_prologue() {
        let bytes = vec![0u8; 8];
        let err = TmbEntry::parse(*b"C006", &bytes, 0).unwrap_err();
        assert!(matches!(err, Error::InvalidTmbEntryLength { .. }));
    }

    #[test]
    fn is_opaque_reporting() {
        let typed = c006_bytes(0, 0, 0, 0, 0);
        let typed_entry = TmbEntry::parse(*b"C006", &typed, 0).expect("parse");
        assert!(!typed_entry.is_opaque());

        let mut opaque = Vec::new();
        opaque.extend_from_slice(b"CZZZ");
        opaque.extend_from_slice(&12u32.to_le_bytes());
        opaque.extend_from_slice(&[0u8; 4]);
        let opaque_entry = TmbEntry::parse(*b"CZZZ", &opaque, 0).expect("parse");
        assert!(opaque_entry.is_opaque());
    }

    #[test]
    fn tag_id_time_match_for_each_variant() {
        let cases: &[(Vec<u8>, [u8; 4], i16, i16)] = &[
            (c006_bytes(1, 2, 0, 0, 0), *b"C006", 1, 2),
            (c010_bytes(3, 4, 0, 0, 0, 0.0, 0.0, 0, 0), *b"C010", 3, 4),
            (c014_bytes(5, 6, 0, 0, 0, 0), *b"C014", 5, 6),
            (c063_bytes(7, 8, 0, 0, 0, 0, 0, 0, 0), *b"C063", 7, 8),
            (c131_bytes(9, 10, 0, 0), *b"C131", 9, 10),
        ];
        for (bytes, tag, want_id, want_time) in cases {
            let entry = TmbEntry::parse(*tag, bytes, 0).expect("parse");
            assert_eq!(entry.tag(), *tag);
            assert_eq!(entry.id(), *want_id, "id mismatch for {tag:?}");
            assert_eq!(entry.time(), *want_time, "time mismatch for {tag:?}");
        }
    }

    #[test]
    fn each_known_c0xx_rejects_wrong_size_with_right_tag() {
        for tag in [*b"C006", *b"C010", *b"C014", *b"C063", *b"C131"] {
            let mut bytes = Vec::with_capacity(16);
            bytes.extend_from_slice(&tag);
            bytes.extend_from_slice(&16u32.to_le_bytes());
            bytes.extend_from_slice(&[0u8; 8]);
            let err = TmbEntry::parse(tag, &bytes, 0).unwrap_err();
            match err {
                Error::InvalidTmbEntryLength { tag: got_tag, .. } => assert_eq!(got_tag, tag),
                other => panic!("expected InvalidTmbEntryLength for {tag:?}, got {other:?}"),
            }
        }
    }

    #[test]
    fn opaque_payload_borrows_into_input() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(b"C777");
        bytes.extend_from_slice(&20u32.to_le_bytes());
        bytes.extend_from_slice(&0i16.to_le_bytes());
        bytes.extend_from_slice(&0i16.to_le_bytes());
        bytes.extend_from_slice(&[0xBB; 8]);
        let entry = TmbEntry::parse(*b"C777", &bytes, 0).expect("parse");
        match entry {
            TmbEntry::Opaque { bytes: payload, .. } => {
                let payload_ptr = payload.as_ptr().addr();
                let input_ptr = bytes.as_ptr().addr();
                assert_eq!(payload_ptr - input_ptr, 12);
            }
            _ => unreachable!(),
        }
    }

    #[test]
    fn start_offset_is_zero_for_opaque() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(b"C666");
        bytes.extend_from_slice(&12u32.to_le_bytes());
        bytes.extend_from_slice(&[0u8; 4]);
        let entry = TmbEntry::parse(*b"C666", &bytes, 0x12345).expect("parse");
        assert_eq!(entry.start_offset(), 0);
    }

    #[test]
    fn header_size_constants_are_consistent() {
        assert_eq!(TMB_ITEM_BASE_HEADER_SIZE, 8);
        assert_eq!(TMB_ITEM_WITH_ID_HEADER_SIZE, 10);
        assert_eq!(TMB_ITEM_WITH_TIME_HEADER_SIZE, 12);
        assert_eq!(C006_ENTRY_SIZE, 0x18);
        assert_eq!(C010_ENTRY_SIZE, 0x28);
        assert_eq!(C014_ENTRY_SIZE, 0x1C);
        assert_eq!(C063_ENTRY_SIZE, 0x20);
        assert_eq!(C131_ENTRY_SIZE, 0x14);
    }

    #[test]
    fn c010_negative_float_round_trip() {
        let bytes = c010_bytes(0, 0, 0, 0, 0, -0.0, f32::MIN, 0, 0);
        let entry = TmbEntry::parse(*b"C010", &bytes, 0).expect("parse");
        match entry {
            TmbEntry::C010(c) => {
                assert!(c.animation_start().is_sign_negative());
                assert_eq!(c.animation_end().to_bits(), f32::MIN.to_bits());
            }
            _ => unreachable!(),
        }
    }

    #[test]
    fn error_display_invalid_tmb_entry_length() {
        let err = Error::InvalidTmbEntryLength {
            tag: *b"C006",
            entry_size: 16,
            expected_or_remaining: 24,
        };
        let msg = format!("{err}");
        assert!(msg.contains("16"));
        assert!(msg.contains("24"));
        assert!(msg.contains("expected_or_remaining"));
    }

    #[test]
    fn error_display_invalid_tmb_entry_count() {
        let err = Error::InvalidTmbEntryCount {
            tag: *b"TMTR",
            count: 99,
            max: 64,
        };
        let msg = format!("{err}");
        assert!(msg.contains("99"));
        assert!(msg.contains("64"));
    }

    #[test]
    fn error_display_invalid_utf8_in_tmb_string() {
        let err = Error::InvalidUtf8InTmbString {
            context: "tmpp.path",
            offset: 256,
        };
        let msg = format!("{err}");
        assert!(msg.contains("tmpp.path"));
        assert!(msg.contains("256"));
    }
}
