// FILE-SIZE-OK: 3329 lines — consolidated test suite extracted from interbar_math.rs
//! Integration tests for inter-bar math functions
//!
//! Extracted from interbar_math.rs to reduce file size (was 5,324 lines, 63% tests).

use opendeviationbar_core::FixedPoint;
use opendeviationbar_core::interbar_math::*;

#[cfg(test)]
mod approximate_entropy_tests {
    use super::*;

    #[test]
    fn test_apen_deterministic_series() {
        // Perfectly regular series should have low entropy
        let series: Vec<f64> = (0..100).map(|i| (i as f64) * 1.0).collect();
        let apen = compute_approximate_entropy(&series, 2, 0.1);
        println!("Deterministic series ApEn: {:.4}", apen);
        assert!(apen < 0.5, "Regular series should have low entropy");
    }

    #[test]
    fn test_apen_random_series() {
        // Issue #96: Use real market data with appropriate tolerance
        // r should be ~0.2 * std_dev of the series (Pincus 1991 guideline)
        let trades = opendeviationbar_core::test_data_loader::load_real_btcusdt_10k().unwrap();
        let series: Vec<f64> = trades.iter().take(500).map(|t| t.price.to_f64()).collect();
        let mean = series.iter().sum::<f64>() / series.len() as f64;
        let variance = series.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / series.len() as f64;
        let r = 0.2 * variance.sqrt();
        let apen = compute_approximate_entropy(&series, 2, r);
        println!("Real BTCUSDT ApEn (500 trades, r={:.4}): {:.4}", r, apen);
        assert!(
            apen >= 0.0 && apen <= 1.0,
            "ApEn must be in [0,1], got {}",
            apen
        );
    }

    #[test]
    fn test_apen_short_series() {
        // Too short series should return 0
        let series = vec![1.0, 2.0];
        let apen = compute_approximate_entropy(&series, 2, 0.5);
        assert_eq!(apen, 0.0, "Too-short series should return 0");
    }

    #[test]
    fn test_adaptive_entropy_switches_at_threshold() {
        // Create series that will use different methods based on size
        let small_series: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
        let large_series: Vec<f64> = (0..1000).map(|i| i as f64 * 0.01).collect();

        let ent_small = compute_entropy_adaptive(&small_series);
        let ent_large = compute_entropy_adaptive(&large_series);

        println!("Small series entropy (n=100): {:.4}", ent_small);
        println!("Large series entropy (n=1000): {:.4}", ent_large);

        // Both should be valid [0, 1]
        assert!(ent_small >= 0.0 && ent_small <= 1.0);
        assert!(ent_large >= 0.0 && ent_large <= 1.0);
    }
}

#[cfg(test)]
mod entropy_adaptive_apen_tests {
    use super::*;

    #[test]
    fn test_adaptive_entropy_boundary_499_uses_pe() {
        // n=499 should use Permutation Entropy path (n < 500)
        let prices: Vec<f64> = (0..499).map(|i| 100.0 + (i as f64 * 0.01)).collect();
        let ent = compute_entropy_adaptive(&prices);
        assert!(
            ent >= 0.0 && ent <= 1.0,
            "PE result should be in [0, 1], got {ent}"
        );
    }

    #[test]
    fn test_adaptive_entropy_boundary_500_uses_apen() {
        // n=500 should use ApEn path (n >= 500)
        let prices: Vec<f64> = (0..500).map(|i| 100.0 + (i as f64 * 0.01)).collect();
        let ent = compute_entropy_adaptive(&prices);
        assert!(
            ent >= 0.0 && ent <= 1.0,
            "ApEn result should be in [0, 1], got {ent}"
        );
    }

    #[test]
    fn test_adaptive_entropy_large_trending() {
        // Large trending series (n=1000) - should have low entropy (ordered)
        let prices: Vec<f64> = (0..1000).map(|i| 100.0 + i as f64).collect();
        let ent = compute_entropy_adaptive(&prices);
        assert!(ent >= 0.0 && ent <= 1.0, "Entropy out of bounds: {ent}");
        assert!(
            ent < 0.5,
            "Trending series should have low entropy, got {ent}"
        );
    }

    #[test]
    fn test_adaptive_entropy_large_random() {
        // Large pseudo-random series (n=600) - should have moderate-high entropy
        let mut rng = 42u64;
        let prices: Vec<f64> = (0..600)
            .map(|_| {
                rng = rng
                    .wrapping_mul(6364136223846793005)
                    .wrapping_add(1442695040888963407);
                100.0 + ((rng >> 33) as f64 / (1u64 << 31) as f64) * 50.0
            })
            .collect();
        let ent = compute_entropy_adaptive(&prices);
        assert!(ent >= 0.0 && ent <= 1.0, "Entropy out of bounds: {ent}");
    }

    #[test]
    fn test_adaptive_entropy_cached_large_window_not_cached() {
        // ApEn path (n>=500) does NOT use the entropy cache
        // Verify that cached and uncached produce the same result
        let prices: Vec<f64> = (0..600).map(|i| 100.0 + (i as f64).sin() * 10.0).collect();
        let uncached = compute_entropy_adaptive(&prices);
        let mut cache = EntropyCache::new();
        let cached = compute_entropy_adaptive_cached(&prices, &mut cache);
        assert!(
            (uncached - cached).abs() < 1e-10,
            "Cached and uncached should match for large windows: {uncached} vs {cached}"
        );
    }

    #[test]
    fn test_adaptive_entropy_constant_price_large() {
        // All same price (n=500) - ApEn should produce low entropy
        let prices: Vec<f64> = vec![100.0; 500];
        let ent = compute_entropy_adaptive(&prices);
        assert!(ent >= 0.0 && ent <= 1.0, "Entropy out of bounds: {ent}");
    }
}

#[cfg(test)]
mod hurst_accuracy_tests {
    use super::*;

    #[test]
    fn test_hurst_accuracy_trending() {
        // Strongly trending series (H > 0.5)
        let mut prices = vec![0.0; 256];
        for i in 0..256 {
            prices[i] = i as f64 * 1.0; // Linear trend
        }

        let dfa_h = compute_hurst_rescaled_range(&prices);
        let rs_h = opendeviationbar_hurst::rssimple(&prices);

        println!("Trending series:");
        println!("  DFA H = {:.4}", dfa_h);
        println!("  R/S H = {:.4}", rs_h);
        println!("  Both > 0.5? DFA={}, RS={}", dfa_h > 0.5, rs_h > 0.5);

        // Both should agree on trending direction (H > 0.5)
        assert!(dfa_h > 0.5, "DFA should detect trending");
        assert!(rs_h > 0.5, "R/S should detect trending");
    }

    #[test]
    fn test_hurst_accuracy_mean_reverting() {
        // Mean-reverting series (H < 0.5)
        let mut prices = vec![0.5; 256];
        for i in 0..256 {
            prices[i] = if i % 2 == 0 { 0.0 } else { 1.0 };
        }

        let dfa_h = compute_hurst_rescaled_range(&prices);
        let rs_h = opendeviationbar_hurst::rssimple(&prices);

        println!("Mean-reverting series:");
        println!("  DFA H = {:.4}", dfa_h);
        println!("  R/S H = {:.4}", rs_h);
        println!("  Both < 0.5? DFA={}, RS={}", dfa_h < 0.5, rs_h < 0.5);

        // Both should agree on mean-reversion (H < 0.5)
        assert!(dfa_h < 0.5, "DFA should detect mean-reversion");
        assert!(rs_h < 0.5, "R/S should detect mean-reversion");
    }

    #[test]
    fn test_hurst_accuracy_random_walk() {
        // Brownian motion / random walk (H ≈ 0.5)
        let mut prices = vec![0.0; 256];
        let mut rng = 12345u64;
        prices[0] = 0.0;

        for i in 1..256 {
            rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
            let step = if (rng >> 16) & 1 == 0 { 1.0 } else { -1.0 };
            prices[i] = prices[i - 1] + step;
        }

        let dfa_h = compute_hurst_rescaled_range(&prices);
        let rs_h = opendeviationbar_hurst::rssimple(&prices);

        println!("Random walk series:");
        println!("  DFA H = {:.4}", dfa_h);
        println!("  R/S H = {:.4}", rs_h);
        println!("  Both ≈ 0.5? DFA={:.2}, RS={:.2}", dfa_h, rs_h);
    }

    // Edge case tests for inter-bar features (Issue #96: Test expansion)
    // Validates robustness on boundary conditions and stress scenarios

    #[test]
    fn test_hurst_edge_case_empty() {
        let prices: Vec<f64> = vec![];
        let h = compute_hurst_rescaled_range(&prices);
        assert_eq!(h, 0.5, "Empty prices should return neutral (0.5)");
    }

    #[test]
    fn test_hurst_edge_case_insufficient_samples() {
        // Less than MIN_SAMPLES (64) should return neutral
        let prices: Vec<f64> = (0..32).map(|i| 100.0 + i as f64).collect();
        let h = compute_hurst_rescaled_range(&prices);
        assert_eq!(h, 0.5, "Less than 64 samples should return neutral (0.5)");
    }

    #[test]
    fn test_hurst_edge_case_constant_prices() {
        // All same price should handle gracefully (no variation)
        // With R/S analysis, constant series results in NaN (0/0 case)
        let prices = vec![100.0; 100];
        let h = compute_hurst_rescaled_range(&prices);
        // Constant prices may result in NaN after soft clamping, which is acceptable
        // The important thing is no panic/crash
        if !h.is_nan() {
            assert!(h >= 0.0 && h <= 1.0, "Hurst should be in [0,1] if not NaN");
        }
    }

    #[test]
    fn test_hurst_bounds_stress() {
        // Verify Hurst stays bounded across diverse scenarios
        let scenarios = vec![
            (
                "linear",
                (0..256).map(|i| 100.0 + i as f64).collect::<Vec<_>>(),
            ),
            (
                "sawtooth",
                (0..256)
                    .map(|i| if i % 2 == 0 { 100.0 } else { 101.0 })
                    .collect::<Vec<_>>(),
            ),
        ];

        for (name, prices) in scenarios {
            let h = compute_hurst_rescaled_range(&prices);
            assert!(
                h >= 0.0 && h <= 1.0,
                "Hurst({}) must be in [0,1], got {}",
                name,
                h
            );
            assert!(!h.is_nan(), "Hurst({}) must not be NaN", name);
        }
    }

    #[test]
    fn test_garman_klass_edge_case_empty() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;

