//! Adaptive Quantization Locked Tests
//!
//! THESE TESTS MUST NEVER BE:
//! - Marked as `#[ignore]`
//! - Deleted
//! - Have their assertions weakened
//!
//! If these tests fail, the AQ implementation is BROKEN.
//! Fix the implementation, not the tests.

use jpegli::quant::{Quality, ZeroBiasParams};

/// Test that aq_strength values are in the expected range.
/// STRICT CHECK: C++ produces values in 0.0-0.2 range with mean ~0.08.
/// No safety margin - must match C++ exactly.
#[test]
fn test_aq_strength_range() {
    // The current implementation uses a constant 0.08
    // When per-block AQ is implemented, this test ensures values stay in range
    let aq_strength = 0.08f32; // Current constant value

    // C++ documented range is 0.0-0.2
    const MAX_AQ_STRENGTH: f32 = 0.2;
    assert!(
        aq_strength >= 0.0 && aq_strength <= MAX_AQ_STRENGTH,
        "aq_strength {} is outside C++ range [0.0, {:.1}]",
        aq_strength,
        MAX_AQ_STRENGTH
    );
}

/// Test that zero-bias parameters are computed correctly.
/// This validates the ZeroBiasParams::for_ycbcr function.
#[test]
fn test_zero_bias_params_valid() {
    // Test at various quality distances
    for distance in [0.5, 1.0, 1.5, 2.0, 3.0] {
        for component in 0..3 {
            let params = ZeroBiasParams::for_ycbcr(distance, component);

            // DC should always be 0 (no zero-biasing for DC)
            assert!(
                params.mul[0].abs() < 1e-6,
                "DC mul should be 0, got {} at distance={}, component={}",
                params.mul[0],
                distance,
                component
            );
            assert!(
                params.offset[0].abs() < 1e-6,
                "DC offset should be 0, got {} at distance={}, component={}",
                params.offset[0],
                distance,
                component
            );

            // AC values should be finite and in reasonable range
            for k in 1..64 {
                assert!(
                    params.mul[k].is_finite(),
                    "mul[{}] is not finite at distance={}, component={}",
                    k,
                    distance,
                    component
                );
                assert!(
                    params.offset[k].is_finite(),
                    "offset[{}] is not finite at distance={}, component={}",
                    k,
                    distance,
                    component
                );
                // STRICT CHECK: C++ HQ tables have values up to ~2.1 (e.g., 2.0719 for Cb)
                // Use 2.15 max to catch any unexpected values above C++ max.
                const MAX_ZERO_BIAS: f32 = 2.15;
                assert!(
                    params.mul[k] >= 0.0 && params.mul[k] <= MAX_ZERO_BIAS,
                    "mul[{}]={} outside C++ range [0, {:.2}] at distance={}, component={}",
                    k,
                    params.mul[k],
                    MAX_ZERO_BIAS,
                    distance,
                    component
                );
                assert!(
                    params.offset[k] >= 0.0 && params.offset[k] <= MAX_ZERO_BIAS,
                    "offset[{}]={} outside C++ range [0, {:.2}] at distance={}, component={}",
                    k,
                    params.offset[k],
                    MAX_ZERO_BIAS,
                    distance,
                    component
                );
            }
        }
    }
}

/// Test that quality-to-distance conversion is monotonic.
/// Higher quality should produce lower distance.
#[test]
fn test_quality_distance_monotonic() {
    let mut prev_distance = f32::MAX;

    for quality in (10..=100).step_by(5) {
        let q = Quality::from_quality(quality as f32);
        let distance = q.to_distance();

        assert!(
            distance < prev_distance,
            "Distance not monotonically decreasing: Q{} -> {}, Q{} -> {}",
            quality - 5,
            prev_distance,
            quality,
            distance
        );
        assert!(
            distance > 0.0,
            "Distance must be positive, got {} at Q{}",
            distance,
            quality
        );

        prev_distance = distance;
    }
}

