//! Comprehensive Asset Selection Tests
//!
//! Tests for Trading Agent Service asset selection module with ML integration.
//! Validates multi-factor scoring (ML 40%, momentum 30%, value 20%, liquidity 10%).

use std::collections::HashMap;
use trading_agent_service::assets::*;

/// Test asset selection with correct factor weights
mod factor_weight_tests {
    use super::*;

    #[test]
    fn test_ml_score_weight_40_percent() {
        // ML score should contribute 40% to composite score
        let asset = create_test_asset_score(
            "ES.FUT", 1.0, // ml_score = 100%
            0.0, // momentum_score = 0%
            0.0, // value_score = 0%
            0.0, // quality_score (liquidity) = 0%
        );

        // Expected: 1.0 * 0.40 = 0.40
        assert!(
            (asset.composite_score - 0.40).abs() < 0.001,
            "ML score should contribute 40% to composite. Expected 0.40, got {}",
            asset.composite_score
        );
    }

    #[test]
    fn test_momentum_score_weight_30_percent() {
        // Momentum score should contribute 30% to composite score
        let asset = create_test_asset_score(
            "NQ.FUT", 0.0, // ml_score = 0%
            1.0, // momentum_score = 100%
            0.0, // value_score = 0%
            0.0, // quality_score = 0%
        );

        // Expected: 1.0 * 0.30 = 0.30
        assert!(
            (asset.composite_score - 0.30).abs() < 0.001,
            "Momentum score should contribute 30% to composite. Expected 0.30, got {}",
            asset.composite_score
        );
    }

    #[test]
    fn test_value_score_weight_20_percent() {
        // Value score should contribute 20% to composite score
        let asset = create_test_asset_score(
            "ZN.FUT", 0.0, // ml_score = 0%
            0.0, // momentum_score = 0%
            1.0, // value_score = 100%
            0.0, // quality_score = 0%
        );

        // Expected: 1.0 * 0.20 = 0.20
        assert!(
            (asset.composite_score - 0.20).abs() < 0.001,
            "Value score should contribute 20% to composite. Expected 0.20, got {}",
            asset.composite_score
        );
    }

    #[test]
    fn test_liquidity_score_weight_10_percent() {
        // Liquidity (quality_score) should contribute 10% to composite score
        let asset = create_test_asset_score(
            "6E.FUT", 0.0, // ml_score = 0%
            0.0, // momentum_score = 0%
            0.0, // value_score = 0%
            1.0, // quality_score (liquidity) = 100%
        );

        // Expected: 1.0 * 0.10 = 0.10
        assert!(
            (asset.composite_score - 0.10).abs() < 0.001,
            "Liquidity score should contribute 10% to composite. Expected 0.10, got {}",
            asset.composite_score
        );
    }

    #[test]
    fn test_composite_score_all_factors() {
        // Test all factors contributing together
        let asset = create_test_asset_score(
            "CL.FUT", 0.9,  // ml_score = 90%
            0.85, // momentum_score = 85%
            0.75, // value_score = 75%
            0.95, // quality_score (liquidity) = 95%
        );

        // Expected: 0.9*0.40 + 0.85*0.30 + 0.75*0.20 + 0.95*0.10
        //         = 0.36 + 0.255 + 0.15 + 0.095 = 0.86
        let expected = 0.9 * 0.40 + 0.85 * 0.30 + 0.75 * 0.20 + 0.95 * 0.10;
        assert!(
            (asset.composite_score - expected).abs() < 0.001,
            "Composite score calculation incorrect. Expected {}, got {}",
            expected,
            asset.composite_score
        );
    }

