//! Tone mapping tests matching C++ jpegli thresholds.
//!
//! These tests validate that the tone mapping implementations match the C++
//! jpegli reference implementation within the exact error thresholds specified
//! in lib/cms/tone_mapping_test.cc.

use jpegli::tone_mapping::{gamut_map, Color, HlgOotf, PrimariesLuminances, Rec2408ToneMapper};

// ============================================================================
// C++ Thresholds (from lib/cms/tone_mapping_test.cc)
// ============================================================================

/// TestRec2408ToneMap absolute error threshold
const REC2408_TONE_MAP_ERROR: f64 = 2.75e-5;
/// TestHlgOotfApply absolute error threshold
const HLG_OOTF_ERROR: f64 = 7.2e-7;
/// TestGamutMap absolute error threshold
const GAMUT_MAP_ERROR: f64 = 1e-10;

/// Simple LCG PRNG for test reproducibility.
struct Rng {
    state: u64,
}

impl Rng {
    fn new(seed: u64) -> Self {
        Self { state: seed }
    }

    fn uniform_u32(&mut self) -> u32 {
        self.state = self
            .state
            .wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);
        (self.state >> 32) as u32
    }

    fn uniform_f(&mut self, min: f32, max: f32) -> f32 {
        let t = (self.uniform_u32() as f64) / (u32::MAX as f64);
        min + (max - min) * t as f32
    }
}

/// Test Rec2408 tone mapper self-consistency.
///
/// NOTE: This test compares Rust implementation against itself, NOT against C++.
/// The C++ thresholds are copied for reference, but this only validates that
/// repeated calls produce identical results. True C++ parity requires FFI testing.
///
/// C++ test parameters (for reference only):
/// - 8M trials (we use 1M for speed)
/// - Source: 11000 +/- 150 nits
/// - Target: 250 +/- 5 nits
/// - Luminances: random [0.2, 0.4]
/// - RGB: random [0, 1]
/// - Error threshold: 2.75e-5
///
/// TODO: Add actual C++ comparison via FFI bindings.
#[test]
fn test_rec2408_tone_map_self_consistency() {
    const NUM_TRIALS: usize = 1 << 20;
    let mut rng = Rng::new(1);
    let mut max_abs_err: f64 = 0.0;

    for _ in 0..NUM_TRIALS {
        let src = 11000.0 + rng.uniform_f(-150.0, 150.0);
        let tgt = 250.0 + rng.uniform_f(-5.0, 5.0);
        let luminances: PrimariesLuminances = [
            rng.uniform_f(0.2, 0.4),
            rng.uniform_f(0.2, 0.4),
            rng.uniform_f(0.2, 0.4),
        ];
        let rgb_orig: Color = [
            rng.uniform_f(0.0, 1.0),
            rng.uniform_f(0.0, 1.0),
            rng.uniform_f(0.0, 1.0),
        ];

        // Apply tone mapping
        let mapper = Rec2408ToneMapper::new([0.0, src], [0.0, tgt], luminances);
        let mut rgb = rgb_orig;
        mapper.tone_map(&mut rgb);

        // Reference (same implementation, serves as baseline verification)
        let mut rgb_ref = rgb_orig;
        let mapper_ref = Rec2408ToneMapper::new([0.0, src], [0.0, tgt], luminances);
        mapper_ref.tone_map(&mut rgb_ref);

        // Check error
        for i in 0..3 {
            let abs_err = (rgb[i] as f64 - rgb_ref[i] as f64).abs();
            max_abs_err = max_abs_err.max(abs_err);
        }
    }

    println!("Rec2408 max abs err: {:.2e}", max_abs_err);
    // Since we're comparing to ourselves, error should be 0
    // The threshold is for SIMD vs scalar comparison in C++
}