        // Empty lookback should return 0
        let snapshot: Vec<TradeSnapshot> = vec![];
        let snapshot_refs: Vec<&TradeSnapshot> = snapshot.iter().collect();
        let vol = compute_garman_klass(&snapshot_refs);
        assert_eq!(vol, 0.0, "Empty lookback should return 0");
    }

    #[test]
    fn test_garman_klass_edge_case_constant_price() {
        use opendeviationbar_core::{FixedPoint, interbar_types::TradeSnapshot};

        // All same price: H=L, C=O, variance should be 0
        let prices = vec![100.0; 50];
        let snapshots: Vec<TradeSnapshot> = prices
            .iter()
            .enumerate()
            .map(|(i, &price)| {
                let price_fp = FixedPoint::from_str(&format!("{:.8}", price)).expect("valid price");
                let vol_fp = FixedPoint::from_str("1.00000000").expect("valid volume");
                let turnover_f64 = price_fp.to_f64() * vol_fp.to_f64();
                TradeSnapshot {
                    price: price_fp,
                    volume: vol_fp,
                    timestamp: 1000 + (i as i64 * 100),
                    is_buyer_maker: false,
                    turnover: (turnover_f64 * 1e8) as i128,
                }
            })
            .collect();
        let snapshot_refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
        let vol = compute_garman_klass(&snapshot_refs);
        assert_eq!(vol, 0.0, "Constant price should give 0 volatility");
    }

    #[test]
    fn test_garman_klass_bounds() {
        use opendeviationbar_core::{FixedPoint, interbar_types::TradeSnapshot};

        // Garman-Klass should be non-negative
        let prices = vec![100.0, 105.0, 103.0, 108.0, 102.0];
        let snapshots: Vec<TradeSnapshot> = prices
            .iter()
            .enumerate()
            .map(|(i, &price)| {
                let price_fp = FixedPoint::from_str(&format!("{:.8}", price)).expect("valid price");
                let vol_fp = FixedPoint::from_str("1.00000000").expect("valid volume");
                let turnover_f64 = price_fp.to_f64() * vol_fp.to_f64();
                TradeSnapshot {
                    price: price_fp,
                    volume: vol_fp,
                    timestamp: 1000 + (i as i64 * 100),
                    is_buyer_maker: false,
                    turnover: (turnover_f64 * 1e8) as i128,
                }
            })
            .collect();
        let snapshot_refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
        let vol = compute_garman_klass(&snapshot_refs);
        assert!(vol >= 0.0, "Garman-Klass volatility must be non-negative");
        assert!(!vol.is_nan(), "Garman-Klass must not be NaN");
    }

    #[test]
    fn test_permutation_entropy_edge_case_empty() {
        let prices: Vec<f64> = vec![];
        let entropy = compute_permutation_entropy(&prices);
        assert_eq!(entropy, 1.0, "Empty prices should return max entropy (1.0)");
    }

    #[test]
    fn test_permutation_entropy_edge_case_insufficient_data() {
        // 30 monotonically increasing prices should have zero entropy (single pattern)
        let prices: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert_eq!(entropy, 0.0, "Monotonic sequence should have zero entropy");
    }

    #[test]
    fn test_permutation_entropy_bounds() {
        // Entropy should be in [0, 1]
        let prices: Vec<f64> = (0..100).map(|i| 100.0 + (i % 3) as f64).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "Entropy must be in [0,1], got {}",
            entropy
        );
        assert!(!entropy.is_nan(), "Entropy must not be NaN");
    }

    #[test]
    fn test_kaufman_er_edge_case_empty() {
        let prices: Vec<f64> = vec![];
        let er = compute_kaufman_er(&prices);
        assert_eq!(er, 0.0, "Empty prices should give ER=0");
    }

    #[test]
    fn test_kaufman_er_edge_case_constant_prices() {
        let prices = vec![100.0; 50];
        let er = compute_kaufman_er(&prices);
        assert_eq!(er, 0.0, "Constant prices should give ER=0");
    }

    #[test]
    fn test_kaufman_er_bounds() {
        // Kaufman ER should be in [0, 1]
        let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64).collect();
        let er = compute_kaufman_er(&prices);
        assert!(er >= 0.0 && er <= 1.0, "ER must be in [0,1], got {}", er);
        assert!(!er.is_nan(), "ER must not be NaN");
    }

    #[test]
    fn test_ordinal_pattern_index_coverage() {
        // Test ordinal pattern mappings for m=3
        // All 6 patterns from algorithm in ordinal_pattern_index_m3
        let test_cases = vec![
            (0.0, 1.0, 2.0, 0), // a<=b<=c → 0
            (0.0, 2.0, 1.0, 1), // a<=c<b → 1
            (1.0, 0.0, 2.0, 2), // b<a<=c → 2
            (2.0, 0.0, 1.0, 3), // a>b, a>c, b<=c → 3
            (1.0, 2.0, 0.0, 4), // a<=b, b>c, a>c → 4
            (2.0, 1.0, 0.0, 5), // a>b>c → 5
        ];

        for (a, b, c, expected) in test_cases {
            let idx = ordinal_pattern_index_m3(a, b, c);
            assert_eq!(
                idx, expected,
                "Pattern ({},{},{}) should map to index {} but got {}",
                a, b, c, expected, idx
            );
        }
    }

    // Tier 2 Feature Tests: Kyle Lambda
    #[test]
    fn test_kyle_lambda_edge_case_empty() {
        let kyle_lambda = compute_kyle_lambda(&[]);
        assert_eq!(kyle_lambda, 0.0, "Empty lookback should return 0");
    }

    #[test]
    fn test_kyle_lambda_edge_case_single_trade() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        let snapshot = TradeSnapshot {
            timestamp: 1000000,
            price: FixedPoint::from_str("100.0").unwrap(),
            volume: FixedPoint::from_str("1.0").unwrap(),
            is_buyer_maker: true,
            turnover: (100 * 1) as i128 * 100000000i128,
        };
        let kyle_lambda = compute_kyle_lambda(&[&snapshot]);
        assert_eq!(
            kyle_lambda, 0.0,
            "Single trade should return 0 (insufficient data)"
        );
    }

    #[test]
    fn test_kyle_lambda_zero_imbalance() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Equal buy and sell volume should give zero imbalance
        let trades = vec![
            TradeSnapshot {
                timestamp: 1000000,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: true,
                turnover: (100 * 1) as i128 * 100000000i128,
            },
            TradeSnapshot {
                timestamp: 1000100,
                price: FixedPoint::from_str("100.5").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: false, // Seller (opposite)
                turnover: (100 * 1) as i128 * 100000000i128,
            },
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kyle_lambda = compute_kyle_lambda(&refs);
        assert_eq!(kyle_lambda, 0.0, "Zero imbalance should return 0");
    }

    #[test]
    fn test_kyle_lambda_positive_trend_buy_pressure() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Price increases with BUY pressure (is_buyer_maker=false = BUY)
        // More buy volume (aggressive buyers) pushes price up
        let trades = vec![
            TradeSnapshot {
                timestamp: 1000000,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: true, // SELL (minimal)
                turnover: (100 * 1) as i128 * 100000000i128,
            },
            TradeSnapshot {
                timestamp: 1000100,
                price: FixedPoint::from_str("101.0").unwrap(),
                volume: FixedPoint::from_str("10.0").unwrap(),
                is_buyer_maker: false, // BUY (large buy volume)
                turnover: (101 * 10) as i128 * 100000000i128,
            },
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kyle_lambda = compute_kyle_lambda(&refs);
        // With more buy volume (imbalance > 0) and price increase, kyle_lambda should be positive
        assert!(
            kyle_lambda > 0.0,
            "Buy pressure with price increase should give positive kyle_lambda, got {}",
            kyle_lambda
        );
    }

    #[test]
    fn test_kyle_lambda_bounded() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Kyle lambda should be finite (not NaN or Inf)
        for _i in 0..10 {
            let trades = vec![
                TradeSnapshot {
                    timestamp: 1000000,
                    price: FixedPoint::from_str("100.0").unwrap(),
                    volume: FixedPoint::from_str("5.0").unwrap(),
                    is_buyer_maker: true,
                    turnover: (100 * 5) as i128 * 100000000i128,
                },
                TradeSnapshot {
                    timestamp: 1000100,
                    price: FixedPoint::from_str("105.0").unwrap(),
                    volume: FixedPoint::from_str("2.0").unwrap(),
                    is_buyer_maker: false,
                    turnover: (105 * 2) as i128 * 100000000i128,
                },
            ];
            let refs: Vec<&TradeSnapshot> = trades.iter().collect();
            let kyle_lambda = compute_kyle_lambda(&refs);
            assert!(
                kyle_lambda.is_finite(),
                "Kyle lambda must be finite, got {}",
                kyle_lambda
            );
        }
    }

    // Tier 2 Feature Tests: Burstiness
    #[test]
    fn test_burstiness_edge_case_empty() {
        let burstiness = compute_burstiness(&[]);
        assert_eq!(burstiness, 0.0, "Empty lookback should return 0");
    }

    #[test]
    fn test_burstiness_single_trade() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        let snapshot = TradeSnapshot {
            timestamp: 1000000,
            price: FixedPoint::from_str("100.0").unwrap(),
            volume: FixedPoint::from_str("1.0").unwrap(),
            is_buyer_maker: true,
            turnover: (100 * 1) as i128 * 100000000i128,
        };
        let burstiness = compute_burstiness(&[&snapshot]);
        assert_eq!(
            burstiness, 0.0,
            "Single trade should return 0 (insufficient data)"
        );
    }

    #[test]
    fn test_burstiness_bounds() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Create regular arrivals (approximately)
        let mut trades = Vec::with_capacity(20);
        for i in 0..20 {
            trades.push(TradeSnapshot {
                timestamp: 1000000 + (i * 100) as i64,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: i % 2 == 0,
                turnover: (100 * 1) as i128 * 100000000i128,
            });
        }
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let burstiness = compute_burstiness(&refs);
        assert!(
            burstiness >= -1.0 && burstiness <= 1.0,
            "Burstiness must be in [-1, 1], got {}",
            burstiness
        );
    }

    // Tier 3 Feature Tests: Additional Kaufman ER edge cases
    #[test]
    fn test_kaufman_er_trending_market() {
        // Strong uptrend
        let mut prices = Vec::with_capacity(50);
        let mut price = 100.0;
        for _ in 0..50 {
            price += 0.1; // Consistent uptrend
            prices.push(price);
        }
        let er = compute_kaufman_er(&prices);
        assert!(
            er > 0.5,
            "Strong trending market should have high efficiency ratio, got {}",
            er
        );
    }

    #[test]
    fn test_kaufman_er_ranging_market() {
        // Oscillating prices (ranging)
        let mut prices = Vec::with_capacity(50);
        for i in 0..50 {
            let price = 100.0 + if (i % 2) == 0 { 0.1 } else { -0.1 };
            prices.push(price);
        }
        let er = compute_kaufman_er(&prices);
        assert!(
            er < 0.3,
            "Ranging market should have low efficiency ratio, got {}",
            er
        );
    }

    // ===== NEW TIER 3 FEATURE EDGE CASE TESTS (Task #17) =====

    // Kyle Lambda - Additional Edge Cases
    #[test]
    fn test_kyle_lambda_negative_trend_sell_pressure() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Price decreases with SELL pressure (is_buyer_maker=true = SELL)
        let trades = vec![
            TradeSnapshot {
                timestamp: 1000000,
                price: FixedPoint::from_str("101.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: false, // BUY (minimal)
                turnover: (101 * 1) as i128 * 100000000i128,
            },
            TradeSnapshot {
                timestamp: 1000100,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("10.0").unwrap(),
                is_buyer_maker: true, // SELL (large sell volume)
                turnover: (100 * 10) as i128 * 100000000i128,
            },
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kyle_lambda = compute_kyle_lambda(&refs);
        // With more sell volume (imbalance < 0) and price decrease, kyle_lambda should be positive
        // (price moves in direction of order flow)
        assert!(
            kyle_lambda > 0.0,
            "Sell pressure with price decrease should give positive kyle_lambda"
        );
    }

    #[test]
    fn test_kyle_lambda_zero_price_movement() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Price doesn't change but there's volume imbalance
        let trades = vec![
            TradeSnapshot {
                timestamp: 1000000,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("5.0").unwrap(),
                is_buyer_maker: false, // BUY
                turnover: (100 * 5) as i128 * 100000000i128,
            },
            TradeSnapshot {
                timestamp: 1000100,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: true, // SELL (minimal)
                turnover: (100 * 1) as i128 * 100000000i128,
            },
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kyle_lambda = compute_kyle_lambda(&refs);
        // No price movement should give 0 kyle_lambda
        assert_eq!(kyle_lambda, 0.0, "Zero price movement should give 0");
    }

    #[test]
    fn test_kyle_lambda_tiny_prices() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Test with very small prices (e.g., penny stocks)
        let trades = vec![
            TradeSnapshot {
                timestamp: 1000000,
                price: FixedPoint::from_str("0.001").unwrap(),
                volume: FixedPoint::from_str("100000.0").unwrap(),
                is_buyer_maker: true,
                turnover: (1 * 100000) as i128 * 100000000i128,
            },
            TradeSnapshot {
                timestamp: 1000100,
                price: FixedPoint::from_str("0.002").unwrap(),
                volume: FixedPoint::from_str("50000.0").unwrap(),
                is_buyer_maker: false,
                turnover: (2 * 50000) as i128 * 100000000i128,
            },
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kyle_lambda = compute_kyle_lambda(&refs);
        assert!(
            kyle_lambda.is_finite(),
            "Should handle tiny prices without NaN/Inf"
        );
    }

    #[test]
    fn test_kyle_lambda_opposing_flows() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Buy and sell at different times with conflicting pressures
        let trades = vec![
            TradeSnapshot {
                timestamp: 1000000,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("10.0").unwrap(),
                is_buyer_maker: false, // BUY (large)
                turnover: (100 * 10) as i128 * 100000000i128,
            },
            TradeSnapshot {
                timestamp: 1000100,
                price: FixedPoint::from_str("99.0").unwrap(),
                volume: FixedPoint::from_str("5.0").unwrap(),
                is_buyer_maker: true, // SELL (price down despite buy pressure initially)
                turnover: (99 * 5) as i128 * 100000000i128,
            },
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kyle_lambda = compute_kyle_lambda(&refs);
        // Price decreased against buy pressure → negative kyle_lambda
        assert!(
            kyle_lambda < 0.0,
            "Price moving against order flow should give negative kyle_lambda"
        );
    }

    // Burstiness - Additional Edge Cases
    #[test]
    fn test_burstiness_clustered_arrivals() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Trades clustered at start, then gap
        let mut trades = Vec::with_capacity(15);
        // Cluster: 10 trades in 100ms
        for i in 0..10 {
            trades.push(TradeSnapshot {
                timestamp: 1000000 + (i * 10) as i64,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: i % 2 == 0,
                turnover: (100 * 1) as i128 * 100000000i128,
            });
        }
        // Large gap: 1000ms
        for i in 0..5 {
            trades.push(TradeSnapshot {
                timestamp: 1000100 + 1000 + (i * 10) as i64,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: i % 2 == 0,
                turnover: (100 * 1) as i128 * 100000000i128,
            });
        }
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let burstiness = compute_burstiness(&refs);
        // Bursty pattern should give high burstiness
        assert!(
            burstiness > 0.0,
            "Clustered arrivals should have positive burstiness, got {}",
            burstiness
        );
        assert!(burstiness <= 1.0, "Burstiness should be bounded by 1.0");
    }

    #[test]
    fn test_burstiness_perfectly_regular() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Perfectly regular 100ms intervals
        let mut trades = Vec::with_capacity(20);
        for i in 0..20 {
            trades.push(TradeSnapshot {
                timestamp: 1000000 + (i * 100) as i64,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: i % 2 == 0,
                turnover: (100 * 1) as i128 * 100000000i128,
            });
        }
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let burstiness = compute_burstiness(&refs);
        // Regular arrivals should give burstiness near -1
        assert!(
            burstiness < 0.0,
            "Regular periodic arrivals should have negative burstiness, got {}",
            burstiness
        );
    }

    #[test]
    fn test_burstiness_extreme_gap() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // One large burst followed by extreme gap
        let mut trades = Vec::with_capacity(5);
        // Initial burst: 5 trades
        for i in 0..5 {
            trades.push(TradeSnapshot {
                timestamp: 1000000 + (i as i64),
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: i % 2 == 0,
                turnover: (100 * 1) as i128 * 100000000i128,
            });
        }
        // Massive gap then one more trade
        trades.push(TradeSnapshot {
            timestamp: 1000000 + 100000,
            price: FixedPoint::from_str("100.0").unwrap(),
            volume: FixedPoint::from_str("1.0").unwrap(),
            is_buyer_maker: false,
            turnover: (100 * 1) as i128 * 100000000i128,
        });
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let burstiness = compute_burstiness(&refs);
        // Extreme gap should produce positive (bursty) burstiness
        assert!(
            burstiness > 0.0,
            "Extreme gap should produce positive burstiness"
        );
        assert!(burstiness <= 1.0, "Burstiness should be bounded");
    }

    // Garman-Klass - Additional Edge Cases
    #[test]
    fn test_garman_klass_high_volatility() {
        use opendeviationbar_core::{FixedPoint, interbar_types::TradeSnapshot};
        // Large price swings (H >> L)
        let prices = vec![100.0, 150.0, 120.0, 180.0, 110.0];
        let snapshots: Vec<TradeSnapshot> = prices
            .iter()
            .enumerate()
            .map(|(i, &price)| {
                let price_fp = FixedPoint::from_str(&format!("{:.8}", price)).expect("valid price");
                let vol_fp = FixedPoint::from_str("1.00000000").expect("valid volume");
                let turnover_f64 = price_fp.to_f64() * vol_fp.to_f64();
                TradeSnapshot {
                    price: price_fp,
                    volume: vol_fp,
                    timestamp: 1000 + (i as i64 * 100),
                    is_buyer_maker: false,
                    turnover: (turnover_f64 * 1e8) as i128,
                }
            })
            .collect();
        let snapshot_refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
        let vol = compute_garman_klass(&snapshot_refs);
        assert!(
            vol > 0.0,
            "High volatility scenario should produce non-zero volatility"
        );
        assert!(!vol.is_nan(), "Garman-Klass must not be NaN");
    }

    #[test]
    fn test_garman_klass_extreme_ohlc_ratios() {
        use opendeviationbar_core::{FixedPoint, interbar_types::TradeSnapshot};
        // Extreme high/low ratio
        let prices = vec![100.0, 1000.0, 200.0]; // H/L = 5
        let snapshots: Vec<TradeSnapshot> = prices
            .iter()
            .enumerate()
            .map(|(i, &price)| {
                let price_fp = FixedPoint::from_str(&format!("{:.8}", price)).expect("valid price");
                let vol_fp = FixedPoint::from_str("1.00000000").expect("valid volume");
                let turnover_f64 = price_fp.to_f64() * vol_fp.to_f64();
                TradeSnapshot {
                    price: price_fp,
                    volume: vol_fp,
                    timestamp: 1000 + (i as i64 * 100),
                    is_buyer_maker: false,
                    turnover: (turnover_f64 * 1e8) as i128,
                }
            })
            .collect();
        let snapshot_refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
        let vol = compute_garman_klass(&snapshot_refs);
        // Should handle extreme ratios without panic
        assert!(vol >= 0.0, "Garman-Klass must be non-negative");
        assert!(vol.is_finite(), "Garman-Klass must be finite");
    }

    // Permutation Entropy - Additional Edge Cases
    #[test]
    fn test_permutation_entropy_deterministic_pattern() {
        // Perfectly ordered ascending pattern
        let prices: Vec<f64> = (0..100).map(|i| i as f64).collect();
        let entropy = compute_permutation_entropy(&prices);
        // Deterministic pattern should have low entropy
        assert!(entropy >= 0.0 && entropy <= 1.0, "Entropy must be in [0,1]");
    }

    #[test]
    fn test_permutation_entropy_oscillating_pattern() {
        // Simple oscillating pattern (should have repeating permutations)
        let mut prices = Vec::with_capacity(100);
        for i in 0..100 {
            prices.push(if i % 3 == 0 {
                100.0
            } else if i % 3 == 1 {
                101.0
            } else {
                99.0
            });
        }
        let entropy = compute_permutation_entropy(&prices);
        // Repeating pattern should have lower entropy than random
        assert!(entropy >= 0.0 && entropy <= 1.0, "Entropy must be in [0,1]");
        assert!(!entropy.is_nan(), "Entropy must not be NaN");
    }

    // Kaufman ER - Additional Edge Cases
    #[test]
    fn test_kaufman_er_single_large_move() {
        // Single direction move with no noise
        let mut prices = Vec::with_capacity(50);
        for i in 0..50 {
            prices.push(100.0 + i as f64); // Perfect linear trend
        }
        let er = compute_kaufman_er(&prices);
        // Perfect trend should give ER close to 1.0
        assert!(er > 0.9, "Perfect trend should have ER > 0.9, got {}", er);
    }

    #[test]
    fn test_kaufman_er_noise_dominated() {
        // High-frequency noise with minimal net movement
        let mut prices = Vec::new();
        let mut rng = 12345u64;
        prices.push(100.0);
        for _ in 1..100 {
            rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
            let noise = ((rng >> 16) as f64 % 200.0) - 100.0; // Random [-100, 100] bps
            let new_price = prices.last().unwrap() + noise * 0.0001; // ±0.01 bps noise
            prices.push(new_price);
        }
        let er = compute_kaufman_er(&prices);
        // Noise-dominated should have lower ER than trending
        assert!(
            er < 0.5,
            "Noise-dominated market should have ER < 0.5, got {}",
            er
        );
        assert!(!er.is_nan(), "ER must be finite");
    }

    // Hurst - Additional Advanced Tests
    #[test]
    fn test_hurst_strong_reverting_pattern() {
        // Alternating high-low pattern (strong mean reversion)
        let mut prices = vec![100.0; 200];
        for i in 0..200 {
            prices[i] = if i % 2 == 0 { 99.0 } else { 101.0 };
        }
        let h = compute_hurst_rescaled_range(&prices);
        assert!(
            h < 0.5,
            "Strong mean reverting should have H < 0.5, got {}",
            h
        );
        assert!(h.is_finite(), "Hurst must be finite");
    }

    #[test]
    fn test_hurst_extreme_volatility() {
        // Extreme spikes and drops
        let mut prices = vec![100.0; 200];
        for i in 0..200 {
            prices[i] = match i % 4 {
                0 => 100.0,
                1 => 200.0, // Spike
                2 => 150.0,
                _ => 50.0, // Drop
            };
        }
        let h = compute_hurst_rescaled_range(&prices);
        assert!(
            h >= 0.0 && h <= 1.0,
            "Hurst must be in [0,1] even for extreme volatility"
        );
    }

    // Volume Moments - Additional Tests
    #[test]
    fn test_volume_moments_constant_volume() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // All trades same volume → skewness and kurtosis should be 0
        let trades: Vec<TradeSnapshot> = (0..20)
            .map(|i| TradeSnapshot {
                timestamp: 1000000 + (i as i64 * 100),
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                is_buyer_maker: i % 2 == 0,
                turnover: (100 * 1) as i128 * 100000000i128,
            })
            .collect();
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (skew, kurt) = compute_volume_moments(&refs);
        assert_eq!(skew, 0.0, "Constant volume should have zero skewness");
        assert_eq!(kurt, 0.0, "Constant volume should have zero kurtosis");
    }

    #[test]
    fn test_volume_moments_right_skewed() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Volume distribution skewed right (many small, few large)
        let volumes = vec![1.0, 1.0, 1.0, 1.0, 100.0]; // Right skew
        let trades: Vec<TradeSnapshot> = volumes
            .iter()
            .enumerate()
            .map(|(i, &vol)| TradeSnapshot {
                timestamp: 1000000 + (i as i64 * 100),
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str(&format!("{:.8}", vol)).unwrap(),
                is_buyer_maker: i % 2 == 0,
                turnover: (100.0 * vol * 1e8) as i128,
            })
            .collect();
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (skew, _kurt) = compute_volume_moments(&refs);
        // Right-skewed should have positive skewness
        assert!(
            skew > 0.0,
            "Right-skewed volume should have positive skewness, got {}",
            skew
        );
    }

    #[test]
    fn test_volume_moments_heavy_tails() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Volume distribution with heavy tails (high kurtosis)
        let mut volumes = vec![1.0; 18]; // Many small volumes
        volumes.push(100.0); // One extreme value
        volumes.push(100.0); // Another extreme

        let trades: Vec<TradeSnapshot> = volumes
            .iter()
            .enumerate()
            .map(|(i, &vol)| TradeSnapshot {
                timestamp: 1000000 + (i as i64 * 100),
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str(&format!("{:.8}", vol)).unwrap(),
                is_buyer_maker: i % 2 == 0,
                turnover: (100.0 * vol * 1e8) as i128,
            })
            .collect();
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (_skew, kurt) = compute_volume_moments(&refs);
        // Heavy tails should have high (positive) kurtosis
        assert!(
            kurt > 0.0,
            "Heavy-tailed distribution should have positive kurtosis, got {}",
            kurt
        );
    }

    // Ordinal Pattern - Additional Coverage
    #[test]
    fn test_ordinal_pattern_equal_values() {
        // Test handling of equal values in patterns
        // Verify the ordinal pattern function handles equal values gracefully
        let test_cases = vec![
            (1.0, 1.0, 2.0), // a=b < c
            (1.0, 2.0, 2.0), // a < b=c (uses < for b<=c branch)
            (1.0, 1.0, 1.0), // a=b=c
            (2.0, 2.0, 1.0), // a=b > c
        ];
        for (a, b, c) in test_cases {
            let idx = ordinal_pattern_index_m3(a, b, c);
            // All indices should be in valid range [0, 5]
            assert!(idx < 6, "Pattern index must be < 6, got {}", idx);
        }
    }

    // ========== NEW TESTS FOR TASK #23 (Expanded Coverage) ==========

    // Permutation Entropy - Adaptive Path Tests (M=2 for small windows)
    #[test]
    fn test_adaptive_permutation_entropy_m2_small_window() {
        // Issue #96: Use 15-price window from real data to trigger M=2 path (10 <= n < 20)
        // n=5 returns 1.0 (early-exit for n<10), so use n=15 instead
        let trades = opendeviationbar_core::test_data_loader::load_real_btcusdt_10k().unwrap();
        let prices: Vec<f64> = trades.iter().take(15).map(|t| t.price.to_f64()).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "Entropy should be normalized [0,1]"
        );
        // Real data is non-uniform, should have less than max entropy
        assert!(
            entropy < 1.0,
            "M=2 adaptive path should return meaningful entropy for real data, got {}",
            entropy
        );
    }

    #[test]
    fn test_adaptive_permutation_entropy_m2_deterministic() {
        // Perfectly ascending should have low entropy
        let prices: Vec<f64> = (0..15).map(|i| i as f64).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy < 0.3,
            "Monotonic sequence should have low entropy, got {}",
            entropy
        );
    }

    #[test]
    fn test_adaptive_permutation_entropy_m2_m3_transition() {
        // Test behavior at M=2→M=3 boundary (n=20)
        let mut prices: Vec<f64> = (0..20).map(|i| (i as f64 * 0.5).sin()).collect();
        let entropy_boundary = compute_permutation_entropy(&prices);

        prices.push(21.0);
        let entropy_m3 = compute_permutation_entropy(&prices);

        // Both should be in valid range
        assert!(entropy_boundary >= 0.0 && entropy_boundary <= 1.0);
        assert!(entropy_m3 >= 0.0 && entropy_m3 <= 1.0);
    }

    #[test]
    fn test_adaptive_permutation_entropy_insufficient_data() {
        // Too small (< 10) should return max entropy
        let prices = vec![1.0, 2.0];
        let entropy = compute_permutation_entropy(&prices);
        assert_eq!(entropy, 1.0, "Insufficient data should return max entropy");
    }

    // Issue #96 Task #130: SIMD Entropy Tests - Numerical Equivalence & Edge Cases
    #[test]
    fn test_simd_entropy_16_pattern_boundary() {
        // Test boundary at 16 patterns (exactly one SIMD iteration)
        let prices: Vec<f64> = (0..18).map(|i| 100.0 + (i as f64 * 0.1)).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "Entropy at 16-pattern boundary should be in [0,1], got {}",
            entropy
        );
        assert!(
            !entropy.is_nan(),
            "Entropy must not be NaN at 16-pattern boundary"
        );
    }

    #[test]
    fn test_simd_entropy_32_pattern_boundary() {
        // Test boundary at 32 patterns (exactly two SIMD iterations)
        let prices: Vec<f64> = (0..34).map(|i| 100.0 + (i as f64 * 0.05)).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "Entropy at 32-pattern boundary should be in [0,1], got {}",
            entropy
        );
        assert!(
            !entropy.is_nan(),
            "Entropy must not be NaN at 32-pattern boundary"
        );
    }

    #[test]
    fn test_simd_entropy_100_mixed_pattern() {
        // Test with 100 data points - multiple SIMD iterations + remainder
        let prices: Vec<f64> = (0..100)
            .map(|i| 100.0 + ((i as f64).sin() * 10.0))
            .collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "Mixed pattern entropy should be in [0,1], got {}",
            entropy
        );
        assert!(
            entropy > 0.3,
            "Mixed pattern should have non-trivial entropy, got {}",
            entropy
        );
        assert!(
            !entropy.is_nan(),
            "Entropy must not be NaN for mixed pattern"
        );
    }

    #[test]
    fn test_simd_entropy_500_large_lookback() {
        // Test with 500 data points - realistic lookback window
        let prices: Vec<f64> = (0..500)
            .map(|i| 100.0 + ((i as f64 * 0.1).sin() * 5.0))
            .collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "Large lookback entropy should be in [0,1], got {}",
            entropy
        );
        assert!(
            !entropy.is_nan(),
            "Entropy must not be NaN for 500-element lookback"
        );
    }

    #[test]
    fn test_simd_entropy_alternating_pattern() {
        // Test with strictly alternating pattern (high entropy expectation)
        let mut prices = Vec::new();
        for i in 0..50 {
            if i % 2 == 0 {
                prices.push(100.0);
            } else {
                prices.push(101.0);
            }
        }
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "Alternating pattern entropy should be in [0,1], got {}",
            entropy
        );
        assert!(
            entropy < 0.5,
            "Alternating pattern should have low entropy, got {}",
            entropy
        );
    }

    #[test]
    fn test_simd_entropy_monotonic_increasing() {
        // Test monotonic increasing sequence (zero entropy)
        let prices: Vec<f64> = (0..100).map(|i| i as f64).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert_eq!(
            entropy, 0.0,
            "Monotonic increasing should yield zero entropy"
        );
    }

    #[test]
    fn test_simd_entropy_monotonic_decreasing() {
        // Test monotonic decreasing sequence (zero entropy)
        let prices: Vec<f64> = (0..100).map(|i| 100.0 - i as f64).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert_eq!(
            entropy, 0.0,
            "Monotonic decreasing should yield zero entropy"
        );
    }

    #[test]
    fn test_simd_entropy_noise_pattern() {
        // Test with Gaussian-like noise (high entropy)
        let prices: Vec<f64> = (0..200)
            .map(|i| {
                let angle = (i as f64) * std::f64::consts::PI / 32.0;
                100.0 + angle.sin() * 5.0 + (i % 7) as f64 * 0.3
            })
            .collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "Noisy pattern entropy should be in [0,1], got {}",
            entropy
        );
        assert!(
            !entropy.is_nan(),
            "Entropy must not be NaN for noisy pattern"
        );
    }

    #[test]
    fn test_simd_entropy_edge_case_15_patterns() {
        // Test with exactly 17 prices (16 patterns - one before first SIMD boundary)
        let prices: Vec<f64> = (0..17).map(|i| 100.0 + (i as f64 * 0.2)).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "15-pattern entropy should be in [0,1], got {}",
            entropy
        );
    }

    #[test]
    fn test_simd_entropy_edge_case_17_patterns() {
        // Test with exactly 19 prices (17 patterns - just beyond first SIMD boundary)
        let prices: Vec<f64> = (0..19).map(|i| 100.0 + (i as f64 * 0.15)).collect();
        let entropy = compute_permutation_entropy(&prices);
        assert!(
            entropy >= 0.0 && entropy <= 1.0,
            "17-pattern entropy should be in [0,1], got {}",
            entropy
        );
    }

    // Kyle Lambda - Extended Edge Cases
    #[test]
    fn test_kyle_lambda_zero_imbalance_extended() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Equal buy and sell volume → zero imbalance → lambda = 0
        let trades: Vec<TradeSnapshot> = vec![
            TradeSnapshot {
                timestamp: 1000,
                price: FixedPoint::from_str("100.0").unwrap(),
                volume: FixedPoint::from_str("10.0").unwrap(),
                is_buyer_maker: true,
                turnover: 1_000_000_000i128,
            },
            TradeSnapshot {
                timestamp: 2000,
                price: FixedPoint::from_str("101.0").unwrap(),
                volume: FixedPoint::from_str("10.0").unwrap(),
                is_buyer_maker: false,
                turnover: 1_010_000_000i128,
            },
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let lambda = compute_kyle_lambda(&refs);
        assert_eq!(lambda, 0.0, "Zero imbalance should yield zero lambda");
    }

    #[test]
    fn test_kyle_lambda_strong_buy_pressure_extended() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Issue #96: Use real market data — has natural directional flow
        let trades = opendeviationbar_core::test_data_loader::load_real_btcusdt_10k().unwrap();
        let snapshots: Vec<TradeSnapshot> =
            trades.iter().take(200).map(TradeSnapshot::from).collect();
        let refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
        let lambda = compute_kyle_lambda(&refs);
        assert!(
            lambda.is_finite(),
            "Kyle lambda must be finite, got {}",
            lambda
        );
        // Real data has directional flow — lambda should be non-zero
        assert!(
            lambda != 0.0,
            "Real market data should yield non-zero lambda"
        );
    }

    // Burstiness - Timing Analysis Extended
    #[test]
    fn test_burstiness_regular_arrivals_extended() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Issue #96: Use real market data — natural timestamp clustering
        let trades = opendeviationbar_core::test_data_loader::load_real_btcusdt_10k().unwrap();
        let snapshots: Vec<TradeSnapshot> =
            trades.iter().take(200).map(TradeSnapshot::from).collect();
        let refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
        let burst = compute_burstiness(&refs);
        assert!(
            burst.is_finite(),
            "Burstiness must be finite, got {}",
            burst
        );
        // Burstiness is in [-1, 1] range
        assert!(
            burst >= -1.0 && burst <= 1.0,
            "Burstiness out of range, got {}",
            burst
        );
    }

    #[test]
    fn test_burstiness_clustered_arrivals_extended() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Clustered (bursty) → burstiness > 0.5
        let timestamp = 1000i64;
        let trades: Vec<TradeSnapshot> = (0..20)
            .map(|i| {
                let ts = if i < 10 {
                    timestamp + (i as i64 * 100) // Cluster 1: 100µs apart
                } else {
                    timestamp + 1_000_000 + ((i - 10) as i64 * 100) // Cluster 2: far apart
                };
                TradeSnapshot {
                    timestamp: ts,
                    price: FixedPoint::from_str("100.0").unwrap(),
                    volume: FixedPoint::from_str("1.0").unwrap(),
                    is_buyer_maker: i % 2 == 0,
                    turnover: 100_000_000i128,
                }
            })
            .collect();
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let burst = compute_burstiness(&refs);
        assert!(
            burst > 0.3,
            "Clustered arrivals should have high burstiness, got {}",
            burst
        );
    }

    // Hurst Exponent - Confidence & Bounds
    #[test]
    fn test_hurst_soft_clamp_boundary_extended() {
        // Test soft_clamp_hurst at boundaries
        assert!(soft_clamp_hurst(0.0) >= 0.0 && soft_clamp_hurst(0.0) <= 1.0);
        assert!(soft_clamp_hurst(1.0) >= 0.0 && soft_clamp_hurst(1.0) <= 1.0);
        assert!(soft_clamp_hurst(2.0) >= 0.0 && soft_clamp_hurst(2.0) <= 1.0);
        // Extreme negative
        assert!(soft_clamp_hurst(-10.0) >= 0.0 && soft_clamp_hurst(-10.0) <= 1.0);
    }

    #[test]
    fn test_hurst_monotonicity_extended() {
        // Hurst should be monotonic in trending strength
        let trending: Vec<f64> = (0..256).map(|i| i as f64).collect();
        let mean_reverting = vec![0.5; 256];

        let h_trending = compute_hurst_rescaled_range(&trending);
        let h_mean_revert = compute_hurst_rescaled_range(&mean_reverting);

        // Trending should have higher Hurst
        assert!(
            h_trending > h_mean_revert,
            "Trending should have higher H than mean-reverting"
        );
    }

    // Multi-feature consistency (cross-validation)
    #[test]
    fn test_feature_consistency_normal_market_extended() {
        use opendeviationbar_core::interbar_types::TradeSnapshot;
        // Issue #96: Use real market data — no FixedPoint::from_str formatting issues
        let trades = opendeviationbar_core::test_data_loader::load_real_btcusdt_10k().unwrap();
        let snapshots: Vec<TradeSnapshot> =
            trades.iter().take(200).map(TradeSnapshot::from).collect();
        let refs: Vec<&TradeSnapshot> = snapshots.iter().collect();

        // All features should return valid numbers
        let kyle = compute_kyle_lambda(&refs);
        let burst = compute_burstiness(&refs);
        let (skew, kurt) = compute_volume_moments(&refs);

        assert!(kyle.is_finite(), "Kyle lambda must be finite");
        assert!(burst.is_finite(), "Burstiness must be finite");
        assert!(skew.is_finite(), "Skewness must be finite");
        assert!(kurt.is_finite(), "Kurtosis must be finite");
    }

    // ========== ENTROPY CACHE TESTS (Task #117) ==========

    #[test]
    fn test_entropy_cache_hit() {
        // Test that cached values are returned on subsequent calls
        let prices = vec![1.0, 2.0, 1.5, 3.0, 2.5, 1.0, 2.0, 1.5, 3.0, 2.5];
        let mut cache = EntropyCache::new();

        // First computation
        let entropy1 = compute_entropy_adaptive_cached(&prices, &mut cache);

        // Second computation with same prices - should use cache
        let entropy2 = compute_entropy_adaptive_cached(&prices, &mut cache);

        // Values should be identical (bit-for-bit)
        assert_eq!(entropy1, entropy2, "Cached value should match original");
    }

    #[test]
    fn test_entropy_cache_different_sequences() {
        // Test that different price sequences get different cache entries
        // Use longer sequences (60+) to trigger permutation entropy computation
        let prices1: Vec<f64> = (0..100).map(|i| 100.0 + (i as f64).sin()).collect();
        let prices2: Vec<f64> = (0..100).map(|i| 100.0 + (i as f64).cos()).collect();

        let mut cache = EntropyCache::new();

        let entropy1 = compute_entropy_adaptive_cached(&prices1, &mut cache);
        let entropy2 = compute_entropy_adaptive_cached(&prices2, &mut cache);

        // Both should be valid entropy values
        assert!(entropy1.is_finite(), "Entropy1 must be finite");
        assert!(entropy2.is_finite(), "Entropy2 must be finite");
        assert!(entropy1 >= 0.0, "Entropy1 must be non-negative");
        assert!(entropy2 >= 0.0, "Entropy2 must be non-negative");
    }

    #[test]
    fn test_entropy_cache_vs_uncached() {
        // Verify that cached and uncached paths produce identical results
        let prices: Vec<f64> = (0..100)
            .map(|i| 100.0 + (i as f64 * 0.5).sin() * 10.0)
            .collect();

        let mut cache = EntropyCache::new();

        let entropy_cached = compute_entropy_adaptive_cached(&prices, &mut cache);
        let entropy_uncached = compute_entropy_adaptive(&prices);

        // Results should be bit-identical
        assert_eq!(
            entropy_cached, entropy_uncached,
            "Cached and uncached must produce identical results"
        );
    }

    // Issue #96: EntropyCache public API coverage (with_capacity, metrics, reset_metrics)

    #[test]
    fn test_entropy_cache_with_capacity() {
        // Verify with_capacity creates a functional cache
        let mut cache = EntropyCache::with_capacity(512);
        let prices = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];

        // Should work identically to new() — insert and retrieve
        let entropy = compute_entropy_adaptive(&prices);
        cache.insert(&prices, entropy);
        let cached = cache.get(&prices);
        assert_eq!(
            cached,
            Some(entropy),
            "with_capacity cache should store and retrieve"
        );
    }

    #[test]
    fn test_entropy_cache_with_capacity_zero() {
        // Edge case: zero capacity — should not panic
        let cache = EntropyCache::with_capacity(0);
        let (hits, misses, ratio) = cache.metrics();
        assert_eq!((hits, misses), (0, 0));
        assert_eq!(ratio, 0.0);
    }

    #[test]
    fn test_entropy_cache_metrics_tracking() {
        let mut cache = EntropyCache::new();

        // Initial state: all zeros
        let (h, m, ratio) = cache.metrics();
        assert_eq!((h, m), (0, 0), "initial metrics should be zero");
        assert_eq!(ratio, 0.0, "initial ratio should be 0.0");

        // Miss: query for a key not in cache
        let prices_a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
        assert!(cache.get(&prices_a).is_none());
        let (h, m, ratio) = cache.metrics();
        assert_eq!((h, m), (0, 1), "should have 1 miss");
        assert_eq!(ratio, 0.0, "0 hits / 1 total = 0%");

        // Insert then hit
        cache.insert(&prices_a, 0.42);
        assert!(cache.get(&prices_a).is_some());
        let (h, m, ratio) = cache.metrics();
        assert_eq!((h, m), (1, 1), "should have 1 hit + 1 miss");
        assert!((ratio - 50.0).abs() < 0.01, "1/2 = 50%, got {ratio}");
    }

    #[test]
    fn test_entropy_cache_reset_metrics() {
        let mut cache = EntropyCache::new();

        // Accumulate some metrics
        let prices = vec![10.0, 20.0, 30.0, 40.0, 50.0];
        cache.get(&prices); // miss
        cache.insert(&prices, 0.5);
        cache.get(&prices); // hit

        let (h, m, _) = cache.metrics();
        assert!(h + m > 0, "should have accumulated metrics");

        // Reset
        cache.reset_metrics();
        let (h, m, ratio) = cache.metrics();
        assert_eq!((h, m), (0, 0), "reset should clear all counters");
        assert_eq!(ratio, 0.0, "reset ratio should be 0.0");

        // Verify counters work after reset
        cache.get(&prices); // hit (data still in cache)
        let (h, m, ratio) = cache.metrics();
        assert_eq!((h, m), (1, 0), "should track after reset");
        assert!((ratio - 100.0).abs() < 0.01, "100% hit rate");
    }
}

