//! Feature Normalization Pipeline (Wave C + Wave D)
//!
//! This module implements online/incremental normalization for 225-dimension ML features.
//! Uses category-specific strategies for optimal ML model convergence.
//!
//! ## Normalization Categories
//! 1. **Price Features** (60 features): Z-score normalization (mean=0, std=1)
//! 2. **Volume Features** (40 features): Percentile rank normalization (0-1)
//! 3. **Microstructure Features** (50 features): Log transform + z-score
//! 4. **Technical Indicators** (10 features): Already normalized (0-1 or -1 to +1)
//! 5. **Time/Statistical Features** (96 features): Already normalized
//! 6. **CUSUM Features** (10 features, Wave D): Z-score normalization (indices 201-210)
//! 7. **ADX Features** (5 features, Wave D): Min-max scaling `[0,1]` (indices 211-215)
//! 8. **Transition Features** (5 features, Wave D): Z-score normalization (indices 216-220)
//! 9. **Adaptive Features** (4 features, Wave D): Min-max scaling `[0,2]` (indices 221-224)
//!
//! ## Performance
//! - Target: <200μs for normalizing all 225 features per bar
//! - Memory: <20KB per symbol (rolling statistics, with lazy allocation)
//! - Online: No batch recomputation required
//!
//! ## Memory Optimization (Wave G)
//! - Fixed-size ring buffers replace `VecDeque` (zero heap allocations)
//! - Lazy buffer initialization (only allocate when first value arrives)
//! - Projected savings: 49.4 KB → ~10 KB per symbol (80% reduction)
//!
//! ## Usage
//! ```rust
//! use ml::features::normalization::FeatureNormalizer;
//!
//! let mut normalizer = FeatureNormalizer::new();
//! let mut features = [0.0; 225];  // Raw features from extraction
//! normalizer.normalize(&mut features)?;  // In-place normalization
//! ```

use anyhow::Result;
use tracing::warn;

const EPSILON: f64 = 1e-8; // Prevent division by zero

/// Fixed-size ring buffer with zero heap allocations
///
/// Uses compile-time const generics for inline storage on the stack.
/// Provides O(1) push/pop operations with automatic overwriting of oldest values.
///
/// ## Memory Layout (Wave G Optimization)
/// - `data`: `[T; N]` stored inline (no heap allocation, no Option overhead)
/// - `head`: Current write position (wraps around at N)
/// - `len`: Number of valid elements (0..=N)
///
/// ## Memory Optimization
/// - OLD: [Option<T>; N] → 16 bytes per f64 element (8 value + 8 discriminant)
/// - NEW: [T; N] → 8 bytes per f64 element (50% reduction)
/// - Savings: 100 elements × 8 bytes = 800 bytes per buffer
///
/// ## Performance
/// - Push: O(1)
/// - Iteration: O(N)
/// - Mean/StdDev: O(N)
/// - Memory: `N × sizeof(T)` bytes (stack-allocated)
///
/// ## Example
/// ```rust
/// let mut buffer: RingBuffer<f64, 100> = RingBuffer::new();
/// buffer.push(42.0);
/// assert_eq!(buffer.len(), 1);
/// assert_eq!(buffer.mean(), 42.0);
/// ```
#[derive(Clone, Debug)]
pub struct RingBuffer<T: Copy + Default, const N: usize> {
    data: [T; N],
    head: usize,
    len: usize,
}

impl<T: Copy + Default, const N: usize> Default for RingBuffer<T, N> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
    /// Create new empty ring buffer
    pub fn new() -> Self {
        Self {
            data: [T::default(); N],
            head: 0,
            len: 0,
        }
    }

    /// Push value into ring buffer (overwrites oldest if full)
    pub const fn push(&mut self, value: T) {
        if N == 0 {
            return; // Zero-capacity buffer: cannot store any values
        }
        self.data[self.head] = value;
        self.head = (self.head + 1) % N;
        if self.len < N {
            self.len += 1;
        }
    }

    /// Get current number of elements
    pub const fn len(&self) -> usize {
        self.len
    }

    /// Check if buffer is empty
    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }

    /// Iterate over valid elements in insertion order
    pub fn iter(&self) -> impl Iterator<Item = &T> + '_ {
        let start_idx = if self.len < N { 0 } else { self.head };

        (0..self.len).map(move |i| {
            let idx = (start_idx + i) % N;
            &self.data[idx]
        })
    }

    /// Clear all elements (resets to default values)
    pub fn clear(&mut self) {
        self.data = [T::default(); N];
        self.head = 0;
        self.len = 0;
    }
}

