//! Financial Calculation Precision Test Suite
//! 
//! Property-based testing for financial calculations, ML prediction consistency,
//! and risk metrics. Ensures mathematical invariants hold under all conditions.

use proptest::prelude::*;
use std::collections::HashMap;

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

    /// Property-based test for `price` arithmetic precision
    proptest! {
        #[test]
        fn test_price_arithmetic_invariants(
            price_a in 0.0001f64..10000.0f64,
            price_b in 0.0001f64..10000.0f64,
            quantity in 1i64..1_000_000i64
        ) {
            // Test addition commutative property
            let p1 = TestPrice::from_f64(price_a).expect("Valid price");
            let p2 = TestPrice::from_f64(price_b).expect("Valid price");
            
            prop_assert_eq!(p1.clone() + p2.clone(), p2.clone() + p1.clone(),
                          "Price addition must be commutative");
            
            // Test multiplication with quantity
            let total_value_1 = p1.clone() * TestQuantity::from_i64(quantity);
            let total_value_2 = TestQuantity::from_i64(quantity) * p1.clone();
            
            prop_assert!((total_value_1.to_f64() - total_value_2.to_f64()).abs() < 1e-10,
                        "Price-quantity multiplication must be commutative");
            
            // Test precision preservation
            let original_precision = count_decimal_places(price_a);
            let reconstructed = TestPrice::from_f64(price_a).expect("Valid price").to_f64();
            let precision_loss = (price_a - reconstructed).abs() / price_a;
            
            prop_assert!(precision_loss < 1e-8,
                        "Price precision loss {} exceeds tolerance for original {}", 
                        precision_loss, price_a);
            
            // Test zero properties
            let zero = TestPrice::zero();
            prop_assert_eq!(p1.clone() + zero.clone(), p1.clone(),
                          "Adding zero must be identity");
            prop_assert_eq!(p1.clone() - p1.clone(), zero,
                          "Self subtraction must equal zero");
        }
    }

    /// Property-based test for PnL calculation accuracy
    proptest! {
        #[test]
        fn test_pnl_calculation_invariants(
            entry_price in 1.0f64..2.0f64,
            exit_price in 1.0f64..2.0f64,
            quantity in 1i64..1_000_000i64,
            is_long in prop::bool::ANY
        ) {
            let pnl_calculator = TestPnLCalculator::new();
            
            let entry = TestPrice::from_f64(entry_price).expect("Valid price");
            let exit = TestPrice::from_f64(exit_price).expect("Valid price");
            let qty = if is_long { 
                TestQuantity::from_i64(quantity) 
            } else { 
                TestQuantity::from_i64(-quantity) 
            };
            
            let pnl = pnl_calculator.calculate_unrealized_pnl(entry.clone(), exit.clone(), qty.clone());
            
            // Test PnL symmetry property
            let opposite_qty = TestQuantity::from_i64(-qty.to_i64());
            let opposite_pnl = pnl_calculator.calculate_unrealized_pnl(entry.clone(), exit.clone(), opposite_qty);
            
            prop_assert!((pnl.to_f64() + opposite_pnl.to_f64()).abs() < 1e-10,
                        "Opposite positions should have opposite PnL");
            
            // Test price reversal property
            let reversed_pnl = pnl_calculator.calculate_unrealized_pnl(exit.clone(), entry.clone(), qty.clone());
            prop_assert!((pnl.to_f64() + reversed_pnl.to_f64()).abs() < 1e-10,
                        "Reversing entry/exit prices should reverse PnL sign");
            
            // Test zero quantity property
            let zero_qty = TestQuantity::from_i64(0);
            let zero_pnl = pnl_calculator.calculate_unrealized_pnl(entry.clone(), exit.clone(), zero_qty);
            prop_assert_eq!(zero_pnl.to_f64(), 0.0,
                          "Zero quantity should result in zero PnL");
            
            // Test linearity property
            let double_qty = TestQuantity::from_i64(qty.to_i64() * 2);
            let double_pnl = pnl_calculator.calculate_unrealized_pnl(entry.clone(), exit.clone(), double_qty);
            
            prop_assert!((double_pnl.to_f64() - 2.0 * pnl.to_f64()).abs() < 1e-8,
                        "PnL should scale linearly with quantity");
        }
    }

    /// Property-based test for risk metrics consistency
    proptest! {
        #[test]
        fn test_risk_metrics_invariants(
            returns in prop::collection::vec(-0.1f64..0.1f64, 100..1000),
            confidence_level in 0.90f64..0.99f64,
            time_horizon in 1u32..30u32
        ) {
            let risk_calculator = TestRiskCalculator::new();
            
            // Calculate Value at Risk
            let var = risk_calculator.calculate_var(&returns, confidence_level, time_horizon);
            
            // VaR should always be negative (loss)
            prop_assert!(var <= 0.0, "VaR should represent a loss (non-positive value)");
            
            // Test VaR monotonicity with confidence level
            if confidence_level < 0.98 {
                let higher_confidence_var = risk_calculator.calculate_var(&returns, confidence_level + 0.01, time_horizon);
                prop_assert!(higher_confidence_var <= var,
                           "Higher confidence level should result in higher (more negative) VaR");
            }
            
            // Test time scaling property
            if time_horizon < 20 {
                let longer_horizon_var = risk_calculator.calculate_var(&returns, confidence_level, time_horizon * 2);
                let scaling_factor = (2.0f64).sqrt(); // Square root of time scaling
                let expected_var = var * scaling_factor;
                
                let scaling_error = ((longer_horizon_var / expected_var) - 1.0).abs();
                prop_assert!(scaling_error < 0.2, // Allow 20% deviation due to estimation methods
                           "VaR should approximately scale with square root of time");
            }
            
            // Calculate Expected Shortfall
            let es = risk_calculator.calculate_expected_shortfall(&returns, confidence_level);
            
            // Expected Shortfall should be more extreme than VaR
            prop_assert!(es <= var,
                        "Expected Shortfall should be greater than or equal to VaR in magnitude");
            
            // Test coherent risk measure properties
            let scaled_returns: Vec<f64> = returns.iter().map(|&r| r * 2.0).collect();
            let scaled_var = risk_calculator.calculate_var(&scaled_returns, confidence_level, time_horizon);
            
            prop_assert!((scaled_var / (var * 2.0) - 1.0).abs() < 0.1,
                        "VaR should approximately scale linearly with position size");
        }
    }

    /// Property-based test for portfolio allocation invariants  
    proptest! {
        #[test]
        fn test_portfolio_allocation_invariants(
            weights in prop::collection::vec(0.0f64..1.0f64, 3..10),
            returns in prop::collection::vec(-0.05f64..0.05f64, 3..10),
            volatilities in prop::collection::vec(0.001f64..0.5f64, 3..10)
        ) {
            prop_assume!(weights.len() == returns.len() && returns.len() == volatilities.len());
            prop_assume!(weights.iter().sum::<f64>() > 0.1); // Ensure meaningful weights
            
            let portfolio_optimizer = TestPortfolioOptimizer::new();
            
            // Normalize weights to sum to 1
            let weight_sum: f64 = weights.iter().sum();
            let normalized_weights: Vec<f64> = weights.iter().map(|&w| w / weight_sum).collect();
            
            let portfolio_return = portfolio_optimizer.calculate_portfolio_return(&normalized_weights, &returns);
            let portfolio_risk = portfolio_optimizer.calculate_portfolio_risk(&normalized_weights, &volatilities);
            
            // Test weight normalization property
            let weight_sum_normalized: f64 = normalized_weights.iter().sum();
            prop_assert!((weight_sum_normalized - 1.0).abs() < 1e-10,
                        "Normalized weights must sum to 1.0");
            
            // Test portfolio return linearity
            let manual_return: f64 = normalized_weights.iter()
                .zip(returns.iter())
                .map(|(&w, &r)| w * r)
                .sum();
            
            prop_assert!((portfolio_return - manual_return).abs() < 1e-10,
                        "Portfolio return calculation must match weighted average");
            
            // Test risk bounds
            let min_individual_risk = volatilities.iter().cloned().fold(f64::INFINITY, f64::min);
            let max_individual_risk = volatilities.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
            
            prop_assert!(portfolio_risk >= 0.0,
                        "Portfolio risk must be non-negative");
            prop_assert!(portfolio_risk <= max_individual_risk,
                        "Portfolio risk should not exceed maximum individual asset risk");
            
            // Test concentration risk
            let max_weight = normalized_weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
            if max_weight > 0.8 {
                // High concentration should result in risk close to that asset's risk
                let dominant_asset_risk = volatilities[normalized_weights.iter()
                    .position(|&w| w == max_weight).unwrap()];
                let risk_difference = (portfolio_risk - dominant_asset_risk).abs();
                
                prop_assert!(risk_difference < dominant_asset_risk * 0.3,
                           "Concentrated portfolio risk should approximate dominant asset risk");
            }
        }
    }

    /// Property-based test for `ML` prediction consistency
    proptest! {
        #[test]
        fn test_ml_prediction_consistency(
            market_data in prop::collection::vec(0.5f64..2.0f64, 10..20),
            model_confidence in 0.0f64..1.0f64,
            prediction_horizon in 1u32..100u32
        ) {
            let ml_predictor = TestMLPredictor::new();
            
            let market_state = TestMarketState::from_prices(market_data.clone());
            let prediction = ml_predictor.predict(&market_state, prediction_horizon);
            
            // Test prediction bounds
            prop_assert!(prediction.probability >= 0.0 && prediction.probability <= 1.0,
                        "Prediction probability must be in [0,1]");
            prop_assert!(prediction.confidence >= 0.0 && prediction.confidence <= 1.0,
                        "Prediction confidence must be in [0,1]");
            
            // Test deterministic consistency
            let prediction2 = ml_predictor.predict(&market_state, prediction_horizon);
            prop_assert!((prediction.probability - prediction2.probability).abs() < 1e-10,
                        "Identical inputs should produce identical predictions");
            
            // Test input sensitivity
            let mut perturbed_data = market_data.clone();
            if let Some(last) = perturbed_data.last_mut() {
                *last *= 1.001; // 0.1% perturbation
            }
            
            let perturbed_state = TestMarketState::from_prices(perturbed_data);
            let perturbed_prediction = ml_predictor.predict(&perturbed_state, prediction_horizon);
            
            let prediction_sensitivity = (prediction.probability - perturbed_prediction.probability).abs();
            prop_assert!(prediction_sensitivity < 0.1,
                        "Small input changes should not cause large prediction changes");
            
            // Test horizon scaling
            if prediction_horizon < 50 {
                let longer_prediction = ml_predictor.predict(&market_state, prediction_horizon * 2);
                
                // Longer horizons should generally have lower confidence
                prop_assert!(longer_prediction.confidence <= prediction.confidence + 0.1,
                           "Longer prediction horizons should not increase confidence significantly");
            }
        }
    }

    /// Property-based test for position sizing algorithms
    proptest! {
        #[test]
        fn test_position_sizing_invariants(
            account_balance in 10000.0f64..1_000_000.0f64,
            win_probability in 0.51f64..0.80f64,
            win_amount in 1.0f64..10.0f64,
            loss_amount in 1.0f64..10.0f64,
            risk_tolerance in 0.01f64..0.10f64
        ) {
            let position_sizer = TestPositionSizer::new();
            
            // Kelly Criterion position size
            let kelly_fraction = position_sizer.calculate_kelly_fraction(
                win_probability, win_amount, loss_amount
            );
            
            // Kelly fraction should be positive for profitable opportunities
            prop_assert!(kelly_fraction >= 0.0,
                        "Kelly fraction should be non-negative for profitable trades");
            
            // Kelly fraction should not exceed 1 for reasonable parameters
            prop_assert!(kelly_fraction <= 1.0,
                        "Kelly fraction should not exceed 100% allocation");
            
            // Risk-adjusted position size
            let position_size = position_sizer.calculate_position_size(
                account_balance, kelly_fraction, risk_tolerance
            );
            
            // Position size should respect risk tolerance
            let max_loss = position_size * loss_amount;
            let portfolio_risk = max_loss / account_balance;
            
            prop_assert!(portfolio_risk <= risk_tolerance * 1.1, // Small tolerance for rounding
                        "Position size should respect risk tolerance");
            
            // Test scaling properties
            let double_balance_size = position_sizer.calculate_position_size(
                account_balance * 2.0, kelly_fraction, risk_tolerance
            );
            
            prop_assert!((double_balance_size / (position_size * 2.0) - 1.0).abs() < 0.01,
                        "Position size should scale approximately linearly with account balance");
            
            // Test edge cases
            if win_probability <= 0.5 {
                let unprofitable_kelly = position_sizer.calculate_kelly_fraction(
                    win_probability, win_amount, loss_amount
                );
                prop_assert!(unprofitable_kelly <= 0.0,
                           "Kelly fraction should be non-positive for unprofitable trades");
            }
        }
    }

    /// Property-based test for order book impact calculations
    proptest! {
        #[test]
        fn test_market_impact_invariants(
            order_size in 1000.0f64..100_000.0f64,
            daily_volume in 100_000.0f64..10_000_000.0f64,
            spread in 0.0001f64..0.01f64,
            volatility in 0.001f64..0.1f64
        ) {
            let impact_calculator = TestMarketImpactCalculator::new();
            
            let participation_rate = order_size / daily_volume;
            let impact = impact_calculator.calculate_linear_impact(
                order_size, daily_volume, spread, volatility
            );
            
            // Market impact should be non-negative
            prop_assert!(impact >= 0.0,
                        "Market impact should be non-negative");
            
            // Impact should increase with order size
            let larger_order_impact = impact_calculator.calculate_linear_impact(
                order_size * 2.0, daily_volume, spread, volatility
            );
            prop_assert!(larger_order_impact >= impact,
                        "Larger orders should have greater or equal market impact");
            
            // Impact should decrease with higher daily volume (more liquidity)
            let higher_volume_impact = impact_calculator.calculate_linear_impact(
                order_size, daily_volume * 2.0, spread, volatility
            );
            prop_assert!(higher_volume_impact <= impact,
                        "Higher daily volume should reduce market impact");
            
            // Impact should be roughly proportional to participation rate for small orders
            if participation_rate < 0.1 {
                let double_participation = impact_calculator.calculate_linear_impact(
                    order_size * 2.0, daily_volume, spread, volatility
                );
                let scaling_ratio = double_participation / impact;
                
                prop_assert!(scaling_ratio >= 1.8 && scaling_ratio <= 2.2,
                           "Market impact should scale approximately linearly for small participation rates");
            }
            
            // Impact should have reasonable bounds
            let impact_in_spreads = impact / spread;
            prop_assert!(impact_in_spreads < 10.0,
                        "Market impact should not exceed 10 spread widths for reasonable parameters");
        }
    }

    // Helper functions
    fn count_decimal_places(value: f64) -> usize {
        let s = format!("{:.10}", value);
        if let Some(dot_pos) = s.find('.') {
            s.len() - dot_pos - 1
        } else {
            0
        }
    }
}