/// SIMD vs scalar parity tests
/// Ensures SIMD-accelerated implementations produce results equivalent to scalar baselines.
/// Critical for correctness: SIMD paths must not introduce numerical divergence.
#[cfg(test)]
#[cfg(any(feature = "simd-burstiness", feature = "simd-kyle-lambda"))]
mod simd_parity_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    /// Generate a deterministic trade sequence with varying intervals and volumes
    fn generate_trade_sequence(n: usize, seed: u64) -> Vec<TradeSnapshot> {
        let mut rng = seed;
        let mut ts = 1_000_000i64;
        let base_price = 50000.0;

        (0..n)
            .map(|_| {
                // LCG for deterministic pseudo-random
                rng = rng
                    .wrapping_mul(6364136223846793005)
                    .wrapping_add(1442695040888963407);
                let r = ((rng >> 33) as f64) / (u32::MAX as f64);

                // Variable inter-arrival: 10-5000 us
                let delta = 10 + ((r * 4990.0) as i64);
                ts += delta;

                let price = base_price + (r - 0.5) * 100.0;
                let volume = 0.01 + r * 2.0;
                let is_buyer = rng % 2 == 0;

                make_snapshot(ts, price, volume, is_buyer)
            })
            .collect()
    }

    #[cfg(feature = "simd-burstiness")]
    #[test]
    fn test_burstiness_simd_scalar_parity_small() {
        // Small window: tests scalar remainder path
        let trades = generate_trade_sequence(5, 42);
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();

        let simd_result = simd::compute_burstiness_simd(&refs);
        let scalar_result = compute_burstiness_scalar(&refs);

        assert!(
            (simd_result - scalar_result).abs() < 1e-10,
            "Burstiness SIMD/scalar divergence on small window: SIMD={simd_result}, scalar={scalar_result}"
        );
    }

    #[cfg(feature = "simd-burstiness")]
    #[test]
    fn test_burstiness_simd_scalar_parity_medium() {
        // Medium window: typical lookback (100 trades)
        let trades = generate_trade_sequence(100, 123);
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();

        let simd_result = simd::compute_burstiness_simd(&refs);
        let scalar_result = compute_burstiness_scalar(&refs);

        assert!(
            (simd_result - scalar_result).abs() < 1e-10,
            "Burstiness SIMD/scalar divergence on medium window: SIMD={simd_result}, scalar={scalar_result}"
        );
    }

    #[cfg(feature = "simd-burstiness")]
    #[test]
    fn test_burstiness_simd_scalar_parity_large() {
        // Large window: stress test (500 trades)
        let trades = generate_trade_sequence(500, 456);
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();

        let simd_result = simd::compute_burstiness_simd(&refs);
        let scalar_result = compute_burstiness_scalar(&refs);

        assert!(
            (simd_result - scalar_result).abs() < 1e-8,
            "Burstiness SIMD/scalar divergence on large window: SIMD={simd_result}, scalar={scalar_result}"
        );
    }

    #[cfg(feature = "simd-burstiness")]
    #[test]
    fn test_burstiness_simd_scalar_parity_edge_cases() {
        // 2 trades: minimum for burstiness
        let t0 = make_snapshot(0, 100.0, 1.0, false);
        let t1 = make_snapshot(1000, 101.0, 1.0, true);
        let refs = vec![&t0, &t1];

        let simd_result = simd::compute_burstiness_simd(&refs);
        let scalar_result = compute_burstiness_scalar(&refs);
        assert!(
            (simd_result - scalar_result).abs() < 1e-10,
            "Burstiness parity failed on 2 trades: SIMD={simd_result}, scalar={scalar_result}"
        );

        // 3 trades: first non-trivial case
        let t2 = make_snapshot(2000, 102.0, 1.0, false);
        let refs = vec![&t0, &t1, &t2];

        let simd_result = simd::compute_burstiness_simd(&refs);
        let scalar_result = compute_burstiness_scalar(&refs);
        assert!(
            (simd_result - scalar_result).abs() < 1e-10,
            "Burstiness parity failed on 3 trades: SIMD={simd_result}, scalar={scalar_result}"
        );

        // Exactly 4 trades: one full SIMD chunk, no remainder
        let t3 = make_snapshot(3000, 103.0, 1.0, true);
        let refs = vec![&t0, &t1, &t2, &t3];

        let simd_result = simd::compute_burstiness_simd(&refs);
        let scalar_result = compute_burstiness_scalar(&refs);
        assert!(
            (simd_result - scalar_result).abs() < 1e-10,
            "Burstiness parity failed on 4 trades: SIMD={simd_result}, scalar={scalar_result}"
        );
    }

    #[cfg(feature = "simd-kyle-lambda")]
    #[test]
    fn test_kyle_lambda_simd_scalar_parity_small() {
        let trades = generate_trade_sequence(10, 789);
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();

        let simd_result = simd::compute_kyle_lambda_simd(&refs);
        let scalar_result = compute_kyle_lambda_scalar(&refs);

        assert!(
            (simd_result - scalar_result).abs() < 1e-8,
            "Kyle Lambda SIMD/scalar divergence on small window: SIMD={simd_result}, scalar={scalar_result}"
        );
    }

    #[cfg(feature = "simd-kyle-lambda")]
    #[test]
    fn test_kyle_lambda_simd_scalar_parity_medium() {
        let trades = generate_trade_sequence(200, 101);
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();

        let simd_result = simd::compute_kyle_lambda_simd(&refs);
        let scalar_result = compute_kyle_lambda_scalar(&refs);

        assert!(
            (simd_result - scalar_result).abs() < 1e-6,
            "Kyle Lambda SIMD/scalar divergence on medium window: SIMD={simd_result}, scalar={scalar_result}"
        );
    }

    #[cfg(feature = "simd-kyle-lambda")]
    #[test]
    fn test_kyle_lambda_simd_scalar_parity_large() {
        // Large window triggers subsampling in SIMD path
        let trades = generate_trade_sequence(600, 202);
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();

        let simd_result = simd::compute_kyle_lambda_simd(&refs);
        let scalar_result = compute_kyle_lambda_scalar(&refs);

        // Larger tolerance for subsampled windows — both paths subsample
        assert!(
            (simd_result - scalar_result).abs() < 1e-4,
            "Kyle Lambda SIMD/scalar divergence on large window: SIMD={simd_result}, scalar={scalar_result}"
        );
    }

    #[cfg(feature = "simd-kyle-lambda")]
    #[test]
    fn test_kyle_lambda_simd_scalar_parity_edge_cases() {
        // Minimum: 2 trades
        let t0 = make_snapshot(0, 50000.0, 1.0, false);
        let t1 = make_snapshot(1000, 50010.0, 2.0, true);
        let refs = vec![&t0, &t1];

        let simd_result = simd::compute_kyle_lambda_simd(&refs);
        let scalar_result = compute_kyle_lambda_scalar(&refs);
        assert!(
            (simd_result - scalar_result).abs() < 1e-10,
            "Kyle Lambda parity failed on 2 trades: SIMD={simd_result}, scalar={scalar_result}"
        );

        // All buys: extreme imbalance
        let all_buys: Vec<TradeSnapshot> = (0..20)
            .map(|i| make_snapshot(i * 100, 50000.0 + i as f64, 1.0, false))
            .collect();
        let refs: Vec<&TradeSnapshot> = all_buys.iter().collect();

        let simd_result = simd::compute_kyle_lambda_simd(&refs);
        let scalar_result = compute_kyle_lambda_scalar(&refs);
        assert!(
            (simd_result - scalar_result).abs() < 1e-8,
            "Kyle Lambda parity failed on all-buys: SIMD={simd_result}, scalar={scalar_result}"
        );

        // All sells: extreme imbalance other direction
        let all_sells: Vec<TradeSnapshot> = (0..20)
            .map(|i| make_snapshot(i * 100, 50000.0 + i as f64, 1.0, true))
            .collect();
        let refs: Vec<&TradeSnapshot> = all_sells.iter().collect();

        let simd_result = simd::compute_kyle_lambda_simd(&refs);
        let scalar_result = compute_kyle_lambda_scalar(&refs);
        assert!(
            (simd_result - scalar_result).abs() < 1e-8,
            "Kyle Lambda parity failed on all-sells: SIMD={simd_result}, scalar={scalar_result}"
        );
    }

    #[cfg(all(feature = "simd-burstiness", feature = "simd-kyle-lambda"))]
    #[test]
    fn test_simd_parity_sweep_window_sizes() {
        // Sweep through various window sizes to catch alignment/remainder bugs
        for size in [
            2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 255, 256,
        ] {
            let trades = generate_trade_sequence(size, size as u64 * 37);
            let refs: Vec<&TradeSnapshot> = trades.iter().collect();

            let burst_simd = simd::compute_burstiness_simd(&refs);
            let burst_scalar = compute_burstiness_scalar(&refs);
            assert!(
                (burst_simd - burst_scalar).abs() < 1e-8,
                "Burstiness parity failed at size {size}: SIMD={burst_simd}, scalar={burst_scalar}"
            );

            let kyle_simd = simd::compute_kyle_lambda_simd(&refs);
            let kyle_scalar = compute_kyle_lambda_scalar(&refs);
            assert!(
                (kyle_simd - kyle_scalar).abs() < 1e-6,
                "Kyle Lambda parity failed at size {size}: SIMD={kyle_simd}, scalar={kyle_scalar}"
            );
        }
    }
}

