//! ADX Feature Extractor for Wave D Feature Engineering (Agent D14)
//!
//! This module implements 5 ADX-based features using Wilder's 14-period algorithm:
//! - Feature 211: ADX (Average Directional Index) - trend strength
//! - Feature 212: +DI (Positive Directional Indicator) - bullish pressure
//! - Feature 213: -DI (Negative Directional Indicator) - bearish pressure
//! - Feature 214: DX (Directional Movement Index) - directional strength
//! - Feature 215: Trend Classification (0=weak <20, 1=moderate 20-40, 2=strong >40)
//!
//! ## Performance Target
//! - <80μs for all 5 features per bar (incremental ADX updates)
//!
//! ## Feature Index Allocation
//! - Features 211-215: ADX directional indicators (5 total)
//! - Part of Wave D Phase 3 (24 regime features, indices 201-225)
//!
//! ## Algorithm: Wilder's 14-Period ADX
//! 1. **True Range (TR)**:
//!    TR = max(high - low, |high - `prev_close`|, |low - `prev_close`|)
//!
//! 2. **Directional Movement (+DM, -DM)**:
//!    +DM = max(0, high - `prev_high`) if `up_move` > `down_move`
//!    -DM = max(0, `prev_low` - low) if `down_move` > `up_move`
//!
//! 3. **Wilder's Smoothing** (exponential moving average with α = 1/14):
//!    First 14 bars: simple sum
//!    Bar 14: smoothed = sum / 14
//!    Bar 15+: smoothed = (`smoothed_prev` × 13 + `new_value`) / 14
//!
//! 4. **Directional Indicators**:
//!    +DI = (smoothed_+DM / `smoothed_TR`) × 100
//!    -DI = (smoothed_-DM / `smoothed_TR`) × 100
//!
//! 5. **DX (Directional Movement Index)**:
//!    DX = (|+DI - -DI| / (+DI + -DI)) × 100
//!
//! 6. **ADX (Average of DX)**:
//!    First 28 bars: simple average of DX
//!    Bar 29+: ADX = (`ADX_prev` × 13 + DX) / 14
//!
//! ## References
//! - Wilder, J. Wells (1978). "New Concepts in Technical Trading Systems"
//! - `WAVE_19_COMPREHENSIVE_FEATURE_ENGINEERING_PLAN.md`
//! - ml/src/regime/trending.rs (ADX implementation reference)

use std::collections::VecDeque;

pub use crate::OHLCVBar;

/// ADX feature extractor using Wilder's 14-period algorithm
///
/// This extractor maintains incremental state for efficient real-time calculation.
/// The ADX requires 28 bars to stabilize (14 bars for smoothing + 14 bars for ADX smoothing).
#[derive(Debug)]
pub struct AdxFeatureExtractor {
    /// Period for Wilder's smoothing (default: 14)
    period: usize,
    /// Bar counter (tracks initialization phase)
    bar_count: usize,
    /// Previous bar (for calculating directional movement)
    prev_bar: Option<OHLCVBar>,

    // Smoothed values (Wilder's EMA)
    /// Smoothed True Range
    smoothed_tr: f64,
    /// Smoothed +DM (Positive Directional Movement)
    smoothed_plus_dm: f64,
    /// Smoothed -DM (Negative Directional Movement)
    smoothed_minus_dm: f64,
    /// Smoothed ADX (average of DX)
    smoothed_adx: f64,

    // Accumulation buffers (for first 'period' bars)
    /// Accumulated TR during initialization
    tr_sum: f64,
    /// Accumulated +DM during initialization
    plus_dm_sum: f64,
    /// Accumulated -DM during initialization
    minus_dm_sum: f64,
    /// DX history for ADX initialization (first 'period' DX values)
    dx_history: VecDeque<f64>,
}

impl AdxFeatureExtractor {
    /// Create new ADX feature extractor with default 14-period Wilder's smoothing
    pub fn new() -> Self {
        Self::with_period(14)
    }

