#![allow(unexpected_cfgs)]
#![cfg(feature = "__trading_service_integration")]
//! TDD Tests for EnsembleCoordinator Database Integration
//!
//! This test suite validates the complete pipeline:
//! ML Model Prediction -> Database Persistence -> Paper Trading Execution

use anyhow::Result;
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use uuid::Uuid;

use ml::ensemble::{EnsembleDecision, ModelVote, TradingAction};
use ml::model_factory;
use trading_service::ensemble_coordinator::EnsembleCoordinator;

// Helper function to create a test coordinator with loaded models
async fn create_test_coordinator_with_models(pool: PgPool) -> Result<EnsembleCoordinator> {
    let mut coordinator = EnsembleCoordinator::new();

    // Create and register LOADED models with model instances
    let dqn_model = model_factory::create_dqn_wrapper_with_id("DQN".to_string())?;
    let ppo_model = model_factory::create_ppo_wrapper_with_id("PPO".to_string())?;
    let tft_model = model_factory::create_tft_wrapper_with_id("TFT".to_string())?;

    coordinator
        .register_loaded_model("DQN".to_string(), dqn_model, 0.33)
        .await?;
    coordinator
        .register_loaded_model("PPO".to_string(), ppo_model, 0.33)
        .await?;
    coordinator
        .register_loaded_model("TFT".to_string(), tft_model, 0.34)
        .await?;

    let coordinator = coordinator.with_db_pool(pool);

    Ok(coordinator)
}

// Helper function to create a test decision
fn create_test_decision() -> EnsembleDecision {
    let mut votes = HashMap::new();
    votes.insert(
        "DQN".to_string(),
        ModelVote::new("DQN".to_string(), 0.75, 0.85, 0.33).with_model_type("DQN".to_string()),
    );
    votes.insert(
        "PPO".to_string(),
        ModelVote::new("PPO".to_string(), 0.65, 0.80, 0.33).with_model_type("PPO".to_string()),
    );
    votes.insert(
        "TFT".to_string(),
        ModelVote::new("TFT".to_string(), 0.70, 0.82, 0.34).with_model_type("TFT".to_string()),
    );

    EnsembleDecision::new(TradingAction::Buy, 0.82, 0.70, 0.15, votes)
        .with_symbol("ES.FUT".to_string())
}

/// Test 1: save_prediction_to_db inserts into ensemble_predictions table
#[sqlx::test(migrations = "../../migrations")]
async fn test_save_prediction_to_db(pool: PgPool) -> Result<()> {
    use trading_service::ensemble_coordinator::EnsemblePrediction;

    // Setup: Create coordinator with database pool
    let coordinator = EnsembleCoordinator::new().with_db_pool(pool.clone());

    // Setup: Create ensemble decision
    let decision = create_test_decision();

    // Create prediction record
    let prediction = EnsemblePrediction::from_decision(
        &decision,
        "ES.FUT".to_string(),
        Some("paper_trading_001".to_string()),
    );

    // Execute: Save to database
    let prediction_id = coordinator.save_prediction_to_db(&prediction).await?;

    // Verify: Prediction was inserted
    let row = sqlx::query!(
        r#"
        SELECT id, symbol, ensemble_action, ensemble_signal, ensemble_confidence,
               disagreement_rate, dqn_signal, ppo_signal, tft_signal
        FROM ensemble_predictions
        WHERE id = $1
        "#,
        prediction_id
    )
    .fetch_one(&pool)
    .await?;

    assert_eq!(row.id, prediction_id);
    assert_eq!(row.symbol, "ES.FUT");
    assert_eq!(row.ensemble_action, "BUY");
    assert!((row.ensemble_signal - 0.70).abs() < 0.01);
    assert!((row.ensemble_confidence - 0.82).abs() < 0.01);
    assert!((row.disagreement_rate - 0.15).abs() < 0.01);

    // Verify per-model votes
    assert!(row.dqn_signal.is_some());
    assert!((row.dqn_signal.unwrap() - 0.75).abs() < 0.01);
    assert!(row.ppo_signal.is_some());
    assert!(row.tft_signal.is_some());

    Ok(())
}

/// Test 2: Background loop runs and saves predictions periodically
#[sqlx::test(migrations = "../../migrations")]
async fn test_background_prediction_loop(pool: PgPool) -> Result<()> {
    use trading_service::ensemble_coordinator::EnsembleConfig;

    // Setup: Create coordinator with loaded models
    let coordinator = Arc::new(create_test_coordinator_with_models(pool.clone()).await?);

    // Configure for single symbol and short interval
    let config = EnsembleConfig {
        symbols: vec!["ES.FUT".to_string()],
        prediction_interval_secs: 1, // 1 second interval for testing
    };
    coordinator.set_config(config).await;

    // Start background loop in separate task
    let coordinator_clone = coordinator.clone();
    let loop_handle = tokio::spawn(async move {
        coordinator_clone
            .populate_predictions_continuously(1) // 1 second interval
            .await
    });

    // Wait for 3 seconds (should generate 3 predictions)
    tokio::time::sleep(Duration::from_secs(3)).await;

    // Verify: Check predictions were inserted
    let count = sqlx::query_scalar!(
        r#"
        SELECT COUNT(*)::bigint
        FROM ensemble_predictions
        WHERE prediction_timestamp >= NOW() - INTERVAL '5 seconds'
        "#
    )
    .fetch_one(&pool)
    .await?;

    assert!(
        count.unwrap() >= 3,
        "Expected at least 3 predictions, got {}",
        count.unwrap()
    );

    // Cleanup: Stop background loop
    loop_handle.abort();

    Ok(())
}

