//! Order Flow Imbalance (OFI) Feature Calculator
//!
//! Implements research-backed OFI features from academic literature:
//! - Cont et al. (2010, 2014): OFI explains 65% of variance in 1-minute price changes
//! - Xu et al. (2019): Multi-level OFI reduces forecast RMSE by 40%
//! - Easley et al. (2011, 2012): VPIN for informed trading detection
//! - Kyle (1985): Market impact coefficient
//!
//! OFI is the #1 predictor of short-term price movements in academic research.
//!
//! # Features (8 total)
//!
//! 1. **OFI Level 1**: Bid-ask imbalance at best price (Cont et al.)
//! 2. **OFI Level 5**: Multi-level imbalance with exponential weighting
//! 3. **Depth Imbalance**: Total bid vs ask volume ratio
//! 4. **VPIN**: Volume-synchronized probability of informed trading
//! 5. **Kyle's Lambda**: Market impact coefficient
//! 6. **Bid Slope**: Order book shape (bid side)
//! 7. **Ask Slope**: Order book shape (ask side)
//! 8. **Trade Imbalance**: Buy/sell volume ratio (inferred)
//!
//! # Performance
//!
//! Target: <100μs per snapshot calculation
//!
//! # Example
//!
//! ```no_run
//! use ml::features::ofi_calculator::OFICalculator;
//! use data::providers::databento::mbp10::Mbp10Snapshot;
//!
//! let mut calculator = OFICalculator::new();
//! let features = calculator.calculate(&snapshots)?;
//!
//! assert!(features.ofi_level1.is_finite());
//! assert!(features.depth_imbalance >= -1.0 && features.depth_imbalance <= 1.0);
//! ```

use data::providers::databento::mbp10::Mbp10Snapshot;
use std::collections::VecDeque;

use crate::MLError;

/// Order Flow Imbalance features (8 features)
#[derive(Debug, Clone)]
pub struct OFIFeatures {
    /// Feature 1: OFI at best bid/ask level (normalized)
    pub ofi_level1: f64,

    /// Feature 2: Multi-level OFI (levels 1-5, weighted)
    pub ofi_level5: f64,

    /// Feature 3: Depth imbalance (bid vs ask volume ratio, [-1, +1])
    pub depth_imbalance: f64,

    /// Feature 4: VPIN (Volume-synchronized probability of informed trading, [0, 1])
    pub vpin: f64,

    /// Feature 5: Kyle's lambda (market impact coefficient)
    pub kyle_lambda: f64,

    /// Feature 6: Bid slope (order book shape)
    pub bid_slope: f64,

    /// Feature 7: Ask slope (order book shape)
    pub ask_slope: f64,

    /// Feature 8: Trade imbalance (inferred from price movement)
    pub trade_imbalance: f64,
}

impl OFIFeatures {
    /// Convert to array (for integration with feature extraction)
    pub const fn to_array(&self) -> [f64; 8] {
        [
            self.ofi_level1,
            self.ofi_level5,
            self.depth_imbalance,
            self.vpin,
            self.kyle_lambda,
            self.bid_slope,
            self.ask_slope,
            self.trade_imbalance,
        ]
    }

    /// Create zero-initialized features (for missing data)
    pub const fn zeros() -> Self {
        Self {
            ofi_level1: 0.0,
            ofi_level5: 0.0,
            depth_imbalance: 0.0,
            vpin: 0.0,
            kyle_lambda: 0.0,
            bid_slope: 0.0,
            ask_slope: 0.0,
            trade_imbalance: 0.0,
        }
    }

    /// Check if all features are finite (no NaN/Inf)
    pub const fn is_valid(&self) -> bool {
        self.ofi_level1.is_finite()
            && self.ofi_level5.is_finite()
            && self.depth_imbalance.is_finite()
            && self.vpin.is_finite()
            && self.kyle_lambda.is_finite()
            && self.bid_slope.is_finite()
            && self.ask_slope.is_finite()
            && self.trade_imbalance.is_finite()
    }
}

/// OFI Calculator with state management
#[derive(Debug)]
pub struct OFICalculator {
    /// Previous snapshot (for delta calculations)
    prev_snapshot: Option<Mbp10Snapshot>,

