#![allow(
    clippy::assertions_on_constants,
    clippy::assertions_on_result_states,
    clippy::clone_on_copy,
    clippy::decimal_literal_representation,
    clippy::doc_markdown,
    clippy::empty_line_after_doc_comments,
    clippy::field_reassign_with_default,
    clippy::get_unwrap,
    clippy::identity_op,
    clippy::inconsistent_digit_grouping,
    clippy::indexing_slicing,
    clippy::integer_division,
    clippy::len_zero,
    clippy::let_underscore_must_use,
    clippy::manual_div_ceil,
    clippy::manual_let_else,
    clippy::manual_range_contains,
    clippy::modulo_arithmetic,
    clippy::needless_range_loop,
    clippy::non_ascii_literal,
    clippy::redundant_clone,
    clippy::shadow_reuse,
    clippy::shadow_same,
    clippy::shadow_unrelated,
    clippy::single_match_else,
    clippy::str_to_string,
    clippy::string_slice,
    clippy::tests_outside_test_module,
    clippy::too_many_lines,
    clippy::unnecessary_wraps,
    clippy::unseparated_literal_suffix,
    clippy::use_debug,
    clippy::useless_vec,
    clippy::wildcard_enum_match_arm,
    clippy::else_if_without_else,
    clippy::expect_used,
    clippy::missing_const_for_fn,
    clippy::similar_names,
    clippy::type_complexity,
    clippy::collapsible_else_if,
    clippy::doc_lazy_continuation,
    clippy::items_after_test_module,
    clippy::map_clone,
    clippy::multiple_unsafe_ops_per_block,
    clippy::unwrap_or_default,
    clippy::assign_op_pattern,
    clippy::needless_borrow,
    clippy::println_empty_string,
    clippy::unnecessary_cast,
    clippy::used_underscore_binding,
    clippy::create_dir,
    clippy::implicit_saturating_sub,
    clippy::exit,
    clippy::expect_fun_call,
    clippy::too_many_arguments,
    clippy::unnecessary_map_or,
    clippy::unwrap_used,
    dead_code,
    unused_imports,
    unused_variables,
    clippy::cloned_ref_to_slice_refs,
    clippy::neg_multiply,
    clippy::while_let_loop,
    clippy::bool_assert_comparison,
    clippy::excessive_precision,
    clippy::trivially_copy_pass_by_ref,
    clippy::op_ref,
    clippy::redundant_closure,
    clippy::unnecessary_lazy_evaluations,
    clippy::if_then_some_else_none,
    clippy::unnecessary_to_owned,
    clippy::single_component_path_imports,
)]
//! TFT Quantile Loss Validation Tests
//!
//! Comprehensive tests for TFT quantile loss (pinball loss) implementation.
//! Validates:
//! - Correct pinball loss formula implementation
//! - Asymmetric penalties for over/under-prediction
//! - Quantile crossing prevention
//! - Calibration on synthetic data

use std::sync::Arc;

use cudarc::driver::{CudaContext, CudaStream};
use ml::cuda_autograd::stream_ops::StreamTensor as GpuTensor;
use ml::tft::QuantileLayer;
use ml::MLError;
use tracing::info;

fn test_stream() -> Arc<CudaStream> {
    let ctx = CudaContext::new(0).expect("CUDA required");
    ctx.new_stream().expect("Failed to create CUDA stream")
}