    #[test]
    fn test_zero_ml_score_still_computes() {
        // Asset with zero ML score should still get score from other factors
        let asset = create_test_asset_score(
            "GC.FUT", 0.0, // ml_score = 0%
            0.8, // momentum_score = 80%
            0.7, // value_score = 70%
            0.9, // quality_score = 90%
        );

        // Expected: 0.0*0.40 + 0.8*0.30 + 0.7*0.20 + 0.9*0.10 = 0.47
        let expected = 0.8 * 0.30 + 0.7 * 0.20 + 0.9 * 0.10;
        assert!(
            (asset.composite_score - expected).abs() < 0.001,
            "Zero ML score should still compute composite. Expected {}, got {}",
            expected,
            asset.composite_score
        );
    }
}

/// Test ML integration and prediction influence
mod ml_integration_tests {
    use super::*;

    #[test]
    fn test_ml_predictions_actually_used() {
        // Verify that ML predictions influence asset selection
        let ml_predictions = create_mock_ml_predictions();

        let asset_high_ml = create_asset_with_ml(
            "ES.FUT",
            &ml_predictions,
            0.5, // momentum
            0.5, // value
            0.5, // liquidity
        );

        let asset_low_ml = create_asset_with_ml(
            "NQ.FUT",
            &ml_predictions,
            0.5, // momentum
            0.5, // value
            0.5, // liquidity
        );

        // ES.FUT has ML confidence 0.9, NQ.FUT has 0.3
        // With same other factors, ES.FUT should score higher due to ML
        assert!(
            asset_high_ml.composite_score > asset_low_ml.composite_score,
            "Asset with higher ML confidence should score higher. ES: {}, NQ: {}",
            asset_high_ml.composite_score,
            asset_low_ml.composite_score
        );

        // Calculate expected difference (40% weight on ML)
        let ml_diff = (0.9 - 0.3) * 0.40;
        let score_diff = asset_high_ml.composite_score - asset_low_ml.composite_score;
        assert!(
            (score_diff - ml_diff).abs() < 0.001,
            "ML score difference should be reflected in composite. Expected diff {}, got {}",
            ml_diff,
            score_diff
        );
    }

    #[test]
    fn test_multi_model_ensemble_scoring() {
        // Test that multiple model scores are aggregated correctly
        let mut model_scores = HashMap::new();
        model_scores.insert("DQN".to_string(), 0.8);
        model_scores.insert("PPO".to_string(), 0.9);
        model_scores.insert("MAMBA2".to_string(), 0.85);
        model_scores.insert("TFT".to_string(), 0.75);

        let asset = create_test_asset_with_models(
            "ES.FUT",
            model_scores.clone(),
            0.7, // momentum
            0.6, // value
            0.9, // liquidity
        );

        // ML score should be average of model scores
        let expected_ml_score = (0.8 + 0.9 + 0.85 + 0.75) / 4.0;
        assert!(
            (asset.ml_score - expected_ml_score).abs() < 0.001,
            "ML score should be average of model scores. Expected {}, got {}",
            expected_ml_score,
            asset.ml_score
        );

        // Verify model_scores map is populated
        assert_eq!(asset.model_scores.len(), 4);
        assert_eq!(asset.model_scores.get("DQN"), Some(&0.8));
        assert_eq!(asset.model_scores.get("MAMBA2"), Some(&0.85));
    }

    #[test]
    fn test_ml_confidence_weighting() {
        // High confidence ML signal should have more impact than low confidence
        let high_confidence = create_test_asset_score("ES.FUT", 0.9, 0.5, 0.5, 0.5);
        let medium_confidence = create_test_asset_score("NQ.FUT", 0.6, 0.5, 0.5, 0.5);
        let low_confidence = create_test_asset_score("ZN.FUT", 0.3, 0.5, 0.5, 0.5);

        // Scores should decrease with ML confidence
        assert!(high_confidence.composite_score > medium_confidence.composite_score);
        assert!(medium_confidence.composite_score > low_confidence.composite_score);

        // Verify exact contribution (40% weight)
        let high_vs_low_diff = (0.9 - 0.3) * 0.40;
        let actual_diff = high_confidence.composite_score - low_confidence.composite_score;
        assert!(
            (actual_diff - high_vs_low_diff).abs() < 0.001,
            "ML confidence difference should be 40% of score difference"
        );
    }

