//! High-level typed sheet access: [`Sheet`], [`OwnedRow`], and supporting iterators.
//!
//! [`Sheet`] is the primary ergonomic entry point for reading Excel data. It
//! holds an [`ExcelHeader`] and a reference to the owning [`SqPack`], and
//! provides methods to iterate rows, look up a specific row, or load a raw
//! page. Rows returned from these methods are [`OwnedRow`]s — they carry
//! their payload in an `Arc<[u8]>` and outlive the sheet and the page they
//! were read from.
//!
//! [`SheetRows`] is the lazy iterator returned by [`Sheet::rows`]; it loads
//! pages on demand. [`SheetIter`] iterates all sheets in the install's EXL.
//! [`SheetNameIter`] iterates just the names.

use std::sync::Arc;

use tracing::{debug, debug_span};

use crate::error::{Error, Result};
use crate::excel::{
    ColumnDefinition, ExcelData, ExcelHeader, ExcelList, Field, Language, PageDefinition, Row,
    SheetKind,
};
use crate::sqpack::SqPack;
use crate::tracing_schema::span_names;

/// High-level handle to a single Excel sheet, backed by a live [`SqPack`] reference.
///
/// Obtained from [`SqPack::sheet`](crate::sqpack::SqPack::sheet) or via
/// [`SheetIter`]. Provides row access with automatic page loading and language
/// selection.
#[derive(Debug)]
pub struct Sheet<'sq> {
    sqpack: &'sq SqPack,
    name: String,
    header: ExcelHeader,
    schema: Arc<[ColumnDefinition]>,
}

impl<'sq> Sheet<'sq> {
    pub(crate) fn from_header(sqpack: &'sq SqPack, name: String, header: ExcelHeader) -> Self {
        let schema: Arc<[ColumnDefinition]> = Arc::from(header.columns().to_vec());
        Self {
            sqpack,
            name,
            header,
            schema,
        }
    }

    /// Sheet name as it appears in `exd/root.exl` (e.g. `"Item"`, `"Quest"`).
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// The parsed EXH header for this sheet.
    #[must_use]
    pub fn header(&self) -> &ExcelHeader {
        &self.header
    }

    /// Column definitions; delegates to [`ExcelHeader::columns`].
    #[must_use]
    pub fn columns(&self) -> &[ColumnDefinition] {
        self.header.columns()
    }

    /// Sheet variant (flat rows vs. sub-rows); delegates to [`ExcelHeader::kind`].
    #[must_use]
    pub fn kind(&self) -> SheetKind {
        self.header.kind()
    }

    /// Language variants available for this sheet; delegates to [`ExcelHeader::languages`].
    #[must_use]
    pub fn languages(&self) -> &[Language] {
        self.header.languages()
    }

    /// Page descriptors; delegates to [`ExcelHeader::pages`].
    #[must_use]
    pub fn pages(&self) -> &[PageDefinition] {
        self.header.pages()
    }

    /// Load and parse one page of row data for the given `language`.
    ///
    /// `page_start_id` must match a [`PageDefinition::start_id`] returned by
    /// [`Sheet::pages`]. Use [`Sheet::rows`] for automatic page iteration.
    pub fn page(&self, page_start_id: u32, language: Language) -> Result<ExcelData> {
        self.sqpack
            .excel_data(&self.name, &self.header, page_start_id, language)
    }

    /// Return a lazy iterator over all rows in the sheet for `language`.
    ///
    /// Pages are loaded on demand. For [`SheetKind::SubRows`] sheets the
    /// iterator flattens sub-rows so each item is a fully-addressed sub-row
    /// with both a `row_id` and a `sub_row_id`. Stops permanently after the
    /// first I/O error.
    #[must_use]
    pub fn rows(&self, language: Language) -> SheetRows<'_> {
        debug!(
            sheet = %self.name,
            ?language,
            pages = self.pages().len(),
            "sqpack.sheet.rows opened",
        );
        SheetRows {
            sheet: self,
            language,
            page_idx: 0,
            current: None,
            row_idx: 0,
            sub_idx: 0,
            current_row_bytes: None,
            done: false,
        }
    }

    /// Look up a single row by ID, returning `None` if absent.
    ///
    /// For [`SheetKind::SubRows`] sheets this returns sub-row 0. Use
    /// [`Sheet::sub_row`] to address a specific sub-row.
    ///
    /// Uses a heuristic page search (see [`Sheet::page_for`]) which may
    /// return `None` for sparse sheets where the row ID falls within a page
    /// range but is not actually present in the index.
    pub fn row(&self, row_id: u32, language: Language) -> Result<Option<OwnedRow>> {
        let span = debug_span!(
            span_names::SQPACK_SHEET_ROW,
            sheet = %self.name,
            row_id,
            ?language,
        );
        let _enter = span.enter();
        let started_at = std::time::Instant::now();

        let result = self.row_inner(row_id, language);
        #[allow(clippy::cast_possible_truncation)]
        let elapsed_ms = started_at.elapsed().as_millis() as u64;
        let outcome = match &result {
            Ok(Some(_)) => "hit",
            Ok(None) => "miss",
            Err(_) => "err",
        };
        debug!(outcome, elapsed_ms, "sqpack.sheet.row");
        result
    }

    fn row_inner(&self, row_id: u32, language: Language) -> Result<Option<OwnedRow>> {
        let Some(page) = self.page_for(row_id) else {
            return Ok(None);
        };
        let data = self.page(page.start_id, language)?;
        let owned = match self.kind() {
            SheetKind::Default => data.row(row_id).map(|row| self.owned_from_borrowed(&row)),
            SheetKind::SubRows => data
                .sub_row(row_id, 0)
                .map(|row| self.owned_from_borrowed(&row)),
        };
        Ok(owned)
    }

    /// Look up a specific sub-row by `(row_id, sub_id)`.
    ///
    /// Returns an error if called on a [`SheetKind::Default`] sheet. Returns
    /// `None` if the row or sub-row is absent.
    pub fn sub_row(
        &self,
        row_id: u32,
        sub_id: u16,
        language: Language,
    ) -> Result<Option<OwnedRow>> {
        let span = debug_span!(
            span_names::SQPACK_SHEET_SUB_ROW,
            sheet = %self.name,
            row_id,
            sub_id,
            ?language,
        );
        let _enter = span.enter();

        if self.kind() != SheetKind::SubRows {
            return Err(Error::SubRowOnDefaultSheet {
                sheet: self.name.clone(),
            });
        }

        let started_at = std::time::Instant::now();
        let result = self.sub_row_inner(row_id, sub_id, language);
        #[allow(clippy::cast_possible_truncation)]
        let elapsed_ms = started_at.elapsed().as_millis() as u64;
        let outcome = match &result {
            Ok(Some(_)) => "hit",
            Ok(None) => "miss",
            Err(_) => "err",
        };
        debug!(outcome, elapsed_ms, "sqpack.sheet.sub_row");
        result
    }

    fn sub_row_inner(
        &self,
        row_id: u32,
        sub_id: u16,
        language: Language,
    ) -> Result<Option<OwnedRow>> {
        let Some(page) = self.page_for(row_id) else {
            return Ok(None);
        };
        let data = self.page(page.start_id, language)?;
        let owned = data
            .sub_row(row_id, sub_id)
            .map(|row| self.owned_from_borrowed(&row));
        Ok(owned)
    }

    /// Heuristically find the page that might contain `row_id`.
    ///
    /// Scans pages linearly and returns the first whose `[start_id, start_id + row_count)` window
    /// covers `row_id`. This is a heuristic: pages can be sparse, so the returned page may not
    /// actually contain the requested ID. Always `None` for IDs that fall outside every page window.
    #[must_use]
    pub fn page_for(&self, row_id: u32) -> Option<&PageDefinition> {
        for page in self.pages() {
            let end_exclusive = page.start_id.saturating_add(page.row_count);
            if row_id >= page.start_id && row_id < end_exclusive {
                return Some(page);
            }
        }
        None
    }

    fn owned_from_borrowed(&self, row: &Row<'_>) -> OwnedRow {
        let bytes: Arc<[u8]> = Arc::from(row.row_payload_bytes());
        self.owned_from_borrowed_shared(row, bytes)
    }

    fn owned_from_borrowed_shared(&self, row: &Row<'_>, bytes: Arc<[u8]>) -> OwnedRow {
        OwnedRow {
            row_id: row.row_id(),
            sub_row_id: row.sub_row_id(),
            sub_row_count: row.sub_row_count(),
            row_size: self.header.row_size(),
            kind: self.header.kind(),
            bytes,
            struct_offset: row.struct_offset_in_row_payload(),
            schema: Arc::clone(&self.schema),
        }
    }
}

