use std::io::Cursor;

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

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

/// Byte length of the fixed `.eid` file header.
pub const EID_HEADER_SIZE: usize = 16;

/// On-disk byte length of a V1 entry (`[name(32), id, position, rotation, padding]`).
pub const EID_V1_ENTRY_SIZE: usize = 64;

/// On-disk byte length of an Old entry (`[id, position, rotation, name(12), padding]`).
pub const EID_OLD_ENTRY_SIZE: usize = 44;

/// Length of the name field in a V1 entry.
pub const EID_V1_NAME_LEN: usize = 32;

/// Length of the name field in an Old entry.
pub const EID_OLD_NAME_LEN: usize = 12;

/// Four-byte file magic: `b"eid\0"`.
pub const EID_MAGIC: [u8; 4] = *b"eid\0";

/// The `version1` field value that identifies a V1 format file.
///
/// Any other value is treated as [`EidVersion::Old`].
pub const EID_V1_VERSION1: i16 = 0x3132;

/// Maximum number of entries accepted in a single file.
pub const EID_MAX_ENTRIES: u32 = 4096;

/// Format variant of a `.eid` file.
///
/// V1 has 32-byte names and places the name field first in each entry.
/// Old has 12-byte names in a different field layout.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EidVersion {
    /// Current format: `version1 == `[`EID_V1_VERSION1`], 64-byte entries.
    V1,
    /// Legacy format: any other `version1` value, 44-byte entries.
    Old,
}

impl EidVersion {
    /// Maps the `version1` header field to an [`EidVersion`].
    ///
    /// Only [`EID_V1_VERSION1`] (`0x3132`) yields [`EidVersion::V1`].
    #[must_use]
    pub fn from_version1(version1: i16) -> Self {
        if version1 == EID_V1_VERSION1 {
            EidVersion::V1
        } else {
            EidVersion::Old
        }
    }

    /// Returns the on-disk byte length per entry for this version.
    #[must_use]
    pub fn entry_size(self) -> usize {
        match self {
            EidVersion::V1 => EID_V1_ENTRY_SIZE,
            EidVersion::Old => EID_OLD_ENTRY_SIZE,
        }
    }

    /// Returns the on-disk name field width for this version.
    #[must_use]
    pub fn name_len(self) -> usize {
        match self {
            EidVersion::V1 => EID_V1_NAME_LEN,
            EidVersion::Old => EID_OLD_NAME_LEN,
        }
    }
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireEidHeader {
    magic: [u8; 4],
    version1: i16,
    version2: i16,
    count: i32,
    unk1: u32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireEidV1Entry {
    name: [u8; EID_V1_NAME_LEN],
    id: i32,
    position: [f32; 3],
    rotation: [f32; 3],
    padding: i32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct WireEidOldEntry {
    id: i32,
    position: [f32; 3],
    rotation: [f32; 3],
    name: [u8; EID_OLD_NAME_LEN],
    padding: i32,
}

fn eid_string_from_padded(bytes: &[u8], 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::InvalidUtf8InEidString { offset })
}

/// A single decoded attachment point from a `.eid` file.
///
/// The field layout in memory is the same regardless of version; the
/// difference is only in the on-disk encoding.
#[derive(Debug, Clone, PartialEq)]
pub struct EidEntry {
    /// Decoded attachment point name (null-padding stripped).
    pub name: String,
    /// Attachment point identifier.
    pub id: i32,
    /// 3D position in model space.
    pub position: [f32; 3],
    /// Euler rotation in model space.
    pub rotation: [f32; 3],
    /// Reserved/padding field; preserved verbatim.
    pub padding: i32,
}

/// A parsed `.eid` equipment deformation file.
///
/// Borrows directly from the caller-supplied byte slice.
/// Use [`Eid::entries`] to access the decoded attachment point table.
#[derive(Debug, Clone)]
pub struct Eid<'a> {
    version: EidVersion,
    version1: i16,
    version2: i16,
    count: u32,
    unk1: u32,
    entries: Vec<EidEntry>,
    bytes: &'a [u8],
}

impl<'a> Eid<'a> {
    /// Parses a `.eid` file from a byte slice.
    ///
    /// Validates the magic, rejects negative or over-cap entry counts, routes
    /// to the correct entry decoder based on `version1`, and checks that the
    /// complete entry table fits within the input.
    ///
    /// # Panics
    ///
    /// Panics if the entry-table size arithmetic overflows, which cannot happen in
    /// practice because entry counts are bounded by [`EID_MAX_ENTRIES`].
    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
        let span = info_span!(span_names::EID_PARSE, bytes = bytes.len());
        let _enter = span.enter();
        let start = std::time::Instant::now();

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

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

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