    #[test]
    fn test_missing_ml_prediction_fallback() {
        // Test behavior when ML predictions are unavailable
        let asset = create_test_asset_score(
            "UNKNOWN.FUT",
            0.0, // No ML prediction available
            0.7, // momentum
            0.6, // value
            0.8, // liquidity
        );

        // Should still compute score from other factors
        let expected = 0.0 * 0.40 + 0.7 * 0.30 + 0.6 * 0.20 + 0.8 * 0.10;
        assert!(
            (asset.composite_score - expected).abs() < 0.001,
            "Should handle missing ML predictions gracefully"
        );
    }
}

/// Test ranking algorithm
mod ranking_algorithm_tests {
    use super::*;

    #[test]
    fn test_top_n_selection() {
        let assets = vec![
            create_test_asset_score("ES.FUT", 0.9, 0.8, 0.7, 0.95), // High score
            create_test_asset_score("NQ.FUT", 0.7, 0.6, 0.5, 0.85), // Medium score
            create_test_asset_score("ZN.FUT", 0.5, 0.4, 0.3, 0.75), // Low score
            create_test_asset_score("6E.FUT", 0.8, 0.7, 0.6, 0.90), // High-medium score
            create_test_asset_score("CL.FUT", 0.3, 0.2, 0.1, 0.65), // Very low score
        ];

        let selected = select_top_n_assets(assets, 3);

        assert_eq!(selected.len(), 3, "Should select exactly 3 assets");

        // Verify ordering (highest to lowest)
        assert_eq!(
            selected[0].symbol, "ES.FUT",
            "Highest score should be first"
        );
        assert_eq!(
            selected[1].symbol, "6E.FUT",
            "Second highest should be second"
        );
        assert_eq!(
            selected[2].symbol, "NQ.FUT",
            "Third highest should be third"
        );

        // Verify scores are descending
        assert!(selected[0].composite_score > selected[1].composite_score);
        assert!(selected[1].composite_score > selected[2].composite_score);
    }

    #[test]
    fn test_ranking_consistency() {
        // Same assets should always rank the same way
        let assets1 = create_test_asset_portfolio();
        let assets2 = create_test_asset_portfolio();

        let selected1 = select_top_n_assets(assets1, 5);
        let selected2 = select_top_n_assets(assets2, 5);

        assert_eq!(selected1.len(), selected2.len());
        for (a1, a2) in selected1.iter().zip(selected2.iter()) {
            assert_eq!(a1.symbol, a2.symbol, "Ranking should be deterministic");
            assert_eq!(a1.composite_score, a2.composite_score);
        }
    }

    #[test]
    fn test_ranking_with_tied_scores() {
        // Test behavior when multiple assets have identical scores
        let assets = vec![
            create_test_asset_score("ES.FUT", 0.8, 0.7, 0.6, 0.9),
            create_test_asset_score("NQ.FUT", 0.8, 0.7, 0.6, 0.9), // Exact tie
            create_test_asset_score("ZN.FUT", 0.9, 0.8, 0.7, 0.95),
        ];

        let selected = select_top_n_assets(assets, 2);

        assert_eq!(selected.len(), 2);
        // Highest unique score should be selected
        assert_eq!(selected[0].symbol, "ZN.FUT");

        // Tied assets should be stable (alphabetical or insert order)
        assert!(
            selected[1].symbol == "ES.FUT" || selected[1].symbol == "NQ.FUT",
            "One of the tied assets should be selected"
        );
    }

    #[test]
    fn test_empty_asset_list() {
        let assets: Vec<AssetScore> = vec![];
        let selected = select_top_n_assets(assets, 3);

        assert_eq!(selected.len(), 0, "Empty input should return empty output");
    }

