use std::io::Cursor;

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

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

/// On-disk size of the TEX file header in bytes.
pub const TEX_HEADER_SIZE: usize = 80;

/// Maximum number of mip levels supported by the on-disk format.
///
/// The [`TexHeader::offset_to_surface`] table has this many slots, so any parsed mip count is
/// clamped to this ceiling.
pub const MAX_MIP_LEVELS: usize = 13;

/// Texture attribute flags extracted from the first `u32` of the TEX header.
///
/// The upper bits encode the texture type (1D, 2D, 3D, cube, 2D array).
/// Use the named `TYPE_*` constants together with [`TexFlags::contains`] to
/// test individual bits, or the convenience predicates such as [`TexFlags::is_cube`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TexFlags(u32);

impl TexFlags {
    /// Bit mask that covers all texture-type bits.
    pub const TYPE_MASK: u32 = 0x13C0_0000;

    /// Texture type bit for 1D textures.
    pub const TYPE_1D: u32 = 0x0040_0000;
    /// Texture type bit for 2D textures.
    pub const TYPE_2D: u32 = 0x0080_0000;
    /// Texture type bit for 3D (volume) textures.
    pub const TYPE_3D: u32 = 0x0100_0000;
    /// Texture type bit for cube-map textures (6 faces per mip).
    pub const TYPE_CUBE: u32 = 0x0200_0000;
    /// Texture type bit for 2D array textures (slice count = [`TexHeader::array_size`]).
    pub const TYPE_2D_ARRAY: u32 = 0x1000_0000;

    /// Wraps a raw `u32` attribute word from the TEX header.
    #[must_use]
    pub fn from_bits(bits: u32) -> Self {
        TexFlags(bits)
    }

    /// Returns the raw `u32` value.
    #[must_use]
    pub fn bits(self) -> u32 {
        self.0
    }

    /// Returns `true` if any bit in `mask` is set.
    #[must_use]
    pub fn contains(self, mask: u32) -> bool {
        self.0 & mask != 0
    }

    /// Returns `true` if the texture-type field equals [`TYPE_1D`](Self::TYPE_1D).
    #[must_use]
    pub fn is_type_1d(self) -> bool {
        self.contains(Self::TYPE_1D)
    }

    /// Returns `true` if the texture-type field equals [`TYPE_2D`](Self::TYPE_2D).
    #[must_use]
    pub fn is_type_2d(self) -> bool {
        self.contains(Self::TYPE_2D)
    }

    /// Returns `true` if the texture-type field equals [`TYPE_3D`](Self::TYPE_3D).
    #[must_use]
    pub fn is_type_3d(self) -> bool {
        self.contains(Self::TYPE_3D)
    }

    /// Returns `true` if the texture-type field equals [`TYPE_CUBE`](Self::TYPE_CUBE).
    #[must_use]
    pub fn is_cube(self) -> bool {
        self.contains(Self::TYPE_CUBE)
    }

    /// Returns `true` if the texture-type field equals [`TYPE_2D_ARRAY`](Self::TYPE_2D_ARRAY).
    #[must_use]
    pub fn is_type_2d_array(self) -> bool {
        self.contains(Self::TYPE_2D_ARRAY)
    }
}

/// Parsed TEX file header.
///
/// All dimension and count fields describe the base (mip 0) surface.  Use the helper methods
/// rather than reading `mip_count_packed` directly — it packs both the mip count and an
/// unknown flag bit.
///
/// Obtained from [`Tex::header`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TexHeader {
    /// Texture attribute flags; encodes the texture type (2D, cube, array, …).
    pub attribute: TexFlags,
    /// Pixel format of the surface data.
    pub format: TextureFormat,
    /// Width of the base (mip 0) surface in texels.
    pub width: u16,
    /// Height of the base (mip 0) surface in texels.
    pub height: u16,
    /// Depth of the base (mip 0) surface in texels (3D textures only).
    pub depth: u16,
    /// Packed mip count — bits 0–6 are the count; bit 7 is an unknown flag.
    ///
    /// Use [`TexHeader::mip_count`] and [`TexHeader::mip_unknown_flag`] instead of
    /// reading this field directly.
    pub mip_count_packed: u8,
    /// Number of array slices (2D-array textures only).
    pub array_size: u8,
    /// Per-LOD start offsets within the mip table.
    pub lod_offset: [u32; 3],
    /// File offsets to the start of each mip level's data.
    pub offset_to_surface: [u32; MAX_MIP_LEVELS],
}

