//! WS reconnect gap-fill integration tests (Issue #257, Task #40).
//!
//! Tests the full gap detection → event emission → command channel → fill pipeline
//! using real Binance API data (REST only — no WS connection needed for these tests).
//!
//! The tests simulate what happens when a WebSocket reconnection creates a gap:
//! 1. TradeIdGapDetector detects the gap
//! 2. GapEvent is emitted
//! 3. GapFillCommand is dispatched
//! 4. Parallel fetch recovers the trades
//! 5. Trades are processed through threshold processors
//!
//! Run: cargo nextest run -p opendeviationbar-streaming --test reconnect_gap_fill --all-features --no-fail-fast

use std::sync::Arc;
use tokio::sync::{mpsc, oneshot};

use opendeviationbar_core::OpenDeviationBarProcessor;
use opendeviationbar_streaming::gap::{
    GapEvent, GapFillCommand, GapFillResult, TradeIdGapDetector, gap_fill_channel,
};

#[cfg(feature = "binance-integration")]
use opendeviationbar_providers::binance::parallel_fetch::fetch_aggtrades_parallel;
#[cfg(feature = "binance-integration")]
use opendeviationbar_providers::binance::{AdaptiveRateLimiter, HistoricalDataLoader};

// ============================================================================
// Task #40: WS reconnect gap-fill simulation
// ============================================================================

#[cfg(feature = "binance-integration")]
#[tokio::test]
async fn test_reconnect_gap_detect_fill_pipeline() {
    // Simulates: WS disconnects → reconnects with gap → detector fires → fill via REST
    let (gap_event_tx, mut gap_event_rx) = mpsc::channel::<GapEvent>(16);
    let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(gap_event_tx));
    let limiter = Arc::new(AdaptiveRateLimiter::binance_default());

    // Step 1: Seed detector with a known trade ID (simulates pre-disconnect state)
    let loader = HistoricalDataLoader::new("BTCUSDT");
    let latest = loader.fetch_latest_aggtrade().await.unwrap();
    let pre_disconnect_id = latest.agg_trade_id - 2000;
    detector.seed(pre_disconnect_id);

    // Step 2: Simulate reconnection — first WS trade has a gap
    let post_reconnect_id = latest.agg_trade_id - 500;
    let gap_result = detector.check(post_reconnect_id);

    assert!(gap_result.is_some(), "gap should be detected on reconnect");
    let (last_known, gap_size) = gap_result.unwrap();
    assert_eq!(last_known, pre_disconnect_id);
    assert_eq!(gap_size, post_reconnect_id - pre_disconnect_id - 1);

    // Step 3: Verify GapEvent was emitted
    let event = gap_event_rx.try_recv().unwrap();
    assert_eq!(&*event.symbol, "BTCUSDT");
    assert_eq!(event.expected_tid, pre_disconnect_id + 1);
    assert_eq!(event.actual_tid, post_reconnect_id);
    assert!(!event.filled_inline);

    // Step 4: Fill the gap via parallel fetch (simulates puck_fill)
    let from_id = pre_disconnect_id + 1;
    let to_id = post_reconnect_id;
    let trades = fetch_aggtrades_parallel("BTCUSDT", from_id, to_id, Some(limiter), None)
        .await
        .unwrap();

    assert!(!trades.is_empty(), "should recover gap trades");

    // Step 5: Verify trade-ID contiguity of recovered trades
    assert_eq!(trades.first().unwrap().agg_trade_id, from_id);
    for window in trades.windows(2) {
        assert_eq!(
            window[1].agg_trade_id,
            window[0].agg_trade_id + 1,
            "recovered trades not contiguous"
        );
    }

    // Step 6: Process through bars and verify Stathera
    let mut processor = OpenDeviationBarProcessor::new(250).unwrap();
    let mut bars = Vec::new();
    for trade in &trades {
        if let Ok(Some(bar)) = processor.process_single_trade(trade) {
            bars.push(bar);
        }
    }
    for window in bars.windows(2) {
        assert_eq!(
            window[0].last_agg_trade_id + 1,
            window[1].first_agg_trade_id,
            "Stathera violation after reconnect fill"
        );
    }

    // Step 7: Mark gap as filled and verify updated event
    detector.mark_filled(last_known, gap_size);
    let filled_event = gap_event_rx.try_recv().unwrap();
    assert!(filled_event.filled_inline, "event should be marked filled_inline");
}