    #[test]
    fn test_select_more_than_available() {
        let assets = vec![
            create_test_asset_score("ES.FUT", 0.9, 0.8, 0.7, 0.95),
            create_test_asset_score("NQ.FUT", 0.7, 0.6, 0.5, 0.85),
        ];

        let selected = select_top_n_assets(assets, 10);

        assert_eq!(
            selected.len(),
            2,
            "Should return all available assets when N > available"
        );
    }
}

/// Test edge cases and error handling
mod edge_case_tests {
    use super::*;

    #[test]
    fn test_negative_scores_rejected() {
        // Negative scores should be clamped or rejected
        let asset = create_test_asset_score("ES.FUT", -0.5, 0.5, 0.5, 0.5);

        // ML score should be clamped to 0.0-1.0 range
        assert!(
            asset.ml_score >= 0.0 && asset.ml_score <= 1.0,
            "Scores should be clamped to valid range"
        );
        assert!(
            asset.composite_score >= 0.0,
            "Composite score should never be negative"
        );
    }

    #[test]
    fn test_scores_above_one_clamped() {
        // Scores above 1.0 should be clamped
        let asset = create_test_asset_score("NQ.FUT", 1.5, 1.2, 0.8, 0.9);

        assert!(
            asset.ml_score <= 1.0,
            "ML score should be clamped to 1.0, got {}",
            asset.ml_score
        );
        assert!(
            asset.momentum_score <= 1.0,
            "Momentum score should be clamped to 1.0"
        );
        assert!(
            asset.composite_score <= 1.0,
            "Composite score should not exceed 1.0"
        );
    }

    #[test]
    fn test_all_zero_scores() {
        let asset = create_test_asset_score("ZN.FUT", 0.0, 0.0, 0.0, 0.0);

        assert_eq!(
            asset.composite_score, 0.0,
            "All zero scores should result in zero composite"
        );
    }

    #[test]
    fn test_nan_score_handling() {
        // Test that NaN values are handled gracefully
        let asset = create_test_asset_score("6E.FUT", f64::NAN, 0.5, 0.5, 0.5);

        assert!(
            !asset.composite_score.is_nan(),
            "Composite score should not be NaN even if input is NaN"
        );
    }

    #[test]
    fn test_infinity_score_handling() {
        // Test that infinity values are handled gracefully
        let asset = create_test_asset_score("CL.FUT", f64::INFINITY, 0.5, 0.5, 0.5);

        assert!(
            asset.composite_score.is_finite(),
            "Composite score should be finite even if input is infinity"
        );
        assert!(asset.composite_score <= 1.0, "Should be clamped to 1.0");
    }

    #[test]
    fn test_no_ml_predictions_available() {
        // Test asset selection when ML models are unavailable
        let assets = vec![
            create_test_asset_score("ES.FUT", 0.0, 0.9, 0.8, 0.95),
            create_test_asset_score("NQ.FUT", 0.0, 0.7, 0.6, 0.85),
            create_test_asset_score("ZN.FUT", 0.0, 0.8, 0.7, 0.90),
        ];

        let selected = select_top_n_assets(assets, 2);

        assert_eq!(selected.len(), 2, "Should still select assets without ML");

        // Should rank by other factors (momentum 30%, value 20%, liquidity 10%)
        assert_eq!(
            selected[0].symbol, "ES.FUT",
            "Best non-ML asset should be selected"
        );
    }

    #[test]
    fn test_all_negative_scores() {
        // Test that negative composite scores are handled
        let assets = vec![
            create_test_asset_score("ES.FUT", 0.1, 0.1, 0.1, 0.1),
            create_test_asset_score("NQ.FUT", 0.05, 0.05, 0.05, 0.05),
            create_test_asset_score("ZN.FUT", 0.15, 0.15, 0.15, 0.15),
        ];

        let selected = select_top_n_assets(assets, 2);

        assert_eq!(selected.len(), 2, "Should select even with low scores");
        // Highest (least negative) should be first
        assert_eq!(selected[0].symbol, "ZN.FUT");
        assert_eq!(selected[1].symbol, "ES.FUT");
    }
}

