//! Stress testing engine for portfolio risk analysis
// #![deny(clippy::unwrap_used, clippy::expect_used)] // COMMENTED: Crate-level allows applied

use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;

use chrono::Utc;
use num::ToPrimitive;
// REMOVED: Direct Decimal usage - use canonical types
use tokio::sync::RwLock;

use crate::error::{RiskError, RiskResult};
use crate::risk_types::{StressScenario, StressTestResult};
use common::{Position, Price};
use config::{AssetClassMapping, RiskConfig, StressScenarioConfig};
use rust_decimal::Decimal;
// CANONICAL TYPE IMPORTS - All types from core

/// Stress testing engine for portfolio risk analysis
#[derive(Debug)]
pub struct StressTester {
    scenarios: Arc<RwLock<HashMap<String, StressScenario>>>,
    risk_config: Arc<RwLock<RiskConfig>>,
    asset_mapping: Arc<RwLock<AssetClassMapping>>,
}

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

impl StressTester {
    /// Create a new stress tester with configurable scenarios
    ///
    /// Initializes a stress tester with scenarios loaded from configuration.
    /// Uses the provided `RiskConfig` to load stress scenarios, or creates
    /// default scenarios if none provided.
    ///
    /// # Arguments
    ///
    /// * `risk_config` - Optional risk configuration containing stress scenarios
    ///
    /// # Returns
    ///
    /// Returns a new `StressTester` instance with configured scenarios loaded.
    #[must_use]
    pub fn new() -> Self {
        Self::with_config(None)
    }

    /// Create a new stress tester with specific risk configuration
    ///
    /// # Arguments
    ///
    /// * `risk_config` - Optional risk configuration containing stress scenarios
    #[must_use]
    pub fn with_config(risk_config: Option<RiskConfig>) -> Self {
        let config = risk_config.unwrap_or_default();
        let asset_mapping = config.asset_class_mapping.clone();

        // Convert configured scenarios to runtime scenarios
        let mut scenarios = HashMap::new();
        for scenario_config in &config.stress_scenarios {
            if scenario_config.is_active {
                scenarios.insert(
                    scenario_config.id.clone(),
                    convert_config_to_scenario(scenario_config, &asset_mapping),
                );
            }
        }

        Self {
            scenarios: Arc::new(RwLock::new(scenarios)),
            risk_config: Arc::new(RwLock::new(config)),
            asset_mapping: Arc::new(RwLock::new(asset_mapping)),
        }
    }

