//! Wave D End-to-End Integration Test
//!
//! Comprehensive integration test validating the complete Wave D trading flow:
//! 1. Load test bars into prices table
//! 2. Call allocate_portfolio (triggers regime detection)
//! 3. Verify regime detection populated database
//! 4. Verify allocations returned with regime-adaptive sizing
//! 5. Generate orders with dynamic stop-loss
//! 6. Verify orders have regime-adaptive stop-loss attached
//! 7. Verify performance targets (<5s end-to-end)
//!
//! AGENT VALIDATION 6/8: Wave D End-to-End Trading Flow

use anyhow::Result;
use serial_test::serial;
use rust_decimal::prelude::*;
use rust_decimal::Decimal;
use sqlx::PgPool;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::Mutex;
use tonic::Request;

use trading_agent_service::dynamic_stop_loss::apply_dynamic_stop_loss;
use trading_agent_service::orders::OrderGenerator;
use trading_agent_service::proto::trading_agent::trading_agent_service_server::TradingAgentService;
use trading_agent_service::proto::trading_agent::*;
use trading_agent_service::service::TradingAgentServiceImpl;

use common::{OrderSide, OrderType};

// ============================================================================
// Test Setup Helpers
// ============================================================================

/// Setup test database with migrations
async fn setup_test_db() -> PgPool {
    let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
        "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string()
    });

    let pool = PgPool::connect(&database_url)
        .await
        .expect("Failed to connect to database");

    // Ensure migrations have been applied
    sqlx::migrate!("../../migrations")
        .run(&pool)
        .await
        .expect("Failed to run migrations");

    pool
}

/// Clean up test data
async fn cleanup_test_data(pool: &PgPool, symbols: &[&str]) -> Result<()> {
    // Clean regime states
    sqlx::query("DELETE FROM regime_states WHERE symbol = ANY($1)")
        .bind(symbols)
        .execute(pool)
        .await?;

    // Clean market data
    sqlx::query("DELETE FROM prices WHERE symbol = ANY($1)")
        .bind(symbols)
        .execute(pool)
        .await?;

    Ok(())
}

/// Load test bars into prices table
async fn load_test_bars(pool: &PgPool, symbol: &str, num_bars: usize) -> Result<()> {
    // Generate realistic OHLCV data
    let base_price = match symbol {
        "ES.FUT" => 4000.0,
        "NQ.FUT" => 20000.0,
        "6E.FUT" => 1.10,
        _ => 100.0,
    };

    let atr = match symbol {
        "ES.FUT" => 60.0,   // $60 ATR (1.5% of price)
        "NQ.FUT" => 300.0,  // $300 ATR (1.5% of price)
        "6E.FUT" => 0.015,  // $0.015 ATR (1.36% of price)
        _ => base_price * 0.015,
    };

    let mut price = base_price;
    let now = chrono::Utc::now();

    for i in 0..num_bars {
        // Add some randomness to create realistic bars
        let high = price + atr * 0.6;
        let low = price - atr * 0.4;
        let close = price + (rand::random::<f64>() - 0.5) * atr * 0.3;
        let open = price;
        let volume = 10000.0 + rand::random::<f64>() * 5000.0;

        // Convert to fixed-point BIGINT (cents)
        let open_cents = (open * 100.0) as i64;
        let high_cents = (high * 100.0) as i64;
        let low_cents = (low * 100.0) as i64;
        let close_cents = (close * 100.0) as i64;
        let volume_i64 = volume as i64;

        // Sequential timestamps (1 minute apart)
        let timestamp = now - chrono::Duration::minutes((num_bars - i) as i64);

        sqlx::query(
            r#"
            INSERT INTO prices (symbol, timestamp, open, high, low, close, volume)
            VALUES ($1, $2, $3, $4, $5, $6, $7)
            ON CONFLICT (symbol, timestamp) DO NOTHING
            "#,
        )
        .bind(symbol)
        .bind(timestamp)
        .bind(open_cents)
        .bind(high_cents)
        .bind(low_cents)
        .bind(close_cents)
        .bind(volume_i64)
        .execute(pool)
        .await?;

        price = close;
    }

    Ok(())
}