    /// Create new ADX feature extractor with custom period
    ///
    /// ## Arguments
    /// - `period`: Wilder's smoothing period (typical: 14)
    ///
    /// ## Example
    /// ```
    /// use ml::features::adx_features::AdxFeatureExtractor;
    ///
    /// // Standard 14-period ADX
    /// let extractor = AdxFeatureExtractor::new();
    ///
    /// // Custom 20-period ADX (smoother, slower)
    /// let extractor = AdxFeatureExtractor::with_period(20);
    /// ```
    pub fn with_period(period: usize) -> Self {
        assert!(period >= 2, "Period must be at least 2");

        Self {
            period,
            bar_count: 0,
            prev_bar: None,
            smoothed_tr: 0.0,
            smoothed_plus_dm: 0.0,
            smoothed_minus_dm: 0.0,
            smoothed_adx: 0.0,
            tr_sum: 0.0,
            plus_dm_sum: 0.0,
            minus_dm_sum: 0.0,
            dx_history: VecDeque::with_capacity(period),
        }
    }

    /// Update ADX state with new bar and return 5 features
    ///
    /// ## Returns
    /// - `[f64; 5]`: [ADX, +DI, -DI, DX, Classification]
    ///   - `[0]` Feature 211: ADX (0-100, trend strength)
    ///   - `[1]` Feature 212: +DI (0-100, bullish pressure)
    ///   - `[2]` Feature 213: -DI (0-100, bearish pressure)
    ///   - `[3]` Feature 214: DX (0-100, directional strength)
    ///   - `[4]` Feature 215: Classification (0=weak, 1=moderate, 2=strong)
    ///
    /// ## Algorithm
    /// 1. Bars 0: Initialize with first bar (return zeros)
    /// 2. Bars 1-13: Accumulate TR, +DM, -DM sums
    /// 3. Bar 14: Initialize smoothed values (sum / period)
    /// 4. Bars 15-27: Update smoothed values, accumulate DX
    /// 5. Bar 28: Initialize ADX (average of DX history)
    /// 6. Bar 29+: Update ADX incrementally
    ///
    /// ## Performance
    /// - O(1) per bar after initialization
    /// - Target: <80μs per bar (validated on real data)
    pub fn update(&mut self, bar: &OHLCVBar) -> [f64; 5] {
        // Bar 0: Initialize with first bar
        if self.prev_bar.is_none() {
            self.prev_bar = Some(*bar);
            self.bar_count = 1;
            return [0.0; 5];
        }

        let Some(prev) = self.prev_bar.as_ref() else {
            return [0.0; 5];
        };

        // Calculate True Range (TR)
        let tr = calculate_true_range(bar, prev);

        // Calculate Directional Movement (+DM, -DM)
        let (plus_dm, minus_dm) = calculate_directional_movement(bar, prev);

        // Bars 1-13: Accumulate sums for initial smoothing
        if self.bar_count < self.period {
            self.tr_sum += tr;
            self.plus_dm_sum += plus_dm;
            self.minus_dm_sum += minus_dm;
        }
        // Bar 14: Initialize smoothed values
        else if self.bar_count == self.period {
            self.smoothed_tr = self.tr_sum / self.period as f64;
            self.smoothed_plus_dm = self.plus_dm_sum / self.period as f64;
            self.smoothed_minus_dm = self.minus_dm_sum / self.period as f64;
        }
        // Bars 15+: Update smoothed values using Wilder's formula
        else {
            self.smoothed_tr = wilder_smooth(self.smoothed_tr, tr, self.period);
            self.smoothed_plus_dm = wilder_smooth(self.smoothed_plus_dm, plus_dm, self.period);
            self.smoothed_minus_dm = wilder_smooth(self.smoothed_minus_dm, minus_dm, self.period);
        }

        self.bar_count += 1;
        self.prev_bar = Some(*bar);

        // Return zeros until we have enough data for DI calculation
        if self.bar_count < self.period + 1 {
            return [0.0; 5];
        }

        // Calculate +DI, -DI (after bar 14)
        let (plus_di, minus_di) = calculate_directional_indicators(
            self.smoothed_plus_dm,
            self.smoothed_minus_dm,
            self.smoothed_tr,
        );

        // Calculate DX (Directional Movement Index)
        let dx = calculate_dx(plus_di, minus_di);

        // Bars 15-27: Accumulate DX history for ADX initialization
        if self.bar_count < 2 * self.period {
            self.dx_history.push_back(dx);
            // Return partial results (DI and DX available, ADX still initializing)
            return [0.0, plus_di, minus_di, dx, 0.0];
        }

        // Bar 28: Initialize ADX (simple average of DX history)
        if self.bar_count == 2 * self.period {
            let dx_sum: f64 = self.dx_history.iter().sum();
            self.smoothed_adx = dx_sum / self.period as f64;
            self.dx_history.clear(); // Free memory after initialization
        }
        // Bars 29+: Update ADX using Wilder's smoothing
        else {
            self.smoothed_adx = wilder_smooth(self.smoothed_adx, dx, self.period);
        }

        // Feature 215: Trend Classification
        let classification = classify_trend_strength(self.smoothed_adx);

        [self.smoothed_adx, plus_di, minus_di, dx, classification]
    }

