use std::io::Cursor;

use binrw::BinRead;
use tracing::{debug, info, info_span, trace, warn};

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

use super::chunks::TmbChunk;

/// Four-byte file magic: `b"TMLB"`.
pub const TMB_MAGIC: [u8; 4] = *b"TMLB";

/// Byte length of the fixed TMB file header (`magic` + `file_size` + `chunk_count`).
pub const TMB_HEADER_SIZE: usize = 12;

/// Maximum number of chunks accepted in a single file.
pub const TMB_MAX_CHUNKS: u32 = 65_536;

/// Maximum payload size accepted for opaque (`C*`) and unknown-tag chunks.
pub const TMB_MAX_OPAQUE_PAYLOAD: u32 = 4 * 1024 * 1024;

#[derive(Debug, BinRead)]
#[brw(little, magic = b"TMLB")]
struct WireTmbHeader {
    file_size: u32,
    chunk_count: u32,
}

/// A parsed `.tmb` timeline binary file.
///
/// Borrows directly from the caller-supplied byte slice. The chunk walk is
/// capped by the `file_size` field in the header, so trailing bytes beyond
/// `file_size` are ignored. Use [`Tmb::chunks`] to iterate all chunks.
#[derive(Debug, Clone)]
pub struct Tmb<'a> {
    file_size: u32,
    chunk_count: u32,
    chunks: Vec<TmbChunk<'a>>,
    bytes: &'a [u8],
}

impl<'a> Tmb<'a> {
    /// Parses a `.tmb` file from a byte slice.
    ///
    /// Validates the `TMLB` magic, checks `file_size` against the input
    /// length, enforces the chunk count cap, and walks all chunks within
    /// `file_size` bytes.
    ///
    /// # Panics
    ///
    /// Panics if `bytes.len()` cannot be represented as a `u64`, which cannot
    /// occur on any supported 64-bit target.
    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
        let span = info_span!(span_names::TMB_PARSE, bytes = bytes.len());
        let _enter = span.enter();
        let start = std::time::Instant::now();

        if bytes.len() < TMB_HEADER_SIZE {
            return Err(Error::InvalidTmbLength {
                found: bytes.len(),
                minimum: TMB_HEADER_SIZE,
            });
        }

        let mut found_magic = [0u8; 4];
        found_magic.copy_from_slice(&bytes[..4]);
        if found_magic != TMB_MAGIC {
            return Err(Error::InvalidTmbMagic { found: found_magic });
        }

        let mut cur = Cursor::new(&bytes[..TMB_HEADER_SIZE]);
        let hdr = WireTmbHeader::read(&mut cur).map_err(|e| Error::decode("tmb.header", &e))?;

        let bytes_len_u64 = u64::try_from(bytes.len()).expect("usize fits u64 on 64-bit targets");
        if u64::from(hdr.file_size) > bytes_len_u64 {
            return Err(Error::InvalidTmbFileSize {
                declared: hdr.file_size,
                found: bytes_len_u64,
            });
        }

        if hdr.chunk_count > TMB_MAX_CHUNKS {
            return Err(Error::InvalidTmbChunkCount {
                count: hdr.chunk_count,
                max: TMB_MAX_CHUNKS,
            });
        }

        let walk_end = usize::try_from(hdr.file_size).expect("file_size <= bytes.len() fits usize");
        let (chunks, tally) = walk_chunks(bytes, hdr.chunk_count, walk_end)?;

        debug!(
            target: "tmb",
            chunk_count = hdr.chunk_count,
            known_chunks = tally.known_chunks,
            opaque_chunks = tally.opaque_chunks,
            entries = tally.entries,
            opaque_entries = tally.opaque_entries,
            "tmb.chunks walked",
        );

        let elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
        info!(
            target: "tmb",
            bytes = bytes.len(),
            file_size = hdr.file_size,
            chunk_count = hdr.chunk_count,
            known_chunks = tally.known_chunks,
            opaque_chunks = tally.opaque_chunks,
            entries = tally.entries,
            opaque_entries = tally.opaque_entries,
            elapsed_ms,
            "tmb.parse",
        );

