//! Validation: Test that SharedMLStrategy correctly passes features from
//! ProductionFeatureExtractorAdapter to model adapters.
//!
//! NOTE: ProductionFeatureExtractorAdapter currently produces 51 features
//! (43 base + 8 OFI placeholders). Full 225 features require MBP-10 data
//! and sequence-level feature engineering done at training time.

use anyhow::Result;
use chrono::Utc;
use common::ml_strategy::{MLModelAdapter, MLPrediction, SharedMLStrategy};
use ml::features::ProductionFeatureExtractorAdapter;

/// Mock adapter that captures features from predictions
struct FeatureCapturingAdapter {
    id: String,
}

impl FeatureCapturingAdapter {
    fn new(id: &str) -> Self {
        Self {
            id: id.to_string(),
        }
    }
}

impl MLModelAdapter for FeatureCapturingAdapter {
    fn predict(&self, features: &[f64]) -> Result<MLPrediction> {
        Ok(MLPrediction {
            model_id: self.id.clone(),
            prediction_value: 0.5,
            confidence: 1.0, // Always pass threshold so we can inspect features
            features: features.to_vec(),
            timestamp: Utc::now(),
            inference_latency_us: 10,
        })
    }

    fn model_id(&self) -> &str {
        &self.id
    }

    fn validate_prediction(&mut self, _prediction: &MLPrediction, _actual_outcome: bool) {}
}

#[tokio::test]
async fn test_production_extractor_feature_count() {
    let extractor = Box::new(ProductionFeatureExtractorAdapter::new());
    let strategy = SharedMLStrategy::new(
        extractor,
        vec![Box::new(FeatureCapturingAdapter::new("capture_v1"))],
        0.0,
    );

    // Warm up the feature extractor with historical data
    for i in 0..100 {
        let price = 100.0 + (i as f64 * 0.1);
        let volume = 1000.0 + (i as f64 * 10.0);
        let _ = strategy
            .get_ensemble_prediction(price, volume, Utc::now())
            .await;
    }

    let predictions = strategy
        .get_ensemble_prediction(100.0, 1000.0, Utc::now())
        .await
        .expect("Should extract features successfully");

    assert!(
        !predictions.is_empty(),
        "Should have at least one prediction"
    );

    let features = &predictions[0].features;

    // ProductionFeatureExtractorAdapter produces 51 features (43 base + 8 OFI placeholders)
    assert_eq!(
        features.len(),
        51,
        "ProductionFeatureExtractorAdapter should produce 51 features, got {}",
        features.len()
    );

    // All features must be finite (no NaN, no Inf)
    for (i, f) in features.iter().enumerate() {
        assert!(f.is_finite(), "Feature at index {} is not finite: {}", i, f);
    }
}

#[tokio::test]
async fn test_feature_extraction_all_finite() {
    let extractor = Box::new(ProductionFeatureExtractorAdapter::new());
    let strategy = SharedMLStrategy::new(
        extractor,
        vec![Box::new(FeatureCapturingAdapter::new("capture_v1"))],
        0.0,
    );

    // Warm up
    for i in 0..100 {
        let price = 100.0 + (i as f64 * 0.1);
        let volume = 1000.0 + (i as f64 * 10.0);
        let _ = strategy
            .get_ensemble_prediction(price, volume, Utc::now())
            .await;
    }

    let predictions = strategy
        .get_ensemble_prediction(100.0, 1000.0, Utc::now())
        .await
        .expect("Should extract features successfully");

    let features = &predictions[0].features;

    // All 51 features should be finite
    for i in 0..features.len() {
        let value = features.get(i);
        assert!(value.is_some(), "Feature at index {} is missing", i);
        assert!(
            value.unwrap().is_finite(),
            "Feature at index {} is not finite: {}",
            i,
            value.unwrap()
        );
    }
}