// Test data structures and implementations
#[derive(Debug, Clone, PartialEq)]
struct TestPrice {
    value: i64, // Store as fixed-point integer for precision
    scale: u32, // Number of decimal places
}

impl TestPrice {
    fn from_f64(value: f64) -> Self {
        let scale = 8; // 8 decimal places
        let scaled_value = (value * 10_i64.pow(scale) as f64).round() as i64;
        Self {
            value: scaled_value,
            scale,
        }
    }
    
    fn to_f64(&self) -> f64 {
        self.value as f64 / 10_i64.pow(self.scale) as f64
    }
    
    fn zero() -> Self {
        Self { value: 0, scale: 8 }
    }
}

impl std::ops::Add for TestPrice {
    type Output = Self;
    
    fn add(self, other: Self) -> Self {
        assert_eq!(self.scale, other.scale);
        Self {
            value: self.value + other.value,
            scale: self.scale,
        }
    }
}

impl std::ops::Sub for TestPrice {
    type Output = Self;
    
    fn sub(self, other: Self) -> Self {
        assert_eq!(self.scale, other.scale);
        Self {
            value: self.value - other.value,
            scale: self.scale,
        }
    }
}

impl std::ops::Mul<TestQuantity> for TestPrice {
    type Output = TestPrice;
    
