use std::io::Cursor;

use binrw::BinRead;
use tracing::{info, info_span};

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

/// Byte length of the fixed `.pap` file header.
pub const PAP_HEADER_SIZE: usize = 26;

/// Byte length of a single [`PapAnimationInfo`] record.
pub const PAP_ANIMATION_INFO_SIZE: usize = 40;

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

/// The only supported `version` field value.
pub const PAP_VERSION_V1: u32 = 0x0002_0001;

/// Maximum number of animation info entries allowed in a single file.
pub const PAP_MAX_ANIMATION_COUNT: u32 = 4096;

#[derive(Debug, BinRead)]
#[brw(little)]
#[allow(dead_code)]
struct WirePapHeader {
    magic: [u8; 4],
    version: u32,
    animation_count: u16,
    model_id: u16,
    model_type: u8,
    variant: u8,
    info_offset: u32,
    havok_offset: u32,
    tmb_offset: u32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WirePapAnimationInfo {
    name: [u8; 32],
    animation_type: u16,
    havok_index: i16,
    face: u32,
}

fn read_and_validate_header(bytes: &[u8]) -> Result<WirePapHeader> {
    if bytes.len() < PAP_HEADER_SIZE {
        return Err(Error::InvalidPapLength {
            found: bytes.len(),
            minimum: PAP_HEADER_SIZE,
        });
    }

    let mut cursor = Cursor::new(bytes);
    let header = WirePapHeader::read(&mut cursor).map_err(|e| Error::decode("pap.header", &e))?;

    if header.magic != PAP_MAGIC {
        return Err(Error::InvalidPapMagic {
            found: header.magic,
        });
    }

    if header.version != PAP_VERSION_V1 {
        return Err(Error::UnsupportedPapVersion {
            version: header.version,
        });
    }

    let animation_count_u32 = u32::from(header.animation_count);
    if animation_count_u32 > PAP_MAX_ANIMATION_COUNT {
        return Err(Error::InvalidPapAnimationCount {
            count: animation_count_u32,
            max: PAP_MAX_ANIMATION_COUNT,
        });
    }

    Ok(header)
}

fn validate_offsets(header: &WirePapHeader, file_size: u64) -> Result<()> {
    let info_offset = u64::from(header.info_offset);
    let havok_offset = u64::from(header.havok_offset);
    let tmb_offset = u64::from(header.tmb_offset);

    for (field, offset) in [
        ("info_offset", info_offset),
        ("havok_offset", havok_offset),
        ("tmb_offset", tmb_offset),
    ] {
        if offset > file_size {
            return Err(Error::InvalidPapOffset {
                field,
                offset,
                file_size,
            });
        }
    }

    if info_offset > havok_offset {
        return Err(Error::InvalidPapOffsetOrdering {
            first_field: "info_offset",
            first_offset: info_offset,
            second_field: "havok_offset",
            second_offset: havok_offset,
        });
    }
    if havok_offset > tmb_offset {
        return Err(Error::InvalidPapOffsetOrdering {
            first_field: "havok_offset",
            first_offset: havok_offset,
            second_field: "tmb_offset",
            second_offset: tmb_offset,
        });
    }

    let count = u64::from(header.animation_count);
    let info_record_bytes =
        count
            .checked_mul(PAP_ANIMATION_INFO_SIZE as u64)
            .ok_or(Error::InvalidPapOffset {
                field: "info_block.end",
                offset: u64::MAX,
                file_size,
            })?;
    let info_end = info_offset
        .checked_add(info_record_bytes)
        .ok_or(Error::InvalidPapOffset {
            field: "info_block.end",
            offset: u64::MAX,
            file_size,
        })?;
    if info_end > file_size {
        return Err(Error::InvalidPapOffset {
            field: "info_block.end",
            offset: info_end,
            file_size,
        });
    }

    Ok(())
}

fn read_animation_info_block(
    bytes: &[u8],
    header: &WirePapHeader,
    animation_count: u32,
) -> Result<Vec<PapAnimationInfo>> {
    let mut cursor = Cursor::new(bytes);
    cursor.set_position(u64::from(header.info_offset));

    let capacity = usize::try_from(animation_count)
        .expect("animation_count is bounded by PAP_MAX_ANIMATION_COUNT");
    let mut animations = Vec::with_capacity(capacity);
    for _ in 0..animation_count {
        let record = WirePapAnimationInfo::read(&mut cursor)
            .map_err(|e| Error::decode("pap.animation_info", &e))?;
        animations.push(PapAnimationInfo {
            name: record.name,
            animation_type: record.animation_type,
            havok_index: record.havok_index,
            face: record.face != 0,
        });
    }
    Ok(animations)
}

/// Metadata for a single animation clip within a `.pap` file.
///
/// `name` is a null-padded UTF-8 string of up to 32 bytes.
/// `havok_index` is the clip index within the Havok binary payload.
/// `face` is true when the on-disk `face` field is non-zero.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PapAnimationInfo {
    /// Null-padded animation name, up to 32 bytes.
    pub name: [u8; 32],
    /// Animation type tag.
    pub animation_type: u16,
    /// Index of this clip within the embedded Havok binary stream.
    pub havok_index: i16,
    /// Whether this is a facial animation.
    pub face: bool,
}

