#![allow(unexpected_cfgs)]
#![cfg(feature = "__trading_service_integration")]
//! Integration tests for ensemble audit logging
//!
//! These tests validate the PostgreSQL audit logging system for ensemble predictions,
//! including prediction inserts, P&L updates, and analysis queries.

use sqlx::PgPool;
use std::collections::HashMap;
use uuid::Uuid;

// Import ensemble types from ml crate
use ml::ensemble::{EnsembleDecision, ModelVote, TradingAction};

// Test database URL
fn get_test_db_url() -> String {
    std::env::var("DATABASE_URL").unwrap_or_else(|_| {
        "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string()
    })
}

#[tokio::test]
async fn test_ensemble_audit_logger_initialization() {
    // Create database pool
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Import audit logger (we'll need to add the module reference)
    // For now, we'll test the SQL schema directly

    // Verify tables exist
    let result = sqlx::query!(
        r#"
        SELECT tablename
        FROM pg_tables
        WHERE schemaname = 'public'
          AND tablename IN ('ensemble_predictions', 'model_performance_attribution', 'ab_test_experiments')
        "#
    )
    .fetch_all(&pool)
    .await
    .expect("Failed to query tables");

    assert_eq!(result.len(), 3, "All ensemble audit tables should exist");

    pool.close().await;
}

#[tokio::test]
async fn test_log_ensemble_prediction() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Create mock ensemble decision
    let mut model_votes = HashMap::new();
    model_votes.insert(
        "DQN".to_string(),
        ModelVote::new("DQN".to_string(), 0.8, 0.9, 0.33),
    );
    model_votes.insert(
        "PPO".to_string(),
        ModelVote::new("PPO".to_string(), 0.7, 0.85, 0.33),
    );
    model_votes.insert(
        "TFT".to_string(),
        ModelVote::new("TFT".to_string(), 0.6, 0.8, 0.34),
    );

    let decision = EnsembleDecision::new(TradingAction::Buy, 0.85, 0.7, 0.15, model_votes);

    // Insert prediction manually (simulating audit logger)
    let id = Uuid::new_v4();
    let symbol = "ES.FUT";

    let result = sqlx::query!(
        r#"
        INSERT INTO ensemble_predictions (
            id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate,
            dqn_signal, dqn_confidence, dqn_weight, dqn_vote,
            ppo_signal, ppo_confidence, ppo_weight, ppo_vote,
            tft_signal, tft_confidence, tft_weight, tft_vote
        ) VALUES (
            $1, $2, $3, $4, $5, $6,
            $7, $8, $9, $10,
            $11, $12, $13, $14,
            $15, $16, $17, $18
        )
        "#,
        id,
        symbol,
        "BUY",
        0.7,
        0.85,
        0.15,
        0.8,
        0.9,
        0.33,
        "BUY",
        0.7,
        0.85,
        0.33,
        "BUY",
        0.6,
        0.8,
        0.34,
        "BUY",
    )
    .execute(&pool)
    .await;

    assert!(result.is_ok(), "Prediction insert should succeed");

    // Verify prediction was inserted
    let record = sqlx::query!(
        r#"
        SELECT id, symbol, ensemble_action, ensemble_confidence, disagreement_rate
        FROM ensemble_predictions
        WHERE id = $1
        "#,
        id
    )
    .fetch_one(&pool)
    .await
    .expect("Failed to fetch prediction");

    assert_eq!(record.symbol, "ES.FUT");
    assert_eq!(record.ensemble_action, "BUY");
    assert!((record.ensemble_confidence - 0.85).abs() < 0.001);
    assert!((record.disagreement_rate - 0.15).abs() < 0.001);

    // Cleanup
    sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", id)
        .execute(&pool)
        .await
        .expect("Failed to cleanup");

    pool.close().await;
}