    /// VPIN calculator
    vpin_calculator: VPINCalculator,

    /// Kyle's lambda calculator
    kyle_lambda_calculator: KyleLambdaCalculator,

    /// Trade imbalance tracker
    trade_imbalance: TradeImbalanceTracker,

    /// OFI statistics for normalization (300-second rolling window)
    ofi_stats: OFIStats,

    /// Previous trade price for Kyle's Lambda price-change computation
    prev_trade_price: Option<f64>,

    /// Whether real trade data has been fed (enables VPIN/Lambda from real trades)
    has_trade_data: bool,
}

impl OFICalculator {
    /// Create new OFI calculator
    pub fn new() -> Self {
        Self {
            prev_snapshot: None,
            vpin_calculator: VPINCalculator::new(50_000, 50), // 50k contracts per bucket, 50 buckets
            kyle_lambda_calculator: KyleLambdaCalculator::new(100), // 100 trade window
            trade_imbalance: TradeImbalanceTracker::new(100),      // 100 trade window
            ofi_stats: OFIStats::new(300),                         // 300 snapshot window (5 min)
            prev_trade_price: None,
            has_trade_data: false,
        }
    }

    /// Feed a real trade event (from Databento `Schema::Trades`).
    ///
    /// Call this for each trade between MBP-10 snapshots to populate VPIN,
    /// Kyle's Lambda, and trade imbalance with actual trade data instead of
    /// inferred proxies.
    ///
    /// # Arguments
    ///
    /// * `price` - Trade price (f64, e.g. 5432.50 for ES futures)
    /// * `volume` - Trade size in contracts
    /// * `is_buy` - True if buyer-initiated (Databento side='B'), false if seller-initiated
    pub fn feed_trade(&mut self, price: f64, volume: u64, is_buy: bool) {
        self.has_trade_data = true;

        // VPIN: accumulate signed volume buckets (Easley et al. 2012)
        self.vpin_calculator.update(volume, is_buy);

        // Kyle's Lambda: price_change vs signed_volume regression (Kyle 1985)
        if let Some(prev_price) = self.prev_trade_price {
            let price_change = price - prev_price;
            let signed_volume = if is_buy {
                volume as f64
            } else {
                -(volume as f64)
            };
            self.kyle_lambda_calculator
                .update(price_change, signed_volume);
        }
        self.prev_trade_price = Some(price);

        // Trade imbalance: real buy/sell classification
        self.trade_imbalance.update_from_trade(volume, is_buy);
    }

    /// Calculate all 8 OFI features from current snapshot
    pub fn calculate(&mut self, current: &Mbp10Snapshot) -> Result<OFIFeatures, MLError> {
        if current.levels.is_empty() {
            return Err(MLError::InsufficientData(
                "MBP-10 snapshot has no levels".to_owned(),
            ));
        }

        // Feature 1: OFI Level 1
        let ofi_l1_raw = if let Some(ref prev) = self.prev_snapshot {
            Self::calc_ofi_l1(current, prev)
        } else {
            0.0
        };
        self.ofi_stats.update(ofi_l1_raw);
        let ofi_level1 = self.ofi_stats.normalize(ofi_l1_raw);

        // Feature 2: OFI Level 5
        let ofi_level5 = if let Some(ref prev) = self.prev_snapshot {
            Self::calc_ofi_l5(current, prev)
        } else {
            0.0
        };

        // Feature 3: Depth Imbalance
        let depth_imbalance = Self::calc_depth_imbalance(current);

        // Feature 4: VPIN — real trade data when available, 0.0 otherwise
        let vpin = self.vpin_calculator.compute();

        // Feature 5: Kyle's Lambda — real trade data when available, 0.0 otherwise
        let kyle_lambda = self.kyle_lambda_calculator.compute();

        // Feature 6-7: Bid/Ask Slope
        let (bid_slope, ask_slope) = Self::calc_slopes(current);

        // Feature 8: Trade Imbalance
        // When real trade data is fed via feed_trade(), use accumulated buy/sell pressure.
        // Otherwise fall back to tick-rule inference from price movement.
        let trade_imbalance = if self.has_trade_data {
            self.trade_imbalance.imbalance()
        } else if let Some(ref prev) = self.prev_snapshot {
            self.trade_imbalance
                .update(current.mid_price(), prev.mid_price())
        } else {
            0.0
        };

        // Store current snapshot for next iteration
        self.prev_snapshot = Some(current.clone());

        Ok(OFIFeatures {
            ofi_level1,
            ofi_level5,
            depth_imbalance,
            vpin,
            kyle_lambda,
            bid_slope,
            ask_slope,
            trade_imbalance,
        })
    }

