//! Software `BCn` block-compression decoders — BC1 through BC5.
//!
//! The single public entry point is [`decompress_to_rgba8`], which dispatches to the appropriate
//! per-format decoder and writes interleaved RGBA8 output.
//!
//! BC7 is intentionally not implemented; it returns [`Error::FormatNotDecompressibleToRgba`].
//! Linear formats (`B8G8R8A8`, `L8`, etc.) also return that error — they do not require
//! decompression and should be used as-is from [`crate::tex::header::Tex::surface_bytes`].

use tracing::{info, info_span};

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

/// Decompresses a single BCn-compressed surface to interleaved RGBA8.
///
/// `src` must be exactly [`TextureFormat::surface_byte_size`]`(width, height)` bytes long.
///
/// Supported formats: BC1, BC2, BC3, BC4, BC5.  BC7 and all linear formats return
/// [`Error::FormatNotDecompressibleToRgba`].
///
/// Emits a `tex.decompress` tracing event on success.
pub fn decompress_to_rgba8(
    format: TextureFormat,
    width: u32,
    height: u32,
    src: &[u8],
) -> Result<Vec<u8>> {
    let span = info_span!(
        span_names::TEX_DECOMPRESS,
        format = format.as_raw(),
        width,
        height,
        bytes = src.len(),
    );
    let _enter = span.enter();
    let start = std::time::Instant::now();

    let expected = format.surface_byte_size(width, height).map_err(|_| {
        Error::FormatNotDecompressibleToRgba {
            format: format.as_raw(),
        }
    })?;
    let src_len_u64 = u64::try_from(src.len()).unwrap_or(u64::MAX);
    if src_len_u64 != expected {
        return Err(Error::TexSurfaceOutOfBounds {
            offset: 0,
            size: expected,
            file_size: src_len_u64,
        });
    }

    let out_len = (width as usize)
        .checked_mul(height as usize)
        .and_then(|v| v.checked_mul(4))
        .ok_or(Error::TexSurfaceOverflow {
            reason: "decompress_to_rgba8 output size overflows usize",
        })?;
    let mut out = vec![0u8; out_len];

    let block_w = width.div_ceil(4).max(1);
    let block_h = height.div_ceil(4).max(1);

    let bytes_per_block = match format {
        TextureFormat::BC1 | TextureFormat::BC4 => 8usize,
        TextureFormat::BC2 | TextureFormat::BC3 | TextureFormat::BC5 => 16,
        TextureFormat::BC7
        | TextureFormat::Unknown(_)
        | TextureFormat::L8
        | TextureFormat::A8
        | TextureFormat::B4G4R4A4
        | TextureFormat::B5G5R5A1
        | TextureFormat::B8G8R8A8
        | TextureFormat::B8G8R8X8
        | TextureFormat::R16G16B16A16F
        | TextureFormat::R32G32B32A32F => {
            return Err(Error::FormatNotDecompressibleToRgba {
                format: format.as_raw(),
            });
        }
    };

    let mut block_pixels = [0u8; 64];
    for by in 0..block_h {
        for bx in 0..block_w {
            let block_idx = (by * block_w + bx) as usize;
            let start_off = block_idx * bytes_per_block;
            let block = &src[start_off..start_off + bytes_per_block];
            match format {
                TextureFormat::BC1 => decode_bc1_block(block, &mut block_pixels),
                TextureFormat::BC2 => decode_bc2_block(block, &mut block_pixels),
                TextureFormat::BC3 => decode_bc3_block(block, &mut block_pixels),
                TextureFormat::BC4 => decode_bc4_block(block, &mut block_pixels),
                TextureFormat::BC5 => decode_bc5_block(block, &mut block_pixels),
                _ => unreachable!("filtered above"),
            }
            write_block_into(width, height, bx, by, &block_pixels, &mut out);
        }
    }

    let elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
    info!(
        target: "tex",
        format = format.as_raw(),
        width,
        height,
        bytes_in = src.len(),
        bytes_out = out.len(),
        elapsed_ms,
        "tex.decompress",
    );

    Ok(out)
}