/// Test 3: Paper trading executor reads predictions from table
#[sqlx::test(migrations = "../../migrations")]
async fn test_paper_trading_reads_predictions(pool: PgPool) -> Result<()> {
    use trading_service::paper_trading_executor::{PaperTradingConfig, PaperTradingExecutor};

    // Setup: Insert test prediction
    let prediction_id = Uuid::new_v4();
    sqlx::query!(
        r#"
        INSERT INTO ensemble_predictions (
            id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate
        ) VALUES (
            $1, 'ES.FUT', 'BUY', 0.75, 0.85, 0.10
        )
        "#,
        prediction_id
    )
    .execute(&pool)
    .await?;

    // Setup: Create paper trading executor
    let config = PaperTradingConfig::default();
    let executor = PaperTradingExecutor::new(pool.clone(), config);

    // Execute: Fetch pending predictions
    let predictions = executor.fetch_pending_predictions().await?;

    // Verify: Prediction was fetched
    assert_eq!(predictions.len(), 1);
    assert_eq!(predictions[0].id, prediction_id);
    assert_eq!(predictions[0].symbol, "ES.FUT");
    assert_eq!(predictions[0].ensemble_action, "BUY");

    Ok(())
}

/// Test 4: End-to-end: ML generates prediction → saved to DB → paper trade executes
#[sqlx::test(migrations = "../../migrations")]
async fn test_e2e_ml_to_paper_trade(pool: PgPool) -> Result<()> {
    use trading_service::paper_trading_executor::{PaperTradingConfig, PaperTradingExecutor};

    // Setup: Create coordinator with loaded models
    let coordinator = Arc::new(create_test_coordinator_with_models(pool.clone()).await?);

    // Setup: Create paper trading executor
    let config = PaperTradingConfig::default();
    let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config));

    // Step 1: Generate and save prediction
    let prediction = coordinator.generate_and_save_prediction("ES.FUT").await?;
    let prediction_id = prediction.id;

    // Step 2: Paper trading executor processes prediction
    let processed_count = executor.execute_cycle().await?;

    assert_eq!(processed_count, 1, "Should have processed 1 prediction");

    // Step 3: Verify prediction was linked to order
    let row = sqlx::query!(
        r#"
        SELECT order_id
        FROM ensemble_predictions
        WHERE id = $1
        "#,
        prediction_id
    )
    .fetch_one(&pool)
    .await?;

    assert!(
        row.order_id.is_some(),
        "Prediction should be linked to order"
    );

    // Step 4: Verify order was created
    let order_count = sqlx::query_scalar!(
        r#"
        SELECT COUNT(*)::bigint
        FROM orders
        WHERE id = $1
        "#,
        row.order_id.unwrap()
    )
    .fetch_one(&pool)
    .await?;

    assert_eq!(order_count.unwrap(), 1, "Order should exist");

    Ok(())
}

/// Test 5: Performance: <100ms to save prediction to database
#[sqlx::test(migrations = "../../migrations")]
async fn test_save_prediction_performance(pool: PgPool) -> Result<()> {
    use trading_service::ensemble_coordinator::EnsemblePrediction;

    // Setup: Create coordinator
    let coordinator = EnsembleCoordinator::new().with_db_pool(pool.clone());

    // Create test prediction
    let decision = create_test_decision();

    // Benchmark: Save 100 predictions
    let mut latencies = Vec::new();
    for _ in 0..100 {
        let prediction = EnsemblePrediction::from_decision(
            &decision,
            "ES.FUT".to_string(),
            Some("paper_trading_001".to_string()),
        );

        let start = Instant::now();
        coordinator.save_prediction_to_db(&prediction).await?;
        let latency = start.elapsed();
        latencies.push(latency.as_micros());
    }

    // Verify: P99 latency < 100ms
    latencies.sort();
    let p99 = latencies[99];
    assert!(
        p99 < 100_000, // 100ms in microseconds
        "P99 latency {}μs exceeds 100ms threshold",
        p99
    );

    // Report performance
    let median = latencies[50];
    let p95 = latencies[95];
    println!(
        "Save prediction latency: median={}μs, p95={}μs, p99={}μs",
        median, p95, p99
    );

    Ok(())
}
