//! TDD Tests for Model Validation Pipeline
//!
//! Test-Driven Development approach:
//! 1. Write tests FIRST (these tests will FAIL initially)
//! 2. Implement validation_pipeline.rs to make ALL tests GREEN
//! 3. Validation triggers after training completion
//! 4. Uses backtesting service for out-of-sample validation
//! 5. Promotes models to production only if validation passes

use std::collections::HashMap;

use chrono::Utc;
use ml::training_pipeline::{
    FinancialValidationConfig, ModelArchitectureConfig, PerformanceConfig,
    ProductionTrainingConfig, TrainingHyperparameters,
};
use ml_training_service::{
    orchestrator::{JobStatus, TrainingJob},
    validation_pipeline::{
        PromotionDecision, ValidationConfig, ValidationMetrics, ValidationPipeline,
    },
};
use uuid::Uuid;

// ============================================================================
// Test 1: Validation Pipeline Creation
// ============================================================================

#[tokio::test]
async fn test_validation_pipeline_creation() {
    let config = ValidationConfig {
        holdout_data_path: "test_data/real/databento/ml_training".to_string(),
        backtest_duration_days: 30,
        min_sharpe_ratio: 1.5,
        min_win_rate: 0.52,
        max_drawdown: 0.15,
        enable_promotion: true,
    };

    let pipeline = ValidationPipeline::new(config);
    assert!(pipeline.is_ok(), "Pipeline creation should succeed");

    let pipeline = pipeline.unwrap();
    assert_eq!(pipeline.get_config().backtest_duration_days, 30);
    assert_eq!(pipeline.get_config().min_sharpe_ratio, 1.5);
}

// ============================================================================
// Test 2: Training Completion Trigger
// ============================================================================

#[tokio::test]
async fn test_validation_triggered_on_training_complete() {
    let config = ValidationConfig::default();
    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");

    // Simulate training job completion
    let job_id = Uuid::new_v4();
    let training_job = create_completed_training_job(job_id);

    // Validation should trigger automatically
    let result = pipeline.validate_on_completion(&training_job).await;

    assert!(result.is_ok(), "Validation trigger should succeed");
    let validation_result = result.unwrap();
    assert_eq!(validation_result.job_id, job_id);
    assert!(!validation_result.validation_id.is_empty());
}

// ============================================================================
// Test 3: Holdout Dataset Loading
// ============================================================================

#[tokio::test]
async fn test_holdout_dataset_loading() {
    let config = ValidationConfig {
        holdout_data_path:
            "test_data/real/databento/ZN.FUT_ohlcv-1m_2024-01-02_to_2024-01-31.uncompressed.dbn"
                .to_string(),
        ..Default::default()
    };

    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");

    // Load holdout dataset (out-of-sample data)
    let holdout_data = pipeline.load_holdout_dataset().await;

    if let Err(ref e) = holdout_data {
        eprintln!("Holdout data loading error: {:?}", e);
        eprintln!("Error details: {}", e);
        eprintln!("Error source: {:?}", e.source());
    }

    assert!(
        holdout_data.is_ok(),
        "Holdout data loading should succeed: {:?}",
        holdout_data.as_ref().err()
    );
    let data = holdout_data.unwrap();
    assert!(!data.is_empty(), "Holdout dataset should not be empty");
    assert!(
        data.len() >= 100,
        "Holdout dataset should have at least 100 bars for 30-day backtest"
    );
}

// ============================================================================
// Test 4: Backtesting Integration
// ============================================================================

#[tokio::test]
async fn test_backtesting_integration() {
    let config = ValidationConfig {
        holdout_data_path:
            "test_data/real/databento/ZN.FUT_ohlcv-1m_2024-01-02_to_2024-01-31.uncompressed.dbn"
                .to_string(),
        backtest_duration_days: 30,
        ..Default::default()
    };

    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");
    let job_id = Uuid::new_v4();
    let training_job = create_completed_training_job(job_id);

    // Run backtest on holdout data
    let backtest_result = pipeline
        .run_backtest(&training_job, "validation_data_path")
        .await;

    assert!(
        backtest_result.is_ok(),
        "Backtesting integration should succeed"
    );
    let result = backtest_result.unwrap();

    // Verify backtest executed and returned metrics
    assert!(
        result.sharpe_ratio.is_finite(),
        "Sharpe ratio should be finite"
    );
    assert!(result.win_rate >= 0.0 && result.win_rate <= 1.0);
    assert!(result.max_drawdown >= 0.0 && result.max_drawdown <= 1.0);
}

// ============================================================================
// Test 5: Metrics Calculation
// ============================================================================