        let Ok(count_u32) = u32::try_from(header.count) else {
            return Err(Error::InvalidEidEntryCount {
                count: u32::from_ne_bytes(header.count.to_ne_bytes()),
                max: EID_MAX_ENTRIES,
            });
        };

        if count_u32 > EID_MAX_ENTRIES {
            return Err(Error::InvalidEidEntryCount {
                count: count_u32,
                max: EID_MAX_ENTRIES,
            });
        }

        let version = EidVersion::from_version1(header.version1);
        let entry_size = version.entry_size();

        let entry_bytes = (count_u32 as usize).checked_mul(entry_size).expect(
            "entry-table arithmetic cannot overflow when count is bounded by EID_MAX_ENTRIES",
        );
        let table_end = EID_HEADER_SIZE.checked_add(entry_bytes).expect(
            "entry-table arithmetic cannot overflow when count is bounded by EID_MAX_ENTRIES",
        );
        if table_end > bytes.len() {
            return Err(Error::InvalidEidLength {
                found: bytes.len(),
                minimum: table_end,
            });
        }

        debug!(
            target: "eid",
            version = ?version,
            count = count_u32,
            entry_size,
            entries_offset = EID_HEADER_SIZE,
            "eid.entries.start",
        );

        let entries = read_entries(&mut cursor, version, count_u32)?;

        let elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
        info!(
            target: "eid",
            bytes = bytes.len(),
            version = ?version,
            header_size = EID_HEADER_SIZE,
            entry_count = count_u32,
            entries_offset = EID_HEADER_SIZE,
            elapsed_ms,
            "eid.parse",
        );

        Ok(Self {
            version,
            version1: header.version1,
            version2: header.version2,
            count: count_u32,
            unk1: header.unk1,
            entries,
            bytes,
        })
    }

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

    /// Raw `version1` field from the header.
    #[must_use]
    pub fn version1(&self) -> i16 {
        self.version1
    }

    /// Raw `version2` field from the header (semantics unknown; preserved verbatim).
    #[must_use]
    pub fn version2(&self) -> i16 {
        self.version2
    }

    /// Number of entries decoded from this file.
    #[must_use]
    pub fn count(&self) -> u32 {
        self.count
    }

    /// Unknown header field at offset 12; preserved verbatim.
    #[must_use]
    pub fn unk1(&self) -> u32 {
        self.unk1
    }

    /// Decoded attachment point entries.
    #[must_use]
    pub fn entries(&self) -> &[EidEntry] {
        &self.entries
    }

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

fn read_entries(
    cursor: &mut Cursor<&[u8]>,
    version: EidVersion,
    count: u32,
) -> Result<Vec<EidEntry>> {
    let capacity = usize::try_from(count).expect("u32 fits usize on supported targets");
    let mut entries = Vec::with_capacity(capacity);

    for index in 0..count {
        let record_offset = cursor.position();
        let entry = match version {
            EidVersion::V1 => {
                let record =
                    WireEidV1Entry::read(cursor).map_err(|e| Error::decode("eid.entry.v1", &e))?;
                let name = eid_string_from_padded(&record.name, record_offset)?;
                EidEntry {
                    name,
                    id: record.id,
                    position: record.position,
                    rotation: record.rotation,
                    padding: record.padding,
                }
            }
            EidVersion::Old => {
                let record = WireEidOldEntry::read(cursor)
                    .map_err(|e| Error::decode("eid.entry.old", &e))?;
                let name_offset = record_offset.saturating_add(0x1C);
                let name = eid_string_from_padded(&record.name, name_offset)?;
                EidEntry {
                    name,
                    id: record.id,
                    position: record.position,
                    rotation: record.rotation,
                    padding: record.padding,
                }
            }
        };

        debug!(
            target: "eid",
            index,
            id = entry.id,
            name = %entry.name,
            "eid.entry",
        );

        entries.push(entry);
    }

    Ok(entries)
}

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

    fn f32x3_bits_eq(a: [f32; 3], b: [f32; 3]) -> bool {
        a[0].to_bits() == b[0].to_bits()
            && a[1].to_bits() == b[1].to_bits()
            && a[2].to_bits() == b[2].to_bits()
    }