fn write_block_into(
    width: u32,
    height: u32,
    bx: u32,
    by: u32,
    block_pixels: &[u8; 64],
    out: &mut [u8],
) {
    let width = width as usize;
    let bx = bx as usize;
    let by = by as usize;
    let h = height as usize;
    for row in 0..4usize {
        let y = by * 4 + row;
        if y >= h {
            break;
        }
        for col in 0..4usize {
            let x = bx * 4 + col;
            if x >= width {
                break;
            }
            let src = (row * 4 + col) * 4;
            let dst = (y * width + x) * 4;
            out[dst] = block_pixels[src];
            out[dst + 1] = block_pixels[src + 1];
            out[dst + 2] = block_pixels[src + 2];
            out[dst + 3] = block_pixels[src + 3];
        }
    }
}

fn rgb565_to_rgb888(c: u16) -> (u8, u8, u8) {
    let r5 = ((c >> 11) & 0x1F) as u8;
    let g6 = ((c >> 5) & 0x3F) as u8;
    let b5 = (c & 0x1F) as u8;
    let r = (r5 << 3) | (r5 >> 2);
    let g = (g6 << 2) | (g6 >> 4);
    let b = (b5 << 3) | (b5 >> 2);
    (r, g, b)
}

fn build_color_palette(color0: u16, color1: u16, with_punchthrough_alpha: bool) -> [[u8; 4]; 4] {
    let (r0, g0, b0) = rgb565_to_rgb888(color0);
    let (r1, g1, b1) = rgb565_to_rgb888(color1);
    let mut pal = [[0u8; 4]; 4];
    pal[0] = [r0, g0, b0, 255];
    pal[1] = [r1, g1, b1, 255];
    if color0 > color1 || !with_punchthrough_alpha {
        pal[2] = [blend_2_1(r0, r1), blend_2_1(g0, g1), blend_2_1(b0, b1), 255];
        pal[3] = [blend_2_1(r1, r0), blend_2_1(g1, g0), blend_2_1(b1, b0), 255];
    } else {
        pal[2] = [blend_avg(r0, r1), blend_avg(g0, g1), blend_avg(b0, b1), 255];
        pal[3] = [0, 0, 0, 0];
    }
    pal
}

#[inline]
fn blend_2_1(a: u8, b: u8) -> u8 {
    let v = (u32::from(a) * 2 + u32::from(b)) / 3;
    u8::try_from(v).unwrap_or(u8::MAX)
}

#[inline]
fn blend_avg(a: u8, b: u8) -> u8 {
    let v = u32::midpoint(u32::from(a), u32::from(b));
    u8::try_from(v).unwrap_or(u8::MAX)
}

fn decode_bc1_block(block: &[u8], out: &mut [u8; 64]) {
    debug_assert_eq!(block.len(), 8);
    let color0 = u16::from_le_bytes([block[0], block[1]]);
    let color1 = u16::from_le_bytes([block[2], block[3]]);
    let pal = build_color_palette(color0, color1, true);
    let indices = u32::from_le_bytes([block[4], block[5], block[6], block[7]]);
    for i in 0..16usize {
        let bits = ((indices >> (i * 2)) & 0b11) as usize;
        let p = pal[bits];
        out[i * 4] = p[0];
        out[i * 4 + 1] = p[1];
        out[i * 4 + 2] = p[2];
        out[i * 4 + 3] = p[3];
    }
}

fn decode_bc2_block(block: &[u8], out: &mut [u8; 64]) {
    debug_assert_eq!(block.len(), 16);
    let alpha = &block[0..8];
    let color0 = u16::from_le_bytes([block[8], block[9]]);
    let color1 = u16::from_le_bytes([block[10], block[11]]);
    let pal = build_color_palette(color0, color1, false);
    let indices = u32::from_le_bytes([block[12], block[13], block[14], block[15]]);
    for i in 0..16usize {
        let bits = ((indices >> (i * 2)) & 0b11) as usize;
        let p = pal[bits];
        out[i * 4] = p[0];
        out[i * 4 + 1] = p[1];
        out[i * 4 + 2] = p[2];
        let nibble = (alpha[i / 2] >> ((i & 1) * 4)) & 0x0F;
        out[i * 4 + 3] = (nibble << 4) | nibble;
    }
}

