use std::io::Cursor;

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

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

use super::collision::CollisionData;
use super::simulator::SimulatorData;

/// Maximum number of records allowed per collision or simulator section.
pub const PHYB_MAX_RECORDS_PER_SECTION: u32 = 4096;

/// Byte length of the V0 all-zero stub.
pub const PHYB_STUB_SIZE: usize = 12;

/// Byte length of the V1 (`PHYB_VERSION_FULL`) header.
pub const PHYB_FULL_HEADER_SIZE: usize = 16;

/// The `version` field value that identifies a fully-populated V1 file.
///
/// Any other non-zero value is currently unsupported. Zero identifies a
/// [`PhybVersion::V0`] stub.
pub const PHYB_VERSION_FULL: u32 = 0x0100_0001;

/// Version tag for a `.phyb` file.
///
/// V0 is a 12-byte all-zero stub with no geometry data.
/// V1 (`version` == [`PHYB_VERSION_FULL`]) contains full collision and simulator sections.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PhybVersion {
    /// All-zero stub; no collision or simulator data present.
    V0,
    /// Full physics body with collision and simulator sections.
    V1,
}

impl PhybVersion {
    /// Maps a raw `version` field to a [`PhybVersion`], or `None` for unknown values.
    #[must_use]
    pub fn from_raw(raw: u32) -> Option<Self> {
        match raw {
            0 => Some(PhybVersion::V0),
            PHYB_VERSION_FULL => Some(PhybVersion::V1),
            _ => None,
        }
    }

    /// Returns the on-disk `version` field value for this variant.
    #[must_use]
    pub fn as_raw(self) -> u32 {
        match self {
            PhybVersion::V0 => 0,
            PhybVersion::V1 => PHYB_VERSION_FULL,
        }
    }
}

#[inline]
pub(crate) fn phyb_string_from_padded(
    bytes: &[u8],
    field: &'static str,
    offset: u64,
) -> Result<String> {
    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
    std::str::from_utf8(&bytes[..end])
        .map(str::to_owned)
        .map_err(|_| Error::InvalidUtf8InPhybString { field, offset })
}

#[derive(Debug, BinRead)]
#[brw(little)]
#[allow(dead_code)]
struct WirePhybFullHeader {
    version: u32,
    data_type: u32,
    collision_offset: u32,
    simulator_offset: u32,
}

/// A parsed `.phyb` physics body file.
///
/// Borrows directly from the caller-supplied byte slice. The [`CollisionData`]
/// and [`SimulatorData`] sections are fully decoded on parse. The optional
/// `EPHB` extended trailer is located lazily via [`Phyb::extended_bytes`].
#[derive(Debug, Clone)]
pub struct Phyb<'a> {
    version: PhybVersion,
    data_type: u32,
    collision_offset: u32,
    simulator_offset: u32,
    collision: CollisionData,
    simulator: SimulatorData,
    extended_offset: Option<u32>,
    bytes: &'a [u8],
}

impl<'a> Phyb<'a> {
    /// Parses a `.phyb` file from a byte slice.
    ///
    /// Determines the version from the first four bytes and delegates to the
    /// appropriate sub-parser. Returns an error for unknown versions, malformed
    /// headers, or offsets outside the file.
    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
        let span = info_span!(span_names::PHYB_PARSE, bytes = bytes.len());
        let _enter = span.enter();
        let start = std::time::Instant::now();

        if bytes.len() < std::mem::size_of::<u32>() {
            return Err(Error::InvalidPhybLength {
                found: bytes.len(),
                minimum: std::mem::size_of::<u32>(),
            });
        }

        let mut probe = Cursor::new(&bytes[..std::mem::size_of::<u32>()]);
        let raw_version =
            u32::read_le(&mut probe).map_err(|e| Error::decode("phyb.version", &e))?;
        let version = PhybVersion::from_raw(raw_version).ok_or(Error::UnsupportedPhybVersion {
            version: raw_version,
        })?;