    /// Extract ADX features from a rolling window of bars (batch processing)
    ///
    /// ## Arguments
    /// - `bars`: Rolling window of OHLCV bars (minimum 28 for stable ADX)
    ///
    /// ## Returns
    /// - `[f64; 5]`: ADX features from the latest bar
    ///
    /// ## Example
    /// ```
    /// use ml::features::adx_features::{AdxFeatureExtractor, OHLCVBar};
    /// use std::collections::VecDeque;
    ///
    /// let mut bars = VecDeque::new();
    /// // ... add bars ...
    ///
    /// let features = AdxFeatureExtractor::extract_from_window(&bars);
    /// assert_eq!(features.len(), 5);
    /// ```
    pub fn extract_from_window(bars: &VecDeque<OHLCVBar>) -> [f64; 5] {
        if bars.len() < 2 {
            return [0.0; 5];
        }

        let mut extractor = Self::new();
        let mut result = [0.0; 5];

        for bar in bars.iter() {
            result = extractor.update(bar);
        }

        result
    }

    /// Reset extractor state (useful for backtesting multiple symbols)
    pub fn reset(&mut self) {
        self.bar_count = 0;
        self.prev_bar = None;
        self.smoothed_tr = 0.0;
        self.smoothed_plus_dm = 0.0;
        self.smoothed_minus_dm = 0.0;
        self.smoothed_adx = 0.0;
        self.tr_sum = 0.0;
        self.plus_dm_sum = 0.0;
        self.minus_dm_sum = 0.0;
        self.dx_history.clear();
    }

    /// Get current bar count (useful for checking initialization status)
    pub const fn bar_count(&self) -> usize {
        self.bar_count
    }

    /// Check if ADX is fully initialized (requires 2 × period bars)
    pub const fn is_initialized(&self) -> bool {
        self.bar_count >= 2 * self.period
    }
}

impl Default for AdxFeatureExtractor {
    fn default() -> Self {
        Self::new()
    }
}

// ===== Helper Functions =====

/// Calculate True Range (TR)
///
/// TR = max(high - low, |high - `prev_close`|, |low - `prev_close`|)
#[inline]
fn calculate_true_range(bar: &OHLCVBar, prev: &OHLCVBar) -> f64 {
    let hl = bar.high - bar.low;
    let hc = (bar.high - prev.close).abs();
    let lc = (bar.low - prev.close).abs();

    hl.max(hc).max(lc)
}

/// Calculate Directional Movement (+DM, -DM)
///
/// Rules:
/// - If (high - `prev_high`) > (`prev_low` - low) AND (high - `prev_high`) > 0:
///   +DM = high - `prev_high`, -DM = 0
/// - Else if (`prev_low` - low) > (high - `prev_high`) AND (`prev_low` - low) > 0:
///   +DM = 0, -DM = `prev_low` - low
/// - Else:
///   +DM = 0, -DM = 0
#[inline]
fn calculate_directional_movement(bar: &OHLCVBar, prev: &OHLCVBar) -> (f64, f64) {
    let up_move = bar.high - prev.high;
    let down_move = prev.low - bar.low;

    let plus_dm = if up_move > down_move && up_move > 0.0 {
        up_move
    } else {
        0.0
    };

    let minus_dm = if down_move > up_move && down_move > 0.0 {
        down_move
    } else {
        0.0
    };

    (plus_dm, minus_dm)
}