/// Create test asset score for allocation request
fn create_asset_score(symbol: &str, composite_score: f64) -> AssetScore {
    AssetScore {
        symbol: symbol.to_string(),
        ml_score: 0.75,
        momentum_score: 0.65,
        value_score: 0.55,
        quality_score: 0.70,
        composite_score,
        model_scores: Default::default(),
    }
}

// ============================================================================
// END-TO-END TEST: Wave D Trading Flow
// ============================================================================

#[tokio::test]
#[serial]
async fn test_wave_d_end_to_end_trading_flow() {
    // 1. Setup
    println!("=== Wave D End-to-End Trading Flow Test ===");
    let pool = setup_test_db().await;
    let symbols = vec!["ES.FUT", "NQ.FUT", "6E.FUT"];

    cleanup_test_data(&pool, &symbols).await.unwrap();

    let test_start = Instant::now();

    // 2. Load test bars into prices table (100 bars per symbol)
    println!("\n[1/7] Loading test market data...");
    for symbol in &symbols {
        load_test_bars(&pool, symbol, 100).await.unwrap();
        println!("  ✓ Loaded 100 bars for {}", symbol);
    }

    // Verify bars were loaded
    for symbol in &symbols {
        let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM prices WHERE symbol = $1")
            .bind(symbol)
            .fetch_one(&pool)
            .await
            .unwrap();
        assert!(count >= 100, "Should have loaded at least 100 bars for {}", symbol);
    }

    // 3. Create Trading Agent Service
    println!("\n[2/7] Initializing Trading Agent Service...");
    let regime_orchestrator = Arc::new(Mutex::new(
        ml::regime::orchestrator::RegimeOrchestrator::new(pool.clone())
            .await
            .expect("Failed to create regime orchestrator"),
    ));

    let service = TradingAgentServiceImpl::new(pool.clone(), regime_orchestrator);
    println!("  ✓ Service initialized");

    // 4. Call allocate_portfolio
    println!("\n[3/7] Calling allocate_portfolio...");
    let allocation_start = Instant::now();

    let request = AllocatePortfolioRequest {
        assets: vec![
            create_asset_score("ES.FUT", 0.75),
            create_asset_score("NQ.FUT", 0.68),
            create_asset_score("6E.FUT", 0.62),
        ],
        strategy: Some(AllocationStrategy {
            allocation_type: AllocationType::Kelly as i32,
            parameters: Default::default(),
        }),
        risk_constraints: Some(RiskConstraints {
            max_position_size_pct: 0.20,
            max_sector_exposure_pct: 0.50,
            max_volatility: 0.25,
            max_var_95: 0.05,
            max_leverage: 2.0,
        }),
        total_capital: 100000.0,
    };

    let response = service
        .allocate_portfolio(Request::new(request))
        .await
        .expect("allocate_portfolio should succeed");

    let allocation_duration = allocation_start.elapsed();
    let allocations = response.into_inner().allocations;

    println!("  ✓ Allocation completed in {}ms", allocation_duration.as_millis());
    println!("  ✓ Received {} allocations", allocations.len());

    // Verify allocation performance (<5s target)
    assert!(
        allocation_duration.as_secs() < 5,
        "Allocation took {}ms (target: <5000ms)",
        allocation_duration.as_millis()
    );

    // 5. Verify regime detection populated database
    println!("\n[4/7] Verifying regime detection...");
    for symbol in &symbols {
        let regime_count: i64 =
            sqlx::query_scalar("SELECT COUNT(*) FROM regime_states WHERE symbol = $1")
                .bind(symbol)
                .fetch_one(&pool)
                .await
                .unwrap();

        assert!(
            regime_count > 0,
            "Regime detection should have populated database for {}",
            symbol
        );

        // Fetch and display regime
        let regime: (String, f64) = sqlx::query_as(
            "SELECT regime, confidence FROM regime_states WHERE symbol = $1 ORDER BY event_timestamp DESC LIMIT 1"
        )
        .bind(symbol)
        .fetch_one(&pool)
        .await
        .unwrap();

        println!("  ✓ {}: {} (confidence: {:.2})", symbol, regime.0, regime.1);
    }

    // 6. Verify allocations returned with regime-adaptive sizing
    println!("\n[5/7] Verifying regime-adaptive allocations...");
    assert!(!allocations.is_empty(), "Should return allocations");

    let mut total_weight = 0.0;
    for allocation in &allocations {
        assert!(
            allocation.target_weight > 0.0,
            "Allocation weight should be positive for {}",
            allocation.symbol
        );
        assert!(
            allocation.target_weight <= 0.20,
            "Allocation weight should not exceed 20% for {}",
            allocation.symbol
        );
        assert!(
            allocation.target_capital > 0.0,
            "Allocated capital should be positive for {}",
            allocation.symbol
        );

        total_weight += allocation.target_weight;

        println!(
            "  ✓ {}: weight={:.2}%, capital=${:.2}",
            allocation.symbol,
            allocation.target_weight * 100.0,
            allocation.target_capital
        );
    }

    assert!(
        total_weight <= 1.0,
        "Total weight {} should not exceed 100%",
        total_weight
    );

    // 7. Generate orders with OrderGenerator
    println!("\n[6/7] Generating orders...");
    let order_gen_start = Instant::now();

    let order_generator = OrderGenerator::new(pool.clone(), 100.0, 1_000_000.0);

    // Create PortfolioAllocation from response
    let mut symbol_weights = std::collections::HashMap::new();
    for alloc in &allocations {
        symbol_weights.insert(alloc.symbol.clone(), alloc.target_weight);
    }

    let portfolio_allocation = trading_agent_service::orders::PortfolioAllocation {
        allocation_id: uuid::Uuid::new_v4().to_string(),
        strategy_id: "test_strategy".to_string(),
        symbol_weights,
        total_capital: Decimal::from_f64_retain(100000.0).unwrap(),
        max_position_size: 0.5, // 50% max position size
        created_at: chrono::Utc::now(),
        rebalance_threshold: 0.05,
    };

    // Generate orders (no current positions)
    let current_positions: Vec<common::Position> = vec![];

    let mut orders = order_generator
        .generate_orders(&portfolio_allocation, &current_positions)
        .await
        .expect("Order generation should succeed");

    let order_gen_duration = order_gen_start.elapsed();

    println!("  ✓ Generated {} orders in {}ms", orders.len(), order_gen_duration.as_millis());

    // Verify orders were generated
    assert!(!orders.is_empty(), "Should generate orders");

    // 8. Apply dynamic stop-loss to orders
    println!("\n[7/7] Applying dynamic stop-loss to orders...");
    let stop_loss_start = Instant::now();

    let mut orders_with_stops = Vec::new();
    for order in orders.iter_mut() {
        let symbol_str = order.symbol.to_string();
        let order_with_stop = apply_dynamic_stop_loss(order.clone(), &symbol_str, &pool)
            .await
            .expect("Stop-loss application should succeed");

        orders_with_stops.push(order_with_stop);
    }

    let stop_loss_duration = stop_loss_start.elapsed();

    println!(
        "  ✓ Applied stop-loss to {} orders in {}ms",
        orders_with_stops.len(),
        stop_loss_duration.as_millis()
    );

    // 9. Verify orders have dynamic stop-loss
    println!("\n[8/8] Verifying orders have dynamic stop-loss...");
    let mut stop_loss_count = 0;

    for order in &orders_with_stops {
        if order.stop_loss.is_some() {
            stop_loss_count += 1;

            let stop_price: Decimal = order.stop_loss.unwrap().into();
            let entry_price = match order.order_type {
                OrderType::Limit => {
                    let price: Decimal = order.price.unwrap().into();
                    price.to_f64().unwrap()
                }
                OrderType::Market => {
                    // Extract from metadata
                    order
                        .metadata
                        .get("estimated_price")
                        .and_then(|v| v.as_f64())
                        .unwrap_or(0.0)
                }
                _ => 0.0,
            };

            let stop_distance = match order.side {
                OrderSide::Buy => entry_price - stop_price.to_f64().unwrap(),
                OrderSide::Sell => stop_price.to_f64().unwrap() - entry_price,
            };

            let stop_pct = (stop_distance / entry_price) * 100.0;

            // Verify stop-loss is reasonable (>2% minimum)
            assert!(
                stop_pct >= 2.0,
                "Stop-loss should be at least 2% for {}, got {:.2}%",
                order.symbol,
                stop_pct
            );

            // Verify regime metadata exists
            assert!(
                order.metadata.get("regime").is_some(),
                "Order should have regime metadata"
            );
            assert!(
                order.metadata.get("stop_multiplier").is_some(),
                "Order should have stop multiplier metadata"
            );

            let regime = order.metadata.get("regime").and_then(|v| v.as_str()).unwrap();
            let multiplier = order
                .metadata
                .get("stop_multiplier")
                .and_then(|v| v.as_f64())
                .unwrap();

            println!(
                "  ✓ {}: {} order @ ${:.2}, stop @ ${:.2} ({:.2}% / {:.1}x {} regime)",
                order.symbol,
                if order.side == OrderSide::Buy { "BUY" } else { "SELL" },
                entry_price,
                stop_price.to_f64().unwrap(),
                stop_pct,
                multiplier,
                regime
            );
        }
    }

    // At least 50% of orders should have stop-loss (some may be rejected if <2%)
    assert!(
        stop_loss_count as f64 / orders_with_stops.len() as f64 >= 0.5,
        "At least 50% of orders should have stop-loss, got {}/{}",
        stop_loss_count,
        orders_with_stops.len()
    );

    // 10. Verify end-to-end performance
    let total_duration = test_start.elapsed();
    println!("\n=== Performance Summary ===");
    println!("  Allocation:       {}ms", allocation_duration.as_millis());
    println!("  Order Generation: {}ms", order_gen_duration.as_millis());
    println!("  Stop-Loss Apply:  {}ms", stop_loss_duration.as_millis());
    println!("  Total E2E:        {}ms", total_duration.as_millis());

    // Verify total performance (<5s target)
    assert!(
        total_duration.as_secs() < 5,
        "End-to-end flow took {}ms (target: <5000ms)",
        total_duration.as_millis()
    );

    // 11. Cleanup
    cleanup_test_data(&pool, &symbols).await.unwrap();

    println!("\n✓ Wave D End-to-End Trading Flow Test PASSED");
}

