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 shared 8-byte prologue (`magic` + `version1` + `version2`).
pub const SKLB_PROLOGUE_SIZE: usize = 8;

/// Total header size for V1 files, including prologue and body.
pub const SKLB_V1_HEADER_SIZE: usize = 28;

/// Total header size for V2 files, which carry wider offset fields.
pub const SKLB_V2_HEADER_SIZE: usize = 36;

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

/// Value of the `version2` field that identifies a V2 skeleton file.
///
/// Any other `version2` value is treated as [`SklbVersion::V1`].
pub const SKLB_V2_VERSION2: u16 = 0x3133;

/// Header format variant of a `.sklb` file.
///
/// The distinction matters only for the header: V1 stores offsets as `u16`,
/// V2 as `u32`. The Havok payload that follows is format-agnostic.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SklbVersion {
    /// Pre-Dawntrail skeleton format with 16-bit offset fields.
    V1,
    /// Dawntrail (and later) skeleton format with 32-bit offset fields.
    V2,
}

impl SklbVersion {
    /// Maps the raw `version2` field to a [`SklbVersion`].
    ///
    /// Only [`SKLB_V2_VERSION2`] (`0x3133`) produces [`SklbVersion::V2`];
    /// every other value is treated as [`SklbVersion::V1`].
    #[must_use]
    pub fn from_version2(version2: u16) -> Self {
        if version2 == SKLB_V2_VERSION2 {
            SklbVersion::V2
        } else {
            SklbVersion::V1
        }
    }
}

#[derive(Debug, BinRead)]
#[brw(little)]
#[allow(dead_code)]
struct WireSklbPrologue {
    magic: [u8; 4],
    version1: u16,
    version2: u16,
}

#[derive(Debug, BinRead)]
#[brw(little)]
#[allow(dead_code)]
struct WireSklbV1Body {
    layer_offset: u16,
    havok_offset: u16,
    body_id: u32,
    mapper_body_id1: u32,
    mapper_body_id2: u32,
    mapper_body_id3: u32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
#[allow(dead_code)]
struct WireSklbV2Body {
    layer_offset: u32,
    havok_offset: u32,
    unk: u32,
    body_id: u32,
    mapper_body_id1: u32,
    mapper_body_id2: u32,
    mapper_body_id3: u32,
}

/// A parsed `.sklb` skeleton file.
///
/// The struct borrows its data from the caller-supplied byte slice; no
/// copying occurs. Use [`Sklb::havok_bytes`] to obtain the opaque Havok
/// binary stream that begins at [`Sklb::havok_offset`].
#[derive(Debug, Clone, Copy)]
pub struct Sklb<'a> {
    version: SklbVersion,
    layer_offset: u32,
    havok_offset: u32,
    bytes: &'a [u8],
}

impl<'a> Sklb<'a> {
    /// Parses a `.sklb` file from a byte slice.
    ///
    /// Validates the magic, determines the version from `version2`, and
    /// checks that `havok_offset` lies within the file. Returns an error
    /// if any check fails.
    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
        let span = info_span!(span_names::SKLB_PARSE, bytes = bytes.len());
        let _enter = span.enter();
        let start = std::time::Instant::now();

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

        let mut cursor = Cursor::new(bytes);
        let prologue =
            WireSklbPrologue::read(&mut cursor).map_err(|e| Error::decode("sklb.prologue", &e))?;

        if prologue.magic != SKLB_MAGIC {
            return Err(Error::InvalidSklbMagic {
                found: prologue.magic,
            });
        }

        let version = SklbVersion::from_version2(prologue.version2);
        let (layer_offset, havok_offset, header_size) = match version {
            SklbVersion::V1 => {
                if bytes.len() < SKLB_V1_HEADER_SIZE {
                    return Err(Error::InvalidSklbLength {
                        found: bytes.len(),
                        minimum: SKLB_V1_HEADER_SIZE,
                    });
                }
                let body = WireSklbV1Body::read(&mut cursor)
                    .map_err(|e| Error::decode("sklb.v1_body", &e))?;
                (
                    u32::from(body.layer_offset),
                    u32::from(body.havok_offset),
                    SKLB_V1_HEADER_SIZE,
                )
            }
            SklbVersion::V2 => {
                if bytes.len() < SKLB_V2_HEADER_SIZE {
                    return Err(Error::InvalidSklbLength {
                        found: bytes.len(),
                        minimum: SKLB_V2_HEADER_SIZE,
                    });
                }
                let body = WireSklbV2Body::read(&mut cursor)
                    .map_err(|e| Error::decode("sklb.v2_body", &e))?;
                (body.layer_offset, body.havok_offset, SKLB_V2_HEADER_SIZE)
            }
        };

