#![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,
)]
//! Multi-CUSUM Integration Tests
//!
//! Tests multi-feature structural break detection with:
//! - Unit tests for all detection modes
//! - Real Databento market data (ES.FUT)
//! - Performance benchmarks
//! - Edge case validation

use ml::regime::multi_cusum::{CUSUMConfig, DetectionMode, MultiCUSUM};

// ============================================================================
// Unit Tests
// ============================================================================

#[test]
fn test_multi_cusum_any_mode_single_feature_trigger() {
    // Test: ANY mode should detect when any single feature triggers
    let configs = vec![
        CUSUMConfig {
            threshold: 4.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 4.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 4.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
    ];
    let weights = vec![0.5, 0.3, 0.2];
    let mut detector = MultiCUSUM::new(configs, weights, DetectionMode::Any).unwrap();

    // Stable phase
    for i in 0..50 {
        let features = vec![0.0, 0.0, 0.0];
        assert!(detector.update(&features, i).is_none());
    }

    // Trigger only first feature
    let mut detected = false;
    for i in 50..100 {
        let features = vec![0.05, 0.0, 0.0]; // Only first breaks (5 std devs)
        if let Some(multi_break) = detector.update(&features, i) {
            assert_eq!(multi_break.triggered_features.len(), 1);
            assert_eq!(multi_break.triggered_features[0], 0);
            assert_eq!(multi_break.detection_score, 1.0);
            detected = true;
            break;
        }
    }

    assert!(
        detected,
        "ANY mode should detect with single feature trigger"
    );
}

#[test]
fn test_multi_cusum_all_mode_requires_all_features() {
    // Test: ALL mode should require all features to trigger
    let configs = vec![
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
    ];
    let weights = vec![0.5, 0.5];
    let mut detector = MultiCUSUM::new(configs, weights, DetectionMode::All).unwrap();

    // Stable phase
    for i in 0..50 {
        let features = vec![0.0, 0.0];
        assert!(detector.update(&features, i).is_none());
    }

    // Trigger only first feature (should NOT detect)
    for i in 50..80 {
        let features = vec![0.05, 0.0];
        assert!(
            detector.update(&features, i).is_none(),
            "ALL mode should not detect with partial triggers"
        );
    }

    // Trigger both features
    let mut detected = false;
    for i in 80..150 {
        let features = vec![0.05, 0.05];
        if let Some(multi_break) = detector.update(&features, i) {
            assert_eq!(multi_break.triggered_features.len(), 2);
            assert_eq!(multi_break.detection_score, 1.0);
            detected = true;
            break;
        }
    }

    assert!(detected, "ALL mode should detect when all features trigger");
}

#[test]
fn test_multi_cusum_weighted_vote_threshold() {
    // Test: WEIGHTED_VOTE mode with importance-based threshold
    let configs = vec![
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
    ];
    let weights = vec![0.5, 0.3, 0.2]; // Returns > Volatility > Volume
    let mode = DetectionMode::WeightedVote { threshold: 0.6 };
    let mut detector = MultiCUSUM::new(configs, weights, mode).unwrap();

    // Stable data
    for i in 0..50 {
        let features = vec![0.0, 0.0, 0.0];
        assert!(detector.update(&features, i).is_none());
    }

    // Trigger only volume (weight=0.2, below 0.6 threshold)
    for i in 50..80 {
        let features = vec![0.0, 0.0, 0.05];
        assert!(
            detector.update(&features, i).is_none(),
            "Score 0.2 < 0.6 threshold"
        );
    }

    // Trigger returns + volatility (0.5 + 0.3 = 0.8 > 0.6)
    let mut detected = false;
    for i in 80..150 {
        let features = vec![0.05, 0.05, 0.0];
        if let Some(multi_break) = detector.update(&features, i) {
            assert!(multi_break.detection_score >= 0.6);
            assert!(multi_break.detection_score <= 1.0);
            assert_eq!(multi_break.triggered_features.len(), 2);
            detected = true;
            break;
        }
    }

    assert!(
        detected,
        "Weighted vote should detect when score >= threshold"
    );
}

#[test]
fn test_multi_cusum_different_thresholds_per_feature() {
    // Test: Different sensitivity per feature (different thresholds)
    let configs = vec![
        CUSUMConfig {
            threshold: 5.0, // Less sensitive (higher threshold)
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 2.0, // More sensitive (lower threshold)
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
    ];
    let weights = vec![0.5, 0.5];
    let mut detector = MultiCUSUM::new(configs, weights, DetectionMode::Any).unwrap();

    // Moderate shift should trigger second feature only
    let mut detected = false;
    for i in 0..100 {
        let features = vec![0.02, 0.02]; // 2 std devs - only triggers feature 1
        if let Some(multi_break) = detector.update(&features, i) {
            assert_eq!(multi_break.triggered_features, vec![1]);
            detected = true;
            break;
        }
    }

    assert!(detected, "More sensitive feature should trigger first");
}

#[test]
fn test_multi_cusum_upward_and_downward_breaks() {
    // Test: Detect both upward and downward structural breaks
    let configs = vec![
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
    ];
    let weights = vec![0.5, 0.5];
    let mut detector = MultiCUSUM::new(configs, weights, DetectionMode::Any).unwrap();

    // Upward break
    let mut upward_detected = false;
    for i in 0..100 {
        let features = vec![0.05, 0.0];
        if detector.update(&features, i).is_some() {
            upward_detected = true;
            break;
        }
    }
    assert!(upward_detected);

    // Downward break (after reset)
    let mut downward_detected = false;
    for i in 100..200 {
        let features = vec![-0.05, 0.0];
        if detector.update(&features, i).is_some() {
            downward_detected = true;
            break;
        }
    }
    assert!(downward_detected);
}

#[test]
fn test_multi_cusum_feature_status_tracking() {
    // Test: Track status of all features
    let configs = vec![
        CUSUMConfig {
            threshold: 4.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 4.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 4.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
    ];
    let weights = vec![0.4, 0.35, 0.25];
    let mut detector = MultiCUSUM::new(configs, weights, DetectionMode::Any).unwrap();

    // Process data
    for i in 0..100 {
        let features = vec![0.0, 0.0, 0.0];
        detector.update(&features, i);
    }

    // Check all statuses
    let statuses = detector.get_feature_statuses();
    assert_eq!(statuses.len(), 3);

    for status in &statuses {
        assert_eq!(status.total_bars, 100);
        assert!(status.bars_since_reset <= 100);
        assert!(status.last_break.is_none());
    }
}

#[test]
fn test_multi_cusum_baseline_update() {
    // Test: Update baseline for adaptive detection
    let configs = vec![
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
    ];
    let weights = vec![0.6, 0.4];
    let mut detector = MultiCUSUM::new(configs, weights, DetectionMode::Any).unwrap();

    // Update baseline for first feature
    detector.update_feature_baseline(0, 0.05, 0.02);

    // New baseline means previous "break" values are now normal
    for i in 0..50 {
        let features = vec![0.05, 0.0]; // Now aligned with new baseline
        assert!(detector.update(&features, i).is_none());
    }
}

#[test]
fn test_multi_cusum_zero_features_rejection() {
    // Test: Reject empty feature configuration
    let configs = Vec::new();
    let weights = Vec::new();
    let result = MultiCUSUM::new(configs, weights, DetectionMode::Any);
    assert!(result.is_err());
    assert!(result.unwrap_err().contains("At least one feature"));
}

#[test]
fn test_multi_cusum_weight_sum_validation() {
    // Test: Weights must sum to 1.0
    let configs = vec![
        CUSUMConfig::default(),
        CUSUMConfig::default(),
        CUSUMConfig::default(),
    ];

    // Weights sum to 0.9 (invalid)
    let bad_weights = vec![0.3, 0.3, 0.3];
    let result = MultiCUSUM::new(configs.clone(), bad_weights, DetectionMode::Any);
    assert!(result.is_err());

    // Weights sum to 1.0 (valid)
    let good_weights = vec![0.4, 0.35, 0.25];
    let result = MultiCUSUM::new(configs, good_weights, DetectionMode::Any);
    assert!(result.is_ok());
}

#[test]
fn test_multi_cusum_negative_weight_rejection() {
    // Test: Negative weights are rejected
    let configs = vec![CUSUMConfig::default(), CUSUMConfig::default()];
    let bad_weights = vec![0.7, -0.3]; // Sum to 0.4, but negative weight
    let result = MultiCUSUM::new(configs, bad_weights, DetectionMode::Any);
    assert!(result.is_err());
    assert!(result.unwrap_err().contains("non-negative"));
}

#[test]
fn test_multi_cusum_performance_benchmark() {
    // Test: Verify <100μs per update for 3-5 features
    use std::time::Instant;

    let configs = vec![
        CUSUMConfig {
            threshold: 4.0,
            baseline_mean: 0.0,
            baseline_std: 0.01,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 3.5,
            baseline_mean: 0.015,
            baseline_std: 0.005,
            min_bars_between: 10,
        },
        CUSUMConfig {
            threshold: 3.0,
            baseline_mean: 100000.0,
            baseline_std: 50000.0,
            min_bars_between: 10,
        },
    ];
    let weights = vec![0.5, 0.3, 0.2];
    let mut detector = MultiCUSUM::new(configs, weights, DetectionMode::Any).unwrap();

    let n_iterations = 1000;
    let start = Instant::now();

    for i in 0..n_iterations {
        let features = vec![
            (i as f64) * 0.0001,
            0.015 + (i as f64) * 0.00001,
            100000.0 + (i as f64) * 10.0,
        ];
        detector.update(&features, i);
    }

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

    use tracing::info;
    info!(avg_latency_us, n_iterations, "Multi-CUSUM average latency (us per update)");
    assert!(
        avg_latency_us < 100.0,
        "Performance target: <100μs per update (got {:.2}μs)",
        avg_latency_us
    );
}
