//! Portfolio Allocation Module
//!
//! Implements portfolio allocation strategies for capital distribution across assets.
//! Supports multiple allocation methodologies including:
//! - Equal Weight: Simple 1/N allocation
//! - Risk Parity: Inverse volatility weighting
//! - Mean-Variance: Markowitz optimization
//! - ML-Optimized: ML-based allocation using predictions
//! - Kelly Criterion: Optimal bet sizing

use common::error::{CommonError, ErrorCategory};
use serde::{Deserialize, Serialize};
use sqlx::{types::Uuid, PgPool};
use std::collections::HashMap;

/// Allocation strategies for portfolio construction
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AllocationStrategy {
    /// Equal weight allocation (1/N)
    EqualWeight,
    /// Risk parity (inverse volatility weighting)
    RiskParity,
    /// Mean-variance optimization (Markowitz)
    MeanVariance,
    /// ML-optimized allocation
    MLOptimized,
    /// Kelly Criterion optimal sizing
    Kelly,
}

/// Portfolio allocation result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortfolioAllocation {
    /// Unique allocation ID
    pub allocation_id: String,
    /// Asset weights (symbol -> weight 0.0-1.0)
    pub assets: HashMap<String, f64>,
    /// Total capital allocated
    pub total_capital: f64,
    /// Strategy used for allocation
    pub strategy: AllocationStrategy,
    /// Risk budget (maximum portfolio volatility)
    pub risk_budget: f64,
    /// Portfolio risk metrics
    pub risk_metrics: RiskMetrics,
}

/// Risk metrics for portfolio
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiskMetrics {
    /// Portfolio volatility (annualized standard deviation)
    pub volatility: f64,
    /// Value at Risk (95% confidence)
    pub var_95: f64,
    /// Portfolio beta (market sensitivity)
    pub beta: f64,
    /// Expected Sharpe ratio
    pub sharpe_ratio: f64,
    /// Maximum drawdown
    pub max_drawdown: f64,
}

/// Allocation constraints
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllocationConstraints {
    /// Maximum position size (e.g., 0.25 = 25%)
    pub max_position_size: f64,
    /// Minimum position size (e.g., 0.05 = 5%, 0.0 = allow zero)
    pub min_position_size: f64,
    /// Maximum sector/industry concentration
    pub max_sector_concentration: Option<f64>,
    /// Maximum leverage allowed (1.0 = no leverage)
    pub max_leverage: f64,
    /// Minimum diversification (number of assets)
    pub min_diversification: usize,
}

impl Default for AllocationConstraints {
    fn default() -> Self {
        Self {
            max_position_size: 0.25,              // 25% max per asset
            min_position_size: 0.05,              // 5% min per asset
            max_sector_concentration: Some(0.40), // 40% max per sector
            max_leverage: 1.0,                    // No leverage
            min_diversification: 4,               // At least 4 assets
        }
    }
}

/// Allocation request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllocationRequest {
    /// Assets to allocate across
    pub assets: Vec<String>,
    /// Total capital to allocate
    pub total_capital: f64,
    /// Allocation strategy
    pub strategy: AllocationStrategy,
    /// Risk budget (max portfolio volatility)
    pub risk_budget: f64,
    /// Allocation constraints
    pub constraints: AllocationConstraints,
    /// Expected returns (required for MeanVariance)
    pub expected_returns: Option<HashMap<String, f64>>,
    /// Win rates (required for Kelly)
    pub win_rates: Option<HashMap<String, f64>>,
}

/// Portfolio allocator
pub struct PortfolioAllocator {
    pool: PgPool,
}

