//! Regression tests for progressive JPEG encoding.
//!
//! These tests verify that progressive JPEG encoding produces valid output
//! that can be decoded by standard decoders.
//!
//! NOTE: We use zune-jpeg for decoder verification as it handles
//! our progressive output correctly.

use jpegli::encoder::ChromaSubsampling;
use jpegli::encoder::{EncoderConfig, PixelLayout};
use std::io::Cursor;
use std::process::Command;

/// Helper function to encode RGB data with given config
fn encode_rgb(
    width: u32,
    height: u32,
    data: &[u8],
    config: &EncoderConfig,
) -> jpegli::encoder::Result<Vec<u8>> {
    let mut enc = config.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)?;
    enc.push_packed(data, enough::Unstoppable)?;
    enc.finish()
}

/// Helper function to encode grayscale data with given config
fn encode_gray(
    width: u32,
    height: u32,
    data: &[u8],
    config: &EncoderConfig,
) -> jpegli::encoder::Result<Vec<u8>> {
    let mut enc = config.encode_from_bytes(width, height, PixelLayout::Gray8Srgb)?;
    enc.push_packed(data, enough::Unstoppable)?;
    enc.finish()
}

/// Helper function to decode JPEG using zune-jpeg
fn decode_with_zune(jpeg_data: &[u8]) -> Result<Vec<u8>, String> {
    let cursor = Cursor::new(jpeg_data);
    zune_jpeg::JpegDecoder::new(cursor)
        .decode()
        .map_err(|e| format!("{:?}", e))
}

/// Test that progressive encoding of a grayscale gradient produces valid output.
#[test]
fn test_progressive_grayscale_gradient() {
    let width = 16u32;
    let height = 16u32;
    let mut data = Vec::with_capacity((width * height) as usize);

    for _y in 0..height {
        for x in 0..width {
            data.push((x * 16) as u8);
        }
    }

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .grayscale()
        .quality(90.0)
        .progressive(true);

    let jpeg_data =
        encode_gray(width, height, &data, &config).expect("Progressive encoding should succeed");

    // Verify the file is a valid JPEG by checking markers
    assert!(jpeg_data.len() > 100, "JPEG should be at least 100 bytes");
    assert_eq!(jpeg_data[0], 0xFF, "Should start with FF");
    assert_eq!(jpeg_data[1], 0xD8, "Should have SOI marker");

    // Check for SOF2 (progressive DCT)
    let mut found_sof2 = false;
    for i in 0..jpeg_data.len() - 1 {
        if jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xC2 {
            found_sof2 = true;
            break;
        }
    }
    assert!(found_sof2, "Progressive JPEG should have SOF2 marker");

    // Verify EOI marker
    assert_eq!(jpeg_data[jpeg_data.len() - 2], 0xFF);
    assert_eq!(jpeg_data[jpeg_data.len() - 1], 0xD9);

    // If djpeg is available, verify the file decodes correctly
    if let Ok(output) = Command::new("djpeg")
        .args(["-outfile", "/dev/null"])
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .spawn()
    {
        let _ = output.wait_with_output();
        // djpeg might not be available in all environments, that's OK
    }
}

/// Test that progressive encoding of solid gray produces valid output.
#[test]
fn test_progressive_solid_gray() {
    let width = 16u32;
    let height = 16u32;
    let data = vec![128u8; (width * height) as usize];

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .grayscale()
        .quality(90.0)
        .progressive(true);

    let jpeg_data =
        encode_gray(width, height, &data, &config).expect("Progressive encoding should succeed");

    // Basic validation
    assert!(jpeg_data.len() > 50);
    assert_eq!(&jpeg_data[0..2], &[0xFF, 0xD8]); // SOI
    assert_eq!(&jpeg_data[jpeg_data.len() - 2..], &[0xFF, 0xD9]); // EOI
}

/// Test that progressive encoding of RGB image produces valid output.
#[test]
fn test_progressive_rgb() {
    let width = 16u32;
    let height = 16u32;
    let mut data = Vec::with_capacity((width * height * 3) as usize);

    // Create a simple gradient
    for y in 0..height {
        for x in 0..width {
            data.push((x * 16) as u8); // R
            data.push((y * 16) as u8); // G
            data.push(128); // B
        }
    }

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter).progressive(true);

    let jpeg_data =
        encode_rgb(width, height, &data, &config).expect("Progressive RGB encoding should succeed");

    // Verify SOF2 marker for progressive
    let mut found_sof2 = false;
    for i in 0..jpeg_data.len() - 1 {
        if jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xC2 {
            found_sof2 = true;
            break;
        }
    }
    assert!(found_sof2, "Progressive JPEG should have SOF2 marker");
}