    /// Run a stress test on a portfolio using a predefined scenario
    ///
    /// Applies the specified stress scenario to the given portfolio positions
    /// and calculates the potential profit/loss and risk metrics under stress
    /// conditions. This is essential for understanding portfolio resilience
    /// during market crises.
    ///
    /// # Arguments
    ///
    /// * `portfolio_id` - Unique identifier for the portfolio being tested
    /// * `scenario_id` - ID of the stress scenario to apply (e.g., "`market_crash_2008`")
    /// * `positions` - Array of current portfolio positions to stress test
    ///
    /// # Returns
    ///
    /// Returns a `StressTestResult` containing detailed analysis including:
    /// - Total profit/loss under stress
    /// - Position-level impacts
    /// - Risk metrics and statistics
    /// - Execution time and metadata
    ///
    /// # Errors
    ///
    /// Returns a `RiskError` if:
    /// - Scenario ID is not found
    /// - Position data is invalid
    /// - Calculation fails due to insufficient data
    pub async fn run_stress_test(
        &self,
        portfolio_id: &str,
        scenario_id: &str,
        positions: &[Position],
    ) -> RiskResult<StressTestResult> {
        let start_time = Instant::now();

        let scenarios = self.scenarios.read().await;
        let scenario = scenarios
            .get(scenario_id)
            .ok_or_else(|| RiskError::Validation {
                field: "scenario_id".to_owned(),
                message: format!("Scenario not found: {scenario_id}"),
            })?;

        // Calculate pre-stress portfolio value
        let pre_stress_results: Result<Vec<Price>, RiskError> = positions
            .iter()
            .map(|p| {
                Decimal::try_from(ToPrimitive::to_f64(&p.market_value).unwrap_or(0.0))
                    .map(Into::into)
                    .map_err(|_| RiskError::Calculation {
                        operation: "pre_stress_portfolio_value".to_owned(),
                        reason: format!(
                            "Failed to convert market value for instrument {}",
                            p.symbol
                        ),
                    })
            })
            .collect();
        let pre_stress_value: Price = pre_stress_results?
            .into_iter()
            .map(|p| {
                p.to_decimal().map_err(|_| RiskError::Calculation {
                    operation: "pre_stress_value_conversion".to_owned(),
                    reason: "Failed to convert pre-stress value to decimal".to_owned(),
                })
            })
            .collect::<Result<Vec<_>, _>>()?
            .into_iter()
            .sum::<Decimal>()
            .into();

        // Apply stress shocks using configurable approach
        let mut post_stress_value = Price::ZERO;
        let mut max_loss_instrument: Option<String> = None;
        let mut max_loss = Price::ZERO;

        for position in positions {
            let stressed_value = if let Some(shock) =
                scenario.market_shocks.get(&position.symbol.to_string())
            {
                let original_value: Price =
                    Decimal::try_from(ToPrimitive::to_f64(&position.market_value).unwrap_or(0.0))
                        .map_err(|_| RiskError::Calculation {
                            operation: "stress_test_original_value".to_owned(),
                            reason: format!(
                                "Failed to convert original market value for instrument {}",
                                position.symbol
                            ),
                        })?
                        .into();
                // Calculate shock multiplier using Decimals to handle negative shocks
                let shock_decimal =
                    Decimal::try_from(*shock / 100.0).map_err(|_| RiskError::Calculation {
                        operation: "shock_conversion".to_owned(),
                        reason: format!("Failed to convert shock value: {shock}"),
                    })?;
                let original_decimal =
                    original_value
                        .to_decimal()
                        .map_err(|_| RiskError::Calculation {
                            operation: "original_value_conversion".to_owned(),
                            reason: "Failed to convert original value to decimal".to_owned(),
                        })?;
                // Apply shock: new_value = original_value * (1 + shock_decimal)
                let new_value_decimal = original_decimal * (Decimal::ONE + shock_decimal);
                let new_value = Price::from_decimal(new_value_decimal.abs()); // Use abs to ensure positive
                let loss = (original_value - new_value).abs();

                if loss > max_loss {
                    max_loss = loss;
                    max_loss_instrument = Some(position.symbol.to_string());
                }

                new_value
            } else {
                let value: Price =
                    Decimal::try_from(ToPrimitive::to_f64(&position.market_value).unwrap_or(0.0))
                        .map_err(|_| RiskError::Calculation {
                            operation: "stress_test_fallback_value".to_owned(),
                            reason: format!(
                                "Failed to convert market value for non-shocked instrument {}",
                                position.symbol
                            ),
                        })?
                        .into();
                value
            };

            let post_stress_decimal =
                post_stress_value
                    .to_decimal()
                    .map_err(|_| RiskError::Calculation {
                        operation: "post_stress_value_conversion".to_owned(),
                        reason: "Failed to convert post-stress value to decimal".to_owned(),
                    })?;
            let stressed_decimal =
                stressed_value
                    .to_decimal()
                    .map_err(|_| RiskError::Calculation {
                        operation: "stressed_value_conversion".to_owned(),
                        reason: "Failed to convert stressed value to decimal".to_owned(),
                    })?;
            post_stress_value = (post_stress_decimal + stressed_decimal).into();
        }

        // Calculate stress PnL using Decimals to handle negative values
        let pre_stress_decimal =
            pre_stress_value
                .to_decimal()
                .map_err(|_| RiskError::Calculation {
                    operation: "pre_stress_value_conversion".to_owned(),
                    reason: "Failed to convert pre stress value to decimal".to_owned(),
                })?;
        let post_stress_decimal =
            post_stress_value
                .to_decimal()
                .map_err(|_| RiskError::Calculation {
                    operation: "post_stress_value_conversion".to_owned(),
                    reason: "Failed to convert post stress value to decimal".to_owned(),
                })?;

        let stress_pnl_decimal = post_stress_decimal - pre_stress_decimal;

        // Convert to Price using abs value (Price cannot be negative)
        let stress_pnl = Price::from_decimal(stress_pnl_decimal.abs());

        let stress_pnl_percentage = if pre_stress_value == Price::ZERO {
            Price::ZERO
        } else {
            let ratio = stress_pnl_decimal / pre_stress_decimal;
            (ratio * Decimal::from(100)).abs().into()
        };

        let execution_time_ms = start_time.elapsed().as_millis() as u64;

        Ok(StressTestResult {
            scenario: scenario.clone(),
            scenario_id: scenario_id.to_owned(),
            portfolio_id: portfolio_id.to_owned(),
            pre_stress_value,
            post_stress_value,
            stressed_portfolio_value: post_stress_value,
            stressed_pnl: stress_pnl,
            stress_pnl: Price::from_decimal(stress_pnl.to_decimal().map_err(|_| {
                RiskError::Calculation {
                    operation: "stress_pnl_final_conversion".to_owned(),
                    reason: "Failed to convert final stress PnL to decimal".to_owned(),
                }
            })?),
            stress_pnl_percentage: stress_pnl_percentage.raw_value() as f64,
            var_breach: false,
            limit_breaches: Vec::new(),
            liquidity_shortfall: Price::ZERO,
            max_loss_instrument,
            max_loss,
            execution_time_ms,
            timestamp: Utc::now(),
            max_drawdown: Price::ZERO,
            risk_metrics: HashMap::new(),
        })
    }

