//! EXD reader — per-page, per-language row data (`exd/<Sheet>_<page>[_<lang>].exd`).
//!
//! An EXD file holds the actual row payloads for one page/language combination
//! of an Excel sheet. The file begins with a fixed 32-byte header, then an
//! index table of `(row_id, file_offset)` entries sorted by ascending row ID,
//! followed by the row payloads themselves.
//!
//! Each row payload starts with a 6-byte row header containing a `data_size`
//! and a `sub_row_count`, then the fixed-size column struct (one per sub-row
//! for [`SheetKind::SubRows`] sheets), and finally a variable-length trailing
//! area used for string data.
//!
//! [`ExcelData::parse`] validates the index, checks bounds, and builds an
//! in-memory index for `O(log n)` row lookups. The raw bytes are retained so
//! that [`Row`] and [`Field`] can return zero-copy string slices.

use std::io::Cursor;

use binrw::BinRead;
use tracing::{debug, debug_span, warn};

use crate::error::{Error, Result};
use crate::excel::{ColumnDataType, ColumnDefinition, ExcelHeader, Language, SheetKind};

const EXD_MAGIC: [u8; 4] = *b"EXDF";

const EXD_VERSION: u16 = 2;

const EXD_FIXED_HEADER_SIZE: usize = 0x20;

const EXD_INDEX_ENTRY_SIZE: usize = 8;

const EXD_ROW_HEADER_SIZE: usize = 6;

/// Parsed row data for one page/language combination of an Excel sheet.
///
/// Holds the raw file bytes and a sorted in-memory index so individual rows
/// can be retrieved in `O(log n)` time via [`ExcelData::row`] or iterated in
/// file order via [`ExcelData::rows`].
///
/// Obtain an `ExcelData` from [`Sheet::page`](crate::excel::Sheet::page) or
/// directly via
/// [`SqPack::excel_data`](crate::sqpack::SqPack::excel_data).
#[derive(Debug, Clone)]
pub struct ExcelData {
    raw: Vec<u8>,
    language: Language,
    version: u16,
    row_size: u16,
    kind: SheetKind,
    columns: Vec<ColumnDefinition>,
    rows: Box<[RowIndexEntry]>,
    total_sub_row_count: usize,
}

impl ExcelData {
    /// Parse an EXD file from raw bytes, using `header` for schema and kind context.
    ///
    /// `language` is recorded as metadata; it does not affect parsing but is
    /// surfaced via [`ExcelData::language`] and used by [`Sheet`](crate::excel::Sheet)
    /// for file-path construction.
    ///
    /// # Panics
    ///
    /// Panics if internal size constants exceed `u32::MAX`, which cannot occur
    /// in practice as they are fixed small values.
    #[allow(clippy::too_many_lines)]
    pub fn parse(bytes: &[u8], header: &ExcelHeader, language: Language) -> Result<Self> {
        let span = debug_span!("exd.parse", bytes = bytes.len());
        let _enter = span.enter();

        if bytes.len() < EXD_FIXED_HEADER_SIZE {
            return Err(Error::InvalidExdLength {
                expected_at_least: EXD_FIXED_HEADER_SIZE,
                found: bytes.len(),
            });
        }

        let wire = {
            let mut cursor = Cursor::new(&bytes[..EXD_FIXED_HEADER_SIZE]);
            ExdWireHeader::read(&mut cursor).map_err(|e| Error::decode("exd_header", &e))?
        };

        if wire.magic != EXD_MAGIC {
            return Err(Error::InvalidMagic);
        }
        if wire.version != EXD_VERSION {
            return Err(Error::UnsupportedExdVersion {
                version: wire.version,
            });
        }

        let index_size = wire.index_size as usize;

        if index_size % EXD_INDEX_ENTRY_SIZE != 0 {
            return Err(Error::MisalignedIndexData {
                size: wire.index_size,
                entry_size: u32::try_from(EXD_INDEX_ENTRY_SIZE)
                    .expect("EXD_INDEX_ENTRY_SIZE fits u32"),
            });
        }

        let expected_at_least =
            EXD_FIXED_HEADER_SIZE
                .checked_add(index_size)
                .ok_or(Error::InvalidExdLength {
                    expected_at_least: usize::MAX,
                    found: bytes.len(),
                })?;
        if bytes.len() < expected_at_least {
            return Err(Error::InvalidExdLength {
                expected_at_least,
                found: bytes.len(),
            });
        }

        let entry_count = index_size / EXD_INDEX_ENTRY_SIZE;
        let entry_count_capped = entry_count.min(bytes.len() / EXD_INDEX_ENTRY_SIZE);
        let mut rows: Vec<RowIndexEntry> = Vec::with_capacity(entry_count_capped);
        let data_region_start = EXD_FIXED_HEADER_SIZE + index_size;
        let file_end = bytes.len();

        let mut cursor = Cursor::new(&bytes[EXD_FIXED_HEADER_SIZE..data_region_start]);
        let mut total_sub_row_count: usize = 0;
        let mut last_row_id: Option<u32> = None;
        for _ in 0..entry_count {
            let wire_entry = ExdWireIndexEntry::read(&mut cursor)
                .map_err(|e| Error::decode("exd_index_entry", &e))?;

            if let Some(prev) = last_row_id {
                if wire_entry.row_id <= prev {
                    return Err(Error::MalformedExdRow {
                        row_id: 0,
                        reason: "index entries not sorted by row_id",
                    });
                }
            }
            last_row_id = Some(wire_entry.row_id);

            let row_offset = wire_entry.offset as usize;
            if row_offset < data_region_start
                || row_offset
                    .checked_add(EXD_ROW_HEADER_SIZE)
                    .is_none_or(|end| end > file_end)
            {
                return Err(Error::MalformedExdRow {
                    row_id: wire_entry.row_id,
                    reason: "row offset out of bounds",
                });
            }

            let row_header_bytes: [u8; EXD_ROW_HEADER_SIZE] = bytes
                [row_offset..row_offset + EXD_ROW_HEADER_SIZE]
                .try_into()
                .expect("slice is exactly EXD_ROW_HEADER_SIZE bytes");
            let data_size = u32::from_be_bytes(
                row_header_bytes[0..4]
                    .try_into()
                    .expect("first 4 bytes of row header"),
            );
            let sub_row_count = u16::from_be_bytes(
                row_header_bytes[4..6]
                    .try_into()
                    .expect("last 2 bytes of row header"),
            );

            let payload_end = row_offset
                .checked_add(EXD_ROW_HEADER_SIZE)
                .and_then(|after_header| after_header.checked_add(data_size as usize))
                .ok_or(Error::MalformedExdRow {
                    row_id: wire_entry.row_id,
                    reason: "row data_size overflows file",
                })?;
            if payload_end > file_end {
                return Err(Error::MalformedExdRow {
                    row_id: wire_entry.row_id,
                    reason: "row data_size overruns file",
                });
            }

            let stored_sub_row_count: u16 = match header.kind() {
                SheetKind::Default => {
                    if sub_row_count != 1 {
                        warn!(
                            row_id = wire_entry.row_id,
                            row_count = sub_row_count,
                            "default-sheet row carries row_count != 1; clamping to 1",
                        );
                    }
                    1
                }
                SheetKind::SubRows => sub_row_count,
            };
            let effective_sub_count: usize = match header.kind() {
                SheetKind::Default => 1,
                SheetKind::SubRows => sub_row_count as usize,
            };
            total_sub_row_count = total_sub_row_count.checked_add(effective_sub_count).ok_or(
                Error::MalformedExdRow {
                    row_id: wire_entry.row_id,
                    reason: "sub-row count sum overflows usize",
                },
            )?;

            rows.push(RowIndexEntry {
                row_id: wire_entry.row_id,
                file_offset: row_offset,
                payload_end,
                sub_row_count: stored_sub_row_count,
            });
        }

        debug!(
            version = wire.version,
            rows = rows.len(),
            total_sub_row_count,
            ?language,
            "parsed EXD",
        );

        Ok(Self {
            raw: bytes.to_vec(),
            language,
            version: wire.version,
            row_size: header.row_size(),
            kind: header.kind(),
            columns: header.columns().to_vec(),
            rows: rows.into_boxed_slice(),
            total_sub_row_count,
        })
    }

