//! End-to-end pipeline integration test: OHLCV -> Features -> Ensemble -> Prediction
//!
//! Proves the critical trading pipeline path works with all 10 real candle
//! inference adapters (DQN, PPO, TFT, Mamba2, Liquid-CfC, TGGN, TLOB, KAN,
//! xLSTM, Diffusion) using random (untrained) weights.
//!
//! No external dependencies: no DB, no Docker, no network.

use std::sync::Arc;

use chrono::Utc;
use trading_service::adapter_bridge::InferenceAdapterBridge;
use trading_service::ensemble_coordinator::EnsembleCoordinator;

/// Register all 10 inference adapters into the coordinator.
///
/// This mirrors the exact construction pattern from `main.rs` in the
/// trading_service binary so the test exercises the real production path.
async fn register_all_10_adapters(coordinator: &EnsembleCoordinator) -> usize {
    let mut registered = 0usize;

    // 1. DQN adapter (51-dim input, 45 factored actions)
    match ml::ensemble::adapters::DqnInferenceAdapter::new(ml::dqn::dqn::DQNConfig {
        state_dim: 51,
        num_actions: 45,
        hidden_dims: vec![64, 64],
        ..Default::default()
    }) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "DQN".to_string(),
                ml::ModelType::DQN,
            ));
            if coordinator
                .register_loaded_model("DQN".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create DQN adapter: {}", e),
    }

    // 2. PPO adapter (51-dim input, 3 actions: buy/sell/hold)
    match ml::ensemble::adapters::PpoInferenceAdapter::new(ml::ppo::ppo::PPOConfig {
        state_dim: 51,
        num_actions: 3,
        policy_hidden_dims: vec![64, 64],
        value_hidden_dims: vec![64, 64],
        ..Default::default()
    }) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "PPO".to_string(),
                ml::ModelType::PPO,
            ));
            if coordinator
                .register_loaded_model("PPO".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create PPO adapter: {}", e),
    }

    // 3. TFT adapter (51-dim split: 3 static + 8 known + 40 unknown)
    match ml::ensemble::adapters::TftInferenceAdapter::new(
        ml::tft::TFTConfig {
            input_dim: 51,
            hidden_dim: 32,
            num_heads: 4,
            num_layers: 1,
            prediction_horizon: 1,
            sequence_length: 10,
            num_quantiles: 3,
            num_static_features: 3,
            num_known_features: 8,
            num_unknown_features: 40,
            ..Default::default()
        },
        10,
    ) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "TFT".to_string(),
                ml::ModelType::TFT,
            ));
            if coordinator
                .register_loaded_model("TFT".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create TFT adapter: {}", e),
    }

    // 4. Mamba2 adapter (d_model=64, zero-pads from 51-dim features)
    match ml::ensemble::adapters::Mamba2InferenceAdapter::new(
        ml::mamba::Mamba2Config {
            d_model: 64,
            d_state: 16,
            d_head: 16,
            num_heads: 2,
            expand: 1,
            num_layers: 1,
            ..Default::default()
        },
        10,
    ) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "MAMBA2".to_string(),
                ml::ModelType::MAMBA,
            ));
            if coordinator
                .register_loaded_model("MAMBA2".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create MAMBA2 adapter: {}", e),
    }

    // 5. Liquid CfC adapter (51-dim input, 3 output: buy/hold/sell)
    match ml::ensemble::adapters::LiquidInferenceAdapter::new(
        ml::liquid::candle_cfc::CfCTrainConfig {
            input_size: 51,
            hidden_size: 32,
            output_size: 3,
            backbone_hidden_sizes: vec![32],
            seq_len: 1,
            device: ml::liquid::candle_cfc::DeviceConfig::Cpu,
            ..ml::liquid::candle_cfc::CfCTrainConfig::default()
        },
    ) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "Liquid-CfC".to_string(),
                ml::ModelType::LNN,
            ));
            if coordinator
                .register_loaded_model("Liquid-CfC".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create Liquid-CfC adapter: {}", e),
    }

    // 6. TGGN adapter (51-dim input, 2-layer candle projection)
    match ml::ensemble::adapters::TggnInferenceAdapter::new(51, 64) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "TGGN".to_string(),
                ml::ModelType::TGGN,
            ));
            if coordinator
                .register_loaded_model("TGGN".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create TGGN adapter: {}", e),
    }

    // 7. TLOB adapter (51-dim features, 3-layer MLP, seq_len=10)
    match ml::ensemble::adapters::TlobInferenceAdapter::new(51, 64, 10) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "TLOB".to_string(),
                ml::ModelType::TLOB,
            ));
            if coordinator
                .register_loaded_model("TLOB".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create TLOB adapter: {}", e),
    }

    // 8. KAN adapter (51-dim input, B-spline activations)
    match ml::ensemble::adapters::KanInferenceAdapter::new(ml::kan::KANConfig {
        layer_widths: vec![51, 32, 16, 1],
        ..Default::default()
    }) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "KAN".to_string(),
                ml::ModelType::KAN,
            ));
            if coordinator
                .register_loaded_model("KAN".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create KAN adapter: {}", e),
    }

    // 9. xLSTM adapter (51-dim input, sLSTM+mLSTM, seq_len=10)
    match ml::ensemble::adapters::XlstmInferenceAdapter::new(
        ml::xlstm::XLSTMConfig {
            input_dim: 51,
            hidden_dim: 64,
            num_blocks: 2,
            num_heads: 2,
            ..Default::default()
        },
        10,
    ) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "xLSTM".to_string(),
                ml::ModelType::XLSTM,
            ));
            if coordinator
                .register_loaded_model("xLSTM".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create xLSTM adapter: {}", e),
    }

    // 10. Diffusion adapter (51-dim features, DDPM denoiser, t=1 inference)
    match ml::ensemble::adapters::DiffusionInferenceAdapter::new(ml::diffusion::DiffusionConfig {
        seq_len: 1,
        feature_dim: 51,
        hidden_dim: 64,
        num_layers: 2,
        time_embed_dim: 32,
        num_timesteps: 100,
        sampling_steps: 5,
        ..Default::default()
    }) {
        Ok(adapter) => {
            let bridge = Arc::new(InferenceAdapterBridge::new(
                Box::new(adapter),
                "Diffusion".to_string(),
                ml::ModelType::Diffusion,
            ));
            if coordinator
                .register_loaded_model("Diffusion".to_string(), bridge, 0.10)
                .await
                .is_ok()
            {
                registered += 1;
            }
        }
        Err(e) => eprintln!("Failed to create Diffusion adapter: {}", e),
    }

    registered
}