    /// Get all available stress test scenarios
    ///
    /// Returns a vector of all predefined and custom stress scenarios
    /// currently available for stress testing. This includes both the
    /// built-in historical scenarios and any custom scenarios that have
    /// been added.
    ///
    /// # Returns
    ///
    /// A vector containing all available `StressScenario` instances.
    pub async fn get_scenarios(&self) -> Vec<StressScenario> {
        let scenarios = self.scenarios.read().await;
        scenarios.values().cloned().collect()
    }

    /// Add a custom stress test scenario
    ///
    /// Adds a new stress scenario to the available scenarios. This allows
    /// for testing custom market conditions or hypothetical scenarios
    /// beyond the predefined historical events.
    ///
    /// # Arguments
    ///
    /// * `scenario` - The stress scenario to add to the collection
    pub async fn add_scenario(&self, scenario: StressScenario) {
        let mut scenarios = self.scenarios.write().await;
        scenarios.insert(scenario.id.clone(), scenario);
    }

    /// Remove a stress test scenario by ID
    ///
    /// Removes a stress scenario from the available scenarios collection.
    /// Note that predefined scenarios can be removed, but it's recommended
    /// to only remove custom scenarios to maintain standard stress testing
    /// capabilities.
    ///
    /// # Arguments
    ///
    /// * `scenario_id` - The ID of the scenario to remove
    ///
    /// # Returns
    ///
    /// Returns `true` if the scenario was found and removed, `false` otherwise.
    pub async fn remove_scenario(&self, scenario_id: &str) -> bool {
        let mut scenarios = self.scenarios.write().await;
        scenarios.remove(scenario_id).is_some()
    }

    pub async fn run_comprehensive_stress_test(
        &self,
        portfolio_id: &str,
        positions: &[Position],
    ) -> RiskResult<Vec<StressTestResult>> {
        let scenarios = self.get_scenarios().await;
        let mut results = Vec::new();

        for scenario in scenarios {
            let result = self
                .run_stress_test(portfolio_id, &scenario.id, positions)
                .await?;
            results.push(result);
        }

        Ok(results)
    }