    /// Language this page was loaded for.
    #[must_use]
    pub fn language(&self) -> Language {
        self.language
    }

    /// EXD format version; always `2` for currently supported files.
    #[must_use]
    pub fn version(&self) -> u16 {
        self.version
    }

    /// Number of top-level rows (index entries) in this page.
    #[must_use]
    pub fn row_count(&self) -> usize {
        self.rows.len()
    }

    /// Total number of individually addressable sub-rows across all top-level rows.
    ///
    /// For [`SheetKind::Default`] sheets this equals [`row_count`](Self::row_count).
    #[must_use]
    pub fn total_sub_row_count(&self) -> usize {
        self.total_sub_row_count
    }

    /// Sheet variant as reflected from the originating [`ExcelHeader`].
    #[must_use]
    pub fn sheet_kind(&self) -> SheetKind {
        self.kind
    }

    /// Iterate over all top-level rows in file (index) order.
    ///
    /// For [`SheetKind::SubRows`] sheets each item is a parent row without a
    /// sub-row ID; use [`Row::sub_rows`] or [`ExcelData::sub_row`] to reach
    /// individual sub-rows.
    pub fn rows(&self) -> impl Iterator<Item = Row<'_>> {
        self.rows.iter().map(move |entry| self.row_for(entry, None))
    }

    /// Return the top-level row at file-order index `idx`, or `None` if out of range.
    #[must_use]
    pub fn row_at(&self, idx: usize) -> Option<Row<'_>> {
        let entry = self.rows.get(idx)?;
        Some(self.row_for(entry, None))
    }

    /// Look up a top-level row by ID in `O(log n)`, or `None` if absent.
    #[must_use]
    pub fn row(&self, row_id: u32) -> Option<Row<'_>> {
        let idx = self
            .rows
            .binary_search_by_key(&row_id, |entry| entry.row_id)
            .ok()?;
        Some(self.row_for(&self.rows[idx], None))
    }

    /// Look up a specific sub-row by `(row_id, sub_id)` in `O(log n)`.
    ///
    /// Always returns `None` on [`SheetKind::Default`] sheets.
    #[must_use]
    pub fn sub_row(&self, row_id: u32, sub_id: u16) -> Option<Row<'_>> {
        if self.kind != SheetKind::SubRows {
            return None;
        }
        let idx = self
            .rows
            .binary_search_by_key(&row_id, |entry| entry.row_id)
            .ok()?;
        let entry = &self.rows[idx];
        if sub_id >= entry.sub_row_count {
            return None;
        }
        Some(self.row_for(entry, Some(sub_id)))
    }

    fn row_for<'a>(&'a self, entry: &'a RowIndexEntry, sub_row_id: Option<u16>) -> Row<'a> {
        Row {
            data: self,
            entry,
            sub_row_id,
        }
    }
}

/// A borrowed view into one row (or sub-row) within an [`ExcelData`] page.
///
/// The row borrows data from its parent [`ExcelData`]; it cannot outlive that
/// value. To obtain an owned, independently-living row use
/// [`Sheet::row`](crate::excel::Sheet::row) or
/// [`Sheet::rows`](crate::excel::Sheet::rows), which return [`OwnedRow`](crate::excel::OwnedRow).
#[derive(Debug, Clone, Copy)]
pub struct Row<'a> {
    data: &'a ExcelData,
    entry: &'a RowIndexEntry,
    sub_row_id: Option<u16>,
}

impl<'a> Row<'a> {
    /// Top-level row ID as stored in the EXD index.
    #[must_use]
    pub fn row_id(&self) -> u32 {
        self.entry.row_id
    }

    /// Sub-row ID within a [`SheetKind::SubRows`] sheet, or `None` for a
    /// top-level parent row or a [`SheetKind::Default`] row.
    #[must_use]
    pub fn sub_row_id(&self) -> Option<u16> {
        self.sub_row_id
    }

    /// Number of sub-rows in this top-level row.
    ///
    /// Always `1` for [`SheetKind::Default`] sheets.
    #[must_use]
    pub fn sub_row_count(&self) -> u16 {
        self.entry.sub_row_count
    }

    /// Number of columns as declared by the originating [`ExcelHeader`].
    #[must_use]
    pub fn column_count(&self) -> usize {
        self.data.columns.len()
    }

    /// Decode the value at `column_idx`, returning a borrowed [`Field`].
    ///
    /// Strings in `Field::String` borrow directly from the page's raw bytes
    /// and therefore have the same lifetime as this `Row`.
    pub fn field(&self, column_idx: usize) -> Result<Field<'a>> {
        let column = *self
            .data
            .columns
            .get(column_idx)
            .ok_or(Error::MalformedExdRow {
                row_id: self.entry.row_id,
                reason: "column index out of range",
            })?;
        let row_size = self.data.row_size as usize;
        let struct_start = self.struct_start();

        let trailing_start = struct_start
            .checked_add(row_size)
            .ok_or(Error::MalformedExdRow {
                row_id: self.entry.row_id,
                reason: "trailing-area base overflows",
            })?;
        if trailing_start > self.entry.payload_end {
            return Err(Error::MalformedExdRow {
                row_id: self.entry.row_id,
                reason: "trailing-area base past row payload end",
            });
        }
        let fixed_bytes = &self.data.raw[struct_start..self.entry.payload_end];
        let trailing_bytes = &self.data.raw[trailing_start..self.entry.payload_end];

        decode_column_field(
            self.entry.row_id,
            column_idx,
            column,
            self.data.row_size,
            fixed_bytes,
            trailing_bytes,
        )
    }

    /// Iterate over sub-rows related to this row.
    ///
    /// For [`SheetKind::Default`] rows the iterator yields the row itself.
    /// For [`SheetKind::SubRows`] rows called on a specific sub-row, it
    /// yields every sibling excluding itself; called on a parent row
    /// (no sub-row ID), it yields all sub-rows.
    pub fn sub_rows(self) -> impl Iterator<Item = Row<'a>> + 'a {
        let data = self.data;
        let entry = self.entry;
        match data.kind {
            SheetKind::Default => SubRowIter::DefaultOnce {
                done: false,
                data,
                entry,
            },
            SheetKind::SubRows => SubRowIter::SubRowsRange {
                next: 0,
                end: entry.sub_row_count,
                skip: self.sub_row_id,
                data,
                entry,
            },
        }
    }

    /// Returns `true` if this is the first (or only) sub-row in its parent row.
    ///
    /// Always `true` for [`SheetKind::Default`] rows. `false` for un-indexed
    /// [`SheetKind::SubRows`] parent rows (sub-row ID `None`).
    #[must_use]
    pub fn is_first_sub_row(&self) -> bool {
        match (self.data.kind, self.sub_row_id) {
            (SheetKind::Default, _) => true,
            (SheetKind::SubRows, None) => false,
            (SheetKind::SubRows, Some(sub_id)) => sub_id == 0,
        }
    }

    pub(crate) fn row_payload_bytes(&self) -> &'a [u8] {
        let payload_start_after_header = self.entry.file_offset + EXD_ROW_HEADER_SIZE;
        let start = match self.data.kind {
            SheetKind::Default => payload_start_after_header,
            SheetKind::SubRows => payload_start_after_header + 2,
        };
        &self.data.raw[start..self.entry.payload_end]
    }

    pub(crate) fn struct_offset_in_row_payload(&self) -> usize {
        match (self.data.kind, self.sub_row_id) {
            (SheetKind::Default, _) | (SheetKind::SubRows, None) => 0,
            (SheetKind::SubRows, Some(sub_id)) => {
                (sub_id as usize) * (self.data.row_size as usize + 2)
            }
        }
    }

    fn struct_start(&self) -> usize {
        let payload_start = self.entry.file_offset + EXD_ROW_HEADER_SIZE;
        match (self.data.kind, self.sub_row_id) {
            (SheetKind::Default, _) | (SheetKind::SubRows, None) => match self.data.kind {
                SheetKind::Default => payload_start,
                SheetKind::SubRows => payload_start + 2,
            },
            (SheetKind::SubRows, Some(sub_id)) => {
                payload_start + (sub_id as usize) * (self.data.row_size as usize + 2) + 2
            }
        }
    }
}