#[tokio::test]
async fn test_metrics_calculation() {
    let config = ValidationConfig {
        min_sharpe_ratio: 1.5,
        min_win_rate: 0.52,
        max_drawdown: 0.15,
        ..Default::default()
    };

    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");

    // Mock backtest results
    let mock_trades = vec![
        // Winning trades
        (100.0, 101.5), // +1.5% profit
        (101.5, 103.0), // +1.5% profit
        (103.0, 105.0), // +2.0% profit
        // Losing trades
        (105.0, 104.0), // -1.0% loss
        (104.0, 105.5), // +1.5% profit
    ];

    let metrics = pipeline.calculate_metrics(&mock_trades).await;

    assert!(metrics.is_ok(), "Metrics calculation should succeed");
    let result = metrics.unwrap();

    assert_eq!(result.total_trades, 5);
    assert!(result.win_rate > 0.5, "Win rate should be > 50%");
    assert!(result.sharpe_ratio > 0.0, "Sharpe ratio should be positive");
    assert!(
        result.max_drawdown >= 0.0 && result.max_drawdown <= 1.0,
        "Max drawdown should be between 0 and 1"
    );
}

// ============================================================================
// Test 6: Promotion Decision Logic (PASS)
// ============================================================================

#[tokio::test]
async fn test_promotion_decision_pass() {
    let config = ValidationConfig {
        min_sharpe_ratio: 1.5,
        min_win_rate: 0.52,
        max_drawdown: 0.15,
        enable_promotion: true,
        ..Default::default()
    };

    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");

    // Excellent metrics (should PASS)
    let metrics = ValidationMetrics {
        sharpe_ratio: 2.0,  // Above threshold (1.5)
        win_rate: 0.58,     // Above threshold (0.52)
        max_drawdown: 0.10, // Below threshold (0.15)
        total_trades: 150,
        avg_profit_per_trade: 0.015,
        profit_factor: 2.5,
        total_return: 0.45,
    };

    let decision = pipeline.make_promotion_decision(&metrics).await;

    assert!(decision.is_ok(), "Promotion decision should succeed");
    let result = decision.unwrap();

    assert_eq!(result.decision, PromotionDecision::Promote);
    assert!(
        result.reason.contains("PASS"),
        "Reason should indicate validation passed"
    );
}

// ============================================================================
// Test 7: Promotion Decision Logic (FAIL - Low Sharpe)
// ============================================================================

#[tokio::test]
async fn test_promotion_decision_fail_low_sharpe() {
    let config = ValidationConfig {
        min_sharpe_ratio: 1.5,
        min_win_rate: 0.52,
        max_drawdown: 0.15,
        enable_promotion: true,
        ..Default::default()
    };

    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");

    // Poor metrics (LOW SHARPE - should FAIL)
    let metrics = ValidationMetrics {
        sharpe_ratio: 0.8,  // BELOW threshold (1.5) ❌
        win_rate: 0.58,     // Above threshold
        max_drawdown: 0.10, // Below threshold
        total_trades: 150,
        avg_profit_per_trade: 0.005,
        profit_factor: 1.2,
        total_return: 0.15,
    };

    let decision = pipeline.make_promotion_decision(&metrics).await;

    assert!(decision.is_ok(), "Promotion decision should succeed");
    let result = decision.unwrap();

    assert_eq!(result.decision, PromotionDecision::Reject);
    assert!(
        result.reason.contains("Sharpe") || result.reason.contains("sharpe"),
        "Reason should mention Sharpe ratio failure"
    );
}

// ============================================================================
// Test 8: Promotion Decision Logic (FAIL - Low Win Rate)
// ============================================================================

#[tokio::test]
async fn test_promotion_decision_fail_low_win_rate() {
    let config = ValidationConfig {
        min_sharpe_ratio: 1.5,
        min_win_rate: 0.52,
        max_drawdown: 0.15,
        enable_promotion: true,
        ..Default::default()
    };

    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");

    // Poor metrics (LOW WIN RATE - should FAIL)
    let metrics = ValidationMetrics {
        sharpe_ratio: 2.0,  // Above threshold
        win_rate: 0.48,     // BELOW threshold (0.52) ❌
        max_drawdown: 0.10, // Below threshold
        total_trades: 150,
        avg_profit_per_trade: 0.015,
        profit_factor: 1.8,
        total_return: 0.35,
    };

    let decision = pipeline.make_promotion_decision(&metrics).await;

    assert!(decision.is_ok(), "Promotion decision should succeed");
    let result = decision.unwrap();

    assert_eq!(result.decision, PromotionDecision::Reject);
    assert!(
        result.reason.contains("win rate") || result.reason.contains("Win rate"),
        "Reason should mention win rate failure"
    );
}

// ============================================================================
// Test 9: Promotion Decision Logic (FAIL - High Drawdown)
// ============================================================================