    /// Feature 1: OFI Level 1 (Best Bid/Ask)
    ///
    /// Formula (Cont et al., 2010):
    /// OFI = I_{P^B ≥ P_{prev}^B} × q^B - I_{P^B ≤ P_{prev}^B} × q_{prev}^B
    ///     - I_{P^A ≤ P_{prev}^A} × q^A + I_{P^A ≥ P_{prev}^A} × q_{prev}^A
    fn calc_ofi_l1(current: &Mbp10Snapshot, previous: &Mbp10Snapshot) -> f64 {
        let curr_level = &current.levels[0];
        let prev_level = &previous.levels[0];

        // Bid contribution
        let bid_contrib = if curr_level.bid_px >= prev_level.bid_px {
            curr_level.bid_sz as f64
        } else {
            -(prev_level.bid_sz as f64)
        };

        // Ask contribution
        let ask_contrib = if curr_level.ask_px <= prev_level.ask_px {
            curr_level.ask_sz as f64
        } else {
            -(prev_level.ask_sz as f64)
        };

        safe_clip(bid_contrib - ask_contrib, -1e6, 1e6)
    }

    /// Feature 2: OFI Level 5 (Multi-Level)
    ///
    /// Exponentially weighted OFI across 5 depth levels
    /// Weights: exp(-0.5 * level)
    fn calc_ofi_l5(current: &Mbp10Snapshot, previous: &Mbp10Snapshot) -> f64 {
        const WEIGHTS: [f64; 5] = [
            1.0,   // Level 0: exp(-0.5 * 0) = 1.0
            0.606, // Level 1: exp(-0.5 * 1) = 0.606
            0.368, // Level 2: exp(-0.5 * 2) = 0.368
            0.223, // Level 3: exp(-0.5 * 3) = 0.223
            0.135, // Level 4: exp(-0.5 * 4) = 0.135
        ];

        let max_levels = current.levels.len().min(previous.levels.len()).min(5);
        let mut weighted_sum = 0.0;

        for (i, weight) in WEIGHTS.iter().enumerate().take(max_levels) {
            let curr_level = &current.levels[i];
            let prev_level = &previous.levels[i];

            let bid_contrib = if curr_level.bid_px >= prev_level.bid_px {
                curr_level.bid_sz as f64
            } else {
                -(prev_level.bid_sz as f64)
            };

            let ask_contrib = if curr_level.ask_px <= prev_level.ask_px {
                curr_level.ask_sz as f64
            } else {
                -(prev_level.ask_sz as f64)
            };

            weighted_sum += weight * (bid_contrib - ask_contrib);
        }

        safe_clip(weighted_sum, -1e6, 1e6)
    }

    /// Feature 3: Depth Imbalance
    ///
    /// Formula: (`total_bid_vol` - `total_ask_vol`) / (`total_bid_vol` + `total_ask_vol`)
    /// Range: [-1, +1]
    fn calc_depth_imbalance(snapshot: &Mbp10Snapshot) -> f64 {
        let bid_depth: f64 = snapshot.levels.iter().map(|l| l.bid_sz as f64).sum();
        let ask_depth: f64 = snapshot.levels.iter().map(|l| l.ask_sz as f64).sum();

        let total = bid_depth + ask_depth;
        if total < 1.0 {
            return 0.0;
        }

        safe_clip((bid_depth - ask_depth) / total, -1.0, 1.0)
    }