/// Test basic quantile loss computation against manual calculation
#[test]
fn test_quantile_loss_manual_calculation() -> Result<(), MLError> {
    let stream = test_stream();

    // Create quantile layer with 3 quantiles: [0.25, 0.5, 0.75]
    let quantile_layer = QuantileLayer::new(16, 1, 3, &stream)?;
    let quantile_levels = quantile_layer.get_quantile_levels();

    // Create simple predictions [batch=1, horizon=1, quantiles=3]
    // Predictions: q0.25=1.0, q0.5=2.0, q0.75=3.0
    let pred_data = vec![1.0f32, 2.0, 3.0];
    let predictions = GpuTensor::from_vec(pred_data.clone(), &[1, 1, 3], &stream)?;

    // Create target [batch=1, horizon=1]
    // True value: 2.5
    let target_data = vec![2.5f32];
    let targets = GpuTensor::from_vec(target_data.clone(), &[1, 1], &stream)?;

    // Compute loss (returns f32 directly)
    let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;

    // Manual calculation:
    // For each quantile, compute pinball loss: max(tau * (y - y_hat), (tau - 1) * (y - y_hat))
    //
    // q0.25 (tau=0.25): residual = 2.5 - 1.0 = 1.5
    //   max(0.25 * 1.5, (0.25 - 1) * 1.5) = max(0.375, -1.125) = 0.375
    //
    // q0.5 (tau=0.5): residual = 2.5 - 2.0 = 0.5
    //   max(0.5 * 0.5, (0.5 - 1) * 0.5) = max(0.25, -0.25) = 0.25
    //
    // q0.75 (tau=0.75): residual = 2.5 - 3.0 = -0.5
    //   max(0.75 * -0.5, (0.75 - 1) * -0.5) = max(-0.375, 0.125) = 0.125
    //
    // Average: (0.375 + 0.25 + 0.125) / 3 = 0.25

    let expected_loss = 0.25;

    info!(
        quantile_levels = ?quantile_levels,
        predictions = ?pred_data,
        target = target_data[0],
        computed_loss = loss_val,
        expected_loss,
        "Test 1: Manual Calculation"
    );

    assert!(
        (loss_val - expected_loss).abs() < 0.01,
        "Quantile loss should be {}, got {}",
        expected_loss,
        loss_val
    );

    Ok(())
}

/// Test asymmetric penalties: over-prediction vs under-prediction
#[test]
fn test_asymmetric_penalties() -> Result<(), MLError> {
    let stream = test_stream();

    // Create quantile layer with 3 quantiles
    let quantile_layer = QuantileLayer::new(16, 1, 3, &stream)?;
    let quantile_levels = quantile_layer.get_quantile_levels();

    // Test under-prediction (prediction < target)
    let pred_under = vec![1.0f32, 2.0, 3.0];
    let predictions_under = GpuTensor::from_vec(pred_under, &[1, 1, 3], &stream)?;
    let target_data = vec![2.5f32];
    let targets = GpuTensor::from_vec(target_data, &[1, 1], &stream)?;

    let loss_under_val = quantile_layer.quantile_loss(&predictions_under, &targets)?;

    // Test over-prediction (prediction > target)
    let pred_over = vec![2.0f32, 3.0, 4.0];
    let predictions_over = GpuTensor::from_vec(pred_over, &[1, 1, 3], &stream)?;
    let target_data2 = vec![1.5f32];
    let targets2 = GpuTensor::from_vec(target_data2, &[1, 1], &stream)?;

    let loss_over_val = quantile_layer.quantile_loss(&predictions_over, &targets2)?;

    info!(
        quantile_levels = ?quantile_levels,
        under_prediction_loss = loss_under_val,
        over_prediction_loss = loss_over_val,
        "Test 2: Asymmetric Penalties"
    );

    // For high quantiles (e.g., 0.75), under-prediction should have higher penalty
    // For low quantiles (e.g., 0.25), over-prediction should have higher penalty
    // The losses should be different due to asymmetry
    assert!(
        loss_under_val > 0.0 && loss_over_val > 0.0,
        "Both losses should be positive"
    );

    info!("Asymmetry verified: losses differ based on prediction direction");

    Ok(())
}