/// A parsed `.pap` animation pack file.
///
/// Borrows directly from the caller-supplied byte slice. Use
/// [`Pap::havok_bytes`] for the Havok region and [`Pap::tmb_footer_bytes`]
/// for the TMB timeline data appended after the Havok payload.
#[derive(Debug, Clone)]
pub struct Pap<'a> {
    version: u32,
    animation_count: u16,
    model_id: u16,
    model_type: u8,
    variant: u8,
    info_offset: u32,
    havok_offset: u32,
    tmb_offset: u32,
    animations: Vec<PapAnimationInfo>,
    bytes: &'a [u8],
}

impl<'a> Pap<'a> {
    /// Parses a `.pap` file from a byte slice.
    ///
    /// Validates magic, version, animation count cap, and all three section
    /// offsets (`info_offset`, `havok_offset`, `tmb_offset`) for in-bounds and
    /// monotonically non-decreasing ordering.
    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
        let span = info_span!(span_names::PAP_PARSE, bytes = bytes.len());
        let _enter = span.enter();
        let start = std::time::Instant::now();

        let header = read_and_validate_header(bytes)?;
        let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
        validate_offsets(&header, file_size)?;

        let animation_count_u32 = u32::from(header.animation_count);
        let animations = read_animation_info_block(bytes, &header, animation_count_u32)?;

        let tmb_offset_u64 = u64::from(header.tmb_offset);
        let havok_len = tmb_offset_u64.saturating_sub(u64::from(header.havok_offset));
        let tmb_footer_len = file_size.saturating_sub(tmb_offset_u64);

        let elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
        info!(
            target: "pap",
            bytes = bytes.len(),
            version = header.version,
            header_size = PAP_HEADER_SIZE,
            animation_count = header.animation_count,
            info_offset = header.info_offset,
            havok_offset = header.havok_offset,
            havok_len,
            tmb_offset = header.tmb_offset,
            tmb_footer_len,
            elapsed_ms,
            "pap.parse",
        );