    /// Feature 6-7: Bid/Ask Slope (Order Book Shape)
    ///
    /// Linear regression of cumulative volume vs. price level
    /// Positive slope = liquidity increases with depth (stable)
    /// Negative slope = liquidity decreases with depth (thin book)
    fn calc_slopes(snapshot: &Mbp10Snapshot) -> (f64, f64) {
        let max_levels = snapshot.levels.len().min(5);

        // Bid side: cumulative volume vs level
        let mut bid_points = Vec::with_capacity(max_levels);
        let mut cum_bid_vol = 0.0;
        for (i, level) in snapshot.levels.iter().take(max_levels).enumerate() {
            cum_bid_vol += level.bid_sz as f64;
            bid_points.push((i as f64, cum_bid_vol));
        }

        // Ask side: cumulative volume vs level
        let mut ask_points = Vec::with_capacity(max_levels);
        let mut cum_ask_vol = 0.0;
        for (i, level) in snapshot.levels.iter().take(max_levels).enumerate() {
            cum_ask_vol += level.ask_sz as f64;
            ask_points.push((i as f64, cum_ask_vol));
        }

        let bid_slope = linear_regression_slope(&bid_points);
        let ask_slope = linear_regression_slope(&ask_points);

        (bid_slope, ask_slope)
    }

    /// Returns true if real trade data has been fed via `feed_trade()`.
    pub const fn has_trade_data(&self) -> bool {
        self.has_trade_data
    }
}

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

/// VPIN (Volume-Synchronized Probability of Informed Trading)
///
/// Based on Easley et al. (2011, 2012)
/// Measures the probability that informed traders are active
#[derive(Debug)]
struct VPINCalculator {
    num_buckets: usize,
    signed_volumes: VecDeque<f64>,
}

impl VPINCalculator {
    fn new(_bucket_size: u64, num_buckets: usize) -> Self {
        Self {
            num_buckets,
            signed_volumes: VecDeque::with_capacity(num_buckets),
        }
    }

    /// Update VPIN with new trade
    /// Note: This is a simplified implementation
    /// Full implementation requires trade-level data
    fn update(&mut self, volume: u64, is_buy: bool) {
        let signed_vol = if is_buy {
            volume as f64
        } else {
            -(volume as f64)
        };

        self.signed_volumes.push_back(signed_vol);
        if self.signed_volumes.len() > self.num_buckets {
            self.signed_volumes.pop_front();
        }
    }

    /// Compute VPIN from accumulated signed volumes
    fn compute(&self) -> f64 {
        if self.signed_volumes.is_empty() {
            return 0.0;
        }

        let abs_sum: f64 = self.signed_volumes.iter().map(|v| v.abs()).sum();
        let total: f64 = self
            .signed_volumes
            .iter()
            .map(|v| v.abs())
            .sum::<f64>()
            .max(1.0);

        safe_clip(abs_sum / total, 0.0, 1.0)
    }
}

/// Kyle's Lambda (Market Impact Coefficient)
///
/// Based on Kyle (1985)
/// Lambda = regression slope of `price_change` vs `signed_volume`
#[derive(Debug)]
struct KyleLambdaCalculator {
    price_changes: VecDeque<f64>,
    signed_volumes: VecDeque<f64>,
    window_size: usize,
}

impl KyleLambdaCalculator {
    fn new(window_size: usize) -> Self {
        Self {
            price_changes: VecDeque::with_capacity(window_size),
            signed_volumes: VecDeque::with_capacity(window_size),
            window_size,
        }
    }

    /// Update with new price change and volume
    fn update(&mut self, price_change: f64, signed_volume: f64) {
        self.price_changes.push_back(price_change);
        self.signed_volumes.push_back(signed_volume);

        if self.price_changes.len() > self.window_size {
            self.price_changes.pop_front();
            self.signed_volumes.pop_front();
        }
    }

    /// Compute Kyle's lambda via linear regression
    fn compute(&self) -> f64 {
        let n = self.price_changes.len();
        if n < 2 {
            return 0.0;
        }

        let mean_price: f64 = self.price_changes.iter().sum::<f64>() / n as f64;
        let mean_vol: f64 = self.signed_volumes.iter().sum::<f64>() / n as f64;

        let mut cov = 0.0;
        let mut var = 0.0;
        for i in 0..n {
            let dp = self.price_changes[i] - mean_price;
            let dv = self.signed_volumes[i] - mean_vol;
            cov += dp * dv;
            var += dv * dv;
        }

        if var < 1e-8 {
            return 0.0;
        }

        let lambda = cov / var;
        safe_clip(lambda, -1e-3, 1e-3) // Typical range for ES futures
    }
}