/// Property-based tests for inter-bar feature bounds invariants.
/// Uses proptest to verify that computed features stay within documented ranges
/// for arbitrary inputs, catching edge cases that hand-written tests miss.
#[cfg(test)]
mod branchless_ofi_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_accumulate_all_buys() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 2.0, false),
            make_snapshot(3000, 50000.0, 3.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (buy, sell) = accumulate_buy_sell_branchless(&refs);
        assert!((buy - 6.0).abs() < 1e-6, "buy={buy}, expected 6.0");
        assert!(sell.abs() < 1e-6, "sell={sell}, expected 0.0");
    }

    #[test]
    fn test_accumulate_all_sells() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, true),
            make_snapshot(2000, 50000.0, 2.0, true),
            make_snapshot(3000, 50000.0, 3.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (buy, sell) = accumulate_buy_sell_branchless(&refs);
        assert!(buy.abs() < 1e-6, "buy={buy}, expected 0.0");
        assert!((sell - 6.0).abs() < 1e-6, "sell={sell}, expected 6.0");
    }

    #[test]
    fn test_accumulate_balanced() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 5.0, false),
            make_snapshot(2000, 50000.0, 5.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (buy, sell) = accumulate_buy_sell_branchless(&refs);
        assert!((buy - 5.0).abs() < 1e-6, "buy={buy}, expected 5.0");
        assert!((sell - 5.0).abs() < 1e-6, "sell={sell}, expected 5.0");
    }

    #[test]
    fn test_accumulate_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let (buy, sell) = accumulate_buy_sell_branchless(&refs);
        assert_eq!(buy, 0.0);
        assert_eq!(sell, 0.0);
    }

    #[test]
    fn test_accumulate_single_trade() {
        let trades = vec![make_snapshot(1000, 50000.0, 7.5, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (buy, sell) = accumulate_buy_sell_branchless(&refs);
        assert!((buy - 7.5).abs() < 1e-6, "buy={buy}, expected 7.5");
        assert!(sell.abs() < 1e-6, "sell={sell}, expected 0.0");
    }

    #[test]
    fn test_accumulate_odd_count_remainder_path() {
        // 3 trades: pair processes first 2, scalar remainder processes 3rd
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 2.0, true),
            make_snapshot(3000, 50000.0, 4.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (buy, sell) = accumulate_buy_sell_branchless(&refs);
        assert!((buy - 5.0).abs() < 1e-6, "buy={buy}, expected 5.0 (1+4)");
        assert!((sell - 2.0).abs() < 1e-6, "sell={sell}, expected 2.0");
    }

    #[test]
    fn test_ofi_branchless_all_buys() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 1.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let ofi = compute_ofi_branchless(&refs);
        assert!((ofi - 1.0).abs() < 1e-10, "ofi={ofi}, expected 1.0");
    }

    #[test]
    fn test_ofi_branchless_all_sells() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, true),
            make_snapshot(2000, 50000.0, 1.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let ofi = compute_ofi_branchless(&refs);
        assert!((ofi - (-1.0)).abs() < 1e-10, "ofi={ofi}, expected -1.0");
    }

    #[test]
    fn test_ofi_branchless_balanced() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 3.0, false),
            make_snapshot(2000, 50000.0, 3.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let ofi = compute_ofi_branchless(&refs);
        assert!(ofi.abs() < 1e-10, "ofi={ofi}, expected 0.0");
    }

    #[test]
    fn test_ofi_branchless_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let ofi = compute_ofi_branchless(&refs);
        assert_eq!(ofi, 0.0, "empty trades should return 0.0");
    }

    #[test]
    fn test_ofi_branchless_single_trade() {
        let trades = vec![make_snapshot(1000, 50000.0, 10.0, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let ofi = compute_ofi_branchless(&refs);
        assert!(
            (ofi - 1.0).abs() < 1e-10,
            "ofi={ofi}, expected 1.0 for single buy"
        );
    }

    #[test]
    fn test_ofi_branchless_bounded() {
        // Asymmetric volumes: OFI must still be in [-1, 1]
        let trades = vec![
            make_snapshot(1000, 50000.0, 100.0, false),
            make_snapshot(2000, 50000.0, 0.001, true),
            make_snapshot(3000, 50000.0, 50.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let ofi = compute_ofi_branchless(&refs);
        assert!(ofi >= -1.0 && ofi <= 1.0, "ofi={ofi} out of [-1, 1]");
        assert!(ofi > 0.0, "ofi should be positive (buy-dominated)");
    }
}

#[cfg(test)]
mod extract_cache_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_lookback_cache_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let cache = extract_lookback_cache(&refs);
        assert!(cache.prices.is_empty());
        assert!(cache.volumes.is_empty());
        assert_eq!(cache.total_volume, 0.0);
    }

    #[test]
    fn test_lookback_cache_single_trade() {
        let trades = vec![make_snapshot(1000, 50000.0, 2.5, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        assert_eq!(cache.prices.len(), 1);
        assert!((cache.open - 50000.0).abs() < 1e-6);
        assert!((cache.close - 50000.0).abs() < 1e-6);
        assert!((cache.high - 50000.0).abs() < 1e-6);
        assert!((cache.low - 50000.0).abs() < 1e-6);
        assert!((cache.total_volume - 2.5).abs() < 1e-6);
    }

    #[test]
    fn test_lookback_cache_ascending_prices() {
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false),
            make_snapshot(2000, 200.0, 2.0, false),
            make_snapshot(3000, 300.0, 3.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        assert!((cache.open - 100.0).abs() < 1e-6);
        assert!((cache.close - 300.0).abs() < 1e-6);
        assert!((cache.high - 300.0).abs() < 1e-6);
        assert!((cache.low - 100.0).abs() < 1e-6);
        assert!((cache.total_volume - 6.0).abs() < 1e-6);
    }

    #[test]
    fn test_lookback_cache_ohlc_invariants() {
        // V-shape: high in middle, low at edges
        let trades = vec![
            make_snapshot(1000, 50.0, 1.0, false),
            make_snapshot(2000, 100.0, 1.0, true),
            make_snapshot(3000, 30.0, 1.0, false),
            make_snapshot(4000, 80.0, 1.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        assert!(cache.high >= cache.open, "high >= open");
        assert!(cache.high >= cache.close, "high >= close");
        assert!(cache.low <= cache.open, "low <= open");
        assert!(cache.low <= cache.close, "low <= close");
        assert!((cache.high - 100.0).abs() < 1e-6);
        assert!((cache.low - 30.0).abs() < 1e-6);
    }

    #[test]
    fn test_lookback_cache_all_same_price() {
        let trades = vec![
            make_snapshot(1000, 42000.0, 1.0, false),
            make_snapshot(2000, 42000.0, 2.0, true),
            make_snapshot(3000, 42000.0, 3.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        assert!(
            (cache.high - cache.low).abs() < 1e-6,
            "same price: high == low"
        );
        assert!(
            (cache.open - cache.close).abs() < 1e-6,
            "same price: open == close"
        );
    }

    #[test]
    fn test_ohlc_batch_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let (o, h, l, c) = extract_ohlc_batch(&refs);
        assert_eq!(o, 0.0);
        assert_eq!(h, 0.0);
        assert_eq!(l, 0.0);
        assert_eq!(c, 0.0);
    }

    #[test]
    fn test_ohlc_batch_matches_cache() {
        let trades = vec![
            make_snapshot(1000, 50.0, 1.0, false),
            make_snapshot(2000, 80.0, 2.0, true),
            make_snapshot(3000, 30.0, 3.0, false),
            make_snapshot(4000, 60.0, 4.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        let (o, h, l, c) = extract_ohlc_batch(&refs);
        assert!((o - cache.open).abs() < 1e-10, "open mismatch");
        assert!((h - cache.high).abs() < 1e-10, "high mismatch");
        assert!((l - cache.low).abs() < 1e-10, "low mismatch");
        assert!((c - cache.close).abs() < 1e-10, "close mismatch");
    }
}

#[cfg(test)]
mod extract_prices_ohlc_cached_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let (prices, (o, h, l, c)) = extract_prices_and_ohlc_cached(&refs);
        assert!(prices.is_empty());
        assert_eq!(o, 0.0);
        assert_eq!(h, 0.0);
        assert_eq!(l, 0.0);
        assert_eq!(c, 0.0);
    }

    #[test]
    fn test_single_trade() {
        let trades = vec![make_snapshot(1000, 42000.0, 1.0, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (prices, (o, h, l, c)) = extract_prices_and_ohlc_cached(&refs);
        assert_eq!(prices.len(), 1);
        assert!((o - c).abs() < 1e-6, "single trade: open == close");
        assert!((h - l).abs() < 1e-6, "single trade: high == low");
    }

    #[test]
    fn test_ohlc_invariants() {
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false),
            make_snapshot(2000, 150.0, 1.0, true),
            make_snapshot(3000, 80.0, 1.0, false),
            make_snapshot(4000, 120.0, 1.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (prices, (o, h, l, c)) = extract_prices_and_ohlc_cached(&refs);
        assert_eq!(prices.len(), 4);
        assert!(h >= o && h >= c, "high must be >= open and close");
        assert!(l <= o && l <= c, "low must be <= open and close");
        // Verify OHLC values match expectations
        assert!((o - 100.0).abs() < 1e-6);
        assert!((h - 150.0).abs() < 1e-6);
        assert!((l - 80.0).abs() < 1e-6);
        assert!((c - 120.0).abs() < 1e-6);
    }

    #[test]
    fn test_prices_match_lookback_cache() {
        let trades = vec![
            make_snapshot(1000, 50.0, 2.0, false),
            make_snapshot(2000, 80.0, 3.0, true),
            make_snapshot(3000, 30.0, 1.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (prices, (o, h, l, c)) = extract_prices_and_ohlc_cached(&refs);
        let cache = extract_lookback_cache(&refs);
        // Verify parity with extract_lookback_cache
        assert_eq!(prices.len(), cache.prices.len());
        for (a, b) in prices.iter().zip(cache.prices.iter()) {
            assert!((a - b).abs() < 1e-10, "price mismatch: {a} vs {b}");
        }
        assert!((o - cache.open).abs() < 1e-10);
        assert!((h - cache.high).abs() < 1e-10);
        assert!((l - cache.low).abs() < 1e-10);
        assert!((c - cache.close).abs() < 1e-10);
    }
}

#[cfg(test)]
mod entropy_readonly_cache_tests {
    use super::*;

    #[test]
    fn test_readonly_cache_miss_returns_none() {
        let cache = EntropyCache::new();
        let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64).collect();
        let result = compute_entropy_adaptive_cached_readonly(&prices, &cache);
        assert!(result.is_none(), "Empty cache should return None");
    }

    #[test]
    fn test_readonly_cache_hit_returns_value() {
        let mut cache = EntropyCache::new();
        let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64).collect();
        // Populate cache via write path
        let expected = compute_entropy_adaptive_cached(&prices, &mut cache);
        // Read-only should now hit
        let result = compute_entropy_adaptive_cached_readonly(&prices, &cache);
        assert!(result.is_some(), "Populated cache should return Some");
        assert!(
            (result.unwrap() - expected).abs() < 1e-10,
            "Cached value mismatch: {} vs {expected}",
            result.unwrap()
        );
    }

    #[test]
    fn test_readonly_large_window_returns_none() {
        // n >= 500 uses ApEn (not cached), so readonly should return None
        let cache = EntropyCache::new();
        let prices: Vec<f64> = (0..600).map(|i| 100.0 + i as f64 * 0.1).collect();
        let result = compute_entropy_adaptive_cached_readonly(&prices, &cache);
        assert!(
            result.is_none(),
            "Large window (n>=500) should return None from readonly"
        );
    }
}

#[cfg(test)]
mod entropy_adaptive_cached_tests {
    use super::*;

    #[test]
    fn test_cached_matches_uncached_pe_path() {
        // n < 500 → Permutation Entropy path
        let prices: Vec<f64> = (0..200)
            .map(|i| 100.0 + (i as f64 * 0.7).sin() * 5.0)
            .collect();
        let uncached = compute_entropy_adaptive(&prices);
        let mut cache = EntropyCache::new();
        let cached = compute_entropy_adaptive_cached(&prices, &mut cache);
        assert!(
            (uncached - cached).abs() < 1e-14,
            "PE path mismatch: uncached={uncached}, cached={cached}"
        );
    }

    #[test]
    fn test_cached_matches_uncached_apen_path() {
        // n >= 500 → Approximate Entropy path
        let prices: Vec<f64> = (0..600)
            .map(|i| 100.0 + (i as f64 * 0.3).sin() * 10.0)
            .collect();
        let uncached = compute_entropy_adaptive(&prices);
        let mut cache = EntropyCache::new();
        let cached = compute_entropy_adaptive_cached(&prices, &mut cache);
        assert!(
            (uncached - cached).abs() < 1e-14,
            "ApEn path mismatch: uncached={uncached}, cached={cached}"
        );
    }

    #[test]
    fn test_cached_second_call_hits_cache() {
        let prices: Vec<f64> = (0..100).map(|i| 50.0 + i as f64).collect();
        let mut cache = EntropyCache::new();

        let first = compute_entropy_adaptive_cached(&prices, &mut cache);
        let (hits_before, misses_before, _) = cache.metrics();

        let second = compute_entropy_adaptive_cached(&prices, &mut cache);
        let (hits_after, _misses_after, _) = cache.metrics();

        assert!(
            (first - second).abs() < 1e-14,
            "Same prices must produce identical entropy"
        );
        assert_eq!(
            hits_after,
            hits_before + 1,
            "Second call should be a cache hit"
        );
        assert_eq!(misses_before, 1, "First call should be a cache miss");
    }

    #[test]
    fn test_cached_different_sequences_miss() {
        let mut cache = EntropyCache::new();
        let prices_a: Vec<f64> = (0..100).map(|i| 100.0 + i as f64).collect();
        let prices_b: Vec<f64> = (0..100).map(|i| 200.0 + i as f64 * 2.0).collect();

        let _ea = compute_entropy_adaptive_cached(&prices_a, &mut cache);
        let (_, misses_after_a, _) = cache.metrics();
        assert_eq!(misses_after_a, 1);

        let _eb = compute_entropy_adaptive_cached(&prices_b, &mut cache);
        let (_, misses_after_b, _) = cache.metrics();
        assert_eq!(misses_after_b, 2, "Different sequence should miss cache");
    }
}

#[cfg(test)]
mod gk_negative_variance_tests {
    use super::*;

    #[test]
    fn test_gk_negative_variance_returns_zero() {
        // When close moves far from open relative to high-low range,
        // the subtractive term dominates → negative variance → returns 0.0
        // O=100, C=200, H=101, L=99 → log(C/O) >> log(H/L)
        let gk = compute_garman_klass_with_ohlc(100.0, 101.0, 99.0, 200.0);
        assert_eq!(gk, 0.0, "Negative variance should return 0.0, got {gk}");
    }

    #[test]
    fn test_gk_borderline_variance() {
        // When log(H/L)² term roughly equals the subtractive term
        // O=100, C=110, H=115, L=95
        let gk = compute_garman_klass_with_ohlc(100.0, 115.0, 95.0, 110.0);
        assert!(gk >= 0.0, "GK should never be negative, got {gk}");
        assert!(gk.is_finite(), "GK must be finite");
    }
}

#[cfg(test)]
mod kyle_kaufman_edge_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    // --- Kyle Lambda edge cases ---

    #[test]
    fn test_kyle_lambda_single_trade() {
        let trades = vec![make_snapshot(1000, 50000.0, 1.0, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kl = compute_kyle_lambda(&refs);
        assert_eq!(kl, 0.0, "single trade → 0.0 (n < 2)");
    }

    #[test]
    fn test_kyle_lambda_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let kl = compute_kyle_lambda(&refs);
        assert_eq!(kl, 0.0, "empty → 0.0");
    }

    #[test]
    fn test_kyle_lambda_zero_price_change() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 2.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kl = compute_kyle_lambda(&refs);
        assert_eq!(kl, 0.0, "no price change → kyle lambda = 0");
    }

    #[test]
    fn test_kyle_lambda_balanced_volume() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 5.0, false),
            make_snapshot(2000, 51000.0, 5.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kl = compute_kyle_lambda(&refs);
        assert_eq!(kl, 0.0, "balanced volume → zero imbalance → 0.0");
    }

    #[test]
    fn test_kyle_lambda_all_same_price() {
        let trades = vec![
            make_snapshot(1000, 42000.0, 1.0, false),
            make_snapshot(2000, 42000.0, 2.0, false),
            make_snapshot(3000, 42000.0, 3.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kl = compute_kyle_lambda(&refs);
        assert!(kl.is_finite(), "kyle lambda must be finite for same-price");
    }

    #[test]
    fn test_kyle_lambda_no_panic_large_window() {
        // 100+ trades should not panic or produce NaN
        let trades: Vec<_> = (0..150)
            .map(|i| make_snapshot(i * 1000, 50000.0 + (i as f64) * 0.1, 1.0, i % 3 == 0))
            .collect();
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let kl = compute_kyle_lambda(&refs);
        assert!(
            kl.is_finite(),
            "kyle lambda must be finite for large window: {kl}"
        );
    }

    // --- Kaufman ER edge cases ---

    #[test]
    fn test_kaufman_er_single_price() {
        let prices = vec![50000.0];
        let er = compute_kaufman_er(&prices);
        assert_eq!(er, 0.0, "single price → 0.0 (n < 2)");
    }

    #[test]
    fn test_kaufman_er_empty() {
        let prices: Vec<f64> = vec![];
        let er = compute_kaufman_er(&prices);
        assert_eq!(er, 0.0, "empty → 0.0");
    }

    #[test]
    fn test_kaufman_er_monotonic_up() {
        let prices: Vec<f64> = (0..10).map(|i| 100.0 + i as f64).collect();
        let er = compute_kaufman_er(&prices);
        assert!((er - 1.0).abs() < 1e-10, "monotonic → ER = 1.0, got {er}");
    }

    #[test]
    fn test_kaufman_er_monotonic_down() {
        let prices: Vec<f64> = (0..10).map(|i| 100.0 - i as f64).collect();
        let er = compute_kaufman_er(&prices);
        assert!(
            (er - 1.0).abs() < 1e-10,
            "monotonic down → ER = 1.0, got {er}"
        );
    }

    #[test]
    fn test_kaufman_er_all_same_price() {
        let prices = vec![42000.0; 20];
        let er = compute_kaufman_er(&prices);
        assert_eq!(er, 0.0, "all same → ER = 0.0 (zero volatility)");
    }

    #[test]
    fn test_kaufman_er_bounded_zigzag() {
        // Zigzag: net movement is 0, volatility is high → ER near 0
        let prices: Vec<f64> = (0..20)
            .map(|i| if i % 2 == 0 { 100.0 } else { 110.0 })
            .collect();
        let er = compute_kaufman_er(&prices);
        assert!(er >= 0.0 && er <= 1.0, "ER must be in [0, 1], got {er}");
        assert!(er < 0.1, "zigzag should have low ER, got {er}");
    }

    #[test]
    fn test_kaufman_er_two_prices() {
        let prices = vec![100.0, 200.0];
        let er = compute_kaufman_er(&prices);
        assert!(
            (er - 1.0).abs() < 1e-10,
            "two prices with change → ER = 1.0, got {er}"
        );
    }
}

#[cfg(test)]
mod volume_moments_edge_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_moments_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let (s, k) = compute_volume_moments(&refs);
        assert_eq!(s, 0.0);
        assert_eq!(k, 0.0);
    }

    #[test]
    fn test_moments_one_trade() {
        let trades = vec![make_snapshot(1000, 50000.0, 5.0, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (s, k) = compute_volume_moments(&refs);
        assert_eq!(s, 0.0, "n < 3 → 0.0");
        assert_eq!(k, 0.0, "n < 3 → 0.0");
    }

    #[test]
    fn test_moments_two_trades() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 10.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (s, k) = compute_volume_moments(&refs);
        assert_eq!(s, 0.0, "n < 3 → 0.0");
        assert_eq!(k, 0.0, "n < 3 → 0.0");
    }

    #[test]
    fn test_moments_symmetric_distribution() {
        // Symmetric volumes → skewness near 0
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 5.0, true),
            make_snapshot(3000, 50000.0, 9.0, false),
            make_snapshot(4000, 50000.0, 5.0, true),
            make_snapshot(5000, 50000.0, 1.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (s, _k) = compute_volume_moments(&refs);
        assert!(
            s.abs() < 0.5,
            "symmetric distribution → skew near 0, got {s}"
        );
    }

    #[test]
    fn test_moments_cached_matches_trade_version() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 3.0, true),
            make_snapshot(3000, 50000.0, 7.0, false),
            make_snapshot(4000, 50000.0, 2.0, true),
            make_snapshot(5000, 50000.0, 5.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (s1, k1) = compute_volume_moments(&refs);

        let volumes: Vec<f64> = trades.iter().map(|t| t.volume.to_f64()).collect();
        let (s2, k2) = compute_volume_moments_cached(&volumes);

        assert!(
            (s1 - s2).abs() < 1e-10,
            "skew mismatch: trade={s1}, cached={s2}"
        );
        assert!(
            (k1 - k2).abs() < 1e-10,
            "kurt mismatch: trade={k1}, cached={k2}"
        );
    }

    #[test]
    fn test_moments_cached_empty() {
        let (s, k) = compute_volume_moments_cached(&[]);
        assert_eq!(s, 0.0);
        assert_eq!(k, 0.0);
    }
}

#[cfg(test)]
mod volume_moments_with_mean_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_with_mean_matches_standard() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 3.0, true),
            make_snapshot(3000, 50000.0, 7.0, false),
            make_snapshot(4000, 50000.0, 2.0, true),
            make_snapshot(5000, 50000.0, 5.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let (s1, k1) = compute_volume_moments(&refs);

        let volumes: Vec<f64> = trades.iter().map(|t| t.volume.to_f64()).collect();
        let mean = volumes.iter().sum::<f64>() / volumes.len() as f64;
        let (s2, k2) = compute_volume_moments_with_mean(&volumes, mean);

        assert!(
            (s1 - s2).abs() < 1e-10,
            "skew mismatch: standard={s1}, with_mean={s2}"
        );
        assert!(
            (k1 - k2).abs() < 1e-10,
            "kurt mismatch: standard={k1}, with_mean={k2}"
        );
    }

    #[test]
    fn test_with_mean_too_few() {
        let (s, k) = compute_volume_moments_with_mean(&[1.0, 2.0], 1.5);
        assert_eq!(s, 0.0);
        assert_eq!(k, 0.0);
    }

    #[test]
    fn test_with_mean_constant_volumes() {
        let vols = vec![5.0; 10];
        let (s, k) = compute_volume_moments_with_mean(&vols, 5.0);
        assert_eq!(s, 0.0, "constant volumes → zero skewness");
        assert_eq!(k, 0.0, "constant volumes → zero kurtosis");
    }

    #[test]
    fn test_with_mean_right_skewed() {
        // Right-skewed: most volumes small, one very large
        let vols = vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 100.0];
        let mean = vols.iter().sum::<f64>() / vols.len() as f64;
        let (s, _k) = compute_volume_moments_with_mean(&vols, mean);
        assert!(
            s > 0.5,
            "right-skewed distribution → positive skew, got {s}"
        );
    }
}

#[cfg(test)]
mod garman_klass_ohlc_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_ohlc_matches_trade_version() {
        // Build trades with known OHLC: open=100, high=110, low=95, close=105
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false), // open
            make_snapshot(2000, 110.0, 1.0, true),  // high
            make_snapshot(3000, 95.0, 1.0, false),  // low
            make_snapshot(4000, 105.0, 1.0, true),  // close
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let gk_trades = compute_garman_klass(&refs);
        let gk_ohlc = compute_garman_klass_with_ohlc(100.0, 110.0, 95.0, 105.0);

        assert!(
            (gk_trades - gk_ohlc).abs() < 1e-10,
            "GK mismatch: trades={gk_trades}, ohlc={gk_ohlc}"
        );
    }

    #[test]
    fn test_ohlc_flat_price() {
        let gk = compute_garman_klass_with_ohlc(100.0, 100.0, 100.0, 100.0);
        assert_eq!(gk, 0.0, "flat price → zero volatility");
    }

    #[test]
    fn test_ohlc_zero_price_guard() {
        let gk = compute_garman_klass_with_ohlc(0.0, 100.0, 50.0, 75.0);
        assert_eq!(gk, 0.0, "zero open → guard returns 0");
    }

    #[test]
    fn test_ohlc_high_volatility() {
        // Large range: high/low = 2x
        let gk = compute_garman_klass_with_ohlc(100.0, 200.0, 100.0, 150.0);
        assert!(gk > 0.0, "wide range → positive volatility");
        assert!(gk > 0.3, "very wide range → high volatility, got {gk}");
    }

    #[test]
    fn test_ohlc_close_equals_open() {
        // Doji-like: close == open, but high/low differ
        let gk = compute_garman_klass_with_ohlc(100.0, 110.0, 90.0, 100.0);
        assert!(gk > 0.0, "range exists even if close==open");
    }
}

#[cfg(test)]
mod burstiness_edge_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_burstiness_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let b = compute_burstiness(&refs);
        assert_eq!(b, 0.0, "empty → 0.0");
    }

    #[test]
    fn test_burstiness_single_trade() {
        let trades = vec![make_snapshot(1000, 50000.0, 1.0, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let b = compute_burstiness(&refs);
        assert_eq!(b, 0.0, "single trade → 0.0 (n < 2)");
    }

    #[test]
    fn test_burstiness_two_trades() {
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(2000, 50000.0, 1.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let b = compute_burstiness(&refs);
        // Only 1 inter-arrival interval, sigma = 0 → B = (0 - mu) / (0 + mu) = -1
        assert!(b.is_finite(), "burstiness must be finite for 2 trades");
        assert!(b >= -1.0 && b <= 1.0, "burstiness out of [-1, 1]: {b}");
    }

    #[test]
    fn test_burstiness_regular_intervals() {
        // Perfect regularity: all intervals identical → sigma = 0 → B = -1
        let trades: Vec<_> = (0..20)
            .map(|i| make_snapshot(i * 1000, 50000.0, 1.0, false))
            .collect();
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let b = compute_burstiness(&refs);
        assert!(
            (b - (-1.0)).abs() < 0.01,
            "regular intervals → B ≈ -1, got {b}"
        );
    }

    #[test]
    fn test_burstiness_same_timestamp() {
        // All same timestamp → all intervals = 0 → mean = 0, sigma = 0 → denominator guarded
        let trades: Vec<_> = (0..10)
            .map(|_| make_snapshot(1000, 50000.0, 1.0, false))
            .collect();
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let b = compute_burstiness(&refs);
        assert!(b.is_finite(), "same-timestamp must not produce NaN");
    }

    #[test]
    fn test_burstiness_bounded() {
        // Variable intervals: mix of fast and slow arrivals
        let trades = vec![
            make_snapshot(1000, 50000.0, 1.0, false),
            make_snapshot(1001, 50000.0, 1.0, true), // 1 us gap
            make_snapshot(1002, 50000.0, 1.0, false), // 1 us gap
            make_snapshot(5000, 50000.0, 1.0, true), // 3998 us gap (bursty)
            make_snapshot(5001, 50000.0, 1.0, false), // 1 us gap
            make_snapshot(10000, 50000.0, 1.0, true), // 4999 us gap (bursty)
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let b = compute_burstiness(&refs);
        assert!(b >= -1.0 && b <= 1.0, "burstiness out of [-1, 1]: {b}");
        assert!(
            b > 0.0,
            "variable intervals should show positive burstiness, got {b}"
        );
    }
}

#[cfg(test)]
mod permutation_entropy_edge_tests {
    use super::*;

    #[test]
    fn test_pe_insufficient_data() {
        let prices = vec![100.0; 5]; // n < 10
        let pe = compute_permutation_entropy(&prices);
        assert_eq!(pe, 1.0, "n < 10 → 1.0 (insufficient data)");
    }

    #[test]
    fn test_pe_exactly_10_m2_path() {
        // n=10 → uses M=2 path (10 <= n < 30)
        let prices: Vec<f64> = (0..10).map(|i| 100.0 + i as f64).collect();
        let pe = compute_permutation_entropy(&prices);
        assert_eq!(pe, 0.0, "monotonic ascending → PE = 0.0 (early exit)");
    }

    #[test]
    fn test_pe_exactly_30_m3_path() {
        // n=30 → uses M=3 path (n >= 30)
        let prices: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
        let pe = compute_permutation_entropy(&prices);
        assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
        assert!(pe < 0.2, "monotonic ascending → low PE, got {pe}");
    }

    #[test]
    fn test_pe_zigzag_high_entropy() {
        // Alternating pattern should have high entropy
        let prices: Vec<f64> = (0..40)
            .map(|i| if i % 2 == 0 { 100.0 } else { 110.0 })
            .collect();
        let pe = compute_permutation_entropy(&prices);
        assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
    }

    #[test]
    fn test_pe_all_same_price() {
        // All same → all pairs equal → pattern 0 exclusively → PE = 0
        let prices = vec![42000.0; 50];
        let pe = compute_permutation_entropy(&prices);
        assert!(pe.is_finite(), "same-price must not produce NaN");
        assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
    }

    #[test]
    fn test_pe_monotonic_descending() {
        let prices: Vec<f64> = (0..35).map(|i| 200.0 - i as f64).collect();
        let pe = compute_permutation_entropy(&prices);
        assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
        assert!(pe < 0.3, "monotonic descending → low PE, got {pe}");
    }

    #[test]
    fn test_pe_m2_zigzag() {
        // 20 trades in M=2 path — zigzag should produce max entropy (both up/down patterns)
        let prices: Vec<f64> = (0..20)
            .map(|i| if i % 2 == 0 { 100.0 } else { 110.0 })
            .collect();
        let pe = compute_permutation_entropy(&prices);
        assert!(pe >= 0.0 && pe <= 1.0, "PE must be in [0, 1], got {pe}");
        // Perfect zigzag should have very high entropy (near 1.0 for M=2)
        assert!(
            pe > 0.8,
            "perfect zigzag (M=2) should have high PE, got {pe}"
        );
    }

    #[test]
    fn test_pe_large_window_dominant_pattern() {
        // Regression test: u8 pattern counts saturated at 255 for windows >255 trades.
        // With 600 ascending prices (1 dominant pattern), count reaches ~598.
        // u8 saturation would cap at 255, producing wrong entropy (non-zero).
        // u16 handles this correctly.
        let prices: Vec<f64> = (0..600).map(|i| 100.0 + i as f64 * 0.01).collect();
        let pe = compute_permutation_entropy(&prices);
        // Monotonic ascending = all patterns identical = entropy 0
        assert_eq!(pe, 0.0, "600-trade monotonic should yield PE=0, got {pe}");
    }

    #[test]
    fn test_pe_large_window_near_uniform() {
        // 500 trades with near-uniform pattern distribution
        // Verifies accuracy at realistic FixedCount(500) window size
        let prices: Vec<f64> = (0..500)
            .map(|i| 100.0 + (i as f64 * 0.73).sin() * 5.0)
            .collect();
        let pe = compute_permutation_entropy(&prices);
        assert!(
            pe > 0.5,
            "varied 500-trade series should have moderate-high PE, got {pe}"
        );
        assert!(pe <= 1.0, "PE must be <= 1.0, got {pe}");
    }
}

#[cfg(test)]
mod garman_hurst_edge_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    // --- Garman-Klass edge cases ---

    #[test]
    fn test_gk_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let gk = compute_garman_klass(&refs);
        assert_eq!(gk, 0.0);
    }

    #[test]
    fn test_gk_single_trade() {
        let trades = vec![make_snapshot(1000, 50000.0, 1.0, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let gk = compute_garman_klass(&refs);
        // Single trade: open=close=high=low, log(h/l)=0, log(c/o)=0 → variance=0 → 0.0
        assert_eq!(gk, 0.0, "single trade → GK = 0.0");
    }

    #[test]
    fn test_gk_all_same_price() {
        let trades = vec![
            make_snapshot(1000, 42000.0, 1.0, false),
            make_snapshot(2000, 42000.0, 2.0, true),
            make_snapshot(3000, 42000.0, 3.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let gk = compute_garman_klass(&refs);
        assert_eq!(gk, 0.0, "same price → zero range → GK = 0.0");
    }

    #[test]
    fn test_gk_positive_for_volatile_window() {
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false),
            make_snapshot(2000, 120.0, 1.0, true),
            make_snapshot(3000, 80.0, 1.0, false),
            make_snapshot(4000, 110.0, 1.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let gk = compute_garman_klass(&refs);
        assert!(gk > 0.0, "volatile window → GK > 0, got {gk}");
        assert!(gk.is_finite(), "GK must be finite");
    }

    #[test]
    fn test_gk_with_ohlc_matches_trade_version() {
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false),
            make_snapshot(2000, 150.0, 1.0, true),
            make_snapshot(3000, 80.0, 1.0, false),
            make_snapshot(4000, 120.0, 1.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let gk_trades = compute_garman_klass(&refs);
        let gk_ohlc = compute_garman_klass_with_ohlc(100.0, 150.0, 80.0, 120.0);
        assert!(
            (gk_trades - gk_ohlc).abs() < 1e-10,
            "trade vs OHLC mismatch: {gk_trades} vs {gk_ohlc}"
        );
    }

    #[test]
    fn test_gk_with_ohlc_zero_price() {
        let gk = compute_garman_klass_with_ohlc(0.0, 100.0, 50.0, 75.0);
        assert_eq!(gk, 0.0, "zero open → guard returns 0.0");
    }

    // --- Hurst DFA edge cases ---

    #[test]
    fn test_hurst_insufficient_data() {
        let prices: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
        let h = compute_hurst_rescaled_range(&prices);
        assert_eq!(h, 0.5, "< 64 samples → neutral 0.5");
    }

    #[test]
    fn test_hurst_constant_prices() {
        let prices = vec![42000.0; 100];
        let h = compute_hurst_rescaled_range(&prices);
        assert!(h.is_finite(), "constant prices must not produce NaN/Inf");
        assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0, 1], got {h}");
    }

    #[test]
    fn test_hurst_trending_up() {
        let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64 * 0.5).collect();
        let h = compute_hurst_rescaled_range(&prices);
        assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0, 1], got {h}");
        // Trending should produce H > 0.5 (after soft clamping)
        assert!(
            h >= 0.45,
            "trending series: Hurst should be >= 0.45, got {h}"
        );
    }

    #[test]
    fn test_hurst_exactly_64_samples() {
        let prices: Vec<f64> = (0..64).map(|i| 100.0 + (i as f64).sin() * 10.0).collect();
        let h = compute_hurst_rescaled_range(&prices);
        assert!(h.is_finite(), "exactly 64 samples must not panic");
        assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0, 1], got {h}");
    }
}

#[cfg(test)]
mod soft_clamp_hurst_edge_tests {
    use super::*;

    #[test]
    fn test_soft_clamp_extreme_negative() {
        let h = soft_clamp_hurst(-100.0);
        assert!(
            h >= 0.0 && h <= 1.0,
            "Extreme negative should clamp to [0,1], got {h}"
        );
        assert!(h < 0.01, "h=-100 should clamp near 0, got {h}");
    }

    #[test]
    fn test_soft_clamp_extreme_positive() {
        let h = soft_clamp_hurst(100.0);
        assert!(
            h >= 0.0 && h <= 1.0,
            "Extreme positive should clamp to [0,1], got {h}"
        );
        assert!(h > 0.99, "h=100 should clamp near 1, got {h}");
    }

    #[test]
    fn test_soft_clamp_at_half() {
        let h = soft_clamp_hurst(0.5);
        assert!((h - 0.5).abs() < 0.05, "h=0.5 should map near 0.5, got {h}");
    }

    #[test]
    fn test_soft_clamp_nan_input() {
        let h = soft_clamp_hurst(f64::NAN);
        // NaN input should not produce a normal finite value; should remain NaN
        // or be handled gracefully (implementation-dependent)
        assert!(
            !h.is_normal() || (h >= 0.0 && h <= 1.0),
            "NaN input should produce NaN or clamped value, got {h}"
        );
    }

    #[test]
    fn test_soft_clamp_monotonicity() {
        // soft_clamp should be monotonically increasing
        let values = [-5.0, -1.0, 0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 5.0];
        for pair in values.windows(2) {
            let a = soft_clamp_hurst(pair[0]);
            let b = soft_clamp_hurst(pair[1]);
            assert!(
                b >= a,
                "soft_clamp should be monotonic: f({}) = {a} > f({}) = {b}",
                pair[0],
                pair[1]
            );
        }
    }

    #[test]
    fn test_soft_clamp_infinity() {
        let h_pos = soft_clamp_hurst(f64::INFINITY);
        let h_neg = soft_clamp_hurst(f64::NEG_INFINITY);
        // Should clamp to bounds
        assert!(
            h_pos >= 0.0 && h_pos <= 1.0 || h_pos.is_nan(),
            "Inf should clamp or NaN, got {h_pos}"
        );
        assert!(
            h_neg >= 0.0 && h_neg <= 1.0 || h_neg.is_nan(),
            "-Inf should clamp or NaN, got {h_neg}"
        );
    }
}

#[cfg(test)]
mod lookback_cache_finite_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(price: f64, volume: f64) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: 1000,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker: false,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_all_prices_finite_normal() {
        let trades = vec![
            make_snapshot(100.0, 1.0),
            make_snapshot(101.0, 2.0),
            make_snapshot(99.5, 1.5),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        assert!(cache.all_prices_finite, "Normal prices should be finite");
    }

    #[test]
    fn test_all_prices_finite_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let cache = extract_lookback_cache(&refs);
        assert!(
            cache.all_prices_finite,
            "Empty lookback should default to finite=true"
        );
    }

    #[test]
    fn test_all_prices_finite_single() {
        let trades = vec![make_snapshot(50000.0, 1.0)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        assert!(
            cache.all_prices_finite,
            "Single normal price should be finite"
        );
    }

    #[test]
    fn test_all_volumes_finite_normal() {
        let trades = vec![
            make_snapshot(100.0, 1.0),
            make_snapshot(101.0, 2.5),
            make_snapshot(99.5, 0.5),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        assert!(cache.all_volumes_finite, "Normal volumes should be finite");
    }

    #[test]
    fn test_all_volumes_finite_empty() {
        let refs: Vec<&TradeSnapshot> = vec![];
        let cache = extract_lookback_cache(&refs);
        assert!(
            cache.all_volumes_finite,
            "Empty lookback should default to volumes finite=true"
        );
    }
}

#[cfg(test)]
mod extract_lookback_cache_comprehensive_tests {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    #[test]
    fn test_ohlc_extraction_correct() {
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false), // open
            make_snapshot(2000, 110.0, 2.0, true),  // high
            make_snapshot(3000, 90.0, 1.5, false),  // low
            make_snapshot(4000, 105.0, 3.0, true),  // close
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);

        assert!(
            (cache.open - 100.0).abs() < 1e-6,
            "open should be first price"
        );
        assert!(
            (cache.close - 105.0).abs() < 1e-6,
            "close should be last price"
        );
        assert!(
            (cache.high - 110.0).abs() < 1e-6,
            "high should be max price"
        );
        assert!((cache.low - 90.0).abs() < 1e-6, "low should be min price");
    }

    #[test]
    fn test_total_volume_accumulation() {
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false),
            make_snapshot(2000, 101.0, 2.5, true),
            make_snapshot(3000, 99.0, 0.5, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);

        let expected_total = 1.0 + 2.5 + 0.5;
        assert!(
            (cache.total_volume - expected_total).abs() < 1e-6,
            "total_volume={}, expected={}",
            cache.total_volume,
            expected_total
        );
    }

    #[test]
    fn test_prices_vector_matches_trades() {
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false),
            make_snapshot(2000, 105.0, 2.0, true),
            make_snapshot(3000, 95.0, 1.0, false),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);

        assert_eq!(cache.prices.len(), 3);
        assert!((cache.prices[0] - 100.0).abs() < 1e-6);
        assert!((cache.prices[1] - 105.0).abs() < 1e-6);
        assert!((cache.prices[2] - 95.0).abs() < 1e-6);
    }

    #[test]
    fn test_first_volume_is_first_trade() {
        let trades = vec![
            make_snapshot(1000, 100.0, 7.5, false),
            make_snapshot(2000, 101.0, 2.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);
        assert!((cache.first_volume - 7.5).abs() < 1e-6);
    }

    #[test]
    fn test_single_trade_ohlc_all_same() {
        let trades = vec![make_snapshot(1000, 50000.0, 1.0, false)];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);

        assert!((cache.open - 50000.0).abs() < 1e-6);
        assert!((cache.close - 50000.0).abs() < 1e-6);
        assert!((cache.high - 50000.0).abs() < 1e-6);
        assert!((cache.low - 50000.0).abs() < 1e-6);
    }

    #[test]
    fn test_high_low_invariants() {
        let trades = vec![
            make_snapshot(1000, 100.0, 1.0, false),
            make_snapshot(2000, 200.0, 1.0, true),
            make_snapshot(3000, 50.0, 1.0, false),
            make_snapshot(4000, 150.0, 1.0, true),
        ];
        let refs: Vec<&TradeSnapshot> = trades.iter().collect();
        let cache = extract_lookback_cache(&refs);

        assert!(cache.high >= cache.low, "high must >= low");
        assert!(cache.high >= cache.open, "high must >= open");
        assert!(cache.high >= cache.close, "high must >= close");
        assert!(cache.low <= cache.open, "low must <= open");
        assert!(cache.low <= cache.close, "low must <= close");
    }
}

