#![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,
)]
//! Huber Loss Tests
//!
//! Test suite for Huber loss implementation in DQN.
//! Huber loss = MSE for small errors, L1 for large errors (robust to outliers).
//!
//! Formula:
//! ```
//! L(x) = {
//!     0.5 * x²                    if |x| <= delta
//!     delta * (|x| - 0.5 * delta) otherwise
//! }
//! ```

use anyhow::Result;
use tracing::info;

/// Huber loss: quadratic for small errors, linear for large errors
/// More robust to outliers than MSE
///
/// This is a host-side implementation operating on plain f32 slices.
/// Suitable for correctness tests (not a GPU hot path).
fn huber_loss(predictions: &[f32], targets: &[f32], delta: f32) -> Result<f32> {
    assert_eq!(
        predictions.len(),
        targets.len(),
        "predictions and targets must have the same length"
    );
    assert!(!predictions.is_empty(), "inputs must be non-empty");

    let mut total_loss = 0.0_f32;
    for (pred, tgt) in predictions.iter().zip(targets.iter()) {
        let error = pred - tgt;
        let abs_error = error.abs();
        let loss = if abs_error <= delta {
            // Quadratic region
            0.5 * error * error
        } else {
            // Linear region
            delta * (abs_error - 0.5 * delta)
        };
        total_loss += loss;
    }

    // Return mean loss
    Ok(total_loss / predictions.len() as f32)
}

#[test]
fn test_huber_small_error_quadratic() -> Result<()> {
    // Test 1: Small error (0.5, delta=1.0) -> quadratic behavior
    // Expected: 0.5 * 0.5^2 = 0.5 * 0.25 = 0.125
    let predictions = [1.5_f32];
    let targets = [1.0_f32];
    let delta = 1.0;

    let loss_value = huber_loss(&predictions, &targets, delta)?;

    let expected = 0.125_f32;
    assert!(
        (loss_value - expected).abs() < 1e-5,
        "Small error loss incorrect: got {}, expected {}",
        loss_value,
        expected
    );

    info!(loss_value, "Test 1 passed: Small error (0.5) quadratic behavior");
    Ok(())
}

#[test]
fn test_huber_large_error_linear() -> Result<()> {
    // Test 2: Large error (5.0, delta=1.0) -> linear behavior
    // Expected: 1.0 * (5.0 - 0.5 * 1.0) = 1.0 * 4.5 = 4.5
    let predictions = [6.0_f32];
    let targets = [1.0_f32];
    let delta = 1.0;

    let loss_value = huber_loss(&predictions, &targets, delta)?;

    let expected = 4.5_f32;
    assert!(
        (loss_value - expected).abs() < 1e-5,
        "Large error loss incorrect: got {}, expected {}",
        loss_value,
        expected
    );

    info!(loss_value, "Test 2 passed: Large error (5.0) linear behavior");
    Ok(())
}

#[test]
fn test_huber_threshold_smooth_transition() -> Result<()> {
    // Test 3: Error at threshold (1.0, delta=1.0) -> smooth transition
    // Quadratic: 0.5 * 1.0^2 = 0.5
    // Linear: 1.0 * (1.0 - 0.5 * 1.0) = 1.0 * 0.5 = 0.5
    // Both formulas should give same result at threshold
    let predictions = [2.0_f32];
    let targets = [1.0_f32];
    let delta = 1.0;

    let loss_value = huber_loss(&predictions, &targets, delta)?;

    let expected = 0.5_f32;
    assert!(
        (loss_value - expected).abs() < 1e-5,
        "Threshold error loss incorrect: got {}, expected {}",
        loss_value,
        expected
    );

    info!(loss_value, "Test 3 passed: Error at threshold (1.0) smooth transition");
    Ok(())
}

#[test]
fn test_huber_negative_errors() -> Result<()> {
    // Test 4: Negative errors handled correctly (symmetry)
    // Error of -5.0 should give same loss as +5.0

    // Positive error
    let pred_pos = [6.0_f32];
    let target_pos = [1.0_f32];
    let loss_pos_value = huber_loss(&pred_pos, &target_pos, 1.0)?;

    // Negative error
    let pred_neg = [-4.0_f32];
    let target_neg = [1.0_f32];
    let loss_neg_value = huber_loss(&pred_neg, &target_neg, 1.0)?;

    assert!(
        (loss_pos_value - loss_neg_value).abs() < 1e-5,
        "Negative error loss asymmetric: pos={}, neg={}",
        loss_pos_value,
        loss_neg_value
    );

    info!(loss_pos_value, loss_neg_value, "Test 4 passed: Negative errors handled correctly");
    Ok(())
}

