#![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,
)]
//! Comprehensive TDD Tests for PAGES Variance Changepoint Detection
//!
//! Test coverage:
//! 1. Basic functionality (initialization, stable variance)
//! 2. Variance change detection (increase, decrease)
//! 3. Edge cases (zero variance, rapid changes)
//! 4. Real market data integration (ES.FUT, NQ.FUT volatility regimes)
//! 5. Performance benchmarks (<80μs target)

use anyhow::Result;
use ml::regime::pages_test::PAGESTest;
use std::time::Instant;
use tracing::info;

// ============================================================================
// Unit Tests: Basic Functionality
// ============================================================================

#[test]
fn test_pages_default_initialization() {
    let pages = PAGESTest::default();
    assert_eq!(pages.get_target_variance(), 1.0);
    assert_eq!(pages.get_drift_allowance(), 0.5);
    assert_eq!(pages.get_detection_threshold(), 5.0);
}

#[test]
fn test_pages_custom_initialization() {
    let pages = PAGESTest::new(2.0, 1.0, 8.0, 50);
    assert_eq!(pages.get_target_variance(), 2.0);
    assert_eq!(pages.get_drift_allowance(), 1.0);
    assert_eq!(pages.get_detection_threshold(), 8.0);
}

#[test]
#[should_panic(expected = "Target variance must be positive")]
fn test_pages_negative_target_variance_panics() {
    PAGESTest::new(-1.0, 0.5, 5.0, 20);
}

#[test]
#[should_panic(expected = "Window size must be at least 2")]
fn test_pages_invalid_window_size_panics() {
    PAGESTest::new(1.0, 0.5, 5.0, 1);
}

// ============================================================================
// Unit Tests: Variance Computation
// ============================================================================

#[test]
fn test_pages_variance_computation_known_values() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    // Add values: [1, 2, 3, 4, 5]
    // Mean = 3, Variance = 2.5 (sample variance with Bessel correction)
    for val in [1.0, 2.0, 3.0, 4.0, 5.0] {
        pages.update(val)?;
    }

    let variance = pages.get_current_variance();
    let expected_variance = 2.5; // Sample variance of [1,2,3,4,5]

    assert!(
        (variance - expected_variance).abs() < 0.01,
        "Expected variance ~{}, got {}",
        expected_variance,
        variance
    );

    Ok(())
}

#[test]
fn test_pages_rolling_window_behavior() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 5);

    // Add 10 values, should keep only last 5
    for i in 1..=10 {
        pages.update(i as f64)?;
    }

    assert_eq!(pages.get_window_fill(), 5);
    assert_eq!(pages.get_update_count(), 10);

    // Variance should be computed on [6, 7, 8, 9, 10]
    // Mean = 8, Variance = 2.5
    let variance = pages.get_current_variance();
    assert!((variance - 2.5).abs() < 0.01);

    Ok(())
}

// ============================================================================
// Unit Tests: Stable Variance (No Detection)
// ============================================================================

#[test]
fn test_pages_stable_variance_no_false_alarms() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    // Generate 100 values from N(0, 1) distribution (variance = 1)
    use rand_distr::{Distribution, Normal};
    let normal = Normal::new(0.0, 1.0).unwrap();
    let mut rng = rand::thread_rng();

    for _ in 0..100 {
        let value = normal.sample(&mut rng);
        let result = pages.update(value)?;
        assert!(
            result.is_none(),
            "Should not detect change when variance is stable at target"
        );
    }

    // Cumulative sum should stay near zero with stable variance
    assert!(
        pages.get_cumulative_sum() < 2.0,
        "Cumulative sum should be low for stable variance"
    );

    Ok(())
}

#[test]
fn test_pages_zero_variance_no_crash() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    // Feed constant value (zero variance)
    for _ in 0..30 {
        let result = pages.update(5.0)?;
        assert!(
            result.is_none(),
            "Zero variance should not trigger detection"
        );
    }

    assert_eq!(pages.get_current_variance(), 0.0);
    assert_eq!(pages.get_cumulative_sum(), 0.0);

    Ok(())
}

// ============================================================================
// Unit Tests: Variance Increase Detection
// ============================================================================