/// Trade Imbalance Tracker
///
/// Infers buy/sell pressure from price movements
/// (Simplified version without actual trade data)
#[derive(Debug)]
struct TradeImbalanceTracker {
    buy_pressure: f64,
    sell_pressure: f64,
    window_size: usize,
    count: usize,
}

impl TradeImbalanceTracker {
    const fn new(window_size: usize) -> Self {
        Self {
            buy_pressure: 0.0,
            sell_pressure: 0.0,
            window_size,
            count: 0,
        }
    }

    /// Current imbalance ratio from accumulated pressure ([-1, +1])
    fn imbalance(&self) -> f64 {
        let total = self.buy_pressure + self.sell_pressure;
        if total < 1e-8 {
            return 0.0;
        }
        safe_clip(
            (self.buy_pressure - self.sell_pressure) / total,
            -1.0,
            1.0,
        )
    }

    /// Update with real trade data (buyer/seller classified)
    fn update_from_trade(&mut self, volume: u64, is_buy: bool) {
        let vol = volume as f64;
        if is_buy {
            self.buy_pressure += vol;
        } else {
            self.sell_pressure += vol;
        }

        self.count += 1;
        if self.count >= self.window_size {
            self.buy_pressure *= 0.95;
            self.sell_pressure *= 0.95;
            self.count = (self.window_size as f64 * 0.95) as usize;
        }
    }

    /// Update with inferred price movement (tick-rule fallback)
    /// Positive price change = buy pressure
    /// Negative price change = sell pressure
    fn update(&mut self, current_mid: f64, previous_mid: f64) -> f64 {
        let price_change = current_mid - previous_mid;

        if price_change > 0.0 {
            self.buy_pressure += price_change.abs();
        } else if price_change < 0.0 {
            self.sell_pressure += price_change.abs();
        } else {
            // No price change, no pressure update
        }

        self.count += 1;

        // Apply decay after window_size updates
        if self.count >= self.window_size {
            self.buy_pressure *= 0.95;
            self.sell_pressure *= 0.95;
            self.count = (self.window_size as f64 * 0.95) as usize;
        }

        let total = self.buy_pressure + self.sell_pressure;
        if total < 1e-8 {
            return 0.0;
        }

        safe_clip(
            (self.buy_pressure - self.sell_pressure) / total,
            -1.0,
            1.0,
        )
    }
}

/// OFI Statistics for normalization
///
/// Maintains rolling window for z-score normalization
#[derive(Debug)]
struct OFIStats {
    window: VecDeque<f64>,
    window_size: usize,
}

impl OFIStats {
    fn new(window_size: usize) -> Self {
        Self {
            window: VecDeque::with_capacity(window_size),
            window_size,
        }
    }

    fn update(&mut self, value: f64) {
        if !value.is_finite() {
            return;
        }

        self.window.push_back(value);
        if self.window.len() > self.window_size {
            self.window.pop_front();
        }
    }

    /// Normalize value using z-score
    /// Returns (value - mean) / std, clipped to [-3, +3]
    fn normalize(&self, value: f64) -> f64 {
        if self.window.len() < 2 {
            return 0.0;
        }

        let mean = self.mean();
        let std = self.std();

        if std < 1e-8 {
            return 0.0;
        }

        safe_clip((value - mean) / std, -3.0, 3.0)
    }

    fn mean(&self) -> f64 {
        let sum: f64 = self.window.iter().sum();
        sum / self.window.len() as f64
    }

    fn std(&self) -> f64 {
        let mean = self.mean();
        let variance: f64 = self
            .window
            .iter()
            .map(|x| (x - mean).powi(2))
            .sum::<f64>()
            / self.window.len() as f64;
        variance.sqrt()
    }
}