        Ok(Self {
            file_size: hdr.file_size,
            chunk_count: hdr.chunk_count,
            chunks,
            bytes,
        })
    }

    /// The `file_size` field from the header; chunk walking stops at this offset.
    #[must_use]
    pub fn file_size(&self) -> u32 {
        self.file_size
    }

    /// Number of chunks declared in the header.
    #[must_use]
    pub fn chunk_count(&self) -> u32 {
        self.chunk_count
    }

    /// All parsed chunks in file order.
    #[must_use]
    pub fn chunks(&self) -> &[TmbChunk<'a>] {
        &self.chunks
    }

    /// Returns the entire input byte slice this `Tmb` was parsed from.
    #[must_use]
    pub fn raw(&self) -> &'a [u8] {
        self.bytes
    }
}

#[derive(Debug, Clone, Copy, Default)]
struct ChunkTally {
    known_chunks: u32,
    opaque_chunks: u32,
    entries: u32,
    opaque_entries: u32,
}

fn walk_chunks<'a>(
    bytes: &'a [u8],
    chunk_count: u32,
    walk_end: usize,
) -> Result<(Vec<TmbChunk<'a>>, ChunkTally)> {
    let mut chunks: Vec<TmbChunk<'a>> =
        Vec::with_capacity(usize::try_from(chunk_count).unwrap_or(0).min(64));
    let mut tally = ChunkTally::default();
    let mut cursor: usize = TMB_HEADER_SIZE;
    let walk_end_u64 = u64::try_from(walk_end).expect("usize fits u64 on 64-bit targets");
    for chunk_index in 0..chunk_count {
        let (chunk, next_cursor, tag, chunk_size) =
            walk_one_chunk(bytes, cursor, walk_end, walk_end_u64)?;
        tally_chunk(&chunk, tag, chunk_size, chunk_index, &mut tally);
        trace!(
            target: "tmb",
            ?tag,
            chunk_index,
            chunk_size,
            chunk_start = cursor,
            "tmb.chunk",
        );
        chunks.push(chunk);
        cursor = next_cursor;
    }
    Ok((chunks, tally))
}

fn walk_one_chunk(
    bytes: &[u8],
    cursor: usize,
    walk_end: usize,
    walk_end_u64: u64,
) -> Result<(TmbChunk<'_>, usize, [u8; 4], u32)> {
    if cursor.checked_add(8).is_none_or(|end| end > walk_end) {
        return Err(Error::InvalidTmbLength {
            found: walk_end,
            minimum: cursor.saturating_add(8),
        });
    }
    let mut tag = [0u8; 4];
    tag.copy_from_slice(&bytes[cursor..cursor + 4]);
    let chunk_size = u32::from_le_bytes([
        bytes[cursor + 4],
        bytes[cursor + 5],
        bytes[cursor + 6],
        bytes[cursor + 7],
    ]);
    let chunk_size_usize =
        usize::try_from(chunk_size).map_err(|_| Error::InvalidTmbChunkLength {
            tag,
            chunk_size,
            file_size: walk_end_u64,
        })?;
    if chunk_size_usize < 8 {
        return Err(Error::InvalidTmbChunkLength {
            tag,
            chunk_size,
            file_size: walk_end_u64,
        });
    }
    let Some(chunk_end) = cursor.checked_add(chunk_size_usize) else {
        return Err(Error::InvalidTmbChunkLength {
            tag,
            chunk_size,
            file_size: walk_end_u64,
        });
    };
    if chunk_end > walk_end {
        return Err(Error::InvalidTmbChunkLength {
            tag,
            chunk_size,
            file_size: walk_end_u64,
        });
    }
    let is_known_chunk_tag = matches!(&tag, b"TMDH" | b"TMPP" | b"TMAL" | b"TMAC" | b"TMTR");
    let is_typed_entry = matches!(&tag, b"C006" | b"C010" | b"C014" | b"C063" | b"C131");
    if !is_known_chunk_tag && !is_typed_entry && chunk_size > TMB_MAX_OPAQUE_PAYLOAD {
        return Err(Error::InvalidTmbChunkLength {
            tag,
            chunk_size,
            file_size: walk_end_u64,
        });
    }
    let body = &bytes[cursor..chunk_end];
    let chunk = TmbChunk::parse(tag, body, cursor)?;
    Ok((chunk, chunk_end, tag, chunk_size))
}