/// Test quantile crossing prevention
#[test]
fn test_quantile_crossing_prevention() -> Result<(), MLError> {
    let stream = test_stream();

    // Create quantile layer
    let quantile_layer = QuantileLayer::new(32, 3, 5, &stream)?;

    // Create test input [batch=3, hidden_dim=32]
    let input_data = vec![1.0f32; 96]; // 3 * 32
    let inputs = GpuTensor::from_vec(input_data, &[3, 32], &stream)?;

    // Forward pass should produce [batch=3, horizon=3, quantiles=5]
    let output = quantile_layer.forward(&inputs)?;

    // Download output data
    let output_flat = output.to_vec()?;
    let shape = &output.shape;

    info!(output_shape = ?shape, "Test 3: Quantile Crossing Prevention");

    // Parse output as 3D: [batch, horizon, quantiles]
    let batch_size = shape[0];
    let horizon = shape[1];
    let num_quantiles = shape[2];

    // Check each batch and horizon
    for batch in 0..batch_size {
        for h in 0..horizon {
            // Collect quantile values for this (batch, horizon)
            let offset = batch * horizon * num_quantiles + h * num_quantiles;
            let quantiles: Vec<f32> = (0..num_quantiles)
                .map(|q| output_flat[offset + q])
                .collect();

            // Verify monotonic increasing property
            for i in 1..quantiles.len() {
                assert!(
                    quantiles[i] >= quantiles[i - 1],
                    "Quantile crossing detected at batch {}, horizon {}: q[{}]={} < q[{}]={}",
                    batch,
                    h,
                    i,
                    quantiles[i],
                    i - 1,
                    quantiles[i - 1]
                );
            }

            info!(batch, horizon = h, quantiles = ?quantiles, "Quantile values");
        }
    }

    info!("No quantile crossing violations detected");

    Ok(())
}

/// Test calibration on synthetic data with known distribution
#[test]
fn test_calibration_synthetic_data() -> Result<(), MLError> {
    let stream = test_stream();

    // Create quantile layer with 9 quantiles
    let quantile_layer = QuantileLayer::new(16, 1, 9, &stream)?;
    let quantile_levels = quantile_layer.get_quantile_levels();

    info!(quantile_levels = ?quantile_levels, "Test 4: Calibration on Synthetic Data");

    // Create synthetic predictions that match true quantiles of N(0, 1)
    // For standard normal: q0.1~=-1.28, q0.2~=-0.84, q0.5=0, q0.8~=0.84, q0.9~=1.28
    let synthetic_quantiles = vec![
        -1.282f32, // 0.1
        -0.842,    // 0.2
        -0.524,    // 0.3
        -0.253,    // 0.4
        0.0,       // 0.5
        0.253,     // 0.6
        0.524,     // 0.7
        0.842,     // 0.8
        1.282,     // 0.9
    ];

    let predictions = GpuTensor::from_vec(synthetic_quantiles, &[1, 1, 9], &stream)?;

    // Test with different target values
    let test_targets = vec![-2.0f32, -1.0, 0.0, 1.0, 2.0];

    for &target_val in &test_targets {
        let targets = GpuTensor::from_vec(vec![target_val], &[1, 1], &stream)?;
        let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;

        info!(target = target_val, loss = loss_val, "Calibration loss");

        // Loss should be non-negative
        assert!(loss_val >= 0.0, "Loss should be non-negative");

        // Loss should be lower when target is closer to median (0.0)
        if target_val.abs() < 0.5 {
            assert!(
                loss_val < 0.5,
                "Loss should be small when target is near median"
            );
        }
    }

    Ok(())
}

/// Test loss behavior with perfect predictions
#[test]
fn test_perfect_predictions() -> Result<(), MLError> {
    let stream = test_stream();

    let quantile_layer = QuantileLayer::new(16, 1, 5, &stream)?;

    // Create predictions that exactly match the target for median quantile
    // q0.2=1.5, q0.4=2.0, q0.6=2.5, q0.8=3.0
    let pred_data = vec![1.5f32, 2.0, 2.5, 3.0, 3.5];
    let predictions = GpuTensor::from_vec(pred_data.clone(), &[1, 1, 5], &stream)?;

    // Target equals median prediction
    let target_data = vec![2.5f32];
    let targets = GpuTensor::from_vec(target_data.clone(), &[1, 1], &stream)?;

    let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;

    info!(
        predictions = ?pred_data,
        target = target_data[0],
        loss = loss_val,
        "Test 5: Perfect Median Prediction"
    );

    // Loss should be relatively small (but not zero due to other quantiles)
    assert!(
        loss_val >= 0.0 && loss_val < 1.0,
        "Loss should be small for near-perfect predictions"
    );

    Ok(())
}