/// Linear regression slope calculation
///
/// Computes slope of y = a + b*x via least squares
fn linear_regression_slope(points: &[(f64, f64)]) -> f64 {
    let n = points.len() as f64;
    if n < 2.0 {
        return 0.0;
    }

    let sum_x: f64 = points.iter().map(|(x, _)| x).sum();
    let sum_y: f64 = points.iter().map(|(_, y)| y).sum();
    let sum_xy: f64 = points.iter().map(|(x, y)| x * y).sum();
    let sum_xx: f64 = points.iter().map(|(x, _)| x * x).sum();

    let denominator = n * sum_xx - sum_x * sum_x;
    if denominator.abs() < 1e-8 {
        return 0.0;
    }

    let slope = (n * sum_xy - sum_x * sum_y) / denominator;
    safe_clip(slope, -1e6, 1e6)
}

/// Safe clipping with NaN/Inf handling
const fn safe_clip(value: f64, min: f64, max: f64) -> f64 {
    if !value.is_finite() {
        return 0.0;
    }
    value.max(min).min(max)
}

#[cfg(test)]
mod tests {
    use super::*;
    use data::providers::databento::mbp10::BidAskPair;

    fn create_test_snapshot(
        bid_px: i64,
        ask_px: i64,
        bid_sz: u32,
        ask_sz: u32,
    ) -> Mbp10Snapshot {
        let levels = vec![
            BidAskPair {
                bid_px,
                bid_sz,
                bid_ct: 5,
                ask_px,
                ask_sz,
                ask_ct: 6,
            },
            BidAskPair {
                bid_px: bid_px - 1000000000,
                bid_sz: bid_sz * 2,
                bid_ct: 8,
                ask_px: ask_px + 1000000000,
                ask_sz: ask_sz * 2,
                ask_ct: 7,
            },
            BidAskPair {
                bid_px: bid_px - 2000000000,
                bid_sz: bid_sz * 3,
                bid_ct: 10,
                ask_px: ask_px + 2000000000,
                ask_sz: ask_sz * 3,
                ask_ct: 9,
            },
        ];

        Mbp10Snapshot::new("ES.FUT".to_owned(), 1640995200000000000, levels, 0, 100)
    }

    #[test]
    fn test_ofi_level1_rising_bid() {
        let prev = create_test_snapshot(150000000000000, 150010000000000, 120, 100);
        let curr = create_test_snapshot(150005000000000, 150010000000000, 120, 100); // Bid rises

        let ofi = OFICalculator::calc_ofi_l1(&curr, &prev);
        assert!(ofi > 0.0, "Rising bid should produce positive OFI");
    }

    #[test]
    fn test_ofi_level1_falling_ask() {
        let prev = create_test_snapshot(150000000000000, 150010000000000, 120, 100);
        let curr = create_test_snapshot(150000000000000, 150005000000000, 120, 100); // Ask falls

        let ofi = OFICalculator::calc_ofi_l1(&curr, &prev);
        assert!(ofi > 0.0, "Falling ask should produce positive OFI");
    }

    #[test]
    fn test_depth_imbalance_balanced() {
        let snapshot = create_test_snapshot(150000000000000, 150010000000000, 100, 100);
        let imbalance = OFICalculator::calc_depth_imbalance(&snapshot);

        // With symmetric levels, imbalance should be near zero
        assert!(
            imbalance.abs() < 0.2,
            "Balanced book should have near-zero imbalance"
        );
    }

    #[test]
    fn test_depth_imbalance_bid_heavy() {
        let snapshot = create_test_snapshot(150000000000000, 150010000000000, 200, 100);
        let imbalance = OFICalculator::calc_depth_imbalance(&snapshot);

        assert!(imbalance > 0.0, "Bid-heavy book should have positive imbalance");
    }

    #[test]
    fn test_slopes_calculation() {
        let snapshot = create_test_snapshot(150000000000000, 150010000000000, 100, 120);
        let (bid_slope, ask_slope) = OFICalculator::calc_slopes(&snapshot);

        assert!(bid_slope.is_finite(), "Bid slope should be finite");
        assert!(ask_slope.is_finite(), "Ask slope should be finite");
        assert!(
            bid_slope > 0.0,
            "Bid slope should be positive (increasing volume)"
        );
        assert!(
            ask_slope > 0.0,
            "Ask slope should be positive (increasing volume)"
        );
    }