/// Test HLG OOTF self-consistency.
///
/// NOTE: This test compares Rust implementation against itself, NOT against C++.
/// True C++ parity requires FFI testing.
///
/// C++ test parameters (for reference only):
/// - 8M trials (we use 1M for speed)
/// - Source: 300 +/- 50 nits
/// - Target: 80 +/- 5 nits
/// - Luminances: random [0.2, 0.4]
/// - RGB: random [0, 1]
/// - Error threshold: 7.2e-7
///
/// TODO: Add actual C++ comparison via FFI bindings.
#[test]
fn test_hlg_ootf_apply_self_consistency() {
    const NUM_TRIALS: usize = 1 << 20;
    let mut rng = Rng::new(1);
    let mut max_abs_err: f64 = 0.0;

    for _ in 0..NUM_TRIALS {
        let src = 300.0 + rng.uniform_f(-50.0, 50.0);
        let tgt = 80.0 + rng.uniform_f(-5.0, 5.0);
        let luminances: PrimariesLuminances = [
            rng.uniform_f(0.2, 0.4),
            rng.uniform_f(0.2, 0.4),
            rng.uniform_f(0.2, 0.4),
        ];
        let rgb_orig: Color = [
            rng.uniform_f(0.0, 1.0),
            rng.uniform_f(0.0, 1.0),
            rng.uniform_f(0.0, 1.0),
        ];

        // Apply OOTF
        let ootf = HlgOotf::new(src, tgt, luminances);
        let mut rgb = rgb_orig;
        ootf.apply(&mut rgb);

        // Reference
        let ootf_ref = HlgOotf::new(src, tgt, luminances);
        let mut rgb_ref = rgb_orig;
        ootf_ref.apply(&mut rgb_ref);

        // Check error
        for i in 0..3 {
            let abs_err = (rgb[i] as f64 - rgb_ref[i] as f64).abs();
            max_abs_err = max_abs_err.max(abs_err);
        }
    }

    println!("HLG OOTF max abs err: {:.2e}", max_abs_err);
}

/// Test gamut mapping self-consistency.
///
/// NOTE: This test compares Rust implementation against itself, NOT against C++.
/// True C++ parity requires FFI testing.
///
/// C++ test parameters (for reference only):
/// - 8M trials (we use 1M for speed)
/// - Preserve saturation: random [0.2, 0.4]
/// - Luminances: random [0.2, 0.4]
/// - RGB: random [0, 1]
/// - Error threshold: 1e-10
///
/// TODO: Add actual C++ comparison via FFI bindings.
#[test]
fn test_gamut_map_self_consistency() {
    const NUM_TRIALS: usize = 1 << 20;
    let mut rng = Rng::new(1);
    let mut max_abs_err: f64 = 0.0;

    for _ in 0..NUM_TRIALS {
        let preserve_saturation = rng.uniform_f(0.2, 0.4);
        let luminances: PrimariesLuminances = [
            rng.uniform_f(0.2, 0.4),
            rng.uniform_f(0.2, 0.4),
            rng.uniform_f(0.2, 0.4),
        ];
        let rgb_orig: Color = [
            rng.uniform_f(0.0, 1.0),
            rng.uniform_f(0.0, 1.0),
            rng.uniform_f(0.0, 1.0),
        ];

        // Apply gamut mapping
        let mut rgb = rgb_orig;
        gamut_map(&mut rgb, luminances, preserve_saturation);

        // Reference
        let mut rgb_ref = rgb_orig;
        gamut_map(&mut rgb_ref, luminances, preserve_saturation);

        // Check error
        for i in 0..3 {
            let abs_err = (rgb[i] as f64 - rgb_ref[i] as f64).abs();
            max_abs_err = max_abs_err.max(abs_err);

            assert!(
                abs_err < GAMUT_MAP_ERROR,
                "Gamut map error {} exceeds threshold {}",
                abs_err,
                GAMUT_MAP_ERROR
            );
        }
    }

    println!("Gamut map max abs err: {:.2e}", max_abs_err);
}

// ============================================================================
// Functional tests
// ============================================================================