#[tokio::test]
async fn test_update_prediction_pnl() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Insert a test prediction
    let id = Uuid::new_v4();

    sqlx::query!(
        r#"
        INSERT INTO ensemble_predictions (
            id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate
        ) VALUES (
            $1, 'NQ.FUT', 'BUY', 0.5, 0.7, 0.2
        )
        "#,
        id
    )
    .execute(&pool)
    .await
    .expect("Failed to insert test prediction");

    // Update P&L (simulating trade close)
    let pnl_cents = 1250; // $12.50 profit
    let commission_cents = 25; // $0.25 commission
    let slippage_bps = 5; // 5 basis points

    let result = sqlx::query!(
        r#"
        UPDATE ensemble_predictions
        SET pnl = $2, commission = $3, slippage_bps = $4
        WHERE id = $1
        "#,
        id,
        pnl_cents,
        commission_cents,
        slippage_bps,
    )
    .execute(&pool)
    .await;

    assert!(result.is_ok(), "P&L update should succeed");

    // Verify update
    let record = sqlx::query!(
        r#"
        SELECT pnl, commission, slippage_bps
        FROM ensemble_predictions
        WHERE id = $1
        "#,
        id
    )
    .fetch_one(&pool)
    .await
    .expect("Failed to fetch updated prediction");

    assert_eq!(record.pnl, Some(pnl_cents));
    assert_eq!(record.commission, Some(commission_cents));
    assert_eq!(record.slippage_bps, Some(slippage_bps));

    // Cleanup
    sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", id)
        .execute(&pool)
        .await
        .expect("Failed to cleanup");

    pool.close().await;
}

#[tokio::test]
async fn test_model_performance_attribution() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Insert model performance record
    let id = Uuid::new_v4();
    let model_id = "DQN";
    let symbol = "ES.FUT";
    let window_hours = 24;
    let total_predictions = 100;
    let correct_predictions = 58;
    let accuracy = 0.58;
    let total_pnl_cents = 5420; // $54.20
    let sharpe_ratio = 1.85;
    let avg_weight = 0.33;
    let avg_confidence = 0.82;

    let result = sqlx::query!(
        r#"
        INSERT INTO model_performance_attribution (
            id, model_id, symbol, window_hours,
            total_predictions, correct_predictions, accuracy,
            total_pnl, sharpe_ratio,
            avg_weight, avg_confidence
        ) VALUES (
            $1, $2, $3, $4,
            $5, $6, $7,
            $8, $9,
            $10, $11
        )
        "#,
        id,
        model_id,
        symbol,
        window_hours,
        total_predictions,
        correct_predictions,
        accuracy,
        total_pnl_cents as i64,
        sharpe_ratio,
        avg_weight,
        avg_confidence,
    )
    .execute(&pool)
    .await;

    assert!(
        result.is_ok(),
        "Performance attribution insert should succeed"
    );

    // Verify insertion
    let record = sqlx::query!(
        r#"
        SELECT model_id, symbol, accuracy, sharpe_ratio, total_pnl
        FROM model_performance_attribution
        WHERE id = $1
        "#,
        id
    )
    .fetch_one(&pool)
    .await
    .expect("Failed to fetch performance record");

    assert_eq!(record.model_id, "DQN");
    assert_eq!(record.symbol, "ES.FUT");
    assert!((record.accuracy - 0.58).abs() < 0.001);
    assert_eq!(record.total_pnl, total_pnl_cents as i64);

    // Cleanup
    sqlx::query!(
        "DELETE FROM model_performance_attribution WHERE id = $1",
        id
    )
    .execute(&pool)
    .await
    .expect("Failed to cleanup");

    pool.close().await;
}