// ============================================================================
// ADDITIONAL E2E TESTS: Edge Cases
// ============================================================================

#[tokio::test]
#[serial]
async fn test_wave_d_e2e_with_crisis_regime() {
    println!("=== Wave D E2E Test: Crisis Regime ===");
    let pool = setup_test_db().await;
    let symbols = vec!["ES.FUT"];

    cleanup_test_data(&pool, &symbols).await.unwrap();

    // Load bars with high volatility to trigger Crisis regime
    // Use very high ATR (200 points = 5% of price)
    let base_price = 4000.0;
    let high_atr = 200.0; // Very high ATR to trigger Crisis

    let now = chrono::Utc::now();
    for i in 0..100 {
        let high = base_price + high_atr;
        let low = base_price - high_atr;
        let close = base_price + (rand::random::<f64>() - 0.5) * high_atr;

        let timestamp = now - chrono::Duration::minutes((100 - i) as i64);

        sqlx::query(
            r#"
            INSERT INTO prices (symbol, timestamp, open, high, low, close, volume)
            VALUES ($1, $2, $3, $4, $5, $6, $7)
            "#,
        )
        .bind("ES.FUT")
        .bind(timestamp)
        .bind((base_price * 100.0) as i64)
        .bind((high * 100.0) as i64)
        .bind((low * 100.0) as i64)
        .bind((close * 100.0) as i64)
        .bind(10000_i64)
        .execute(&pool)
        .await
        .unwrap();
    }

    // Manually insert Crisis regime to ensure it's detected
    sqlx::query(
        r#"
        INSERT INTO regime_states (symbol, event_timestamp, regime, confidence)
        VALUES ($1, NOW(), $2, $3)
        "#,
    )
    .bind("ES.FUT")
    .bind("Crisis")
    .bind(0.95)
    .execute(&pool)
    .await
    .unwrap();

    // Create service and allocate
    let regime_orchestrator = Arc::new(Mutex::new(
        ml::regime::orchestrator::RegimeOrchestrator::new(pool.clone())
            .await
            .unwrap(),
    ));
    let service = TradingAgentServiceImpl::new(pool.clone(), regime_orchestrator);

    let request = AllocatePortfolioRequest {
        assets: vec![create_asset_score("ES.FUT", 0.70)],
        strategy: Some(AllocationStrategy {
            allocation_type: AllocationType::Kelly as i32,
            parameters: Default::default(),
        }),
        risk_constraints: Some(RiskConstraints {
            max_position_size_pct: 0.20,
            max_sector_exposure_pct: 0.50,
            max_volatility: 0.25,
            max_var_95: 0.05,
            max_leverage: 2.0,
        }),
        total_capital: 100000.0,
    };

    let response = service.allocate_portfolio(Request::new(request)).await.unwrap();
    let allocations = response.into_inner().allocations;

    // In Crisis regime, position size should be severely reduced (0.2x multiplier)
    assert!(!allocations.is_empty());
    let es_alloc = allocations.iter().find(|a| a.symbol == "ES.FUT").unwrap();

    // With Crisis regime (0.2x), capital should be significantly reduced
    // Expected: <5% of total capital (0.2x position multiplier)
    let allocation_pct = (es_alloc.target_capital / 100000.0) * 100.0;
    assert!(
        allocation_pct < 5.0,
        "Crisis regime should severely limit allocation to <5%, got {:.2}%",
        allocation_pct
    );

    println!("  ✓ Crisis regime allocation: {:.2}% of capital", allocation_pct);

    cleanup_test_data(&pool, &symbols).await.unwrap();
}