fn tally_chunk(
    chunk: &TmbChunk<'_>,
    tag: [u8; 4],
    chunk_size: u32,
    chunk_index: u32,
    tally: &mut ChunkTally,
) {
    match chunk {
        TmbChunk::Tmdh(_)
        | TmbChunk::Tmpp(_)
        | TmbChunk::Tmal(_)
        | TmbChunk::Tmac(_)
        | TmbChunk::Tmtr(_) => {
            tally.known_chunks += 1;
        }
        TmbChunk::Entry(entry) => {
            tally.entries += 1;
            if entry.is_opaque() {
                tally.opaque_entries += 1;
            }
        }
        TmbChunk::Opaque { .. } => {
            tally.opaque_chunks += 1;
            warn!(
                target: "tmb",
                ?tag,
                chunk_size,
                chunk_index,
                "unknown TMB chunk tag — surfaced as Opaque for forward-compat",
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tmb::entries::TmbEntry;

    fn build_empty_tmb() -> Vec<u8> {
        let mut buf = Vec::with_capacity(TMB_HEADER_SIZE);
        buf.extend_from_slice(&TMB_MAGIC);
        buf.extend_from_slice(
            &u32::try_from(TMB_HEADER_SIZE)
                .expect("size fits u32")
                .to_le_bytes(),
        );
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf
    }

    fn build_with_tmdh() -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(&TMB_MAGIC);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());

        buf.extend_from_slice(b"TMDH");
        buf.extend_from_slice(&16u32.to_le_bytes());
        buf.extend_from_slice(&7i16.to_le_bytes());
        buf.extend_from_slice(&100i16.to_le_bytes());
        buf.extend_from_slice(&50i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());

        let total = u32::try_from(buf.len()).expect("fits");
        buf[4..8].copy_from_slice(&total.to_le_bytes());
        buf
    }

    fn build_with_opaque() -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(&TMB_MAGIC);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(b"ZZZZ");
        buf.extend_from_slice(&16u32.to_le_bytes());
        buf.extend_from_slice(&[0xAA; 8]);
        let total = u32::try_from(buf.len()).expect("fits");
        buf[4..8].copy_from_slice(&total.to_le_bytes());
        buf
    }

    #[test]
    fn parses_empty_tmb() {
        let bytes = build_empty_tmb();
        let tmb = Tmb::parse(&bytes).expect("parse");
        assert_eq!(
            tmb.file_size(),
            u32::try_from(TMB_HEADER_SIZE).expect("12 fits u32")
        );
        assert_eq!(tmb.chunk_count(), 0);
        assert!(tmb.chunks().is_empty());
    }

    #[test]
    fn parses_tmdh_chunk() {
        let bytes = build_with_tmdh();
        let tmb = Tmb::parse(&bytes).expect("parse");
        assert_eq!(tmb.chunk_count(), 1);
        assert_eq!(tmb.chunks().len(), 1);
        match &tmb.chunks()[0] {
            TmbChunk::Tmdh(tmdh) => {
                assert_eq!(tmdh.id(), 7);
                assert_eq!(tmdh.unknown_1(), 100);
                assert_eq!(tmdh.length(), 50);
                assert_eq!(tmdh.unknown_3(), 0);
            }
            other => panic!("expected Tmdh, got {other:?}"),
        }
    }

    #[test]
    fn unknown_chunk_surfaces_as_opaque() {
        let bytes = build_with_opaque();
        let tmb = Tmb::parse(&bytes).expect("parse");
        assert_eq!(tmb.chunks().len(), 1);
        match &tmb.chunks()[0] {
            TmbChunk::Opaque {
                tag,
                bytes,
                start_offset,
            } => {
                assert_eq!(tag, b"ZZZZ");
                assert_eq!(bytes.len(), 16);
                assert_eq!(*start_offset, TMB_HEADER_SIZE);
            }
            other => panic!("expected Opaque chunk, got {other:?}"),
        }
    }

    #[test]
    fn rejects_empty_input() {
        let err = Tmb::parse(&[]).unwrap_err();
        match err {
            Error::InvalidTmbLength { found, minimum } => {
                assert_eq!(found, 0);
                assert_eq!(minimum, TMB_HEADER_SIZE);
            }
            other => panic!("expected InvalidTmbLength, got {other:?}"),
        }
    }

    #[test]
    fn rejects_bad_magic() {
        let mut bytes = vec![0xDEu8, 0xAD, 0xBE, 0xEF];
        bytes.extend_from_slice(&12u32.to_le_bytes());
        bytes.extend_from_slice(&0u32.to_le_bytes());
        let err = Tmb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidTmbMagic { found } => assert_eq!(found, [0xDE, 0xAD, 0xBE, 0xEF]),
            other => panic!("expected InvalidTmbMagic, got {other:?}"),
        }
    }

    #[test]
    fn rejects_chunk_count_over_cap() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&TMB_MAGIC);
        bytes.extend_from_slice(&12u32.to_le_bytes());
        bytes.extend_from_slice(&(TMB_MAX_CHUNKS + 1).to_le_bytes());
        let err = Tmb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidTmbChunkCount { count, max } => {
                assert_eq!(count, TMB_MAX_CHUNKS + 1);
                assert_eq!(max, TMB_MAX_CHUNKS);
            }
            other => panic!("expected InvalidTmbChunkCount, got {other:?}"),
        }
    }

    #[test]
    fn rejects_chunk_size_below_header() {
        let declared_file_size = 20u32;
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&TMB_MAGIC);
        bytes.extend_from_slice(&declared_file_size.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(b"TMDH");
        bytes.extend_from_slice(&4u32.to_le_bytes());
        bytes.extend_from_slice(&[0u8; 8]);
        let err = Tmb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidTmbChunkLength {
                tag,
                chunk_size,
                file_size,
            } => {
                assert_eq!(tag, *b"TMDH");
                assert_eq!(chunk_size, 4);
                assert_eq!(file_size, u64::from(declared_file_size));
            }
            other => panic!("expected InvalidTmbChunkLength, got {other:?}"),
        }
    }

    #[test]
    fn rejects_chunk_size_past_eof() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&TMB_MAGIC);
        bytes.extend_from_slice(&20u32.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(b"TMDH");
        bytes.extend_from_slice(&999u32.to_le_bytes());
        let err = Tmb::parse(&bytes).unwrap_err();
        assert!(matches!(err, Error::InvalidTmbChunkLength { .. }));
    }

    #[test]
    fn rejects_file_size_past_input() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&TMB_MAGIC);
        bytes.extend_from_slice(&u32::MAX.to_le_bytes());
        bytes.extend_from_slice(&0u32.to_le_bytes());
        let err = Tmb::parse(&bytes).unwrap_err();
        assert!(matches!(err, Error::InvalidTmbFileSize { .. }));
    }

    #[test]
    fn raw_borrows_entire_input() {
        let bytes = build_with_tmdh();
        let tmb = Tmb::parse(&bytes).expect("parse");
        assert_eq!(tmb.raw().as_ptr(), bytes.as_ptr());
        assert_eq!(tmb.raw().len(), bytes.len());
    }

    #[test]
    fn opaque_chunk_borrows_into_input() {
        let bytes = build_with_opaque();
        let tmb = Tmb::parse(&bytes).expect("parse");
        match &tmb.chunks()[0] {
            TmbChunk::Opaque {
                bytes: opaque,
                start_offset,
                ..
            } => {
                let opaque_ptr = opaque.as_ptr().addr();
                let bytes_ptr = bytes.as_ptr().addr();
                assert!(opaque_ptr >= bytes_ptr);
                assert!(opaque_ptr + opaque.len() <= bytes_ptr + bytes.len());
                assert_eq!(*start_offset, TMB_HEADER_SIZE);
            }
            _ => unreachable!(),
        }
    }

    #[test]
    fn rejects_truncated_chunk_header() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&TMB_MAGIC);
        bytes.extend_from_slice(&16u32.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(b"TMDH");
        let err = Tmb::parse(&bytes).unwrap_err();
        assert!(matches!(err, Error::InvalidTmbLength { .. }));
    }

    #[test]
    fn rejects_opaque_chunk_over_payload_cap() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&TMB_MAGIC);
        bytes.extend_from_slice(&20u32.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(b"ZZZZ");
        bytes.extend_from_slice(&(TMB_MAX_OPAQUE_PAYLOAD + 1).to_le_bytes());
        let err = Tmb::parse(&bytes).unwrap_err();
        assert!(matches!(err, Error::InvalidTmbChunkLength { .. }));
    }

    #[test]
    fn rejects_opaque_entry_over_payload_cap() {
        let inflated_size = TMB_MAX_OPAQUE_PAYLOAD + 1;
        let total_size = TMB_HEADER_SIZE + (inflated_size as usize);
        let mut bytes = Vec::with_capacity(total_size);
        bytes.extend_from_slice(&TMB_MAGIC);
        bytes.extend_from_slice(
            &u32::try_from(total_size)
                .expect("test buffer size fits u32")
                .to_le_bytes(),
        );
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(b"C999");
        bytes.extend_from_slice(&inflated_size.to_le_bytes());
        bytes.resize(total_size, 0xAA);
        let err = Tmb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidTmbChunkLength {
                tag, chunk_size, ..
            } => {
                assert_eq!(tag, *b"C999");
                assert_eq!(chunk_size, inflated_size);
            }
            other => panic!("expected InvalidTmbChunkLength, got {other:?}"),
        }
    }

    #[test]
    fn rejects_file_size_exceeding_input() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&TMB_MAGIC);
        let declared =
            u32::try_from(bytes.len() + TMB_HEADER_SIZE + 1).expect("test buffer size fits u32");
        bytes.extend_from_slice(&declared.to_le_bytes());
        bytes.extend_from_slice(&0u32.to_le_bytes());
        let err = Tmb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidTmbFileSize { declared: d, found } => {
                assert_eq!(d, declared);
                assert_eq!(found, bytes.len() as u64);
            }
            other => panic!("expected InvalidTmbFileSize, got {other:?}"),
        }
    }

    #[test]
    fn chunks_past_file_size_are_unreachable() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&TMB_MAGIC);
        let file_size = TMB_HEADER_SIZE + 16;
        buf.extend_from_slice(
            &u32::try_from(file_size)
                .expect("size fits u32")
                .to_le_bytes(),
        );
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(b"TMDH");
        buf.extend_from_slice(&16u32.to_le_bytes());
        buf.extend_from_slice(&1i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());
        buf.extend_from_slice(&10i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());
        buf.extend_from_slice(b"TMDH");
        buf.extend_from_slice(&16u32.to_le_bytes());
        buf.extend_from_slice(&2i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());
        buf.extend_from_slice(&20i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());

        let tmb = Tmb::parse(&buf).expect("parse");
        assert_eq!(tmb.chunks().len(), 1);
        assert_eq!(tmb.raw().len(), buf.len());
    }

    #[test]
    fn error_display_invalid_tmb_file_size_contains_fields() {
        let err = Error::InvalidTmbFileSize {
            declared: 999,
            found: 32,
        };
        let msg = format!("{err}");
        assert!(msg.contains("999"));
        assert!(msg.contains("32"));
    }

    #[test]
    fn error_display_invalid_tmb_length_contains_both_fields() {
        let err = Error::InvalidTmbLength {
            found: 5,
            minimum: TMB_HEADER_SIZE,
        };
        let msg = format!("{err}");
        assert!(msg.contains('5'));
        assert!(msg.contains(&TMB_HEADER_SIZE.to_string()));
    }

    #[test]
    fn error_display_invalid_tmb_magic_contains_bytes() {
        let err = Error::InvalidTmbMagic {
            found: [0xDE, 0xAD, 0xBE, 0xEF],
        };
        let msg = format!("{err}");
        assert!(msg.contains("0xde") || msg.contains("0xDE") || msg.contains("222"));
    }

    #[test]
    fn error_display_invalid_tmb_chunk_count_contains_values() {
        let err = Error::InvalidTmbChunkCount {
            count: 99_999,
            max: TMB_MAX_CHUNKS,
        };
        let msg = format!("{err}");
        assert!(msg.contains("99999"));
        assert!(msg.contains(&TMB_MAX_CHUNKS.to_string()));
    }

    #[test]
    fn error_display_invalid_tmb_chunk_length_contains_fields() {
        let err = Error::InvalidTmbChunkLength {
            tag: *b"TMDH",
            chunk_size: 4,
            file_size: 32,
        };
        let msg = format!("{err}");
        assert!(msg.contains('4'));
        assert!(msg.contains("32"));
    }

    #[test]
    fn parses_mixed_chunks_in_order() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&TMB_MAGIC);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&3u32.to_le_bytes());
        buf.extend_from_slice(b"TMDH");
        buf.extend_from_slice(&16u32.to_le_bytes());
        buf.extend_from_slice(&1i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());
        buf.extend_from_slice(&10i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());
        buf.extend_from_slice(b"ZZZZ");
        buf.extend_from_slice(&12u32.to_le_bytes());
        buf.extend_from_slice(&[0u8; 4]);
        buf.extend_from_slice(b"TMDH");
        buf.extend_from_slice(&16u32.to_le_bytes());
        buf.extend_from_slice(&2i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());
        buf.extend_from_slice(&20i16.to_le_bytes());
        buf.extend_from_slice(&0i16.to_le_bytes());

        let total = u32::try_from(buf.len()).expect("fits");
        buf[4..8].copy_from_slice(&total.to_le_bytes());

        let tmb = Tmb::parse(&buf).expect("parse");
        assert_eq!(tmb.chunks().len(), 3);
        assert!(matches!(tmb.chunks()[0], TmbChunk::Tmdh(_)));
        assert!(matches!(tmb.chunks()[1], TmbChunk::Opaque { .. }));
        assert!(matches!(tmb.chunks()[2], TmbChunk::Tmdh(_)));
    }

    #[test]
    fn typed_chunk_borrows_into_input() {
        let bytes = build_with_tmdh();
        let tmb = Tmb::parse(&bytes).expect("parse");
        match &tmb.chunks()[0] {
            TmbChunk::Tmdh(tmdh) => {
                let off = tmdh.start_offset();
                assert_eq!(&bytes[off..off + 4], b"TMDH");
            }
            _ => unreachable!(),
        }
    }

    #[test]
    fn debug_clone_is_derived() {
        let bytes = build_empty_tmb();
        let tmb = Tmb::parse(&bytes).expect("parse");
        let cloned = tmb.clone();
        let debug = format!("{cloned:?}");
        assert!(debug.contains("Tmb"));
    }

    #[test]
    fn typed_entry_routes_through_full_parse_pipeline() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&TMB_MAGIC);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(b"C010");
        buf.extend_from_slice(&40u32.to_le_bytes());
        buf.extend_from_slice(&9i16.to_le_bytes());
        buf.extend_from_slice(&3i16.to_le_bytes());
        buf.extend_from_slice(&50i32.to_le_bytes());
        buf.extend_from_slice(&1i32.to_le_bytes());
        buf.extend_from_slice(&0x0Fu32.to_le_bytes());
        buf.extend_from_slice(&1.5f32.to_le_bytes());
        buf.extend_from_slice(&60.0f32.to_le_bytes());
        buf.extend_from_slice(&0x80u32.to_le_bytes());
        buf.extend_from_slice(&(-7i32).to_le_bytes());
        let total = u32::try_from(buf.len()).expect("fits");
        buf[4..8].copy_from_slice(&total.to_le_bytes());

        let tmb = Tmb::parse(&buf).expect("parse");
        assert_eq!(tmb.chunks().len(), 1);
        match &tmb.chunks()[0] {
            TmbChunk::Entry(entry) => match entry {
                TmbEntry::C010(c) => {
                    assert_eq!(c.id(), 9);
                    assert_eq!(c.time(), 3);
                    assert_eq!(c.duration(), 50);
                    assert_eq!(c.unknown_1(), 1);
                    assert_eq!(c.flags(), 0x0F);
                    assert!((c.animation_start() - 1.5).abs() < f32::EPSILON);
                    assert!((c.animation_end() - 60.0).abs() < f32::EPSILON);
                    assert_eq!(c.path_offset(), 0x80);
                    assert_eq!(c.unknown_2(), -7);
                }
                other => panic!("expected TmbEntry::C010, got {other:?}"),
            },
            other => panic!("expected TmbChunk::Entry, got {other:?}"),
        }
    }

    #[test]
    fn opaque_entry_bytes_borrow_into_input_after_full_parse() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&TMB_MAGIC);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(b"C999");
        buf.extend_from_slice(&20u32.to_le_bytes());
        buf.extend_from_slice(&11i16.to_le_bytes());
        buf.extend_from_slice(&13i16.to_le_bytes());
        buf.extend_from_slice(&[0xDD; 8]);
        let total = u32::try_from(buf.len()).expect("fits");
        buf[4..8].copy_from_slice(&total.to_le_bytes());

        let tmb = Tmb::parse(&buf).expect("parse");
        match &tmb.chunks()[0] {
            TmbChunk::Entry(TmbEntry::Opaque { bytes: payload, .. }) => {
                let payload_ptr = payload.as_ptr().addr();
                let input_ptr = buf.as_ptr().addr();
                assert!(
                    payload_ptr >= input_ptr,
                    "opaque entry payload starts before input buffer",
                );
                assert!(
                    payload_ptr + payload.len() <= input_ptr + buf.len(),
                    "opaque entry payload extends past input buffer end",
                );
                assert!(
                    payload.iter().all(|&b| b == 0xDD),
                    "opaque entry payload was not byte-for-byte from input",
                );
            }
            other => panic!("expected TmbChunk::Entry(TmbEntry::Opaque), got {other:?}"),
        }
    }

    #[test]
    fn rejects_chunk_count_u32_max() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&TMB_MAGIC);
        bytes.extend_from_slice(&12u32.to_le_bytes());
        bytes.extend_from_slice(&u32::MAX.to_le_bytes());
        let err = Tmb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidTmbChunkCount { count, max } => {
                assert_eq!(count, u32::MAX);
                assert_eq!(max, TMB_MAX_CHUNKS);
            }
            other => panic!("expected InvalidTmbChunkCount, got {other:?}"),
        }
    }

    #[test]
    fn entry_opaque_dispatch_counts_correctly() {
        let mut buf = Vec::new();
        buf.extend_from_slice(&TMB_MAGIC);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(b"C999");
        buf.extend_from_slice(&20u32.to_le_bytes());
        buf.extend_from_slice(&42i16.to_le_bytes());
        buf.extend_from_slice(&7i16.to_le_bytes());
        buf.extend_from_slice(&[0xCC; 8]);
        let total = u32::try_from(buf.len()).expect("fits");
        buf[4..8].copy_from_slice(&total.to_le_bytes());

        let tmb = Tmb::parse(&buf).expect("parse");
        assert_eq!(tmb.chunks().len(), 1);
        match &tmb.chunks()[0] {
            TmbChunk::Entry(entry) => match entry {
                TmbEntry::Opaque {
                    tag,
                    id,
                    time,
                    bytes,
                } => {
                    assert_eq!(tag, b"C999");
                    assert_eq!(*id, 42);
                    assert_eq!(*time, 7);
                    assert_eq!(bytes.len(), 8);
                }
                other => panic!("expected Opaque entry, got {other:?}"),
            },
            other => panic!("expected Entry, got {other:?}"),
        }
    }
}