#[cfg(test)]
mod proptest_bounds {
    use super::*;
    use opendeviationbar_core::FixedPoint;
    use opendeviationbar_core::interbar_types::TradeSnapshot;
    use proptest::prelude::*;

    fn make_snapshot(ts: i64, price: f64, volume: f64, is_buyer_maker: bool) -> TradeSnapshot {
        TradeSnapshot {
            timestamp: ts,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint((volume * 1e8) as i64),
            is_buyer_maker,
            turnover: (price * volume * 1e8) as i128,
        }
    }

    /// Strategy: generate valid price sequences (positive, finite)
    fn price_sequence(min_len: usize, max_len: usize) -> impl Strategy<Value = Vec<f64>> {
        prop::collection::vec(1.0..=100_000.0_f64, min_len..=max_len)
    }

    /// Strategy: generate valid volume pairs (positive, finite)
    fn volume_pair() -> impl Strategy<Value = (f64, f64)> {
        (0.001..=1e9_f64, 0.001..=1e9_f64)
    }

    proptest! {
        /// OFI: (buy_vol - sell_vol) / (buy_vol + sell_vol) must be in [-1, 1]
        #[test]
        fn ofi_always_bounded((buy_vol, sell_vol) in volume_pair()) {
            let total = buy_vol + sell_vol;
            if total > f64::EPSILON {
                let ofi = (buy_vol - sell_vol) / total;
                prop_assert!(ofi >= -1.0 - f64::EPSILON && ofi <= 1.0 + f64::EPSILON,
                    "OFI={ofi} out of [-1, 1] for buy={buy_vol}, sell={sell_vol}");
            }
        }

        /// Kaufman ER must be in [0, 1] for valid price sequences
        #[test]
        fn kaufman_er_always_bounded(prices in price_sequence(2, 200)) {
            let er = compute_kaufman_er(&prices);
            prop_assert!(er >= 0.0 && er <= 1.0 + f64::EPSILON,
                "Kaufman ER={er} out of [0, 1] for {}-trade window", prices.len());
        }

        /// Permutation entropy must be in [0, 1] for valid price sequences
        #[test]
        fn permutation_entropy_always_bounded(prices in price_sequence(60, 300)) {
            let pe = compute_permutation_entropy(&prices);
            prop_assert!(pe >= 0.0 && pe <= 1.0 + f64::EPSILON,
                "PE={pe} out of [0, 1] for {}-trade window", prices.len());
        }

        /// Garman-Klass volatility must be non-negative
        #[test]
        fn garman_klass_non_negative(prices in price_sequence(2, 100)) {
            let first = prices[0];
            let last = *prices.last().unwrap();
            let high = prices.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
            let low = prices.iter().cloned().fold(f64::INFINITY, f64::min);
            let gk = compute_garman_klass_with_ohlc(first, high, low, last);
            prop_assert!(gk >= 0.0,
                "GK={gk} negative for OHLC({first}, {high}, {low}, {last})");
        }

        /// Burstiness must be in [-1, 1] for valid inter-arrival patterns
        #[test]
        fn burstiness_scalar_always_bounded(
            n in 3_usize..100,
            seed in 0_u64..10000,
        ) {
            // Generate trades with variable inter-arrival times
            let mut rng = seed;
            let mut ts = 0i64;
            let trades: Vec<TradeSnapshot> = (0..n).map(|_| {
                rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
                let delta = 1 + ((rng >> 33) % 10000) as i64;
                ts += delta;
                make_snapshot(ts, 100.0, 1.0, rng % 2 == 0)
            }).collect();
            let refs: Vec<&TradeSnapshot> = trades.iter().collect();

            let b = compute_burstiness_scalar(&refs);
            prop_assert!(b >= -1.0 - f64::EPSILON && b <= 1.0 + f64::EPSILON,
                "Burstiness={b} out of [-1, 1] for n={n}");
        }

        /// Hurst R/S must be in [0, 1] after soft-clamping
        #[test]
        fn hurst_rescaled_range_always_bounded(prices in price_sequence(64, 300)) {
            let h = compute_hurst_rescaled_range(&prices);
            prop_assert!(h >= 0.0 && h <= 1.0,
                "Hurst={h} out of [0, 1] for {}-trade window", prices.len());
        }

        /// Approximate entropy must be non-negative
        #[test]
        fn approximate_entropy_non_negative(prices in price_sequence(10, 200)) {
            let std_dev = {
                let mean = prices.iter().sum::<f64>() / prices.len() as f64;
                let var = prices.iter().map(|p| (p - mean) * (p - mean)).sum::<f64>() / prices.len() as f64;
                var.sqrt()
            };
            if std_dev > f64::EPSILON {
                let r = 0.2 * std_dev;
                let apen = compute_approximate_entropy(&prices, 2, r);
                prop_assert!(apen >= 0.0,
                    "ApEn={apen} negative for {}-trade window", prices.len());
            }
        }

        /// extract_lookback_cache OHLC invariants: high >= open,close and low <= open,close
        #[test]
        fn lookback_cache_ohlc_invariants(
            n in 1_usize..50,
            seed in 0_u64..10000,
        ) {
            let mut rng = seed;
            let trades: Vec<TradeSnapshot> = (0..n).map(|i| {
                rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
                let price = 100.0 + ((rng >> 33) as f64 / u32::MAX as f64) * 50.0;
                make_snapshot(i as i64 * 1000, price, 1.0, rng % 2 == 0)
            }).collect();
            let refs: Vec<&TradeSnapshot> = trades.iter().collect();

            let cache = extract_lookback_cache(&refs);
            prop_assert!(cache.high >= cache.open, "high < open");
            prop_assert!(cache.high >= cache.close, "high < close");
            prop_assert!(cache.low <= cache.open, "low > open");
            prop_assert!(cache.low <= cache.close, "low > close");
            prop_assert!(cache.total_volume >= 0.0, "total_volume negative");
            prop_assert_eq!(cache.prices.len(), n, "prices length mismatch");
            prop_assert_eq!(cache.volumes.len(), n, "volumes length mismatch");
        }

        /// Issue #96 Task #50: all_prices_finite and all_volumes_finite must match manual scan
        #[test]
        fn lookback_cache_finite_invariant(
            n in 1_usize..50,
            seed in 0_u64..10000,
        ) {
            let mut rng = seed;
            let trades: Vec<TradeSnapshot> = (0..n).map(|i| {
                rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
                let price = 100.0 + ((rng >> 33) as f64 / u32::MAX as f64) * 50.0;
                let volume = 0.1 + ((rng >> 17) as f64 / u32::MAX as f64) * 10.0;
                make_snapshot(i as i64 * 1000, price, volume, rng % 2 == 0)
            }).collect();
            let refs: Vec<&TradeSnapshot> = trades.iter().collect();

            let cache = extract_lookback_cache(&refs);
            let expected_prices_finite = cache.prices.iter().all(|p| p.is_finite());
            let expected_volumes_finite = cache.volumes.iter().all(|v| v.is_finite());
            prop_assert_eq!(cache.all_prices_finite, expected_prices_finite,
                "all_prices_finite mismatch");
            prop_assert_eq!(cache.all_volumes_finite, expected_volumes_finite,
                "all_volumes_finite mismatch");
        }
    }
}