#[tokio::test]
async fn test_ab_test_experiment_tracking() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Create A/B test experiment
    let test_id = Uuid::new_v4();
    let test_name = format!("test_ensemble_vs_dqn_{}", test_id);

    let result = sqlx::query!(
        r#"
        INSERT INTO ab_test_experiments (
            test_id, test_name, description,
            control_variant, treatment_variant,
            traffic_split, min_sample_size, significance_level,
            status
        ) VALUES (
            $1, $2, 'Testing ensemble vs single DQN model',
            'DQN_ONLY', 'ENSEMBLE',
            0.5, 1000, 0.05,
            'running'
        )
        "#,
        test_id,
        test_name,
    )
    .execute(&pool)
    .await;

    assert!(result.is_ok(), "A/B test creation should succeed");

    // Insert predictions for control group
    for i in 0..5 {
        let pred_id = Uuid::new_v4();
        sqlx::query!(
            r#"
            INSERT INTO ensemble_predictions (
                id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate,
                ab_test_id, ab_group
            ) VALUES (
                $1, 'ES.FUT', 'BUY', 0.6, 0.75, 0.2,
                $2, 'control'
            )
            "#,
            pred_id,
            test_id,
        )
        .execute(&pool)
        .await
        .expect("Failed to insert control prediction");
    }

    // Insert predictions for treatment group
    for i in 0..5 {
        let pred_id = Uuid::new_v4();
        sqlx::query!(
            r#"
            INSERT INTO ensemble_predictions (
                id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate,
                ab_test_id, ab_group
            ) VALUES (
                $1, 'ES.FUT', 'BUY', 0.7, 0.85, 0.15,
                $2, 'treatment'
            )
            "#,
            pred_id,
            test_id,
        )
        .execute(&pool)
        .await
        .expect("Failed to insert treatment prediction");
    }

    // Query A/B test assignments
    let results = sqlx::query!(
        r#"
        SELECT ab_group, COUNT(*) as count
        FROM ensemble_predictions
        WHERE ab_test_id = $1
        GROUP BY ab_group
        "#,
        test_id
    )
    .fetch_all(&pool)
    .await
    .expect("Failed to query A/B test results");

    assert_eq!(
        results.len(),
        2,
        "Should have both control and treatment groups"
    );

    for record in results {
        let count = record.count.expect("Count should not be null");
        assert_eq!(count, 5, "Each group should have 5 predictions");
    }

    // Cleanup
    sqlx::query!(
        "DELETE FROM ensemble_predictions WHERE ab_test_id = $1",
        test_id
    )
    .execute(&pool)
    .await
    .expect("Failed to cleanup predictions");

    sqlx::query!(
        "DELETE FROM ab_test_experiments WHERE test_id = $1",
        test_id
    )
    .execute(&pool)
    .await
    .expect("Failed to cleanup experiment");

    pool.close().await;
}

#[tokio::test]
async fn test_batch_prediction_insert_performance() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    let batch_size = 100;
    let start = std::time::Instant::now();

    // Insert 100 predictions in a transaction
    let mut tx = pool.begin().await.expect("Failed to start transaction");

    for i in 0..batch_size {
        let id = Uuid::new_v4();
        sqlx::query!(
            r#"
            INSERT INTO ensemble_predictions (
                id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate,
                inference_latency_us, aggregation_latency_us
            ) VALUES (
                $1, 'ES.FUT', 'BUY', 0.6, 0.75, 0.2,
                42, 8
            )
            "#,
            id
        )
        .execute(&mut *tx)
        .await
        .expect("Failed to insert prediction in batch");
    }

    tx.commit().await.expect("Failed to commit transaction");

    let elapsed = start.elapsed();
    let throughput = batch_size as f64 / elapsed.as_secs_f64();

    println!(
        "Batch insert performance: {} predictions in {:.2}ms ({:.0} predictions/sec)",
        batch_size,
        elapsed.as_secs_f64() * 1000.0,
        throughput
    );

    // Performance assertion: Should handle >1000 predictions/sec
    assert!(
        throughput > 100.0,
        "Batch insert throughput should exceed 100 predictions/sec, got {:.0}",
        throughput
    );

    // Cleanup
    sqlx::query!(
        "DELETE FROM ensemble_predictions WHERE symbol = 'ES.FUT' AND inference_latency_us = 42"
    )
    .execute(&pool)
    .await
    .expect("Failed to cleanup batch");

    pool.close().await;
}

#[tokio::test]
async fn test_analysis_query_performance() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Test high disagreement query performance
    let start = std::time::Instant::now();

    let _results = sqlx::query!(
        r#"
        SELECT id, symbol, disagreement_rate, ensemble_action
        FROM ensemble_predictions
        WHERE disagreement_rate > 0.5
        ORDER BY disagreement_rate DESC
        LIMIT 100
        "#
    )
    .fetch_all(&pool)
    .await
    .expect("Failed to execute high disagreement query");

    let elapsed = start.elapsed();

    println!(
        "High disagreement query latency: {:.2}ms",
        elapsed.as_secs_f64() * 1000.0
    );

    // Performance assertion: Query should complete in <100ms
    assert!(
        elapsed.as_millis() < 100,
        "High disagreement query should complete in <100ms, took {}ms",
        elapsed.as_millis()
    );

    pool.close().await;
}