/// Test that progressive encoding produces multiple scans.
#[test]
fn test_progressive_has_multiple_scans() {
    let width = 32u32;
    let height = 32u32;
    let mut data = Vec::with_capacity((width * height) as usize);

    for y in 0..height {
        for x in 0..width {
            data.push(((x + y) * 4) as u8);
        }
    }

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .grayscale()
        .quality(85.0)
        .progressive(true);

    let jpeg_data = encode_gray(width, height, &data, &config).expect("Encoding should succeed");

    // Count SOS markers (Start Of Scan)
    let mut sos_count = 0;
    for i in 0..jpeg_data.len() - 1 {
        if jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xDA {
            sos_count += 1;
        }
    }

    // Progressive JPEG should have at least 2 scans (DC + AC)
    assert!(
        sos_count >= 2,
        "Progressive JPEG should have at least 2 scans, found {}",
        sos_count
    );
}

/// Test that progressive mode correctly requires optimized Huffman tables.
/// Progressive + fixed Huffman is not supported (JPEG standard requires optimization for progressive).
#[test]
fn test_progressive_optimized_smaller() {
    let width = 64u32;
    let height = 64u32;
    let mut data = Vec::with_capacity((width * height * 3) as usize);

    // Create a more complex pattern to show optimization benefit
    for y in 0..height {
        for x in 0..width {
            let val = ((x * 13 + y * 17) % 256) as u8;
            data.push(val); // R
            data.push(255 - val); // G
            data.push((val / 2) + 64); // B
        }
    }

    // Progressive + fixed Huffman should fail (not supported)
    let config_no_opt = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(true)
        .optimize_huffman(false);

    let result = encode_rgb(width, height, &data, &config_no_opt);
    assert!(result.is_err(), "Progressive + fixed Huffman should fail");

    // Progressive + optimized Huffman should succeed
    let config_opt = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(true)
        .optimize_huffman(true);

    let opt_data = encode_rgb(width, height, &data, &config_opt)
        .expect("Progressive with optimized Huffman should succeed");

    // Should be valid JPEG
    assert_eq!(&opt_data[0..2], &[0xFF, 0xD8]);

    // Compare with baseline + optimized to verify progressive is smaller
    let config_baseline = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(false)
        .optimize_huffman(true);

    let baseline_data =
        encode_rgb(width, height, &data, &config_baseline).expect("Baseline should succeed");

    // Progressive should be smaller than baseline (or close)
    let savings = baseline_data.len() as f64 - opt_data.len() as f64;
    let savings_pct = savings / baseline_data.len() as f64 * 100.0;
    println!(
        "Progressive: {} bytes, Baseline: {} bytes, Savings: {:.1}%",
        opt_data.len(),
        baseline_data.len(),
        savings_pct
    );
}

/// Test that progressive encoding with optimized tables can be decoded by external decoders.
#[test]
fn test_progressive_optimized_external_decode() {
    let width = 32u32;
    let height = 32u32;
    let mut data = Vec::with_capacity((width * height) as usize);

    for y in 0..height {
        for x in 0..width {
            data.push(((x * 8 + y * 8) % 256) as u8);
        }
    }

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .grayscale()
        .quality(90.0)
        .progressive(true);

    let jpeg_data = encode_gray(width, height, &data, &config).expect("Encoding should succeed");

    // Verify it's a valid JPEG structure
    assert_eq!(&jpeg_data[0..2], &[0xFF, 0xD8]); // SOI
    assert_eq!(&jpeg_data[jpeg_data.len() - 2..], &[0xFF, 0xD9]); // EOI

    // Verify it has DHT and SOF2 markers
    let mut found_dht = false;
    let mut found_sof2 = false;
    for i in 0..jpeg_data.len() - 1 {
        if jpeg_data[i] == 0xFF {
            match jpeg_data[i + 1] {
                0xC4 => found_dht = true,
                0xC2 => found_sof2 = true,
                _ => {}
            }
        }
    }
    assert!(found_dht, "Should have DHT marker");
    assert!(found_sof2, "Should have SOF2 marker for progressive");

    // Try decoding with zune-jpeg (external crate)
    // This verifies our output is standards-compliant
    // Note: zune-jpeg decodes grayscale to RGB (3 bytes per pixel)
    let decoded = decode_with_zune(&jpeg_data).expect("zune-jpeg should decode");
    assert_eq!(decoded.len(), (width * height * 3) as usize);
}