#[test]
fn test_pages_variance_increase_detection_synthetic() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.25, 4.0, 20);

    // Phase 1: Stable variance ≈ 1.0 (30 samples)
    use rand_distr::{Distribution, Normal};
    let normal_stable = Normal::new(0.0, 1.0).unwrap();
    let mut rng = rand::thread_rng();

    for _ in 0..30 {
        pages.update(normal_stable.sample(&mut rng))?;
    }

    // Phase 2: Increased variance ≈ 4.0 (2x std dev)
    let normal_volatile = Normal::new(0.0, 2.0).unwrap();

    let mut detected = false;
    let mut detection_lag = 0;

    for _ in 0..50 {
        detection_lag += 1;
        let value = normal_volatile.sample(&mut rng);

        if let Some(change) = pages.update(value)? {
            detected = true;
            assert!(
                change.variance_ratio > 2.0,
                "Should detect significant variance increase (ratio > 2.0)"
            );
            assert_eq!(change.target_variance, 1.0);
            assert!(change.pages_statistic > 4.0);
            info!(detection_lag, variance_ratio = change.variance_ratio, "Detected variance increase");
            break;
        }
    }

    assert!(
        detected,
        "Should detect variance increase within 50 samples"
    );
    assert!(
        detection_lag < 30,
        "Detection lag should be reasonable (<30 samples)"
    );

    Ok(())
}

#[test]
fn test_pages_large_variance_spike() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    // Stable phase
    for i in 0..20 {
        pages.update(if i % 2 == 0 { 1.0 } else { -1.0 })?;
    }

    // Sudden large spike (10x variance increase)
    let mut detected = false;
    for i in 0..20 {
        let value = if i % 2 == 0 { 10.0 } else { -10.0 };
        if let Some(change) = pages.update(value)? {
            detected = true;
            assert!(
                change.variance_ratio > 5.0,
                "Should detect large variance spike"
            );
            break;
        }
    }

    assert!(detected, "Should quickly detect large variance spike");

    Ok(())
}

// ============================================================================
// Unit Tests: Variance Decrease Detection
// ============================================================================

#[test]
fn test_pages_variance_decrease_detection() -> Result<()> {
    // PAGES test with low target variance (monitoring for increases from low baseline)
    // For decrease detection, we need high target variance
    let mut pages = PAGESTest::new(4.0, 0.5, 5.0, 20);

    // Phase 1: High variance ≈ 4.0
    use rand_distr::{Distribution, Normal};
    let normal_volatile = Normal::new(0.0, 2.0).unwrap();
    let mut rng = rand::thread_rng();

    for _ in 0..30 {
        pages.update(normal_volatile.sample(&mut rng))?;
    }

    // Phase 2: Decreased variance ≈ 1.0
    let normal_stable = Normal::new(0.0, 1.0).unwrap();

    // For decrease detection with one-sided CUSUM, we need to invert the logic
    // or use two-sided test. For now, verify that variance does decrease
    // but may not trigger alarm (one-sided test monitors increases)

    for _ in 0..30 {
        pages.update(normal_stable.sample(&mut rng))?;
    }

    let current_var = pages.get_current_variance();
    assert!(
        current_var < 2.0,
        "Variance should have decreased from 4.0 to ~1.0"
    );

    // Note: One-sided PAGES primarily detects increases relative to target
    // For comprehensive variance monitoring, use two-sided test or separate
    // PAGES instances for increase and decrease

    Ok(())
}

// ============================================================================
// Unit Tests: Reset Functionality
// ============================================================================

#[test]
fn test_pages_reset_clears_state() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    // Accumulate state
    for i in 0..25 {
        pages.update((i as f64) * 2.0)?;
    }

    assert!(pages.get_window_fill() > 0);
    assert!(pages.get_update_count() > 0);
    assert!(pages.get_cumulative_sum() >= 0.0);

    // Reset
    pages.reset();

    // Verify all state cleared
    assert_eq!(pages.get_window_fill(), 0);
    assert_eq!(pages.get_update_count(), 0);
    assert_eq!(pages.get_cumulative_sum(), 0.0);
    assert_eq!(pages.get_current_variance(), 0.0);

    // Verify can start fresh analysis
    pages.update(1.0)?;
    assert_eq!(pages.get_update_count(), 1);

    Ok(())
}

// ============================================================================
// Unit Tests: Error Handling
// ============================================================================

#[test]
fn test_pages_rejects_nan() {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    let result = pages.update(f64::NAN);
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("non-finite"));
}

#[test]
fn test_pages_rejects_infinity() {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    let result = pages.update(f64::INFINITY);
    assert!(result.is_err());

    let result = pages.update(f64::NEG_INFINITY);
    assert!(result.is_err());
}

// ============================================================================
// Integration Tests: Real Market Data (ES.FUT, NQ.FUT)
// ============================================================================