/// Apply Wilder's smoothing formula
///
/// `smoothed_new` = (`smoothed_old` × (period - 1) + `new_value`) / period
#[inline]
fn wilder_smooth(smoothed: f64, new_value: f64, period: usize) -> f64 {
    (smoothed * (period - 1) as f64 + new_value) / period as f64
}

/// Calculate Directional Indicators (+DI, -DI)
///
/// +DI = (smoothed_+DM / `smoothed_TR`) × 100
/// -DI = (smoothed_-DM / `smoothed_TR`) × 100
#[inline]
fn calculate_directional_indicators(
    smoothed_plus_dm: f64,
    smoothed_minus_dm: f64,
    smoothed_tr: f64,
) -> (f64, f64) {
    if smoothed_tr < 1e-10 {
        return (0.0, 0.0);
    }

    let plus_di = (smoothed_plus_dm / smoothed_tr) * 100.0;
    let minus_di = (smoothed_minus_dm / smoothed_tr) * 100.0;

    (
        safe_clip(plus_di, 0.0, 100.0),
        safe_clip(minus_di, 0.0, 100.0),
    )
}

/// Calculate DX (Directional Movement Index)
///
/// DX = (|+DI - -DI| / (+DI + -DI)) × 100
#[inline]
fn calculate_dx(plus_di: f64, minus_di: f64) -> f64 {
    let sum = plus_di + minus_di;

    if sum < 1e-10 {
        return 0.0;
    }

    let dx = ((plus_di - minus_di).abs() / sum) * 100.0;
    safe_clip(dx, 0.0, 100.0)
}

/// Classify trend strength based on ADX value
///
/// Classification:
/// - 0: Weak trend (ADX < 20) - ranging/choppy market
/// - 1: Moderate trend (20 ≤ ADX < 40) - established trend
/// - 2: Strong trend (ADX ≥ 40) - powerful trend
#[inline]
fn classify_trend_strength(adx: f64) -> f64 {
    if adx < 20.0 {
        0.0
    } else if adx < 40.0 {
        1.0
    } else {
        2.0
    }
}