/// Test performance characteristics
mod performance_tests {
    use super::*;
    use std::time::Instant;

    #[test]
    fn test_selection_performance_100_assets() {
        // Test that asset selection completes within 2s for 100 assets
        let assets = (0..100)
            .map(|i| {
                create_test_asset_score(
                    &format!("ASSET{}.FUT", i),
                    rand_score(),
                    rand_score(),
                    rand_score(),
                    rand_score(),
                )
            })
            .collect();

        let start = Instant::now();
        let selected = select_top_n_assets(assets, 20);
        let duration = start.elapsed();

        assert_eq!(selected.len(), 20);
        assert!(
            duration.as_secs() < 2,
            "Selection should complete within 2s, took {:?}",
            duration
        );
    }

    #[test]
    fn test_scoring_performance() {
        // Test that individual score calculation is fast
        let start = Instant::now();

        for i in 0..1000 {
            let _ = create_test_asset_score(
                &format!("ASSET{}.FUT", i),
                rand_score(),
                rand_score(),
                rand_score(),
                rand_score(),
            );
        }

        let duration = start.elapsed();

        assert!(
            duration.as_millis() < 100,
            "1000 score calculations should take < 100ms, took {:?}",
            duration
        );
    }

    #[test]
    fn test_ranking_performance() {
        // Test ranking performance with large dataset
        let assets: Vec<_> = (0..1000)
            .map(|i| {
                create_test_asset_score(
                    &format!("ASSET{}.FUT", i),
                    rand_score(),
                    rand_score(),
                    rand_score(),
                    rand_score(),
                )
            })
            .collect();

        let start = Instant::now();
        let selected = select_top_n_assets(assets, 50);
        let duration = start.elapsed();

        assert_eq!(selected.len(), 50);
        assert!(
            duration.as_millis() < 500,
            "Ranking 1000 assets should take < 500ms, took {:?}",
            duration
        );
    }
}

/// Test integration with real market data scenarios
mod market_scenario_tests {
    use super::*;

    #[test]
    fn test_high_volatility_market() {
        // In high volatility, momentum scores should be higher
        let assets = vec![
            create_test_asset_score("ES.FUT", 0.7, 0.9, 0.5, 0.8), // High momentum
            create_test_asset_score("NQ.FUT", 0.7, 0.3, 0.8, 0.8), // High value
            create_test_asset_score("ZN.FUT", 0.7, 0.5, 0.5, 0.9), // High liquidity
        ];

        let selected = select_top_n_assets(assets, 3);

        // ES.FUT should rank highest due to high momentum (30% weight)
        assert_eq!(selected[0].symbol, "ES.FUT");
    }

    #[test]
    fn test_mean_reversion_scenario() {
        // In mean reversion, value scores matter more
        let assets = vec![
            create_test_asset_score("ES.FUT", 0.6, 0.3, 0.9, 0.7), // High value
            create_test_asset_score("NQ.FUT", 0.6, 0.9, 0.3, 0.7), // High momentum
            create_test_asset_score("ZN.FUT", 0.6, 0.5, 0.5, 0.9), // Balanced
        ];

        let selected = select_top_n_assets(assets, 3);

        // ES.FUT should be competitive due to value score (20% weight)
        assert!(selected.iter().any(|a| a.symbol == "ES.FUT"));
    }