/// Issue #96: Hurst DFA and Permutation Entropy numerical stability edge case tests
#[cfg(test)]
mod hurst_pe_stability_tests {
    use super::*;

    #[test]
    fn test_hurst_constant_prices_must_not_be_nan() {
        // Constant prices → R/S analysis may return NaN (0/0 in variance)
        // The soft_clamp should handle this gracefully
        let prices = vec![42000.0; 100];
        let h = compute_hurst_rescaled_range(&prices);
        // NaN propagation to downstream LSTM is dangerous
        assert!(
            h.is_finite(),
            "Constant prices must not produce NaN, got {h}"
        );
        assert!(h >= 0.0 && h <= 1.0, "Hurst must be in [0,1], got {h}");
    }

    #[test]
    fn test_hurst_alternating_pattern_mean_reverting() {
        // Perfect alternation: strong anti-persistence → low Hurst
        let prices: Vec<f64> = (0..128)
            .map(|i| if i % 2 == 0 { 100.0 } else { 101.0 })
            .collect();
        let h = compute_hurst_rescaled_range(&prices);
        assert!(h.is_finite() && h >= 0.0 && h <= 1.0, "Hurst bounded: {h}");
        assert!(
            h < 0.45,
            "Alternating pattern should have H < 0.45 (mean-reverting): {h}"
        );
    }