#[tokio::test]
async fn test_timescaledb_hypertable_functionality() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Verify hypertable was created for ensemble_predictions
    let result = sqlx::query!(
        r#"
        SELECT hypertable_name
        FROM timescaledb_information.hypertable
        WHERE hypertable_name IN ('ensemble_predictions', 'model_performance_attribution')
        "#
    )
    .fetch_all(&pool)
    .await
    .expect("Failed to query hypertables");

    assert_eq!(
        result.len(),
        2,
        "Both ensemble_predictions and model_performance_attribution should be hypertables"
    );

    pool.close().await;
}

#[tokio::test]
async fn test_feature_snapshot_jsonb() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Create feature snapshot JSON
    let feature_snapshot = serde_json::json!({
        "ohlcv": {
            "open": 5000.50,
            "high": 5010.25,
            "low": 4995.00,
            "close": 5005.75,
            "volume": 125000
        },
        "technical_indicators": {
            "rsi": 62.5,
            "macd": 12.3,
            "bollinger_upper": 5020.0,
            "bollinger_lower": 4990.0
        }
    });

    let id = Uuid::new_v4();

    // Insert prediction with feature snapshot
    let result = sqlx::query!(
        r#"
        INSERT INTO ensemble_predictions (
            id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate,
            feature_snapshot
        ) VALUES (
            $1, 'ES.FUT', 'BUY', 0.6, 0.75, 0.2,
            $2
        )
        "#,
        id,
        feature_snapshot,
    )
    .execute(&pool)
    .await;

    assert!(result.is_ok(), "Feature snapshot insert should succeed");

    // Query feature snapshot
    let record = sqlx::query!(
        r#"
        SELECT feature_snapshot
        FROM ensemble_predictions
        WHERE id = $1
        "#,
        id
    )
    .fetch_one(&pool)
    .await
    .expect("Failed to fetch feature snapshot");

    let snapshot = record
        .feature_snapshot
        .expect("Feature snapshot should not be null");
    let rsi = snapshot["technical_indicators"]["rsi"].as_f64();
    assert_eq!(rsi, Some(62.5), "RSI should match");

    // Cleanup
    sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", id)
        .execute(&pool)
        .await
        .expect("Failed to cleanup");

    pool.close().await;
}

#[tokio::test]
async fn test_continuous_aggregate_views() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Query hourly ensemble performance view
    let results = sqlx::query!(
        r#"
        SELECT bucket, symbol, prediction_count, avg_confidence
        FROM ensemble_performance_hourly
        ORDER BY bucket DESC
        LIMIT 10
        "#
    )
    .fetch_all(&pool)
    .await;

    // View should exist (even if empty)
    assert!(
        results.is_ok(),
        "Continuous aggregate view should be queryable"
    );

    // Query daily model performance view
    let results = sqlx::query!(
        r#"
        SELECT bucket, model_id, avg_accuracy, avg_sharpe_ratio
        FROM model_performance_daily
        ORDER BY bucket DESC
        LIMIT 10
        "#
    )
    .fetch_all(&pool)
    .await;

    assert!(
        results.is_ok(),
        "Daily model performance view should be queryable"
    );

    pool.close().await;
}

#[tokio::test]
async fn test_utility_functions() {
    let pool = PgPool::connect(&get_test_db_url())
        .await
        .expect("Failed to connect to test database");

    // Test get_top_models_24h function
    let results = sqlx::query!(
        r#"
        SELECT * FROM get_top_models_24h(NULL::VARCHAR, 5)
        "#
    )
    .fetch_all(&pool)
    .await;

    assert!(
        results.is_ok(),
        "get_top_models_24h function should execute"
    );

    // Test calculate_model_correlation_7d function
    let results = sqlx::query!(
        r#"
        SELECT * FROM calculate_model_correlation_7d(NULL::VARCHAR)
        "#
    )
    .fetch_all(&pool)
    .await;

    assert!(
        results.is_ok(),
        "calculate_model_correlation_7d function should execute"
    );

    // Test get_high_disagreement_events_24h function
    let results = sqlx::query!(
        r#"
        SELECT * FROM get_high_disagreement_events_24h(NULL::VARCHAR, 0.5, 100)
        "#
    )
    .fetch_all(&pool)
    .await;

    assert!(
        results.is_ok(),
        "get_high_disagreement_events_24h function should execute"
    );

    pool.close().await;
}