impl TexHeader {
    /// Returns the actual mip count, masking off the unknown flag bit.
    #[must_use]
    pub fn mip_count(self) -> u8 {
        self.mip_count_packed & 0x7F
    }

    /// Returns the value of the unknown high bit in [`mip_count_packed`](Self::mip_count_packed).
    #[must_use]
    pub fn mip_unknown_flag(self) -> bool {
        self.mip_count_packed & 0x80 != 0
    }

    /// Returns the number of independent surfaces per mip level.
    ///
    /// - Cube maps: always 6.
    /// - 2D arrays: equal to [`array_size`](Self::array_size).
    /// - 3D textures: equal to [`depth`](Self::depth) of mip 0.
    /// - All others: 1.
    #[must_use]
    pub fn slices_per_mip(self) -> u32 {
        match self.attribute.bits() & TexFlags::TYPE_MASK {
            TexFlags::TYPE_CUBE => 6,
            TexFlags::TYPE_2D_ARRAY => u32::from(self.array_size),
            TexFlags::TYPE_3D => u32::from(self.depth),
            _ => 1,
        }
    }
}

#[derive(Debug, BinRead)]
#[brw(little)]
struct TexWireHeader {
    attribute: u32,
    format: u32,
    width: u16,
    height: u16,
    depth: u16,
    mip_count_packed: u8,
    array_size: u8,
    lod_offset: [u32; 3],
    offset_to_surface: [u32; MAX_MIP_LEVELS],
}

impl From<TexWireHeader> for TexHeader {
    fn from(wire: TexWireHeader) -> Self {
        TexHeader {
            attribute: TexFlags::from_bits(wire.attribute),
            format: TextureFormat::from_raw(wire.format),
            width: wire.width,
            height: wire.height,
            depth: wire.depth,
            mip_count_packed: wire.mip_count_packed,
            array_size: wire.array_size,
            lod_offset: wire.lod_offset,
            offset_to_surface: wire.offset_to_surface,
        }
    }
}

/// Zero-copy view over a parsed TEX file.
///
/// Holds a reference to the original byte slice; no pixel data is copied until
/// [`Tex::surface_bytes`] or [`Tex::decompress_surface`] is called.
#[derive(Debug, Clone, Copy)]
pub struct Tex<'a> {
    header: TexHeader,
    body: &'a [u8],
}

impl<'a> Tex<'a> {
    /// Parses a TEX file from `bytes`, returning a zero-copy view.
    ///
    /// Emits a `tex.parse` tracing event on success.  Returns
    /// [`Error::InvalidTexLength`] if `bytes` is shorter than [`TEX_HEADER_SIZE`].
    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
        let span = info_span!(span_names::TEX_PARSE, bytes = bytes.len());
        let _enter = span.enter();
        let start = std::time::Instant::now();

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

        let mut cursor = Cursor::new(bytes);
        let wire = TexWireHeader::read(&mut cursor).map_err(|e| Error::decode("tex_header", &e))?;
        let header = TexHeader::from(wire);

        let elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
        info!(
            target: "tex",
            bytes = bytes.len(),
            format = header.format.as_raw(),
            width = header.width,
            height = header.height,
            depth = header.depth,
            mip_count = header.mip_count(),
            array_size = header.array_size,
            slices_per_mip = header.slices_per_mip(),
            elapsed_ms,
            "tex.parse",
        );