#[tokio::test]
#[serial]
async fn test_wave_d_e2e_with_trending_regime() {
    println!("=== Wave D E2E Test: Trending Regime ===");
    let pool = setup_test_db().await;
    let symbols = vec!["NQ.FUT"];

    cleanup_test_data(&pool, &symbols).await.unwrap();

    // Load bars with strong trend to trigger Trending regime
    load_test_bars(&pool, "NQ.FUT", 100).await.unwrap();

    // Manually insert Trending regime
    sqlx::query(
        r#"
        INSERT INTO regime_states (symbol, event_timestamp, regime, confidence, adx, plus_di, minus_di)
        VALUES ($1, NOW(), $2, $3, $4, $5, $6)
        "#,
    )
    .bind("NQ.FUT")
    .bind("Trending")
    .bind(0.88)
    .bind(35.0) // High ADX indicates strong trend
    .bind(30.0)
    .bind(10.0)
    .execute(&pool)
    .await
    .unwrap();

    // Create service and allocate
    let regime_orchestrator = Arc::new(Mutex::new(
        ml::regime::orchestrator::RegimeOrchestrator::new(pool.clone())
            .await
            .unwrap(),
    ));
    let service = TradingAgentServiceImpl::new(pool.clone(), regime_orchestrator);

    let request = AllocatePortfolioRequest {
        assets: vec![create_asset_score("NQ.FUT", 0.80)],
        strategy: Some(AllocationStrategy {
            allocation_type: AllocationType::Kelly as i32,
            parameters: Default::default(),
        }),
        risk_constraints: Some(RiskConstraints {
            max_position_size_pct: 0.20,
            max_sector_exposure_pct: 0.50,
            max_volatility: 0.25,
            max_var_95: 0.05,
            max_leverage: 2.0,
        }),
        total_capital: 100000.0,
    };

    let response = service.allocate_portfolio(Request::new(request)).await.unwrap();
    let allocations = response.into_inner().allocations;

    assert!(!allocations.is_empty());
    let nq_alloc = allocations.iter().find(|a| a.symbol == "NQ.FUT").unwrap();

    // Trending regime (1.5x) should increase position size
    // Expected: 10-20% of capital (1.5x boost from base Kelly)
    let allocation_pct = (nq_alloc.target_capital / 100000.0) * 100.0;
    println!("  ✓ Trending regime allocation: {:.2}% of capital", allocation_pct);

    cleanup_test_data(&pool, &symbols).await.unwrap();
}