    fn build_header(version1: i16, count: i32, unk1: u32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(EID_HEADER_SIZE);
        buf.extend_from_slice(&EID_MAGIC);
        buf.extend_from_slice(&version1.to_le_bytes());
        buf.extend_from_slice(&0x0000i16.to_le_bytes());
        buf.extend_from_slice(&count.to_le_bytes());
        buf.extend_from_slice(&unk1.to_le_bytes());
        assert_eq!(buf.len(), EID_HEADER_SIZE);
        buf
    }

    fn pack_name(name: &str, width: usize) -> Vec<u8> {
        assert!(
            name.len() <= width,
            "name `{name}` does not fit in {width}-byte buffer"
        );
        let mut buf = vec![0u8; width];
        buf[..name.len()].copy_from_slice(name.as_bytes());
        buf
    }

    fn build_v1_entry(
        name: &str,
        id: i32,
        position: [f32; 3],
        rotation: [f32; 3],
        padding: i32,
    ) -> Vec<u8> {
        let mut buf = Vec::with_capacity(EID_V1_ENTRY_SIZE);
        buf.extend_from_slice(&pack_name(name, EID_V1_NAME_LEN));
        buf.extend_from_slice(&id.to_le_bytes());
        for v in position {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        for v in rotation {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        buf.extend_from_slice(&padding.to_le_bytes());
        assert_eq!(buf.len(), EID_V1_ENTRY_SIZE);
        buf
    }

    fn build_old_entry(
        name: &str,
        id: i32,
        position: [f32; 3],
        rotation: [f32; 3],
        padding: i32,
    ) -> Vec<u8> {
        let mut buf = Vec::with_capacity(EID_OLD_ENTRY_SIZE);
        buf.extend_from_slice(&id.to_le_bytes());
        for v in position {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        for v in rotation {
            buf.extend_from_slice(&v.to_le_bytes());
        }
        buf.extend_from_slice(&pack_name(name, EID_OLD_NAME_LEN));
        buf.extend_from_slice(&padding.to_le_bytes());
        assert_eq!(buf.len(), EID_OLD_ENTRY_SIZE);
        buf
    }

    #[test]
    fn v1_entry_size_matches_field_widths() {
        let computed = EID_V1_NAME_LEN
            + std::mem::size_of::<i32>()
            + 3 * std::mem::size_of::<f32>()
            + 3 * std::mem::size_of::<f32>()
            + std::mem::size_of::<i32>();
        assert_eq!(computed, EID_V1_ENTRY_SIZE, "V1 entry-size const drifted");
    }

    #[test]
    fn old_entry_size_matches_field_widths() {
        let computed = std::mem::size_of::<i32>()
            + 3 * std::mem::size_of::<f32>()
            + 3 * std::mem::size_of::<f32>()
            + EID_OLD_NAME_LEN
            + std::mem::size_of::<i32>();
        assert_eq!(computed, EID_OLD_ENTRY_SIZE, "Old entry-size const drifted");
    }

    #[test]
    fn header_size_matches_field_widths() {
        let computed = 4
            + std::mem::size_of::<i16>()
            + std::mem::size_of::<i16>()
            + std::mem::size_of::<i32>()
            + std::mem::size_of::<u32>();
        assert_eq!(computed, EID_HEADER_SIZE, "header-size const drifted");
    }

    #[test]
    fn version_from_version1_dispatch() {
        assert_eq!(EidVersion::from_version1(0x3132), EidVersion::V1);
        assert_eq!(EidVersion::from_version1(0x0000), EidVersion::Old);
        assert_eq!(EidVersion::from_version1(0x3131), EidVersion::Old);
        assert_eq!(EidVersion::from_version1(0x3133), EidVersion::Old);
        assert_eq!(EidVersion::from_version1(-1), EidVersion::Old);
        assert_eq!(EidVersion::from_version1(i16::MIN), EidVersion::Old);
        assert_eq!(EidVersion::from_version1(i16::MAX), EidVersion::Old);
    }

    #[test]
    fn version_entry_size_dispatch() {
        assert_eq!(EidVersion::V1.entry_size(), EID_V1_ENTRY_SIZE);
        assert_eq!(EidVersion::Old.entry_size(), EID_OLD_ENTRY_SIZE);
    }

    #[test]
    fn version_name_len_dispatch() {
        assert_eq!(EidVersion::V1.name_len(), EID_V1_NAME_LEN);
        assert_eq!(EidVersion::Old.name_len(), EID_OLD_NAME_LEN);
    }

    #[test]
    fn v1_round_trip_populates_every_entry_field() {
        let name = "n_root";
        let id = 0x1234_5678i32;
        let position = [1.5f32, -2.25f32, 3.75f32];
        let rotation = [0.125f32, 0.5f32, -0.75f32];
        let padding = i32::from_ne_bytes(0x0BAD_F00Du32.to_ne_bytes());

        let mut bytes = build_header(EID_V1_VERSION1, 1, 0xDEAD_BEEF);
        bytes.extend_from_slice(&build_v1_entry(name, id, position, rotation, padding));

        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.version(), EidVersion::V1);
        assert_eq!(eid.version1(), EID_V1_VERSION1);
        assert_eq!(eid.version2(), 0);
        assert_eq!(eid.count(), 1);
        assert_eq!(eid.unk1(), 0xDEAD_BEEF);
        assert_eq!(eid.entries().len(), 1);

        let entry = &eid.entries()[0];
        assert_eq!(entry.name, name, "name round-trip");
        assert_eq!(entry.id, id, "id round-trip");
        assert!(
            f32x3_bits_eq(entry.position, position),
            "position round-trip: got {:?}, expected {:?}",
            entry.position,
            position,
        );
        assert!(
            f32x3_bits_eq(entry.rotation, rotation),
            "rotation round-trip: got {:?}, expected {:?}",
            entry.rotation,
            rotation,
        );
        assert_eq!(entry.padding, padding, "padding round-trip");
    }

    #[test]
    fn old_round_trip_populates_every_entry_field() {
        let name = "j_kao";
        let id = -42i32;
        let position = [10.0f32, 20.0f32, 30.0f32];
        let rotation = [90.0f32, 45.0f32, 180.0f32];
        let padding = 0x1234_5678i32;

        let mut bytes = build_header(0x0000, 1, 0xCAFE_F00D);
        bytes.extend_from_slice(&build_old_entry(name, id, position, rotation, padding));

        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.version(), EidVersion::Old);
        assert_eq!(eid.version1(), 0);
        assert_eq!(eid.count(), 1);
        assert_eq!(eid.unk1(), 0xCAFE_F00D);
        assert_eq!(eid.entries().len(), 1);

        let entry = &eid.entries()[0];
        assert_eq!(entry.name, name, "name round-trip");
        assert_eq!(entry.id, id, "id round-trip");
        assert!(
            f32x3_bits_eq(entry.position, position),
            "position round-trip: got {:?}, expected {:?}",
            entry.position,
            position,
        );
        assert!(
            f32x3_bits_eq(entry.rotation, rotation),
            "rotation round-trip: got {:?}, expected {:?}",
            entry.rotation,
            rotation,
        );
        assert_eq!(entry.padding, padding, "padding round-trip");
    }

    #[test]
    fn v1_multiple_entries_decode_in_order() {
        let entries = [
            ("n_root", 0i32, [0.0f32; 3], [0.0f32; 3], 0i32),
            ("n_hara", 1i32, [1.0f32, 2.0f32, 3.0f32], [0.1f32; 3], 0i32),
            (
                "j_kao",
                2i32,
                [-1.0f32, -2.0f32, -3.0f32],
                [0.5f32; 3],
                0i32,
            ),
        ];

        let count = i32::try_from(entries.len()).expect("count fits i32");
        let mut bytes = build_header(EID_V1_VERSION1, count, 0);
        for (name, id, pos, rot, pad) in entries {
            bytes.extend_from_slice(&build_v1_entry(name, id, pos, rot, pad));
        }

        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.entries().len(), entries.len());
        for (parsed, expected) in eid.entries().iter().zip(entries.iter()) {
            assert_eq!(parsed.name, expected.0);
            assert_eq!(parsed.id, expected.1);
            assert!(
                f32x3_bits_eq(parsed.position, expected.2),
                "position round-trip"
            );
            assert!(
                f32x3_bits_eq(parsed.rotation, expected.3),
                "rotation round-trip"
            );
            assert_eq!(parsed.padding, expected.4);
        }
    }