    #[test]
    fn test_low_liquidity_environment() {
        // When liquidity is scarce, liquidity score becomes critical
        let assets = vec![
            create_test_asset_score("ES.FUT", 0.7, 0.7, 0.7, 0.95), // High liquidity
            create_test_asset_score("NQ.FUT", 0.8, 0.8, 0.8, 0.3),  // Low liquidity
            create_test_asset_score("ZN.FUT", 0.75, 0.75, 0.75, 0.6), // Medium liquidity
        ];

        let selected = select_top_n_assets(assets, 3);

        // Despite NQ having slightly higher scores, ES should be preferred for liquidity
        // But 10% weight is small, so NQ might still win overall
        // Let's verify composite calculation
        let _es_composite = 0.7 * 0.4 + 0.7 * 0.3 + 0.7 * 0.2 + 0.95 * 0.1; // = 0.725
        let _nq_composite = 0.8 * 0.4 + 0.8 * 0.3 + 0.8 * 0.2 + 0.3 * 0.1; // = 0.75

        // NQ should still rank higher despite low liquidity (80% vs 70% on other factors)
        assert_eq!(selected[0].symbol, "NQ.FUT");
    }

    #[test]
    fn test_ml_disagreement_scenario() {
        // When ML models disagree, use average
        let mut model_scores_bullish = HashMap::new();
        model_scores_bullish.insert("DQN".to_string(), 0.9);
        model_scores_bullish.insert("PPO".to_string(), 0.85);
        model_scores_bullish.insert("MAMBA2".to_string(), 0.2);
        model_scores_bullish.insert("TFT".to_string(), 0.3);

        let asset = create_test_asset_with_models("ES.FUT", model_scores_bullish, 0.7, 0.6, 0.8);

        // Average: (0.9 + 0.85 + 0.2 + 0.3) / 4 = 0.5625
        let expected_ml = (0.9 + 0.85 + 0.2 + 0.3) / 4.0;
        assert!(
            (asset.ml_score - expected_ml).abs() < 0.001,
            "Should average disagreeing models"
        );
    }
}

// =============================================================================
// Test Helpers
// =============================================================================

/// Create a test asset score with specified factor values
fn create_test_asset_score(
    symbol: &str,
    ml_score: f64,
    momentum_score: f64,
    value_score: f64,
    quality_score: f64,
) -> AssetScore {
    // Use the actual implementation which handles NaN/infinity correctly
    AssetScore::new(
        symbol.to_string(),
        ml_score,
        momentum_score,
        value_score,
        quality_score,
    )
}

/// Create asset with ML predictions
fn create_asset_with_ml(
    symbol: &str,
    ml_predictions: &HashMap<String, f64>,
    momentum: f64,
    value: f64,
    liquidity: f64,
) -> AssetScore {
    let ml_score = ml_predictions.get(symbol).copied().unwrap_or(0.0);
    create_test_asset_score(symbol, ml_score, momentum, value, liquidity)
}

/// Create asset with multiple model scores
fn create_test_asset_with_models(
    symbol: &str,
    model_scores: HashMap<String, f64>,
    momentum: f64,
    value: f64,
    liquidity: f64,
) -> AssetScore {
    // Average model scores for ML score
    let ml_score = if model_scores.is_empty() {
        0.0
    } else {
        model_scores.values().sum::<f64>() / model_scores.len() as f64
    };

    let mut asset = create_test_asset_score(symbol, ml_score, momentum, value, liquidity);
    asset.model_scores = model_scores;
    asset
}

/// Mock ML predictions
fn create_mock_ml_predictions() -> HashMap<String, f64> {
    let mut predictions = HashMap::new();
    predictions.insert("ES.FUT".to_string(), 0.9);
    predictions.insert("NQ.FUT".to_string(), 0.3);
    predictions.insert("ZN.FUT".to_string(), 0.7);
    predictions.insert("6E.FUT".to_string(), 0.6);
    predictions.insert("CL.FUT".to_string(), 0.8);
    predictions
}