/// Test optimized progressive with a larger image shows file size benefit.
#[test]
fn test_progressive_optimized_larger_image() {
    let width = 256u32;
    let height = 256u32;
    let mut data = Vec::with_capacity((width * height * 3) as usize);

    // Create a realistic pattern with varied content
    for y in 0..height {
        for x in 0..width {
            // Mix of gradients and noise-like patterns
            let base = ((x as f32 / width as f32) * 255.0) as u8;
            let noise = ((x * 7 + y * 13) % 64) as u8;
            data.push(base.wrapping_add(noise)); // R
            data.push(255u8.wrapping_sub(base)); // G
            data.push(((y * 255) / height) as u8); // B
        }
    }

    // Progressive with optimized Huffman
    let config_prog = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(true)
        .optimize_huffman(true);
    let prog_data = encode_rgb(width, height, &data, &config_prog)
        .expect("Progressive encoding should succeed");

    // Baseline with optimized Huffman for comparison
    let config_baseline = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(false)
        .optimize_huffman(true);
    let baseline_data = encode_rgb(width, height, &data, &config_baseline)
        .expect("Baseline encoding should succeed");

    // Compare sizes - progressive may be larger due to scan overhead
    // but should not be dramatically larger
    let size_ratio = prog_data.len() as f64 / baseline_data.len() as f64;
    println!(
        "256x256 RGB: progressive={} bytes, baseline={} bytes, ratio={:.2}",
        prog_data.len(),
        baseline_data.len(),
        size_ratio
    );

    // Progressive should not be more than 15% larger (scan overhead)
    assert!(
        size_ratio < 1.15,
        "Progressive should not be much larger than baseline: ratio={}",
        size_ratio
    );

    // Verify both decode correctly with zune-jpeg
    let decoded_prog = decode_with_zune(&prog_data).expect("Progressive should decode");
    let decoded_baseline = decode_with_zune(&baseline_data).expect("Baseline should decode");

    assert_eq!(decoded_prog.len(), decoded_baseline.len());
}

/// Test progressive optimized with uniform solid color.
#[test]
fn test_progressive_optimized_solid_color() {
    let width = 64u32;
    let height = 64u32;
    // Solid red
    let data: Vec<u8> = (0..(width * height)).flat_map(|_| [255u8, 0, 0]).collect();

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(90.0)
        .progressive(true)
        .optimize_huffman(true);
    let jpeg_data = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");

    // Solid colors should compress very well
    assert!(jpeg_data.len() < 2000, "Solid color should compress well");

    // Verify decode with zune-jpeg
    let decoded = decode_with_zune(&jpeg_data).expect("Should decode");
    assert_eq!(decoded.len(), (width * height * 3) as usize);
}

/// Test progressive optimized with high-frequency content.
#[test]
fn test_progressive_optimized_high_frequency() {
    let width = 64u32;
    let height = 64u32;
    let mut data = Vec::with_capacity((width * height * 3) as usize);

    // Checkerboard pattern - high frequency content
    for y in 0..height {
        for x in 0..width {
            let val = if (x + y) % 2 == 0 { 255u8 } else { 0u8 };
            data.push(val);
            data.push(val);
            data.push(val);
        }
    }

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(true)
        .optimize_huffman(true);
    let jpeg_data = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");

    // Verify decode with zune-jpeg
    let decoded = decode_with_zune(&jpeg_data).expect("Should decode");
    assert_eq!(decoded.len(), (width * height * 3) as usize);
}