/// An Excel row whose payload is owned (via `Arc`) and independent of any [`ExcelData`] page.
///
/// Produced by [`Sheet::row`], [`Sheet::sub_row`], and the [`SheetRows`] iterator.
/// Because the bytes are reference-counted, cloning is cheap — the payload is not copied.
///
/// String values returned from [`OwnedRow::field`] borrow from `self` with lifetime `'_`,
/// not from the originating page.
#[derive(Debug, Clone)]
pub struct OwnedRow {
    row_id: u32,
    sub_row_id: Option<u16>,
    sub_row_count: u16,
    row_size: u16,
    kind: SheetKind,
    bytes: Arc<[u8]>,
    struct_offset: usize,
    schema: Arc<[ColumnDefinition]>,
}

impl OwnedRow {
    /// Top-level row ID.
    #[must_use]
    pub fn row_id(&self) -> u32 {
        self.row_id
    }

    /// Sub-row ID for [`SheetKind::SubRows`] sheets, or `None` for default-sheet rows.
    #[must_use]
    pub fn sub_row_id(&self) -> Option<u16> {
        self.sub_row_id
    }

    /// Number of sub-rows in this top-level row.
    #[must_use]
    pub fn sub_row_count(&self) -> u16 {
        self.sub_row_count
    }

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

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

    /// Decode the value at `column_idx`.
    ///
    /// String values borrow from `self`'s internal `Arc` payload and are
    /// valid for `'_` (the lifetime of the [`OwnedRow`] borrow, not the page).
    pub fn field(&self, column_idx: usize) -> Result<Field<'_>> {
        let column = *self.schema.get(column_idx).ok_or(Error::MalformedExdRow {
            row_id: self.row_id,
            reason: "column index out of range",
        })?;
        let fixed_bytes = self
            .bytes
            .get(self.struct_offset..)
            .ok_or(Error::MalformedExdRow {
                row_id: self.row_id,
                reason: "row payload shorter than struct_offset",
            })?;
        let row_size_usize = self.row_size as usize;
        if fixed_bytes.len() < row_size_usize {
            return Err(Error::MalformedExdRow {
                row_id: self.row_id,
                reason: "row payload shorter than declared row_size",
            });
        }
        let trailing_bytes = &fixed_bytes[row_size_usize..];
        crate::excel::exd::decode_column_field(
            self.row_id,
            column_idx,
            column,
            self.row_size,
            fixed_bytes,
            trailing_bytes,
        )
    }

    /// Return an iterator over all fields in column order.
    #[must_use]
    pub fn fields(&self) -> OwnedRowFields<'_> {
        OwnedRowFields {
            row: self,
            next_idx: 0,
        }
    }

    #[cfg(test)]
    fn bytes_ptr_for_test(&self) -> *const u8 {
        self.bytes.as_ptr()
    }
}

/// Iterator over all fields of an [`OwnedRow`] in column order.
///
/// Produced by [`OwnedRow::fields`]. Each item is `Result<Field<'_>>` so
/// decode errors on individual columns do not abort the iteration.
#[derive(Debug)]
pub struct OwnedRowFields<'a> {
    row: &'a OwnedRow,
    next_idx: usize,
}

impl<'a> Iterator for OwnedRowFields<'a> {
    type Item = Result<Field<'a>>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.next_idx >= self.row.schema.len() {
            return None;
        }
        let idx = self.next_idx;
        self.next_idx += 1;
        Some(self.row.field(idx))
    }
}

/// Lazy row iterator for an Excel sheet, produced by [`Sheet::rows`].
///
/// Loads pages on demand. For [`SheetKind::SubRows`] sheets sub-rows are
/// flattened: each `Item` is a fully-addressed `(row_id, sub_row_id)` pair.
/// The iterator latches `done` on the first I/O error so subsequent calls
/// to `next` always return `None`.
///
/// Sub-rows within the same top-level row share a single `Arc` payload
/// allocation to avoid redundant copies.
pub struct SheetRows<'sq> {
    sheet: &'sq Sheet<'sq>,
    language: Language,
    page_idx: usize,
    current: Option<CurrentPage>,
    row_idx: usize,
    sub_idx: u16,
    current_row_bytes: Option<Arc<[u8]>>,
    done: bool,
}

struct CurrentPage {
    data: ExcelData,
    row_count: usize,
}

impl Iterator for SheetRows<'_> {
    type Item = Result<OwnedRow>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.done {
            return None;
        }

        loop {
            let Some(current) = self.current.as_ref() else {
                let pages = self.sheet.pages();
                if self.page_idx >= pages.len() {
                    self.done = true;
                    return None;
                }
                let page = pages[self.page_idx];
                self.page_idx += 1;
                match self.sheet.page(page.start_id, self.language) {
                    Ok(data) => {
                        let row_count = data.row_count();
                        self.current = Some(CurrentPage { data, row_count });
                        self.row_idx = 0;
                        self.sub_idx = 0;
                        self.current_row_bytes = None;
                    }
                    Err(e) => {
                        self.done = true;
                        return Some(Err(e));
                    }
                }
                continue;
            };

            if self.row_idx >= current.row_count {
                self.current = None;
                self.current_row_bytes = None;
                continue;
            }

            let row = current
                .data
                .row_at(self.row_idx)
                .expect("row_idx bounded by row_count");

            match self.sheet.kind() {
                SheetKind::Default => {
                    let owned = self.sheet.owned_from_borrowed(&row);
                    self.row_idx += 1;
                    return Some(Ok(owned));
                }
                SheetKind::SubRows => {
                    let total = row.sub_row_count();
                    if total == 0 {
                        self.row_idx += 1;
                        self.sub_idx = 0;
                        self.current_row_bytes = None;
                        continue;
                    }
                    let Some(sub) = current.data.sub_row(row.row_id(), self.sub_idx) else {
                        self.row_idx += 1;
                        self.sub_idx = 0;
                        self.current_row_bytes = None;
                        continue;
                    };
                    let bytes = if let Some(arc) = self.current_row_bytes.as_ref() {
                        Arc::clone(arc)
                    } else {
                        let arc: Arc<[u8]> = Arc::from(sub.row_payload_bytes());
                        self.current_row_bytes = Some(Arc::clone(&arc));
                        arc
                    };
                    let owned = self.sheet.owned_from_borrowed_shared(&sub, bytes);
                    self.sub_idx += 1;
                    return Some(Ok(owned));
                }
            }
        }
    }
}

/// Iterator over all sheets in the install, produced by [`SqPack::sheets`](crate::sqpack::SqPack::sheets).
///
/// Each item is `Result<Sheet<'_>>` so failures to open individual sheet
/// headers do not abort the iteration.
///
/// Implements [`ExactSizeIterator`]; [`len`](SheetIter::len) returns the
/// number of remaining sheets.
pub struct SheetIter<'sq> {
    sqpack: &'sq SqPack,
    list: ExcelList,
    next_idx: usize,
}

impl<'sq> SheetIter<'sq> {
    pub(crate) fn new(sqpack: &'sq SqPack, list: ExcelList) -> Self {
        Self {
            sqpack,
            list,
            next_idx: 0,
        }
    }

    /// Number of sheets remaining in the iteration.
    #[must_use]
    pub fn len(&self) -> usize {
        ExactSizeIterator::len(self)
    }

    /// `true` if no sheets remain.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

impl<'sq> Iterator for SheetIter<'sq> {
    type Item = Result<Sheet<'sq>>;