#[cfg(feature = "binance-integration")]
#[tokio::test]
async fn test_command_channel_fill_with_live_data() {
    // Simulates Python-side fill_gap() dispatching through command channel
    let (cmd_tx, mut cmd_rx) = gap_fill_channel(16);
    let limiter = Arc::new(AdaptiveRateLimiter::binance_default());

    let loader = HistoricalDataLoader::new("BTCUSDT");
    let latest = loader.fetch_latest_aggtrade().await.unwrap();
    let from_tid = latest.agg_trade_id - 1500;
    let to_tid = latest.agg_trade_id - 500;

    // Caller sends command (simulates PyStreamManager.fill_gap())
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(GapFillCommand {
            symbol: Arc::from("BTCUSDT"),
            from_tid,
            to_tid,
            response_tx,
        })
        .await
        .unwrap();

    // Engine receives and processes (simulates symbol_task select! branch)
    let cmd = cmd_rx.recv().await.unwrap();
    assert_eq!(&*cmd.symbol, "BTCUSDT");

    let start = std::time::Instant::now();
    let trades = fetch_aggtrades_parallel(
        &cmd.symbol,
        cmd.from_tid,
        cmd.to_tid,
        Some(limiter),
        None,
    )
    .await
    .unwrap();
    let duration_ms = start.elapsed().as_millis() as u64;

    // Count bars produced
    let mut processor = OpenDeviationBarProcessor::new(250).unwrap();
    let mut bars_produced = 0usize;
    for trade in &trades {
        if let Ok(Some(_)) = processor.process_single_trade(trade) {
            bars_produced += 1;
        }
    }

    // Send result back
    let result = GapFillResult {
        trades_fetched: trades.len(),
        bars_produced,
        duration_ms,
        partial: false,
    };
    cmd.response_tx.send(result).unwrap();

    // Caller receives result
    let result = response_rx.await.unwrap();
    assert!(result.trades_fetched > 0, "should fetch trades");
    assert!(!result.partial, "should not be partial");
    assert!(
        result.duration_ms < 30_000,
        "fill should complete in <30s, took {}ms",
        result.duration_ms
    );
}

#[tokio::test]
async fn test_large_gap_deferred_as_partial() {
    // Gaps > MAX_GAP_FILL_TRADES should be deferred (marked partial)
    let max_gap_fill: i64 = 100_000;
    let (gap_event_tx, _) = mpsc::channel::<GapEvent>(16);
    let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(gap_event_tx));

    detector.seed(1_000_000);
    let result = detector.check(1_200_000); // 199,999 trade gap

    assert!(result.is_some());
    let (_, gap_size) = result.unwrap();
    assert!(
        gap_size > max_gap_fill,
        "gap should exceed MAX_GAP_FILL_TRADES"
    );

    // Engine would mark this as partial (defer to kintsugi)
    let partial = gap_size > max_gap_fill;
    assert!(partial, "large gap should be deferred as partial");
}

#[tokio::test]
async fn test_concurrent_gap_detection_multi_symbol() {
    // Multiple symbols detect gaps simultaneously — shared event channel
    let (tx, mut rx) = mpsc::channel::<GapEvent>(32);

    let symbols = ["BTCUSDT", "ETHUSDT", "SOLUSDT"];
    let mut detectors: Vec<_> = symbols
        .iter()
        .map(|s| TradeIdGapDetector::new(Arc::from(*s), Some(tx.clone())))
        .collect();

    // Seed all detectors
    for (i, d) in detectors.iter_mut().enumerate() {
        d.seed((i as i64 + 1) * 10_000);
    }

    // Simulate concurrent gaps
    for (i, d) in detectors.iter_mut().enumerate() {
        let gap_target = (i as i64 + 1) * 10_000 + 500;
        let result = d.check(gap_target);
        assert!(result.is_some(), "gap should be detected for {}", symbols[i]);
    }

    // Collect all events (non-blocking — detectors used try_send)
    let mut events = Vec::new();
    while let Ok(event) = rx.try_recv() {
        events.push(event);
    }

    assert_eq!(events.len(), 3, "should have 3 gap events");

    // Verify each symbol has an event
    let event_symbols: Vec<String> = events.iter().map(|e| e.symbol.to_string()).collect();
    for s in symbols {
        assert!(
            event_symbols.contains(&s.to_string()),
            "missing event for {s}"
        );
    }
}

#[tokio::test]
async fn test_gap_detector_state_after_fill() {
    // After a gap is filled, detector should track the new last_known_tid
    let (tx, _) = mpsc::channel::<GapEvent>(16);
    let mut detector = TradeIdGapDetector::new(Arc::from("BTCUSDT"), Some(tx));

    detector.seed(1000);

    // Gap detected
    let result = detector.check(1100);
    assert!(result.is_some());

    // After fill, update state to the filled range
    detector.update_last_tid(1099); // Filled up to 1099

    // Now the incoming WS trade (1100) should be contiguous
    assert!(
        detector.check(1100).is_none(),
        "after fill, 1100 should be contiguous with 1099"
    );

    // And the next trade should also be fine
    detector.update_last_tid(1100);
    assert!(detector.check(1101).is_none());
}