/// Test progressive optimized at various quality levels.
#[test]
fn test_progressive_optimized_quality_levels() {
    let width = 64u32;
    let height = 64u32;
    let mut data = Vec::with_capacity((width * height * 3) as usize);

    // Use a more varied pattern to ensure all symbol categories appear
    for y in 0..height {
        for x in 0..width {
            let noise = ((x * 7 + y * 13) % 64) as u8;
            data.push(((x * 4) as u8).wrapping_add(noise));
            data.push(((y * 4) as u8).wrapping_add(noise / 2));
            data.push(128u8.wrapping_add(noise));
        }
    }

    let mut prev_size = 0usize;

    for quality in [70.0, 85.0, 95.0] {
        let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
            .quality(quality)
            .progressive(true)
            .optimize_huffman(true);
        let jpeg_data = encode_rgb(width, height, &data, &config)
            .unwrap_or_else(|_| panic!("Q{} encoding should succeed", quality));

        // Higher quality should generally produce larger files
        if prev_size > 0 {
            assert!(
                jpeg_data.len() >= prev_size - 100,
                "Q{} ({} bytes) should not be much smaller than lower quality ({} bytes)",
                quality,
                jpeg_data.len(),
                prev_size
            );
        }
        prev_size = jpeg_data.len();

        // Verify decode with zune-jpeg
        decode_with_zune(&jpeg_data).unwrap_or_else(|_| panic!("Q{} should decode", quality));
    }
}

/// Test progressive optimized with single 8x8 block (edge case).
#[test]
fn test_progressive_optimized_single_block() {
    let width = 8u32;
    let height = 8u32;
    let data: Vec<u8> = (0..64).flat_map(|i| [i as u8 * 4, 128, 64]).collect();

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(90.0)
        .progressive(true)
        .optimize_huffman(true);
    let jpeg_data = encode_rgb(width, height, &data, &config).expect("Single block should encode");

    // Should still be valid
    assert_eq!(&jpeg_data[0..2], &[0xFF, 0xD8]);
    assert_eq!(&jpeg_data[jpeg_data.len() - 2..], &[0xFF, 0xD9]);

    decode_with_zune(&jpeg_data).expect("Single block should decode");
}

/// Test progressive optimized grayscale at various sizes.
#[test]
fn test_progressive_optimized_grayscale_sizes() {
    for size in [16u32, 32, 64, 128] {
        let mut data = Vec::with_capacity((size * size) as usize);
        for y in 0..size {
            for x in 0..size {
                data.push(((x + y) * 255 / (2 * size - 2)) as u8);
            }
        }

        let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
            .grayscale()
            .quality(85.0)
            .progressive(true)
            .optimize_huffman(true);
        let jpeg_data = encode_gray(size, size, &data, &config)
            .unwrap_or_else(|_| panic!("{}x{} gray should encode", size, size));

        let decoded = decode_with_zune(&jpeg_data)
            .unwrap_or_else(|_| panic!("{}x{} gray should decode", size, size));

        // zune-jpeg decodes grayscale to RGB (3 bytes per pixel)
        assert_eq!(decoded.len(), (size * size * 3) as usize);
    }
}

/// Test that progressive optimized produces valid scan structure.
#[test]
fn test_progressive_optimized_scan_structure() {
    let width = 32u32;
    let height = 32u32;
    let data: Vec<u8> = (0..(width * height * 3)).map(|i| (i % 256) as u8).collect();

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(true)
        .optimize_huffman(true);
    let jpeg_data = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");

    // Count markers
    let mut sos_count = 0;
    let mut dht_count = 0;
    let mut dqt_count = 0;
    let mut sof2_count = 0;

    let mut i = 0;
    while i < jpeg_data.len() - 1 {
        if jpeg_data[i] == 0xFF {
            match jpeg_data[i + 1] {
                0xDA => sos_count += 1,
                0xC4 => dht_count += 1,
                0xDB => dqt_count += 1,
                0xC2 => sof2_count += 1,
                _ => {}
            }
        }
        i += 1;
    }

    assert_eq!(sof2_count, 1, "Should have exactly 1 SOF2 marker");
    assert!(dht_count >= 1, "Should have at least 1 DHT marker");
    assert!(dqt_count >= 1, "Should have at least 1 DQT marker");
    assert!(
        sos_count >= 2,
        "Progressive should have at least 2 SOS markers"
    );
}