// Extension trait for f64 statistics
impl<const N: usize> RingBuffer<f64, N> {
    /// Compute mean of all values
    pub fn mean(&self) -> f64 {
        if self.len == 0 {
            return 0.0;
        }
        let sum: f64 = self.iter().copied().sum();
        sum / self.len as f64
    }

    /// Compute standard deviation
    pub fn std_dev(&self) -> f64 {
        if self.len < 2 {
            return 0.0;
        }
        let mean = self.mean();
        let variance: f64 =
            self.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (self.len - 1) as f64;
        variance.max(0.0).sqrt()
    }

    /// Get minimum value
    pub fn min(&self) -> f64 {
        self.iter().copied().fold(f64::MAX, f64::min)
    }

    /// Get maximum value
    pub fn max(&self) -> f64 {
        self.iter().copied().fold(f64::MIN, f64::max)
    }
}

/// Main feature normalizer coordinating all normalization strategies
#[derive(Debug)]
pub struct FeatureNormalizer {
    /// Price feature normalizers (indices 15-74, 60 features)
    price_normalizers: Vec<RollingZScore>,

    /// Volume feature normalizers (indices 75-114, 40 features)
    volume_normalizers: Vec<RollingPercentileRank>,

    /// Microstructure feature normalizers (indices 115-164, 50 features)
    microstructure_normalizers: Vec<LogZScoreNormalizer>,

    /// CUSUM feature normalizers (indices 201-210, 10 features, Wave D)
    cusum_normalizers: Vec<RollingZScore>,

    /// ADX feature normalizers (indices 211-215, 5 features, Wave D)
    adx_normalizers: Vec<RollingPercentileRank>,

    /// Transition feature normalizers (indices 216-220, 5 features, Wave D)
    transition_normalizers: Vec<RollingZScore>,

    /// Adaptive feature normalizers (indices 221-224, 4 features, Wave D)
    adaptive_normalizers: Vec<RollingPercentileRank>,

    /// NaN handler for input validation
    nan_handler: NaNHandler,
}

impl FeatureNormalizer {
    /// Create new feature normalizer with default window sizes
    pub fn new() -> Self {
        Self::with_config(50, 50, 20, 30)
    }

    /// Create feature normalizer with custom window sizes
    ///
    /// # Arguments
    /// - `price_window`: Rolling window for price features (recommended: 50)
    /// - `volume_window`: Rolling window for volume features (recommended: 50)
    /// - `microstructure_window`: Rolling window for microstructure features (recommended: 20)
    /// - `regime_window`: Rolling window for Wave D regime features (recommended: 30)
    pub fn with_config(
        price_window: usize,
        volume_window: usize,
        microstructure_window: usize,
        regime_window: usize,
    ) -> Self {
        Self {
            // 60 price features (indices 15-74)
            price_normalizers: (0..60).map(|_| RollingZScore::new(price_window)).collect(),

            // 40 volume features (indices 75-114)
            volume_normalizers: (0..40)
                .map(|_| RollingPercentileRank::new(volume_window))
                .collect(),

            // 50 microstructure features (indices 115-164)
            // Scale factors: roll=1.0, amihud=1e8, corwin=100.0, others=1.0
            microstructure_normalizers: vec![
                // Roll spread (index 115)
                LogZScoreNormalizer::new(1.0, microstructure_window),
                // Amihud illiquidity (index 116)
                LogZScoreNormalizer::new(1e8, microstructure_window),
                // Corwin-Schultz spread (index 117)
                LogZScoreNormalizer::new(100.0, microstructure_window),
                // Remaining 47 microstructure features (indices 118-164)
            ]
            .into_iter()
            .chain((0..47).map(|_| LogZScoreNormalizer::new(1.0, microstructure_window)))
            .collect(),

            // Wave D: 10 CUSUM features (indices 201-210)
            cusum_normalizers: (0..10).map(|_| RollingZScore::new(regime_window)).collect(),

            // Wave D: 5 ADX features (indices 211-215)
            adx_normalizers: (0..5)
                .map(|_| RollingPercentileRank::new(regime_window))
                .collect(),

            // Wave D: 5 transition features (indices 216-220)
            transition_normalizers: (0..5).map(|_| RollingZScore::new(regime_window)).collect(),

            // Wave D: 4 adaptive features (indices 221-224)
            adaptive_normalizers: (0..4)
                .map(|_| RollingPercentileRank::new(regime_window))
                .collect(),

            nan_handler: NaNHandler::new(),
        }
    }