fn decode_bc3_block(block: &[u8], out: &mut [u8; 64]) {
    debug_assert_eq!(block.len(), 16);
    let alpha = decode_bc4_palette_and_indices(&block[0..8]);
    let color0 = u16::from_le_bytes([block[8], block[9]]);
    let color1 = u16::from_le_bytes([block[10], block[11]]);
    let pal = build_color_palette(color0, color1, false);
    let indices = u32::from_le_bytes([block[12], block[13], block[14], block[15]]);
    for i in 0..16usize {
        let bits = ((indices >> (i * 2)) & 0b11) as usize;
        let p = pal[bits];
        out[i * 4] = p[0];
        out[i * 4 + 1] = p[1];
        out[i * 4 + 2] = p[2];
        out[i * 4 + 3] = alpha[i];
    }
}

fn decode_bc4_block(block: &[u8], out: &mut [u8; 64]) {
    debug_assert_eq!(block.len(), 8);
    let values = decode_bc4_palette_and_indices(block);
    for i in 0..16usize {
        let v = values[i];
        out[i * 4] = v;
        out[i * 4 + 1] = v;
        out[i * 4 + 2] = v;
        out[i * 4 + 3] = 255;
    }
}

fn decode_bc4_palette_and_indices(block: &[u8]) -> [u8; 16] {
    debug_assert_eq!(block.len(), 8);
    let a0 = block[0];
    let a1 = block[1];
    let mut palette = [0u8; 8];
    palette[0] = a0;
    palette[1] = a1;
    if a0 > a1 {
        for i in 0..6u32 {
            let n = i + 1;
            let v = (u32::from(a0) * (7 - n) + u32::from(a1) * n) / 7;
            palette[i as usize + 2] = u8::try_from(v).unwrap_or(u8::MAX);
        }
    } else {
        for i in 0..4u32 {
            let n = i + 1;
            let v = (u32::from(a0) * (5 - n) + u32::from(a1) * n) / 5;
            palette[i as usize + 2] = u8::try_from(v).unwrap_or(u8::MAX);
        }
        palette[6] = 0;
        palette[7] = 255;
    }

    let mut bits: u64 = 0;
    for (i, &b) in block[2..8].iter().enumerate() {
        bits |= u64::from(b) << (i * 8);
    }
    let mut out = [0u8; 16];
    for (i, slot) in out.iter_mut().enumerate() {
        let idx = ((bits >> (i * 3)) & 0b111) as usize;
        *slot = palette[idx];
    }
    out
}