/// Test non-square image dimensions.
#[test]
fn test_progressive_optimized_non_square() {
    // Wide image
    let width = 128u32;
    let height = 32u32;
    let mut data = Vec::with_capacity((width * height * 3) as usize);
    for y in 0..height {
        for x in 0..width {
            data.push((x * 2) as u8);
            data.push((y * 8) as u8);
            data.push(128);
        }
    }

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(true)
        .optimize_huffman(true);
    let jpeg_data = encode_rgb(width, height, &data, &config).expect("Wide image should encode");

    let decoded = decode_with_zune(&jpeg_data).expect("Wide image should decode");
    assert_eq!(decoded.len(), (width * height * 3) as usize);

    // Tall image
    let width = 32u32;
    let height = 128u32;
    let mut data = Vec::with_capacity((width * height * 3) as usize);
    for y in 0..height {
        for x in 0..width {
            data.push((x * 8) as u8);
            data.push((y * 2) as u8);
            data.push(128);
        }
    }

    let jpeg_data = encode_rgb(width, height, &data, &config).expect("Tall image should encode");

    let decoded = decode_with_zune(&jpeg_data).expect("Tall image should decode");
    assert_eq!(decoded.len(), (width * height * 3) as usize);
}

/// Test non-multiple-of-8 dimensions (requires padding).
#[test]
fn test_progressive_optimized_odd_dimensions() {
    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(85.0)
        .progressive(true)
        .optimize_huffman(true);

    for (width, height) in [(17u32, 23u32), (33, 41), (65, 70), (100, 99)] {
        let mut data = Vec::with_capacity((width * height * 3) as usize);
        for y in 0..height {
            for x in 0..width {
                data.push(((x.wrapping_mul(7).wrapping_add(y.wrapping_mul(3))) % 256) as u8);
                data.push(((x.wrapping_mul(3).wrapping_add(y.wrapping_mul(11))) % 256) as u8);
                data.push(((x.wrapping_add(y.wrapping_mul(5))) % 256) as u8);
            }
        }

        let jpeg_data = encode_rgb(width, height, &data, &config)
            .unwrap_or_else(|_| panic!("{}x{} should encode", width, height));

        // Verify full decode works and size is correct using zune-jpeg
        let decoded = decode_with_zune(&jpeg_data)
            .unwrap_or_else(|_| panic!("{}x{} should decode", width, height));
        assert_eq!(
            decoded.len(),
            (width * height * 3) as usize,
            "Decoded size mismatch for {}x{}",
            width,
            height
        );
    }
}

/// Test baseline encoding still works after progressive changes.
#[test]
fn test_baseline_still_works() {
    let width = 16u32;
    let height = 16u32;
    let mut data = Vec::with_capacity((width * height) as usize);

    for _y in 0..height {
        for x in 0..width {
            data.push((x * 16) as u8);
        }
    }

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .grayscale()
        .quality(90.0)
        .progressive(false);

    let jpeg_data =
        encode_gray(width, height, &data, &config).expect("Baseline encoding should succeed");

    // Verify SOF0 marker for baseline (not SOF2)
    let mut found_sof0 = false;
    for i in 0..jpeg_data.len() - 1 {
        if jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xC0 {
            found_sof0 = true;
            break;
        }
    }
    assert!(found_sof0, "Baseline JPEG should have SOF0 marker");

    // Should only have 1 SOS marker
    let sos_count = (0..jpeg_data.len() - 1)
        .filter(|&i| jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xDA)
        .count();
    assert_eq!(sos_count, 1, "Baseline JPEG should have exactly 1 scan");
}

/// Comprehensive test of progressive encoding at all quality levels from 10 to 100.
#[test]
fn test_progressive_all_quality_levels() {
    let width = 64u32;
    let height = 64u32;

    // Test image with varied content
    let mut data = Vec::with_capacity((width * height * 3) as usize);
    for y in 0..height {
        for x in 0..width {
            let noise = ((x * 7 + y * 13) % 64) as u8;
            data.push(((x * 4) as u8).wrapping_add(noise));
            data.push(((y * 4) as u8).wrapping_add(noise / 2));
            data.push(128u8.wrapping_add(noise));
        }
    }

    let mut prev_size = 0usize;

    // Test quality levels from 10 to 100 in steps of 5
    for q in (10..=100).step_by(5) {
        let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
            .quality(q as f32)
            .progressive(true)
            .optimize_huffman(true);
        let jpeg_data = encode_rgb(width, height, &data, &config)
            .unwrap_or_else(|_| panic!("Q{} encoding should succeed", q));

        let size = jpeg_data.len();

        // Verify it decodes with zune-jpeg
        decode_with_zune(&jpeg_data).unwrap_or_else(|_| panic!("Q{} should decode", q));

        // Check size progression (higher Q should generally be >= lower Q - 500 bytes tolerance)
        if prev_size > 0 {
            assert!(
                size >= prev_size.saturating_sub(500),
                "Q{} ({} bytes) should not be much smaller than Q{} ({} bytes)",
                q,
                size,
                q - 5,
                prev_size
            );
        }
        prev_size = size;

        println!("Q{}: {} bytes - OK", q, size);
    }
}