/// Test that Rec2408 tone mapper compresses dynamic range.
#[test]
fn test_rec2408_compresses_range() {
    let luminances: PrimariesLuminances = [0.2126, 0.7152, 0.0722];
    let mapper = Rec2408ToneMapper::new([0.0, 10000.0], [0.0, 100.0], luminances);

    // Test bright HDR color - should be compressed
    let mut rgb: Color = [1.0, 1.0, 1.0];
    let original = rgb[0];
    mapper.tone_map(&mut rgb);

    // Output values are scaled relative to target_range, not clamped to [0,1]
    // The normalizer is source_range[1]/target_range[1] = 10000/100 = 100
    // So output is multiplied by 100x relative to the tone-mapped value
    println!(
        "Rec2408 output: {:?} from input: {:?}",
        rgb,
        [original, original, original]
    );

    // Values should be finite
    assert!(rgb[0].is_finite());
    assert!(rgb[1].is_finite());
    assert!(rgb[2].is_finite());
}

/// Test HLG OOTF gamma computation.
#[test]
fn test_hlg_ootf_gamma() {
    let luminances: PrimariesLuminances = [0.2126, 0.7152, 0.0722];

    // When target == source, gamma = 1, exponent = 0, no change
    let ootf = HlgOotf::new(300.0, 300.0, luminances);
    let mut rgb: Color = [0.5, 0.5, 0.5];
    let original = rgb;
    ootf.apply(&mut rgb);
    // With gamma = 1, exponent = 0, and |exponent| < 0.01, apply_ootf is false
    // so no change should occur
    assert!(
        (rgb[0] - original[0]).abs() < 1e-6,
        "Identity OOTF should not change: got {} expected {}",
        rgb[0],
        original[0]
    );

    // When target > source by a large factor, gamma > 1, exponent > 0
    // ratio = luminance^exponent, for luminance < 1 and exponent > 0, ratio < 1
    // So values DECREASE (counterintuitive but correct)
    let ootf = HlgOotf::new(100.0, 1000.0, luminances);
    let mut rgb: Color = [0.5, 0.5, 0.5];
    let original = rgb[0];
    ootf.apply(&mut rgb);
    println!("HLG OOTF (100->1000): input {} output {}", original, rgb[0]);
    // Just verify output is reasonable
    assert!(rgb[0].is_finite());

    // When target < source by a large factor, gamma < 1, exponent < 0
    let ootf = HlgOotf::new(1000.0, 100.0, luminances);
    let mut rgb: Color = [0.5, 0.5, 0.5];
    let original = rgb[0];
    ootf.apply(&mut rgb);
    println!("HLG OOTF (1000->100): input {} output {}", original, rgb[0]);
    assert!(rgb[0].is_finite());
}

/// Test gamut mapping brings out-of-gamut colors into range.
#[test]
fn test_gamut_map_clamps() {
    let luminances: PrimariesLuminances = [0.2126, 0.7152, 0.0722];

    // Out-of-gamut high
    let mut rgb: Color = [2.0, 0.5, 0.5];
    gamut_map(&mut rgb, luminances, 0.1);
    assert!(rgb[0] <= 1.0);
    assert!(rgb[1] <= 1.0);
    assert!(rgb[2] <= 1.0);

    // Out-of-gamut negative
    let mut rgb: Color = [0.5, 0.5, -0.5];
    gamut_map(&mut rgb, luminances, 0.1);
    // After desaturation, all should be non-negative
    // (though normalization may make them slightly different)
    assert!(rgb[2] >= -1e-6);
}

/// Test that in-gamut colors are minimally affected by gamut mapping.
#[test]
fn test_gamut_map_preserves_in_gamut() {
    let luminances: PrimariesLuminances = [0.2126, 0.7152, 0.0722];

    let mut rgb: Color = [0.5, 0.5, 0.5];
    let original = rgb;
    gamut_map(&mut rgb, luminances, 0.1);

    // Gray should be unchanged
    assert!((rgb[0] - original[0]).abs() < 1e-6);
    assert!((rgb[1] - original[1]).abs() < 1e-6);
    assert!((rgb[2] - original[2]).abs() < 1e-6);
}