fn decode_bc5_block(block: &[u8], out: &mut [u8; 64]) {
    debug_assert_eq!(block.len(), 16);
    let r = decode_bc4_palette_and_indices(&block[0..8]);
    let g = decode_bc4_palette_and_indices(&block[8..16]);
    for i in 0..16usize {
        out[i * 4] = r[i];
        out[i * 4 + 1] = g[i];
        out[i * 4 + 2] = 0;
        out[i * 4 + 3] = 255;
    }
}

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

    #[test]
    fn rgb565_endpoints_round_trip() {
        assert_eq!(rgb565_to_rgb888(0xFFFF), (255, 255, 255));
        assert_eq!(rgb565_to_rgb888(0x0000), (0, 0, 0));
        assert_eq!(rgb565_to_rgb888(0xF800), (255, 0, 0));
        assert_eq!(rgb565_to_rgb888(0x07E0), (0, 255, 0));
        assert_eq!(rgb565_to_rgb888(0x001F), (0, 0, 255));
    }

    #[test]
    fn bc1_solid_red_block_decodes_to_red() {
        let block: [u8; 8] = [0x00, 0xF8, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC1, 4, 4, &block).expect("decompress");
        assert_eq!(rgba.len(), 4 * 4 * 4);
        for i in 0..16 {
            assert_eq!(rgba[i * 4], 255, "pixel {i} R");
            assert_eq!(rgba[i * 4 + 1], 0, "pixel {i} G");
            assert_eq!(rgba[i * 4 + 2], 0, "pixel {i} B");
            assert_eq!(rgba[i * 4 + 3], 255, "pixel {i} A");
        }
    }

    #[test]
    fn bc1_punchthrough_alpha_third_index_is_transparent() {
        let block: [u8; 8] = [0x00, 0xF8, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF];
        let rgba = decompress_to_rgba8(TextureFormat::BC1, 4, 4, &block).expect("decompress");
        for i in 0..16 {
            assert_eq!(rgba[i * 4 + 3], 0, "pixel {i} alpha should be 0");
        }
    }

    #[test]
    fn bc3_alpha_endpoints_dominate_when_indices_zero() {
        let block: [u8; 16] = [
            0x80, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
            0x00, 0x00,
        ];
        let rgba = decompress_to_rgba8(TextureFormat::BC3, 4, 4, &block).expect("decompress");
        for i in 0..16 {
            assert_eq!(rgba[i * 4], 255, "pixel {i} R");
            assert_eq!(rgba[i * 4 + 1], 255, "pixel {i} G");
            assert_eq!(rgba[i * 4 + 2], 255, "pixel {i} B");
            assert_eq!(rgba[i * 4 + 3], 0x80, "pixel {i} A");
        }
    }

    #[test]
    fn bc5_red_green_block_decodes_per_channel() {
        let block: [u8; 16] = [
            0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00,
        ];
        let rgba = decompress_to_rgba8(TextureFormat::BC5, 4, 4, &block).expect("decompress");
        for i in 0..16 {
            assert_eq!(rgba[i * 4], 0xC0, "pixel {i} R");
            assert_eq!(rgba[i * 4 + 1], 0x40, "pixel {i} G");
            assert_eq!(rgba[i * 4 + 2], 0, "pixel {i} B");
            assert_eq!(rgba[i * 4 + 3], 255, "pixel {i} A");
        }
    }

    #[test]
    fn bc4_interpolated_values_use_seven_step_palette() {
        let indices_bits: u32 = 0b111_110_101_100_011_010_001_000;
        let b0 = (indices_bits & 0xFF) as u8;
        let b1 = ((indices_bits >> 8) & 0xFF) as u8;
        let b2 = ((indices_bits >> 16) & 0xFF) as u8;
        let block: [u8; 8] = [0x00, 0x70, b0, b1, b2, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC4, 4, 2, &block).expect("decompress");
        let r0 = rgba[0];
        let r1 = rgba[4];
        let r6 = rgba[24];
        let r7 = rgba[28];
        assert_eq!(r0, 0x00, "index 0 → a0");
        assert_eq!(r1, 0x70, "index 1 → a1");
        assert_eq!(r6, 0x00, "index 6 → sentinel 0 (a0 < a1 mode)");
        assert_eq!(r7, 0xFF, "index 7 → sentinel 255 (a0 < a1 mode)");
    }

    #[test]
    fn bc4_block_decodes_to_red_channel() {
        let block: [u8; 8] = [0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC4, 4, 4, &block).expect("decompress");
        for i in 0..16 {
            assert_eq!(rgba[i * 4], 0xC0, "pixel {i} R");
            assert_eq!(rgba[i * 4 + 1], 0xC0, "pixel {i} G");
            assert_eq!(rgba[i * 4 + 2], 0xC0, "pixel {i} B");
            assert_eq!(rgba[i * 4 + 3], 255, "pixel {i} A");
        }
    }

    #[test]
    fn bc2_alpha_nibbles_unpack_correctly() {
        let block: [u8; 16] = [
            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00,
        ];
        let rgba = decompress_to_rgba8(TextureFormat::BC2, 4, 4, &block).expect("decompress");
        for i in 0..16 {
            assert_eq!(rgba[i * 4], 255, "pixel {i} R");
            assert_eq!(rgba[i * 4 + 1], 0, "pixel {i} G");
            assert_eq!(rgba[i * 4 + 2], 0, "pixel {i} B");
            assert_eq!(rgba[i * 4 + 3], 0xFF, "pixel {i} A");
        }
    }

    #[test]
    fn decompress_rejects_non_bc_format() {
        let dummy = vec![0u8; 64];
        let err = decompress_to_rgba8(TextureFormat::B8G8R8A8, 4, 4, &dummy)
            .expect_err("non-bc rejected");
        assert!(matches!(
            err,
            Error::FormatNotDecompressibleToRgba { format: 0x1450 }
        ));
    }

    #[test]
    fn decompress_rejects_bc7() {
        let dummy = vec![0u8; 16];
        let err = decompress_to_rgba8(TextureFormat::BC7, 4, 4, &dummy).expect_err("bc7 rejected");
        assert!(matches!(
            err,
            Error::FormatNotDecompressibleToRgba { format: 0x6432 }
        ));
    }

    #[test]
    fn decompress_rejects_truncated_input() {
        let truncated = vec![0u8; 16];
        let err = decompress_to_rgba8(TextureFormat::BC1, 8, 8, &truncated)
            .expect_err("truncated rejected");
        assert!(matches!(err, Error::TexSurfaceOutOfBounds { .. }));
    }

    #[test]
    fn bc1_non_block_aligned_output_dimensions_are_clipped() {
        let red: [u8; 8] = [0x00, 0xF8, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00];
        let green: [u8; 8] = [0xE0, 0x07, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00];
        let blue: [u8; 8] = [0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00];
        let white: [u8; 8] = [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00];

        let mut src = Vec::with_capacity(32);
        src.extend_from_slice(&red);
        src.extend_from_slice(&green);
        src.extend_from_slice(&blue);
        src.extend_from_slice(&white);

        let rgba = decompress_to_rgba8(TextureFormat::BC1, 6, 6, &src).expect("decompress");
        assert_eq!(rgba.len(), 6 * 6 * 4);

        for y in 0..6usize {
            for x in 0..6usize {
                let bx = x / 4;
                let by = y / 4;
                let idx = (y * 6 + x) * 4;
                let (r, g, b) = match (bx, by) {
                    (0, 0) => (255, 0, 0),
                    (1, 0) => (0, 255, 0),
                    (0, 1) => (0, 0, 255),
                    (1, 1) => (255, 255, 255),
                    _ => unreachable!(),
                };
                assert_eq!(rgba[idx], r, "pixel ({x},{y}) R");
                assert_eq!(rgba[idx + 1], g, "pixel ({x},{y}) G");
                assert_eq!(rgba[idx + 2], b, "pixel ({x},{y}) B");
                assert_eq!(rgba[idx + 3], 255, "pixel ({x},{y}) A");
            }
        }
    }

    #[test]
    fn bc1_four_colour_interpolated_indices_use_floor_div() {
        let block: [u8; 8] = [0x00, 0x08, 0x00, 0x00, 0b1110_0100, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC1, 4, 1, &block).expect("decompress");
        assert_eq!(&rgba[0..4], &[8, 0, 0, 255], "pixel 0 = pal[0]");
        assert_eq!(&rgba[4..8], &[0, 0, 0, 255], "pixel 1 = pal[1]");
        assert_eq!(
            &rgba[8..12],
            &[5, 0, 0, 255],
            "pixel 2 = pal[2] = (2*8 + 0)/3 = 5 floor"
        );
        assert_eq!(
            &rgba[12..16],
            &[2, 0, 0, 255],
            "pixel 3 = pal[3] = (8 + 2*0)/3 = 2 floor (round would yield 3)"
        );
    }

    #[test]
    fn bc3_alpha_interpolated_indices_use_floor_div() {
        let alpha_idx: u32 = 0b111_110_101_100_011_010_001_000;
        let b0 = (alpha_idx & 0xFF) as u8;
        let b1 = ((alpha_idx >> 8) & 0xFF) as u8;
        let b2 = ((alpha_idx >> 16) & 0xFF) as u8;
        let block: [u8; 16] = [
            0xFF, 0x00, b0, b1, b2, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00,
            0x00,
        ];
        let rgba = decompress_to_rgba8(TextureFormat::BC3, 4, 2, &block).expect("decompress");
        assert_eq!(rgba[3], 0xFF, "alpha index 0");
        assert_eq!(rgba[7], 0x00, "alpha index 1");
        assert_eq!(rgba[11], 218, "alpha index 2 — floor (218 vs round 219)");
        assert_eq!(
            rgba[15], 182,
            "alpha index 3 — floor (182, agrees with round)"
        );
        assert_eq!(rgba[19], 145, "alpha index 4 — floor (145 vs round 146)");
        assert_eq!(rgba[23], 109, "alpha index 5 — floor (109 vs round 110)");
        assert_eq!(rgba[27], 72, "alpha index 6 — floor (72 vs round 73)");
        assert_eq!(rgba[31], 36, "alpha index 7 — floor (36 vs round 37)");
    }

    #[test]
    fn bc4_eight_step_indices_floor_div_matches_lumina() {
        let alpha_idx: u32 = 0b111_110_101_100_011_010_001_000;
        let b0 = (alpha_idx & 0xFF) as u8;
        let b1 = ((alpha_idx >> 8) & 0xFF) as u8;
        let b2 = ((alpha_idx >> 16) & 0xFF) as u8;
        let block: [u8; 8] = [0xFF, 0x00, b0, b1, b2, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC4, 4, 2, &block).expect("decompress");
        assert_eq!(rgba[0], 0xFF, "index 0 → a0");
        assert_eq!(rgba[4], 0x00, "index 1 → a1");
        assert_eq!(rgba[8], 218, "index 2 → floor 218");
        assert_eq!(rgba[12], 182, "index 3 → floor 182");
        assert_eq!(rgba[16], 145, "index 4 → floor 145");
        assert_eq!(rgba[20], 109, "index 5 → floor 109");
        assert_eq!(rgba[24], 72, "index 6 → floor 72");
        assert_eq!(rgba[28], 36, "index 7 → floor 36");
    }

    #[test]
    fn bc4_six_step_indices_floor_div_matches_lumina() {
        let alpha_idx: u32 = 0b111_110_101_100_011_010_001_000;
        let b0 = (alpha_idx & 0xFF) as u8;
        let b1 = ((alpha_idx >> 8) & 0xFF) as u8;
        let b2 = ((alpha_idx >> 16) & 0xFF) as u8;
        let block: [u8; 8] = [0x00, 0x01, b0, b1, b2, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC4, 4, 2, &block).expect("decompress");
        assert_eq!(rgba[0], 0x00, "index 0 → a0");
        assert_eq!(rgba[4], 0x01, "index 1 → a1");
        assert_eq!(rgba[8], 0, "index 2 → floor 0");
        assert_eq!(rgba[12], 0, "index 3 → floor 0");
        assert_eq!(rgba[16], 0, "index 4 → floor 0 (round=1)");
        assert_eq!(rgba[20], 0, "index 5 → floor 0 (round=1)");
        assert_eq!(rgba[24], 0, "index 6 → sentinel 0");
        assert_eq!(rgba[28], 0xFF, "index 7 → sentinel 255");
    }

    #[test]
    fn bc5_interpolated_indices_use_floor_div() {
        let alpha_idx: u32 = 0b111_110_101_100_011_010_001_000;
        let b0 = (alpha_idx & 0xFF) as u8;
        let b1 = ((alpha_idx >> 8) & 0xFF) as u8;
        let b2 = ((alpha_idx >> 16) & 0xFF) as u8;
        let block: [u8; 16] = [
            0xFF, 0x00, b0, b1, b2, 0x00, 0x00, 0x00, 0xFF, 0x00, b0, b1, b2, 0x00, 0x00, 0x00,
        ];
        let rgba = decompress_to_rgba8(TextureFormat::BC5, 4, 2, &block).expect("decompress");
        assert_eq!(rgba[8], 218, "R index 2");
        assert_eq!(rgba[9], 218, "G index 2");
        assert_eq!(rgba[12], 182, "R index 3");
        assert_eq!(rgba[13], 182, "G index 3");
        assert_eq!(rgba[16], 145, "R index 4");
        assert_eq!(rgba[17], 145, "G index 4");
        assert_eq!(rgba[20], 109, "R index 5");
        assert_eq!(rgba[21], 109, "G index 5");
        assert_eq!(rgba[10], 0);
        assert_eq!(rgba[11], 255);
    }

    #[test]
    fn decompress_rejects_unknown_format() {
        let dummy = vec![0u8; 8];
        let err = decompress_to_rgba8(TextureFormat::Unknown(0x9999), 4, 4, &dummy)
            .expect_err("unknown rejected");
        assert!(
            matches!(err, Error::FormatNotDecompressibleToRgba { .. }),
            "expected FormatNotDecompressibleToRgba, got {err:?}"
        );
    }

    #[test]
    fn decompress_rejects_oversize_input() {
        let oversize = vec![0u8; 16];
        let err = decompress_to_rgba8(TextureFormat::BC1, 4, 4, &oversize)
            .expect_err("oversize rejected");
        assert!(matches!(err, Error::TexSurfaceOutOfBounds { .. }));
    }

    #[test]
    fn decompress_rejects_linear_format_b8g8r8x8() {
        for fmt in [
            TextureFormat::L8,
            TextureFormat::A8,
            TextureFormat::B4G4R4A4,
            TextureFormat::B5G5R5A1,
            TextureFormat::B8G8R8A8,
            TextureFormat::B8G8R8X8,
            TextureFormat::R16G16B16A16F,
            TextureFormat::R32G32B32A32F,
        ] {
            let size = fmt.surface_byte_size(4, 4).unwrap();
            let size_usize = usize::try_from(size).expect("test size fits usize");
            let dummy = vec![0u8; size_usize];
            let err = decompress_to_rgba8(fmt, 4, 4, &dummy)
                .expect_err("linear format should be rejected");
            assert!(
                matches!(err, Error::FormatNotDecompressibleToRgba { .. }),
                "{fmt:?}: expected FormatNotDecompressibleToRgba, got {err:?}"
            );
        }
    }

    #[test]
    fn decompress_output_length_is_width_times_height_times_4_for_all_bcn() {
        for &(fmt, bpb) in &[
            (TextureFormat::BC1, 8usize),
            (TextureFormat::BC2, 16usize),
            (TextureFormat::BC3, 16usize),
            (TextureFormat::BC4, 8usize),
            (TextureFormat::BC5, 16usize),
        ] {
            for &(w, h) in &[(4u32, 4u32), (8u32, 8u32)] {
                let bw = (w.div_ceil(4).max(1)) as usize;
                let bh = (h.div_ceil(4).max(1)) as usize;
                let src = vec![0u8; bw * bh * bpb];
                let rgba = decompress_to_rgba8(fmt, w, h, &src)
                    .unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} decompress failed: {e}"));
                assert_eq!(
                    rgba.len(),
                    (w * h * 4) as usize,
                    "{fmt:?} {w}x{h}: output length wrong"
                );
            }
        }
    }

    #[test]
    fn decompress_1x1_bc1_produces_4_bytes() {
        let block: [u8; 8] = [0x00, 0xF8, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC1, 1, 1, &block).expect("decompress");
        assert_eq!(rgba.len(), 4, "1x1 output must be 4 bytes");
        assert_eq!(&rgba, &[255, 0, 0, 255]);
    }

    #[test]
    fn decompress_1x1_bc4_produces_4_bytes_with_rgb_replicated() {
        let block: [u8; 8] = [0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC4, 1, 1, &block).expect("decompress");
        assert_eq!(rgba.len(), 4);
        assert_eq!(rgba[0], 0xAB, "R");
        assert_eq!(rgba[1], 0xAB, "G — replicated from R");
        assert_eq!(rgba[2], 0xAB, "B — replicated from R");
        assert_eq!(rgba[3], 255, "A must be 255 for BC4");
    }

    #[test]
    fn decompress_1x1_bc5_produces_4_bytes_blue_zero_alpha_max() {
        let block: [u8; 16] = [
            0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00,
        ];
        let rgba = decompress_to_rgba8(TextureFormat::BC5, 1, 1, &block).expect("decompress");
        assert_eq!(rgba.len(), 4);
        assert_eq!(rgba[0], 0xC0, "R");
        assert_eq!(rgba[1], 0x40, "G");
        assert_eq!(rgba[2], 0, "B must be 0 for BC5");
        assert_eq!(rgba[3], 255, "A must be 255 for BC5");
    }

    #[test]
    fn decompress_7x7_bc3_output_length_correct() {
        let src = vec![0u8; 64];
        let rgba = decompress_to_rgba8(TextureFormat::BC3, 7, 7, &src).expect("decompress");
        assert_eq!(rgba.len(), 196);
    }

    #[test]
    fn decompress_256x256_bc1_output_length_is_256_kib() {
        let src = vec![0u8; 32_768];
        let rgba = decompress_to_rgba8(TextureFormat::BC1, 256, 256, &src).expect("decompress");
        assert_eq!(rgba.len(), 262_144);
    }

    #[test]
    fn bc1_color0_equals_color1_all_indices_zero_decodes_to_endpoint_color() {
        let block: [u8; 8] = [0x00, 0xF8, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00];
        let rgba = decompress_to_rgba8(TextureFormat::BC1, 4, 4, &block).expect("decompress");
        for i in 0..16 {
            assert_eq!(rgba[i * 4], 255, "pixel {i} R");
            assert_eq!(rgba[i * 4 + 1], 0, "pixel {i} G");
            assert_eq!(rgba[i * 4 + 2], 0, "pixel {i} B");
            assert_eq!(
                rgba[i * 4 + 3],
                255,
                "pixel {i} A must be opaque (index 0 = pal[0])"
            );
        }
    }

    #[test]
    fn bc2_alpha_nibble_zero_produces_alpha_byte_zero() {
        let block: [u8; 16] = [
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0x00,
            0x00, 0x00,
        ];
        let rgba = decompress_to_rgba8(TextureFormat::BC2, 4, 4, &block).expect("decompress");
        for i in 0..16 {
            assert_eq!(rgba[i * 4 + 3], 0, "pixel {i} alpha should be 0");
        }
    }

    #[test]
    fn bc2_alpha_nibble_mid_value_bit_replicates() {
        let block: [u8; 16] = [
            0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x00, 0xF8, 0x00, 0xF8, 0x00, 0x00,
            0x00, 0x00,
        ];
        let rgba = decompress_to_rgba8(TextureFormat::BC2, 4, 4, &block).expect("decompress");
        assert_eq!(rgba[3], 0x55, "pixel 0 alpha");
        assert_eq!(rgba[7], 0x88, "pixel 1 alpha");
    }

    fn lcg_next(state: &mut u64) -> u8 {
        *state = state
            .wrapping_mul(6_364_136_223_846_793_005)
            .wrapping_add(1_442_695_040_888_963_407);
        ((*state >> 33) & 0xFF) as u8
    }

    #[test]
    fn tex_parse_never_panics_on_random_80_byte_buffers() {
        use crate::tex::header::Tex;

        let mut state: u64 = 0xDEAD_BEEF_CAFE_F00D;
        for _ in 0..1000 {
            let buf: Vec<u8> = (0..80).map(|_| lcg_next(&mut state)).collect();
            let _ = Tex::parse(&buf);
        }
    }

    #[test]
    fn tex_parse_never_panics_on_random_short_buffers() {
        use crate::error::Error;
        use crate::tex::header::{TEX_HEADER_SIZE, Tex};

        let mut state: u64 = 0xCAFE_BABE_0000_0001;
        for len in 0..TEX_HEADER_SIZE {
            let buf: Vec<u8> = (0..len).map(|_| lcg_next(&mut state)).collect();
            let err = Tex::parse(&buf).expect_err("sub-80 must error");
            assert!(
                matches!(err, Error::InvalidTexLength { .. }),
                "len={len}: expected InvalidTexLength, got {err:?}"
            );
        }
    }
}