/// A decoded column value from an Excel row.
///
/// String values borrow from the originating [`ExcelData`]'s raw bytes and
/// therefore carry the page's lifetime `'a`. All numeric variants are `Copy`.
///
/// [`Field`] is constructed by [`Row::field`] and [`OwnedRow::field`](crate::excel::OwnedRow::field).
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum Field<'a> {
    /// NUL-terminated UTF-8 string borrowed from the page's trailing area.
    String(&'a str),
    Bool(bool),
    Int8(i8),
    UInt8(u8),
    Int16(i16),
    UInt16(u16),
    Int32(i32),
    UInt32(u32),
    Float32(f32),
    Int64(i64),
    UInt64(u64),
}

impl<'a> Field<'a> {
    /// Return the inner `&str` if this is a [`Field::String`], otherwise `None`.
    #[must_use]
    pub fn as_str(&self) -> Option<&'a str> {
        match *self {
            Field::String(s) => Some(s),
            _ => None,
        }
    }

    /// Return the inner `bool` if this is a [`Field::Bool`], otherwise `None`.
    #[must_use]
    pub fn as_bool(&self) -> Option<bool> {
        match *self {
            Field::Bool(v) => Some(v),
            _ => None,
        }
    }

    /// Return the inner `u32` if this is a [`Field::UInt32`], otherwise `None`.
    #[must_use]
    pub fn as_u32(&self) -> Option<u32> {
        match *self {
            Field::UInt32(v) => Some(v),
            _ => None,
        }
    }

    /// Return the inner `i32` if this is a [`Field::Int32`], otherwise `None`.
    #[must_use]
    pub fn as_i32(&self) -> Option<i32> {
        match *self {
            Field::Int32(v) => Some(v),
            _ => None,
        }
    }

    /// Return the inner `u16` if this is a [`Field::UInt16`], otherwise `None`.
    #[must_use]
    pub fn as_u16(&self) -> Option<u16> {
        match *self {
            Field::UInt16(v) => Some(v),
            _ => None,
        }
    }

    /// Return the inner `i16` if this is a [`Field::Int16`], otherwise `None`.
    #[must_use]
    pub fn as_i16(&self) -> Option<i16> {
        match *self {
            Field::Int16(v) => Some(v),
            _ => None,
        }
    }

    /// Return the inner `u8` if this is a [`Field::UInt8`], otherwise `None`.
    #[must_use]
    pub fn as_u8(&self) -> Option<u8> {
        match *self {
            Field::UInt8(v) => Some(v),
            _ => None,
        }
    }

    /// Return the inner `i8` if this is a [`Field::Int8`], otherwise `None`.
    #[must_use]
    pub fn as_i8(&self) -> Option<i8> {
        match *self {
            Field::Int8(v) => Some(v),
            _ => None,
        }
    }

    /// Return the inner `f32` if this is a [`Field::Float32`], otherwise `None`.
    #[must_use]
    pub fn as_f32(&self) -> Option<f32> {
        match *self {
            Field::Float32(v) => Some(v),
            _ => None,
        }
    }

    /// Widen any integer variant to `i64`.
    ///
    /// Returns `None` for non-integer variants (`String`, `Bool`, `Float32`).
    /// [`Field::UInt64`] values that exceed `i64::MAX` also return `None`.
    #[must_use]
    pub fn to_i64(&self) -> Option<i64> {
        match *self {
            Field::Int8(v) => Some(i64::from(v)),
            Field::UInt8(v) => Some(i64::from(v)),
            Field::Int16(v) => Some(i64::from(v)),
            Field::UInt16(v) => Some(i64::from(v)),
            Field::Int32(v) => Some(i64::from(v)),
            Field::UInt32(v) => Some(i64::from(v)),
            Field::Int64(v) => Some(v),
            Field::UInt64(v) => i64::try_from(v).ok(),
            _ => None,
        }
    }
}

pub(crate) const fn column_byte_width(ty: ColumnDataType) -> usize {
    match ty {
        ColumnDataType::Bool
        | ColumnDataType::Int8
        | ColumnDataType::UInt8
        | ColumnDataType::PackedBool0
        | ColumnDataType::PackedBool1
        | ColumnDataType::PackedBool2
        | ColumnDataType::PackedBool3
        | ColumnDataType::PackedBool4
        | ColumnDataType::PackedBool5
        | ColumnDataType::PackedBool6
        | ColumnDataType::PackedBool7 => 1,
        ColumnDataType::Int16 | ColumnDataType::UInt16 => 2,
        ColumnDataType::String
        | ColumnDataType::Int32
        | ColumnDataType::UInt32
        | ColumnDataType::Float32 => 4,
        ColumnDataType::Int64 | ColumnDataType::UInt64 => 8,
    }
}

#[allow(clippy::too_many_lines)]
pub(crate) fn decode_column_field<'a>(
    row_id: u32,
    column_idx: usize,
    column: ColumnDefinition,
    row_size: u16,
    fixed_bytes: &'a [u8],
    trailing_bytes: &'a [u8],
) -> Result<Field<'a>> {
    let column_offset = column.offset as usize;
    let row_size = row_size as usize;

    if column_offset
        .checked_add(column_byte_width(column.data_type))
        .is_none_or(|end| end > row_size)
    {
        return Err(Error::MalformedExdRow {
            row_id,
            reason: "column offset out of bounds",
        });
    }
    if fixed_bytes.len() < row_size {
        return Err(Error::MalformedExdRow {
            row_id,
            reason: "row payload shorter than declared row_size",
        });
    }

    let cell_at = column_offset;
    match column.data_type {
        ColumnDataType::String => {
            let string_offset = u32::from_be_bytes(
                fixed_bytes[cell_at..cell_at + 4]
                    .try_into()
                    .expect("4 bytes for u32"),
            ) as usize;
            if string_offset >= trailing_bytes.len() {
                return Err(Error::MalformedExdRow {
                    row_id,
                    reason: "string offset past trailing area",
                });
            }
            let trailing_slice = &trailing_bytes[string_offset..];
            let nul_pos =
                trailing_slice
                    .iter()
                    .position(|&b| b == 0)
                    .ok_or(Error::MalformedExdRow {
                        row_id,
                        reason: "string missing NUL terminator",
                    })?;
            let string_bytes = &trailing_slice[..nul_pos];
            let s = std::str::from_utf8(string_bytes)
                .map_err(|_| Error::InvalidUtf8InExdString { row_id, column_idx })?;
            Ok(Field::String(s))
        }
        ColumnDataType::Bool => Ok(Field::Bool(fixed_bytes[cell_at] != 0)),
        ColumnDataType::Int8 =>
        {
            #[allow(clippy::cast_possible_wrap)]
            Ok(Field::Int8(fixed_bytes[cell_at] as i8))
        }
        ColumnDataType::UInt8 => Ok(Field::UInt8(fixed_bytes[cell_at])),
        ColumnDataType::Int16 => Ok(Field::Int16(i16::from_be_bytes(
            fixed_bytes[cell_at..cell_at + 2]
                .try_into()
                .expect("2 bytes for i16"),
        ))),
        ColumnDataType::UInt16 => Ok(Field::UInt16(u16::from_be_bytes(
            fixed_bytes[cell_at..cell_at + 2]
                .try_into()
                .expect("2 bytes for u16"),
        ))),
        ColumnDataType::Int32 => Ok(Field::Int32(i32::from_be_bytes(
            fixed_bytes[cell_at..cell_at + 4]
                .try_into()
                .expect("4 bytes for i32"),
        ))),
        ColumnDataType::UInt32 => Ok(Field::UInt32(u32::from_be_bytes(
            fixed_bytes[cell_at..cell_at + 4]
                .try_into()
                .expect("4 bytes for u32"),
        ))),
        ColumnDataType::Float32 => Ok(Field::Float32(f32::from_be_bytes(
            fixed_bytes[cell_at..cell_at + 4]
                .try_into()
                .expect("4 bytes for f32"),
        ))),
        ColumnDataType::Int64 => Ok(Field::Int64(i64::from_be_bytes(
            fixed_bytes[cell_at..cell_at + 8]
                .try_into()
                .expect("8 bytes for i64"),
        ))),
        ColumnDataType::UInt64 => Ok(Field::UInt64(u64::from_be_bytes(
            fixed_bytes[cell_at..cell_at + 8]
                .try_into()
                .expect("8 bytes for u64"),
        ))),
        ColumnDataType::PackedBool0
        | ColumnDataType::PackedBool1
        | ColumnDataType::PackedBool2
        | ColumnDataType::PackedBool3
        | ColumnDataType::PackedBool4
        | ColumnDataType::PackedBool5
        | ColumnDataType::PackedBool6
        | ColumnDataType::PackedBool7 => {
            let bit = column
                .data_type
                .packed_bool_bit()
                .expect("variant matched PackedBoolN");
            Ok(Field::Bool((fixed_bytes[cell_at] >> bit) & 1 != 0))
        }
    }
}