    fn mul(self, quantity: TestQuantity) -> TestPrice {
        TestPrice {
            value: self.value * quantity.value,
            scale: self.scale,
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
struct TestQuantity {
    value: i64,
}

impl TestQuantity {
    fn from_i64(value: i64) -> Self {
        Self { value }
    }
    
    fn to_i64(&self) -> i64 {
        self.value
    }
    
    fn to_f64(&self) -> f64 {
        self.value as f64
    }
}

impl std::ops::Mul<TestPrice> for TestQuantity {
    type Output = TestPrice;
    
    fn mul(self, price: TestPrice) -> TestPrice {
        price * self
    }
}

#[derive(Debug)]
struct TestPnLCalculator;

impl TestPnLCalculator {
    fn new() -> Self { Self }
    
    fn calculate_unrealized_pnl(&self, entry_price: TestPrice, current_price: TestPrice, quantity: TestQuantity) -> TestPrice {
        let price_diff = current_price - entry_price;
        price_diff * quantity
    }
}

#[derive(Debug)]
struct TestRiskCalculator;

impl TestRiskCalculator {
    fn new() -> Self { Self }
    
    fn calculate_var(&self, returns: &[f64], confidence_level: f64, _time_horizon: u32) -> f64 {
        let mut sorted_returns = returns.to_vec();
        sorted_returns.sort_by(|a, b| a.partial_cmp(b).unwrap());
        
        let percentile_index = ((1.0 - confidence_level) * sorted_returns.len() as f64) as usize;
        sorted_returns.get(percentile_index).copied().unwrap_or(0.0)
    }
    
    fn calculate_expected_shortfall(&self, returns: &[f64], confidence_level: f64) -> f64 {
        let var = self.calculate_var(returns, confidence_level, 1);
        
        let tail_returns: Vec<f64> = returns.iter()
            .filter(|&&r| r <= var)
            .copied()
            .collect();
            
        if tail_returns.is_empty() {
            var
        } else {
            tail_returns.iter().sum::<f64>() / tail_returns.len() as f64
        }
    }
}

#[derive(Debug)]
struct TestPortfolioOptimizer;

impl TestPortfolioOptimizer {
    fn new() -> Self { Self }
    
    fn calculate_portfolio_return(&self, weights: &[f64], returns: &[f64]) -> f64 {
        weights.iter().zip(returns.iter()).map(|(&w, &r)| w * r).sum()
    }
    
    fn calculate_portfolio_risk(&self, weights: &[f64], volatilities: &[f64]) -> f64 {
        // Simplified calculation assuming zero correlation for testing
        let variance: f64 = weights.iter()
            .zip(volatilities.iter())
            .map(|(&w, &v)| (w * v).powi(2))
            .sum();
        variance.sqrt()
    }
}

#[derive(Debug)]
struct TestMarketState {
    prices: Vec<f64>,
}

impl TestMarketState {
    fn from_prices(prices: Vec<f64>) -> Self {
        Self { prices }
    }
}

#[derive(Debug)]
struct TestMLPredictor;

#[derive(Debug)]
struct MLPrediction {
    probability: f64,
    confidence: f64,
}

impl TestMLPredictor {
    fn new() -> Self { Self }
    
    fn predict(&self, market_state: &TestMarketState, _horizon: u32) -> MLPrediction {
        // Simplified deterministic prediction for testing
        let last_price = market_state.prices.last().unwrap_or(&1.0);
        let price_hash = (last_price * 1000000.0) as u64;
        
        MLPrediction {
            probability: ((price_hash % 1000) as f64) / 1000.0,
            confidence: 0.75, // Fixed confidence for deterministic testing
        }
    }
}

#[derive(Debug)]
struct TestPositionSizer;

impl TestPositionSizer {
    fn new() -> Self { Self }
    
    fn calculate_kelly_fraction(&self, win_prob: f64, win_amount: f64, loss_amount: f64) -> f64 {
        let expected_return = win_prob * win_amount - (1.0 - win_prob) * loss_amount;
        if expected_return <= 0.0 {
            0.0
        } else {
            expected_return / win_amount
        }
    }
    
    fn calculate_position_size(&self, balance: f64, kelly_fraction: f64, risk_tolerance: f64) -> f64 {
        let kelly_size = balance * kelly_fraction;
        let risk_adjusted_size = balance * risk_tolerance;
        kelly_size.min(risk_adjusted_size)
    }
}

#[derive(Debug)]
struct TestMarketImpactCalculator;

impl TestMarketImpactCalculator {
    fn new() -> Self { Self }
    
    fn calculate_linear_impact(&self, order_size: f64, daily_volume: f64, spread: f64, volatility: f64) -> f64 {
        let participation_rate = order_size / daily_volume;
        let base_impact = spread * 0.5; // Half spread as base impact
        let volume_impact = volatility * participation_rate;
        
        base_impact + volume_impact
    }
}