        Ok(Self {
            header,
            body: bytes,
        })
    }

    /// Returns a reference to the parsed [`TexHeader`].
    #[must_use]
    pub fn header(&self) -> &TexHeader {
        &self.header
    }

    /// Returns the entire raw byte slice passed to [`Tex::parse`].
    #[must_use]
    pub fn raw(&self) -> &'a [u8] {
        self.body
    }

    /// Returns the width in texels of the given mip level (minimum 1).
    #[must_use]
    pub fn width_of_mip(&self, mip: usize) -> u32 {
        let shift = u32::try_from(mip.min(31)).unwrap_or(31);
        let w = u32::from(self.header.width);
        (w >> shift).max(1)
    }

    /// Returns the height in texels of the given mip level (minimum 1).
    #[must_use]
    pub fn height_of_mip(&self, mip: usize) -> u32 {
        let shift = u32::try_from(mip.min(31)).unwrap_or(31);
        let h = u32::from(self.header.height);
        (h >> shift).max(1)
    }

    /// Returns the slice count for the given mip level.
    ///
    /// For cube maps and 2D arrays the slice count is constant across all mips.
    /// For 3D textures the depth halves per mip (floored to 1).
    #[must_use]
    pub fn depth_of_mip(&self, mip: usize) -> u32 {
        if self.header.attribute.is_cube() || self.header.attribute.is_type_2d_array() {
            return self.header.slices_per_mip();
        }
        let shift = u32::try_from(mip.min(31)).unwrap_or(31);
        let d = u32::from(self.header.depth);
        (d >> shift).max(1)
    }

    /// Returns the effective mip count, clamped to `[1, MAX_MIP_LEVELS]`.
    #[must_use]
    pub fn mip_count(&self) -> usize {
        let count = usize::from(self.header.mip_count()).max(1);
        count.min(MAX_MIP_LEVELS)
    }

    /// Returns the number of slices per mip level; delegates to [`TexHeader::slices_per_mip`].
    #[must_use]
    pub fn slices_per_mip(&self) -> u32 {
        self.header.slices_per_mip()
    }

    /// Returns the raw (possibly compressed) bytes for a single surface.
    ///
    /// `mip` must be less than [`Tex::mip_count`]; `slice` must be less than
    /// [`Tex::depth_of_mip`].  Returns [`Error::MipOutOfRange`] or
    /// [`Error::SliceOutOfRange`] if the indices are out of bounds.
    pub fn surface_bytes(&self, mip: usize, slice: u32) -> Result<&'a [u8]> {
        if mip >= self.mip_count() {
            return Err(Error::MipOutOfRange {
                mip,
                mip_count: self.mip_count(),
            });
        }
        let slice_count = self.depth_of_mip(mip);
        if slice >= slice_count {
            return Err(Error::SliceOutOfRange { slice, slice_count });
        }

        let width = self.width_of_mip(mip);
        let height = self.height_of_mip(mip);
        let surface_size = self.header.format.surface_byte_size(width, height)?;

        let mip_base = u64::from(self.header.offset_to_surface[mip]);
        let slice_size = surface_size;
        let file_size = u64::try_from(self.body.len()).unwrap_or(u64::MAX);
        let slice_byte_offset =
            u64::from(slice)
                .checked_mul(slice_size)
                .ok_or(Error::TexSurfaceOverflow {
                    reason: "slice * surface_size overflowed u64",
                })?;
        let slice_offset =
            mip_base
                .checked_add(slice_byte_offset)
                .ok_or(Error::TexSurfaceOverflow {
                    reason: "mip_base + slice * surface_size overflowed u64",
                })?;
        let slice_end = slice_offset
            .checked_add(slice_size)
            .ok_or(Error::TexSurfaceOverflow {
                reason: "slice_offset + surface_size overflowed u64",
            })?;

        if slice_end > file_size {
            return Err(Error::TexSurfaceOutOfBounds {
                offset: slice_offset,
                size: slice_size,
                file_size,
            });
        }

        let start = usize::try_from(slice_offset).map_err(|_| Error::TexSurfaceOverflow {
            reason: "slice_offset does not fit usize",
        })?;
        let end = usize::try_from(slice_end).map_err(|_| Error::TexSurfaceOverflow {
            reason: "slice_end does not fit usize",
        })?;

        Ok(&self.body[start..end])
    }

    /// Decompresses a single surface to RGBA8, delegating to [`decompress_to_rgba8`].
    ///
    /// [`decompress_to_rgba8`]: crate::tex::bcn::decompress_to_rgba8
    pub fn decompress_surface(&self, mip: usize, slice: u32) -> Result<Vec<u8>> {
        let src = self.surface_bytes(mip, slice)?;
        crate::tex::bcn::decompress_to_rgba8(
            self.header.format,
            self.width_of_mip(mip),
            self.height_of_mip(mip),
            src,
        )
    }

    /// Returns an iterator that walks every surface in mip-major, slice-minor order.
    ///
    /// The iterator terminates on the first error; call [`Tex::surfaces`] to collect all
    /// surfaces into a `Vec` and propagate any error.
    #[must_use]
    pub fn iter_surfaces(&self) -> SurfaceIter<'_, 'a> {
        SurfaceIter {
            tex: self,
            mip: 0,
            slice: 0,
            done: false,
        }
    }

    /// Collects all surfaces into a `Vec`, returning the first error encountered.
    pub fn surfaces(&self) -> Result<Vec<SurfaceInfo<'a>>> {
        self.iter_surfaces().collect()
    }
}