/// Safe clipping: Clip value to [min, max] range, handles NaN/Inf
#[inline]
const fn safe_clip(value: f64, min: f64, max: f64) -> f64 {
    if !value.is_finite() {
        return 0.0;
    }
    value.clamp(min, max)
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;

    // ===== Test Helper Functions =====

    fn create_bars(prices: Vec<f64>) -> VecDeque<OHLCVBar> {
        prices
            .into_iter()
            .map(|p| OHLCVBar {
                timestamp: Utc::now(),
                open: p,
                high: p * 1.01,
                low: p * 0.99,
                close: p,
                volume: 1000.0,
            })
            .collect()
    }

    fn create_trending_bars(start: f64, count: usize, trend_strength: f64) -> VecDeque<OHLCVBar> {
        (0..count)
            .map(|i| {
                let price = start + trend_strength * i as f64;
                OHLCVBar {
                    timestamp: Utc::now(),
                    open: price,
                    high: price * 1.02,
                    low: price * 0.98,
                    close: price,
                    volume: 1000.0,
                }
            })
            .collect()
    }

    fn create_ranging_bars(center: f64, count: usize) -> VecDeque<OHLCVBar> {
        (0..count)
            .map(|i| {
                let price = center + 0.5 * ((i as f64 * 0.5).sin());
                OHLCVBar {
                    timestamp: Utc::now(),
                    open: price,
                    high: price * 1.005,
                    low: price * 0.995,
                    close: price,
                    volume: 1000.0,
                }
            })
            .collect()
    }

    fn assert_approx_eq(a: f64, b: f64, epsilon: f64) {
        assert!(
            (a - b).abs() < epsilon,
            "{} != {} (epsilon: {})",
            a,
            b,
            epsilon
        );
    }

    // ===== Unit Tests: Helper Functions =====

    #[test]
    fn test_true_range_calculation() {
        let prev = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 102.0,
            low: 98.0,
            close: 100.0,
            volume: 1000.0,
        };

        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 101.0,
            high: 104.0,
            low: 99.0,
            close: 103.0,
            volume: 1000.0,
        };

        let tr = calculate_true_range(&bar, &prev);
        // TR = max(104 - 99, |104 - 100|, |99 - 100|) = max(5, 4, 1) = 5
        assert_approx_eq(tr, 5.0, 0.01);
    }

    #[test]
    fn test_directional_movement_uptrend() {
        let prev = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 102.0,
            low: 98.0,
            close: 100.0,
            volume: 1000.0,
        };

        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 101.0,
            high: 105.0,
            low: 99.0,
            close: 104.0,
            volume: 1000.0,
        };

        let (plus_dm, minus_dm) = calculate_directional_movement(&bar, &prev);
        // up_move = 105 - 102 = 3, down_move = 98 - 99 = -1
        // +DM = 3 (up_move > down_move), -DM = 0
        assert_approx_eq(plus_dm, 3.0, 0.01);
        assert_eq!(minus_dm, 0.0);
    }

    #[test]
    fn test_directional_movement_downtrend() {
        let prev = OHLCVBar {
            timestamp: Utc::now(),
            open: 100.0,
            high: 102.0,
            low: 98.0,
            close: 100.0,
            volume: 1000.0,
        };

        let bar = OHLCVBar {
            timestamp: Utc::now(),
            open: 99.0,
            high: 101.0,
            low: 95.0,
            close: 96.0,
            volume: 1000.0,
        };

        let (plus_dm, minus_dm) = calculate_directional_movement(&bar, &prev);
        // up_move = 101 - 102 = -1, down_move = 98 - 95 = 3
        // +DM = 0, -DM = 3 (down_move > up_move)
        assert_eq!(plus_dm, 0.0);
        assert_approx_eq(minus_dm, 3.0, 0.01);
    }

    #[test]
    fn test_wilder_smooth() {
        let smoothed = 10.0;
        let new_value = 14.0;
        let period = 14;

        let result = wilder_smooth(smoothed, new_value, period);
        // (10 × 13 + 14) / 14 = (130 + 14) / 14 = 144 / 14 = 10.2857
        assert_approx_eq(result, 10.2857, 0.01);
    }

    #[test]
    fn test_directional_indicators() {
        let (plus_di, minus_di) = calculate_directional_indicators(3.0, 1.0, 5.0);
        // +DI = (3 / 5) × 100 = 60, -DI = (1 / 5) × 100 = 20
        assert_approx_eq(plus_di, 60.0, 0.01);
        assert_approx_eq(minus_di, 20.0, 0.01);
    }

    #[test]
    fn test_directional_indicators_zero_tr() {
        let (plus_di, minus_di) = calculate_directional_indicators(3.0, 1.0, 0.0);
        // Zero TR should return (0, 0) to avoid division by zero
        assert_eq!(plus_di, 0.0);
        assert_eq!(minus_di, 0.0);
    }

    #[test]
    fn test_calculate_dx() {
        let dx = calculate_dx(60.0, 20.0);
        // DX = |60 - 20| / (60 + 20) × 100 = 40 / 80 × 100 = 50
        assert_approx_eq(dx, 50.0, 0.01);
    }

    #[test]
    fn test_calculate_dx_equal_di() {
        let dx = calculate_dx(50.0, 50.0);
        // DX = |50 - 50| / (50 + 50) × 100 = 0 / 100 × 100 = 0
        assert_eq!(dx, 0.0);
    }

    #[test]
    fn test_classify_trend_strength() {
        assert_eq!(classify_trend_strength(15.0), 0.0); // Weak
        assert_eq!(classify_trend_strength(25.0), 1.0); // Moderate
        assert_eq!(classify_trend_strength(45.0), 2.0); // Strong
    }

    #[test]
    fn test_safe_clip() {
        assert_eq!(safe_clip(50.0, 0.0, 100.0), 50.0);
        assert_eq!(safe_clip(-10.0, 0.0, 100.0), 0.0);
        assert_eq!(safe_clip(150.0, 0.0, 100.0), 100.0);
        assert_eq!(safe_clip(f64::NAN, 0.0, 100.0), 0.0);
        assert_eq!(safe_clip(f64::INFINITY, 0.0, 100.0), 0.0);
    }

    // ===== Integration Tests: AdxFeatureExtractor =====

    #[test]
    fn test_extractor_initialization() {
        let extractor = AdxFeatureExtractor::new();
        assert_eq!(extractor.period, 14);
        assert_eq!(extractor.bar_count, 0);
        assert!(!extractor.is_initialized());
    }

    #[test]
    fn test_extractor_insufficient_data() {
        let mut extractor = AdxFeatureExtractor::new();
        let bars = create_bars(vec![100.0, 101.0, 102.0]);

        for bar in bars.iter() {
            let features = extractor.update(bar);
            // All zeros until we have enough data
            assert_eq!(features, [0.0; 5]);
        }
    }

    #[test]
    fn test_extractor_trending_market() {
        let mut extractor = AdxFeatureExtractor::new();
        let bars = create_trending_bars(100.0, 40, 0.5); // Strong uptrend

        let mut features = [0.0; 5];
        for bar in bars.iter() {
            features = extractor.update(bar);
        }

        // After 40 bars, ADX should be initialized and detect trending market
        assert!(extractor.is_initialized());
        assert!(features[0] > 0.0, "ADX: {}", features[0]); // ADX > 0
        assert!(features[1] > features[2], "+DI > -DI in uptrend"); // +DI > -DI
        assert!(features[3] > 0.0, "DX: {}", features[3]); // DX > 0
    }

    #[test]
    fn test_extractor_ranging_market() {
        let mut extractor = AdxFeatureExtractor::new();
        let bars = create_ranging_bars(100.0, 40); // Oscillating market

        let mut features = [0.0; 5];
        for bar in bars.iter() {
            features = extractor.update(bar);
        }

        // After 40 bars, ADX should be initialized
        assert!(extractor.is_initialized());
        // ADX should be lower in ranging market (typically <20)
        // But classification depends on oscillation amplitude
        assert!(
            features[0] >= 0.0 && features[0] <= 100.0,
            "ADX: {}",
            features[0]
        );
        assert!(
            features[4] >= 0.0 && features[4] <= 2.0,
            "Classification: {}",
            features[4]
        );
    }

    #[test]
    fn test_extract_from_window() {
        let bars = create_trending_bars(100.0, 40, 0.3);
        let features = AdxFeatureExtractor::extract_from_window(&bars);

        // Should return valid features after processing 40 bars
        assert_eq!(features.len(), 5);
        assert!(features[0].is_finite(), "ADX: {}", features[0]);
        assert!(features[1].is_finite(), "+DI: {}", features[1]);
        assert!(features[2].is_finite(), "-DI: {}", features[2]);
        assert!(features[3].is_finite(), "DX: {}", features[3]);
        assert!(
            features[4] >= 0.0 && features[4] <= 2.0,
            "Classification: {}",
            features[4]
        );
    }

    #[test]
    fn test_extractor_reset() {
        let mut extractor = AdxFeatureExtractor::new();
        let bars = create_trending_bars(100.0, 30, 0.5);

        for bar in bars.iter() {
            extractor.update(bar);
        }

        assert!(extractor.bar_count() > 0);

        extractor.reset();

        assert_eq!(extractor.bar_count(), 0);
        assert!(!extractor.is_initialized());
        assert!(extractor.prev_bar.is_none());
    }

    #[test]
    fn test_extractor_feature_ranges() {
        let mut extractor = AdxFeatureExtractor::new();
        let bars = create_trending_bars(100.0, 40, 0.4);

        let mut features = [0.0; 5];
        for bar in bars.iter() {
            features = extractor.update(bar);
        }

        // Validate feature ranges
        assert!(
            features[0] >= 0.0 && features[0] <= 100.0,
            "ADX: {}",
            features[0]
        );
        assert!(
            features[1] >= 0.0 && features[1] <= 100.0,
            "+DI: {}",
            features[1]
        );
        assert!(
            features[2] >= 0.0 && features[2] <= 100.0,
            "-DI: {}",
            features[2]
        );
        assert!(
            features[3] >= 0.0 && features[3] <= 100.0,
            "DX: {}",
            features[3]
        );
        assert!(
            features[4] == 0.0 || features[4] == 1.0 || features[4] == 2.0,
            "Classification: {}",
            features[4]
        );
    }

    #[test]
    fn test_extractor_custom_period() {
        let mut extractor = AdxFeatureExtractor::with_period(10);
        assert_eq!(extractor.period, 10);

        let bars = create_trending_bars(100.0, 30, 0.5);
        let mut features = [0.0; 5];
        for bar in bars.iter() {
            features = extractor.update(bar);
        }

        // Should initialize faster with shorter period (10 × 2 = 20 bars)
        assert!(extractor.is_initialized());
        assert!(features[0] >= 0.0);
    }

    #[test]
    fn test_extractor_downtrend() {
        let mut extractor = AdxFeatureExtractor::new();
        let bars = create_trending_bars(150.0, 40, -0.5); // Strong downtrend

        let mut features = [0.0; 5];
        for bar in bars.iter() {
            features = extractor.update(bar);
        }

        // After 40 bars, should detect downtrend
        assert!(extractor.is_initialized());
        assert!(features[0] > 0.0, "ADX: {}", features[0]); // ADX > 0
        assert!(features[2] > features[1], "-DI > +DI in downtrend"); // -DI > +DI
    }

    #[test]
    fn test_extractor_extreme_volatility() {
        let mut extractor = AdxFeatureExtractor::new();
        let mut bars = create_ranging_bars(100.0, 30);

        // Add extreme spike
        bars.push_back(OHLCVBar {
            timestamp: Utc::now(),
            open: 150.0,
            high: 180.0,
            low: 140.0,
            close: 170.0,
            volume: 5000.0,
        });

        let mut features = [0.0; 5];
        for bar in bars.iter() {
            features = extractor.update(bar);
        }

        // Should handle extreme volatility gracefully
        assert!(
            features[0].is_finite() && features[0] >= 0.0,
            "ADX: {}",
            features[0]
        );
        assert!(
            features[1].is_finite() && features[1] >= 0.0,
            "+DI: {}",
            features[1]
        );
        assert!(
            features[2].is_finite() && features[2] >= 0.0,
            "-DI: {}",
            features[2]
        );
    }

    #[test]
    fn test_extractor_constant_prices() {
        let mut extractor = AdxFeatureExtractor::new();
        let bars = create_bars(vec![100.0; 40]);

        let mut features = [0.0; 5];
        for bar in bars.iter() {
            features = extractor.update(bar);
        }

        // Constant prices should result in very low ADX
        assert!(features[0] < 5.0, "ADX: {}", features[0]);
        assert_eq!(features[4], 0.0, "Classification: {}", features[4]); // Weak trend
    }

    // ===== Performance Regression Tests =====

    #[test]
    fn test_incremental_vs_batch_consistency() {
        let bars = create_trending_bars(100.0, 40, 0.4);

        // Incremental processing
        let mut extractor_incremental = AdxFeatureExtractor::new();
        let mut features_incremental = [0.0; 5];
        for bar in bars.iter() {
            features_incremental = extractor_incremental.update(bar);
        }

        // Batch processing
        let features_batch = AdxFeatureExtractor::extract_from_window(&bars);

        // Results should be identical
        for i in 0..5 {
            assert_approx_eq(features_incremental[i], features_batch[i], 0.01);
        }
    }
}