        let (data_type, collision_offset, simulator_offset, collision, simulator, extended_offset) =
            match version {
                PhybVersion::V0 => parse_v0(bytes)?,
                PhybVersion::V1 => parse_v1(bytes)?,
            };

        let elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
        info!(
            target: "phyb",
            bytes = bytes.len(),
            version = ?version,
            data_type,
            collision_offset,
            simulator_offset,
            simulator_count = simulator.simulators.len(),
            capsules = collision.capsules.len(),
            ellipsoids = collision.ellipsoids.len(),
            normal_planes = collision.normal_planes.len(),
            three_point_planes = collision.three_point_planes.len(),
            spheres = collision.spheres.len(),
            elapsed_ms,
            "phyb.parse",
        );

        Ok(Self {
            version,
            data_type,
            collision_offset,
            simulator_offset,
            collision,
            simulator,
            extended_offset,
            bytes,
        })
    }

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

    /// Returns the `data_type` field from the V1 header; always 0 for V0.
    #[must_use]
    pub fn data_type(&self) -> u32 {
        self.data_type
    }

    /// Byte offset of the collision section within the file.
    #[must_use]
    pub fn collision_offset(&self) -> u32 {
        self.collision_offset
    }

    /// Byte offset of the simulator section within the file.
    #[must_use]
    pub fn simulator_offset(&self) -> u32 {
        self.simulator_offset
    }

    /// Decoded collision geometry primitives.
    #[must_use]
    pub fn collision(&self) -> &CollisionData {
        &self.collision
    }

    /// Decoded simulator chains and associated data.
    #[must_use]
    pub fn simulator(&self) -> &SimulatorData {
        &self.simulator
    }

    /// Returns the `EPHB` extended trailer bytes, if present.
    ///
    /// Detection scans the last 64 bytes of the file for the `EPHB` magic,
    /// validates 4-byte alignment and a non-zero size field.
    #[must_use]
    pub fn extended_bytes(&self) -> Option<&'a [u8]> {
        let start = self.extended_offset?;
        let idx = usize::try_from(start).ok()?;
        if idx >= self.bytes.len() {
            return None;
        }
        Some(&self.bytes[idx..])
    }

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

type ParsedSections = (u32, u32, u32, CollisionData, SimulatorData, Option<u32>);

fn parse_v0(bytes: &[u8]) -> Result<ParsedSections> {
    if bytes.len() < PHYB_STUB_SIZE {
        return Err(Error::InvalidPhybLength {
            found: bytes.len(),
            minimum: PHYB_STUB_SIZE,
        });
    }
    let data_type = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
    if data_type != 0 {
        return Err(Error::InvalidPhybStub {
            field: "data_type",
            value: data_type,
        });
    }
    let section_offsets = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
    if section_offsets != 0 {
        return Err(Error::InvalidPhybStub {
            field: "section_offsets",
            value: section_offsets,
        });
    }
    Ok((
        0,
        0,
        0,
        CollisionData::default(),
        SimulatorData::default(),
        None,
    ))
}

