#![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,
)]
//! TDD Tests for Volatility-Based Epsilon Adaptation
//!
//! Tests for dynamic epsilon adjustment based on market volatility:
//! - Low volatility (σ < 0.01): epsilon × 0.5 (exploit more, explore less)
//! - Medium volatility (0.01 ≤ σ ≤ 0.05): epsilon × 1.0 (no adjustment)
//! - High volatility (σ > 0.05): epsilon × 2.0 (explore more, exploit less)
//!
//! Volatility is calculated as rolling 20-period standard deviation of returns.
//! Final epsilon is always clamped to [0.05, 0.95].

#[cfg(test)]
mod volatility_epsilon_tests {
    use approx::assert_abs_diff_eq;
    use tracing::info;

    /// Calculate rolling standard deviation of returns
    /// Returns the standard deviation of the last `window` returns
    ///
    /// # Arguments
    /// * `returns` - Historical returns (typically log returns)
    /// * `window` - Rolling window size (default: 20)
    ///
    /// # Returns
    /// Standard deviation of returns in the window, or 0.0 if insufficient data
    fn calculate_returns_volatility(returns: &[f64], window: usize) -> f64 {
        if returns.len() < window {
            return 0.0;
        }

        let recent_returns = &returns[returns.len() - window..];
        let mean = recent_returns.iter().sum::<f64>() / window as f64;
        let variance = recent_returns
            .iter()
            .map(|r| (r - mean).powi(2))
            .sum::<f64>()
            / window as f64;

        variance.sqrt()
    }

    /// Calculate volatility-adjusted epsilon
    ///
    /// # Arguments
    /// * `base_epsilon` - Base exploration rate (before volatility adjustment)
    /// * `volatility` - Market volatility (standard deviation of returns)
    ///
    /// # Returns
    /// Adjusted epsilon, clamped to [0.05, 0.95]
    fn calculate_volatility_adjusted_epsilon(base_epsilon: f64, volatility: f64) -> f64 {
        let multiplier = if volatility < 0.01 {
            0.5 // Low volatility: exploit more
        } else if volatility > 0.05 {
            2.0 // High volatility: explore more
        } else {
            // Linear interpolation for medium volatility: 0.01 ≤ σ ≤ 0.05
            // At σ=0.01: m=0.5, at σ=0.05: m=2.0
            // m(σ) = 0.5 + (σ - 0.01) / 0.04 × 1.5
            0.5 + (volatility - 0.01) / 0.04 * 1.5
        };

        (base_epsilon * multiplier).clamp(0.05, 0.95)
    }

    /// Convert prices to log returns
    ///
    /// # Arguments
    /// * `prices` - Historical prices
    ///
    /// # Returns
    /// Vector of log returns (ln(price[t] / price[t-1]))
    fn prices_to_log_returns(prices: &[f64]) -> Vec<f64> {
        prices
            .windows(2)
            .map(|w| (w[1] / w[0]).ln())
            .collect()
    }

    // ============================================================================
    // TEST 1: Low Volatility Regime
    // ============================================================================
    #[test]
    fn test_epsilon_low_volatility_regime() {
        // Scenario: Stable market, very low returns volatility (σ < 0.01)
        // Expected: Exploit more (epsilon × 0.5)

        let base_epsilon = 0.5;
        let volatility = 0.005; // σ = 0.5% (very low)

        let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);

        // Expected: 0.5 × 0.5 = 0.25
        assert_abs_diff_eq!(adjusted_epsilon, 0.25, epsilon = 1e-6);