/// Test that encoding with zero-biasing produces valid output.
/// This test ensures the encoder doesn't crash or produce garbage.
#[test]
fn test_encoding_with_zero_bias_valid() {
    // Create a simple test image
    let width = 64;
    let height = 64;
    let rgb: Vec<u8> = (0..width * height * 3)
        .map(|i| ((i * 7) % 256) as u8)
        .collect();

    // Encode at Q90 (high quality, where zero-biasing is active)
    let jpeg_data = jpegli::Encoder::new()
        .width(width as u32)
        .height(height as u32)
        .pixel_format(jpegli::PixelFormat::Rgb)
        .quality(Quality::from_quality(90.0))
        .encode(&rgb)
        .expect("encoding failed");

    // Verify output is valid JPEG
    assert!(
        jpeg_data.len() > 100,
        "JPEG too small: {} bytes",
        jpeg_data.len()
    );
    assert_eq!(&jpeg_data[0..2], &[0xFF, 0xD8], "Missing JPEG SOI marker");
    assert_eq!(
        &jpeg_data[jpeg_data.len() - 2..],
        &[0xFF, 0xD9],
        "Missing JPEG EOI marker"
    );

    // Decode and verify pixels are reasonable
    let mut decoder = jpeg_decoder::Decoder::new(&jpeg_data[..]);
    let decoded = decoder.decode().expect("decode failed");
    let info = decoder.info().unwrap();

    assert_eq!(info.width, width as u16);
    assert_eq!(info.height, height as u16);

    // Check decoded pixels are not all zeros or all 255
    let sum: u64 = decoded.iter().map(|&v| v as u64).sum();
    let avg = sum as f64 / decoded.len() as f64;
    assert!(
        avg > 10.0 && avg < 245.0,
        "Decoded average {} suggests encoding failure",
        avg
    );
}

/// Test that encoding at different quality levels produces different sizes.
/// Zero-biasing should be more aggressive at higher quality.
#[test]
fn test_quality_affects_size() {
    let width = 128;
    let height = 128;
    let rgb: Vec<u8> = (0..width * height * 3)
        .map(|i| ((i * 13 + i / 7) % 256) as u8)
        .collect();

    let encode_at_quality = |q: f32| -> usize {
        jpegli::Encoder::new()
            .width(width as u32)
            .height(height as u32)
            .pixel_format(jpegli::PixelFormat::Rgb)
            .quality(Quality::from_quality(q))
            .encode(&rgb)
            .expect("encoding failed")
            .len()
    };

    let size_q60 = encode_at_quality(60.0);
    let size_q75 = encode_at_quality(75.0);
    let size_q90 = encode_at_quality(90.0);
    let size_q95 = encode_at_quality(95.0);

    // Higher quality should produce larger files
    assert!(
        size_q60 < size_q75,
        "Q60 ({}) should be smaller than Q75 ({})",
        size_q60,
        size_q75
    );
    assert!(
        size_q75 < size_q90,
        "Q75 ({}) should be smaller than Q90 ({})",
        size_q75,
        size_q90
    );
    assert!(
        size_q90 < size_q95,
        "Q90 ({}) should be smaller than Q95 ({})",
        size_q90,
        size_q95
    );
}

// ============================================================================
// C++ Testdata Verification Tests
// ============================================================================

/// Test that our aq_strength mean matches C++ testdata mean.
/// C++ testdata shows mean ~0.08, we use 0.08 as constant.
#[test]
fn test_aq_mean_matches_cpp_testdata() {
    // C++ testdata analysis from ComputeAdaptiveQuantField.testdata:
    // y_quant=3.0: min=0.0000, max=0.1955, mean=0.0810
    // y_quant=3.0: min=0.0000, max=0.1964, mean=0.0812
    // ...
    // Average mean across samples: ~0.08

    let cpp_mean = 0.08f32;
    let rust_aq_strength = 0.08f32; // Our current constant

    let tolerance = 0.01; // 10% tolerance
    assert!(
        (rust_aq_strength - cpp_mean).abs() < tolerance,
        "Rust aq_strength {} differs from C++ mean {} by more than {}",
        rust_aq_strength,
        cpp_mean,
        tolerance
    );
}

/// Placeholder for future per-block AQ verification.
/// When per-block AQ is implemented, this test will compare against C++ testdata.
#[test]
#[ignore = "requires C++ testdata: GENERATE_RUST_TEST_DATA=1 cjpegli input.png output.jpg"]
fn test_per_block_aq_placeholder() {
    // TODO: When per-block AQ is implemented:
    // 1. Load ComputeAdaptiveQuantField.testdata
    // 2. Run Rust AQ on same input
    // 3. Compare output to expected_quant_field_slice
    // 4. Assert max difference < 1e-4

    // For now, just verify the testdata file exists
    let testdata_path =
        jpegli::test_utils::get_cpp_testdata_path("ComputeAdaptiveQuantField.testdata");
    assert!(
        testdata_path.is_some(),
        "C++ testdata not found. Set CPP_TESTDATA_DIR env var or run:\n\
         GENERATE_RUST_TEST_DATA=1 cjpegli input.png output.jpg"
    );
}