#[must_use]
pub(crate) fn language_path_suffix(lang: Language) -> &'static str {
    match lang {
        Language::None => "",
        Language::Japanese => "ja",
        Language::English => "en",
        Language::German => "de",
        Language::French => "fr",
        Language::ChineseSimplified => "chs",
        Language::ChineseTraditional => "cht",
        Language::Korean => "ko",
        Language::TraditionalChinese => "tc",
    }
}

#[derive(Debug, Clone, Copy)]
struct RowIndexEntry {
    row_id: u32,
    file_offset: usize,
    payload_end: usize,
    sub_row_count: u16,
}

enum SubRowIter<'a> {
    DefaultOnce {
        done: bool,
        data: &'a ExcelData,
        entry: &'a RowIndexEntry,
    },
    SubRowsRange {
        next: u16,
        end: u16,
        skip: Option<u16>,
        data: &'a ExcelData,
        entry: &'a RowIndexEntry,
    },
}

impl<'a> Iterator for SubRowIter<'a> {
    type Item = Row<'a>;

    fn next(&mut self) -> Option<Row<'a>> {
        match self {
            SubRowIter::DefaultOnce { done, data, entry } => {
                if *done {
                    None
                } else {
                    *done = true;
                    Some(data.row_for(entry, None))
                }
            }
            SubRowIter::SubRowsRange {
                next,
                end,
                skip,
                data,
                entry,
            } => {
                while *next < *end {
                    let sub_id = *next;
                    *next += 1;
                    if Some(sub_id) == *skip {
                        continue;
                    }
                    return Some(data.row_for(entry, Some(sub_id)));
                }
                None
            }
        }
    }
}

#[derive(Debug, BinRead)]
#[brw(big)]
struct ExdWireHeader {
    magic: [u8; 4],
    version: u16,
    _unknown1: u16,
    index_size: u32,
    _opaque: [u8; 20],
}

#[derive(Debug, BinRead)]
#[brw(big)]
struct ExdWireIndexEntry {
    row_id: u32,
    offset: u32,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::excel::exh::ColumnDefinition;

    #[allow(clippy::needless_pass_by_value)]
    fn make_header(
        row_size: u16,
        kind: SheetKind,
        columns: Vec<ColumnDefinition>,
        languages: Vec<Language>,
    ) -> ExcelHeader {
        let mut buf = Vec::new();
        buf.extend_from_slice(b"EXHF");
        buf.extend_from_slice(&3u16.to_be_bytes());
        buf.extend_from_slice(&row_size.to_be_bytes());
        let column_count = u16::try_from(columns.len()).expect("column_count fits u16");
        buf.extend_from_slice(&column_count.to_be_bytes());
        buf.extend_from_slice(&1u16.to_be_bytes());
        let language_count = u16::try_from(languages.len()).expect("language_count fits u16");
        buf.extend_from_slice(&language_count.to_be_bytes());
        buf.extend_from_slice(&0u16.to_be_bytes());
        buf.push(0);
        buf.push(match kind {
            SheetKind::Default => 1,
            SheetKind::SubRows => 2,
        });
        buf.extend_from_slice(&0u16.to_be_bytes());
        buf.extend_from_slice(&0u32.to_be_bytes());
        buf.extend_from_slice(&0u32.to_be_bytes());
        buf.extend_from_slice(&0u32.to_be_bytes());
        for col in &columns {
            let raw_ty = column_type_raw(col.data_type);
            buf.extend_from_slice(&raw_ty.to_be_bytes());
            buf.extend_from_slice(&col.offset.to_be_bytes());
        }
        buf.extend_from_slice(&0u32.to_be_bytes());
        buf.extend_from_slice(&0u32.to_be_bytes());
        for lang in &languages {
            buf.push(language_raw(*lang));
            buf.push(0);
        }
        ExcelHeader::parse(&buf).expect("hand-built EXH should parse")
    }

    fn column_type_raw(ty: ColumnDataType) -> u16 {
        match ty {
            ColumnDataType::String => 0x0000,
            ColumnDataType::Bool => 0x0001,
            ColumnDataType::Int8 => 0x0002,
            ColumnDataType::UInt8 => 0x0003,
            ColumnDataType::Int16 => 0x0004,
            ColumnDataType::UInt16 => 0x0005,
            ColumnDataType::Int32 => 0x0006,
            ColumnDataType::UInt32 => 0x0007,
            ColumnDataType::Float32 => 0x0009,
            ColumnDataType::Int64 => 0x000B,
            ColumnDataType::UInt64 => 0x000C,
            ColumnDataType::PackedBool0 => 0x0019,
            ColumnDataType::PackedBool1 => 0x001A,
            ColumnDataType::PackedBool2 => 0x001B,
            ColumnDataType::PackedBool3 => 0x001C,
            ColumnDataType::PackedBool4 => 0x001D,
            ColumnDataType::PackedBool5 => 0x001E,
            ColumnDataType::PackedBool6 => 0x001F,
            ColumnDataType::PackedBool7 => 0x0020,
        }
    }

    fn language_raw(lang: Language) -> u8 {
        match lang {
            Language::None => 0,
            Language::Japanese => 1,
            Language::English => 2,
            Language::German => 3,
            Language::French => 4,
            Language::ChineseSimplified => 5,
            Language::ChineseTraditional => 6,
            Language::Korean => 7,
            Language::TraditionalChinese => 8,
        }
    }

    struct RowSpec {
        row_id: u32,
        fixed: Vec<u8>,
        trailing: Vec<u8>,
    }

    struct SubRowSpec {
        row_id: u32,
        sub_fixed: Vec<Vec<u8>>,
        trailing: Vec<u8>,
    }