#[tokio::test]
async fn test_promotion_decision_fail_high_drawdown() {
    let config = ValidationConfig {
        min_sharpe_ratio: 1.5,
        min_win_rate: 0.52,
        max_drawdown: 0.15,
        enable_promotion: true,
        ..Default::default()
    };

    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");

    // Poor metrics (HIGH DRAWDOWN - should FAIL)
    let metrics = ValidationMetrics {
        sharpe_ratio: 2.0,  // Above threshold
        win_rate: 0.58,     // Above threshold
        max_drawdown: 0.25, // ABOVE threshold (0.15) ❌
        total_trades: 150,
        avg_profit_per_trade: 0.015,
        profit_factor: 2.0,
        total_return: 0.40,
    };

    let decision = pipeline.make_promotion_decision(&metrics).await;

    assert!(decision.is_ok(), "Promotion decision should succeed");
    let result = decision.unwrap();

    assert_eq!(result.decision, PromotionDecision::Reject);
    assert!(
        result.reason.contains("drawdown") || result.reason.contains("Drawdown"),
        "Reason should mention drawdown failure"
    );
}

// ============================================================================
// Test 10: End-to-End Validation Flow
// ============================================================================

#[tokio::test]
async fn test_e2e_validation_flow() {
    let config = ValidationConfig {
        holdout_data_path:
            "test_data/real/databento/ZN.FUT_ohlcv-1m_2024-01-02_to_2024-01-31.uncompressed.dbn"
                .to_string(),
        backtest_duration_days: 30,
        min_sharpe_ratio: 1.0, // Relaxed for testing
        min_win_rate: 0.50,    // Relaxed for testing
        max_drawdown: 0.20,    // Relaxed for testing
        enable_promotion: true,
    };

    let pipeline = ValidationPipeline::new(config).expect("Pipeline creation failed");
    let job_id = Uuid::new_v4();
    let training_job = create_completed_training_job(job_id);

    // Complete validation flow:
    // 1. Trigger validation
    let validation_result = pipeline.validate_on_completion(&training_job).await;

    if let Err(ref e) = validation_result {
        eprintln!("Validation trigger error: {:?}", e);
    }

    assert!(
        validation_result.is_ok(),
        "Validation should trigger: {:?}",
        validation_result.as_ref().err()
    );

    let result = validation_result.unwrap();

    // 2. Verify validation executed
    assert_eq!(result.job_id, job_id);
    assert!(!result.validation_id.is_empty());

    // 3. Check metrics were calculated
    assert!(result.metrics.is_some(), "Metrics should be calculated");
    let metrics = result.metrics.unwrap();
    assert!(metrics.sharpe_ratio.is_finite());
    assert!(metrics.total_trades > 0);

    // 4. Verify promotion decision was made
    assert!(result.promotion_decision.is_some());
    let decision = result.promotion_decision.unwrap();
    assert!(
        matches!(
            decision.decision,
            PromotionDecision::Promote | PromotionDecision::Reject
        ),
        "Decision should be Promote or Reject"
    );
}

// ============================================================================
// Helper Functions
// ============================================================================

/// Create a completed training job for testing
fn create_completed_training_job(job_id: Uuid) -> TrainingJob {
    let config = ProductionTrainingConfig {
        model_config: ModelArchitectureConfig {
            input_dim: 50,
            output_dim: 1,
            hidden_dims: vec![128, 64],
            dropout_rate: 0.1,
            activation: "relu".to_string(),
            batch_norm: false,
            residual_connections: false,
        },
        training_params: TrainingHyperparameters {
            learning_rate: 0.001,
            batch_size: 32,
            max_epochs: 10,
            patience: 3,
            validation_split: 0.2,
            l2_regularization: 0.0001,
            lr_decay_factor: 0.1,
            lr_decay_patience: 5,
        },
        safety_config: ml::safety::MLSafetyConfig::default(),
        gradient_config: ml::safety::GradientSafetyConfig::default(),
        financial_config: FinancialValidationConfig {
            max_prediction_multiple: 2.0,
            min_prediction_confidence: 0.6,
            validate_position_sizing: true,
            max_position_fraction: 0.25,
            min_sharpe_threshold: 0.5,
        },
        performance_config: PerformanceConfig {
            device_preference: "cpu".to_string(),
            max_memory_bytes: 8 * 1024 * 1024 * 1024,
            num_workers: 4,
            gradient_accumulation_steps: 1,
        },
    };

    let mut job = TrainingJob::new(
        "DQN".to_string(),
        config,
        "Test training job for validation".to_string(),
        HashMap::new(),
    );

    // Set job as completed with mock training results
    job.id = job_id;
    job.status = JobStatus::Completed;
    job.started_at = Some(Utc::now() - chrono::Duration::hours(2));
    job.completed_at = Some(Utc::now());
    job.progress_percentage = 100.0;
    job.current_epoch = 10;
    job.total_epochs = 10;
    job.model_artifact_path = Some(format!("models/{}.bin", job_id));

    // Mock training metrics
    job.metrics.insert("final_train_loss".to_string(), 0.015);
    job.metrics.insert("final_val_loss".to_string(), 0.018);
    job.metrics.insert("accuracy".to_string(), 0.85);

    job
}