    fn next(&mut self) -> Option<Self::Item> {
        let (name, _id) = self.list.entry(self.next_idx)?;
        let name_owned = name.to_owned();
        self.next_idx += 1;
        match self.sqpack.excel_header(&name_owned) {
            Ok(header) => Some(Ok(Sheet::from_header(self.sqpack, name_owned, header))),
            Err(e) => Some(Err(e)),
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let remaining = self.list.len().saturating_sub(self.next_idx);
        (remaining, Some(remaining))
    }
}

impl ExactSizeIterator for SheetIter<'_> {
    fn len(&self) -> usize {
        self.list.len().saturating_sub(self.next_idx)
    }
}

/// Iterator over all sheet names in the install, produced by [`SqPack::sheet_names`](crate::sqpack::SqPack::sheet_names).
///
/// Cheaper than [`SheetIter`] when only names are needed: no EXH file is opened.
/// Implements [`ExactSizeIterator`].
pub struct SheetNameIter {
    list: ExcelList,
    next_idx: usize,
}

impl SheetNameIter {
    pub(crate) fn new(list: ExcelList) -> Self {
        Self { list, next_idx: 0 }
    }

    /// Number of sheet names remaining.
    #[must_use]
    pub fn len(&self) -> usize {
        ExactSizeIterator::len(self)
    }

    /// `true` if no names remain.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

impl Iterator for SheetNameIter {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        let (name, _id) = self.list.entry(self.next_idx)?;
        self.next_idx += 1;
        Some(name.to_owned())
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let remaining = self.list.len().saturating_sub(self.next_idx);
        (remaining, Some(remaining))
    }
}

impl ExactSizeIterator for SheetNameIter {
    fn len(&self) -> usize {
        self.list.len().saturating_sub(self.next_idx)
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;
    use std::sync::Arc;

    use crate::excel::exh::{ColumnDataType, ColumnDefinition, Language, SheetKind};
    use crate::excel::{Field, exd::language_path_suffix};
    use crate::vfs::InMemoryFs;
    use crate::{Error, SqPack};

    const INSTALL_ROOT: &str = "/install";
    const ARCHIVE_FOLDER: &str = "/install/sqpack/ffxiv";
    const ARCHIVE_STEM: &str = "0a0000.win32";

    const SQPACK_HEADER_SIZE: u32 = 1024;

    fn sqpack_header(file_type: u32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(SQPACK_HEADER_SIZE as usize);
        buf.extend_from_slice(b"SqPack\0\0");
        buf.push(0);
        buf.extend_from_slice(&[0u8; 3]);
        buf.extend_from_slice(&SQPACK_HEADER_SIZE.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(&file_type.to_le_bytes());
        buf.resize(SQPACK_HEADER_SIZE as usize, 0);
        buf
    }

    fn index_header(index_data_offset: u32, index_data_size: u32) -> Vec<u8> {
        let mut buf = Vec::with_capacity(1024);
        buf.extend_from_slice(&1024u32.to_le_bytes());
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(&index_data_offset.to_le_bytes());
        buf.extend_from_slice(&index_data_size.to_le_bytes());
        buf.resize(buf.len() + 64, 0);
        buf.extend_from_slice(&1u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.resize(buf.len() + 64, 0);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.resize(buf.len() + 64, 0);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.resize(buf.len() + 64, 0);
        buf.extend_from_slice(&0u32.to_le_bytes());
        buf.resize(buf.len() + 656, 0);
        buf.resize(buf.len() + 64, 0);
        assert_eq!(buf.len(), 1024);
        buf
    }

    fn build_index_file(entries: &[(&str, u32)]) -> Vec<u8> {
        const INDEX_ENTRY_SIZE: u32 = crate::index::INDEX_ENTRY_SIZE;
        let mut buf = sqpack_header(2);
        let index_data_offset = SQPACK_HEADER_SIZE + 1024;
        let index_data_size =
            u32::try_from(entries.len()).expect("entries fit u32") * INDEX_ENTRY_SIZE;
        buf.extend_from_slice(&index_header(index_data_offset, index_data_size));

        let mut hashed: Vec<(u64, u32)> = entries
            .iter()
            .map(|(path, off)| (crate::hash::hash_index1_key(path), *off / 8))
            .collect();
        hashed.sort_by_key(|(k, _)| *k);
        for (k, raw) in &hashed {
            buf.extend_from_slice(&k.to_le_bytes());
            buf.extend_from_slice(&raw.to_le_bytes());
            buf.extend_from_slice(&0u32.to_le_bytes());
        }
        buf
    }

    fn build_dat_file(payloads: &[&[u8]]) -> (Vec<u8>, Vec<u32>) {
        const BLOCK_HEADER_SIZE: u32 = crate::dat::BLOCK_HEADER_SIZE;
        const BLOCK_TYPE_UNCOMPRESSED: u32 = 32000;

        let mut buf = sqpack_header(1);
        let mut offsets = Vec::with_capacity(payloads.len());
        for payload in payloads {
            while buf.len() % 128 != 0 {
                buf.push(0);
            }
            let offset = u32::try_from(buf.len()).expect("offset fits u32");
            offsets.push(offset);
            let raw_file_size = u32::try_from(payload.len()).expect("len fits u32");

            buf.extend_from_slice(&32u32.to_le_bytes());
            buf.extend_from_slice(&2u32.to_le_bytes());
            buf.extend_from_slice(&raw_file_size.to_le_bytes());
            buf.extend_from_slice(&0u32.to_le_bytes());
            buf.extend_from_slice(&0u32.to_le_bytes());
            buf.extend_from_slice(&1u32.to_le_bytes());
            let compressed_size =
                u16::try_from(BLOCK_HEADER_SIZE as usize + payload.len()).expect("u16");
            let uncompressed_size = u16::try_from(payload.len()).expect("u16");
            buf.extend_from_slice(&0u32.to_le_bytes());
            buf.extend_from_slice(&compressed_size.to_le_bytes());
            buf.extend_from_slice(&uncompressed_size.to_le_bytes());
            buf.extend_from_slice(&BLOCK_HEADER_SIZE.to_le_bytes());
            buf.extend_from_slice(&0u32.to_le_bytes());
            buf.extend_from_slice(&BLOCK_TYPE_UNCOMPRESSED.to_le_bytes());
            buf.extend_from_slice(&u32::from(uncompressed_size).to_le_bytes());
            buf.extend_from_slice(payload);
        }
        (buf, offsets)
    }

    fn install_with_files(files: &[(String, Vec<u8>)]) -> SqPack {
        let payloads: Vec<&[u8]> = files.iter().map(|(_, bytes)| bytes.as_slice()).collect();
        let (dat_bytes, offsets) = build_dat_file(&payloads);

        let index_entries: Vec<(&str, u32)> = files
            .iter()
            .zip(offsets.iter())
            .map(|((path, _), off)| (path.as_str(), *off))
            .collect();
        let index_bytes = build_index_file(&index_entries);

        let fs = InMemoryFs::new();
        fs.insert(
            PathBuf::from(format!("{ARCHIVE_FOLDER}/{ARCHIVE_STEM}.index")),
            index_bytes,
        );
        fs.insert(
            PathBuf::from(format!("{ARCHIVE_FOLDER}/{ARCHIVE_STEM}.dat0")),
            dat_bytes,
        );
        SqPack::open_with_vfs(INSTALL_ROOT, Arc::new(fs)).expect("open install")
    }

    fn build_exh_bytes(
        row_size: u16,
        kind: SheetKind,
        columns: &[ColumnDefinition],
        pages: &[(u32, u32)],
        languages: &[Language],
    ) -> Vec<u8> {
        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()).unwrap();
        let page_count = u16::try_from(pages.len()).unwrap();
        let language_count = u16::try_from(languages.len()).unwrap();
        buf.extend_from_slice(&column_count.to_be_bytes());
        buf.extend_from_slice(&page_count.to_be_bytes());
        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());
        let total_row_count: u32 = pages.iter().map(|(_, n)| *n).sum();
        buf.extend_from_slice(&total_row_count.to_be_bytes());
        buf.extend_from_slice(&0u32.to_be_bytes());
        buf.extend_from_slice(&0u32.to_be_bytes());
        for col in columns {
            buf.extend_from_slice(&column_type_raw(col.data_type).to_be_bytes());
            buf.extend_from_slice(&col.offset.to_be_bytes());
        }
        for (start_id, rc) in pages {
            buf.extend_from_slice(&start_id.to_be_bytes());
            buf.extend_from_slice(&rc.to_be_bytes());
        }
        for lang in languages {
            buf.push(language_raw(*lang));
            buf.push(0);
        }
        buf
    }

    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>,
    }