fn parse_v1(bytes: &[u8]) -> Result<ParsedSections> {
    if bytes.len() < PHYB_FULL_HEADER_SIZE {
        return Err(Error::InvalidPhybLength {
            found: bytes.len(),
            minimum: PHYB_FULL_HEADER_SIZE,
        });
    }
    let mut cur = Cursor::new(&bytes[..PHYB_FULL_HEADER_SIZE]);
    let hdr = WirePhybFullHeader::read(&mut cur).map_err(|e| Error::decode("phyb.header", &e))?;

    let file_size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
    for (field, offset) in [
        ("collision_offset", u64::from(hdr.collision_offset)),
        ("simulator_offset", u64::from(hdr.simulator_offset)),
    ] {
        if offset > file_size {
            return Err(Error::InvalidPhybOffset {
                field,
                offset,
                file_size,
            });
        }
    }

    let degenerate = hdr.data_type == 0;
    if degenerate && bytes.len() != PHYB_FULL_HEADER_SIZE {
        return Err(Error::InvalidPhybLength {
            found: bytes.len(),
            minimum: PHYB_FULL_HEADER_SIZE,
        });
    }

    if !degenerate {
        let header_size = u64::try_from(PHYB_FULL_HEADER_SIZE).unwrap_or(u64::MAX);
        for (field, offset) in [
            ("collision_offset", u64::from(hdr.collision_offset)),
            ("simulator_offset", u64::from(hdr.simulator_offset)),
        ] {
            if offset < header_size {
                return Err(Error::InvalidPhybOffset {
                    field,
                    offset,
                    file_size,
                });
            }
        }
    }

    let no_collision = hdr.collision_offset == hdr.simulator_offset;

    let collision = if degenerate || no_collision {
        CollisionData::default()
    } else {
        CollisionData::parse(
            bytes,
            usize::try_from(hdr.collision_offset).map_err(|_| Error::InvalidPhybOffset {
                field: "collision_offset",
                offset: u64::from(hdr.collision_offset),
                file_size,
            })?,
        )?
    };

    let (simulator, simulator_section_end) = if degenerate {
        (SimulatorData::default(), PHYB_FULL_HEADER_SIZE)
    } else {
        let sim_start =
            usize::try_from(hdr.simulator_offset).map_err(|_| Error::InvalidPhybOffset {
                field: "simulator_offset",
                offset: u64::from(hdr.simulator_offset),
                file_size,
            })?;
        let sim = SimulatorData::parse(bytes, sim_start)?;
        (sim, sim_start)
    };

    let extended_offset = detect_extended_trailer(bytes, simulator_section_end);

    Ok((
        hdr.data_type,
        hdr.collision_offset,
        hdr.simulator_offset,
        collision,
        simulator,
        extended_offset,
    ))
}