        let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
        let havok_offset_u64 = u64::from(havok_offset);
        if havok_offset_u64 > file_size {
            return Err(Error::InvalidSklbHavokOffset {
                havok_offset: havok_offset_u64,
                file_size,
            });
        }

        let havok_len = bytes
            .len()
            .saturating_sub(usize::try_from(havok_offset).unwrap_or(usize::MAX));

        let elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
        info!(
            target: "sklb",
            bytes = bytes.len(),
            version = ?version,
            havok_offset,
            havok_len,
            layer_offset,
            header_size,
            elapsed_ms,
            "sklb.parse",
        );

        Ok(Self {
            version,
            layer_offset,
            havok_offset,
            bytes,
        })
    }

    /// Returns the header format variant.
    #[must_use]
    pub fn version(&self) -> SklbVersion {
        self.version
    }

    /// Byte offset of the skeleton layer data within the file.
    #[must_use]
    pub fn layer_offset(&self) -> u32 {
        self.layer_offset
    }

    /// Byte offset at which the Havok binary stream begins.
    ///
    /// This is the start of the opaque payload returned by [`Sklb::havok_bytes`].
    #[must_use]
    pub fn havok_offset(&self) -> u32 {
        self.havok_offset
    }

    /// Zero-copy slice of the Havok binary stream.
    ///
    /// Returns `bytes[havok_offset..]`, which is the entire tail of the file
    /// from the Havok start to EOF. Returns an empty slice if `havok_offset`
    /// is exactly at EOF.
    #[must_use]
    pub fn havok_bytes(&self) -> &'a [u8] {
        let start = usize::try_from(self.havok_offset).unwrap_or(usize::MAX);
        if start >= self.bytes.len() {
            return &[];
        }
        &self.bytes[start..]
    }

    /// Returns the entire input byte slice this `Sklb` 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];

    fn build_v1(version2: u16, layer_offset: u16, payload: &[u8]) -> Vec<u8> {
        let havok_offset = u16::try_from(SKLB_V1_HEADER_SIZE).expect("28 fits u16");
        let mut buf = Vec::with_capacity(SKLB_V1_HEADER_SIZE + payload.len());
        buf.extend_from_slice(&SKLB_MAGIC);
        buf.extend_from_slice(&0x3030u16.to_le_bytes());
        buf.extend_from_slice(&version2.to_le_bytes());
        buf.extend_from_slice(&layer_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        for body_field in [0u32; 4] {
            buf.extend_from_slice(&body_field.to_le_bytes());
        }
        assert_eq!(buf.len(), SKLB_V1_HEADER_SIZE);
        buf.extend_from_slice(payload);
        buf
    }

    fn build_v2(layer_offset: u32, payload: &[u8]) -> Vec<u8> {
        let havok_offset = u32::try_from(SKLB_V2_HEADER_SIZE).expect("36 fits u32");
        let mut buf = Vec::with_capacity(SKLB_V2_HEADER_SIZE + payload.len());
        buf.extend_from_slice(&SKLB_MAGIC);
        buf.extend_from_slice(&0x3030u16.to_le_bytes());
        buf.extend_from_slice(&SKLB_V2_VERSION2.to_le_bytes());
        buf.extend_from_slice(&layer_offset.to_le_bytes());
        buf.extend_from_slice(&havok_offset.to_le_bytes());
        for body_field in [0u32; 5] {
            buf.extend_from_slice(&body_field.to_le_bytes());
        }
        assert_eq!(buf.len(), SKLB_V2_HEADER_SIZE);
        buf.extend_from_slice(payload);
        buf
    }

    #[test]
    fn parses_v1_with_version2_0x3131() {
        let bytes = build_v1(0x3131, 0x2E, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(sklb.version(), SklbVersion::V1);
        assert_eq!(sklb.layer_offset(), 0x2E);
        assert_eq!(sklb.havok_offset() as usize, SKLB_V1_HEADER_SIZE);
        assert_eq!(sklb.havok_bytes(), &HAVOK_MAGIC_BYTES);
    }

    #[test]
    fn parses_v1_with_version2_0x3132() {
        let bytes = build_v1(0x3132, 0x2E, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(sklb.version(), SklbVersion::V1);
        assert_eq!(sklb.layer_offset(), 0x2E);
        assert_eq!(sklb.havok_bytes(), &HAVOK_MAGIC_BYTES);
    }

    #[test]
    fn parses_v2_with_version2_0x3133() {
        let bytes = build_v2(0x40, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(sklb.version(), SklbVersion::V2);
        assert_eq!(sklb.layer_offset(), 0x40);
        assert_eq!(sklb.havok_offset() as usize, SKLB_V2_HEADER_SIZE);
        assert_eq!(sklb.havok_bytes(), &HAVOK_MAGIC_BYTES);
    }

    #[test]
    fn face_pap_quirk_parses_as_v1() {
        let bytes = build_v1(0x3031, 0x2E, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(sklb.version(), SklbVersion::V1);
        assert_eq!(sklb.havok_bytes(), &HAVOK_MAGIC_BYTES);
    }

    #[test]
    fn rejects_empty_slice() {
        let bytes: &[u8] = &[];
        let err = Sklb::parse(bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidSklbLength {
                found: 0,
                minimum: SKLB_PROLOGUE_SIZE,
            }
        ));
    }

    #[test]
    fn rejects_slice_too_short_for_v1_body() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&SKLB_MAGIC);
        bytes.extend_from_slice(&0x3030u16.to_le_bytes());
        bytes.extend_from_slice(&0x3131u16.to_le_bytes());
        bytes.extend_from_slice(&[0u8; 4]);
        let err = Sklb::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidSklbLength {
                found: 12,
                minimum: SKLB_V1_HEADER_SIZE,
            }
        ));
    }

    #[test]
    fn rejects_slice_too_short_for_v2_body() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&SKLB_MAGIC);
        bytes.extend_from_slice(&0x3030u16.to_le_bytes());
        bytes.extend_from_slice(&SKLB_V2_VERSION2.to_le_bytes());
        bytes.extend_from_slice(&[0u8; 16]);
        let err = Sklb::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidSklbLength {
                found: 24,
                minimum: SKLB_V2_HEADER_SIZE,
            }
        ));
    }

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

    #[test]
    fn rejects_havok_offset_past_end_of_file() {
        let mut bytes = Vec::with_capacity(SKLB_V1_HEADER_SIZE);
        bytes.extend_from_slice(&SKLB_MAGIC);
        bytes.extend_from_slice(&0x3030u16.to_le_bytes());
        bytes.extend_from_slice(&0x3131u16.to_le_bytes());
        bytes.extend_from_slice(&0x2Eu16.to_le_bytes());
        bytes.extend_from_slice(&9999u16.to_le_bytes());
        for _ in 0..4 {
            bytes.extend_from_slice(&0u32.to_le_bytes());
        }
        assert_eq!(bytes.len(), SKLB_V1_HEADER_SIZE);
        let err = Sklb::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidSklbHavokOffset {
                havok_offset: 9999,
                file_size: 28,
            }
        ));
    }

    #[test]
    fn version_from_version2_dispatch() {
        assert_eq!(SklbVersion::from_version2(0x3131), SklbVersion::V1);
        assert_eq!(SklbVersion::from_version2(0x3132), SklbVersion::V1);
        assert_eq!(SklbVersion::from_version2(0x3133), SklbVersion::V2);
        assert_eq!(SklbVersion::from_version2(0x0000), SklbVersion::V1);
        assert_eq!(SklbVersion::from_version2(0xFFFF), SklbVersion::V1);
    }

    #[test]
    fn havok_bytes_borrows_from_input() {
        let bytes = build_v1(0x3131, 0x2E, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        let havok = sklb.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_offset_at_eof_yields_empty_slice() {
        let mut bytes = Vec::with_capacity(SKLB_V1_HEADER_SIZE);
        bytes.extend_from_slice(&SKLB_MAGIC);
        bytes.extend_from_slice(&0x3030u16.to_le_bytes());
        bytes.extend_from_slice(&0x3131u16.to_le_bytes());
        bytes.extend_from_slice(&0x2Eu16.to_le_bytes());
        let havok_offset = u16::try_from(SKLB_V1_HEADER_SIZE).expect("28 fits u16");
        bytes.extend_from_slice(&havok_offset.to_le_bytes());
        for _ in 0..4 {
            bytes.extend_from_slice(&0u32.to_le_bytes());
        }
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(sklb.havok_bytes(), &[] as &[u8]);
    }

    #[test]
    fn raw_borrows_entire_input_v1() {
        let bytes = build_v1(0x3131, 0x2E, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(
            sklb.raw().as_ptr(),
            bytes.as_ptr(),
            "raw() must point to the start of the input"
        );
        assert_eq!(
            sklb.raw().len(),
            bytes.len(),
            "raw() must span the full input length"
        );
    }

    #[test]
    fn raw_borrows_entire_input_v2() {
        let bytes = build_v2(0x40, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(sklb.raw().as_ptr(), bytes.as_ptr());
        assert_eq!(sklb.raw().len(), bytes.len());
    }

    #[test]
    fn v2_havok_offset_at_eof_yields_empty_slice() {
        let mut bytes = Vec::with_capacity(SKLB_V2_HEADER_SIZE);
        bytes.extend_from_slice(&SKLB_MAGIC);
        bytes.extend_from_slice(&0x3030u16.to_le_bytes());
        bytes.extend_from_slice(&SKLB_V2_VERSION2.to_le_bytes());
        let havok_offset = u32::try_from(SKLB_V2_HEADER_SIZE).expect("36 fits u32");
        bytes.extend_from_slice(&0x40u32.to_le_bytes());
        bytes.extend_from_slice(&havok_offset.to_le_bytes());
        for _ in 0..5 {
            bytes.extend_from_slice(&0u32.to_le_bytes());
        }
        assert_eq!(bytes.len(), SKLB_V2_HEADER_SIZE);
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(sklb.havok_bytes(), &[] as &[u8]);
    }

    #[test]
    fn v2_havok_offset_u32_max_rejects_without_overflow() {
        let mut bytes = Vec::with_capacity(SKLB_V2_HEADER_SIZE);
        bytes.extend_from_slice(&SKLB_MAGIC);
        bytes.extend_from_slice(&0x3030u16.to_le_bytes());
        bytes.extend_from_slice(&SKLB_V2_VERSION2.to_le_bytes());
        bytes.extend_from_slice(&0x40u32.to_le_bytes());
        bytes.extend_from_slice(&u32::MAX.to_le_bytes());
        for _ in 0..5 {
            bytes.extend_from_slice(&0u32.to_le_bytes());
        }
        assert_eq!(bytes.len(), SKLB_V2_HEADER_SIZE);
        let err = Sklb::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidSklbHavokOffset {
                havok_offset: 0xFFFF_FFFF,
                file_size: 36,
            }
        ));
    }

    #[test]
    fn v1_layer_offset_u16_max_widens_to_u32() {
        let bytes = build_v1(0x3131, u16::MAX, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        assert_eq!(sklb.version(), SklbVersion::V1);
        assert_eq!(
            sklb.layer_offset(),
            u32::from(u16::MAX),
            "u16::MAX layer_offset must widen cleanly to u32"
        );
    }

    #[test]
    fn rejects_three_bytes() {
        let bytes = [0u8; 3];
        let err = Sklb::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidSklbLength {
                found: 3,
                minimum: SKLB_PROLOGUE_SIZE,
            }
        ));
    }

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

    #[test]
    fn rejects_seven_bytes_one_short_of_prologue() {
        let mut bytes = [0u8; 7];
        bytes[0..4].copy_from_slice(&SKLB_MAGIC);
        let err = Sklb::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidSklbLength {
                found: 7,
                minimum: SKLB_PROLOGUE_SIZE,
            }
        ));
    }

    #[test]
    fn version_from_version2_dispatch_extended() {
        assert_eq!(SklbVersion::from_version2(0x0001), SklbVersion::V1);
        assert_eq!(SklbVersion::from_version2(0x3130), SklbVersion::V1);
        assert_eq!(SklbVersion::from_version2(0x3134), SklbVersion::V1);
        assert_eq!(SklbVersion::from_version2(0x8000), SklbVersion::V1);
        assert_eq!(SklbVersion::from_version2(0xFFFE), SklbVersion::V1);
    }

    #[test]
    fn error_display_invalid_sklb_magic_contains_bytes() {
        let err = Error::InvalidSklbMagic { found: *b"NOPE" };
        let msg = format!("{err}");
        assert!(
            msg.contains("NOPE") || msg.contains("4e4f5045") || msg.contains("4E4F5045") || {
                msg.contains("78") || msg.contains("0x") || msg.contains('[')
            },
            "InvalidSklbMagic Display should include the found bytes: {msg:?}"
        );
        assert!(
            msg.contains("blks"),
            "InvalidSklbMagic Display should mention b\"blks\": {msg:?}"
        );
    }

    #[test]
    fn error_display_invalid_sklb_havok_offset_contains_both_fields() {
        let err = Error::InvalidSklbHavokOffset {
            havok_offset: 9999,
            file_size: 28,
        };
        let msg = format!("{err}");
        assert!(
            msg.contains("9999"),
            "InvalidSklbHavokOffset Display must include havok_offset: {msg:?}"
        );
        assert!(
            msg.contains("28"),
            "InvalidSklbHavokOffset Display must include file_size: {msg:?}"
        );
    }

    #[test]
    fn error_display_invalid_sklb_length_contains_both_fields() {
        let err = Error::InvalidSklbLength {
            found: 4,
            minimum: SKLB_PROLOGUE_SIZE,
        };
        let msg = format!("{err}");
        assert!(
            msg.contains('4'),
            "InvalidSklbLength Display must include found: {msg:?}"
        );
        assert!(
            msg.contains(&SKLB_PROLOGUE_SIZE.to_string()),
            "InvalidSklbLength Display must include minimum: {msg:?}"
        );
    }

    #[test]
    fn havok_bytes_borrows_from_input_and_has_correct_length() {
        let bytes = build_v1(0x3131, 0x2E, &HAVOK_MAGIC_BYTES);
        let sklb = Sklb::parse(&bytes).expect("parse");
        let havok = sklb.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"
        );

        let expected_len = HAVOK_MAGIC_BYTES.len();
        assert_eq!(
            havok.len(),
            expected_len,
            "havok_bytes length must equal the payload byte count"
        );
    }

    #[test]
    fn v1_round_trip_all_accessors() {
        let mut payload = Vec::with_capacity(64);
        payload.extend_from_slice(&HAVOK_MAGIC_BYTES);
        payload.extend(std::iter::repeat_n(0xABu8, 64 - HAVOK_MAGIC_BYTES.len()));

        let bytes = build_v1(0x3131, 0x2E, &payload);
        let sklb = Sklb::parse(&bytes).expect("parse");

        assert_eq!(sklb.version(), SklbVersion::V1, "version");
        assert_eq!(sklb.layer_offset(), 0x2E, "layer_offset");
        assert_eq!(
            sklb.havok_offset() as usize,
            SKLB_V1_HEADER_SIZE,
            "havok_offset"
        );
        assert_eq!(sklb.havok_bytes(), payload.as_slice(), "havok_bytes");
        assert_eq!(sklb.raw().len(), bytes.len(), "raw length");
        assert_eq!(sklb.raw().as_ptr(), bytes.as_ptr(), "raw pointer");

        assert_eq!(
            sklb.havok_offset() as usize + sklb.havok_bytes().len(),
            sklb.raw().len(),
            "havok_offset + havok_bytes.len() must equal file size"
        );
    }

    #[test]
    fn v2_round_trip_all_accessors() {
        let mut payload = Vec::with_capacity(64);
        payload.extend_from_slice(&HAVOK_MAGIC_BYTES);
        payload.extend(std::iter::repeat_n(0xCDu8, 64 - HAVOK_MAGIC_BYTES.len()));

        let bytes = build_v2(0x40, &payload);
        let sklb = Sklb::parse(&bytes).expect("parse");

        assert_eq!(sklb.version(), SklbVersion::V2, "version");
        assert_eq!(sklb.layer_offset(), 0x40, "layer_offset");
        assert_eq!(
            sklb.havok_offset() as usize,
            SKLB_V2_HEADER_SIZE,
            "havok_offset"
        );
        assert_eq!(sklb.havok_bytes(), payload.as_slice(), "havok_bytes");
        assert_eq!(sklb.raw().len(), bytes.len(), "raw length");
        assert_eq!(sklb.raw().as_ptr(), bytes.as_ptr(), "raw pointer");

        assert_eq!(
            sklb.havok_offset() as usize + sklb.havok_bytes().len(),
            sklb.raw().len(),
            "havok_offset + havok_bytes.len() must equal file size"
        );
    }
}