    #[test]
    fn old_multiple_entries_decode_in_order() {
        let entries = [
            ("a", 0i32, [0.0f32; 3], [0.0f32; 3], 0i32),
            ("bone_2", 5i32, [1.0f32, 2.0f32, 3.0f32], [10.0f32; 3], 0i32),
        ];

        let count = i32::try_from(entries.len()).expect("count fits i32");
        let mut bytes = build_header(0x0000, count, 0);
        for (name, id, pos, rot, pad) in entries {
            bytes.extend_from_slice(&build_old_entry(name, id, pos, rot, pad));
        }

        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.entries().len(), entries.len());
        for (parsed, expected) in eid.entries().iter().zip(entries.iter()) {
            assert_eq!(parsed.name, expected.0);
            assert_eq!(parsed.id, expected.1);
        }
    }

    #[test]
    fn empty_entry_table_parses_cleanly() {
        let bytes = build_header(EID_V1_VERSION1, 0, 0);
        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.count(), 0);
        assert!(eid.entries().is_empty());
        assert_eq!(eid.version(), EidVersion::V1);
    }

    #[test]
    fn count_at_cap_parses_cleanly() {
        let count = EID_MAX_ENTRIES;
        let count_i32 = i32::try_from(count).expect("cap fits i32");
        let mut bytes = build_header(0x0000, count_i32, 0);
        for _ in 0..count {
            bytes.extend_from_slice(&build_old_entry("n_root", 0, [0.0; 3], [0.0; 3], 0));
        }
        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.count(), count);
        assert_eq!(eid.entries().len(), count as usize);
    }

    #[test]
    fn count_above_cap_rejected() {
        let count = EID_MAX_ENTRIES + 1;
        let count_i32 = i32::try_from(count).expect("cap+1 fits i32");
        let bytes = build_header(EID_V1_VERSION1, count_i32, 0);
        let err = Eid::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidEidEntryCount { count: got, max } => {
                assert_eq!(got, count);
                assert_eq!(max, EID_MAX_ENTRIES);
            }
            other => panic!("expected InvalidEidEntryCount, got {other:?}"),
        }
    }

    #[test]
    fn negative_count_rejected() {
        let bytes = build_header(EID_V1_VERSION1, -1i32, 0);
        let err = Eid::parse(&bytes).unwrap_err();
        assert!(
            matches!(err, Error::InvalidEidEntryCount { .. }),
            "expected InvalidEidEntryCount, got {err:?}"
        );
    }

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

    #[test]
    fn rejects_slice_too_short_for_header() {
        let bytes = vec![0u8; EID_HEADER_SIZE - 1];
        let err = Eid::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidEidLength {
                found: 15,
                minimum: EID_HEADER_SIZE,
            }
        ));
    }

    #[test]
    fn rejects_wrong_magic() {
        let mut bytes = build_header(EID_V1_VERSION1, 0, 0);
        bytes[0..4].copy_from_slice(b"NOPE");
        let err = Eid::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidEidMagic { found } => assert_eq!(&found, b"NOPE"),
            other => panic!("expected InvalidEidMagic, got {other:?}"),
        }
    }

    #[test]
    fn rejects_v1_entry_table_overrunning_input() {
        let bytes = build_header(EID_V1_VERSION1, 1, 0);
        let err = Eid::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidEidLength {
                found: EID_HEADER_SIZE,
                minimum: _,
            }
        ));
        if let Error::InvalidEidLength { minimum, .. } = err {
            assert_eq!(minimum, EID_HEADER_SIZE + EID_V1_ENTRY_SIZE);
        }
    }

    #[test]
    fn rejects_old_entry_table_overrunning_input() {
        let bytes = build_header(0x0000, 1, 0);
        let err = Eid::parse(&bytes).unwrap_err();
        if let Error::InvalidEidLength { minimum, .. } = err {
            assert_eq!(minimum, EID_HEADER_SIZE + EID_OLD_ENTRY_SIZE);
        } else {
            panic!("expected InvalidEidLength, got {err:?}");
        }
    }

    #[test]
    fn rejects_invalid_utf8_in_v1_name() {
        let mut name_buf = vec![0u8; EID_V1_NAME_LEN];
        name_buf[0] = 0xFF;
        name_buf[1] = 0xFE;

        let mut bytes = build_header(EID_V1_VERSION1, 1, 0);
        bytes.extend_from_slice(&name_buf);
        bytes.extend_from_slice(&0i32.to_le_bytes());
        for _ in 0..3 {
            bytes.extend_from_slice(&0f32.to_le_bytes());
        }
        for _ in 0..3 {
            bytes.extend_from_slice(&0f32.to_le_bytes());
        }
        bytes.extend_from_slice(&0i32.to_le_bytes());

        let err = Eid::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidUtf8InEidString { offset } => {
                assert_eq!(offset, EID_HEADER_SIZE as u64);
            }
            other => panic!("expected InvalidUtf8InEidString, got {other:?}"),
        }
    }

    #[test]
    fn rejects_invalid_utf8_in_old_name() {
        let mut bytes = build_header(0x0000, 1, 0);
        bytes.extend_from_slice(&0i32.to_le_bytes());
        for _ in 0..3 {
            bytes.extend_from_slice(&0f32.to_le_bytes());
        }
        for _ in 0..3 {
            bytes.extend_from_slice(&0f32.to_le_bytes());
        }
        let mut name_buf = vec![0u8; EID_OLD_NAME_LEN];
        name_buf[0] = 0xC0;
        name_buf[1] = 0x80;
        bytes.extend_from_slice(&name_buf);
        bytes.extend_from_slice(&0i32.to_le_bytes());

        let err = Eid::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidUtf8InEidString { offset } => {
                assert_eq!(offset, (EID_HEADER_SIZE + 0x1C) as u64);
            }
            other => panic!("expected InvalidUtf8InEidString, got {other:?}"),
        }
    }

    #[test]
    fn u32_max_count_adversarial_rejected_without_overflow() {
        let bytes = build_header(EID_V1_VERSION1, i32::MAX, 0);
        let err = Eid::parse(&bytes).unwrap_err();
        match err {
            Error::InvalidEidEntryCount { count, max } => {
                assert_eq!(count, i32::MAX as u32);
                assert_eq!(max, EID_MAX_ENTRIES);
            }
            other => panic!("expected InvalidEidEntryCount, got {other:?}"),
        }
    }

    #[test]
    fn unsupported_version_variant_is_constructible() {
        let err = Error::UnsupportedEidVersion { version: 0xDEAD };
        let msg = format!("{err}");
        assert!(msg.contains("EID"), "Display: {msg}");
    }

    #[test]
    fn raw_borrows_entire_input() {
        let mut bytes = build_header(EID_V1_VERSION1, 1, 0);
        bytes.extend_from_slice(&build_v1_entry("n_root", 0, [0.0; 3], [0.0; 3], 0));

        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(
            eid.raw().as_ptr(),
            bytes.as_ptr(),
            "raw() must point to the start of the input"
        );
        assert_eq!(
            eid.raw().len(),
            bytes.len(),
            "raw() must span the full input length"
        );
    }

    #[test]
    fn raw_borrows_entire_input_old() {
        let mut bytes = build_header(0x0000, 1, 0);
        bytes.extend_from_slice(&build_old_entry("n_root", 0, [0.0; 3], [0.0; 3], 0));

        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.raw().as_ptr(), bytes.as_ptr());
        assert_eq!(eid.raw().len(), bytes.len());
    }

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

    #[test]
    fn error_display_invalid_eid_magic_contains_expected_bytes() {
        let err = Error::InvalidEidMagic { found: *b"NOPE" };
        let msg = format!("{err}");
        assert!(
            msg.contains("eid"),
            "Display must mention the expected magic: {msg:?}"
        );
        assert!(
            msg.contains("78"),
            "Display must include the found bytes (78 = 'N'): {msg:?}"
        );
    }

    #[test]
    fn error_display_invalid_eid_entry_count_contains_both_fields() {
        let err = Error::InvalidEidEntryCount {
            count: 9999,
            max: EID_MAX_ENTRIES,
        };
        let msg = format!("{err}");
        assert!(msg.contains("9999"), "Display must include count: {msg:?}");
        assert!(
            msg.contains(&EID_MAX_ENTRIES.to_string()),
            "Display must include max: {msg:?}"
        );
    }

    #[test]
    fn error_display_invalid_utf8_in_eid_string_contains_offset() {
        let err = Error::InvalidUtf8InEidString { offset: 0x42 };
        let msg = format!("{err}");
        assert!(
            msg.contains("66"),
            "Display must include decimal offset: {msg:?}"
        );
    }

    #[test]
    fn error_display_unsupported_eid_version_contains_version() {
        let err = Error::UnsupportedEidVersion { version: 0xDEAD };
        let msg = format!("{err}");
        assert!(
            msg.contains("DEAD") || msg.contains("dead") || msg.contains("57005"),
            "Display must include version: {msg:?}"
        );
    }

    #[test]
    fn padding_slot_preserved_v1() {
        let sentinel = 0x7FFF_FFFFi32;
        let mut bytes = build_header(EID_V1_VERSION1, 1, 0);
        bytes.extend_from_slice(&build_v1_entry("n_root", 0, [0.0; 3], [0.0; 3], sentinel));
        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.entries()[0].padding, sentinel);
    }

    #[test]
    fn padding_slot_preserved_old() {
        let sentinel = -0x4000_0000i32;
        let mut bytes = build_header(0x0000, 1, 0);
        bytes.extend_from_slice(&build_old_entry("n_root", 0, [0.0; 3], [0.0; 3], sentinel));
        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.entries()[0].padding, sentinel);
    }

    #[test]
    fn version2_preserved_verbatim() {
        let mut bytes = Vec::with_capacity(EID_HEADER_SIZE);
        bytes.extend_from_slice(&EID_MAGIC);
        bytes.extend_from_slice(&EID_V1_VERSION1.to_le_bytes());
        bytes.extend_from_slice(&0x7FFFi16.to_le_bytes());
        bytes.extend_from_slice(&0i32.to_le_bytes());
        bytes.extend_from_slice(&0u32.to_le_bytes());
        assert_eq!(bytes.len(), EID_HEADER_SIZE);

        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.version2(), 0x7FFF);
    }

    #[test]
    fn unk1_preserved_verbatim() {
        let bytes = build_header(EID_V1_VERSION1, 0, 0xCAFE_BABE);
        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.unk1(), 0xCAFE_BABE);
    }

    #[test]
    fn empty_name_decodes_to_empty_string() {
        let mut bytes = build_header(EID_V1_VERSION1, 1, 0);
        bytes.extend_from_slice(&build_v1_entry("", 0, [0.0; 3], [0.0; 3], 0));
        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.entries()[0].name, "");
    }

    #[test]
    fn fully_packed_v1_name_decodes_without_terminator() {
        let full_name = "0123456789abcdef0123456789ABCDEF";
        assert_eq!(full_name.len(), EID_V1_NAME_LEN);

        let mut bytes = build_header(EID_V1_VERSION1, 1, 0);
        bytes.extend_from_slice(&build_v1_entry(full_name, 0, [0.0; 3], [0.0; 3], 0));
        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.entries()[0].name, full_name);
    }

    #[test]
    fn fully_packed_old_name_decodes_without_terminator() {
        let full_name = "012345678901";
        assert_eq!(full_name.len(), EID_OLD_NAME_LEN);

        let mut bytes = build_header(0x0000, 1, 0);
        bytes.extend_from_slice(&build_old_entry(full_name, 0, [0.0; 3], [0.0; 3], 0));
        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.entries()[0].name, full_name);
    }

    #[test]
    fn position_special_floats_round_trip() {
        let position = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY];
        let mut bytes = build_header(EID_V1_VERSION1, 1, 0);
        bytes.extend_from_slice(&build_v1_entry("n_root", 0, position, [0.0; 3], 0));
        let eid = Eid::parse(&bytes).expect("parse");
        let parsed = eid.entries()[0].position;
        assert!(parsed[0].is_nan(), "NaN must survive: {parsed:?}");
        assert!(parsed[1].is_infinite() && parsed[1].is_sign_positive());
        assert!(parsed[2].is_infinite() && parsed[2].is_sign_negative());
    }

    #[test]
    fn rotation_special_floats_round_trip_v1() {
        let rotation = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY];
        let mut bytes = build_header(EID_V1_VERSION1, 1, 0);
        bytes.extend_from_slice(&build_v1_entry("n_root", 0, [0.0; 3], rotation, 0));
        let eid = Eid::parse(&bytes).expect("parse");
        let parsed = eid.entries()[0].rotation;
        assert!(
            parsed[0].is_nan(),
            "NaN must survive in V1 rotation: {parsed:?}"
        );
        assert!(
            parsed[1].is_infinite() && parsed[1].is_sign_positive(),
            "+inf must survive in V1 rotation: {parsed:?}",
        );
        assert!(
            parsed[2].is_infinite() && parsed[2].is_sign_negative(),
            "-inf must survive in V1 rotation: {parsed:?}",
        );
    }

    #[test]
    fn rotation_special_floats_round_trip_old() {
        let rotation = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY];
        let mut bytes = build_header(0x0000, 1, 0);
        bytes.extend_from_slice(&build_old_entry("n_root", 0, [0.0; 3], rotation, 0));
        let eid = Eid::parse(&bytes).expect("parse");
        let parsed = eid.entries()[0].rotation;
        assert!(
            parsed[0].is_nan(),
            "NaN must survive in Old rotation: {parsed:?}"
        );
        assert!(
            parsed[1].is_infinite() && parsed[1].is_sign_positive(),
            "+inf must survive in Old rotation: {parsed:?}",
        );
        assert!(
            parsed[2].is_infinite() && parsed[2].is_sign_negative(),
            "-inf must survive in Old rotation: {parsed:?}",
        );
    }

    #[test]
    fn v1_bytes_misrouted_through_old_decoder_field_values_differ() {
        let name = "n_root";
        let id_v1 = 0x4141_4141i32;
        let position = [1.0f32, 2.0f32, 3.0f32];
        let rotation = [0.1f32, 0.2f32, 0.3f32];

        let mut v1_bytes = build_header(EID_V1_VERSION1, 1, 0);
        v1_bytes.extend_from_slice(&build_v1_entry(name, id_v1, position, rotation, 0));
        let eid_v1 = Eid::parse(&v1_bytes).expect("V1 parse");
        assert_eq!(eid_v1.version(), EidVersion::V1, "must route as V1");
        assert_eq!(eid_v1.entries()[0].id, id_v1, "V1 id must round-trip");
        assert_eq!(eid_v1.entries()[0].name, name, "V1 name must round-trip");

        let mut old_misrouted = build_header(0x0000, 1, 0);
        old_misrouted.extend_from_slice(&v1_bytes[EID_HEADER_SIZE..]);
        let eid_old = Eid::parse(&old_misrouted).expect("Old-misrouted parse must succeed");
        assert_eq!(eid_old.version(), EidVersion::Old, "must route as Old");
        let expected_garbled_id = i32::from_le_bytes(*b"n_ro");
        assert_eq!(
            eid_old.entries()[0].id,
            expected_garbled_id,
            "misrouted Old id must equal the garbled first-4-bytes-of-V1-name value",
        );
        assert_ne!(
            eid_old.entries()[0].id,
            id_v1,
            "misrouted Old id must NOT equal the original V1 id",
        );
    }

    #[test]
    fn trailing_bytes_after_entry_table_ignored() {
        let mut bytes = build_header(EID_V1_VERSION1, 1, 0);
        bytes.extend_from_slice(&build_v1_entry("n_root", 42, [1.0; 3], [0.5; 3], 0));
        let exact_len = bytes.len();

        bytes.push(0u8);

        let eid = Eid::parse(&bytes).expect("trailing byte must not cause a parse failure");
        assert_eq!(eid.entries().len(), 1);
        assert_eq!(eid.entries()[0].id, 42);
        assert_eq!(eid.entries()[0].name, "n_root");
        assert_eq!(eid.raw().len(), exact_len + 1);
        assert_eq!(eid.raw().as_ptr(), bytes.as_ptr());
    }

    #[test]
    fn old_multiple_entries_all_fields_decoded_in_order() {
        let entries = [
            (
                "a",
                7i32,
                [1.5f32, -2.25f32, 3.75f32],
                [90.0f32, 45.0f32, 0.0f32],
                0x1234_5678i32,
            ),
            (
                "bone_2",
                -99i32,
                [10.0f32, 20.0f32, 30.0f32],
                [180.0f32, 270.0f32, 360.0f32],
                -1i32,
            ),
        ];

        let count = i32::try_from(entries.len()).expect("count fits i32");
        let mut bytes = build_header(0x0000, count, 0);
        for (name, id, pos, rot, pad) in entries {
            bytes.extend_from_slice(&build_old_entry(name, id, pos, rot, pad));
        }

        let eid = Eid::parse(&bytes).expect("parse");
        assert_eq!(eid.version(), EidVersion::Old);
        assert_eq!(eid.entries().len(), entries.len());
        for (parsed, (exp_name, exp_id, exp_pos, exp_rot, exp_pad)) in
            eid.entries().iter().zip(entries.iter())
        {
            assert_eq!(parsed.name, *exp_name, "name");
            assert_eq!(parsed.id, *exp_id, "id");
            assert!(
                f32x3_bits_eq(parsed.position, *exp_pos),
                "position round-trip: got {:?}, expected {:?}",
                parsed.position,
                exp_pos,
            );
            assert!(
                f32x3_bits_eq(parsed.rotation, *exp_rot),
                "rotation round-trip: got {:?}, expected {:?}",
                parsed.rotation,
                exp_rot,
            );
            assert_eq!(parsed.padding, *exp_pad, "padding");
        }
    }
}