    /// Normalize 225-dimensional feature vector in-place
    ///
    /// # Arguments
    /// - `features`: Mutable reference to feature vector (modified in-place)
    ///
    /// # Returns
    /// - `Ok(())` if normalization successful
    /// - `Err(anyhow::Error)` if any feature is non-finite after normalization
    ///
    /// # Feature Ranges
    /// - Indices 0-4: OHLCV (already normalized, no change)
    /// - Indices 5-14: Technical indicators (already normalized, no change)
    /// - Indices 15-74: Price features (z-score normalization)
    /// - Indices 75-114: Volume features (percentile rank normalization)
    /// - Indices 115-164: Microstructure features (log + z-score normalization)
    /// - Indices 165-224: Time/statistical features (already normalized, no change)
    pub fn normalize(&mut self, features: &mut [f64; 225]) -> Result<()> {
        // 1. Handle NaN/Inf in input (impute with last valid value)
        self.nan_handler.handle_input(features);

        // 2. Validate input (all features must be finite after imputation)
        for (i, &val) in features.iter().enumerate() {
            if !val.is_finite() {
                anyhow::bail!("Feature {} is non-finite after imputation: {}", i, val);
            }
        }

        // 3. Normalize OHLCV (indices 0-4) - ALREADY NORMALIZED, skip

        // 4. Normalize Technical Indicators (indices 5-14) - ALREADY NORMALIZED, skip

        // 5. Normalize Price Patterns (indices 15-74)
        for (idx, feature) in features[15..75].iter_mut().enumerate() {
            *feature = self.price_normalizers[idx].update(*feature);
        }

        // 6. Normalize Volume Patterns (indices 75-114)
        for (idx, feature) in features[75..115].iter_mut().enumerate() {
            *feature = self.volume_normalizers[idx].update(*feature);
        }

        // 7. Normalize Microstructure (indices 115-164)
        for (idx, feature) in features[115..165].iter_mut().enumerate() {
            *feature = self.microstructure_normalizers[idx].update(*feature);
        }

        // 8. Time features (165-174) - ALREADY NORMALIZED, skip

        // 9. Statistical features (175-200) - ALREADY NORMALIZED, skip

        // 10. Normalize CUSUM Features (indices 201-210, Wave D)
        for (idx, feature) in features[201..211].iter_mut().enumerate() {
            *feature = self.cusum_normalizers[idx].update(*feature);
        }

        // 11. Normalize ADX Features (indices 211-215, Wave D)
        // ADX features are already in [0, 100] range, scale to [0, 1]
        for (idx, feature) in features[211..216].iter_mut().enumerate() {
            let scaled = *feature / 100.0; // Scale from [0, 100] to [0, 1]
            *feature = self.adx_normalizers[idx].update(scaled);
        }

        // 12. Normalize Transition Features (indices 216-220, Wave D)
        for (idx, feature) in features[216..221].iter_mut().enumerate() {
            *feature = self.transition_normalizers[idx].update(*feature);
        }

        // 13. Normalize Adaptive Features (indices 221-224, Wave D)
        for (idx, feature) in features[221..225].iter_mut().enumerate() {
            *feature = self.adaptive_normalizers[idx].update(*feature);
        }

        // 14. Final validation (all features must be finite)
        for (i, &val) in features.iter().enumerate() {
            if !val.is_finite() {
                anyhow::bail!("Normalized feature {} is non-finite: {}", i, val);
            }
        }

        Ok(())
    }

    /// Reset all normalizers (useful for backtesting)
    pub fn reset(&mut self) {
        for norm in &mut self.price_normalizers {
            norm.reset();
        }
        for norm in &mut self.volume_normalizers {
            norm.reset();
        }
        for norm in &mut self.microstructure_normalizers {
            norm.reset();
        }
        // Wave D normalizers
        for norm in &mut self.cusum_normalizers {
            norm.reset();
        }
        for norm in &mut self.adx_normalizers {
            norm.reset();
        }
        for norm in &mut self.transition_normalizers {
            norm.reset();
        }
        for norm in &mut self.adaptive_normalizers {
            norm.reset();
        }
        self.nan_handler.reset();
    }