fn detect_extended_trailer(bytes: &[u8], simulator_section_end: usize) -> Option<u32> {
    const MAGIC: &[u8; 4] = b"EPHB";
    let window_start = bytes.len().saturating_sub(64);
    let window_start = window_start.max(simulator_section_end);
    if window_start.checked_add(MAGIC.len())? > bytes.len() {
        return None;
    }
    let slice = &bytes[window_start..];
    let mut found: Option<usize> = None;
    for (idx, win) in slice.windows(MAGIC.len()).enumerate() {
        if win == MAGIC {
            let absolute = window_start + idx;
            if (absolute - simulator_section_end) % 4 != 0 {
                continue;
            }
            if absolute + 8 > bytes.len() {
                continue;
            }
            let size = u32::from_le_bytes([
                bytes[absolute + 4],
                bytes[absolute + 5],
                bytes[absolute + 6],
                bytes[absolute + 7],
            ]);
            if size == 0 {
                continue;
            }
            found = Some(absolute);
        }
    }
    u32::try_from(found?).ok()
}

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

    fn build_stub_phyb() -> Vec<u8> {
        let mut buf = Vec::with_capacity(PHYB_STUB_SIZE);
        for _ in 0..3 {
            buf.extend_from_slice(&0u32.to_le_bytes());
        }
        buf
    }

    fn build_degenerate_v1_phyb() -> Vec<u8> {
        let header_size = u32::try_from(PHYB_FULL_HEADER_SIZE).expect("16 fits u32");
        let mut buf = Vec::with_capacity(PHYB_FULL_HEADER_SIZE);
        buf.extend_from_slice(&PHYB_VERSION_FULL.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&header_size.to_le_bytes());
        buf.extend_from_slice(&header_size.to_le_bytes());
        assert_eq!(buf.len(), PHYB_FULL_HEADER_SIZE);
        buf
    }

    fn build_minimal_populated_v1_phyb() -> Vec<u8> {
        let mut buf = Vec::new();
        let collision_offset: u32 = 16;
        let simulator_offset: u32 = 24;
        buf.extend_from_slice(&PHYB_VERSION_FULL.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(&collision_offset.to_le_bytes());
        buf.extend_from_slice(&simulator_offset.to_le_bytes());
        buf.extend_from_slice(&[0u8; 8]);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf
    }

    #[test]
    fn version_from_raw_dispatch() {
        assert_eq!(PhybVersion::from_raw(0), Some(PhybVersion::V0));
        assert_eq!(
            PhybVersion::from_raw(PHYB_VERSION_FULL),
            Some(PhybVersion::V1)
        );
        assert_eq!(PhybVersion::from_raw(1), None);
        assert_eq!(PhybVersion::from_raw(0xDEAD_BEEF), None);
        assert_eq!(PhybVersion::V0.as_raw(), 0);
        assert_eq!(PhybVersion::V1.as_raw(), PHYB_VERSION_FULL);
    }

    #[test]
    fn parses_v0_stub() {
        let bytes = build_stub_phyb();
        let phyb = Phyb::parse(&bytes).expect("parse");
        assert_eq!(phyb.version(), PhybVersion::V0);
        assert_eq!(phyb.data_type(), 0);
        assert_eq!(phyb.collision_offset(), 0);
        assert_eq!(phyb.simulator_offset(), 0);
        assert!(phyb.collision().capsules.is_empty());
        assert!(phyb.simulator().simulators.is_empty());
        assert!(phyb.extended_bytes().is_none());
    }

    #[test]
    fn parses_degenerate_v1() {
        let bytes = build_degenerate_v1_phyb();
        let phyb = Phyb::parse(&bytes).expect("parse");
        assert_eq!(phyb.version(), PhybVersion::V1);
        assert_eq!(phyb.data_type(), 0);
        let header_size = u32::try_from(PHYB_FULL_HEADER_SIZE).expect("16 fits u32");
        assert_eq!(phyb.collision_offset(), header_size);
        assert_eq!(phyb.simulator_offset(), header_size);
        assert!(phyb.collision().capsules.is_empty());
        assert!(phyb.simulator().simulators.is_empty());
    }

    #[test]
    fn parses_minimal_populated_v1() {
        let bytes = build_minimal_populated_v1_phyb();
        let phyb = Phyb::parse(&bytes).expect("parse");
        assert_eq!(phyb.version(), PhybVersion::V1);
        assert_eq!(phyb.data_type(), 1);
        assert_eq!(phyb.collision_offset(), 16);
        assert_eq!(phyb.simulator_offset(), 24);
        assert!(phyb.collision().capsules.is_empty());
        assert!(phyb.simulator().simulators.is_empty());
    }

    #[test]
    fn rejects_empty_input() {
        let bytes: &[u8] = &[];
        let err = Phyb::parse(bytes).unwrap_err();
        assert!(matches!(err, Error::InvalidPhybLength { .. }));
    }

    #[test]
    fn rejects_v0_too_short() {
        let bytes = 0u32.to_le_bytes();
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybLength { found, minimum } => {
                assert_eq!(found, 4);
                assert_eq!(minimum, PHYB_STUB_SIZE);
            }
            other => panic!("expected InvalidPhybLength, got {other:?}"),
        }
    }

    #[test]
    fn rejects_v1_too_short() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&PHYB_VERSION_FULL.to_le_bytes());
        bytes.extend_from_slice(&0u32.to_le_bytes());
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybLength { found, minimum } => {
                assert_eq!(found, 8);
                assert_eq!(minimum, PHYB_FULL_HEADER_SIZE);
            }
            other => panic!("expected InvalidPhybLength, got {other:?}"),
        }
    }

    #[test]
    fn rejects_unsupported_version() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
        bytes.extend_from_slice(&[0u8; 12]);
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::UnsupportedPhybVersion { version } => assert_eq!(version, 0xDEAD_BEEF),
            other => panic!("expected UnsupportedPhybVersion, got {other:?}"),
        }
    }

    #[test]
    fn rejects_collision_offset_past_eof() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&PHYB_VERSION_FULL.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(&9999u32.to_le_bytes());
        bytes.extend_from_slice(
            &u32::try_from(PHYB_FULL_HEADER_SIZE)
                .expect("16 fits u32")
                .to_le_bytes(),
        );
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybOffset { field, offset, .. } => {
                assert_eq!(field, "collision_offset");
                assert_eq!(offset, 9999);
            }
            other => panic!("expected InvalidPhybOffset, got {other:?}"),
        }
    }

    #[test]
    fn rejects_simulator_offset_past_eof() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&PHYB_VERSION_FULL.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(
            &u32::try_from(PHYB_FULL_HEADER_SIZE)
                .expect("16 fits u32")
                .to_le_bytes(),
        );
        bytes.extend_from_slice(&u32::MAX.to_le_bytes());
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybOffset { field, .. } => assert_eq!(field, "simulator_offset"),
            other => panic!("expected InvalidPhybOffset, got {other:?}"),
        }
    }

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

    #[test]
    fn extended_bytes_none_when_no_ephb_magic() {
        let bytes = build_minimal_populated_v1_phyb();
        let phyb = Phyb::parse(&bytes).expect("parse");
        assert!(phyb.extended_bytes().is_none());
    }

    #[test]
    fn extended_bytes_detects_ephb_magic_after_simulator() {
        let mut bytes = build_minimal_populated_v1_phyb();
        let pre_len = bytes.len();
        bytes.extend_from_slice(b"EPHB\x01\x02\x03\x04");
        let phyb = Phyb::parse(&bytes).expect("parse");
        let ext = phyb.extended_bytes().expect("ext present");
        assert!(ext.starts_with(b"EPHB"));
        let ext_ptr = ext.as_ptr().addr();
        let bytes_ptr = bytes.as_ptr().addr();
        assert_eq!(ext_ptr - bytes_ptr, pre_len);
    }

    #[test]
    fn phyb_string_from_padded_strips_at_null() {
        let mut buf = [0u8; 32];
        buf[..4].copy_from_slice(b"abcd");
        buf[8] = b'Z';
        let s = phyb_string_from_padded(&buf, "test.field", 0).expect("strict decode");
        assert_eq!(s, "abcd");
    }

    #[test]
    fn phyb_string_from_padded_handles_full_buffer_without_null() {
        let mut buf = [0u8; 32];
        for (i, b) in buf.iter_mut().enumerate() {
            *b = b'A' + u8::try_from(i % 26).expect("0..26 fits u8");
        }
        let s = phyb_string_from_padded(&buf, "test.field", 0).expect("strict decode");
        assert_eq!(s.len(), 32);
    }

    #[test]
    fn phyb_string_from_padded_rejects_invalid_utf8() {
        let mut buf = [0u8; 32];
        buf[0] = 0xFF;
        buf[1] = b'x';
        let err = phyb_string_from_padded(&buf, "test.field", 42).unwrap_err();
        match err {
            Error::InvalidUtf8InPhybString { field, offset } => {
                assert_eq!(field, "test.field");
                assert_eq!(offset, 42);
            }
            other => panic!("expected InvalidUtf8InPhybString, got {other:?}"),
        }
    }

    #[test]
    fn error_display_invalid_phyb_length_contains_both_fields() {
        let err = Error::InvalidPhybLength {
            found: 4,
            minimum: PHYB_STUB_SIZE,
        };
        let msg = format!("{err}");
        assert!(msg.contains('4'));
        assert!(msg.contains(&PHYB_STUB_SIZE.to_string()));
    }

    #[test]
    fn error_display_unsupported_phyb_version_contains_value() {
        let err = Error::UnsupportedPhybVersion {
            version: 0xDEAD_BEEF,
        };
        let msg = format!("{err}");
        assert!(
            msg.contains("0xdeadbeef") || msg.contains("0xDEADBEEF"),
            "version should be rendered in hex: {msg:?}",
        );
    }

    #[test]
    fn error_display_invalid_phyb_offset_contains_field_and_offset() {
        let err = Error::InvalidPhybOffset {
            field: "collision_offset",
            offset: 9999,
            file_size: 20,
        };
        let msg = format!("{err}");
        assert!(msg.contains("collision_offset"));
        assert!(msg.contains("9999"));
        assert!(msg.contains("20"));
    }

    #[test]
    fn error_display_invalid_phyb_count_contains_kind_and_values() {
        let err = Error::InvalidPhybCount {
            kind: "num_simulators",
            count: 9999,
            max: PHYB_MAX_RECORDS_PER_SECTION,
        };
        let msg = format!("{err}");
        assert!(msg.contains("num_simulators"));
        assert!(msg.contains("9999"));
        assert!(msg.contains(&PHYB_MAX_RECORDS_PER_SECTION.to_string()));
    }

    #[test]
    fn rejects_v0_stub_with_nonzero_data_type() {
        let mut bytes = Vec::with_capacity(PHYB_STUB_SIZE);
        bytes.extend_from_slice(&0u32.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(&0u32.to_le_bytes());
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybStub { field, value } => {
                assert_eq!(field, "data_type");
                assert_eq!(value, 1);
            }
            other => panic!("expected InvalidPhybStub, got {other:?}"),
        }
    }

    #[test]
    fn rejects_v0_stub_with_nonzero_section_offsets() {
        let mut bytes = Vec::with_capacity(PHYB_STUB_SIZE);
        bytes.extend_from_slice(&0u32.to_le_bytes());
        bytes.extend_from_slice(&0u32.to_le_bytes());
        bytes.extend_from_slice(&0x100u32.to_le_bytes());
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybStub { field, .. } => {
                assert_eq!(field, "section_offsets");
            }
            other => panic!("expected InvalidPhybStub, got {other:?}"),
        }
    }

    #[test]
    fn accepts_v0_all_zero_stub() {
        let bytes = build_stub_phyb();
        let phyb = Phyb::parse(&bytes).expect("parse");
        assert_eq!(phyb.version(), PhybVersion::V0);
        assert_eq!(phyb.data_type(), 0);
        assert_eq!(phyb.collision_offset(), 0);
        assert_eq!(phyb.simulator_offset(), 0);
    }

    #[test]
    fn rejects_v1_collision_offset_zero() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&PHYB_VERSION_FULL.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(&0u32.to_le_bytes());
        bytes.extend_from_slice(
            &u32::try_from(PHYB_FULL_HEADER_SIZE)
                .expect("16 fits u32")
                .to_le_bytes(),
        );
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybOffset { field, offset, .. } => {
                assert_eq!(field, "collision_offset");
                assert_eq!(offset, 0);
            }
            other => panic!("expected InvalidPhybOffset, got {other:?}"),
        }
    }

    #[test]
    fn rejects_v1_simulator_offset_zero() {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&PHYB_VERSION_FULL.to_le_bytes());
        bytes.extend_from_slice(&1u32.to_le_bytes());
        bytes.extend_from_slice(
            &u32::try_from(PHYB_FULL_HEADER_SIZE)
                .expect("16 fits u32")
                .to_le_bytes(),
        );
        bytes.extend_from_slice(&0u32.to_le_bytes());
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybOffset { field, offset, .. } => {
                assert_eq!(field, "simulator_offset");
                assert_eq!(offset, 0);
            }
            other => panic!("expected InvalidPhybOffset, got {other:?}"),
        }
    }

    #[test]
    fn rejects_v1_data_type_zero_with_extra_bytes() {
        let mut bytes = build_degenerate_v1_phyb();
        bytes.extend_from_slice(&[0xCC; 64]);
        let err = Phyb::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidPhybLength { found, minimum } => {
                assert_eq!(found, PHYB_FULL_HEADER_SIZE + 64);
                assert_eq!(minimum, PHYB_FULL_HEADER_SIZE);
            }
            other => panic!("expected InvalidPhybLength, got {other:?}"),
        }
    }

    #[test]
    fn ephb_substring_in_bone_name_does_not_false_positive() {
        let mut bytes = build_minimal_populated_v1_phyb();
        let payload_pre = bytes.len();
        bytes.extend_from_slice(b"EPHBx_collider");
        bytes.extend_from_slice(&[0u8; 80]);
        let phyb = Phyb::parse(&bytes).expect("parse");
        assert!(
            phyb.extended_bytes().is_none(),
            "EPHB substring in bone-name range must not trigger detection"
        );
        assert!(phyb.raw().len() >= payload_pre);
    }

    #[test]
    fn v0_with_trailing_bytes_does_not_misread_them() {
        let mut bytes = build_stub_phyb();
        bytes.extend_from_slice(&[0xAA; 64]);
        let phyb = Phyb::parse(&bytes).expect("parse");
        assert_eq!(phyb.version(), PhybVersion::V0);
        assert_eq!(phyb.raw().len(), bytes.len());
    }

    #[test]
    fn raw_borrows_entire_input_v1() {
        let bytes = build_minimal_populated_v1_phyb();
        let phyb = Phyb::parse(&bytes).expect("parse");
        assert_eq!(phyb.raw().as_ptr(), bytes.as_ptr());
        assert_eq!(phyb.raw().len(), bytes.len());
    }

    #[test]
    fn extended_bytes_pointer_identity() {
        let mut bytes = build_minimal_populated_v1_phyb();
        let ephb_offset = bytes.len();
        bytes.extend_from_slice(b"EPHB\x01\x00\x00\x01");
        let phyb = Phyb::parse(&bytes).expect("parse");
        let ext = phyb.extended_bytes().expect("ext present");
        let ext_start = ext.as_ptr().addr();
        let bytes_start = bytes.as_ptr().addr();
        assert_eq!(
            ext_start - bytes_start,
            ephb_offset,
            "extended_bytes must borrow from input at the EPHB offset",
        );
        assert_eq!(ext.len(), bytes.len() - ephb_offset);
    }

    #[test]
    fn error_display_invalid_phyb_stub_contains_field_and_value() {
        let err = Error::InvalidPhybStub {
            field: "data_type",
            value: 0x0000_0001,
        };
        let msg = format!("{err}");
        assert!(msg.contains("data_type"), "field name missing: {msg:?}");
        assert!(
            msg.contains("0x00000001") || msg.contains('1'),
            "value missing: {msg:?}",
        );
    }

    #[test]
    fn error_display_invalid_phyb_sub_array_overlap_contains_all_fields() {
        let err = crate::error::Error::InvalidPhybSubArrayOverlap {
            simulator_index: 2,
            prev_field: "phyb.chain.array",
            prev_end: 200,
            next_field: "phyb.simulator.pin",
            next_start: 180,
        };
        let msg = format!("{err}");
        assert!(msg.contains('2'), "simulator index missing: {msg:?}");
        assert!(
            msg.contains("phyb.chain.array"),
            "prev_field missing: {msg:?}",
        );
        assert!(msg.contains("200"), "prev_end missing: {msg:?}");
        assert!(
            msg.contains("phyb.simulator.pin"),
            "next_field missing: {msg:?}",
        );
        assert!(msg.contains("180"), "next_start missing: {msg:?}");
    }

    #[test]
    fn error_display_invalid_utf8_in_phyb_string_contains_field_and_offset() {
        let err = Error::InvalidUtf8InPhybString {
            field: "capsule.end_bone_name",
            offset: 128,
        };
        let msg = format!("{err}");
        assert!(
            msg.contains("capsule.end_bone_name"),
            "field missing: {msg:?}",
        );
        assert!(msg.contains("128"), "offset missing: {msg:?}");
    }
}