    fn build_exd_default(rows: &[RowSpec], row_size: u16) -> Vec<u8> {
        build_exd_default_with_sub_count(rows, row_size, 1)
    }

    fn build_exd_default_with_sub_count(
        rows: &[RowSpec],
        row_size: u16,
        sub_count: u16,
    ) -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(b"EXDF");
        buf.extend_from_slice(&2u16.to_be_bytes());
        buf.extend_from_slice(&0u16.to_be_bytes());
        buf.extend_from_slice(&[0u8; 4]);
        buf.extend_from_slice(&[0u8; 20]);
        assert_eq!(buf.len(), EXD_FIXED_HEADER_SIZE);

        let index_size =
            u32::try_from(rows.len() * EXD_INDEX_ENTRY_SIZE).expect("index_size fits u32");
        let index_start = buf.len();
        for row in rows {
            buf.extend_from_slice(&row.row_id.to_be_bytes());
            buf.extend_from_slice(&0u32.to_be_bytes());
        }

        for (i, row) in rows.iter().enumerate() {
            assert_eq!(
                row.fixed.len(),
                row_size as usize,
                "row {} fixed portion must equal row_size",
                row.row_id,
            );
            let row_offset = u32::try_from(buf.len()).expect("row_offset fits u32");
            let idx_entry_offset = index_start + i * EXD_INDEX_ENTRY_SIZE + 4;
            buf[idx_entry_offset..idx_entry_offset + 4].copy_from_slice(&row_offset.to_be_bytes());

            let payload_len =
                u32::try_from(row.fixed.len() + row.trailing.len()).expect("payload_len fits u32");
            buf.extend_from_slice(&payload_len.to_be_bytes());
            buf.extend_from_slice(&sub_count.to_be_bytes());
            buf.extend_from_slice(&row.fixed);
            buf.extend_from_slice(&row.trailing);
        }