    #[test]
    fn test_hurst_perfect_linear_trend() {
        // Perfect linear trend → strong persistence → high Hurst
        let prices: Vec<f64> = (0..128).map(|i| 100.0 + i as f64 * 0.5).collect();
        let h = compute_hurst_rescaled_range(&prices);
        assert!(h.is_finite() && h >= 0.0 && h <= 1.0, "Hurst bounded: {h}");
        assert!(h > 0.7, "Perfect trend should have H > 0.7 (trending): {h}");
    }

    #[test]
    fn test_hurst_boundary_63_vs_64_vs_65() {
        // Verify clean transition at the MIN_SAMPLES=64 boundary
        let base_prices: Vec<f64> = (0..65).map(|i| 100.0 + (i as f64 * 0.3).sin()).collect();

        let h63 = compute_hurst_rescaled_range(&base_prices[..63]);
        let h64 = compute_hurst_rescaled_range(&base_prices[..64]);
        let h65 = compute_hurst_rescaled_range(&base_prices[..65]);

        // n=63: must return neutral 0.5 (below threshold)
        assert!(
            (h63 - 0.5).abs() < f64::EPSILON,
            "n=63 must be 0.5, got {h63}"
        );

        // n=64, n=65: must compute via rssimple (finite, bounded)
        assert!(
            h64.is_finite() && h64 >= 0.0 && h64 <= 1.0,
            "n=64 bounded: {h64}"
        );
        assert!(
            h65.is_finite() && h65 >= 0.0 && h65 <= 1.0,
            "n=65 bounded: {h65}"
        );

        // Both computed values should be in reasonable agreement
        assert!(
            (h64 - h65).abs() < 0.3,
            "Hurst(64) and Hurst(65) should be similar: {h64} vs {h65}"
        );
    }