/// Test progressive encoding with various image types at multiple quality levels.
#[test]
fn test_progressive_quality_various_content() {
    struct TestCase {
        name: &'static str,
        width: u32,
        height: u32,
        generator: fn(u32, u32) -> Vec<u8>,
    }

    fn solid_red(w: u32, h: u32) -> Vec<u8> {
        (0..w * h).flat_map(|_| [255, 0, 0]).collect()
    }

    fn gradient(w: u32, h: u32) -> Vec<u8> {
        (0..h)
            .flat_map(|y| (0..w).flat_map(move |x| [(x * 255 / w) as u8, (y * 255 / h) as u8, 128]))
            .collect()
    }

    fn checkerboard(w: u32, h: u32) -> Vec<u8> {
        (0..h)
            .flat_map(|y| {
                (0..w).flat_map(move |x| {
                    if (x / 8 + y / 8) % 2 == 0 {
                        [0, 0, 0]
                    } else {
                        [255, 255, 255]
                    }
                })
            })
            .collect()
    }

    fn photo_like(w: u32, h: u32) -> Vec<u8> {
        (0..h)
            .flat_map(|y| {
                (0..w).flat_map(move |x| {
                    let r = ((x.wrapping_mul(17) ^ y.wrapping_mul(31)) % 256) as u8;
                    let g = ((x.wrapping_mul(13) ^ y.wrapping_mul(23)) % 256) as u8;
                    let b = ((x.wrapping_mul(11) ^ y.wrapping_mul(19)) % 256) as u8;
                    [r, g, b]
                })
            })
            .collect()
    }

    let test_cases = [
        TestCase {
            name: "solid_red",
            width: 32,
            height: 32,
            generator: solid_red,
        },
        TestCase {
            name: "gradient",
            width: 64,
            height: 64,
            generator: gradient,
        },
        TestCase {
            name: "checkerboard",
            width: 48,
            height: 48,
            generator: checkerboard,
        },
        TestCase {
            name: "photo_like",
            width: 128,
            height: 96,
            generator: photo_like,
        },
        TestCase {
            name: "photo_like_odd",
            width: 127,
            height: 95,
            generator: photo_like,
        },
    ];

    let quality_levels = [30.0, 50.0, 75.0, 85.0, 95.0];

    for tc in &test_cases {
        let data = (tc.generator)(tc.width, tc.height);

        for &q in &quality_levels {
            let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
                .quality(q)
                .progressive(true)
                .optimize_huffman(true);
            let jpeg_data = encode_rgb(tc.width, tc.height, &data, &config)
                .unwrap_or_else(|_| panic!("{} Q{} encoding should succeed", tc.name, q));

            decode_with_zune(&jpeg_data)
                .unwrap_or_else(|_| panic!("{} Q{} should decode", tc.name, q));

            println!(
                "{} {}x{} Q{}: {} bytes",
                tc.name,
                tc.width,
                tc.height,
                q,
                jpeg_data.len()
            );
        }
    }
}

/// Test extreme low quality (Q3) progressive encoding.
#[test]
fn test_progressive_extreme_low_quality() {
    let width = 64u32;
    let height = 64u32;

    // Photo-like content
    let data: Vec<u8> = (0..height)
        .flat_map(|y| {
            (0..width).flat_map(move |x| {
                let r = ((x.wrapping_mul(17) ^ y.wrapping_mul(31)) % 256) as u8;
                let g = ((x.wrapping_mul(13) ^ y.wrapping_mul(23)) % 256) as u8;
                let b = ((x.wrapping_mul(11) ^ y.wrapping_mul(19)) % 256) as u8;
                [r, g, b]
            })
        })
        .collect();

    for q in [1.0, 2.0, 3.0, 5.0, 7.0, 10.0] {
        let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
            .quality(q)
            .progressive(true)
            .optimize_huffman(true);
        let jpeg_data = encode_rgb(width, height, &data, &config)
            .unwrap_or_else(|_| panic!("Q{} encoding should succeed", q));

        decode_with_zune(&jpeg_data).unwrap_or_else(|_| panic!("Q{} should decode", q));

        println!("Q{}: {} bytes", q, jpeg_data.len());
    }
}