/// End-to-end pipeline test: OHLCV -> features -> ensemble (10 models) -> prediction.
///
/// This test proves the critical trading pipeline works in-process with no
/// external dependencies. All 10 candle-backed inference adapters are
/// initialised with random weights and fed synthetic market data.
#[tokio::test(flavor = "multi_thread")]
async fn test_e2e_ohlcv_to_prediction_10_models() {
    // ---- Step 1: Create ensemble coordinator and register all 10 models ----
    let coordinator = EnsembleCoordinator::new();

    let registered = register_all_10_adapters(&coordinator).await;
    let model_count = coordinator.model_count().await;

    println!(
        "[E2E] Registered {}/{} adapters (coordinator reports {} models)",
        registered, 10, model_count,
    );
    assert_eq!(
        model_count, 10,
        "All 10 inference adapters must be registered"
    );

    // ---- Step 2: Feed 60 synthetic OHLCV bars to warm up feature extractors ----
    let symbol = "ES.FUT";
    let base_time = Utc::now();
    let base_price = 4500.0_f64;

    for i in 0..60 {
        // Gentle uptrend with small noise
        let price = base_price + (i as f64) * 0.25;
        let volume = 10_000.0 + (i as f64) * 50.0;
        let ts = base_time + chrono::Duration::seconds(i * 60);

        coordinator
            .update_market_data(symbol, price, volume, ts)
            .await
            .unwrap_or_else(|e| panic!("update_market_data failed at bar {}: {}", i, e));
    }
    println!("[E2E] Fed 60 OHLCV bars for {}", symbol);

    // ---- Step 3: Extract features (should be real 51-dim after warmup) ----
    let features = coordinator
        .fetch_features_for_symbol(symbol)
        .await
        .unwrap_or_else(|e| panic!("fetch_features_for_symbol failed: {}", e));

    assert_eq!(
        features.values.len(),
        51,
        "Feature vector must be 51-dimensional"
    );
    assert_eq!(features.names.len(), 51, "Feature names must be 51");
    assert_eq!(features.symbol.as_deref(), Some(symbol));

    // After warmup, at least some features should be non-zero
    let non_zero = features.values.iter().filter(|v| v.abs() > 1e-12).count();
    println!(
        "[E2E] Feature extraction: {}/51 non-zero features",
        non_zero
    );
    assert!(
        non_zero > 5,
        "Expected many non-zero features after 60-bar warmup, got {}",
        non_zero,
    );

    // All features must be finite
    for (idx, &val) in features.values.iter().enumerate() {
        assert!(
            val.is_finite(),
            "Feature {} ({}) is NaN/Inf: {}",
            idx,
            features.names.get(idx).map(|s| s.as_str()).unwrap_or("?"),
            val,
        );
    }

    // ---- Step 4: Run ensemble prediction ----
    let decision = coordinator
        .predict(&features)
        .await
        .unwrap_or_else(|e| panic!("Ensemble prediction failed: {}", e));

    println!(
        "[E2E] Ensemble decision: action={:?}, confidence={:.4}, signal={:.4}, \
         disagreement={:.4}, model_votes={}",
        decision.action,
        decision.confidence,
        decision.signal,
        decision.disagreement_rate,
        decision.model_count(),
    );

    // Print per-model votes for debugging
    for (model_id, vote) in &decision.model_votes {
        println!(
            "  [{}] signal={:.4}, confidence={:.4}, weight={:.4}",
            model_id, vote.signal, vote.confidence, vote.weight,
        );
    }

    // ---- Step 5: Assertions ----

    // Confidence must be in [0.0, 1.0]
    assert!(
        decision.confidence >= 0.0 && decision.confidence <= 1.0,
        "Confidence {:.4} must be in [0.0, 1.0]",
        decision.confidence,
    );

    // Action must be Buy, Sell, or Hold (exhaustive match proves it)
    match decision.action {
        ml::ensemble::TradingAction::Buy
        | ml::ensemble::TradingAction::Sell
        | ml::ensemble::TradingAction::Hold => {}
    }

    // Model count must be 10 (all models contributed a vote)
    assert_eq!(
        decision.model_count(),
        10,
        "All 10 models should have contributed votes, got {}",
        decision.model_count(),
    );

    // Signal must be finite
    assert!(
        decision.signal.is_finite(),
        "Signal must be finite, got {}",
        decision.signal,
    );

    // Disagreement rate must be in [0.0, 1.0]
    assert!(
        decision.disagreement_rate >= 0.0 && decision.disagreement_rate <= 1.0,
        "Disagreement rate {:.4} must be in [0.0, 1.0]",
        decision.disagreement_rate,
    );

    println!("[E2E] All assertions passed -- pipeline is wired end-to-end!");
}