#[test]
#[ignore = "Requires DBN test data files"]
fn test_pages_es_fut_volatility_regimes() -> Result<()> {
    // This test validates PAGES test on real ES.FUT data
    // Expected: Detect regime changes during market open/close, news events

    // Load ES.FUT data (implementation depends on available test data)
    // let bars = load_dbn_test_data("ES.FUT")?;

    // Initialize PAGES with parameters tuned for ES.FUT
    // ES.FUT typical intraday variance: ~1.0-2.0 points²
    let mut pages = PAGESTest::new(1.5, 0.5, 5.0, 20);

    // Simulate ES.FUT price returns (replace with real data when available)
    let simulated_returns = vec![
        // Low volatility period (09:30-10:00)
        0.1, -0.05, 0.08, -0.06, 0.04, 0.03, -0.02, 0.05, -0.03, 0.06,
        // High volatility spike (10:00-10:30, news event)
        0.8, -0.6, 0.9, -0.7, 0.85, 0.75, -0.65, 0.8, -0.5, 0.7,
    ];

    let mut detections = Vec::new();

    for (idx, &ret) in simulated_returns.iter().enumerate() {
        if let Some(change) = pages.update(ret)? {
            detections.push((idx, change));
            info!(idx, variance_ratio = change.variance_ratio, pages_statistic = change.pages_statistic, "ES.FUT variance change");
        }
    }

    // Should detect volatility spike
    assert!(
        !detections.is_empty(),
        "Should detect volatility regime change in ES.FUT"
    );

    // First detection should be during high volatility period (indices 10+)
    assert!(
        detections[0].0 >= 10,
        "Should detect change during high volatility period"
    );

    Ok(())
}

#[test]
#[ignore = "Requires DBN test data files"]
fn test_pages_nq_fut_market_open_volatility() -> Result<()> {
    // NQ.FUT typically shows volatility spike at market open (09:30 ET)
    let mut pages = PAGESTest::new(2.0, 0.5, 5.0, 20);

    // Simulate pre-market (low vol) → market open (high vol)
    let simulated_returns = vec![
        // Pre-market: low volatility
        0.05, -0.03, 0.04, -0.02, 0.03, 0.02, -0.01, 0.03, -0.02, 0.04,
        // Market open: volatility surge
        1.5, -1.2, 1.8, -1.4, 1.6, 1.3, -1.1, 1.4, -0.9, 1.2,
    ];

    let mut detected = false;

    for (idx, &ret) in simulated_returns.iter().enumerate() {
        if let Some(change) = pages.update(ret)? {
            detected = true;
            assert!(idx >= 10, "Should detect change during market open period");
            assert!(
                change.variance_ratio > 2.0,
                "Market open should show significant variance increase"
            );
            break;
        }
    }

    assert!(detected, "Should detect market open volatility spike");

    Ok(())
}

// ============================================================================
// Performance Benchmarks
// ============================================================================

#[test]
fn test_pages_performance_latency() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    // Warmup
    for i in 0..100 {
        pages.update(i as f64)?;
    }

    // Benchmark 1000 updates
    let iterations = 1000;
    let start = Instant::now();

    for i in 0..iterations {
        pages.update((i as f64) * 0.1)?;
    }

    let duration = start.elapsed();
    let avg_latency_us = duration.as_micros() as f64 / iterations as f64;

    info!(avg_latency_us, "PAGES update latency (us per update, target <80)");

    assert!(
        avg_latency_us < 80.0,
        "PAGES update latency {:.2}μs exceeds 80μs target",
        avg_latency_us
    );

    Ok(())
}

#[test]
fn test_pages_memory_efficiency() {
    // PAGES should use minimal memory (VecDeque + running stats)
    let pages = PAGESTest::new(1.0, 0.5, 5.0, 50);

    let size = std::mem::size_of_val(&pages);
    info!(size, "PAGESTest struct size (bytes)");

    // VecDeque overhead + running stats should be < 1KB even with window=50
    assert!(
        size < 1024,
        "PAGESTest memory usage {} bytes exceeds 1KB",
        size
    );
}

// ============================================================================
// Property-Based Tests
// ============================================================================

#[test]
fn test_pages_cumulative_sum_non_negative() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    use rand_distr::{Distribution, Normal};
    let normal = Normal::new(0.0, 1.5).unwrap();
    let mut rng = rand::thread_rng();

    for _ in 0..100 {
        pages.update(normal.sample(&mut rng))?;

        // Page's statistic must always be non-negative (max with 0)
        assert!(
            pages.get_cumulative_sum() >= 0.0,
            "Cumulative sum should never be negative"
        );
    }

    Ok(())
}

#[test]
fn test_pages_variance_always_non_negative() -> Result<()> {
    let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

    for i in -50..50 {
        pages.update(i as f64)?;

        let variance = pages.get_current_variance();
        assert!(
            variance >= 0.0,
            "Variance should never be negative, got {}",
            variance
        );
    }

    Ok(())
}
