//! Integration test: ML inference -> ensemble vote -> order generation
//!
//! Verifies the complete pipeline from feature vector through model
//! inference, ensemble aggregation, and order signal generation.
//! This is a critical path test for the ML -> Order execution pipeline.

use ml::ensemble::inference_adapter::{
    EnsemblePrediction, FeatureVector, ModelInferenceAdapter, PredictionMeta,
};
use ml::ensemble::inference_ensemble::InferenceEnsemble;
use ml::MLResult;

/// Test adapter that simulates a bullish DQN model
struct MockDQNAdapter;

impl ModelInferenceAdapter for MockDQNAdapter {
    fn model_name(&self) -> &str {
        "DQN-v1"
    }

    fn predict(&self, _features: &FeatureVector) -> MLResult<EnsemblePrediction> {
        Ok(EnsemblePrediction {
            model_name: "DQN-v1".to_string(),
            direction: 0.8,
            confidence: 0.85,
            metadata: PredictionMeta::default(),
        })
    }

    fn is_ready(&self) -> bool {
        true
    }
}

/// Test adapter that simulates a bearish PPO model
struct MockPPOAdapter;

impl ModelInferenceAdapter for MockPPOAdapter {
    fn model_name(&self) -> &str {
        "PPO-v1"
    }

    fn predict(&self, _features: &FeatureVector) -> MLResult<EnsemblePrediction> {
        Ok(EnsemblePrediction {
            model_name: "PPO-v1".to_string(),
            direction: -0.3,
            confidence: 0.6,
            metadata: PredictionMeta::default(),
        })
    }

    fn is_ready(&self) -> bool {
        true
    }
}

/// Test adapter that returns an error (simulates model failure)
struct FailingAdapter;

impl ModelInferenceAdapter for FailingAdapter {
    fn model_name(&self) -> &str {
        "FailingModel"
    }

    fn predict(&self, _features: &FeatureVector) -> MLResult<EnsemblePrediction> {
        Err(ml::MLError::InferenceError(
            "Simulated model failure".to_string(),
        ))
    }

    fn is_ready(&self) -> bool {
        true
    }
}

/// Test adapter that returns NaN direction and confidence
struct NaNAdapter;

impl ModelInferenceAdapter for NaNAdapter {
    fn model_name(&self) -> &str {
        "NaN-model"
    }

    fn predict(&self, _features: &FeatureVector) -> MLResult<EnsemblePrediction> {
        Ok(EnsemblePrediction {
            model_name: "NaN-model".to_string(),
            direction: f64::NAN,
            confidence: f64::NAN,
            metadata: PredictionMeta::default(),
        })
    }

    fn is_ready(&self) -> bool {
        true
    }
}

/// Test adapter that is never ready (simulates an unloaded model)
struct NotReadyAdapter;

impl ModelInferenceAdapter for NotReadyAdapter {
    fn model_name(&self) -> &str {
        "NotReady"
    }

    fn predict(&self, _features: &FeatureVector) -> MLResult<EnsemblePrediction> {
        Ok(EnsemblePrediction {
            model_name: "NotReady".to_string(),
            direction: 1.0,
            confidence: 1.0,
            metadata: PredictionMeta::default(),
        })
    }

    fn is_ready(&self) -> bool {
        false
    }
}

fn make_feature_vector() -> FeatureVector {
    FeatureVector {
        values: vec![0.1; 51],
        timestamp: 1_700_000_000_000_000,
    }
}

// ---------------------------------------------------------------------------
// Test 1: Full ML -> Order pipeline
// ---------------------------------------------------------------------------

#[test]
fn test_ml_to_order_pipeline() {
    // 1. Create a canonical 51-dim feature vector
    let features = make_feature_vector();
    assert_eq!(features.values.len(), 51, "Feature vector must be 51-dim");

    // 2. Create ensemble with mock adapters (bullish DQN + bearish PPO)
    let adapters: Vec<Box<dyn ModelInferenceAdapter>> = vec![
        Box::new(MockDQNAdapter),
        Box::new(MockPPOAdapter),
    ];
    let ensemble = InferenceEnsemble::new(adapters);

    // 3. Verify both models are ready
    assert_eq!(ensemble.ready_count(), 2, "Both mock models should be ready");

    // 4. Run ensemble prediction
    let prediction = ensemble.predict(&features);
    assert!(prediction.is_ok(), "Ensemble prediction should succeed");
    let pred = prediction.unwrap_or_else(|e| panic!("Prediction failed: {e}"));

    // 5. Verify prediction properties are well-formed
    assert!(pred.direction.is_finite(), "Direction must be finite");
    assert!(pred.confidence.is_finite(), "Confidence must be finite");
    assert!(
        pred.confidence >= 0.0 && pred.confidence <= 1.0,
        "Confidence {} should be in [0.0, 1.0]",
        pred.confidence
    );
    assert!(
        pred.direction >= -1.0 && pred.direction <= 1.0,
        "Direction {} should be in [-1.0, 1.0]",
        pred.direction
    );

    // 6. Generate order signal from prediction
    //    The DQN model (dir=0.8, conf=0.85) dominates the PPO model (dir=-0.3, conf=0.6)
    //    because higher confidence gives it more weight in the ensemble.
    //    Expected net direction: positive (bullish).
    let order_side = if pred.direction > 0.0 { "Buy" } else { "Sell" };
    let order_size = (pred.confidence * 100.0).round();

    assert_eq!(
        order_side, "Buy",
        "Net bullish ensemble (DQN dominates) should generate Buy, got direction={}",
        pred.direction
    );
    assert!(
        order_size > 0.0,
        "Order size should be positive, got {}",
        order_size
    );
    assert!(
        order_size <= 100.0,
        "Order size should be <= 100, got {}",
        order_size
    );

    // 7. Verify model name reflects aggregation
    assert!(
        pred.model_name.contains("ENSEMBLE"),
        "Aggregated prediction model_name should contain 'ENSEMBLE', got '{}'",
        pred.model_name
    );
}

