//! Ensemble integration test -- all 4 models contributing to a single trading decision
//!
//! Verifies: DQN + PPO + TFT + Mamba2 -> EnsembleCoordinator -> EnsembleDecision
//! This is the critical test that proves the ensemble pipeline works end-to-end.

use ml::dqn::dqn::DQNConfig;
use ml::ensemble::adapters::{
    DqnInferenceAdapter, Mamba2InferenceAdapter, PpoInferenceAdapter, TftInferenceAdapter,
};
use ml::ensemble::coordinator::EnsembleCoordinator;
use ml::ensemble::inference_adapter::{FeatureVector, ModelInferenceAdapter};
use ml::ensemble::TradingAction;
use ml::mamba::Mamba2Config;
use ml::ppo::ppo::PPOConfig;
use ml::tft::TFTConfig;
use ml::Features;

fn small_dqn_config() -> DQNConfig {
    DQNConfig {
        state_dim: 51,
        num_actions: 5,
        hidden_dims: vec![32, 32],
        ..Default::default()
    }
}

fn small_ppo_config() -> PPOConfig {
    PPOConfig {
        state_dim: 64,
        num_actions: 45,
        policy_hidden_dims: vec![32, 32],
        value_hidden_dims: vec![32, 32],
        ..Default::default()
    }
}

fn small_tft_config() -> TFTConfig {
    TFTConfig {
        input_dim: 20,
        hidden_dim: 32,
        num_heads: 2,
        num_layers: 1,
        prediction_horizon: 5,
        sequence_length: 4,
        num_quantiles: 9,
        num_static_features: 6,
        num_known_features: 6,
        num_unknown_features: 8,
        dropout_rate: 0.0,
        ..Default::default()
    }
}

fn small_mamba2_config() -> Mamba2Config {
    Mamba2Config {
        d_model: 32,
        d_state: 8,
        d_head: 8,
        num_heads: 2,
        expand: 2,
        num_layers: 1,
        max_seq_len: 8,
        dropout: 0.0,
        ..Default::default()
    }
}

const TFT_SEQ_LEN: usize = 4;
const MAMBA2_SEQ_LEN: usize = 4;

#[tokio::test]
async fn test_ensemble_all_4_models_produce_decision() {
    // Create all 4 adapters
    let dqn = DqnInferenceAdapter::new(small_dqn_config()).expect("DQN adapter creation failed");
    let ppo = PpoInferenceAdapter::new(small_ppo_config()).expect("PPO adapter creation failed");
    let tft =
        TftInferenceAdapter::new(small_tft_config(), TFT_SEQ_LEN).expect("TFT adapter creation failed");
    let mamba2 = Mamba2InferenceAdapter::new(small_mamba2_config(), MAMBA2_SEQ_LEN)
        .expect("Mamba2 adapter creation failed");

    // Warm up TFT and Mamba2 sequence buffers
    for i in 0..TFT_SEQ_LEN {
        let fv = FeatureVector {
            values: vec![0.1; 51],
            timestamp: 1_700_000_000 + i as i64,
        };
        let _ = tft.predict(&fv);
        let _ = mamba2.predict(&fv);
    }

    // Verify all adapters are ready
    assert!(dqn.is_ready(), "DQN should be ready");
    assert!(ppo.is_ready(), "PPO should be ready");
    assert!(tft.is_ready(), "TFT should be ready after warmup");
    assert!(mamba2.is_ready(), "Mamba2 should be ready after warmup");

    // Build ensemble coordinator
    let mut coordinator = EnsembleCoordinator::new();
    coordinator.add_adapter(Box::new(dqn));
    coordinator.add_adapter(Box::new(ppo));
    coordinator.add_adapter(Box::new(tft));
    coordinator.add_adapter(Box::new(mamba2));

    // Register models with equal weights
    coordinator
        .register_model("DQN".to_string(), 0.25)
        .await
        .expect("register DQN");
    coordinator
        .register_model("PPO".to_string(), 0.25)
        .await
        .expect("register PPO");
    coordinator
        .register_model("TFT".to_string(), 0.25)
        .await
        .expect("register TFT");
    coordinator
        .register_model("MAMBA-2".to_string(), 0.25)
        .await
        .expect("register MAMBA-2");

    // Create Features for ensemble prediction
    let features = Features::new(
        vec![0.1; 51],
        (0..51).map(|i| format!("f{}", i)).collect(),
    );

    // Get ensemble decision
    let decision = coordinator
        .predict(&features)
        .await
        .expect("Ensemble prediction should succeed");

    // Verify decision is valid
    assert!(
        decision.confidence >= 0.0 && decision.confidence <= 1.0,
        "ensemble confidence {} out of [0,1]",
        decision.confidence
    );
    assert!(
        decision.signal >= -1.0 && decision.signal <= 1.0,
        "ensemble signal {} out of [-1,1]",
        decision.signal
    );
    assert!(decision.signal.is_finite(), "signal must not be NaN/Inf");
    assert!(
        decision.confidence.is_finite(),
        "confidence must not be NaN/Inf"
    );

    // Verify we got votes from all 4 models
    assert!(!decision.model_votes.is_empty(), "Should have model votes");

    // Action should be Buy, Sell, or Hold
    match decision.action {
        TradingAction::Buy | TradingAction::Sell | TradingAction::Hold => {} // Valid
    }

    println!(
        "Ensemble decision: action={:?}, signal={:.4}, confidence={:.4}, disagreement={:.4}",
        decision.action, decision.signal, decision.confidence, decision.disagreement_rate
    );
    for (name, vote) in &decision.model_votes {
        println!(
            "  {} -> signal={:.4}, confidence={:.4}",
            name, vote.signal, vote.confidence
        );
    }
}

#[tokio::test]
async fn test_ensemble_no_adapters_returns_error() {
    let coordinator = EnsembleCoordinator::new();
    let features = Features::new(vec![0.1; 51], vec!["f0".to_string()]);

    let result = coordinator.predict(&features).await;
    assert!(result.is_err(), "Empty ensemble should return error");
}