        buf[0x08..0x0C].copy_from_slice(&index_size.to_be_bytes());
        buf
    }

    fn build_exd_subrows(rows: &[SubRowSpec], row_size: u16) -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(b"EXDF");
        buf.extend_from_slice(&2u16.to_be_bytes());
        buf.extend_from_slice(&0u16.to_be_bytes());
        buf.extend_from_slice(&[0u8; 4]);
        buf.extend_from_slice(&[0u8; 20]);
        assert_eq!(buf.len(), EXD_FIXED_HEADER_SIZE);

        let index_size =
            u32::try_from(rows.len() * EXD_INDEX_ENTRY_SIZE).expect("index_size fits u32");
        let index_start = buf.len();
        for row in rows {
            buf.extend_from_slice(&row.row_id.to_be_bytes());
            buf.extend_from_slice(&0u32.to_be_bytes());
        }

        for (i, row) in rows.iter().enumerate() {
            let row_offset = u32::try_from(buf.len()).expect("row_offset fits u32");
            let idx_entry_offset = index_start + i * EXD_INDEX_ENTRY_SIZE + 4;
            buf[idx_entry_offset..idx_entry_offset + 4].copy_from_slice(&row_offset.to_be_bytes());

            let sub_count = u16::try_from(row.sub_fixed.len()).expect("sub_count fits u16");
            let payload_len = (sub_count as usize) * (row_size as usize + 2) + row.trailing.len();
            buf.extend_from_slice(
                &u32::try_from(payload_len)
                    .expect("payload_len fits u32")
                    .to_be_bytes(),
            );
            buf.extend_from_slice(&sub_count.to_be_bytes());
            for (sub_id, fixed) in row.sub_fixed.iter().enumerate() {
                assert_eq!(
                    fixed.len(),
                    row_size as usize,
                    "row {} sub_id {} fixed portion must equal row_size",
                    row.row_id,
                    sub_id,
                );
                buf.extend_from_slice(
                    &u16::try_from(sub_id)
                        .expect("sub_id fits u16")
                        .to_be_bytes(),
                );
                buf.extend_from_slice(fixed);
            }
            buf.extend_from_slice(&row.trailing);
        }

        buf[0x08..0x0C].copy_from_slice(&index_size.to_be_bytes());
        buf
    }

    #[test]
    fn parses_default_happy_path_with_mixed_columns() {
        let columns = vec![
            ColumnDefinition {
                data_type: ColumnDataType::String,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Bool,
                offset: 4,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool3,
                offset: 5,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int16,
                offset: 6,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Float32,
                offset: 8,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt32,
                offset: 12,
            },
        ];
        let header = make_header(16, SheetKind::Default, columns, vec![Language::English]);

        let mut row1_fixed = vec![0u8; 16];
        row1_fixed[0..4].copy_from_slice(&0u32.to_be_bytes());
        row1_fixed[4] = 1;
        row1_fixed[5] = 0b0000_1000;
        row1_fixed[6..8].copy_from_slice(&(-42i16).to_be_bytes());
        row1_fixed[8..12].copy_from_slice(&1.5f32.to_be_bytes());
        row1_fixed[12..16].copy_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
        let row1_trailing = b"hi\0".to_vec();

        let mut row5_fixed = vec![0u8; 16];
        row5_fixed[0..4].copy_from_slice(&0u32.to_be_bytes());
        row5_fixed[4] = 0;
        row5_fixed[5] = 0b1111_0111;
        row5_fixed[6..8].copy_from_slice(&0i16.to_be_bytes());
        row5_fixed[8..12].copy_from_slice(&0f32.to_be_bytes());
        row5_fixed[12..16].copy_from_slice(&0u32.to_be_bytes());
        let row5_trailing = b"world\0".to_vec();

        let mut row9_fixed = vec![0u8; 16];
        row9_fixed[0..4].copy_from_slice(&0u32.to_be_bytes());
        let row9_trailing = b"\0".to_vec();

        let specs = vec![
            RowSpec {
                row_id: 1,
                fixed: row1_fixed,
                trailing: row1_trailing,
            },
            RowSpec {
                row_id: 5,
                fixed: row5_fixed,
                trailing: row5_trailing,
            },
            RowSpec {
                row_id: 9,
                fixed: row9_fixed,
                trailing: row9_trailing,
            },
        ];

        let bytes = build_exd_default(&specs, 16);
        let exd = ExcelData::parse(&bytes, &header, Language::English).expect("parse");

        assert_eq!(exd.row_count(), 3);
        assert_eq!(exd.total_sub_row_count(), 3);
        assert_eq!(exd.rows().count(), 3);
        assert_eq!(exd.language(), Language::English);
        assert_eq!(exd.version(), 2);
        assert_eq!(exd.sheet_kind(), SheetKind::Default);

        let row1 = exd.row(1).expect("row 1");
        assert_eq!(row1.row_id(), 1);
        assert_eq!(row1.sub_row_id(), None);
        assert_eq!(row1.sub_row_count(), 1);
        assert_eq!(row1.column_count(), 6);
        assert_eq!(row1.field(0).unwrap(), Field::String("hi"));
        assert_eq!(row1.field(1).unwrap(), Field::Bool(true));
        assert_eq!(row1.field(2).unwrap(), Field::Bool(true));
        assert_eq!(row1.field(3).unwrap(), Field::Int16(-42));
        assert_eq!(row1.field(4).unwrap(), Field::Float32(1.5));
        assert_eq!(row1.field(5).unwrap(), Field::UInt32(0xDEAD_BEEF));

        let row5 = exd.row(5).expect("row 5");
        assert_eq!(row5.field(0).unwrap(), Field::String("world"));
        assert_eq!(row5.field(1).unwrap(), Field::Bool(false));
        assert_eq!(row5.field(2).unwrap(), Field::Bool(false));

        let row9 = exd.row(9).expect("row 9");
        assert_eq!(row9.field(0).unwrap(), Field::String(""));

        assert!(exd.row(0).is_none());
        assert!(exd.row(2).is_none());
        assert!(exd.row(100).is_none());
    }

    #[test]
    fn parses_subrows_happy_path_and_iterates() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::SubRows, columns, vec![Language::None]);

        let mut row1_subs: Vec<Vec<u8>> = Vec::new();
        for v in [10i32, 20, 30] {
            row1_subs.push(v.to_be_bytes().to_vec());
        }
        let row7_subs = vec![999i32.to_be_bytes().to_vec()];

        let specs = vec![
            SubRowSpec {
                row_id: 1,
                sub_fixed: row1_subs,
                trailing: Vec::new(),
            },
            SubRowSpec {
                row_id: 7,
                sub_fixed: row7_subs,
                trailing: Vec::new(),
            },
        ];

        let bytes = build_exd_subrows(&specs, 4);
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");

        assert_eq!(exd.row_count(), 2);
        assert_eq!(exd.total_sub_row_count(), 4);
        assert_eq!(exd.sheet_kind(), SheetKind::SubRows);

        let row1 = exd.row(1).expect("row 1");
        assert_eq!(row1.sub_row_count(), 3);
        let subs: Vec<_> = row1.sub_rows().collect();
        assert_eq!(subs.len(), 3);
        assert_eq!(subs[0].sub_row_id(), Some(0));
        assert_eq!(subs[1].sub_row_id(), Some(1));
        assert_eq!(subs[2].sub_row_id(), Some(2));
        assert_eq!(subs[0].field(0).unwrap(), Field::Int32(10));
        assert_eq!(subs[1].field(0).unwrap(), Field::Int32(20));
        assert_eq!(subs[2].field(0).unwrap(), Field::Int32(30));

        let s = exd.sub_row(1, 1).expect("sub_row(1, 1)");
        assert_eq!(s.field(0).unwrap(), Field::Int32(20));
        assert_eq!(s.row_id(), 1);
        assert_eq!(s.sub_row_id(), Some(1));
        assert_eq!(s.sub_row_count(), 3);

        assert!(exd.sub_row(1, 3).is_none());
        assert!(exd.sub_row(2, 0).is_none());

        let row7 = exd.row(7).expect("row 7");
        let row7_subs: Vec<_> = row7.sub_rows().collect();
        assert_eq!(row7_subs.len(), 1);
        assert_eq!(row7_subs[0].field(0).unwrap(), Field::Int32(999));
    }

    #[test]
    fn sub_row_returns_none_on_default_sheet() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let rows = vec![RowSpec {
            row_id: 0,
            fixed: vec![7u8],
            trailing: Vec::new(),
        }];
        let bytes = build_exd_default(&rows, 1);
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");

        assert!(exd.sub_row(0, 0).is_none());
        let row = exd.row(0).expect("row 0");
        let collected: Vec<_> = row.sub_rows().collect();
        assert_eq!(collected.len(), 1);
        assert_eq!(collected[0].row_id(), 0);
    }

    #[test]
    fn every_scalar_column_type_round_trips() {
        let columns = vec![
            ColumnDefinition {
                data_type: ColumnDataType::Int8,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt8,
                offset: 1,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int16,
                offset: 2,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt16,
                offset: 4,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int32,
                offset: 6,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt32,
                offset: 10,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Float32,
                offset: 14,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int64,
                offset: 18,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt64,
                offset: 26,
            },
        ];
        let row_size = 34;
        let header = make_header(row_size, SheetKind::Default, columns, vec![Language::None]);

        let mut fixed = vec![0u8; row_size as usize];
        #[allow(clippy::cast_sign_loss)]
        let int8_byte = -5i8 as u8;
        fixed[0] = int8_byte;
        fixed[1] = 200u8;
        fixed[2..4].copy_from_slice(&(-12345i16).to_be_bytes());
        fixed[4..6].copy_from_slice(&60_000u16.to_be_bytes());
        fixed[6..10].copy_from_slice(&(-123_456_789i32).to_be_bytes());
        fixed[10..14].copy_from_slice(&3_000_000_000u32.to_be_bytes());
        fixed[14..18].copy_from_slice(&std::f32::consts::PI.to_be_bytes());
        fixed[18..26].copy_from_slice(&(-1i64).to_be_bytes());
        fixed[26..34].copy_from_slice(&u64::MAX.to_be_bytes());

        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed,
                trailing: Vec::new(),
            }],
            row_size,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let row = exd.row(0).unwrap();
        assert_eq!(row.field(0).unwrap(), Field::Int8(-5));
        assert_eq!(row.field(1).unwrap(), Field::UInt8(200));
        assert_eq!(row.field(2).unwrap(), Field::Int16(-12345));
        assert_eq!(row.field(3).unwrap(), Field::UInt16(60_000));
        assert_eq!(row.field(4).unwrap(), Field::Int32(-123_456_789));
        assert_eq!(row.field(5).unwrap(), Field::UInt32(3_000_000_000));
        assert_eq!(row.field(6).unwrap(), Field::Float32(std::f32::consts::PI));
        assert_eq!(row.field(7).unwrap(), Field::Int64(-1));
        assert_eq!(row.field(8).unwrap(), Field::UInt64(u64::MAX));
    }

    #[test]
    fn every_packed_bool_variant_decodes() {
        let columns = vec![
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool0,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool1,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool2,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool3,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool4,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool5,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool6,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool7,
                offset: 0,
            },
        ];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed: vec![0b1010_1010u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let row = exd.row(0).unwrap();
        let expected = [false, true, false, true, false, true, false, true];
        for (i, want) in expected.iter().enumerate() {
            assert_eq!(row.field(i).unwrap(), Field::Bool(*want), "bit {i}");
        }
    }

    #[test]
    fn bool_treats_any_nonzero_byte_as_true() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::Bool,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        for raw in [0u8, 1, 2, 0x7F, 0xFF] {
            let bytes = build_exd_default(
                &[RowSpec {
                    row_id: 0,
                    fixed: vec![raw],
                    trailing: Vec::new(),
                }],
                1,
            );
            let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
            let row = exd.row(0).unwrap();
            assert_eq!(
                row.field(0).unwrap(),
                Field::Bool(raw != 0),
                "byte {raw:#x}"
            );
        }
    }

    #[test]
    fn string_truncates_at_first_nul() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::String,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let mut fixed = vec![0u8; 4];
        fixed[0..4].copy_from_slice(&0u32.to_be_bytes());
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed,
                trailing: b"abc\0extra\0".to_vec(),
            }],
            4,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let row = exd.row(0).unwrap();
        assert_eq!(row.field(0).unwrap(), Field::String("abc"));
    }

    #[test]
    fn string_with_bad_utf8_errors() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::String,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let mut fixed = vec![0u8; 4];
        fixed[0..4].copy_from_slice(&0u32.to_be_bytes());
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 42,
                fixed,
                trailing: vec![0xFFu8, 0xFEu8, 0u8],
            }],
            4,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let row = exd.row(42).unwrap();
        let err = row.field(0).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidUtf8InExdString {
                row_id: 42,
                column_idx: 0,
            }
        ));
    }

    #[test]
    fn string_offset_past_trailing_area_errors() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::String,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let mut fixed = vec![0u8; 4];
        fixed[0..4].copy_from_slice(&999u32.to_be_bytes());
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed,
                trailing: b"hi\0".to_vec(),
            }],
            4,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let err = exd.row(0).unwrap().field(0).unwrap_err();
        assert!(matches!(err, Error::MalformedExdRow { row_id: 0, .. }));
    }

    #[test]
    fn string_missing_nul_errors() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::String,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let mut fixed = vec![0u8; 4];
        fixed[0..4].copy_from_slice(&0u32.to_be_bytes());
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 7,
                fixed,
                trailing: b"abc".to_vec(),
            }],
            4,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let err = exd.row(7).unwrap().field(0).unwrap_err();
        assert!(matches!(
            err,
            Error::MalformedExdRow {
                row_id: 7,
                reason: "string missing NUL terminator",
            },
        ));
    }

    #[test]
    fn rejects_bad_magic() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let mut bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed: vec![0u8],
                trailing: Vec::new(),
            }],
            1,
        );
        bytes[0] = b'X';
        let err = ExcelData::parse(&bytes, &header, Language::None).unwrap_err();
        assert!(matches!(err, Error::InvalidMagic));
    }

    #[test]
    fn rejects_wrong_version() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let mut bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed: vec![0u8],
                trailing: Vec::new(),
            }],
            1,
        );
        bytes[0x04..0x06].copy_from_slice(&3u16.to_be_bytes());
        let err = ExcelData::parse(&bytes, &header, Language::None).unwrap_err();
        assert!(matches!(err, Error::UnsupportedExdVersion { version: 3 }));
    }

    #[test]
    fn rejects_truncated_after_header() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed: vec![0u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let truncated = bytes[..EXD_FIXED_HEADER_SIZE].to_vec();
        let err = ExcelData::parse(&truncated, &header, Language::None).unwrap_err();
        assert!(matches!(err, Error::InvalidExdLength { .. }));
    }

    #[test]
    fn rejects_truncated_after_index() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed: vec![0u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let truncated = bytes[..EXD_FIXED_HEADER_SIZE + EXD_INDEX_ENTRY_SIZE].to_vec();
        let err = ExcelData::parse(&truncated, &header, Language::None).unwrap_err();
        assert!(matches!(
            err,
            Error::MalformedExdRow {
                row_id: 0,
                reason: "row offset out of bounds",
            },
        ));
    }

    #[test]
    fn rejects_truncated_mid_row_payload() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt32,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed: 0u32.to_be_bytes().to_vec(),
                trailing: Vec::new(),
            }],
            4,
        );
        let mut shorter = bytes;
        shorter.truncate(shorter.len() - 2);
        let err = ExcelData::parse(&shorter, &header, Language::None).unwrap_err();
        assert!(matches!(
            err,
            Error::MalformedExdRow {
                row_id: 0,
                reason: "row data_size overruns file",
            },
        ));
    }

    #[test]
    fn rejects_column_offset_out_of_bounds() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 2,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed: vec![0u8; 4],
                trailing: Vec::new(),
            }],
            4,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let err = exd.row(0).unwrap().field(0).unwrap_err();
        assert!(matches!(
            err,
            Error::MalformedExdRow {
                row_id: 0,
                reason: "column offset out of bounds",
            },
        ));
    }

    #[test]
    fn rejects_unsorted_index_entries() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let mut bytes = build_exd_default(
            &[
                RowSpec {
                    row_id: 1,
                    fixed: vec![0u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 2,
                    fixed: vec![0u8],
                    trailing: Vec::new(),
                },
            ],
            1,
        );
        let index_start = EXD_FIXED_HEADER_SIZE;
        let first_id = bytes[index_start..index_start + 4].to_vec();
        let second_id = bytes[index_start + 8..index_start + 12].to_vec();
        bytes[index_start..index_start + 4].copy_from_slice(&second_id);
        bytes[index_start + 8..index_start + 12].copy_from_slice(&first_id);
        let err = ExcelData::parse(&bytes, &header, Language::None).unwrap_err();
        assert!(matches!(
            err,
            Error::MalformedExdRow {
                row_id: 0,
                reason: "index entries not sorted by row_id",
            },
        ));
    }

    #[test]
    fn language_path_suffix_covers_every_variant() {
        assert_eq!(language_path_suffix(Language::None), "");
        assert_eq!(language_path_suffix(Language::Japanese), "ja");
        assert_eq!(language_path_suffix(Language::English), "en");
        assert_eq!(language_path_suffix(Language::German), "de");
        assert_eq!(language_path_suffix(Language::French), "fr");
        assert_eq!(language_path_suffix(Language::ChineseSimplified), "chs");
        assert_eq!(language_path_suffix(Language::ChineseTraditional), "cht");
        assert_eq!(language_path_suffix(Language::Korean), "ko");
        assert_eq!(language_path_suffix(Language::TraditionalChinese), "tc");
    }

    #[test]
    fn parses_zero_rows() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let bytes = build_exd_default(&[], 1);
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        assert_eq!(exd.row_count(), 0);
        assert_eq!(exd.total_sub_row_count(), 0);
        assert!(exd.rows().next().is_none());
        assert!(exd.row(0).is_none());
    }

    #[test]
    fn default_sheet_tolerates_row_count_not_one() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let bytes = build_exd_default_with_sub_count(
            &[RowSpec {
                row_id: 7,
                fixed: vec![0u8],
                trailing: Vec::new(),
            }],
            1,
            5,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse must succeed");
        let row = exd.row(7).expect("row 7");
        assert_eq!(
            row.sub_row_count(),
            1,
            "Default-sheet sub_row_count is clamped to 1 regardless of wire value",
        );
        assert_eq!(
            exd.total_sub_row_count(),
            1,
            "Default-sheet total counts each row once",
        );
    }

    #[test]
    fn sub_row_iter_excludes_self() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::SubRows, columns, vec![Language::None]);
        let row_subs: Vec<Vec<u8>> = [100i32, 200, 300]
            .into_iter()
            .map(|v| v.to_be_bytes().to_vec())
            .collect();
        let specs = vec![SubRowSpec {
            row_id: 42,
            sub_fixed: row_subs,
            trailing: Vec::new(),
        }];
        let bytes = build_exd_subrows(&specs, 4);
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");

        let middle = exd.sub_row(42, 1).expect("sub_row(42, 1)");
        let siblings: Vec<_> = middle.sub_rows().collect();
        assert_eq!(siblings.len(), 2);
        assert_eq!(siblings[0].sub_row_id(), Some(0));
        assert_eq!(siblings[1].sub_row_id(), Some(2));
        assert_eq!(siblings[0].field(0).unwrap(), Field::Int32(100));
        assert_eq!(siblings[1].field(0).unwrap(), Field::Int32(300));

        let first = exd.sub_row(42, 0).expect("sub_row(42, 0)");
        let siblings_of_first: Vec<_> = first.sub_rows().collect();
        assert_eq!(siblings_of_first.len(), 2);
        assert_eq!(siblings_of_first[0].sub_row_id(), Some(1));
        assert_eq!(siblings_of_first[1].sub_row_id(), Some(2));

        let last = exd.sub_row(42, 2).expect("sub_row(42, 2)");
        let siblings_of_last: Vec<_> = last.sub_rows().collect();
        assert_eq!(siblings_of_last.len(), 2);
        assert_eq!(siblings_of_last[0].sub_row_id(), Some(0));
        assert_eq!(siblings_of_last[1].sub_row_id(), Some(1));

        let parent = exd.row(42).expect("row 42");
        assert_eq!(parent.sub_rows().count(), 3);
    }

    #[test]
    fn is_first_sub_row_predicate() {
        let cols = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let default_header = make_header(1, SheetKind::Default, cols, vec![Language::None]);
        let default_bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed: vec![0u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let default_exd =
            ExcelData::parse(&default_bytes, &default_header, Language::None).expect("parse");
        let default_row = default_exd.row(0).expect("row 0");
        assert!(default_row.is_first_sub_row());

        let sub_cols = vec![ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let sub_header = make_header(4, SheetKind::SubRows, sub_cols, vec![Language::None]);
        let row_subs: Vec<Vec<u8>> = [10i32, 20]
            .into_iter()
            .map(|v| v.to_be_bytes().to_vec())
            .collect();
        let specs = vec![SubRowSpec {
            row_id: 1,
            sub_fixed: row_subs,
            trailing: Vec::new(),
        }];
        let sub_bytes = build_exd_subrows(&specs, 4);
        let sub_exd = ExcelData::parse(&sub_bytes, &sub_header, Language::None).expect("parse");

        let parent = sub_exd.row(1).expect("row 1");
        assert!(!parent.is_first_sub_row());

        let sub0 = sub_exd.sub_row(1, 0).expect("sub_row(1, 0)");
        assert!(sub0.is_first_sub_row());

        let sub1 = sub_exd.sub_row(1, 1).expect("sub_row(1, 1)");
        assert!(!sub1.is_first_sub_row());
    }

    #[test]
    fn numeric_columns_decode_big_endian_byte_order() {
        let columns = vec![
            ColumnDefinition {
                data_type: ColumnDataType::UInt32,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int16,
                offset: 4,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Float32,
                offset: 6,
            },
        ];
        let row_size = 10u16;
        let header = make_header(row_size, SheetKind::Default, columns, vec![Language::None]);

        let fixed = vec![0x12, 0x34, 0x56, 0x78, 0xFF, 0xFE, 0x40, 0x49, 0x0F, 0xDB];
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed,
                trailing: Vec::new(),
            }],
            row_size,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let row = exd.row(0).unwrap();
        assert_eq!(row.field(0).unwrap(), Field::UInt32(0x1234_5678));
        assert_eq!(row.field(1).unwrap(), Field::Int16(-2));
        let f = match row.field(2).unwrap() {
            Field::Float32(v) => v,
            other => panic!("expected Float32, got {other:?}"),
        };
        assert!(
            (f - std::f32::consts::PI).abs() < 1e-6,
            "expected ~PI, got {f}",
        );
    }

    #[test]
    fn string_offset_at_trailing_size_errors() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::String,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let trailing = b"hi\0".to_vec();
        let trailing_size = u32::try_from(trailing.len()).expect("trailing.len() fits u32");
        let mut fixed = vec![0u8; 4];
        fixed[0..4].copy_from_slice(&trailing_size.to_be_bytes());
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed,
                trailing,
            }],
            4,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        let err = exd.row(0).unwrap().field(0).unwrap_err();
        assert!(matches!(
            err,
            Error::MalformedExdRow {
                row_id: 0,
                reason: "string offset past trailing area",
            },
        ));
    }

    #[test]
    fn subrows_with_string_column_decodes_lumina_compatible_offsets() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::String,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::SubRows, columns, vec![Language::None]);

        let sub0_fixed = 6u32.to_be_bytes().to_vec();
        let sub1_fixed = 6u32.to_be_bytes().to_vec();
        let mut trailing = b"alpha".to_vec();
        trailing.push(0);
        trailing.extend_from_slice(b"beta");
        trailing.push(0);

        let specs = vec![SubRowSpec {
            row_id: 1,
            sub_fixed: vec![sub0_fixed, sub1_fixed],
            trailing,
        }];

        let bytes = build_exd_subrows(&specs, 4);
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");

        let sub0 = exd.sub_row(1, 0).expect("sub_row(1, 0)");
        let sub1 = exd.sub_row(1, 1).expect("sub_row(1, 1)");
        assert_eq!(sub0.field(0).unwrap(), Field::String("alpha"));
        assert_eq!(sub1.field(0).unwrap(), Field::String("beta"));
    }

    #[test]
    fn parse_does_not_overallocate_on_huge_declared_index_size() {
        let mut bytes = Vec::with_capacity(EXD_FIXED_HEADER_SIZE);
        bytes.extend_from_slice(b"EXDF");
        bytes.extend_from_slice(&2u16.to_be_bytes());
        bytes.extend_from_slice(&0u16.to_be_bytes());
        let evil: u32 = 0xFFFF_FFF8;
        bytes.extend_from_slice(&evil.to_be_bytes());
        bytes.extend_from_slice(&[0u8; 20]);
        assert_eq!(bytes.len(), EXD_FIXED_HEADER_SIZE);

        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);

        let err = ExcelData::parse(&bytes, &header, Language::None).unwrap_err();
        assert!(
            matches!(err, Error::InvalidExdLength { .. }),
            "expected InvalidExdLength, got {err:?}",
        );
    }

    #[test]
    fn rejects_misaligned_index_size() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header = make_header(1, SheetKind::Default, columns, vec![Language::None]);
        let mut bytes = Vec::new();
        bytes.extend_from_slice(b"EXDF");
        bytes.extend_from_slice(&2u16.to_be_bytes());
        bytes.extend_from_slice(&0u16.to_be_bytes());
        bytes.extend_from_slice(&5u32.to_be_bytes());
        bytes.extend_from_slice(&[0u8; 20]);
        bytes.extend_from_slice(&[0u8; 8]);
        let err = ExcelData::parse(&bytes, &header, Language::None).unwrap_err();
        assert!(
            matches!(
                err,
                Error::MisalignedIndexData {
                    size: 5,
                    entry_size: 8
                }
            ),
            "expected MisalignedIndexData, got {err:?}",
        );
    }

    #[test]
    fn row_at_returns_row_by_file_order_index() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::UInt32,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let specs: Vec<RowSpec> = [10u32, 20, 30]
            .into_iter()
            .map(|v| RowSpec {
                row_id: v,
                fixed: v.to_be_bytes().to_vec(),
                trailing: Vec::new(),
            })
            .collect();
        let bytes = build_exd_default(&specs, 4);
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");

        assert_eq!(exd.row_at(0).expect("idx 0").row_id(), 10);
        assert_eq!(exd.row_at(1).expect("idx 1").row_id(), 20);
        assert_eq!(exd.row_at(2).expect("idx 2").row_id(), 30);
        assert!(exd.row_at(3).is_none(), "idx 3 must be None");
        assert_eq!(exd.row_at(0).expect("repeat idx 0").row_id(), 10);
        assert_eq!(exd.row_at(1).unwrap().field(0).unwrap(), Field::UInt32(20),);
    }

    #[test]
    fn excel_data_clone_yields_same_rows() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::English]);
        let specs: Vec<RowSpec> = [1u32, 2, 3]
            .into_iter()
            .map(|v| RowSpec {
                row_id: v,
                fixed: v.to_be_bytes().to_vec(),
                trailing: Vec::new(),
            })
            .collect();
        let bytes = build_exd_default(&specs, 4);
        let original = ExcelData::parse(&bytes, &header, Language::English).expect("parse");
        let cloned = original.clone();

        assert_eq!(original.row_count(), cloned.row_count());
        assert_eq!(original.language(), cloned.language());
        for idx in 0..original.row_count() {
            let orig_row = original.row_at(idx).expect("row");
            let clone_row = cloned.row_at(idx).expect("row");
            assert_eq!(orig_row.row_id(), clone_row.row_id());
            assert_eq!(orig_row.field(0).unwrap(), clone_row.field(0).unwrap());
        }
    }

    #[test]
    fn string_offset_one_byte_before_trailing_end_works() {
        let columns = vec![ColumnDefinition {
            data_type: ColumnDataType::String,
            offset: 0,
        }];
        let header = make_header(4, SheetKind::Default, columns, vec![Language::None]);
        let trailing = b"ab\0".to_vec();
        let trailing_size = u32::try_from(trailing.len()).expect("trailing.len() fits u32");
        let mut fixed = vec![0u8; 4];
        fixed[0..4].copy_from_slice(&(trailing_size - 1).to_be_bytes());
        let bytes = build_exd_default(
            &[RowSpec {
                row_id: 0,
                fixed,
                trailing,
            }],
            4,
        );
        let exd = ExcelData::parse(&bytes, &header, Language::None).expect("parse");
        assert_eq!(exd.row(0).unwrap().field(0).unwrap(), Field::String(""));
    }
}