impl PortfolioAllocator {
    /// Create new portfolio allocator
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }

    /// Allocate portfolio across assets
    pub async fn allocate_portfolio(
        &self,
        request: AllocationRequest,
    ) -> Result<PortfolioAllocation, CommonError> {
        // Validate request
        self.validate_request(&request)?;

        // Compute allocation weights based on strategy
        let weights = match request.strategy {
            AllocationStrategy::EqualWeight => self.equal_weight_allocation(&request.assets),
            AllocationStrategy::RiskParity => self.risk_parity_allocation(&request.assets).await?,
            AllocationStrategy::MeanVariance => {
                let returns = request.expected_returns.ok_or_else(|| {
                    CommonError::validation("Expected returns required for MeanVariance strategy")
                })?;
                self.mean_variance_allocation(&request.assets, &returns)
                    .await?
            },
            AllocationStrategy::MLOptimized => {
                self.ml_optimized_allocation(&request.assets).await?
            },
            AllocationStrategy::Kelly => {
                let win_rates = request.win_rates.ok_or_else(|| {
                    CommonError::validation("Win rates required for Kelly strategy")
                })?;
                let returns = request.expected_returns.ok_or_else(|| {
                    CommonError::validation("Expected returns required for Kelly strategy")
                })?;
                self.kelly_allocation(&request.assets, &win_rates, &returns)?
            },
        };

        // Apply constraints
        let constrained_weights = self.apply_constraints(weights, &request.constraints)?;

        // Calculate risk metrics
        let risk_metrics = self
            .calculate_risk_metrics(&request.assets, &constrained_weights)
            .await?;

        // Verify risk budget
        if risk_metrics.volatility > request.risk_budget {
            return Err(CommonError::validation(format!(
                "Portfolio volatility {:.2}% exceeds risk budget {:.2}%",
                risk_metrics.volatility * 100.0,
                request.risk_budget * 100.0
            )));
        }

        let allocation = PortfolioAllocation {
            allocation_id: Uuid::new_v4().to_string(),
            assets: constrained_weights,
            total_capital: request.total_capital,
            strategy: request.strategy,
            risk_budget: request.risk_budget,
            risk_metrics,
        };

        // Persist allocation
        self.persist_allocation(&allocation).await?;

        Ok(allocation)
    }

    /// Get existing allocation by ID
    pub async fn get_allocation(
        &self,
        allocation_id: &str,
    ) -> Result<PortfolioAllocation, CommonError> {
        // Parse allocation_id string to UUID
        let uuid = Uuid::parse_str(allocation_id)
            .map_err(|e| CommonError::validation(format!("Invalid allocation ID format: {}", e)))?;

        let record = sqlx::query!(
            r#"
            SELECT allocation_data
            FROM portfolio_allocations
            WHERE allocation_id = $1
            "#,
            uuid
        )
        .fetch_optional(&self.pool)
        .await
        .map_err(|e| CommonError::service(ErrorCategory::Database, format!("Query failed: {}", e)))?
        .ok_or_else(|| {
            CommonError::validation(format!("Allocation {} not found", allocation_id))
        })?;

        let allocation: PortfolioAllocation = serde_json::from_value(record.allocation_data)
            .map_err(|e| {
                CommonError::serialization(format!("Failed to deserialize allocation: {}", e))
            })?;

        Ok(allocation)
    }

    /// Rebalance portfolio
    pub async fn rebalance_portfolio(
        &self,
        allocation_id: &str,
    ) -> Result<PortfolioAllocation, CommonError> {
        let current = self.get_allocation(allocation_id).await?;

        // Create rebalance request with same parameters
        let request = AllocationRequest {
            assets: current.assets.keys().cloned().collect(),
            total_capital: current.total_capital,
            strategy: current.strategy,
            risk_budget: current.risk_budget,
            constraints: AllocationConstraints::default(),
            expected_returns: None,
            win_rates: None,
        };

        self.allocate_portfolio(request).await
    }

    /// Equal weight allocation (1/N)
    fn equal_weight_allocation(&self, assets: &[String]) -> HashMap<String, f64> {
        let weight = 1.0 / assets.len() as f64;
        assets
            .iter()
            .map(|symbol| (symbol.clone(), weight))
            .collect()
    }

    /// Risk parity allocation (inverse volatility weighting)
    async fn risk_parity_allocation(
        &self,
        assets: &[String],
    ) -> Result<HashMap<String, f64>, CommonError> {
        // Get historical volatilities
        let volatilities = self.get_asset_volatilities(assets).await?;

        // Inverse volatility weights
        let mut weights = HashMap::new();
        let mut total_inv_vol = 0.0;

        for symbol in assets {
            let vol = volatilities.get(symbol).ok_or_else(|| {
                CommonError::validation(format!("No volatility data for {}", symbol))
            })?;

            if *vol <= 0.0 {
                return Err(CommonError::validation(format!(
                    "Invalid volatility {} for {}",
                    vol, symbol
                )));
            }

            let inv_vol = 1.0 / vol;
            weights.insert(symbol.clone(), inv_vol);
            total_inv_vol += inv_vol;
        }

        // Normalize to sum to 1.0
        for weight in weights.values_mut() {
            *weight /= total_inv_vol;
        }

        Ok(weights)
    }

    /// Mean-variance optimization (Markowitz)
    async fn mean_variance_allocation(
        &self,
        assets: &[String],
        expected_returns: &HashMap<String, f64>,
    ) -> Result<HashMap<String, f64>, CommonError> {
        // Get covariance matrix
        let _cov_matrix = self.get_covariance_matrix(assets).await?;

        // Simplified Markowitz: maximize Sharpe ratio
        // In production, would use quadratic programming solver

        // For now, use risk-adjusted return weighting
        let volatilities = self.get_asset_volatilities(assets).await?;
        let mut weights = HashMap::new();
        let mut total_score = 0.0;

        for symbol in assets {
            let ret = expected_returns.get(symbol).ok_or_else(|| {
                CommonError::validation(format!("No expected return for {}", symbol))
            })?;
            let vol = volatilities
                .get(symbol)
                .ok_or_else(|| CommonError::validation(format!("No volatility for {}", symbol)))?;

            // Sharpe ratio proxy (assuming risk-free rate = 0)
            let score = if *vol > 0.0 { ret / vol } else { 0.0 };

            if score > 0.0 {
                weights.insert(symbol.clone(), score);
                total_score += score;
            }
        }

        // Normalize
        if total_score > 0.0 {
            for weight in weights.values_mut() {
                *weight /= total_score;
            }
        } else {
            // Fallback to equal weight
            return Ok(self.equal_weight_allocation(assets));
        }

        Ok(weights)
    }

    /// ML-optimized allocation
    async fn ml_optimized_allocation(
        &self,
        assets: &[String],
    ) -> Result<HashMap<String, f64>, CommonError> {
        // Get ML predictions for each asset
        let predictions = self.get_ml_predictions(assets).await?;

        // Get covariance matrix
        let _cov_matrix = self.get_covariance_matrix(assets).await?;

        // Weight by prediction confidence and inverse correlation
        let mut weights = HashMap::new();
        let mut total_score = 0.0;

        for symbol in assets {
            let pred = predictions.get(symbol).ok_or_else(|| {
                CommonError::validation(format!("No ML prediction for {}", symbol))
            })?;

            // Score based on prediction and diversification
            let score = pred.abs();

            if score > 0.0 {
                weights.insert(symbol.clone(), score);
                total_score += score;
            }
        }

        // Normalize
        if total_score > 0.0 {
            for weight in weights.values_mut() {
                *weight /= total_score;
            }
        } else {
            return Ok(self.equal_weight_allocation(assets));
        }

        Ok(weights)
    }

    /// Kelly Criterion allocation
    fn kelly_allocation(
        &self,
        assets: &[String],
        win_rates: &HashMap<String, f64>,
        expected_returns: &HashMap<String, f64>,
    ) -> Result<HashMap<String, f64>, CommonError> {
        let mut weights = HashMap::new();
        let mut total_kelly = 0.0;

        for symbol in assets {
            let win_rate = win_rates
                .get(symbol)
                .ok_or_else(|| CommonError::validation(format!("No win rate for {}", symbol)))?;
            let expected_return = expected_returns.get(symbol).ok_or_else(|| {
                CommonError::validation(format!("No expected return for {}", symbol))
            })?;

            // Kelly formula: f* = (p*b - q) / b
            // where p = win probability, q = 1-p, b = odds (return ratio)
            let p = win_rate;
            let q = 1.0 - p;
            let b = expected_return.abs();

            if b > 0.0 {
                let kelly = (p * b - q) / b;
                // Use fractional Kelly (25%) for safety
                let fractional_kelly = (kelly * 0.25).max(0.0);

                if fractional_kelly > 0.0 {
                    weights.insert(symbol.clone(), fractional_kelly);
                    total_kelly += fractional_kelly;
                }
            }
        }

        // Normalize to sum to 1.0
        if total_kelly > 0.0 {
            for weight in weights.values_mut() {
                *weight /= total_kelly;
            }
        } else {
            // No positive Kelly fractions - equal weight
            return Ok(self.equal_weight_allocation(assets));
        }

        Ok(weights)
    }

    /// Apply allocation constraints
    fn apply_constraints(
        &self,
        mut weights: HashMap<String, f64>,
        constraints: &AllocationConstraints,
    ) -> Result<HashMap<String, f64>, CommonError> {
        // Apply position size limits
        let mut total_weight = 0.0;
        let mut removed = Vec::new();

        for (symbol, weight) in &mut weights {
            if *weight < constraints.min_position_size {
                removed.push(symbol.clone());
                continue;
            }

            if *weight > constraints.max_position_size {
                *weight = constraints.max_position_size;
            }

            total_weight += *weight;
        }

        // Remove positions below minimum
        for symbol in removed {
            weights.remove(&symbol);
        }

        // Check minimum diversification
        if weights.len() < constraints.min_diversification {
            return Err(CommonError::validation(format!(
                "Insufficient diversification: {} assets (min: {})",
                weights.len(),
                constraints.min_diversification
            )));
        }

        // Check leverage BEFORE normalization
        let leverage: f64 = weights.values().sum();
        if leverage > constraints.max_leverage {
            return Err(CommonError::validation(format!(
                "Leverage {:.2} exceeds maximum {:.2}",
                leverage, constraints.max_leverage
            )));
        }

        // Renormalize to sum to 1.0
        if total_weight > 0.0 {
            for weight in weights.values_mut() {
                *weight /= total_weight;
            }
        }

        // Re-apply caps after normalization to ensure constraints are still met
        // Use iterative algorithm: cap overweight positions, redistribute to uncapped ones
        // Stop when either converged or no uncapped positions remain
        const MAX_ITERATIONS: usize = 100;
        for iteration in 0..MAX_ITERATIONS {
            let mut capped_total = 0.0;
            let mut uncapped_total = 0.0;
            let mut capped_symbols = Vec::new();
            let mut uncapped_symbols = Vec::new();

            // Identify capped and uncapped positions
            for (symbol, weight) in &weights {
                if *weight > constraints.max_position_size + 1e-10 {
                    capped_symbols.push(symbol.clone());
                    capped_total += constraints.max_position_size;
                } else {
                    uncapped_symbols.push(symbol.clone());
                    uncapped_total += *weight;
                }
            }

            // If nothing is over the cap, we're done
            if capped_symbols.is_empty() {
                break;
            }

            // Cap the overweight positions
            for symbol in &capped_symbols {
                weights.insert(symbol.clone(), constraints.max_position_size);
            }

            // Redistribute remaining allocation across uncapped positions
            let remaining = 1.0 - capped_total;

            // If no uncapped positions or all positions are at cap, we can't redistribute
            // This happens when max_position_size * num_assets < 1.0
            if uncapped_symbols.is_empty() || remaining <= 0.0 || uncapped_total <= 0.0 {
                break;
            }

            let scale = remaining / uncapped_total;

            // Only redistribute if it won't cause new violations
            // If scale would push any uncapped position over the limit, stop iterating
            let max_uncapped_after_scale = uncapped_symbols.iter()
                .map(|s| weights[s] * scale)
                .fold(0.0f64, |a, b| a.max(b));

            if max_uncapped_after_scale > constraints.max_position_size + 1e-10 {
                // Would cause oscillation - stop here
                // All positions get their proportional share of remaining space
                for symbol in &uncapped_symbols {
                    if let Some(weight) = weights.get_mut(symbol) {
                        *weight = (*weight / uncapped_total) * remaining;
                    }
                }
                break;
            }

            for symbol in &uncapped_symbols {
                if let Some(weight) = weights.get_mut(symbol) {
                    *weight *= scale;
                }
            }

            // Safety: break if we've reached the last iteration
            if iteration == MAX_ITERATIONS - 1 {
                break;
            }
        }

        Ok(weights)
    }

    /// Calculate portfolio risk metrics
    async fn calculate_risk_metrics(
        &self,
        assets: &[String],
        weights: &HashMap<String, f64>,
    ) -> Result<RiskMetrics, CommonError> {
        // Get historical data
        let _volatilities = self.get_asset_volatilities(assets).await?;
        let cov_matrix = self.get_covariance_matrix(assets).await?;

        // Portfolio volatility: sqrt(w' * Σ * w)
        let mut portfolio_variance = 0.0;
        for (i, symbol_i) in assets.iter().enumerate() {
            for (j, symbol_j) in assets.iter().enumerate() {
                let w_i = weights.get(symbol_i).unwrap_or(&0.0);
                let w_j = weights.get(symbol_j).unwrap_or(&0.0);
                let cov = cov_matrix[i][j];
                portfolio_variance += w_i * w_j * cov;
            }
        }
        let volatility = portfolio_variance.sqrt();

        // VaR (95% confidence): -1.645 * volatility * sqrt(capital)
        let var_95 = 1.645 * volatility;

        // Portfolio beta (simplified: weighted average)
        let beta = weights.values().sum::<f64>() / weights.len() as f64;

        // Expected Sharpe ratio (simplified)
        let sharpe_ratio = if volatility > 0.0 {
            1.0 / volatility
        } else {
            0.0
        };

        // Max drawdown (estimated from volatility)
        let max_drawdown = volatility * 2.0;

        Ok(RiskMetrics {
            volatility,
            var_95,
            beta,
            sharpe_ratio,
            max_drawdown,
        })
    }

    /// Persist allocation to database
    async fn persist_allocation(
        &self,
        allocation: &PortfolioAllocation,
    ) -> Result<(), CommonError> {
        let allocation_json = serde_json::to_value(allocation)
            .map_err(|e| CommonError::serialization(format!("Failed to serialize: {}", e)))?;

        sqlx::query!(
            r#"
            INSERT INTO portfolio_allocations (allocation_id, allocation_data, created_at)
            VALUES ($1, $2, NOW())
            ON CONFLICT (allocation_id) DO UPDATE
            SET allocation_data = $2, updated_at = NOW()
            "#,
            Uuid::parse_str(&allocation.allocation_id).map_err(|e| CommonError::validation(
                format!("Invalid allocation_id UUID: {}", e)
            ))?,
            allocation_json
        )
        .execute(&self.pool)
        .await
        .map_err(|e| {
            CommonError::service(ErrorCategory::Database, format!("Insert failed: {}", e))
        })?;

        Ok(())
    }

    /// Validate allocation request
    fn validate_request(&self, request: &AllocationRequest) -> Result<(), CommonError> {
        if request.assets.is_empty() {
            return Err(CommonError::validation("Assets list cannot be empty"));
        }

        if request.total_capital <= 0.0 {
            return Err(CommonError::validation("Total capital must be positive"));
        }

        if request.risk_budget <= 0.0 || request.risk_budget > 1.0 {
            return Err(CommonError::validation(
                "Risk budget must be between 0 and 1",
            ));
        }

        if request.constraints.max_position_size <= 0.0
            || request.constraints.max_position_size > 1.0
        {
            return Err(CommonError::validation(
                "Max position size must be between 0 and 1",
            ));
        }

        if request.constraints.min_position_size < 0.0
            || request.constraints.min_position_size > 1.0
        {
            return Err(CommonError::validation(
                "Min position size must be between 0 and 1",
            ));
        }

        if request.constraints.max_leverage <= 0.0 {
            return Err(CommonError::validation("Max leverage must be positive"));
        }

        Ok(())
    }

    /// Get asset volatilities from database
    async fn get_asset_volatilities(
        &self,
        assets: &[String],
    ) -> Result<HashMap<String, f64>, CommonError> {
        tracing::warn!(
            "get_asset_volatilities: using default values (not yet wired to market data)"
        );
        let mut volatilities = HashMap::new();
        for symbol in assets.iter() {
            // 20% annual volatility default — replace with historical calculation
            volatilities.insert(symbol.clone(), 0.20);
        }
        Ok(volatilities)
    }

    /// Get covariance matrix for assets
    async fn get_covariance_matrix(&self, assets: &[String]) -> Result<Vec<Vec<f64>>, CommonError> {
        tracing::warn!(
            "get_covariance_matrix: using default values (not yet wired to market data)"
        );
        let n = assets.len();
        let mut matrix = vec![vec![0.0; n]; n];

        for i in 0..n {
            for j in 0..n {
                if i == j {
                    // Variance on diagonal: 0.20^2 = 0.04
                    matrix[i][j] = 0.04;
                } else {
                    // Low correlation off-diagonal
                    matrix[i][j] = 0.01;
                }
            }
        }

        Ok(matrix)
    }

    /// Get ML predictions for assets
    async fn get_ml_predictions(
        &self,
        assets: &[String],
    ) -> Result<HashMap<String, f64>, CommonError> {
        tracing::warn!(
            "get_ml_predictions: using default values (not yet wired to market data)"
        );
        let mut predictions = HashMap::new();
        for symbol in assets.iter() {
            // Neutral prediction — replace with actual ML inference
            predictions.insert(symbol.clone(), 0.0);
        }
        Ok(predictions)
    }
}

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

    fn create_test_request(strategy: AllocationStrategy) -> AllocationRequest {
        let mut expected_returns = HashMap::new();
        expected_returns.insert("AAPL".to_string(), 0.12);
        expected_returns.insert("GOOGL".to_string(), 0.15);
        expected_returns.insert("MSFT".to_string(), 0.10);
        expected_returns.insert("AMZN".to_string(), 0.18);

        let mut win_rates = HashMap::new();
        win_rates.insert("AAPL".to_string(), 0.55);
        win_rates.insert("GOOGL".to_string(), 0.60);
        win_rates.insert("MSFT".to_string(), 0.52);
        win_rates.insert("AMZN".to_string(), 0.58);

        AllocationRequest {
            assets: vec![
                "AAPL".to_string(),
                "GOOGL".to_string(),
                "MSFT".to_string(),
                "AMZN".to_string(),
            ],
            total_capital: 100000.0,
            strategy,
            risk_budget: 0.20,
            constraints: AllocationConstraints::default(),
            expected_returns: Some(expected_returns),
            win_rates: Some(win_rates),
        }
    }

    #[tokio::test]
    async fn test_equal_weight_allocation() {
        let pool = PgPool::connect_lazy("postgresql://test").unwrap();
        let allocator = PortfolioAllocator::new(pool);

        let assets = vec![
            "AAPL".to_string(),
            "GOOGL".to_string(),
            "MSFT".to_string(),
            "AMZN".to_string(),
        ];
        let weights = allocator.equal_weight_allocation(&assets);

        assert_eq!(weights.len(), 4);
        for weight in weights.values() {
            assert!((weight - 0.25).abs() < 1e-10);
        }

        let total: f64 = weights.values().sum();
        assert!((total - 1.0).abs() < 1e-10);
    }

    #[tokio::test]
    async fn test_kelly_allocation() {
        let pool = PgPool::connect_lazy("postgresql://test").unwrap();
        let allocator = PortfolioAllocator::new(pool);

        let assets = vec!["AAPL".to_string(), "GOOGL".to_string()];

        let mut win_rates = HashMap::new();
        win_rates.insert("AAPL".to_string(), 0.60);
        win_rates.insert("GOOGL".to_string(), 0.55);

        // Updated: Use higher expected returns to create positive edge for Kelly formula
        // Kelly requires p*b > q (win_rate * return > loss_rate)
        // AAPL: 0.60 * 0.80 = 0.48 > 0.40 (positive edge, kelly = 0.10 → fractional = 0.025)
        // GOOGL: 0.55 * 0.85 = 0.4675 > 0.45 (positive edge, kelly = 0.0206 → fractional = 0.00515)
        let mut expected_returns = HashMap::new();
        expected_returns.insert("AAPL".to_string(), 0.80);
        expected_returns.insert("GOOGL".to_string(), 0.85);

        let weights = allocator
            .kelly_allocation(&assets, &win_rates, &expected_returns)
            .unwrap();

        assert_eq!(weights.len(), 2);

        let total: f64 = weights.values().sum();
        assert!((total - 1.0).abs() < 1e-10);

        // AAPL should have higher weight (better win rate and return)
        assert!(weights["AAPL"] > weights["GOOGL"]);
    }

    #[tokio::test]
    async fn test_apply_constraints() {
        let pool = PgPool::connect_lazy("postgresql://test").unwrap();
        let allocator = PortfolioAllocator::new(pool);

        let mut weights = HashMap::new();
        weights.insert("AAPL".to_string(), 0.60); // Exceeds max
        weights.insert("GOOGL".to_string(), 0.30);
        weights.insert("MSFT".to_string(), 0.03); // Below min
        weights.insert("AMZN".to_string(), 0.07);

        let constraints = AllocationConstraints {
            max_position_size: 0.25,
            min_position_size: 0.05,
            max_sector_concentration: None,
            max_leverage: 1.0,
            min_diversification: 3,
        };

        let constrained = allocator.apply_constraints(weights, &constraints).unwrap();

        // MSFT should be removed (below min)
        assert!(!constrained.contains_key("MSFT"));

        // AAPL should be capped at max
        assert!(constrained["AAPL"] <= constraints.max_position_size);

        // Should sum to 1.0
        let total: f64 = constrained.values().sum();
        assert!((total - 1.0).abs() < 1e-10);

        // All remaining positions above min
        for weight in constrained.values() {
            assert!(*weight >= constraints.min_position_size);
        }
    }

    #[tokio::test]
    async fn test_validate_request() {
        let pool = PgPool::connect_lazy("postgresql://test").unwrap();
        let allocator = PortfolioAllocator::new(pool);

        // Valid request
        let request = create_test_request(AllocationStrategy::EqualWeight);
        assert!(allocator.validate_request(&request).is_ok());

        // Empty assets
        let mut bad_request = request.clone();
        bad_request.assets.clear();
        assert!(allocator.validate_request(&bad_request).is_err());

        // Negative capital
        let mut bad_request = request.clone();
        bad_request.total_capital = -1000.0;
        assert!(allocator.validate_request(&bad_request).is_err());

        // Invalid risk budget
        let mut bad_request = request.clone();
        bad_request.risk_budget = 1.5;
        assert!(allocator.validate_request(&bad_request).is_err());

        // Invalid max position size
        let mut bad_request = request.clone();
        bad_request.constraints.max_position_size = 1.5;
        assert!(allocator.validate_request(&bad_request).is_err());
    }

    #[tokio::test]
    async fn test_constraint_enforcement() {
        let pool = PgPool::connect_lazy("postgresql://test").unwrap();
        let allocator = PortfolioAllocator::new(pool);

        // Test min diversification constraint
        let mut weights = HashMap::new();
        weights.insert("AAPL".to_string(), 0.50);
        weights.insert("GOOGL".to_string(), 0.50);

        let constraints = AllocationConstraints {
            max_position_size: 0.50,
            min_position_size: 0.0,
            max_sector_concentration: None,
            max_leverage: 1.0,
            min_diversification: 4, // Require at least 4 assets
        };

        let result = allocator.apply_constraints(weights, &constraints);
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("Insufficient diversification"));
    }

    #[tokio::test]
    async fn test_leverage_constraint() {
        let pool = PgPool::connect_lazy("postgresql://test").unwrap();
        let allocator = PortfolioAllocator::new(pool);

        let mut weights = HashMap::new();
        weights.insert("AAPL".to_string(), 0.50);
        weights.insert("GOOGL".to_string(), 0.40);
        weights.insert("MSFT".to_string(), 0.30);
        weights.insert("AMZN".to_string(), 0.20);

        let constraints = AllocationConstraints {
            max_position_size: 0.50,
            min_position_size: 0.05,
            max_sector_concentration: None,
            max_leverage: 1.0, // No leverage allowed
            min_diversification: 2,
        };

        let result = allocator.apply_constraints(weights, &constraints);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Leverage"));
    }
}