#[test]
fn test_huber_batch_mixed_errors() -> Result<()> {
    // Test 5: Batch of errors (mixed small/large)
    // Errors: [0.5, 2.0, 5.0, 0.1] with delta=1.0
    // Expected losses:
    //   0.5: 0.5 * 0.5^2 = 0.125 (quadratic)
    //   2.0: 1.0 * (2.0 - 0.5) = 1.5 (linear)
    //   5.0: 1.0 * (5.0 - 0.5) = 4.5 (linear)
    //   0.1: 0.5 * 0.1^2 = 0.005 (quadratic)
    // Average: (0.125 + 1.5 + 4.5 + 0.005) / 4 = 6.13 / 4 = 1.5325
    let predictions = [1.5_f32, 3.0, 6.0, 1.1];
    let targets = [1.0_f32, 1.0, 1.0, 1.0];
    let delta = 1.0;

    let loss_value = huber_loss(&predictions, &targets, delta)?;

    let expected = 1.5325_f32;
    assert!(
        (loss_value - expected).abs() < 1e-3,
        "Batch loss incorrect: got {}, expected {}",
        loss_value,
        expected
    );

    info!(loss_value, "Test 5 passed: Batch of mixed errors average loss");
    Ok(())
}

#[test]
fn test_huber_gradient_bounded() -> Result<()> {
    // Test 6: Gradient is bounded for large errors
    // For MSE: gradient = 2 * error (unbounded, grows linearly)
    // For Huber: gradient = delta for |error| > delta (bounded)
    //
    // Error of 100.0 with delta=1.0:
    // MSE gradient would be 200 (2 * 100)
    // Huber gradient is bounded by delta=1.0
    //
    // We verify this by checking that large errors don't cause
    // disproportionately large losses (which would indicate large gradients)

    // Small outlier: error=10
    let pred_small = [11.0_f32];
    let target_small = [1.0_f32];
    let loss_small_value = huber_loss(&pred_small, &target_small, 1.0)?;

    // Large outlier: error=100
    let pred_large = [101.0_f32];
    let target_large = [1.0_f32];
    let loss_large_value = huber_loss(&pred_large, &target_large, 1.0)?;

    // Huber loss should grow linearly with error size (not quadratically)
    // loss_large / loss_small should be approximately 100/10 = 10 (linear growth)
    // For MSE it would be (100^2)/(10^2) = 100 (quadratic growth)
    let ratio = loss_large_value / loss_small_value;

    assert!(
        ratio > 8.0 && ratio < 12.0,
        "Gradient not bounded: loss ratio {} (expected ~10 for linear growth, ~100 for quadratic)",
        ratio
    );

    info!(ratio, "Test 6 passed: Gradient bounded for large errors, linear growth confirmed");
    Ok(())
}

#[test]
fn test_huber_vs_mse_convergence() -> Result<()> {
    // Test 7: Huber converges faster than MSE on outlier data
    // Simulate training data with outliers: [1.0, 1.1, 0.9, 10.0, 1.05]
    // The outlier (10.0) should have less influence on Huber loss
    let predictions = [2.0_f32, 2.1, 1.9, 11.0, 2.05];
    let targets = [1.0_f32, 1.1, 0.9, 10.0, 1.05];
    let delta = 1.0;

    // Huber loss
    let huber_value = huber_loss(&predictions, &targets, delta)?;

    // MSE loss (computed manually)
    let mse_value: f32 = predictions
        .iter()
        .zip(targets.iter())
        .map(|(p, t)| (p - t) * (p - t))
        .sum::<f32>()
        / predictions.len() as f32;

    // Huber should be significantly smaller than MSE due to outlier robustness
    // The outlier (error=1.0) contributes:
    //   MSE: 1.0^2 = 1.0
    //   Huber: 1.0 * (1.0 - 0.5) = 0.5
    // So Huber should be approximately half of MSE
    assert!(
        huber_value < mse_value,
        "Huber loss should be smaller than MSE for outlier data: huber={}, mse={}",
        huber_value,
        mse_value
    );

    let reduction = (mse_value - huber_value) / mse_value * 100.0;
    info!(mse_value, huber_value, reduction, "Test 7 passed: Huber converges better on outliers");
    Ok(())
}

#[test]
fn test_huber_different_deltas() -> Result<()> {
    // Additional test: Different delta values affect transition point
    let predictions = [3.0_f32];
    let targets = [1.0_f32]; // Error = 2.0

    // With delta=1.0: error=2.0 is large (linear)
    let loss1 = huber_loss(&predictions, &targets, 1.0)?;

    // With delta=3.0: error=2.0 is small (quadratic)
    let loss3 = huber_loss(&predictions, &targets, 3.0)?;

    // delta=1.0: 1.0 * (2.0 - 0.5 * 1.0) = 1.5 (linear)
    // delta=3.0: 0.5 * 2.0^2 = 2.0 (quadratic)
    assert!(
        (loss1 - 1.5).abs() < 1e-5,
        "Delta=1.0 loss incorrect: got {}, expected 1.5",
        loss1
    );
    assert!(
        (loss3 - 2.0).abs() < 1e-5,
        "Delta=3.0 loss incorrect: got {}, expected 2.0",
        loss3
    );

    info!(loss_delta1 = loss1, loss_delta3 = loss3, "Additional test passed: Different deltas work correctly");
    Ok(())
}