    /// Get normalization statistics for debugging
    pub fn get_stats(&self) -> NormalizationStats {
        NormalizationStats {
            price_mean: self
                .price_normalizers
                .first()
                .map(|n| n.mean)
                .unwrap_or(0.0),
            price_std: self
                .price_normalizers
                .first()
                .map(|n| n.std())
                .unwrap_or(0.0),
            volume_percentile: self
                .volume_normalizers
                .first()
                .and_then(|n| {
                    n.buffer
                        .as_ref()
                        .map(|b| b.len() as f64 / n.window_size as f64)
                })
                .unwrap_or(0.0),
            nan_count: self.nan_handler.total_nan_count(),
        }
    }
}

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

/// Normalization statistics for debugging
#[derive(Debug, Clone)]
pub struct NormalizationStats {
    pub price_mean: f64,
    pub price_std: f64,
    pub volume_percentile: f64,
    pub nan_count: u32,
}

//
// Category 1: Z-Score Normalization for Price Features
//

/// Rolling z-score normalization with lazy buffer allocation
///
/// Uses fixed-size ring buffer (const generic) for zero heap allocations.
/// Buffer is lazily initialized only when first value arrives.
///
/// ## Memory Optimization
/// - OLD: `VecDeque` with `capacity=window_size` → heap allocation
/// - NEW: Option<`RingBuffer`<f64, 100>> → None until first value
/// - Savings: 800 bytes per normalizer (100 × 8 bytes)
#[derive(Debug)]
pub struct RollingZScore {
    window_size: usize,
    /// Lazy ring buffer (None until first value)
    buffer: Option<RingBuffer<f64, 100>>,
    mean: f64,
    m2: f64, // Sum of squared deviations (for std)
    count: usize,
}

impl RollingZScore {
    /// Create new z-score normalizer with specified window size
    ///
    /// Note: Buffer is NOT allocated until first value arrives (lazy init)
    pub fn new(window_size: usize) -> Self {
        Self {
            window_size: window_size.min(100), // Cap at 100 (ring buffer size)
            buffer: None,                      // Lazy allocation
            mean: 0.0,
            m2: 0.0,
            count: 0,
        }
    }

    /// Update normalizer with new value and return normalized value
    ///
    /// # Returns
    /// - Normalized value clipped to ±3σ
    /// - Returns 0.0 during warmup period (first 10 values)
    pub fn update(&mut self, value: f64) -> f64 {
        // Lazy buffer initialization
        let buffer = self.buffer.get_or_insert_with(RingBuffer::new);

        // Track old value for rolling window update
        let old_val = (buffer.len() >= self.window_size).then(|| {
            // Extract oldest value before push (will be overwritten)
            let start_idx = buffer.head;
            buffer.data[start_idx]
        });

        // Add new value
        buffer.push(value);

        if let Some(old_val) = old_val {
            // Rolling window: remove old value, add new value
            let delta = value - old_val;
            self.mean += delta / self.count as f64;
            self.m2 += delta * (value - self.mean + old_val - self.mean);
        } else {
            // Warmup phase: incremental update
            self.count = buffer.len();
            let delta = value - self.mean;
            self.mean += delta / self.count as f64;
            let delta2 = value - self.mean;
            self.m2 += delta * delta2;
        }

        // Warmup period: return 0.0 for first 10 values (insufficient data)
        if self.count < 10 {
            return 0.0;
        }

        // Compute normalized value
        let std = self.std();
        let normalized = (value - self.mean) / (std + EPSILON);

        // Clip to ±3σ (99.7% of Gaussian distribution)
        normalized.clamp(-3.0, 3.0)
    }

    /// Get current standard deviation
    pub fn std(&self) -> f64 {
        if self.count < 2 {
            return 0.0;
        }
        // Ensure m2 is non-negative (prevent NaN from floating-point errors)
        let variance = self.m2.max(0.0) / (self.count - 1) as f64;
        variance.sqrt()
    }

    /// Reset normalizer state
    pub const fn reset(&mut self) {
        self.buffer = None; // Drop buffer (lazy reallocation on next update)
        self.mean = 0.0;
        self.m2 = 0.0;
        self.count = 0;
    }
}

//
// Category 2: Percentile Rank Normalization for Volume Features
//

/// Rolling percentile rank normalization with lazy buffer allocation
///
/// Maps values to [0, 1] based on their rank within rolling window.
/// Handles skewed distributions (e.g., volume) better than z-score.
///
/// ## Memory Optimization
/// - OLD: `VecDeque` with `capacity=window_size` → heap allocation
/// - NEW: Option<`RingBuffer`<f64, 100>> → None until first value
/// - Savings: 800 bytes per normalizer
#[derive(Debug)]
pub struct RollingPercentileRank {
    window_size: usize,
    /// Lazy ring buffer (None until first value)
    buffer: Option<RingBuffer<f64, 100>>,
}