// ---------------------------------------------------------------------------
// Test 2: Ensemble handles all-NaN models gracefully
// ---------------------------------------------------------------------------

#[test]
fn test_ensemble_handles_all_models_returning_nan() {
    let adapters: Vec<Box<dyn ModelInferenceAdapter>> = vec![Box::new(NaNAdapter)];
    let ensemble = InferenceEnsemble::new(adapters);
    let features = make_feature_vector();

    // The NaN circuit breaker should filter out the NaN model,
    // leaving zero successful predictions -> error.
    let result = ensemble.predict(&features);
    assert!(
        result.is_err(),
        "All-NaN ensemble should return error, but got: {:?}",
        result
    );
}

// ---------------------------------------------------------------------------
// Test 3: Ensemble handles a mix of good + failing models
// ---------------------------------------------------------------------------

#[test]
fn test_ensemble_survives_partial_model_failure() {
    let adapters: Vec<Box<dyn ModelInferenceAdapter>> = vec![
        Box::new(MockDQNAdapter),
        Box::new(FailingAdapter),
    ];
    let ensemble = InferenceEnsemble::new(adapters);
    let features = make_feature_vector();

    // The failing model is skipped; DQN alone should produce a valid prediction.
    let result = ensemble.predict(&features);
    assert!(
        result.is_ok(),
        "Ensemble with one good model should succeed, got: {:?}",
        result
    );

    let pred = result.unwrap_or_else(|e| panic!("Prediction failed: {e}"));
    assert!(
        pred.direction.is_finite(),
        "Direction must be finite after partial failure"
    );
    assert!(
        pred.confidence.is_finite(),
        "Confidence must be finite after partial failure"
    );
}

// ---------------------------------------------------------------------------
// Test 4: Ensemble with no ready models
// ---------------------------------------------------------------------------

#[test]
fn test_ensemble_no_ready_models_returns_error() {
    let adapters: Vec<Box<dyn ModelInferenceAdapter>> = vec![Box::new(NotReadyAdapter)];
    let ensemble = InferenceEnsemble::new(adapters);
    let features = make_feature_vector();

    let result = ensemble.predict(&features);
    assert!(
        result.is_err(),
        "Ensemble with no ready models should return error"
    );
}

// ---------------------------------------------------------------------------
// Test 5: Ensemble with custom weights changes outcome
// ---------------------------------------------------------------------------

#[test]
fn test_ensemble_custom_weights_affect_direction() {
    // Two opposing models with equal confidence
    struct BullAdapter;
    impl ModelInferenceAdapter for BullAdapter {
        fn model_name(&self) -> &str { "Bull" }
        fn predict(&self, _: &FeatureVector) -> MLResult<EnsemblePrediction> {
            Ok(EnsemblePrediction {
                model_name: "Bull".to_string(),
                direction: 1.0,
                confidence: 0.8,
                metadata: PredictionMeta::default(),
            })
        }
        fn is_ready(&self) -> bool { true }
    }

    struct BearAdapter;
    impl ModelInferenceAdapter for BearAdapter {
        fn model_name(&self) -> &str { "Bear" }
        fn predict(&self, _: &FeatureVector) -> MLResult<EnsemblePrediction> {
            Ok(EnsemblePrediction {
                model_name: "Bear".to_string(),
                direction: -1.0,
                confidence: 0.8,
                metadata: PredictionMeta::default(),
            })
        }
        fn is_ready(&self) -> bool { true }
    }

    let adapters: Vec<Box<dyn ModelInferenceAdapter>> = vec![
        Box::new(BullAdapter),
        Box::new(BearAdapter),
    ];

    // Without custom weights, equal confidence => direction ~ 0.0
    let ensemble_equal = InferenceEnsemble::new(adapters);
    let features = make_feature_vector();
    let pred_equal = ensemble_equal
        .predict(&features)
        .unwrap_or_else(|e| panic!("Equal-weight prediction failed: {e}"));
    assert!(
        pred_equal.direction.abs() < 0.01,
        "Equal weight/confidence opposing models should cancel out, got {}",
        pred_equal.direction
    );

    // With Bull weighted 3x heavier, direction should be strongly positive
    let adapters2: Vec<Box<dyn ModelInferenceAdapter>> = vec![
        Box::new(BullAdapter),
        Box::new(BearAdapter),
    ];
    let mut ensemble_weighted = InferenceEnsemble::new(adapters2);
    ensemble_weighted.set_weight("Bull", 3.0);
    ensemble_weighted.set_weight("Bear", 1.0);

    let pred_weighted = ensemble_weighted
        .predict(&features)
        .unwrap_or_else(|e| panic!("Weighted prediction failed: {e}"));
    assert!(
        pred_weighted.direction > 0.3,
        "Bull-weighted ensemble should have positive direction, got {}",
        pred_weighted.direction
    );
}

// ---------------------------------------------------------------------------
// Test 6: Order sizing from confidence
// ---------------------------------------------------------------------------

#[test]
fn test_order_sizing_from_confidence() {
    // Verify that different confidence levels produce proportional order sizes
    for (conf, expected_min, expected_max) in [
        (0.0, 0.0, 0.0),
        (0.5, 49.0, 51.0),
        (1.0, 99.0, 101.0),
    ] {
        let size = (conf * 100.0_f64).round();
        assert!(
            size >= expected_min && size <= expected_max,
            "Confidence {} -> size {}, expected [{}, {}]",
            conf,
            size,
            expected_min,
            expected_max
        );
    }
}