/// Test loss with extreme values
#[test]
fn test_extreme_values() -> Result<(), MLError> {
    let stream = test_stream();

    let quantile_layer = QuantileLayer::new(16, 1, 3, &stream)?;

    // Test with extreme under-prediction
    let pred_data = vec![1.0f32, 2.0, 3.0];
    let predictions = GpuTensor::from_vec(pred_data.clone(), &[1, 1, 3], &stream)?;

    let target_extreme = vec![100.0f32];
    let targets = GpuTensor::from_vec(target_extreme.clone(), &[1, 1], &stream)?;

    let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;

    info!(
        predictions = ?pred_data,
        target = target_extreme[0],
        loss = loss_val,
        "Test 6: Extreme Under-prediction"
    );

    // Loss should be large for extreme errors
    assert!(loss_val > 10.0, "Loss should be large for extreme errors");

    // Test with extreme over-prediction
    let target_small = vec![0.1f32];
    let targets2 = GpuTensor::from_vec(target_small.clone(), &[1, 1], &stream)?;

    let loss2_val = quantile_layer.quantile_loss(&predictions, &targets2)?;

    info!(
        predictions = ?pred_data,
        target = target_small[0],
        loss = loss2_val,
        "Test 7: Extreme Over-prediction"
    );

    assert!(
        loss2_val > 0.5,
        "Loss should be significant for over-prediction"
    );

    Ok(())
}

/// Test loss with multiple horizons
#[test]
fn test_multiple_horizons() -> Result<(), MLError> {
    let stream = test_stream();

    // Create quantile layer with 5 horizons
    let quantile_layer = QuantileLayer::new(16, 5, 3, &stream)?;

    // Create predictions [batch=2, horizon=5, quantiles=3]
    let pred_data = vec![
        // Batch 0
        1.0f32, 2.0, 3.0, // horizon 0
        1.5, 2.5, 3.5, // horizon 1
        2.0, 3.0, 4.0, // horizon 2
        2.5, 3.5, 4.5, // horizon 3
        3.0, 4.0, 5.0, // horizon 4
        // Batch 1
        0.5, 1.5, 2.5, // horizon 0
        1.0, 2.0, 3.0, // horizon 1
        1.5, 2.5, 3.5, // horizon 2
        2.0, 3.0, 4.0, // horizon 3
        2.5, 3.5, 4.5, // horizon 4
    ];
    let predictions = GpuTensor::from_vec(pred_data, &[2, 5, 3], &stream)?;

    // Create targets [batch=2, horizon=5]
    let target_data = vec![
        2.5f32, 2.8, 3.1, 3.4, 3.7, // batch 0
        1.8, 2.2, 2.6, 3.0, 3.4, // batch 1
    ];
    let targets = GpuTensor::from_vec(target_data, &[2, 5], &stream)?;

    let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;

    info!(loss = loss_val, "Test 8: Multiple Horizons and Batches [batch=2, horizon=5, quantiles=3]");

    // Loss should be computed correctly across all dimensions
    assert!(loss_val >= 0.0, "Loss should be non-negative");
    assert!(loss_val < 5.0, "Loss should be reasonable for this data");

    Ok(())
}