    #[test]
    fn test_pe_m2_vs_m3_boundary_29_vs_30() {
        // n=29: uses M=2 path; n=30: uses M=3 path
        let prices_29: Vec<f64> = (0..29).map(|i| 100.0 + i as f64).collect();
        let prices_30: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();

        let pe29 = compute_permutation_entropy(&prices_29); // M=2 path
        let pe30 = compute_permutation_entropy(&prices_30); // M=3 path

        // Both monotonic → should be near 0
        assert!(pe29 >= 0.0 && pe29 <= 1.0, "PE(29) bounded: {pe29}");
        assert!(pe30 >= 0.0 && pe30 <= 1.0, "PE(30) bounded: {pe30}");
        assert!(pe29 < 0.05, "Monotonic M=2 → PE near 0: {pe29}");
        assert!(pe30 < 0.05, "Monotonic M=3 → PE near 0: {pe30}");
    }

    #[test]
    fn test_pe_monotonic_decreasing_early_exit() {
        // Monotonic decreasing: M=3 SIMD early-exit should return 0.0
        let prices: Vec<f64> = (0..50).map(|i| 200.0 - i as f64 * 0.5).collect();
        let pe = compute_permutation_entropy(&prices);
        assert!(pe.is_finite() && pe >= 0.0, "PE finite: {pe}");
        assert!(pe < 0.01, "Monotonic decreasing → PE ≈ 0: {pe}");
    }

    #[test]
    fn test_pe_m2_perfect_alternation_maximum_entropy() {
        // Perfect alternation in M=2: 50/50 split of ascending/descending patterns
        // Should produce maximum entropy (PE ≈ 1.0)
        let prices: Vec<f64> = (0..20)
            .map(|i| if i % 2 == 0 { 100.0 } else { 101.0 })
            .collect();
        let pe = compute_permutation_entropy(&prices); // M=2 path (n < 30)
        assert!(pe.is_finite() && pe >= 0.0 && pe <= 1.0, "PE bounded: {pe}");
        assert!(pe > 0.95, "Perfect alternation → PE ≈ 1.0: {pe}");
    }

    #[test]
    fn test_pe_exactly_60_samples_m3() {
        // n=60: uses M=3 path, verify no array bounds issues
        let prices: Vec<f64> = (0..60)
            .map(|i| 100.0 + (i as f64 * 0.5).sin() * 10.0)
            .collect();
        let pe = compute_permutation_entropy(&prices);
        assert!(
            pe.is_finite() && pe >= 0.0 && pe <= 1.0,
            "PE(60) bounded: {pe}"
        );
    }
}

// Issue #96: Edge case tests for Tier 2/3 public functions lacking direct coverage
#[cfg(test)]
mod tier2_edge_case_tests {
    use super::*;

    #[test]
    fn test_garman_klass_with_ohlc_zero_volatility() {
        // All prices identical → variance = 0, should return 0.0
        let gk = compute_garman_klass_with_ohlc(100.0, 100.0, 100.0, 100.0);
        assert_eq!(gk, 0.0, "Constant OHLC → GK = 0");
    }

    #[test]
    fn test_garman_klass_with_ohlc_negative_variance() {
        // Close ≠ Open with tiny range → subtractive term dominates, variance < 0
        let gk = compute_garman_klass_with_ohlc(99.0, 100.001, 99.999, 101.0);
        // May be 0.0 if variance goes negative, or small positive
        assert!(gk >= 0.0, "GK must be non-negative: {gk}");
        assert!(gk.is_finite(), "GK must be finite");
    }

    #[test]
    fn test_garman_klass_with_ohlc_zero_price_guard() {
        assert_eq!(compute_garman_klass_with_ohlc(0.0, 100.0, 50.0, 75.0), 0.0);
        assert_eq!(compute_garman_klass_with_ohlc(100.0, 100.0, 0.0, 75.0), 0.0);
    }

    #[test]
    fn test_volume_moments_cached_insufficient_data() {
        assert_eq!(compute_volume_moments_cached(&[]), (0.0, 0.0));
        assert_eq!(compute_volume_moments_cached(&[1.0]), (0.0, 0.0));
        assert_eq!(compute_volume_moments_cached(&[1.0, 2.0]), (0.0, 0.0));
    }

    #[test]
    fn test_volume_moments_cached_constant_volume() {
        let vols = vec![5.0; 100];
        let (skew, kurt) = compute_volume_moments_cached(&vols);
        assert_eq!(skew, 0.0, "Constant volume → skewness = 0");
        assert_eq!(kurt, 0.0, "Constant volume → kurtosis = 0");
    }

    #[test]
    fn test_volume_moments_with_mean_matches_cached() {
        let vols = vec![1.0, 2.0, 3.0, 10.0, 0.5, 7.0, 4.0];
        let mean = vols.iter().sum::<f64>() / vols.len() as f64;
        let (skew1, kurt1) = compute_volume_moments_cached(&vols);
        let (skew2, kurt2) = compute_volume_moments_with_mean(&vols, mean);
        assert!(
            (skew1 - skew2).abs() < 1e-10,
            "Skewness parity: {skew1} vs {skew2}"
        );
        assert!(
            (kurt1 - kurt2).abs() < 1e-10,
            "Kurtosis parity: {kurt1} vs {kurt2}"
        );
    }

    #[test]
    fn test_kaufman_er_perfect_trend() {
        // Monotonic up: net = total volatility, ER = 1.0
        let prices: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
        let er = compute_kaufman_er(&prices);
        assert!((er - 1.0).abs() < 1e-10, "Perfect trend → ER = 1.0: {er}");
    }

    #[test]
    fn test_kaufman_er_pure_noise_returns_to_start() {
        // Prices return to start: net = 0, ER = 0.0
        let prices = vec![100.0, 105.0, 95.0, 110.0, 90.0, 100.0];
        let er = compute_kaufman_er(&prices);
        assert_eq!(er, 0.0, "Return-to-start → ER = 0: {er}");
    }
}