        info!(vol_pct = volatility * 100.0, base_epsilon, adjusted_epsilon, "Low vol epsilon (exploit boost)");
    }

    // ============================================================================
    // TEST 2: High Volatility Regime
    // ============================================================================
    #[test]
    fn test_epsilon_high_volatility_regime() {
        // Scenario: Volatile market, high returns volatility (σ > 0.05)
        // Expected: Explore more (epsilon × 2.0)

        let base_epsilon = 0.5;
        let volatility = 0.08; // σ = 8.0% (high)

        let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);

        // Expected: 0.5 × 2.0 = 1.0, clamped to 0.95
        assert_abs_diff_eq!(adjusted_epsilon, 0.95, epsilon = 1e-6);

        info!(vol_pct = volatility * 100.0, base_epsilon, adjusted_epsilon, "High vol epsilon (exploration boost, clamped)");
    }

    // ============================================================================
    // TEST 3: Medium Volatility Regime
    // ============================================================================
    #[test]
    fn test_epsilon_medium_volatility() {
        // Scenario: Normal market, medium returns volatility (0.01 ≤ σ ≤ 0.05)
        // Expected: No adjustment (epsilon × 1.0)

        let base_epsilon = 0.5;
        let volatility = 0.02; // σ = 2.0% (medium)

        let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);

        // Linear interpolation: m = 0.5 + (0.02 - 0.01) / 0.04 × 1.5 = 0.5 + 0.375 = 0.875
        // ε = 0.5 × 0.875 = 0.4375
        let expected = 0.4375;
        assert_abs_diff_eq!(adjusted_epsilon, expected, epsilon = 1e-6);

        info!(vol_pct = volatility * 100.0, base_epsilon, adjusted_epsilon, "Medium vol epsilon (interpolated)");
    }

    // ============================================================================
    // TEST 4: Volatility Calculation with Rolling Window
    // ============================================================================
    #[test]
    fn test_volatility_calculation_rolling_window() {
        // Scenario: Calculate volatility from 25 price points, use 20-period window
        // Expected: Rolling std dev matches manual calculation

        // Create price series with known volatility
        let prices = vec![
            100.0, 101.0, 100.5, 102.0, 101.5, 103.0, 102.5, 104.0, 103.5, 105.0, 104.5, 106.0,
            105.5, 107.0, 106.5, 108.0, 107.5, 109.0, 108.5, 110.0, 109.5, 111.0, 110.5, 112.0,
            111.5,
        ];

        let returns = prices_to_log_returns(&prices);
        let volatility = calculate_returns_volatility(&returns, 20);

        // Volatility should be positive and reasonable
        assert!(volatility > 0.0, "Volatility should be positive");
        assert!(volatility < 0.05, "Volatility should be less than 5%");

        info!(volatility, "Rolling window (20 periods) volatility");
    }

    // ============================================================================
    // TEST 5: Epsilon Clamping to [0.05, 0.95]
    // ============================================================================
    #[test]
    fn test_epsilon_clamping() {
        // Test case 1: Very low epsilon (0.1) in low volatility regime
        // Would normally be 0.1 × 0.5 = 0.05 (exactly at floor)
        let result1 = calculate_volatility_adjusted_epsilon(0.1, 0.005);
        assert_abs_diff_eq!(result1, 0.05, epsilon = 1e-6);

        // Test case 2: Very low epsilon (0.05) in high volatility regime
        // Would normally be 0.05 × 2.0 = 0.1 (above floor, below cap)
        let result2 = calculate_volatility_adjusted_epsilon(0.05, 0.08);
        assert_abs_diff_eq!(result2, 0.1, epsilon = 1e-6);

        // Test case 3: Very high epsilon (1.0) in high volatility regime
        // Would normally be 1.0 × 2.0 = 2.0 (clamped to 0.95)
        let result3 = calculate_volatility_adjusted_epsilon(1.0, 0.08);
        assert_abs_diff_eq!(result3, 0.95, epsilon = 1e-6);

        // Test case 4: Edge case - epsilon at 0.95 in high volatility
        // Would normally be 0.95 × 2.0 = 1.9 (clamped to 0.95)
        let result4 = calculate_volatility_adjusted_epsilon(0.95, 0.08);
        assert_abs_diff_eq!(result4, 0.95, epsilon = 1e-6);

        info!(
            low_vol_result = result1,
            low_eps_high_vol_result = result2,
            high_eps_high_vol_result = result3,
            at_cap_high_vol_result = result4,
            "Epsilon clamping [0.05, 0.95]"
        );
    }

    // ============================================================================
    // TEST 6: Volatility Regime Transitions (Smooth vs. Jarring)
    // ============================================================================
    #[test]
    fn test_volatility_regime_transitions() {
        // Scenario: Simulate market transitioning from low to high volatility
        // Expected: Smooth epsilon adjustment (no jumps)

        let base_epsilon = 0.5;
        let volatilities = vec![
            0.005, 0.006, 0.007, 0.008, 0.009, 0.010, 0.015, 0.020, 0.025, 0.030, 0.035, 0.040,
            0.045, 0.050, 0.055, 0.060, 0.070, 0.080,
        ];

        let mut previous_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatilities[0]);
        let mut max_jump = 0.0;

        info!("Volatility regime transitions (smooth adaptation)");

        for vol in &volatilities {
            let adjusted = calculate_volatility_adjusted_epsilon(base_epsilon, *vol);
            let jump = (adjusted - previous_epsilon).abs();

            if jump > max_jump {
                max_jump = jump;
            }

            info!(vol_pct = vol * 100.0, adjusted_epsilon = adjusted, delta_epsilon = jump, "Volatility transition step");

            previous_epsilon = adjusted;
        }

        // Max jump should be reasonable (< 0.1 between consecutive points)
        // Worst case transition is from vol=0.045 (m≈1.375, ε≈0.6875) to vol=0.050 (m=2.0, ε=0.95)
        // Jump = |0.95 - 0.6875| ≈ 0.2625
        assert!(
            max_jump <= 0.30,
            "Maximum epsilon jump should be ≤0.30, got {:.4}",
            max_jump
        );

        info!(max_jump, "Maximum epsilon jump");
    }

    // ============================================================================
    // TEST 7: Insufficient History (< 20 samples)
    // ============================================================================
    #[test]
    fn test_insufficient_history() {
        // Scenario: Early in training with < 20 price observations
        // Expected: Use base epsilon (volatility = 0.0 → multiplier = 1.0)

        let base_epsilon = 0.5;

        // Simulate insufficient returns history
        let returns = vec![0.005, -0.003, 0.002, -0.001]; // Only 4 returns
        let volatility = calculate_returns_volatility(&returns, 20);

        assert_eq!(
            volatility, 0.0,
            "Volatility should be 0 for insufficient history"
        );

        let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);

        // With vol=0, multiplier should be between 0.5 and 1.0 (actually at 0.01 boundary)
        // But since vol=0 < 0.01, multiplier = 0.5
        assert_abs_diff_eq!(adjusted_epsilon, 0.25, epsilon = 1e-6);

        info!(base_epsilon, adjusted_epsilon, "Insufficient history (4 samples) epsilon");
    }

    // ============================================================================
    // TEST 8: Volatility Outlier Handling
    // ============================================================================
    #[test]
    fn test_volatility_outlier_handling() {
        // Scenario: Price data with occasional extreme movements (flash crashes, gaps)
        // Expected: Rolling std dev captures outliers but isn't destroyed by them

        // Normal prices with one outlier
        let prices = vec![
            100.0, 100.5, 101.0, 100.5, 101.0, 100.5, 101.0, 100.5, 101.0, 100.5, 101.0, 100.5,
            101.0, 100.5, 101.0, 100.5, 101.0, 100.5, 101.0, 85.0, // Flash crash
            95.0, 100.0, 100.5, 101.0, 100.5,
        ];

        let returns = prices_to_log_returns(&prices);
        let volatility = calculate_returns_volatility(&returns, 20);

        // Volatility should be elevated but not infinite
        assert!(volatility > 0.01, "Outlier should increase volatility");
        assert!(volatility < 1.0, "Volatility should remain bounded");

        let adjusted_epsilon = calculate_volatility_adjusted_epsilon(0.5, volatility);

        // With elevated volatility, epsilon should be boosted
        assert!(
            adjusted_epsilon > 0.5,
            "Elevated volatility should boost epsilon"
        );

        info!(volatility, adjusted_epsilon, "Outlier handling epsilon");
    }

    // ============================================================================
    // TEST 9: Volatility Logging (Every 100 Steps)
    // ============================================================================
    #[test]
    fn test_volatility_logging() {
        // Scenario: Track volatility regime over multiple epochs
        // Expected: Log regime changes at regular intervals (every 100 steps)

        let mut step_count = 0;
        let log_interval = 100;
        let base_epsilon = 0.5;

        // Simulate 5 epochs with different volatility patterns
        let volatilities = vec![
            vec![0.005; 100], // Epoch 1: Low volatility (100 steps)
            vec![0.025; 100], // Epoch 2: Medium volatility
            vec![0.075; 100], // Epoch 3: High volatility
            vec![0.015; 100], // Epoch 4: Back to low
            vec![0.040; 100], // Epoch 5: Medium-high
        ];

        info!(log_interval, "Volatility logging");

        for (epoch, vol_series) in volatilities.iter().enumerate() {
            for vol in vol_series {
                if step_count % log_interval == 0 {
                    let adjusted = calculate_volatility_adjusted_epsilon(base_epsilon, *vol);

                    let regime = if vol < &0.01 {
                        "Low (exploit)"
                    } else if vol > &0.05 {
                        "High (explore)"
                    } else {
                        "Medium (normal)"
                    };

                    info!(epoch = epoch + 1, step = step_count, vol_pct = vol * 100.0, regime, adjusted_epsilon = adjusted, "Volatility log step");
                }
                step_count += 1;
            }
        }

        // Verify we logged approximately 5 times (one per epoch)
        assert!(step_count > 400, "Should have simulated >400 steps");
        info!(regime_changes = 5, steps = 500, "Logged regime changes");
    }

    // ============================================================================
    // TEST 10: Epsilon-Volatility Correlation
    // ============================================================================
    #[test]
    fn test_epsilon_correlation_with_vol() {
        // Scenario: Verify positive correlation between volatility and adjusted epsilon
        // Expected: As volatility increases, epsilon increases

        let base_epsilon = 0.5;
        let volatilities = vec![
            0.005, 0.01, 0.015, 0.02, 0.025, 0.03, 0.04, 0.05, 0.06, 0.08, 0.10,
        ];

        let mut previous_epsilon = 0.0;
        let mut correlation_count = 0;

        info!("Epsilon-volatility correlation check");

        for vol in &volatilities {
            let adjusted = calculate_volatility_adjusted_epsilon(base_epsilon, *vol);
            let delta = adjusted - previous_epsilon;

            let is_increasing = if delta > -0.001 { "✓" } else { "✗" };

            if adjusted >= previous_epsilon {
                correlation_count += 1;
            }

            info!(vol_pct = vol * 100.0, adjusted_epsilon = adjusted, delta, is_increasing, "Epsilon-vol correlation step");

            previous_epsilon = adjusted;
        }

        // Out of 10 transitions, nearly all should be monotonically increasing
        // (Small decreases at boundaries are acceptable due to clamping)
        assert!(
            correlation_count >= 9,
            "Epsilon should increase with volatility ({})",
            correlation_count
        );

        info!(increasing_transitions = correlation_count, total = 10, "Positive correlation confirmed");
    }

    // ============================================================================
    // TEST 11: Boundary Cases
    // ============================================================================
    #[test]
    fn test_boundary_cases() {
        // Edge case 1: Exactly at low/high volatility boundary
        let vol_low_boundary = 0.01;
        let vol_high_boundary = 0.05;

        let eps1 = calculate_volatility_adjusted_epsilon(0.5, vol_low_boundary);
        let eps2 = calculate_volatility_adjusted_epsilon(0.5, vol_high_boundary);

        info!(low_boundary_eps = eps1, high_boundary_eps = eps2, "Boundary cases");

        // At σ=0.01, we're at the transition point
        // m = 0.5 + (0.01 - 0.01) / 0.04 × 1.5 = 0.5
        // ε = 0.5 × 0.5 = 0.25
        assert_abs_diff_eq!(eps1, 0.25, epsilon = 1e-6);

        // At σ=0.05, we're at the transition point
        // m = 0.5 + (0.05 - 0.01) / 0.04 × 1.5 = 0.5 + 1.5 = 2.0
        // ε = 0.5 × 2.0 = 1.0 → clamped to 0.95
        assert_abs_diff_eq!(eps2, 0.95, epsilon = 1e-6);

        // Edge case 2: Zero epsilon (exploration disabled)
        let eps_zero = calculate_volatility_adjusted_epsilon(0.0, 0.05);
        assert_abs_diff_eq!(eps_zero, 0.05, epsilon = 1e-6); // Clamped to floor

        // Edge case 3: Very small non-zero epsilon
        let eps_tiny = calculate_volatility_adjusted_epsilon(0.001, 0.08);
        assert_abs_diff_eq!(eps_tiny, 0.05, epsilon = 1e-6); // Clamped to floor

        info!("All boundary cases validated");
    }

    // ============================================================================
    // TEST 12: Long-Term Volatility Stability
    // ============================================================================
    #[test]
    fn test_long_term_volatility_stability() {
        // Scenario: Simulate 1000 steps with fluctuating volatility
        // Expected: Rolling window prevents extreme oscillations

        use rand::Rng;
        let mut rng = rand::thread_rng();

        let base_epsilon = 0.5;
        let mut prices = vec![100.0];
        let mut epsilon_values = Vec::new();

        // Generate 1000 steps of price data with random volatility regimes
        for step in 0..1000 {
            let regime_switch = step % 200; // Change regime every 200 steps
            let volatility_target = match regime_switch / 100 {
                0 => 0.008, // Low volatility
                _ => 0.060, // High volatility
            };

            // Add price with controlled randomness
            let return_shock = rng.gen_range(-1.0..1.0) * volatility_target;
            let new_price = prices.last().unwrap() * (1.0 + return_shock);
            prices.push(new_price);

            if prices.len() > 20 {
                let returns = prices_to_log_returns(&prices);
                let volatility = calculate_returns_volatility(&returns, 20);
                let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);
                epsilon_values.push(adjusted_epsilon);
            }
        }

        // Calculate statistics
        let mean_epsilon = epsilon_values.iter().sum::<f64>() / epsilon_values.len() as f64;
        let variance = epsilon_values
            .iter()
            .map(|e| (e - mean_epsilon).powi(2))
            .sum::<f64>()
            / epsilon_values.len() as f64;
        let std_dev = variance.sqrt();

        // Over 980 steps, epsilon should be relatively stable
        // Mean should be somewhere between regimes (0.25 - 0.95)
        assert!(mean_epsilon > 0.20, "Mean epsilon should be > 0.20");
        assert!(mean_epsilon < 1.0, "Mean epsilon should be < 1.0");

        // Std dev should be reasonable (not wildly oscillating)
        assert!(std_dev < 0.3, "Epsilon std dev should be < 0.3, got {:.4}", std_dev);

        info!(
            mean_epsilon,
            std_dev,
            min_epsilon = epsilon_values.iter().cloned().fold(f64::INFINITY, f64::min),
            max_epsilon = epsilon_values.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
            "Long-term stability (1000 steps)"
        );
    }
}