impl RollingPercentileRank {
    /// Create new percentile rank normalizer
    ///
    /// Note: Buffer is NOT allocated until first value arrives (lazy init)
    pub fn new(window_size: usize) -> Self {
        Self {
            window_size: window_size.min(100), // Cap at 100 (ring buffer size)
            buffer: None,                      // Lazy allocation
        }
    }

    /// Update normalizer with new value and return percentile rank [0, 1]
    ///
    /// # Returns
    /// - Percentile rank in [0, 1] range
    /// - Returns 0.5 during warmup period (first 10 values)
    pub fn update(&mut self, value: f64) -> f64 {
        // Lazy buffer initialization
        let buffer = self.buffer.get_or_insert_with(RingBuffer::new);

        // Add new value
        buffer.push(value);

        // Warmup period: return 0.5 (median) for first 10 values
        if buffer.len() < 10 {
            return 0.5;
        }

        // Compute percentile rank (count of values < current value)
        let rank = buffer.iter().filter(|&&v| v < value).count();

        // Normalize to [0, 1]
        let normalized = rank as f64 / buffer.len() as f64;
        normalized.clamp(0.0, 1.0)
    }

    /// Reset normalizer state
    pub const fn reset(&mut self) {
        self.buffer = None; // Drop buffer (lazy reallocation on next update)
    }
}

//
// Category 3: Log Transform + Z-Score for Microstructure Features
//

/// Log transform followed by z-score normalization
///
/// Handles highly skewed microstructure features (e.g., Amihud illiquidity: 1e-9 to 1e-5).
/// Log transform stabilizes variance and makes distribution more Gaussian.
#[derive(Debug)]
pub struct LogZScoreNormalizer {
    scale_factor: f64,
    zscore: RollingZScore,
}

impl LogZScoreNormalizer {
    /// Create new log+z-score normalizer
    ///
    /// # Arguments
    /// - `scale_factor`: Multiplier before log transform (maps to reasonable log range)
    /// - `window_size`: Rolling window size for z-score computation
    ///
    /// # Scale Factors
    /// - Roll spread: 1.0 (already in price units, ~0.01-10.0)
    /// - Amihud illiquidity: 1e8 (map 1e-8 → 1.0 for ln)
    /// - Corwin-Schultz spread: 100.0 (map 0.01 → 1.0 for ln)
    pub fn new(scale_factor: f64, window_size: usize) -> Self {
        Self {
            scale_factor,
            zscore: RollingZScore::new(window_size),
        }
    }

    /// Update normalizer with new value and return normalized value
    ///
    /// # Returns
    /// - Normalized value clipped to ±3σ after log transform
    /// - Returns 0.0 during warmup period
    pub fn update(&mut self, value: f64) -> f64 {
        // Step 1: Log transform
        let log_val = if value > 0.0 {
            (value * self.scale_factor).ln()
        } else {
            // Zero or negative values: map to -10.0 (extreme negative, clipped to -3σ)
            -10.0
        };

        // Step 2: Z-score normalization
        self.zscore.update(log_val)
    }

    /// Reset normalizer state
    pub const fn reset(&mut self) {
        self.zscore.reset();
    }
}

//
// NaN/Inf Handling
//

/// NaN/Inf handler using last-valid-value imputation
///
/// Prevents NaN propagation through feature pipeline while preserving
/// temporal continuity (better than zero imputation which biases toward zero).
#[derive(Debug)]
struct NaNHandler {
    last_valid: [f64; 225],
    nan_count: [u32; 225],
}

impl NaNHandler {
    const fn new() -> Self {
        Self {
            last_valid: [0.0; 225],
            nan_count: [0; 225],
        }
    }

    /// Handle NaN/Inf in input features (impute with last valid value)
    fn handle_input(&mut self, features: &mut [f64; 225]) {
        for (i, val) in features.iter_mut().enumerate() {
            if !val.is_finite() {
                // Impute with last valid value
                *val = self.last_valid[i];
                self.nan_count[i] += 1;

                // Log warning if excessive NaNs (every 100 occurrences)
                if self.nan_count[i] % 100 == 0 {
                    warn!(
                        feature_index = i,
                        nan_count = self.nan_count[i],
                        "Excessive NaN occurrences for feature"
                    );
                }
            } else {
                // Update last valid value
                self.last_valid[i] = *val;
                self.nan_count[i] = 0; // Reset counter
            }
        }
    }