    fn build_exd_default_bytes(rows: &[RowSpec], row_size: u16) -> Vec<u8> {
        const EXD_FIXED_HEADER_SIZE: usize = 0x20;
        const EXD_INDEX_ENTRY_SIZE: usize = 8;

        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).unwrap();
        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);
            let row_offset = u32::try_from(buf.len()).unwrap();
            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()).unwrap();
            buf.extend_from_slice(&payload_len.to_be_bytes());
            buf.extend_from_slice(&1u16.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
    }

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

    fn build_exd_subrows_bytes(rows: &[SubRowSpec], row_size: u16) -> Vec<u8> {
        const EXD_FIXED_HEADER_SIZE: usize = 0x20;
        const EXD_INDEX_ENTRY_SIZE: usize = 8;

        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).unwrap();
        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()).unwrap();
            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()).unwrap();
            let payload_len = (sub_count as usize) * (row_size as usize + 2) + row.trailing.len();
            buf.extend_from_slice(&u32::try_from(payload_len).unwrap().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);
                buf.extend_from_slice(&u16::try_from(sub_id).unwrap().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
    }

    fn exl_bytes_for(names: &[&str]) -> Vec<u8> {
        let mut out = String::from("EXLT,2\n");
        for (i, name) in names.iter().enumerate() {
            out.push_str(name);
            out.push(',');
            out.push_str(&i.to_string());
            out.push('\n');
        }
        out.into_bytes()
    }

    fn exd_path(sheet: &str, page: u32, language: Language) -> String {
        let suffix = language_path_suffix(language);
        if suffix.is_empty() {
            format!("exd/{sheet}_{page}.exd")
        } else {
            format!("exd/{sheet}_{page}_{suffix}.exd")
        }
    }

    fn default_row_spec(id: u32, int: i32, byte_at_8: u8, trailing: &str) -> RowSpec {
        let mut fixed = vec![0u8; 12];
        fixed[0..4].copy_from_slice(&int.to_be_bytes());
        fixed[4..8].copy_from_slice(&0u32.to_be_bytes());
        fixed[8] = byte_at_8;
        let mut trailing_bytes = trailing.as_bytes().to_vec();
        trailing_bytes.push(0);
        RowSpec {
            row_id: id,
            fixed,
            trailing: trailing_bytes,
        }
    }

    #[test]
    #[allow(clippy::too_many_lines)]
    fn sheet_handle_iterates_default_sheet_across_pages_and_languages() {
        let columns = [
            ColumnDefinition {
                data_type: ColumnDataType::Int32,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::String,
                offset: 4,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt8,
                offset: 8,
            },
        ];
        let exh = build_exh_bytes(
            12,
            SheetKind::Default,
            &columns,
            &[(1, 2), (100, 1)],
            &[Language::English, Language::Japanese],
        );

        let exd_en_1 = build_exd_default_bytes(
            &[
                default_row_spec(1, 11, 0xAA, "one"),
                default_row_spec(2, 22, 0xBB, "two"),
            ],
            12,
        );
        let exd_en_100 =
            build_exd_default_bytes(&[default_row_spec(100, 100, 0xCC, "hundred")], 12);
        let exd_ja_1 = build_exd_default_bytes(
            &[
                default_row_spec(1, 11, 0xAA, "ichi"),
                default_row_spec(2, 22, 0xBB, "ni"),
            ],
            12,
        );
        let exd_ja_100 = build_exd_default_bytes(&[default_row_spec(100, 100, 0xCC, "hyaku")], 12);

        let exl = exl_bytes_for(&["Foo"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Foo.exh".to_string(), exh),
            (exd_path("Foo", 1, Language::English), exd_en_1),
            (exd_path("Foo", 100, Language::English), exd_en_100),
            (exd_path("Foo", 1, Language::Japanese), exd_ja_1),
            (exd_path("Foo", 100, Language::Japanese), exd_ja_100),
        ]);

        let sheet = sq.sheet("Foo").expect("open sheet");
        assert_eq!(sheet.name(), "Foo");
        assert_eq!(sheet.columns().len(), 3);
        assert_eq!(sheet.kind(), SheetKind::Default);
        assert_eq!(sheet.pages().len(), 2);
        assert_eq!(sheet.languages(), &[Language::English, Language::Japanese]);

        let walked: Vec<_> = sheet
            .rows(Language::English)
            .collect::<Result<Vec<_>, _>>()
            .expect("walk en");
        assert_eq!(walked.len(), 3);
        assert_eq!(walked[0].row_id(), 1);
        assert_eq!(walked[1].row_id(), 2);
        assert_eq!(walked[2].row_id(), 100);
        assert!(walked.iter().all(|r| r.sub_row_id().is_none()));

        let head = &walked[0];
        match head.field(1).unwrap() {
            Field::String(s) => assert_eq!(s, "one"),
            other => panic!("expected String, got {other:?}"),
        }
        let borrowed = match head.field(1).unwrap() {
            Field::String(s) => s,
            other => panic!("expected String, got {other:?}"),
        };
        assert_eq!(borrowed.len(), 3);
        let collected: Vec<_> = head.fields().collect::<Result<Vec<_>, _>>().unwrap();
        assert_eq!(collected.len(), 3);
        assert_eq!(collected[0], Field::Int32(11));
        assert_eq!(collected[1], Field::String("one"));
        assert_eq!(collected[2], Field::UInt8(0xAA));

        assert!(walked.iter().all(super::OwnedRow::is_first_sub_row));

        let walked_ja: Vec<_> = sheet
            .rows(Language::Japanese)
            .collect::<Result<Vec<_>, _>>()
            .expect("walk ja");
        assert_eq!(walked_ja.len(), 3);
        match walked_ja[2].field(1).unwrap() {
            Field::String(s) => assert_eq!(s, "hyaku"),
            other => panic!("expected String, got {other:?}"),
        }

        let second = sheet
            .row(2, Language::English)
            .expect("row 2 ok")
            .expect("row 2 present");
        assert_eq!(second.row_id(), 2);
        assert_eq!(second.field(0).unwrap(), Field::Int32(22));

        let page1 = sheet.page(1, Language::English).expect("page 1 en");
        assert_eq!(page1.row_count(), 2);
    }

    #[test]
    fn sheet_handle_iterates_subrows_sheet_with_flattening() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            4,
            SheetKind::SubRows,
            &columns,
            &[(1, 2)],
            &[Language::None],
        );

        let first = SubRowSpec {
            row_id: 1,
            sub_fixed: vec![10i32.to_be_bytes().to_vec(), 20i32.to_be_bytes().to_vec()],
            trailing: Vec::new(),
        };
        let second = SubRowSpec {
            row_id: 2,
            sub_fixed: vec![100i32.to_be_bytes().to_vec(), 200i32.to_be_bytes().to_vec()],
            trailing: Vec::new(),
        };
        let exd = build_exd_subrows_bytes(&[first, second], 4);

        let exl = exl_bytes_for(&["Bar"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Bar.exh".to_string(), exh),
            (exd_path("Bar", 1, Language::None), exd),
        ]);

        let sheet = sq.sheet("Bar").expect("open Bar");
        assert_eq!(sheet.kind(), SheetKind::SubRows);

        let rows: Vec<_> = sheet
            .rows(Language::None)
            .collect::<Result<Vec<_>, _>>()
            .expect("walk");
        assert_eq!(rows.len(), 4);
        assert_eq!(rows[0].row_id(), 1);
        assert_eq!(rows[0].sub_row_id(), Some(0));
        assert!(rows[0].is_first_sub_row());
        assert_eq!(rows[0].field(0).unwrap(), Field::Int32(10));
        assert_eq!(rows[1].row_id(), 1);
        assert_eq!(rows[1].sub_row_id(), Some(1));
        assert!(!rows[1].is_first_sub_row());
        assert_eq!(rows[1].field(0).unwrap(), Field::Int32(20));
        assert_eq!(rows[2].row_id(), 2);
        assert_eq!(rows[2].sub_row_id(), Some(0));
        assert_eq!(rows[2].field(0).unwrap(), Field::Int32(100));
        assert_eq!(rows[3].row_id(), 2);
        assert_eq!(rows[3].sub_row_id(), Some(1));
        assert_eq!(rows[3].field(0).unwrap(), Field::Int32(200));

        let r1 = sheet
            .row(1, Language::None)
            .expect("ok")
            .expect("row 1 present");
        assert_eq!(r1.row_id(), 1);
        assert_eq!(r1.sub_row_id(), Some(0));
    }

    #[test]
    fn sheet_rows_surfaces_language_not_available() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 1)],
            &[Language::English],
        );
        let exd = build_exd_default_bytes(
            &[RowSpec {
                row_id: 0,
                fixed: vec![1u8],
                trailing: Vec::new(),
            }],
            1,
        );

        let exl = exl_bytes_for(&["Baz"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Baz.exh".to_string(), exh),
            (exd_path("Baz", 0, Language::English), exd),
        ]);

        let sheet = sq.sheet("Baz").expect("open Baz");
        let first = sheet.rows(Language::German).next();
        match first {
            Some(Err(Error::LanguageNotAvailable {
                requested: Language::German,
                ..
            })) => {}
            other => panic!("expected LanguageNotAvailable, got {other:?}"),
        }
        let mut iter = sheet.rows(Language::German);
        let _ = iter.next();
        assert!(iter.next().is_none(), "iterator must stop after Err");

        let err = sheet.row(0, Language::German).unwrap_err();
        assert!(matches!(
            err,
            Error::LanguageNotAvailable {
                requested: Language::German,
                ..
            },
        ));
    }

    #[test]
    fn sheet_rows_stops_after_missing_page_file() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 2), (10, 1)],
            &[Language::English],
        );
        let exd_first = build_exd_default_bytes(
            &[
                RowSpec {
                    row_id: 0,
                    fixed: vec![1u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 1,
                    fixed: vec![2u8],
                    trailing: Vec::new(),
                },
            ],
            1,
        );

        let exl = exl_bytes_for(&["Qux"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Qux.exh".to_string(), exh),
            (exd_path("Qux", 0, Language::English), exd_first),
        ]);

        let sheet = sq.sheet("Qux").expect("open Qux");
        let mut iter = sheet.rows(Language::English);
        let r0 = iter.next().expect("row 0").expect("ok");
        assert_eq!(r0.row_id(), 0);
        let r1 = iter.next().expect("row 1").expect("ok");
        assert_eq!(r1.row_id(), 1);
        let err = iter.next().expect("missing page item");
        assert!(
            matches!(err, Err(Error::PathNotFound { .. })),
            "got {err:?}"
        );
        assert!(iter.next().is_none(), "iterator must latch done after Err");
    }

    #[test]
    fn sheet_row_page_lookup_corner_cases() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(10, 2), (100, 3)],
            &[Language::None],
        );
        let exd_p1 = build_exd_default_bytes(
            &[
                RowSpec {
                    row_id: 10,
                    fixed: vec![1u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 11,
                    fixed: vec![2u8],
                    trailing: Vec::new(),
                },
            ],
            1,
        );
        let exd_p2 = build_exd_default_bytes(
            &[
                RowSpec {
                    row_id: 100,
                    fixed: vec![10u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 101,
                    fixed: vec![20u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 102,
                    fixed: vec![30u8],
                    trailing: Vec::new(),
                },
            ],
            1,
        );

        let exl = exl_bytes_for(&["P"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/P.exh".to_string(), exh),
            (exd_path("P", 10, Language::None), exd_p1),
            (exd_path("P", 100, Language::None), exd_p2),
        ]);

        let sheet = sq.sheet("P").expect("open");

        assert!(sheet.row(5, Language::None).expect("ok").is_none());
        assert!(sheet.row(50, Language::None).expect("ok").is_none());
        assert!(sheet.row(200, Language::None).expect("ok").is_none());
        let r = sheet
            .row(101, Language::None)
            .expect("ok")
            .expect("present");
        assert_eq!(r.row_id(), 101);
        assert_eq!(r.field(0).unwrap(), Field::UInt8(20));
    }

    #[test]
    fn sheet_row_returns_none_when_heuristic_points_at_sparse_page() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(10, 5)],
            &[Language::None],
        );
        let exh = {
            let _ = exh;
            build_exh_bytes(
                1,
                SheetKind::Default,
                &columns,
                &[(10, 2)],
                &[Language::None],
            )
        };
        let exd = build_exd_default_bytes(
            &[
                RowSpec {
                    row_id: 10,
                    fixed: vec![1u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 14,
                    fixed: vec![14u8],
                    trailing: Vec::new(),
                },
            ],
            1,
        );

        let exl = exl_bytes_for(&["S"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/S.exh".to_string(), exh),
            (exd_path("S", 10, Language::None), exd),
        ]);

        let sheet = sq.sheet("S").expect("open");
        assert!(
            sheet.row(14, Language::None).expect("ok").is_none(),
            "page_for is a heuristic; row 14 lives outside the [10, 12) window",
        );
        assert!(
            sheet.row(11, Language::None).expect("ok").is_none(),
            "heuristic claims row 11 is in the page; the page index says it isn't",
        );
        let r = sheet
            .row(10, Language::None)
            .expect("ok")
            .expect("row 10 present");
        assert_eq!(r.row_id(), 10);
    }

    #[test]
    fn sqpack_sheets_and_sheet_names_yield_every_entry() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header_a = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 1)],
            &[Language::None],
        );
        let header_b = header_a.clone();
        let page_a = build_exd_default_bytes(
            &[RowSpec {
                row_id: 0,
                fixed: vec![1u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let page_b = page_a.clone();

        let exl = exl_bytes_for(&["A", "B"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/A.exh".to_string(), header_a),
            ("exd/B.exh".to_string(), header_b),
            (exd_path("A", 0, Language::None), page_a),
            (exd_path("B", 0, Language::None), page_b),
        ]);

        let names: Vec<_> = sq.sheet_names().expect("names").collect();
        assert_eq!(names, vec!["A".to_owned(), "B".to_owned()]);

        let sheets: Vec<_> = sq
            .sheets()
            .expect("sheets")
            .collect::<Result<Vec<_>, _>>()
            .expect("all open");
        assert_eq!(sheets.len(), 2);
        assert_eq!(sheets[0].name(), "A");
        assert_eq!(sheets[1].name(), "B");
    }

    #[test]
    fn sheet_iterators_are_exact_size() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let header_a = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 1)],
            &[Language::None],
        );
        let header_b = header_a.clone();
        let header_c = header_a.clone();
        let page_a = build_exd_default_bytes(
            &[RowSpec {
                row_id: 0,
                fixed: vec![1u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let page_b = page_a.clone();
        let page_c = page_a.clone();

        let exl = exl_bytes_for(&["A", "B", "C"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/A.exh".to_string(), header_a),
            ("exd/B.exh".to_string(), header_b),
            ("exd/C.exh".to_string(), header_c),
            (exd_path("A", 0, Language::None), page_a),
            (exd_path("B", 0, Language::None), page_b),
            (exd_path("C", 0, Language::None), page_c),
        ]);

        let mut sheets = sq.sheets().expect("sheets");
        assert_eq!(sheets.len(), 3);
        assert_eq!(sheets.size_hint(), (3, Some(3)));
        let _ = sheets.next().expect("first").expect("ok");
        assert_eq!(sheets.len(), 2);
        assert_eq!(sheets.size_hint(), (2, Some(2)));
        let _ = sheets.next().expect("second").expect("ok");
        let _ = sheets.next().expect("third").expect("ok");
        assert_eq!(sheets.len(), 0);
        assert_eq!(sheets.size_hint(), (0, Some(0)));
        assert!(sheets.next().is_none());

        let mut names = sq.sheet_names().expect("names");
        assert_eq!(names.len(), 3);
        assert_eq!(names.size_hint(), (3, Some(3)));
        let _ = names.next().expect("first");
        assert_eq!(names.len(), 2);
        assert_eq!(names.size_hint(), (2, Some(2)));
        let _ = names.next().expect("second");
        let _ = names.next().expect("third");
        assert_eq!(names.len(), 0);
        assert_eq!(names.size_hint(), (0, Some(0)));
        assert!(names.next().is_none());
    }

    #[test]
    fn sqpack_sheets_continues_past_per_sheet_failure() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh_a = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 1)],
            &[Language::None],
        );

        let exl = exl_bytes_for(&["A", "B", "C"]);
        let exh_c = exh_a.clone();
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/A.exh".to_string(), exh_a),
            ("exd/C.exh".to_string(), exh_c),
        ]);

        let items: Vec<_> = sq.sheets().expect("sheets").collect();
        assert_eq!(items.len(), 3);
        assert!(items[0].is_ok(), "A should open");
        assert!(items[1].is_err(), "B should fail");
        assert!(items[2].is_ok(), "C should open after B failed");
    }

    #[test]
    fn sheet_row_heuristic_misses_sparse_row() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 3), (10, 3)],
            &[Language::None],
        );

        let page_a = build_exd_default_bytes(
            &[
                RowSpec {
                    row_id: 0,
                    fixed: vec![100u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 1,
                    fixed: vec![101u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 2,
                    fixed: vec![102u8],
                    trailing: Vec::new(),
                },
            ],
            1,
        );
        let page_b = build_exd_default_bytes(
            &[
                RowSpec {
                    row_id: 5,
                    fixed: vec![5u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 10,
                    fixed: vec![10u8],
                    trailing: Vec::new(),
                },
                RowSpec {
                    row_id: 11,
                    fixed: vec![11u8],
                    trailing: Vec::new(),
                },
            ],
            1,
        );

        let exl = exl_bytes_for(&["Sparse"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Sparse.exh".to_string(), exh),
            (exd_path("Sparse", 0, Language::None), page_a),
            (exd_path("Sparse", 10, Language::None), page_b),
        ]);

        let sheet = sq.sheet("Sparse").expect("open Sparse");

        let missed = sheet.row(5, Language::None).expect("ok");
        assert!(
            missed.is_none(),
            "Sheet::row should miss sparse row 5 under the heuristic, got {missed:?}",
        );

        let walked: Vec<u32> = sheet
            .rows(Language::None)
            .collect::<Result<Vec<_>, _>>()
            .expect("walk")
            .into_iter()
            .map(|r| r.row_id())
            .collect();
        assert!(
            walked.contains(&5),
            "Sheet::rows should find row 5 via exhaustive iteration; got {walked:?}",
        );
        assert_eq!(walked, vec![0, 1, 2, 5, 10, 11]);

        let r10 = sheet
            .row(10, Language::None)
            .expect("ok")
            .expect("row 10 present");
        assert_eq!(r10.row_id(), 10);
    }

    #[test]
    #[allow(clippy::too_many_lines)]
    fn decode_parity_with_borrowed_row() {
        const ROW_SIZE: u16 = 47;
        let columns = [
            ColumnDefinition {
                data_type: ColumnDataType::Bool,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int8,
                offset: 1,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt8,
                offset: 2,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int16,
                offset: 3,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt16,
                offset: 5,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int32,
                offset: 7,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt32,
                offset: 11,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Float32,
                offset: 15,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int64,
                offset: 19,
            },
            ColumnDefinition {
                data_type: ColumnDataType::UInt64,
                offset: 27,
            },
            ColumnDefinition {
                data_type: ColumnDataType::String,
                offset: 35,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool0,
                offset: 39,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool1,
                offset: 40,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool2,
                offset: 41,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool3,
                offset: 42,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool4,
                offset: 43,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool5,
                offset: 44,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool6,
                offset: 45,
            },
            ColumnDefinition {
                data_type: ColumnDataType::PackedBool7,
                offset: 46,
            },
        ];

        let mut fixed = vec![0u8; ROW_SIZE as usize];
        fixed[0] = 0x01;
        fixed[1] = 0x7F;
        fixed[2] = 0xAB;
        fixed[3..5].copy_from_slice(&0x1234_i16.to_be_bytes());
        fixed[5..7].copy_from_slice(&0x5678_u16.to_be_bytes());
        fixed[7..11].copy_from_slice(&0x1122_3344_i32.to_be_bytes());
        fixed[11..15].copy_from_slice(&0x5566_7788_u32.to_be_bytes());
        fixed[15..19].copy_from_slice(&core::f32::consts::PI.to_be_bytes());
        fixed[19..27].copy_from_slice(&0x0102_0304_0506_0708_i64.to_be_bytes());
        fixed[27..35].copy_from_slice(&0x1020_3040_5060_7080_u64.to_be_bytes());
        fixed[35..39].copy_from_slice(&0u32.to_be_bytes());
        fixed[39] = 0b0000_0001;
        fixed[40] = 0b0000_0010;
        fixed[41] = 0b0000_0100;
        fixed[42] = 0b0000_1000;
        fixed[43] = 0b0001_0000;
        fixed[44] = 0b0010_0000;
        fixed[45] = 0b0100_0000;
        fixed[46] = 0b1000_0000;

        let mut trailing = b"parity".to_vec();
        trailing.push(0);

        let exh = build_exh_bytes(
            ROW_SIZE,
            SheetKind::Default,
            &columns,
            &[(1, 1)],
            &[Language::English],
        );
        let exd = build_exd_default_bytes(
            &[RowSpec {
                row_id: 1,
                fixed,
                trailing,
            }],
            ROW_SIZE,
        );

        let exl = exl_bytes_for(&["Parity"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Parity.exh".to_string(), exh),
            (exd_path("Parity", 1, Language::English), exd),
        ]);

        let header = sq.excel_header("Parity").expect("header");
        let data = sq
            .excel_data("Parity", &header, 1, Language::English)
            .expect("data");
        let borrowed = data.row(1).expect("borrowed row");

        let sheet = sq.sheet("Parity").expect("sheet");
        let owned = sheet
            .row(1, Language::English)
            .expect("ok")
            .expect("owned row");

        assert_eq!(borrowed.column_count(), owned.column_count());
        for (idx, column) in columns.iter().enumerate() {
            let b = borrowed.field(idx).expect("borrowed field");
            let o = owned.field(idx).expect("owned field");
            assert_eq!(
                b, o,
                "decode-table drift at column {idx} \
                 (type {:?}): borrowed={b:?} owned={o:?}",
                column.data_type,
            );
        }
    }

    #[test]
    fn sheet_sub_row_returns_addressed_sub_row() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            4,
            SheetKind::SubRows,
            &columns,
            &[(1, 2)],
            &[Language::None],
        );
        let specs = vec![SubRowSpec {
            row_id: 1,
            sub_fixed: vec![
                10i32.to_be_bytes().to_vec(),
                20i32.to_be_bytes().to_vec(),
                30i32.to_be_bytes().to_vec(),
            ],
            trailing: Vec::new(),
        }];
        let exd = build_exd_subrows_bytes(&specs, 4);
        let exl = exl_bytes_for(&["S"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/S.exh".to_string(), exh),
            (exd_path("S", 1, Language::None), exd),
        ]);
        let sheet = sq.sheet("S").expect("open");
        assert_eq!(sheet.kind(), SheetKind::SubRows);

        let s0 = sheet
            .sub_row(1, 0, Language::None)
            .expect("ok")
            .expect("sub_row(1, 0) present");
        assert_eq!(s0.row_id(), 1);
        assert_eq!(s0.sub_row_id(), Some(0));
        assert_eq!(s0.field(0).unwrap(), Field::Int32(10));

        let s1 = sheet
            .sub_row(1, 1, Language::None)
            .expect("ok")
            .expect("sub_row(1, 1) present");
        assert_eq!(s1.sub_row_id(), Some(1));
        assert_eq!(s1.field(0).unwrap(), Field::Int32(20));

        let s2 = sheet
            .sub_row(1, 2, Language::None)
            .expect("ok")
            .expect("sub_row(1, 2) present");
        assert_eq!(s2.sub_row_id(), Some(2));
        assert_eq!(s2.field(0).unwrap(), Field::Int32(30));
    }

    #[test]
    fn sheet_sub_row_rejects_default_sheet() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 1)],
            &[Language::None],
        );
        let exd = build_exd_default_bytes(
            &[RowSpec {
                row_id: 0,
                fixed: vec![7u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let exl = exl_bytes_for(&["D"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/D.exh".to_string(), exh),
            (exd_path("D", 0, Language::None), exd),
        ]);
        let sheet = sq.sheet("D").expect("open");
        let err = sheet.sub_row(0, 0, Language::None).unwrap_err();
        match err {
            Error::SubRowOnDefaultSheet { sheet: name } => {
                assert_eq!(name, "D");
            }
            other => panic!("expected SubRowOnDefaultSheet, got {other:?}"),
        }
    }

    #[test]
    fn sheet_sub_row_returns_none_for_missing_addresses() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            4,
            SheetKind::SubRows,
            &columns,
            &[(1, 1)],
            &[Language::None],
        );
        let specs = vec![SubRowSpec {
            row_id: 1,
            sub_fixed: vec![10i32.to_be_bytes().to_vec(), 20i32.to_be_bytes().to_vec()],
            trailing: Vec::new(),
        }];
        let exd = build_exd_subrows_bytes(&specs, 4);
        let exl = exl_bytes_for(&["M"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/M.exh".to_string(), exh),
            (exd_path("M", 1, Language::None), exd),
        ]);
        let sheet = sq.sheet("M").expect("open");

        let oob = sheet.sub_row(1, 5, Language::None).expect("ok");
        assert!(oob.is_none(), "expected None for out-of-range sub_id");

        let absent_row = sheet.sub_row(999, 0, Language::None).expect("ok");
        assert!(
            absent_row.is_none(),
            "expected None for row_id outside any page's window",
        );
    }

    #[test]
    fn owned_row_is_send_and_sync() {
        const fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<crate::excel::OwnedRow>();
    }

    #[test]
    fn subrows_iteration_shares_row_level_payload_arc() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            4,
            SheetKind::SubRows,
            &columns,
            &[(1, 1)],
            &[Language::None],
        );
        let specs = vec![SubRowSpec {
            row_id: 1,
            sub_fixed: vec![
                100i32.to_be_bytes().to_vec(),
                200i32.to_be_bytes().to_vec(),
                300i32.to_be_bytes().to_vec(),
            ],
            trailing: Vec::new(),
        }];
        let exd = build_exd_subrows_bytes(&specs, 4);
        let exl = exl_bytes_for(&["Share"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Share.exh".to_string(), exh),
            (exd_path("Share", 1, Language::None), exd),
        ]);
        let sheet = sq.sheet("Share").expect("open");

        let rows: Vec<_> = sheet
            .rows(Language::None)
            .collect::<Result<Vec<_>, _>>()
            .expect("iterate");
        assert_eq!(rows.len(), 3);
        assert_eq!(rows[0].field(0).unwrap(), Field::Int32(100));
        assert_eq!(rows[1].field(0).unwrap(), Field::Int32(200));
        assert_eq!(rows[2].field(0).unwrap(), Field::Int32(300));

        let p0 = rows[0].bytes_ptr_for_test();
        let p1 = rows[1].bytes_ptr_for_test();
        let p2 = rows[2].bytes_ptr_for_test();
        assert_eq!(p0, p1, "sub_id 0 and 1 must share row-level Arc<[u8]>");
        assert_eq!(p1, p2, "sub_id 1 and 2 must share row-level Arc<[u8]>");
    }

    #[test]
    fn subrows_iteration_does_not_share_arc_across_parent_rows() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            4,
            SheetKind::SubRows,
            &columns,
            &[(1, 2)],
            &[Language::None],
        );
        let specs = vec![
            SubRowSpec {
                row_id: 1,
                sub_fixed: vec![10i32.to_be_bytes().to_vec()],
                trailing: Vec::new(),
            },
            SubRowSpec {
                row_id: 2,
                sub_fixed: vec![20i32.to_be_bytes().to_vec()],
                trailing: Vec::new(),
            },
        ];
        let exd = build_exd_subrows_bytes(&specs, 4);
        let exl = exl_bytes_for(&["Diff"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Diff.exh".to_string(), exh),
            (exd_path("Diff", 1, Language::None), exd),
        ]);
        let sheet = sq.sheet("Diff").expect("open");

        let rows: Vec<_> = sheet
            .rows(Language::None)
            .collect::<Result<Vec<_>, _>>()
            .expect("iterate");
        assert_eq!(rows.len(), 2);
        let p0 = rows[0].bytes_ptr_for_test();
        let p1 = rows[1].bytes_ptr_for_test();
        assert_ne!(
            p0, p1,
            "different parent rows must have distinct row-level buffers",
        );
    }

    #[test]
    fn owned_subrows_with_string_column_matches_borrowed() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::String,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            4,
            SheetKind::SubRows,
            &columns,
            &[(1, 1)],
            &[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 exd = build_exd_subrows_bytes(&specs, 4);
        let exl = exl_bytes_for(&["Sub"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Sub.exh".to_string(), exh),
            (exd_path("Sub", 1, Language::None), exd),
        ]);

        let sheet = sq.sheet("Sub").expect("open");
        let header = sq.excel_header("Sub").expect("header");
        let data = sq
            .excel_data("Sub", &header, 1, Language::None)
            .expect("data");

        let b0 = data.sub_row(1, 0).expect("borrowed sub_row(1, 0)");
        let b1 = data.sub_row(1, 1).expect("borrowed sub_row(1, 1)");
        assert_eq!(b0.field(0).unwrap(), Field::String("alpha"));
        assert_eq!(b1.field(0).unwrap(), Field::String("beta"));

        let o0 = sheet
            .sub_row(1, 0, Language::None)
            .expect("ok")
            .expect("owned sub_row(1, 0)");
        let o1 = sheet
            .sub_row(1, 1, Language::None)
            .expect("ok")
            .expect("owned sub_row(1, 1)");
        assert_eq!(o0.field(0).unwrap(), Field::String("alpha"));
        assert_eq!(o1.field(0).unwrap(), Field::String("beta"));

        let rows: Vec<_> = sheet
            .rows(Language::None)
            .collect::<Result<Vec<_>, _>>()
            .expect("iterate");
        assert_eq!(rows.len(), 2);
        assert_eq!(rows[0].field(0).unwrap(), Field::String("alpha"));
        assert_eq!(rows[1].field(0).unwrap(), Field::String("beta"));
    }

    #[test]
    fn sheet_rows_skips_subrows_entry_with_zero_sub_rows() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::Int32,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            4,
            SheetKind::SubRows,
            &columns,
            &[(1, 3)],
            &[Language::None],
        );

        let specs = vec![
            SubRowSpec {
                row_id: 1,
                sub_fixed: vec![10i32.to_be_bytes().to_vec(), 20i32.to_be_bytes().to_vec()],
                trailing: Vec::new(),
            },
            SubRowSpec {
                row_id: 2,
                sub_fixed: Vec::new(),
                trailing: Vec::new(),
            },
            SubRowSpec {
                row_id: 3,
                sub_fixed: vec![30i32.to_be_bytes().to_vec()],
                trailing: Vec::new(),
            },
        ];
        let exd = build_exd_subrows_bytes(&specs, 4);

        let exl = exl_bytes_for(&["Skip"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Skip.exh".to_string(), exh),
            (exd_path("Skip", 1, Language::None), exd),
        ]);

        let sheet = sq.sheet("Skip").expect("open Skip");

        let rows: Vec<_> = sheet
            .rows(Language::None)
            .collect::<Result<Vec<_>, _>>()
            .expect("iterate without erroring");

        assert_eq!(
            rows.len(),
            3,
            "row 2's zero sub-rows must contribute nothing to iteration",
        );

        assert_eq!(rows[0].row_id(), 1);
        assert_eq!(rows[0].sub_row_id(), Some(0));
        assert_eq!(rows[0].field(0).unwrap(), Field::Int32(10));
        assert_eq!(rows[1].row_id(), 1);
        assert_eq!(rows[1].sub_row_id(), Some(1));
        assert_eq!(rows[1].field(0).unwrap(), Field::Int32(20));

        assert_eq!(rows[2].row_id(), 3);
        assert_eq!(rows[2].sub_row_id(), Some(0));
        assert_eq!(rows[2].field(0).unwrap(), Field::Int32(30));

        assert!(
            rows.iter().all(|r| r.row_id() != 2),
            "no OwnedRow should be yielded for the zero-sub-row entry",
        );
    }

    #[test]
    fn sheet_and_sheet_rows_are_send_and_sync() {
        const fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<crate::excel::Sheet<'_>>();
        assert_send_sync::<crate::excel::SheetRows<'_>>();
    }

    #[test]
    fn sheet_iter_and_name_iter_is_empty_tracks_remaining_entries() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 1)],
            &[Language::None],
        );
        let page = build_exd_default_bytes(
            &[RowSpec {
                row_id: 0,
                fixed: vec![0u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let exl = exl_bytes_for(&["Z"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Z.exh".to_string(), exh),
            (exd_path("Z", 0, Language::None), page),
        ]);

        let mut iter = sq.sheets().expect("sheets");
        assert!(!iter.is_empty(), "SheetIter with 1 entry must not be empty");
        let _ = iter.next().expect("first entry");
        assert!(
            iter.is_empty(),
            "SheetIter after consuming all entries must be empty"
        );

        let mut names = sq.sheet_names().expect("names");
        assert!(
            !names.is_empty(),
            "SheetNameIter with 1 name must not be empty"
        );
        let _ = names.next().expect("first name");
        assert!(
            names.is_empty(),
            "SheetNameIter after consuming all names must be empty"
        );
    }

    #[test]
    fn owned_row_clone_shares_arc_and_decodes_identically() {
        let columns = [
            ColumnDefinition {
                data_type: ColumnDataType::Int32,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::String,
                offset: 4,
            },
        ];
        let exh = build_exh_bytes(
            8,
            SheetKind::Default,
            &columns,
            &[(1, 1)],
            &[Language::English],
        );
        let mut fixed = vec![0u8; 8];
        fixed[0..4].copy_from_slice(&42i32.to_be_bytes());
        fixed[4..8].copy_from_slice(&0u32.to_be_bytes());
        let mut trailing = b"hello".to_vec();
        trailing.push(0);
        let exd = build_exd_default_bytes(
            &[RowSpec {
                row_id: 1,
                fixed,
                trailing,
            }],
            8,
        );
        let exl = exl_bytes_for(&["Clone"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/Clone.exh".to_string(), exh),
            (exd_path("Clone", 1, Language::English), exd),
        ]);
        let sheet = sq.sheet("Clone").expect("open");
        let original = sheet.row(1, Language::English).expect("ok").expect("row 1");
        let cloned = original.clone();

        assert_eq!(original.row_id(), cloned.row_id());
        assert_eq!(original.sub_row_id(), cloned.sub_row_id());
        assert_eq!(original.column_count(), cloned.column_count());

        assert_eq!(
            original.field(0).unwrap(),
            cloned.field(0).unwrap(),
            "Int32 field must match after clone",
        );
        assert_eq!(
            original.field(1).unwrap(),
            cloned.field(1).unwrap(),
            "String field must match after clone",
        );

        assert_eq!(
            original.bytes_ptr_for_test(),
            cloned.bytes_ptr_for_test(),
            "clone must share the Arc<[u8]> allocation, not copy bytes",
        );
    }

    #[test]
    fn owned_row_fields_iterator_exhausts_columns_in_order() {
        let columns = [
            ColumnDefinition {
                data_type: ColumnDataType::UInt8,
                offset: 0,
            },
            ColumnDefinition {
                data_type: ColumnDataType::Int16,
                offset: 1,
            },
        ];
        let exh = build_exh_bytes(
            3,
            SheetKind::Default,
            &columns,
            &[(0, 1)],
            &[Language::None],
        );
        let mut fixed = vec![0u8; 3];
        fixed[0] = 0xAB;
        fixed[1..3].copy_from_slice(&0x1234_i16.to_be_bytes());
        let exd = build_exd_default_bytes(
            &[RowSpec {
                row_id: 0,
                fixed,
                trailing: Vec::new(),
            }],
            3,
        );
        let exl = exl_bytes_for(&["F"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/F.exh".to_string(), exh),
            (exd_path("F", 0, Language::None), exd),
        ]);
        let sheet = sq.sheet("F").expect("open");
        let row = sheet.row(0, Language::None).expect("ok").expect("row 0");

        let mut fields = row.fields();
        assert_eq!(fields.next().unwrap().unwrap(), Field::UInt8(0xAB));
        assert_eq!(fields.next().unwrap().unwrap(), Field::Int16(0x1234),);
        assert!(
            fields.next().is_none(),
            "must return None after all columns"
        );
        assert!(fields.next().is_none(), "must stay None on repeated poll");
    }

    #[test]
    fn excel_data_language_none_accepted_for_empty_language_list() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(1, SheetKind::Default, &columns, &[(0, 1)], &[]);
        let exd = build_exd_default_bytes(
            &[RowSpec {
                row_id: 0,
                fixed: vec![7u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let exl = exl_bytes_for(&["NL"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/NL.exh".to_string(), exh),
            (exd_path("NL", 0, Language::None), exd),
        ]);

        let header = sq.excel_header("NL").expect("header");
        assert!(
            header.languages().is_empty(),
            "fixture must have no languages"
        );

        let data = sq
            .excel_data("NL", &header, 0, Language::None)
            .expect("excel_data should accept Language::None for an empty language list");
        assert_eq!(data.row_count(), 1);
        let row = data.row(0).expect("row 0");
        assert_eq!(row.field(0).unwrap(), Field::UInt8(7));
    }

    #[test]
    fn excel_data_rejects_language_mismatch_on_none_only_sheet() {
        let columns = [ColumnDefinition {
            data_type: ColumnDataType::UInt8,
            offset: 0,
        }];
        let exh = build_exh_bytes(
            1,
            SheetKind::Default,
            &columns,
            &[(0, 1)],
            &[Language::None],
        );
        let exd = build_exd_default_bytes(
            &[RowSpec {
                row_id: 0,
                fixed: vec![1u8],
                trailing: Vec::new(),
            }],
            1,
        );
        let exl = exl_bytes_for(&["NN"]);
        let sq = install_with_files(&[
            ("exd/root.exl".to_string(), exl),
            ("exd/NN.exh".to_string(), exh),
            (exd_path("NN", 0, Language::None), exd),
        ]);

        let header = sq.excel_header("NN").expect("header");
        let err = sq
            .excel_data("NN", &header, 0, Language::English)
            .unwrap_err();
        assert!(
            matches!(
                err,
                Error::LanguageNotAvailable {
                    requested: Language::English,
                    ..
                }
            ),
            "expected LanguageNotAvailable, got {err:?}",
        );
    }

    #[test]
    fn sqpack_sheet_open_returns_error_when_exh_missing() {
        let exl = exl_bytes_for(&["Ghost"]);
        let sq = install_with_files(&[("exd/root.exl".to_string(), exl)]);
        let err = sq.sheet("Ghost").unwrap_err();
        assert!(
            matches!(err, Error::PathNotFound { .. }),
            "expected PathNotFound, got {err:?}",
        );
    }
}