    /// Update the risk configuration and reload scenarios
    ///
    /// This method allows for hot-reloading of stress scenarios from updated
    /// configuration without requiring a restart of the stress testing engine.
    ///
    /// # Arguments
    ///
    /// * `new_config` - Updated risk configuration containing new scenarios
    pub async fn update_config(&self, new_config: RiskConfig) {
        let asset_mapping = new_config.asset_class_mapping.clone();

        // Update the configuration
        {
            let mut config = self.risk_config.write().await;
            *config = new_config;
        }

        // Update asset mapping
        {
            let mut mapping = self.asset_mapping.write().await;
            *mapping = asset_mapping.clone();
        }

        // Reload scenarios from new configuration
        {
            let config = self.risk_config.read().await;
            let mut scenarios = self.scenarios.write().await;
            scenarios.clear();

            for scenario_config in &config.stress_scenarios {
                if scenario_config.is_active {
                    scenarios.insert(
                        scenario_config.id.clone(),
                        convert_config_to_scenario(scenario_config, &asset_mapping),
                    );
                }
            }
        }
    }

    /// Get the current risk configuration
    pub async fn get_config(&self) -> RiskConfig {
        self.risk_config.read().await.clone()
    }

    /// Get the current asset class mapping
    pub async fn get_asset_mapping(&self) -> AssetClassMapping {
        self.asset_mapping.read().await.clone()
    }
}

/// Convert a configuration-based stress scenario to a runtime stress scenario
///
/// This function bridges the gap between the configuration system and the runtime
/// stress testing engine by converting configurable scenarios into the format
/// expected by the stress testing logic.
fn convert_config_to_scenario(
    config: &StressScenarioConfig,
    asset_mapping: &AssetClassMapping,
) -> StressScenario {
    let mut market_shocks = HashMap::new();

    // Add instrument-specific shocks
    for (symbol, shock) in &config.instrument_shocks {
        market_shocks.insert(symbol.clone(), *shock / 100.0); // Convert percentage to decimal
    }

    // Add asset class-based shocks for all mapped symbols
    for (symbol, asset_class) in &asset_mapping.mappings {
        if let Some(shock) = config.asset_class_shocks.get(asset_class) {
            // Only add if no instrument-specific shock exists
            if !market_shocks.contains_key(symbol) {
                market_shocks.insert(symbol.clone(), *shock / 100.0); // Convert percentage to decimal
            }
        }
    }

    // Convert volatility multipliers from asset class to instrument level
    let mut volatility_multipliers = HashMap::new();
    for (symbol, asset_class) in &asset_mapping.mappings {
        if let Some(multiplier) = config.volatility_multipliers.get(asset_class) {
            volatility_multipliers.insert(symbol.clone(), *multiplier);
        }
    }

    // Convert liquidity haircuts from asset class to instrument level
    let mut liquidity_haircuts = HashMap::new();
    for (symbol, asset_class) in &asset_mapping.mappings {
        if let Some(haircut) = config.liquidity_haircuts.get(asset_class) {
            liquidity_haircuts.insert(symbol.clone(), *haircut);
        }
    }

    StressScenario {
        id: config.id.clone(),
        name: config.name.clone(),
        price_shocks: market_shocks.clone(), // Alias for backward compatibility
        market_shocks,
        volatility_multiplier: config.volatility_multiplier,
        volatility_multipliers,
        correlation_changes: HashMap::new(), // Could be extended later
        correlation_adjustments: config.correlation_adjustments.clone(),
        liquidity_haircuts,
    }
}

#[cfg(test)]
#[allow(clippy::str_to_string, clippy::use_debug)]
mod tests {
    use super::*;
    use config::RiskAssetClass;
    use num::FromPrimitive;
    // operations module removed - use direct imports from common
    // Types already imported via prelude at top of file