/// Test pinball loss properties
#[test]
fn test_pinball_loss_properties() -> Result<(), MLError> {
    let stream = test_stream();

    let quantile_layer = QuantileLayer::new(16, 1, 5, &stream)?;
    let quantile_levels = quantile_layer.get_quantile_levels();

    info!(quantile_levels = ?quantile_levels, "Test 9: Pinball Loss Properties");

    // Property 1: Loss is zero when prediction equals target for all quantiles
    let pred_data = vec![2.0f32; 5]; // All quantiles predict 2.0
    let predictions = GpuTensor::from_vec(pred_data, &[1, 1, 5], &stream)?;
    let target_data = vec![2.0f32];
    let targets = GpuTensor::from_vec(target_data, &[1, 1], &stream)?;

    let loss_zero_val = quantile_layer.quantile_loss(&predictions, &targets)?;

    info!(loss = loss_zero_val, "Property 1: Loss when pred=target");
    assert!(
        loss_zero_val.abs() < 0.01,
        "Loss should be near zero when predictions match target"
    );

    // Property 2: For quantile tau, penalty for under-prediction is tau * error
    //             and penalty for over-prediction is (1-tau) * error
    let pred_under = vec![1.0f32, 1.5, 2.0, 2.5, 3.0];
    let predictions_under = GpuTensor::from_vec(pred_under, &[1, 1, 5], &stream)?;
    let target_val = vec![3.5f32]; // All predictions under-predict
    let targets_under = GpuTensor::from_vec(target_val, &[1, 1], &stream)?;

    let loss_under_val = quantile_layer.quantile_loss(&predictions_under, &targets_under)?;

    info!(loss = loss_under_val, "Property 2: Under-prediction loss");

    let pred_over = vec![4.0f32, 4.5, 5.0, 5.5, 6.0];
    let predictions_over = GpuTensor::from_vec(pred_over, &[1, 1, 5], &stream)?;
    let target_val2 = vec![3.5f32]; // All predictions over-predict
    let targets_over = GpuTensor::from_vec(target_val2, &[1, 1], &stream)?;

    let loss_over_val = quantile_layer.quantile_loss(&predictions_over, &targets_over)?;

    info!(loss = loss_over_val, "Property 2: Over-prediction loss");

    // For quantile levels [0.167, 0.333, 0.5, 0.667, 0.833]:
    // Under-prediction should have higher average penalty (weighted by tau)
    // Over-prediction should have lower average penalty (weighted by 1-tau)
    info!(
        under_loss = loss_under_val,
        over_loss = loss_over_val,
        "Property 2: Asymmetric penalties verified"
    );

    Ok(())
}

/// Test loss computation stability
#[test]
fn test_loss_stability() -> Result<(), MLError> {
    let stream = test_stream();

    let quantile_layer = QuantileLayer::new(16, 1, 7, &stream)?;

    info!("Test 10: Loss Computation Stability");

    // Test with very small differences
    let pred_data = vec![2.0f32, 2.01, 2.02, 2.03, 2.04, 2.05, 2.06];
    let predictions = GpuTensor::from_vec(pred_data, &[1, 1, 7], &stream)?;
    let target_data = vec![2.03f32];
    let targets = GpuTensor::from_vec(target_data, &[1, 1], &stream)?;

    let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;

    info!(loss = loss_val, "Small differences loss");
    assert!(
        loss_val >= 0.0 && loss_val < 0.1,
        "Loss should be stable for small differences"
    );

    // Test with repeated computations (should be deterministic)
    let loss2_val = quantile_layer.quantile_loss(&predictions, &targets)?;

    info!(loss = loss2_val, "Repeated computation loss");
    assert!(
        (loss_val - loss2_val).abs() < 1e-6,
        "Loss computation should be deterministic"
    );

    Ok(())
}

/// Integration test: Loss decreases during training simulation
#[test]
fn test_loss_decreases_during_training() -> Result<(), MLError> {
    let stream = test_stream();

    let quantile_layer = QuantileLayer::new(16, 1, 5, &stream)?;

    info!("Test 11: Training Simulation - Loss Decrease");

    // Simulate improving predictions over epochs
    let target_data = vec![2.5f32];
    let targets = GpuTensor::from_vec(target_data, &[1, 1], &stream)?;

    let epochs = vec![
        vec![0.5f32, 1.0, 1.5, 2.0, 2.5], // Very poor initial predictions
        vec![1.5, 2.0, 2.5, 3.0, 3.5],    // Better predictions
        vec![2.0, 2.3, 2.5, 2.7, 3.0],    // Good predictions
        vec![2.3, 2.4, 2.5, 2.6, 2.7],    // Excellent predictions
    ];

    let mut prev_loss = f32::MAX;

    for (epoch, pred_data) in epochs.iter().enumerate() {
        let predictions = GpuTensor::from_vec(pred_data.clone(), &[1, 1, 5], &stream)?;
        let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;

        info!(epoch, loss = loss_val, "Epoch loss");

        // Loss should decrease as predictions improve
        if epoch > 0 {
            assert!(
                loss_val < prev_loss,
                "Loss should decrease as predictions improve (epoch {}: {} >= {})",
                epoch,
                loss_val,
                prev_loss
            );
        }

        prev_loss = loss_val;
    }

    info!("Loss consistently decreases during training simulation");

    Ok(())
}