    #[test]
    fn test_ofi_calculator_full_cycle() {
        let mut calculator = OFICalculator::new();

        let snapshot1 = create_test_snapshot(150000000000000, 150010000000000, 100, 120);
        let snapshot2 = create_test_snapshot(150005000000000, 150010000000000, 110, 115);

        // First calculation (no previous)
        let features1 = calculator.calculate(&snapshot1).unwrap();
        assert_eq!(features1.ofi_level1, 0.0, "First OFI should be zero");

        // Second calculation (with previous)
        let features2 = calculator.calculate(&snapshot2).unwrap();
        assert!(features2.is_valid(), "All features should be finite");
        assert!(
            features2.ofi_level1 != 0.0,
            "Second OFI should be non-zero"
        );
    }

    #[test]
    fn test_ofi_features_to_array() {
        let features = OFIFeatures {
            ofi_level1: 1.0,
            ofi_level5: 2.0,
            depth_imbalance: 0.5,
            vpin: 0.3,
            kyle_lambda: 0.001,
            bid_slope: 100.0,
            ask_slope: 110.0,
            trade_imbalance: 0.2,
        };

        let array = features.to_array();
        assert_eq!(array.len(), 8);
        assert_eq!(array[0], 1.0);
        assert_eq!(array[7], 0.2);
    }

    #[test]
    fn test_feed_trade_vpin_and_kyle_lambda() {
        let mut calculator = OFICalculator::new();

        // Feed a sequence of trades
        calculator.feed_trade(5432.50, 10, true);  // buy
        calculator.feed_trade(5432.75, 5, true);   // buy
        calculator.feed_trade(5432.50, 15, false);  // sell
        calculator.feed_trade(5432.25, 8, false);   // sell
        calculator.feed_trade(5432.50, 12, true);  // buy

        assert!(calculator.has_trade_data());

        // VPIN should be non-zero after feeding trades
        let vpin = calculator.vpin_calculator.compute();
        assert!(vpin > 0.0, "VPIN should be > 0 after feeding trades, got {}", vpin);

        // Kyle's Lambda should be computable after 2+ trades
        let lambda = calculator.kyle_lambda_calculator.compute();
        assert!(lambda.is_finite(), "Kyle's Lambda should be finite");

        // Trade imbalance: 3 buys (27 contracts) vs 2 sells (23 contracts) → positive
        let imbalance = calculator.trade_imbalance.imbalance();
        assert!(imbalance > 0.0, "More buy pressure should give positive imbalance, got {}", imbalance);
    }

    #[test]
    fn test_feed_trade_with_snapshot() {
        let mut calculator = OFICalculator::new();

        // Feed trades between snapshots
        calculator.feed_trade(5432.50, 10, true);
        calculator.feed_trade(5432.75, 5, false);

        // Then calculate OFI from a snapshot — VPIN/Lambda should reflect trade data
        let snap1 = create_test_snapshot(150000000000000, 150010000000000, 100, 120);
        let features1 = calculator.calculate(&snap1).unwrap();

        // VPIN should be non-zero (fed by real trades)
        assert!(features1.vpin > 0.0, "VPIN should reflect real trade data");
        assert!(features1.is_valid());
    }

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

    #[test]
    fn test_linear_regression_slope() {
        let points = vec![(0.0, 0.0), (1.0, 2.0), (2.0, 4.0), (3.0, 6.0)];
        let slope = linear_regression_slope(&points);
        assert!((slope - 2.0).abs() < 0.01, "Slope should be ~2.0");
    }

    #[test]
    fn test_ofi_stats_normalization() {
        let mut stats = OFIStats::new(10);

        // Add values with mean=100, std=10
        for i in 0..10 {
            stats.update(90.0 + i as f64 * 2.0);
        }

        let normalized = stats.normalize(100.0); // Mean value
        assert!(
            normalized.abs() < 0.5,
            "Mean value should normalize near zero"
        );

        let normalized_high = stats.normalize(130.0); // +3 std
        assert!(
            normalized_high > 2.0,
            "High value should have high z-score"
        );
    }
}