/// Create test portfolio of assets
fn create_test_asset_portfolio() -> Vec<AssetScore> {
    vec![
        create_test_asset_score("ES.FUT", 0.9, 0.8, 0.7, 0.95),
        create_test_asset_score("NQ.FUT", 0.7, 0.6, 0.5, 0.85),
        create_test_asset_score("ZN.FUT", 0.8, 0.7, 0.6, 0.90),
        create_test_asset_score("6E.FUT", 0.6, 0.5, 0.4, 0.80),
        create_test_asset_score("CL.FUT", 0.85, 0.75, 0.65, 0.88),
        create_test_asset_score("GC.FUT", 0.5, 0.4, 0.3, 0.75),
    ]
}

/// Select top N assets by composite score
fn select_top_n_assets(assets: Vec<AssetScore>, n: usize) -> Vec<AssetScore> {
    let selector = AssetSelector::new();
    selector.select_top_n(assets, n)
}

/// Generate random score in 0.0-1.0 range
fn rand_score() -> f64 {
    use std::collections::hash_map::RandomState;
    use std::hash::BuildHasher;

    let hasher = RandomState::new();
    let hash = hasher.hash_one(std::time::SystemTime::now());

    (hash % 1000) as f64 / 1000.0
}

#[cfg(test)]
mod integration_tests {
    use super::*;

    #[test]
    fn test_end_to_end_asset_selection() {
        // Full pipeline test: ML predictions -> scoring -> ranking -> selection
        let ml_predictions = create_mock_ml_predictions();

        let assets = vec![
            create_asset_with_ml("ES.FUT", &ml_predictions, 0.8, 0.7, 0.95),
            create_asset_with_ml("NQ.FUT", &ml_predictions, 0.6, 0.5, 0.85),
            create_asset_with_ml("ZN.FUT", &ml_predictions, 0.7, 0.6, 0.90),
            create_asset_with_ml("6E.FUT", &ml_predictions, 0.5, 0.4, 0.80),
            create_asset_with_ml("CL.FUT", &ml_predictions, 0.75, 0.65, 0.88),
        ];

        let selected = select_top_n_assets(assets, 3);

        assert_eq!(selected.len(), 3, "Should select exactly 3 assets");

        // Verify ML integration: ES.FUT has highest ML (0.9)
        assert_eq!(
            selected[0].symbol, "ES.FUT",
            "Highest ML score should rank first"
        );

        // Verify scoring factors are all considered
        for asset in &selected {
            assert!(asset.ml_score >= 0.0 && asset.ml_score <= 1.0);
            assert!(asset.momentum_score >= 0.0 && asset.momentum_score <= 1.0);
            assert!(asset.value_score >= 0.0 && asset.value_score <= 1.0);
            assert!(asset.quality_score >= 0.0 && asset.quality_score <= 1.0);
            assert!(asset.composite_score >= 0.0 && asset.composite_score <= 1.0);
        }

        // Verify descending order
        assert!(selected[0].composite_score >= selected[1].composite_score);
        assert!(selected[1].composite_score >= selected[2].composite_score);
    }

    #[test]
    fn test_factor_weight_verification() {
        // Verify that documented weights (40/30/20/10) are actually used
        let test_cases = vec![
            (1.0, 0.0, 0.0, 0.0, 0.40), // ML only
            (0.0, 1.0, 0.0, 0.0, 0.30), // Momentum only
            (0.0, 0.0, 1.0, 0.0, 0.20), // Value only
            (0.0, 0.0, 0.0, 1.0, 0.10), // Liquidity only
            (1.0, 1.0, 1.0, 1.0, 1.00), // All max
            (0.5, 0.5, 0.5, 0.5, 0.50), // All half
        ];

        for (ml, momentum, value, liquidity, expected) in test_cases {
            let asset = create_test_asset_score("TEST.FUT", ml, momentum, value, liquidity);
            assert!(
                (asset.composite_score - expected).abs() < 0.001,
                "Factor weights incorrect: expected {}, got {} for scores ({}, {}, {}, {})",
                expected,
                asset.composite_score,
                ml,
                momentum,
                value,
                liquidity
            );
        }
    }
}