    fn create_test_positions() -> Result<Vec<Position>, Box<dyn std::error::Error>> {
        let now = Utc::now();
        Ok(vec![
            Position {
                id: uuid::Uuid::new_v4(),
                symbol: "AAPL".to_string(),
                quantity: FromPrimitive::from_f64(100.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 100.0 to decimal".to_owned())
                })?,
                avg_price: FromPrimitive::from_f64(150.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 150.0 to decimal".to_owned())
                })?,
                avg_cost: FromPrimitive::from_f64(150.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 150.0 to decimal".to_owned())
                })?,
                basis: FromPrimitive::from_f64(15000.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 15000.0 to decimal".to_owned())
                })?,
                average_price: FromPrimitive::from_f64(150.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 150.0 to decimal".to_owned())
                })?,
                market_value: FromPrimitive::from_f64(15000.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 15000.0 to decimal".to_owned())
                })?,
                unrealized_pnl: Decimal::ZERO,
                realized_pnl: Decimal::ZERO,
                created_at: now,
                updated_at: now,
                last_updated: now,
                current_price: None,
                notional_value: FromPrimitive::from_f64(15000.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 15000.0 to decimal".to_owned())
                })?,
                margin_requirement: Decimal::ZERO,
            },
            Position {
                id: uuid::Uuid::new_v4(),
                symbol: "GOOGL".to_string(),
                quantity: FromPrimitive::from_f64(50.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 50.0 to decimal".to_owned())
                })?,
                avg_price: FromPrimitive::from_f64(2500.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 2500.0 to decimal".to_owned())
                })?,
                avg_cost: FromPrimitive::from_f64(2500.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 2500.0 to decimal".to_owned())
                })?,
                basis: FromPrimitive::from_f64(125000.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 125000.0 to decimal".to_owned())
                })?,
                average_price: FromPrimitive::from_f64(2500.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 2500.0 to decimal".to_owned())
                })?,
                market_value: FromPrimitive::from_f64(125000.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 125000.0 to decimal".to_owned())
                })?,
                unrealized_pnl: Decimal::ZERO,
                realized_pnl: Decimal::ZERO,
                created_at: now,
                updated_at: now,
                last_updated: now,
                current_price: None,
                notional_value: FromPrimitive::from_f64(125000.0).ok_or_else(|| {
                    RiskError::CalculationError("Failed to convert 125000.0 to decimal".to_owned())
                })?,
                margin_requirement: Decimal::ZERO,
            },
        ])
    }

    fn create_test_scenario_config() -> StressScenarioConfig {
        let mut asset_class_shocks = HashMap::new();
        asset_class_shocks.insert(RiskAssetClass::Technology, -10.0); // -10%
        asset_class_shocks.insert(RiskAssetClass::LargeCapEquity, -15.0); // -15%

        StressScenarioConfig {
            id: "test_scenario".to_string(),
            name: "Test Scenario".to_string(),
            description: "Test scenario for unit testing".to_string(),
            instrument_shocks: HashMap::new(),
            asset_class_shocks,
            volatility_multiplier: 1.0,
            volatility_multipliers: HashMap::new(),
            correlation_adjustments: HashMap::new(),
            liquidity_haircuts: HashMap::new(),
            is_active: true,
        }
    }

    fn create_test_risk_config() -> RiskConfig {
        RiskConfig {
            stress_scenarios: vec![create_test_scenario_config()],
            asset_class_mapping: create_test_asset_mapping(),
            default_volatility_multiplier: 1.0,
            max_portfolio_loss_pct: 20.0,
            var_confidence_level: 0.95,
            var_time_horizon_days: 1,
        }
    }

    fn create_test_asset_mapping() -> AssetClassMapping {
        let mut mappings = HashMap::new();
        mappings.insert("AAPL".to_string(), RiskAssetClass::Technology);
        mappings.insert("GOOGL".to_string(), RiskAssetClass::Technology);
        mappings.insert("SPY".to_string(), RiskAssetClass::LargeCapEquity);

        AssetClassMapping {
            mappings,
            default_class: RiskAssetClass::LargeCapEquity,
        }
    }

    #[tokio::test]
    async fn test_stress_scenario_application() -> Result<(), Box<dyn std::error::Error>> {
        let _tester = StressTester::new();
        // Test passes if no panic
        Ok(())
    }

    #[tokio::test]
    async fn test_add_remove_scenario() -> Result<(), Box<dyn std::error::Error>> {
        let risk_config = create_test_risk_config();
        let tester = StressTester::with_config(Some(risk_config));

        // Test scenario should be loaded from config
        let scenarios = tester.get_scenarios().await;
        assert!(scenarios.iter().any(|s| s.id == "test_scenario"));

        // Test removing scenario
        let removed = tester.remove_scenario("test_scenario").await;
        assert!(removed);

        let scenarios = tester.get_scenarios().await;
        assert!(!scenarios.iter().any(|s| s.id == "test_scenario"));
        Ok(())
    }

    #[tokio::test]
    async fn test_stress_test_execution() -> Result<(), Box<dyn std::error::Error>> {
        let risk_config = create_test_risk_config();
        let tester = StressTester::with_config(Some(risk_config));
        let positions = create_test_positions()?;

        let result = tester
            .run_stress_test("test_portfolio", "test_scenario", &positions)
            .await;

        if let Err(e) = &result {
            eprintln!("Stress test error: {:?}", e);
        }
        assert!(result.is_ok());

        let result = result?;
        assert_eq!(result.portfolio_id, "test_portfolio");
        assert_eq!(result.scenario_id, "test_scenario");
        assert!(result.stress_pnl > Price::ZERO); // Should show loss magnitude due to price drops
                                                  // execution_time_ms is always >= 0 (u64), so we just verify it exists
        let _ = result.execution_time_ms; // Acknowledge the field exists
        Ok(())
    }
    #[tokio::test]
    async fn test_predefined_scenarios() -> Result<(), Box<dyn std::error::Error>> {
        let tester = StressTester::new(); // Uses default config with predefined scenarios
        let scenarios = tester.get_scenarios().await;

        // Should have default scenarios from configuration
        assert!(scenarios.iter().any(|s| s.id == "market_crash_2008"));
        assert!(scenarios.iter().any(|s| s.id == "covid_crash_2020"));
        assert!(scenarios.iter().any(|s| s.id == "flash_crash_2010"));
        assert!(scenarios.iter().any(|s| s.id == "volatility_spike"));
        assert!(scenarios.iter().any(|s| s.id == "interest_rate_shock"));
        Ok(())
    }

    #[tokio::test]
    async fn test_comprehensive_stress_test() -> Result<(), Box<dyn std::error::Error>> {
        let tester = StressTester::new();
        let positions = create_test_positions()?;

        let results = tester
            .run_comprehensive_stress_test("test_portfolio", &positions)
            .await?;

        // Should have results for multiple scenarios (default config has 5 scenarios)
        assert!(!results.is_empty());
        assert!(results.len() >= 5);

        // All results should be for the same portfolio
        for result in &results {
            assert_eq!(result.portfolio_id, "test_portfolio");
        }
        Ok(())
    }

    #[tokio::test]
    async fn test_config_update() -> Result<(), Box<dyn std::error::Error>> {
        let initial_config = create_test_risk_config();
        let tester = StressTester::with_config(Some(initial_config));

        // Initially should have test scenario
        let scenarios = tester.get_scenarios().await;
        assert!(scenarios.iter().any(|s| s.id == "test_scenario"));

        // Update config with default scenarios
        let new_config = RiskConfig::default();
        tester.update_config(new_config).await;

        // Should now have default scenarios instead
        let scenarios = tester.get_scenarios().await;
        assert!(!scenarios.iter().any(|s| s.id == "test_scenario"));
        assert!(scenarios.iter().any(|s| s.id == "market_crash_2008"));

        Ok(())
    }
}
