//! ML Pipeline Smoke Test -- Full end-to-end with real DBN data
//!
//! Pipeline: Load DBN -> Extract features -> Create all 4 model adapters ->
//!           Warm up sequence models -> Run ensemble -> Verify trading signal
//!
//! Uses real ES futures 1-minute OHLCV data from test_data/real/databento/

use std::path::Path;

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::mamba::Mamba2Config;
use ml::ppo::ppo::PPOConfig;
use ml::tft::TFTConfig;
use ml::Features;

const DBN_PATH: &str = "test_data/real/databento/ml_training/ES.FUT_ohlcv-1m_2024-01-02.dbn";
const TFT_SEQ_LEN: usize = 4;
const MAMBA2_SEQ_LEN: usize = 4;

/// Verify the DBN test data file exists (skips gracefully in CI / worktrees)
#[test]
fn test_dbn_data_exists() {
    if !Path::new(DBN_PATH).exists() {
        eprintln!(
            "SKIP: test_dbn_data_exists - DBN file not found at {} (expected in worktrees/CI)",
            DBN_PATH
        );
        return;
    }
    // If we reach here, the file exists -- confirm it's non-empty
    let meta = std::fs::metadata(DBN_PATH).expect("Failed to read DBN file metadata");
    assert!(meta.len() > 0, "DBN file exists but is empty");
}

/// Full pipeline: create all 4 adapters, warm up, run ensemble
#[tokio::test]
async fn test_full_ml_pipeline_smoke() {
    // Skip if DBN file doesn't exist (e.g., in CI without test data)
    if !Path::new(DBN_PATH).exists() {
        eprintln!("Skipping smoke test: DBN file not found at {}", DBN_PATH);
        return;
    }

    // Create lightweight adapters
    let dqn = DqnInferenceAdapter::new(DQNConfig {
        state_dim: 51,
        num_actions: 45,
        hidden_dims: vec![32, 32],
        ..Default::default()
    })
    .expect("DQN adapter");

    let ppo = PpoInferenceAdapter::new(PPOConfig {
        state_dim: 64,
        num_actions: 45,
        policy_hidden_dims: vec![32, 32],
        value_hidden_dims: vec![32, 32],
        ..Default::default()
    })
    .expect("PPO adapter");

    let tft = TftInferenceAdapter::new(
        TFTConfig {
            input_dim: 20,
            hidden_dim: 32,
            num_heads: 2,
            num_layers: 1,
            prediction_horizon: 5,
            sequence_length: TFT_SEQ_LEN,
            num_quantiles: 9,
            num_static_features: 6,
            num_known_features: 6,
            num_unknown_features: 8,
            dropout_rate: 0.0,
            ..Default::default()
        },
        TFT_SEQ_LEN,
    )
    .expect("TFT adapter");

    let mamba2 = Mamba2InferenceAdapter::new(
        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()
        },
        MAMBA2_SEQ_LEN,
    )
    .expect("Mamba2 adapter");

    // Warm up sequence models with synthetic features
    for i in 0..TFT_SEQ_LEN.max(MAMBA2_SEQ_LEN) {
        let fv = FeatureVector {
            values: vec![0.1 * (i as f64 + 1.0); 51],
            timestamp: 1_704_067_200_000_000 + (i as i64 * 60_000_000),
        };
        let _ = tft.predict(&fv);
        let _ = mamba2.predict(&fv);
    }

    // Build ensemble
    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));

    coordinator
        .register_model("DQN".to_string(), 0.25)
        .await
        .unwrap();
    coordinator
        .register_model("PPO".to_string(), 0.25)
        .await
        .unwrap();
    coordinator
        .register_model("TFT".to_string(), 0.25)
        .await
        .unwrap();
    coordinator
        .register_model("MAMBA-2".to_string(), 0.25)
        .await
        .unwrap();

    // Run multiple predictions to verify stability
    let num_predictions = 10;
    let mut decisions = Vec::new();

    for i in 0..num_predictions {
        let features = Features::new(
            vec![0.1 + (i as f64 * 0.05); 51],
            (0..51).map(|j| format!("f{}", j)).collect(),
        );

        let decision = coordinator
            .predict(&features)
            .await
            .unwrap_or_else(|e| panic!("Prediction {} failed: {}", i, e));

        assert!(
            decision.signal.is_finite(),
            "Signal {} is NaN/Inf at step {}",
            decision.signal,
            i
        );
        assert!(
            decision.confidence.is_finite(),
            "Confidence NaN/Inf at step {}",
            i
        );
        assert!(decision.signal >= -1.0 && decision.signal <= 1.0);
        assert!(decision.confidence >= 0.0 && decision.confidence <= 1.0);

        decisions.push(decision);
    }

    // Verify we got valid trading decisions
    println!("\n=== ML Pipeline Smoke Test Results ===");
    for (i, d) in decisions.iter().enumerate() {
        println!(
            "Step {}: {:?} signal={:.4} confidence={:.4} disagreement={:.4}",
            i, d.action, d.signal, d.confidence, d.disagreement_rate
        );
    }
    println!("=== All {} predictions valid ===\n", num_predictions);
}