    /// Get total NaN count across all features
    fn total_nan_count(&self) -> u32 {
        self.nan_count.iter().sum()
    }

    /// Reset handler state
    const fn reset(&mut self) {
        self.last_valid = [0.0; 225];
        self.nan_count = [0; 225];
    }
}

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

    //
    // RollingZScore Tests (5 tests)
    //

    #[test]
    fn test_rolling_zscore_basic() {
        let mut zscore = RollingZScore::new(10);

        // Add 10 values: 1, 2, 3, ..., 10 (mean=5.5, std≈2.87)
        for i in 1..=10 {
            zscore.update(i as f64);
        }

        // Next value: 11 (z-score ≈ (11 - 5.5) / 2.87 ≈ 1.92)
        let normalized = zscore.update(11.0);
        assert!(
            normalized > 1.5 && normalized < 2.5,
            "Z-score for 11 should be ~1.92, got {}",
            normalized
        );
    }

    #[test]
    fn test_rolling_zscore_warmup() {
        let mut zscore = RollingZScore::new(50);

        // First 10 values should return 0.0 (warmup period)
        for i in 1..=9 {
            let normalized = zscore.update(i as f64);
            assert_eq!(
                normalized, 0.0,
                "Warmup period should return 0.0, got {}",
                normalized
            );
        }
    }

    #[test]
    fn test_rolling_zscore_clipping() {
        let mut zscore = RollingZScore::new(20);

        // Add 20 normal values (mean=50, std≈1)
        for _ in 0..20 {
            zscore.update(50.0);
        }

        // Add extreme outlier (>3σ)
        let normalized = zscore.update(100.0);
        assert!(
            normalized <= 3.0,
            "Z-score should be clipped to +3σ, got {}",
            normalized
        );
    }

    #[test]
    fn test_rolling_zscore_mean_std() {
        let mut zscore = RollingZScore::new(50);

        // Add 50 values: 1, 2, 3, ..., 50 (mean=25.5)
        for i in 1..=50 {
            zscore.update(i as f64);
        }

        assert!(
            (zscore.mean - 25.5).abs() < 0.1,
            "Mean should be ~25.5, got {}",
            zscore.mean
        );
        assert!(
            zscore.std() > 14.0 && zscore.std() < 15.0,
            "Std should be ~14.4, got {}",
            zscore.std()
        );
    }

    #[test]
    fn test_rolling_zscore_reset() {
        let mut zscore = RollingZScore::new(20);

        // Add 20 values
        for i in 1..=20 {
            zscore.update(i as f64);
        }

        // Reset
        zscore.reset();
        assert_eq!(zscore.count, 0, "Count should be 0 after reset");
        assert_eq!(zscore.mean, 0.0, "Mean should be 0.0 after reset");
        assert!(zscore.buffer.is_none(), "Buffer should be None after reset");
    }

    //
    // RollingPercentileRank Tests (5 tests)
    //

    #[test]
    fn test_percentile_rank_basic() {
        let mut percentile = RollingPercentileRank::new(10);

        // Add 10 values: 1, 2, 3, ..., 10
        for i in 1..=10 {
            percentile.update(i as f64);
        }

        // Next value: 5.5 (rank should be ~5/10 = 0.5)
        let normalized = percentile.update(5.5);
        assert!(
            (normalized - 0.5).abs() < 0.2,
            "Percentile rank for 5.5 should be ~0.5, got {}",
            normalized
        );
    }

    #[test]
    fn test_percentile_rank_warmup() {
        let mut percentile = RollingPercentileRank::new(50);

        // First 10 values should return 0.5 (warmup period)
        for i in 1..=9 {
            let normalized = percentile.update(i as f64);
            assert_eq!(
                normalized, 0.5,
                "Warmup period should return 0.5, got {}",
                normalized
            );
        }
    }

    #[test]
    fn test_percentile_rank_bounds() {
        let mut percentile = RollingPercentileRank::new(20);

        // Add 20 values: 1, 2, 3, ..., 20
        for i in 1..=20 {
            percentile.update(i as f64);
        }

        // Minimum value: 0 (rank = 0)
        let min_normalized = percentile.update(0.0);
        assert_eq!(min_normalized, 0.0, "Minimum should be 0.0");

        // Maximum value: 100 (rank = 1.0)
        let max_normalized = percentile.update(100.0);
        assert!(
            max_normalized >= 0.9,
            "Maximum should be close to 1.0, got {}",
            max_normalized
        );
    }

    #[test]
    fn test_percentile_rank_skewed_distribution() {
        let mut percentile = RollingPercentileRank::new(20);

        // Add skewed distribution: 1, 1, 1, ..., 1 (18 times), 100, 100
        for _ in 0..18 {
            percentile.update(1.0);
        }
        percentile.update(100.0);
        percentile.update(100.0);

        // New value: 50 (rank should be ~18/20 = 0.9)
        let normalized = percentile.update(50.0);
        assert!(
            normalized > 0.8,
            "Percentile rank for 50 in skewed distribution should be high, got {}",
            normalized
        );
    }

    #[test]
    fn test_percentile_rank_reset() {
        let mut percentile = RollingPercentileRank::new(20);

        // Add 20 values
        for i in 1..=20 {
            percentile.update(i as f64);
        }

        // Reset
        percentile.reset();
        assert!(
            percentile.buffer.is_none(),
            "Buffer should be None after reset"
        );
    }

    //
    // LogZScoreNormalizer Tests (5 tests)
    //

    #[test]
    fn test_log_zscore_basic() {
        let mut log_zscore = LogZScoreNormalizer::new(1.0, 20);

        // Add 20 values: 1, 2, 3, ..., 20 (log transforms to more Gaussian)
        for i in 1..=20 {
            log_zscore.update(i as f64);
        }

        // Next value: 10 (log(10) ≈ 2.3, should be near mean after normalization)
        let normalized = log_zscore.update(10.0);
        assert!(
            normalized.abs() < 2.0,
            "Normalized log(10) should be near 0, got {}",
            normalized
        );
    }

    #[test]
    fn test_log_zscore_scale_factor() {
        let mut log_zscore = LogZScoreNormalizer::new(1e8, 20);

        // Add 20 small values: 1e-8, 2e-8, ..., 20e-8 (Amihud illiquidity range)
        for i in 1..=20 {
            log_zscore.update(i as f64 * 1e-8);
        }

        // Next value: 10e-8 (log(10e-8 * 1e8) = log(1) = 0)
        let normalized = log_zscore.update(10e-8);
        assert!(
            normalized.abs() < 2.0,
            "Normalized log(10e-8) with scale 1e8 should be near 0, got {}",
            normalized
        );
    }

    #[test]
    fn test_log_zscore_zero_handling() {
        let mut log_zscore = LogZScoreNormalizer::new(1.0, 20);

        // Add 20 positive values
        for i in 1..=20 {
            log_zscore.update(i as f64);
        }

        // Zero value should map to -10.0 before z-score (extreme negative)
        let normalized = log_zscore.update(0.0);
        assert!(
            normalized <= -3.0,
            "Zero should be clipped to -3σ or lower, got {}",
            normalized
        );
    }

    #[test]
    fn test_log_zscore_negative_handling() {
        let mut log_zscore = LogZScoreNormalizer::new(1.0, 20);

        // Add 20 positive values
        for i in 1..=20 {
            log_zscore.update(i as f64);
        }

        // Negative value should map to -10.0 (same as zero)
        let normalized = log_zscore.update(-5.0);
        assert!(
            normalized <= -3.0,
            "Negative should be clipped to -3σ or lower, got {}",
            normalized
        );
    }

    #[test]
    fn test_log_zscore_reset() {
        let mut log_zscore = LogZScoreNormalizer::new(1.0, 20);

        // Add 20 values
        for i in 1..=20 {
            log_zscore.update(i as f64);
        }

        // Reset
        log_zscore.reset();
        assert_eq!(
            log_zscore.zscore.count, 0,
            "Z-score count should be 0 after reset"
        );
    }

    //
    // NaNHandler Tests (5 tests)
    //

    #[test]
    fn test_nan_handler_basic() {
        let mut handler = NaNHandler::new();
        let mut features = [1.0; 225];

        // Set feature 10 to NaN
        features[10] = f64::NAN;

        // Handle input (should impute with last valid value = 0.0 initially)
        handler.handle_input(&mut features);
        assert_eq!(
            features[10], 0.0,
            "NaN should be imputed with last valid value (0.0)"
        );
    }

    #[test]
    fn test_nan_handler_last_valid_value() {
        let mut handler = NaNHandler::new();

        // First update: set feature 10 to 42.0
        let mut features = [0.0; 225];
        features[10] = 42.0;
        handler.handle_input(&mut features);

        // Second update: set feature 10 to NaN (should impute with 42.0)
        features[10] = f64::NAN;
        handler.handle_input(&mut features);
        assert_eq!(
            features[10], 42.0,
            "NaN should be imputed with last valid value (42.0)"
        );
    }

    #[test]
    fn test_nan_handler_inf() {
        let mut handler = NaNHandler::new();
        let mut features = [1.0; 225];

        // Set feature 20 to Inf
        features[20] = f64::INFINITY;

        // Handle input (should impute with last valid value = 0.0)
        handler.handle_input(&mut features);
        assert_eq!(
            features[20], 0.0,
            "Inf should be imputed with last valid value (0.0)"
        );
    }

    #[test]
    fn test_nan_handler_count() {
        let mut handler = NaNHandler::new();

        // First update: NaN at feature 10
        let mut features = [0.0; 225];
        features[10] = f64::NAN;
        handler.handle_input(&mut features);
        assert_eq!(handler.nan_count[10], 1, "NaN count should be 1");

        // Second update: valid value at feature 10 (count resets)
        features[10] = 42.0;
        handler.handle_input(&mut features);
        assert_eq!(
            handler.nan_count[10], 0,
            "NaN count should reset to 0 after valid value"
        );
    }

    #[test]
    fn test_nan_handler_reset() {
        let mut handler = NaNHandler::new();

        // Add some NaNs
        let mut features = [0.0; 225];
        features[10] = f64::NAN;
        handler.handle_input(&mut features);

        // Reset
        handler.reset();
        assert_eq!(
            handler.total_nan_count(),
            0,
            "Total NaN count should be 0 after reset"
        );
    }

    //
    // FeatureNormalizer Integration Tests (5 tests)
    //

    #[test]
    fn test_feature_normalizer_basic() {
        let mut normalizer = FeatureNormalizer::new();
        let mut features = [1.0; 225];

        // Normalize (should succeed)
        let result = normalizer.normalize(&mut features);
        assert!(result.is_ok(), "Normalization should succeed");
    }

    #[test]
    fn test_feature_normalizer_price_features() {
        let mut normalizer = FeatureNormalizer::new();

        // Feed 50 bars to warmup
        for i in 0..50 {
            let mut features = [0.0; 225];
            features[15] = i as f64; // Price feature at index 15
            normalizer.normalize(&mut features).unwrap();
        }

        // Next bar: should have normalized price feature
        let mut features = [0.0; 225];
        features[15] = 25.0; // Mean value
        normalizer.normalize(&mut features).unwrap();

        // Price feature should be near 0 (mean)
        assert!(
            features[15].abs() < 1.0,
            "Normalized price feature should be near 0, got {}",
            features[15]
        );
    }

    #[test]
    fn test_feature_normalizer_volume_features() {
        let mut normalizer = FeatureNormalizer::new();

        // Feed 50 bars to warmup
        for i in 0..50 {
            let mut features = [0.0; 225];
            features[75] = i as f64; // Volume feature at index 75
            normalizer.normalize(&mut features).unwrap();
        }

        // Next bar: minimum volume
        let mut features = [0.0; 225];
        features[75] = 0.0;
        normalizer.normalize(&mut features).unwrap();

        // Volume feature should be 0.0 (minimum percentile)
        assert!(
            features[75] < 0.2,
            "Normalized volume feature should be near 0.0, got {}",
            features[75]
        );
    }

    #[test]
    fn test_feature_normalizer_nan_handling() {
        let mut normalizer = FeatureNormalizer::new();

        // First bar: valid features
        let mut features = [42.0; 225];
        normalizer.normalize(&mut features).unwrap();

        // Second bar: NaN at feature 15 (should impute with last valid value)
        let mut features2 = [42.0; 225];
        features2[15] = f64::NAN;
        let result = normalizer.normalize(&mut features2);
        assert!(
            result.is_ok(),
            "Normalization should succeed after NaN imputation"
        );
    }

    #[test]
    fn test_feature_normalizer_reset() {
        let mut normalizer = FeatureNormalizer::new();

        // Feed 50 bars
        for i in 0..50 {
            let mut features = [i as f64; 225];
            normalizer.normalize(&mut features).unwrap();
        }

        // Reset
        normalizer.reset();

        // Stats should be reset
        let stats = normalizer.get_stats();
        assert_eq!(stats.price_mean, 0.0, "Mean should be 0.0 after reset");
        assert_eq!(stats.nan_count, 0, "NaN count should be 0 after reset");
    }
}