/// Metadata and raw bytes for a single texture surface (one mip/slice combination).
///
/// Produced by [`Tex::surfaces`] and [`Tex::iter_surfaces`].
#[derive(Debug, Clone, Copy)]
pub struct SurfaceInfo<'a> {
    /// Zero-based mip level index.
    pub mip: usize,
    /// Zero-based slice index within the mip.
    pub slice: u32,
    /// Width of this surface in texels.
    pub width: u32,
    /// Height of this surface in texels.
    pub height: u32,
    /// Slice count for this mip level (see [`Tex::depth_of_mip`]).
    pub depth: u32,
    /// Raw (possibly block-compressed) bytes for this surface.
    pub bytes: &'a [u8],
}

/// Iterator over all surfaces in a [`Tex`], in mip-major, slice-minor order.
///
/// Obtained from [`Tex::iter_surfaces`].  Stops permanently after the first error.
#[derive(Debug)]
pub struct SurfaceIter<'tex, 'a> {
    tex: &'tex Tex<'a>,
    mip: usize,
    slice: u32,
    done: bool,
}

impl<'a> Iterator for SurfaceIter<'_, 'a> {
    type Item = Result<SurfaceInfo<'a>>;

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

        let mip = self.mip;
        let slice = self.slice;
        let depth = self.tex.depth_of_mip(mip);
        debug_assert!(depth >= 1);

        match self.tex.surface_bytes(mip, slice) {
            Ok(bytes) => {
                let width = self.tex.width_of_mip(mip);
                let height = self.tex.height_of_mip(mip);
                self.slice += 1;
                if self.slice >= depth {
                    self.mip += 1;
                    self.slice = 0;
                }
                Some(Ok(SurfaceInfo {
                    mip,
                    slice,
                    width,
                    height,
                    depth,
                    bytes,
                }))
            }
            Err(err) => {
                self.done = true;
                Some(Err(err))
            }
        }
    }
}

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

    #[allow(clippy::cast_possible_truncation)]
    const HEADER_SIZE_U32: u32 = TEX_HEADER_SIZE as u32;

    struct Fixture<'a> {
        attribute: u32,
        format: u32,
        width: u16,
        height: u16,
        depth: u16,
        mip_count: u8,
        array_size: u8,
        offset_to_surface: [u32; MAX_MIP_LEVELS],
        body_bytes: &'a [u8],
    }

    fn build_tex(f: &Fixture<'_>) -> Vec<u8> {
        let mut buf = Vec::with_capacity(TEX_HEADER_SIZE + f.body_bytes.len());
        buf.extend_from_slice(&f.attribute.to_le_bytes());
        buf.extend_from_slice(&f.format.to_le_bytes());
        buf.extend_from_slice(&f.width.to_le_bytes());
        buf.extend_from_slice(&f.height.to_le_bytes());
        buf.extend_from_slice(&f.depth.to_le_bytes());
        buf.push(f.mip_count);
        buf.push(f.array_size);
        for lod in [0u32; 3] {
            buf.extend_from_slice(&lod.to_le_bytes());
        }
        for off in f.offset_to_surface {
            buf.extend_from_slice(&off.to_le_bytes());
        }
        assert_eq!(buf.len(), TEX_HEADER_SIZE);
        buf.extend_from_slice(f.body_bytes);
        buf
    }

    #[test]
    fn parses_minimal_2d_bc1() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0xABu8; 32];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x3420,
            width: 8,
            height: 8,
            depth: 1,
            mip_count: 1,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });

        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.header().format, TextureFormat::BC1);
        assert_eq!(tex.header().width, 8);
        assert_eq!(tex.header().height, 8);
        assert_eq!(tex.mip_count(), 1);
        assert_eq!(tex.slices_per_mip(), 1);
        let surf = tex.surface_bytes(0, 0).expect("surface");
        assert_eq!(surf.len(), 32);
        assert!(surf.iter().all(|&b| b == 0xAB));
    }

    #[test]
    fn surface_size_uses_per_mip_dimensions() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        let base = HEADER_SIZE_U32;
        offsets[0] = base;
        offsets[1] = base + 128;
        offsets[2] = base + 128 + 32;
        offsets[3] = base + 128 + 32 + 8;
        let mut body = Vec::new();
        body.extend(std::iter::repeat_n(0x10u8, 128));
        body.extend(std::iter::repeat_n(0x20u8, 32));
        body.extend(std::iter::repeat_n(0x30u8, 8));
        body.extend(std::iter::repeat_n(0x40u8, 8));
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x3420,
            width: 16,
            height: 16,
            depth: 1,
            mip_count: 4,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });

        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.width_of_mip(0), 16);
        assert_eq!(tex.width_of_mip(1), 8);
        assert_eq!(tex.width_of_mip(2), 4);
        assert_eq!(tex.width_of_mip(3), 2);
        let m3 = tex.surface_bytes(3, 0).expect("surface");
        assert_eq!(m3.len(), 8);
        assert!(m3.iter().all(|&b| b == 0x40));
    }

    #[test]
    fn iter_surfaces_walks_mip_major_slice_minor() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let mut body = Vec::with_capacity(64);
        for slice in 0..4u8 {
            body.extend(std::iter::repeat_n(slice, 16));
        }
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D_ARRAY,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 1,
            array_size: 4,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.slices_per_mip(), 4);
        let surfaces = tex.surfaces().expect("surfaces");
        assert_eq!(surfaces.len(), 4);
        for (i, surf) in surfaces.iter().enumerate() {
            assert_eq!(surf.mip, 0, "mip {i}");
            assert_eq!(surf.slice, u32::try_from(i).unwrap());
            assert_eq!(surf.bytes.len(), 16);
            assert!(surf.bytes.iter().all(|&b| b == u8::try_from(i).unwrap()));
        }
    }

    #[test]
    fn rejects_input_shorter_than_header() {
        let bytes = vec![0u8; 32];
        let err = Tex::parse(&bytes).unwrap_err();
        assert!(matches!(
            err,
            Error::InvalidTexLength {
                found: 32,
                minimum: TEX_HEADER_SIZE,
            }
        ));
    }

    #[test]
    fn rejects_surface_offset_past_end_of_file() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = 1000;
        let body = vec![0u8; 16];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 1,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        let err = tex.surface_bytes(0, 0).unwrap_err();
        assert!(matches!(err, Error::TexSurfaceOutOfBounds { .. }));
    }

    #[test]
    fn surface_bytes_rejects_mip_out_of_range() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0u8; 16];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 1,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        let err = tex.surface_bytes(5, 0).unwrap_err();
        assert!(matches!(
            err,
            Error::MipOutOfRange {
                mip: 5,
                mip_count: 1
            }
        ));
    }

    #[test]
    fn surface_bytes_rejects_slice_out_of_range() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0u8; 32];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D_ARRAY,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 1,
            array_size: 2,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        let err = tex.surface_bytes(0, 5).unwrap_err();
        assert!(matches!(
            err,
            Error::SliceOutOfRange {
                slice: 5,
                slice_count: 2
            }
        ));
    }

    #[test]
    fn surface_bytes_rejects_unknown_format() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0u8; 16];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0xDEAD_BEEF,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 1,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        let err = tex.surface_bytes(0, 0).unwrap_err();
        assert!(matches!(
            err,
            Error::UnsupportedTextureFormat {
                format: 0xDEAD_BEEF
            }
        ));
    }

    #[test]
    fn tex_flags_type_bits_recognised() {
        let f = TexFlags::from_bits(TexFlags::TYPE_2D | TexFlags::TYPE_CUBE);
        assert!(f.is_type_2d());
        assert!(f.is_cube());
        assert!(!f.is_type_3d());
        assert!(!f.is_type_2d_array());
        assert_eq!(f.bits(), TexFlags::TYPE_2D | TexFlags::TYPE_CUBE);
    }

    #[test]
    fn mip_count_packed_split_works() {
        let h = TexHeader {
            attribute: TexFlags::from_bits(0),
            format: TextureFormat::BC1,
            width: 4,
            height: 4,
            depth: 1,
            mip_count_packed: 0x85,
            array_size: 0,
            lod_offset: [0; 3],
            offset_to_surface: [0; MAX_MIP_LEVELS],
        };
        assert_eq!(h.mip_count(), 5);
        assert!(h.mip_unknown_flag());
    }

    #[test]
    fn mip_count_clamped_to_table_capacity() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0u8; 32];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x3420,
            width: 8,
            height: 8,
            depth: 1,
            mip_count: 50,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.mip_count(), MAX_MIP_LEVELS);
        let err = tex.surface_bytes(MAX_MIP_LEVELS, 0).unwrap_err();
        assert!(matches!(
            err,
            Error::MipOutOfRange {
                mip: 13,
                mip_count: 13
            }
        ));
        let err = tex.surface_bytes(20, 0).unwrap_err();
        assert!(matches!(err, Error::MipOutOfRange { mip: 20, .. }));
    }

    #[test]
    fn mip_count_zero_treated_as_one() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0xCDu8; 16];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 0,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.mip_count(), 1);
        let surf = tex.surface_bytes(0, 0).expect("surface 0,0 ok");
        assert_eq!(surf.len(), 16);
        let err = tex.surface_bytes(1, 0).unwrap_err();
        assert!(matches!(
            err,
            Error::MipOutOfRange {
                mip: 1,
                mip_count: 1
            }
        ));
    }

    #[test]
    fn slices_per_mip_cube_returns_six() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let mut body = Vec::with_capacity(96);
        for face in 0..6u8 {
            body.extend(std::iter::repeat_n(face, 16));
        }
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_CUBE,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 1,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.slices_per_mip(), 6);
        for face in 0..6u32 {
            let surf = tex.surface_bytes(0, face).expect("face surface");
            assert_eq!(surf.len(), 16);
            let expected = u8::try_from(face).unwrap();
            assert!(surf.iter().all(|&b| b == expected), "face {face}");
        }
    }

    #[test]
    fn slices_per_mip_3d_returns_depth() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let mut body = Vec::with_capacity(48);
        for z in 0..3u8 {
            body.extend(std::iter::repeat_n(z + 0xA0, 16));
        }
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_3D,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 3,
            mip_count: 1,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.slices_per_mip(), 3);
        assert_eq!(tex.depth_of_mip(0), 3);
        for z in 0..3u32 {
            let surf = tex.surface_bytes(0, z).expect("z slice");
            assert_eq!(surf.len(), 16);
            let expected = u8::try_from(z).unwrap() + 0xA0;
            assert!(surf.iter().all(|&b| b == expected), "z = {z}");
        }
    }

    #[test]
    fn slices_per_mip_multiple_type_bits_defaults_to_one() {
        let h = TexHeader {
            attribute: TexFlags::from_bits(TexFlags::TYPE_2D | TexFlags::TYPE_CUBE),
            format: TextureFormat::L8,
            width: 4,
            height: 4,
            depth: 1,
            mip_count_packed: 1,
            array_size: 0,
            lod_offset: [0; 3],
            offset_to_surface: [0; MAX_MIP_LEVELS],
        };
        assert_eq!(h.slices_per_mip(), 1);
    }

    #[test]
    fn multi_mip_bc4_surface_decode() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        let base = HEADER_SIZE_U32;
        offsets[0] = base;
        offsets[1] = base + 32;
        offsets[2] = base + 32 + 8;
        let mut body = Vec::new();
        body.extend(std::iter::repeat_n(0x11u8, 32));
        body.extend(std::iter::repeat_n(0x22u8, 8));
        body.extend(std::iter::repeat_n(0x33u8, 8));
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x6120,
            width: 8,
            height: 8,
            depth: 1,
            mip_count: 3,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        let m0 = tex.surface_bytes(0, 0).expect("m0");
        assert_eq!(m0.len(), 32);
        assert!(m0.iter().all(|&b| b == 0x11));
        let m1 = tex.surface_bytes(1, 0).expect("m1");
        assert_eq!(m1.len(), 8);
        assert!(m1.iter().all(|&b| b == 0x22));
        let m2 = tex.surface_bytes(2, 0).expect("m2");
        assert_eq!(m2.len(), 8);
        assert!(m2.iter().all(|&b| b == 0x33));
    }

    #[test]
    fn width_of_mip_and_height_of_mip_clamp_to_one() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0u8; 8];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x3420,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 3,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.width_of_mip(0), 4);
        assert_eq!(tex.width_of_mip(1), 2);
        assert_eq!(tex.width_of_mip(2), 1);
        assert_eq!(tex.width_of_mip(3), 1, "past bit-width floor must be 1");
        assert_eq!(tex.width_of_mip(31), 1, "large mip clamped to 1");
        assert_eq!(tex.height_of_mip(2), 1);
        assert_eq!(tex.height_of_mip(31), 1);
    }

    #[test]
    fn depth_of_mip_scales_for_3d_texture() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0u8; 16];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_3D,
            format: 0x1130,
            width: 2,
            height: 2,
            depth: 4,
            mip_count: 1,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.depth_of_mip(0), 4, "mip 0 depth");
        assert_eq!(tex.depth_of_mip(1), 2, "mip 1 depth halved");
        assert_eq!(tex.depth_of_mip(2), 1, "mip 2 depth clamped to 1");
        assert_eq!(tex.depth_of_mip(31), 1, "large mip depth stays 1");
    }

    #[test]
    fn depth_of_mip_constant_for_cube_and_array() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        let body = vec![0u8; 96];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_CUBE,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 1,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(tex.depth_of_mip(0), 6);
        assert_eq!(tex.depth_of_mip(5), 6, "cube depth constant across mips");
    }

    #[test]
    fn iter_surfaces_stops_on_error_and_yields_one_err() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        offsets[1] = 9999;
        let body = vec![0u8; 16];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 2,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        let mut it = tex.iter_surfaces();
        let first = it.next().expect("must yield first item");
        assert!(first.is_ok(), "mip 0 surface must succeed");
        let second = it.next().expect("must yield second item");
        assert!(
            matches!(second, Err(Error::TexSurfaceOutOfBounds { .. })),
            "mip 1 surface must error"
        );
        assert!(it.next().is_none(), "iterator must terminate after error");
    }

    #[test]
    fn surface_bytes_mip_count_packed_high_bit_does_not_corrupt_count() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        offsets[1] = HEADER_SIZE_U32 + 8;
        let body = vec![0u8; 16];
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x3420,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 0x82,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        assert_eq!(
            tex.mip_count(),
            2,
            "high bit must not inflate the mip count"
        );
        assert!(tex.header().mip_unknown_flag(), "flag bit must be set");
        let m1 = tex.surface_bytes(1, 0).expect("mip 1 ok");
        assert_eq!(m1.len(), 8);
        let err = tex.surface_bytes(2, 0).unwrap_err();
        assert!(matches!(err, Error::MipOutOfRange { mip: 2, .. }));
    }

    #[test]
    fn surfaces_monotonically_non_increasing_dimensions_across_mips() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        let base = HEADER_SIZE_U32;
        offsets[0] = base;
        offsets[1] = base + 64;
        offsets[2] = base + 64 + 16;
        offsets[3] = base + 64 + 16 + 8;
        let mut body = Vec::new();
        body.extend(std::iter::repeat_n(0xAAu8, 64));
        body.extend(std::iter::repeat_n(0xBBu8, 16));
        body.extend(std::iter::repeat_n(0xCCu8, 8));
        body.extend(std::iter::repeat_n(0xDDu8, 8));
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_2D,
            format: 0x3420,
            width: 16,
            height: 8,
            depth: 1,
            mip_count: 4,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        let surfaces = tex.surfaces().expect("surfaces");
        assert_eq!(surfaces.len(), 4);
        for pair in surfaces.windows(2) {
            let (a, b) = (&pair[0], &pair[1]);
            assert!(
                b.width <= a.width,
                "mip {} width {} > mip {} width {}",
                b.mip,
                b.width,
                a.mip,
                a.width
            );
            assert!(
                b.height <= a.height,
                "mip {} height {} > mip {} height {}",
                b.mip,
                b.height,
                a.mip,
                a.height
            );
        }
    }

    #[test]
    fn iter_surfaces_yields_mip_count_times_slices_surfaces_for_cube() {
        let mut offsets = [0u32; MAX_MIP_LEVELS];
        offsets[0] = HEADER_SIZE_U32;
        offsets[1] = HEADER_SIZE_U32 + 96;
        let mut body = Vec::new();
        body.extend(std::iter::repeat_n(0u8, 96));
        body.extend(std::iter::repeat_n(0u8, 24));
        let bytes = build_tex(&Fixture {
            attribute: TexFlags::TYPE_CUBE,
            format: 0x1130,
            width: 4,
            height: 4,
            depth: 1,
            mip_count: 2,
            array_size: 0,
            offset_to_surface: offsets,
            body_bytes: &body,
        });
        let tex = Tex::parse(&bytes).expect("parse");
        let surfaces = tex.surfaces().expect("surfaces");
        assert_eq!(surfaces.len(), 2 * 6, "2 mips * 6 cube faces = 12 surfaces");
        for s in surfaces.iter().take(6) {
            assert_eq!(s.mip, 0, "first six must be mip 0");
        }
        for s in surfaces.iter().skip(6) {
            assert_eq!(s.mip, 1, "last six must be mip 1");
        }
    }
}