        Ok(Self {
            version: header.version,
            animation_count: header.animation_count,
            model_id: header.model_id,
            model_type: header.model_type,
            variant: header.variant,
            info_offset: header.info_offset,
            havok_offset: header.havok_offset,
            tmb_offset: header.tmb_offset,
            animations,
            bytes,
        })
    }

    /// Returns the raw version field; always [`PAP_VERSION_V1`] for parsed files.
    #[must_use]
    pub fn version(&self) -> u32 {
        self.version
    }

    /// Number of [`PapAnimationInfo`] entries in this file.
    #[must_use]
    pub fn animation_count(&self) -> u16 {
        self.animation_count
    }

    /// Model identifier from the header.
    #[must_use]
    pub fn model_id(&self) -> u16 {
        self.model_id
    }

    /// Model type from the header.
    #[must_use]
    pub fn model_type(&self) -> u8 {
        self.model_type
    }

    /// Model variant from the header.
    #[must_use]
    pub fn variant(&self) -> u8 {
        self.variant
    }

    /// Byte offset of the animation info block within the file.
    #[must_use]
    pub fn info_offset(&self) -> u32 {
        self.info_offset
    }

    /// Byte offset at which the Havok binary stream begins.
    #[must_use]
    pub fn havok_offset(&self) -> u32 {
        self.havok_offset
    }

    /// Byte offset at which the TMB footer begins.
    #[must_use]
    pub fn tmb_offset(&self) -> u32 {
        self.tmb_offset
    }

    /// Decoded animation metadata entries.
    #[must_use]
    pub fn animations(&self) -> &[PapAnimationInfo] {
        &self.animations
    }

    /// Zero-copy slice of the Havok binary payload: `bytes[havok_offset..tmb_offset]`.
    #[must_use]
    pub fn havok_bytes(&self) -> &'a [u8] {
        let start = usize::try_from(self.havok_offset).unwrap_or(usize::MAX);
        let end = usize::try_from(self.tmb_offset).unwrap_or(usize::MAX);
        debug_assert!(
            start <= end,
            "havok_offset <= tmb_offset is a parse invariant"
        );
        debug_assert!(
            end <= self.bytes.len(),
            "tmb_offset <= file_size is a parse invariant"
        );
        &self.bytes[start..end]
    }

    /// Zero-copy slice of the TMB footer: `bytes[tmb_offset..]`.
    ///
    /// Pass this directly to [`Tmb::parse`](crate::tmb::Tmb::parse) if you
    /// need to decode the embedded timeline data.
    #[must_use]
    pub fn tmb_footer_bytes(&self) -> &'a [u8] {
        let start = usize::try_from(self.tmb_offset).unwrap_or(usize::MAX);
        debug_assert!(
            start <= self.bytes.len(),
            "tmb_offset <= file_size is a parse invariant"
        );
        &self.bytes[start..]
    }

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

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

    const HAVOK_MAGIC_BYTES: [u8; 8] = [0x1E, 0x0D, 0xB0, 0xCA, 0xCE, 0xFA, 0x11, 0xD0];

    const TMLB_MAGIC: [u8; 4] = *b"TMLB";

    fn make_record(name: &[u8], animation_type: u16, havok_index: i16, face: u32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(PAP_ANIMATION_INFO_SIZE);
        let mut padded = [0u8; 32];
        let n = name.len().min(32);
        padded[..n].copy_from_slice(&name[..n]);
        buf.extend_from_slice(&padded);
        buf.extend_from_slice(&animation_type.to_le_bytes());
        buf.extend_from_slice(&havok_index.to_le_bytes());
        buf.extend_from_slice(&face.to_le_bytes());
        assert_eq!(buf.len(), PAP_ANIMATION_INFO_SIZE);
        buf
    }

    fn build_happy(animation_count: u16, havok_body: &[u8], tmb_body: &[u8]) -> Vec<u8> {
        let info_offset = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let info_size = u32::from(animation_count)
            * u32::try_from(PAP_ANIMATION_INFO_SIZE).expect("40 fits u32");
        let havok_offset = info_offset + info_size;
        let havok_size = u32::try_from(havok_body.len()).expect("body fits u32");
        let tmb_offset = havok_offset + havok_size;

        let mut buf = Vec::new();
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&animation_count.to_le_bytes());
        buf.extend_from_slice(&0x0101u16.to_le_bytes());
        buf.push(0x00);
        buf.push(0x01);
        buf.extend_from_slice(&info_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        buf.extend_from_slice(&tmb_offset.to_le_bytes());
        assert_eq!(buf.len(), PAP_HEADER_SIZE);

        for i in 0..animation_count {
            let havok_index = i16::try_from(i).unwrap_or(0);
            let rec = make_record(format!("anim_{i:02}").as_bytes(), 0x0001, havok_index, 0);
            buf.extend_from_slice(&rec);
        }
        buf.extend_from_slice(havok_body);
        buf.extend_from_slice(tmb_body);
        buf
    }

    fn make_tmb(payload_len: usize) -> Vec<u8> {
        let mut tmb = Vec::with_capacity(4 + payload_len);
        tmb.extend_from_slice(&TMLB_MAGIC);
        tmb.extend(std::iter::repeat_n(0u8, payload_len));
        tmb
    }

    #[test]
    fn parses_v1_happy_path() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(4));
        let pap = Pap::parse(&bytes).expect("parse");
        assert_eq!(pap.version(), PAP_VERSION_V1);
        assert_eq!(pap.animation_count(), 1);
        assert_eq!(pap.animations().len(), 1);
        assert_eq!(pap.model_id(), 0x0101);
        assert_eq!(pap.model_type(), 0x00);
        assert_eq!(pap.variant(), 0x01);
        assert_eq!(pap.havok_bytes(), &HAVOK_MAGIC_BYTES);
        assert!(pap.tmb_footer_bytes().starts_with(&TMLB_MAGIC));
    }

    #[test]
    fn parses_with_three_animations() {
        let bytes = build_happy(3, &HAVOK_MAGIC_BYTES, &make_tmb(8));
        let pap = Pap::parse(&bytes).expect("parse");
        assert_eq!(pap.animation_count(), 3);
        assert_eq!(pap.animations().len(), 3);
        for (i, info) in pap.animations().iter().enumerate() {
            assert_eq!(info.havok_index, i16::try_from(i).expect("fits i16"));
            assert_eq!(info.animation_type, 0x0001);
            assert!(!info.face);
            assert_eq!(info.name.len(), 32);
        }
    }

    #[test]
    fn parses_with_zero_animations() {
        let bytes = build_happy(0, &HAVOK_MAGIC_BYTES, &make_tmb(0));
        let pap = Pap::parse(&bytes).expect("parse");
        assert_eq!(pap.animation_count(), 0);
        assert!(pap.animations().is_empty());
        assert_eq!(pap.havok_bytes(), &HAVOK_MAGIC_BYTES);
    }

    #[test]
    fn parses_with_empty_tmb_footer() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &[]);
        let pap = Pap::parse(&bytes).expect("parse");
        assert!(pap.tmb_footer_bytes().is_empty());
    }

    #[test]
    fn rejects_empty_slice() {
        let err = Pap::parse(&[]).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapLength {
                found: 0,
                minimum: PAP_HEADER_SIZE,
            }
        ));
    }

    #[test]
    fn rejects_slice_shorter_than_header() {
        let bytes = [0u8; PAP_HEADER_SIZE - 1];
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapLength {
                found: 25,
                minimum: PAP_HEADER_SIZE,
            }
        ));
    }

    #[test]
    fn rejects_four_bytes_magic_only() {
        let mut bytes = [0u8; 4];
        bytes.copy_from_slice(&PAP_MAGIC);
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapLength {
                found: 4,
                minimum: PAP_HEADER_SIZE,
            }
        ));
    }

    #[test]
    fn rejects_wrong_magic() {
        let mut bytes = vec![0u8; PAP_HEADER_SIZE];
        bytes[0..4].copy_from_slice(b"NOPE");
        bytes[4..8].copy_from_slice(&PAP_VERSION_V1.to_le_bytes());
        let err = Pap::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPapMagic { found } => assert_eq!(&found, b"NOPE"),
            other => panic!("expected InvalidPapMagic, got {other:?}"),
        }
    }

    #[test]
    fn rejects_unknown_version() {
        let mut bytes = vec![0u8; PAP_HEADER_SIZE];
        bytes[0..4].copy_from_slice(&PAP_MAGIC);
        bytes[4..8].copy_from_slice(&0x0002_0002u32.to_le_bytes());
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::UnsupportedPapVersion {
                version: 0x0002_0002,
            }
        ));
    }

    #[test]
    fn rejects_zero_version() {
        let mut bytes = vec![0u8; PAP_HEADER_SIZE];
        bytes[0..4].copy_from_slice(&PAP_MAGIC);
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(err, Error::UnsupportedPapVersion { version: 0 }));
    }

    #[test]
    fn rejects_animation_count_above_cap() {
        let mut bytes = vec![0u8; PAP_HEADER_SIZE];
        bytes[0..4].copy_from_slice(&PAP_MAGIC);
        bytes[4..8].copy_from_slice(&PAP_VERSION_V1.to_le_bytes());
        bytes[8..10].copy_from_slice(&0xFFFFu16.to_le_bytes());
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapAnimationCount {
                count: 0xFFFF,
                max: PAP_MAX_ANIMATION_COUNT,
            }
        ));
    }

    fn build_with_offsets(
        info_offset: u32,
        havok_offset: u32,
        tmb_offset: u32,
        trailer: &[u8],
    ) -> Vec<u8> {
        let mut buf = Vec::with_capacity(PAP_HEADER_SIZE + trailer.len());
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.push(0);
        buf.push(0);
        buf.extend_from_slice(&info_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        buf.extend_from_slice(&tmb_offset.to_le_bytes());
        assert_eq!(buf.len(), PAP_HEADER_SIZE);
        buf.extend_from_slice(trailer);
        buf
    }

    #[test]
    fn rejects_info_offset_past_eof() {
        let bytes = build_with_offsets(9999, 9999, 9999, &[]);
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffset {
                field: "info_offset",
                offset: 9999,
                file_size: 26,
            }
        ));
    }

    #[test]
    fn rejects_havok_offset_past_eof() {
        let bytes = build_with_offsets(26, 9999, 9999, &[]);
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffset {
                field: "havok_offset",
                offset: 9999,
                file_size: 26,
            }
        ));
    }

    #[test]
    fn rejects_tmb_offset_past_eof() {
        let bytes = build_with_offsets(26, 26, 9999, &[]);
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffset {
                field: "tmb_offset",
                offset: 9999,
                file_size: 26,
            }
        ));
    }

    #[test]
    fn rejects_info_offset_after_havok_offset() {
        let bytes = build_with_offsets(100, 50, 150, &[0u8; 200 - PAP_HEADER_SIZE]);
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffsetOrdering {
                first_field: "info_offset",
                first_offset: 100,
                second_field: "havok_offset",
                second_offset: 50,
            }
        ));
    }

    #[test]
    fn rejects_havok_offset_after_tmb_offset() {
        let bytes = build_with_offsets(26, 100, 50, &[0u8; 200 - PAP_HEADER_SIZE]);
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffsetOrdering {
                first_field: "havok_offset",
                first_offset: 100,
                second_field: "tmb_offset",
                second_offset: 50,
            }
        ));
    }

    #[test]
    fn rejects_info_block_overrunning_eof() {
        let mut buf = Vec::with_capacity(100);
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&5u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.push(0);
        buf.push(0);
        buf.extend_from_slice(&26u32.to_le_bytes());
        buf.extend_from_slice(&100u32.to_le_bytes());
        buf.extend_from_slice(&100u32.to_le_bytes());
        buf.resize(100, 0);
        let err = Pap::parse(&buf).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffset {
                field: "info_block.end",
                ..
            }
        ));
    }

    #[test]
    fn animation_info_decode_failure_is_unreachable_via_validator() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(0));
        let pap = Pap::parse(&bytes).expect("parse");
        assert_eq!(pap.animations().len(), 1);
    }

    #[test]
    fn offsets_at_eof_yield_empty_slices() {
        let bytes = build_with_offsets(26, 26, 26, &[]);
        let pap = Pap::parse(&bytes).expect("parse");
        assert!(pap.animations().is_empty());
        assert!(pap.havok_bytes().is_empty());
        assert!(pap.tmb_footer_bytes().is_empty());
    }

    #[test]
    fn havok_bytes_size_equals_tmb_minus_havok() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(16));
        let pap = Pap::parse(&bytes).expect("parse");
        let expected_len = usize::try_from(pap.tmb_offset() - pap.havok_offset()).expect("fits");
        assert_eq!(pap.havok_bytes().len(), expected_len);
    }

    #[test]
    fn havok_bytes_borrows_from_input() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(4));
        let pap = Pap::parse(&bytes).expect("parse");
        let havok = pap.havok_bytes();
        let havok_start = havok.as_ptr() as usize;
        let bytes_start = bytes.as_ptr() as usize;
        let bytes_end = bytes_start + bytes.len();
        assert!(
            havok_start >= bytes_start && havok_start < bytes_end,
            "havok_bytes must borrow from the input slice"
        );
    }

    #[test]
    fn havok_bytes_starts_at_exact_offset() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(8));
        let pap = Pap::parse(&bytes).expect("parse");
        let expected_start = &bytes[pap.havok_offset() as usize..];
        assert_eq!(
            pap.havok_bytes().as_ptr(),
            expected_start.as_ptr(),
            "havok_bytes() must start at exactly bytes[havok_offset..]"
        );
        assert_eq!(
            pap.havok_bytes().len(),
            usize::try_from(pap.tmb_offset() - pap.havok_offset()).expect("fits"),
            "havok_bytes() length must equal tmb_offset - havok_offset"
        );
    }

    #[test]
    fn tmb_footer_bytes_borrows_from_input() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(4));
        let pap = Pap::parse(&bytes).expect("parse");
        let tmb = pap.tmb_footer_bytes();
        let tmb_start = tmb.as_ptr() as usize;
        let bytes_start = bytes.as_ptr() as usize;
        let bytes_end = bytes_start + bytes.len();
        assert!(
            tmb_start >= bytes_start && tmb_start < bytes_end,
            "tmb_footer_bytes must borrow from the input slice"
        );
    }

    #[test]
    fn tmb_footer_bytes_starts_at_exact_offset_and_has_correct_length() {
        let tmb_payload_len = 12usize;
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(tmb_payload_len));
        let pap = Pap::parse(&bytes).expect("parse");
        let expected_start = &bytes[pap.tmb_offset() as usize..];
        assert_eq!(
            pap.tmb_footer_bytes().as_ptr(),
            expected_start.as_ptr(),
            "tmb_footer_bytes() must start at exactly bytes[tmb_offset..]"
        );
        let expected_len = bytes.len() - pap.tmb_offset() as usize;
        assert_eq!(
            pap.tmb_footer_bytes().len(),
            expected_len,
            "tmb_footer_bytes() length must equal file_size - tmb_offset"
        );
        assert_eq!(expected_len, TMLB_MAGIC.len() + tmb_payload_len);
    }

    #[test]
    fn raw_borrows_entire_input() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(4));
        let pap = Pap::parse(&bytes).expect("parse");
        assert_eq!(pap.raw().as_ptr(), bytes.as_ptr());
        assert_eq!(pap.raw().len(), bytes.len());
    }

    #[test]
    fn animation_info_face_flag_zero() {
        let bytes = build_happy(1, &HAVOK_MAGIC_BYTES, &make_tmb(0));
        let pap = Pap::parse(&bytes).expect("parse");
        assert!(!pap.animations()[0].face);
    }

    #[test]
    fn animation_info_face_flag_nonzero_is_true() {
        let info_offset = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let info_size = u32::try_from(PAP_ANIMATION_INFO_SIZE).expect("40 fits u32");
        let havok_offset = info_offset + info_size;
        let tmb_offset = havok_offset + u32::try_from(HAVOK_MAGIC_BYTES.len()).expect("fits");

        let mut buf = Vec::new();
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&1u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.push(0);
        buf.push(0);
        buf.extend_from_slice(&info_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        buf.extend_from_slice(&tmb_offset.to_le_bytes());
        buf.extend_from_slice(&make_record(b"face", 0, 0, 1));
        buf.extend_from_slice(&HAVOK_MAGIC_BYTES);

        let pap = Pap::parse(&buf).expect("parse");
        assert!(pap.animations()[0].face);
    }

    fn name_prefix(name: &[u8; 32]) -> &[u8] {
        let end = name.iter().position(|&b| b == 0).unwrap_or(name.len());
        &name[..end]
    }

    #[test]
    fn animation_info_name_strips_null_padding() {
        let info_offset = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let info_size = u32::try_from(PAP_ANIMATION_INFO_SIZE).expect("40 fits u32");
        let havok_offset = info_offset + info_size;
        let tmb_offset = havok_offset;

        let mut buf = Vec::new();
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&1u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.push(0);
        buf.push(0);
        buf.extend_from_slice(&info_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        buf.extend_from_slice(&tmb_offset.to_le_bytes());
        buf.extend_from_slice(&make_record(b"idle", 0, 0, 0));

        let pap = Pap::parse(&buf).expect("parse");
        let name = name_prefix(&pap.animations()[0].name);
        assert_eq!(name, b"idle");
    }

    #[test]
    fn animation_info_full_32_byte_name_has_no_null_terminator() {
        let info_offset = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let info_size = u32::try_from(PAP_ANIMATION_INFO_SIZE).expect("40 fits u32");
        let havok_offset = info_offset + info_size;
        let tmb_offset = havok_offset;

        let mut buf = Vec::new();
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&1u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.push(0);
        buf.push(0);
        buf.extend_from_slice(&info_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        buf.extend_from_slice(&tmb_offset.to_le_bytes());
        let mut full_name = [b'A'; 32];
        full_name[31] = b'Z';
        let mut rec = Vec::with_capacity(PAP_ANIMATION_INFO_SIZE);
        rec.extend_from_slice(&full_name);
        rec.extend_from_slice(&0u16.to_le_bytes());
        rec.extend_from_slice(&0i16.to_le_bytes());
        rec.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&rec);

        let pap = Pap::parse(&buf).expect("parse");
        let name = name_prefix(&pap.animations()[0].name);
        assert_eq!(name.len(), 32);
        assert_eq!(name[31], b'Z');
    }

    #[test]
    fn animation_info_havok_index_negative_round_trips() {
        let info_offset = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let info_size = u32::try_from(PAP_ANIMATION_INFO_SIZE).expect("40 fits u32");
        let havok_offset = info_offset + info_size;
        let tmb_offset = havok_offset;

        let mut buf = Vec::new();
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&1u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.push(0);
        buf.push(0);
        buf.extend_from_slice(&info_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        buf.extend_from_slice(&tmb_offset.to_le_bytes());
        buf.extend_from_slice(&make_record(b"x", 0, -1, 0));

        let pap = Pap::parse(&buf).expect("parse");
        assert_eq!(pap.animations()[0].havok_index, -1);
    }

    #[test]
    fn error_display_invalid_pap_magic_mentions_pap() {
        let err = Error::InvalidPapMagic { found: *b"NOPE" };
        let msg = format!("{err}");
        assert!(
            msg.contains("pap "),
            "Display must mention b\"pap \": {msg:?}"
        );
    }

    #[test]
    fn error_display_unsupported_pap_version_mentions_value() {
        let err = Error::UnsupportedPapVersion {
            version: 0xDEAD_BEEF,
        };
        let msg = format!("{err}");
        assert!(
            msg.contains("DEAD") || msg.contains("dead") || msg.contains("3735928559"),
            "Display must mention the offending version: {msg:?}"
        );
    }

    #[test]
    fn error_display_invalid_pap_offset_mentions_field() {
        let err = Error::InvalidPapOffset {
            field: "havok_offset",
            offset: 9999,
            file_size: 26,
        };
        let msg = format!("{err}");
        assert!(msg.contains("havok_offset"));
        assert!(msg.contains("9999"));
        assert!(msg.contains("26"));
    }

    #[test]
    fn error_display_invalid_pap_animation_count_mentions_cap() {
        let err = Error::InvalidPapAnimationCount {
            count: 65535,
            max: PAP_MAX_ANIMATION_COUNT,
        };
        let msg = format!("{err}");
        assert!(msg.contains("65535"));
        assert!(msg.contains(&PAP_MAX_ANIMATION_COUNT.to_string()));
    }

    #[test]
    fn full_round_trip_all_accessors() {
        let bytes = build_happy(2, &HAVOK_MAGIC_BYTES, &make_tmb(32));
        let pap = Pap::parse(&bytes).expect("parse");

        assert_eq!(pap.version(), PAP_VERSION_V1);
        assert_eq!(pap.animation_count(), 2);
        assert_eq!(pap.model_id(), 0x0101);
        assert_eq!(pap.model_type(), 0x00);
        assert_eq!(pap.variant(), 0x01);
        assert_eq!(pap.info_offset() as usize, PAP_HEADER_SIZE);
        assert_eq!(
            pap.havok_offset() as usize,
            PAP_HEADER_SIZE + 2 * PAP_ANIMATION_INFO_SIZE
        );
        assert_eq!(
            pap.tmb_offset() as usize,
            PAP_HEADER_SIZE + 2 * PAP_ANIMATION_INFO_SIZE + HAVOK_MAGIC_BYTES.len()
        );
        assert_eq!(pap.animations().len(), 2);
        assert_eq!(pap.havok_bytes(), &HAVOK_MAGIC_BYTES);
        assert!(pap.tmb_footer_bytes().starts_with(&TMLB_MAGIC));
        assert_eq!(pap.tmb_footer_bytes().len(), 4 + 32);
        assert_eq!(pap.raw().as_ptr(), bytes.as_ptr());
        assert_eq!(pap.raw().len(), bytes.len());

        assert_eq!(
            pap.havok_offset() as usize + pap.havok_bytes().len(),
            pap.tmb_offset() as usize
        );
        assert_eq!(
            pap.tmb_offset() as usize + pap.tmb_footer_bytes().len(),
            pap.raw().len()
        );
    }

    #[test]
    fn parses_at_animation_count_cap() {
        let count = PAP_MAX_ANIMATION_COUNT;
        let info_offset = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let info_size = count * u32::try_from(PAP_ANIMATION_INFO_SIZE).expect("40 fits u32");
        let havok_offset = info_offset + info_size;
        let tmb_offset = havok_offset;
        let file_size = usize::try_from(tmb_offset).expect("fits");

        let mut buf = Vec::with_capacity(file_size);
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&u16::try_from(count).expect("4096 fits u16").to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.push(0);
        buf.push(0);
        buf.extend_from_slice(&info_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        buf.extend_from_slice(&tmb_offset.to_le_bytes());
        buf.resize(file_size, 0);

        let pap = Pap::parse(&buf).expect("parse at cap");
        assert_eq!(u32::from(pap.animation_count()), count);
        assert_eq!(u32::try_from(pap.animations().len()).expect("fits"), count);
    }

    #[test]
    fn rejects_animation_count_one_above_cap() {
        let mut bytes = vec![0u8; PAP_HEADER_SIZE];
        bytes[0..4].copy_from_slice(&PAP_MAGIC);
        bytes[4..8].copy_from_slice(&PAP_VERSION_V1.to_le_bytes());
        let count = u16::try_from(PAP_MAX_ANIMATION_COUNT + 1).expect("4097 fits u16");
        bytes[8..10].copy_from_slice(&count.to_le_bytes());
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapAnimationCount {
                count: 4097,
                max: 4096,
            }
        ));
    }

    #[test]
    fn rejects_animation_count_u16_max() {
        let mut bytes = vec![0u8; PAP_HEADER_SIZE];
        bytes[0..4].copy_from_slice(&PAP_MAGIC);
        bytes[4..8].copy_from_slice(&PAP_VERSION_V1.to_le_bytes());
        bytes[8..10].copy_from_slice(&u16::MAX.to_le_bytes());
        let err = Pap::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapAnimationCount { count, max } if count > max
        ));
    }

    fn build_header_with_offsets_only(info: u32, havok: u32, tmb: u32) -> Vec<u8> {
        let mut full = Vec::with_capacity(PAP_HEADER_SIZE);
        full.extend_from_slice(&PAP_MAGIC);
        full.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        full.extend_from_slice(&0u16.to_le_bytes());
        full.extend_from_slice(&0u16.to_le_bytes());
        full.push(0);
        full.push(0);
        full.extend_from_slice(&info.to_le_bytes());
        full.extend_from_slice(&havok.to_le_bytes());
        full.extend_from_slice(&tmb.to_le_bytes());
        assert_eq!(full.len(), PAP_HEADER_SIZE);
        full
    }

    #[test]
    fn u32_max_offset_rejects_cleanly() {
        let full = build_header_with_offsets_only(u32::MAX, u32::MAX, u32::MAX);
        let err = Pap::parse(&full).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffset {
                field: "info_offset",
                offset: 0xFFFF_FFFF,
                file_size: 26,
            }
        ));
    }

    #[test]
    fn u32_max_havok_offset_rejects_cleanly() {
        let info = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let full = build_header_with_offsets_only(info, u32::MAX, u32::MAX);
        let err = Pap::parse(&full).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffset {
                field: "havok_offset",
                offset: 0xFFFF_FFFF,
                file_size: 26,
            }
        ));
    }

    #[test]
    fn u32_max_tmb_offset_rejects_cleanly() {
        let info = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let havok = info;
        let full = build_header_with_offsets_only(info, havok, u32::MAX);
        let err = Pap::parse(&full).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffset {
                field: "tmb_offset",
                offset: 0xFFFF_FFFF,
                file_size: 26,
            }
        ));
    }

    #[test]
    fn u32_max_info_offset_ordering_violation_does_not_overflow() {
        let full = build_header_with_offsets_only(u32::MAX, 0, 0);
        let err = Pap::parse(&full).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidPapOffset {
                field: "info_offset",
                offset: 0xFFFF_FFFF,
                file_size: 26,
            }
        ));
    }

    #[test]
    fn error_display_invalid_pap_length_mentions_sizes() {
        let err = Error::InvalidPapLength {
            found: 10,
            minimum: PAP_HEADER_SIZE,
        };
        let msg = format!("{err}");
        assert!(
            msg.contains("10"),
            "Display must mention the found length: {msg:?}"
        );
        assert!(
            msg.contains(&PAP_HEADER_SIZE.to_string()),
            "Display must mention the minimum length: {msg:?}"
        );
    }

    #[test]
    fn error_display_invalid_pap_offset_ordering_mentions_fields_and_values() {
        let err = Error::InvalidPapOffsetOrdering {
            first_field: "havok_offset",
            first_offset: 200,
            second_field: "tmb_offset",
            second_offset: 100,
        };
        let msg = format!("{err}");
        assert!(
            msg.contains("havok_offset"),
            "Display must mention first_field: {msg:?}"
        );
        assert!(
            msg.contains("tmb_offset"),
            "Display must mention second_field: {msg:?}"
        );
        assert!(
            msg.contains("200"),
            "Display must mention first_offset: {msg:?}"
        );
        assert!(
            msg.contains("100"),
            "Display must mention second_offset: {msg:?}"
        );
    }

    #[test]
    fn pap_clone_has_same_accessor_values() {
        let bytes = build_happy(2, &HAVOK_MAGIC_BYTES, &make_tmb(16));
        let pap = Pap::parse(&bytes).expect("parse");
        let cloned = pap.clone();

        assert_eq!(cloned.version(), pap.version());
        assert_eq!(cloned.animation_count(), pap.animation_count());
        assert_eq!(cloned.model_id(), pap.model_id());
        assert_eq!(cloned.model_type(), pap.model_type());
        assert_eq!(cloned.variant(), pap.variant());
        assert_eq!(cloned.info_offset(), pap.info_offset());
        assert_eq!(cloned.havok_offset(), pap.havok_offset());
        assert_eq!(cloned.tmb_offset(), pap.tmb_offset());
        assert_eq!(cloned.animations().len(), pap.animations().len());
        assert_eq!(cloned.havok_bytes(), pap.havok_bytes());
        assert_eq!(cloned.tmb_footer_bytes(), pap.tmb_footer_bytes());
        assert_eq!(
            cloned.raw().as_ptr(),
            pap.raw().as_ptr(),
            "cloned Pap's raw() must point at the same input bytes"
        );

        assert_ne!(
            pap.animations().as_ptr(),
            cloned.animations().as_ptr(),
            "cloned animations vec must be a separate allocation"
        );
    }

    #[test]
    fn animation_info_face_flag_u32_max_is_true() {
        let info_offset = u32::try_from(PAP_HEADER_SIZE).expect("26 fits u32");
        let info_size = u32::try_from(PAP_ANIMATION_INFO_SIZE).expect("40 fits u32");
        let havok_offset = info_offset + info_size;
        let tmb_offset = havok_offset;

        let mut buf = Vec::new();
        buf.extend_from_slice(&PAP_MAGIC);
        buf.extend_from_slice(&PAP_VERSION_V1.to_le_bytes());
        buf.extend_from_slice(&1u16.to_le_bytes());
        buf.extend_from_slice(&0u16.to_le_bytes());
        buf.push(0);
        buf.push(0);
        buf.extend_from_slice(&info_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        buf.extend_from_slice(&tmb_offset.to_le_bytes());
        buf.extend_from_slice(&make_record(b"x", 0, 0, u32::MAX));

        let pap = Pap::parse(&buf).expect("parse");
        assert!(
            pap.animations()[0].face,
            "face u32::MAX must map to bool true"
        );
    }

    #[test]
    fn animation_info_partial_eq_and_copy() {
        let a = PapAnimationInfo {
            name: {
                let mut n = [0u8; 32];
                n[..4].copy_from_slice(b"idle");
                n
            },
            animation_type: 1,
            havok_index: 0,
            face: false,
        };
        let b = a;
        assert_eq!(
            a, b,
            "identical PapAnimationInfo records must compare equal"
        );

        let different = PapAnimationInfo { face: true, ..a };
        assert_ne!(a, different, "differing face field must break equality");
    }
}