/// Test libjpeg/djpeg compatibility with complex noise image.
///
/// This test replicates the issue where djpeg reports "bad Huffman code" or
/// "extraneous bytes before marker" errors on our progressive output.
///
/// NOTE: This test is marked as ignored because it currently fails.
/// The djpeg tool (and jpeg-decoder crate) reject our AC refinement encoding,
/// even though zune-jpeg decodes it correctly.
#[test]
#[ignore] // KNOWN BUG: djpeg rejects our AC refinement encoding
fn test_libjpeg_compatibility_noise() {
    let width = 64u32;
    let height = 64u32;

    // Generate deterministic noise pattern that triggers AC refinement
    let data: Vec<u8> = (0..height)
        .flat_map(|y| {
            (0..width).flat_map(move |x| {
                let r = ((x.wrapping_mul(17) ^ y.wrapping_mul(31)) % 256) as u8;
                let g = ((x.wrapping_mul(13) ^ y.wrapping_mul(23)) % 256) as u8;
                let b = ((x.wrapping_mul(11) ^ y.wrapping_mul(19)) % 256) as u8;
                [r, g, b]
            })
        })
        .collect();

    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(50.0)
        .progressive(true)
        .optimize_huffman(true);
    let jpeg_data = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");

    // Verify the file decodes with our test decoders
    let zune_decoded = decode_with_zune(&jpeg_data).expect("zune-jpeg should decode");

    // Basic sanity check
    assert_eq!(
        zune_decoded.len(),
        (width * height * 3) as usize,
        "Decoded size should match"
    );

    // Now test with djpeg if available

    // Write JPEG to temp file
    let temp_path = std::env::temp_dir().join("test_libjpeg_compat.jpg");
    std::fs::write(&temp_path, &jpeg_data).expect("Failed to write temp file");

    // Run djpeg
    let output = Command::new("djpeg")
        .arg(&temp_path)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .output();

    match output {
        Ok(result) => {
            let stderr = String::from_utf8_lossy(&result.stderr);
            if !stderr.is_empty() {
                eprintln!("djpeg stderr: {}", stderr);
            }
            assert!(
                result.status.success(),
                "djpeg should decode without errors. stderr: {}",
                stderr
            );
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            eprintln!("djpeg not found, skipping libjpeg compatibility check");
        }
        Err(e) => {
            panic!("Failed to run djpeg: {}", e);
        }
    }

    // Clean up
    let _ = std::fs::remove_file(&temp_path);
}

/// Test that we match C++ jpegli decoded output pixel-for-pixel.
///
/// This test is ignored because it requires the C++ cjpegli tool to be built.
#[test]
#[ignore] // Requires C++ cjpegli tool
fn test_cpp_pixel_parity() {
    let width = 64u32;
    let height = 64u32;

    // Generate test image
    let data: Vec<u8> = (0..height)
        .flat_map(|y| {
            (0..width).flat_map(move |x| {
                let r = ((x.wrapping_mul(17) ^ y.wrapping_mul(31)) % 256) as u8;
                let g = ((x.wrapping_mul(13) ^ y.wrapping_mul(23)) % 256) as u8;
                let b = ((x.wrapping_mul(11) ^ y.wrapping_mul(19)) % 256) as u8;
                [r, g, b]
            })
        })
        .collect();

    // Encode with Rust
    let config = EncoderConfig::new(90.0, ChromaSubsampling::Quarter)
        .quality(50.0)
        .progressive(true)
        .optimize_huffman(true);
    let rust_jpeg = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");

    // Decode with zune-jpeg
    let rust_decoded = decode_with_zune(&rust_jpeg).expect("Should decode");

    // Check that decoded size matches
    assert_eq!(
        rust_decoded.len(),
        (width * height * 3) as usize,
        "Decoded size should match input dimensions"
    );

    println!(
        "Rust JPEG: {} bytes, decoded to {} bytes",
        rust_jpeg.len(),
        rust_decoded.len()
    );
}
