//! E2E gap-fill validation against live Binance API (Issue #257, Tasks #39, #41).
//!
//! Tests the full gap detect → parallel fill → bar production pipeline using
//! real market data. Validates that parallel-fetched trades produce bit-exact
//! bars compared to sequential fetching (Oracle L4).
//!
//! Run: cargo nextest run -p opendeviationbar-providers --test gap_fill_e2e --no-fail-fast

use std::sync::Arc;

use opendeviationbar_core::{AggTrade, OpenDeviationBarProcessor};
use opendeviationbar_providers::binance::parallel_fetch::fetch_aggtrades_parallel;
use opendeviationbar_providers::binance::{AdaptiveRateLimiter, HistoricalDataLoader};

use std::fs::File;
use std::io::Write;

struct TestTelemetry {
    log: File,
}

impl TestTelemetry {
    fn new(test_name: &str) -> Self {
        let dir = "target/test-telemetry";
        std::fs::create_dir_all(dir).ok();
        let path = format!("{dir}/{test_name}.ndjson");
        Self {
            log: File::create(path).unwrap(),
        }
    }

    fn emit(&mut self, event: &str, data: serde_json::Value) {
        let entry = serde_json::json!({
            "ts": chrono::Utc::now().to_rfc3339(),
            "event": event,
            "data": data,
        });
        writeln!(self.log, "{}", serde_json::to_string(&entry).unwrap()).ok();
    }
}

/// Helper: produce bars from a trade sequence using a fresh processor.
fn produce_bars(
    trades: &[AggTrade],
    threshold_dbps: u32,
) -> Vec<opendeviationbar_core::OpenDeviationBar> {
    let mut processor = OpenDeviationBarProcessor::new(threshold_dbps).unwrap();
    let mut bars = Vec::new();
    for trade in trades {
        if let Ok(Some(bar)) = processor.process_single_trade(trade) {
            bars.push(bar);
        }
    }
    bars
}

/// Helper: fetch trades sequentially via Ariadne-style pagination.
async fn fetch_sequential(
    symbol: &str,
    from_id: i64,
    to_id: i64,
    limiter: Option<Arc<AdaptiveRateLimiter>>,
) -> Vec<AggTrade> {
    let loader = match &limiter {
        Some(rl) => HistoricalDataLoader::new(symbol).with_rate_limiter(Arc::clone(rl)),
        None => HistoricalDataLoader::new(symbol),
    };
    let mut trades = Vec::new();
    let mut cursor = from_id;
    while cursor < to_id {
        let page = loader.fetch_aggtrades_by_id(cursor, 1000).await.unwrap();
        if page.is_empty() {
            break;
        }
        cursor = page.last().unwrap().agg_trade_id + 1;
        for t in page {
            if t.agg_trade_id < to_id {
                trades.push(t);
            }
        }
    }
    trades
}

// ============================================================================
// Task #39: Gap detect → fill → verify E2E
// ============================================================================

#[tokio::test]
async fn test_gap_detect_fill_verify_pipeline() {
    let mut tel = TestTelemetry::new("gap_detect_fill_verify");
    let loader = HistoricalDataLoader::new("BTCUSDT");

    // Step 1: Get anchor trade ID (recent)
    let anchor = loader.fetch_latest_aggtrade().await.unwrap();
    let anchor_id = anchor.agg_trade_id;

    tel.emit("anchor", serde_json::json!({ "agg_trade_id": anchor_id }));

    // Step 2: Define a simulated gap of ~2000 trades
    let gap_start = anchor_id - 3000;
    let gap_end = anchor_id - 1000; // 2000-trade gap

    tel.emit(
        "simulated_gap",
        serde_json::json!({
            "gap_start": gap_start,
            "gap_end": gap_end,
            "gap_size": gap_end - gap_start,
        }),
    );

    // Step 3: Fill the gap via parallel fetch
    let limiter = Arc::new(AdaptiveRateLimiter::binance_default());
    let filled_trades =
        fetch_aggtrades_parallel("BTCUSDT", gap_start, gap_end, Some(limiter.clone()), None)
            .await
            .unwrap();

    tel.emit(
        "fill_result",
        serde_json::json!({
            "trades_fetched": filled_trades.len(),
            "first_id": filled_trades.first().map(|t| t.agg_trade_id),
            "last_id": filled_trades.last().map(|t| t.agg_trade_id),
        }),
    );

    assert!(!filled_trades.is_empty(), "should fill gap trades");
    assert_eq!(
        filled_trades.first().unwrap().agg_trade_id,
        gap_start,
        "fill should start at gap_start"
    );

    // Step 4: Verify trade-ID contiguity in filled trades
    let mut gaps_found = 0;
    for window in filled_trades.windows(2) {
        if window[1].agg_trade_id != window[0].agg_trade_id + 1 {
            gaps_found += 1;
        }
    }
    assert_eq!(gaps_found, 0, "filled trades should be contiguous");

    // Step 5: Produce bars and verify Stathera invariant
    let bars = produce_bars(&filled_trades, 250);

    tel.emit(
        "bars_produced",
        serde_json::json!({
            "bar_count": bars.len(),
            "first_bar_first_tid": bars.first().map(|b| b.first_agg_trade_id),
            "last_bar_last_tid": bars.last().map(|b| b.last_agg_trade_id),
        }),
    );

    // Stathera invariant: consecutive bars have contiguous trade-ID ranges
    for window in bars.windows(2) {
        assert_eq!(
            window[0].last_agg_trade_id + 1,
            window[1].first_agg_trade_id,
            "Stathera invariant violated: bar[i].last={} + 1 != bar[i+1].first={}",
            window[0].last_agg_trade_id,
            window[1].first_agg_trade_id
        );
    }
}

// ============================================================================
// Task #41: Oracle L4 — parallel vs sequential bit-exact verification
// ============================================================================

#[tokio::test]
async fn test_oracle_l4_parallel_vs_sequential_bars() {
    let mut tel = TestTelemetry::new("oracle_l4_parallel_vs_sequential");
    let limiter = Arc::new(AdaptiveRateLimiter::binance_default());
    let loader = HistoricalDataLoader::new("BTCUSDT");

    // Get anchor
    let anchor = loader.fetch_latest_aggtrade().await.unwrap();
    let from_id = anchor.agg_trade_id - 2500;
    let to_id = anchor.agg_trade_id - 500; // 2000-trade range

    tel.emit(
        "range",
        serde_json::json!({ "from_id": from_id, "to_id": to_id, "size": to_id - from_id }),
    );

    // Fetch via parallel
    let parallel_trades =
        fetch_aggtrades_parallel("BTCUSDT", from_id, to_id, Some(limiter.clone()), None)
            .await
            .unwrap();

    // Fetch via sequential
    let sequential_trades =
        fetch_sequential("BTCUSDT", from_id, to_id, Some(limiter.clone())).await;

    tel.emit(
        "trade_counts",
        serde_json::json!({
            "parallel": parallel_trades.len(),
            "sequential": sequential_trades.len(),
        }),
    );

    assert_eq!(
        parallel_trades.len(),
        sequential_trades.len(),
        "trade count mismatch: parallel={} vs sequential={}",
        parallel_trades.len(),
        sequential_trades.len()
    );

    // Verify trade-ID identity
    for (p, s) in parallel_trades.iter().zip(sequential_trades.iter()) {
        assert_eq!(p.agg_trade_id, s.agg_trade_id, "trade ID mismatch");
        assert_eq!(p.price, s.price, "price mismatch at tid={}", p.agg_trade_id);
        assert_eq!(
            p.volume, s.volume,
            "volume mismatch at tid={}",
            p.agg_trade_id
        );
        assert_eq!(
            p.is_buyer_maker, s.is_buyer_maker,
            "maker flag mismatch at tid={}",
            p.agg_trade_id
        );
    }

    // Produce bars from both trade sets
    for threshold in [250, 500, 1000] {
        let parallel_bars = produce_bars(&parallel_trades, threshold);
        let sequential_bars = produce_bars(&sequential_trades, threshold);

        tel.emit(
            "bar_comparison",
            serde_json::json!({
                "threshold": threshold,
                "parallel_bars": parallel_bars.len(),
                "sequential_bars": sequential_bars.len(),
            }),
        );

        assert_eq!(
            parallel_bars.len(),
            sequential_bars.len(),
            "bar count mismatch at threshold={threshold}: parallel={} vs sequential={}",
            parallel_bars.len(),
            sequential_bars.len()
        );

        // Bit-exact bar comparison (OHLCV + trade-ID ranges)
        for (i, (pb, sb)) in parallel_bars.iter().zip(sequential_bars.iter()).enumerate() {
            assert_eq!(
                pb.open, sb.open,
                "open mismatch at bar {i} threshold={threshold}"
            );
            assert_eq!(
                pb.high, sb.high,
                "high mismatch at bar {i} threshold={threshold}"
            );
            assert_eq!(
                pb.low, sb.low,
                "low mismatch at bar {i} threshold={threshold}"
            );
            assert_eq!(
                pb.close, sb.close,
                "close mismatch at bar {i} threshold={threshold}"
            );
            assert_eq!(
                pb.volume, sb.volume,
                "volume mismatch at bar {i} threshold={threshold}"
            );
            assert_eq!(
                pb.first_agg_trade_id, sb.first_agg_trade_id,
                "first_tid mismatch at bar {i} threshold={threshold}"
            );
            assert_eq!(
                pb.last_agg_trade_id, sb.last_agg_trade_id,
                "last_tid mismatch at bar {i} threshold={threshold}"
            );
        }
    }
}

#[tokio::test]
async fn test_oracle_l4_stathera_post_fill() {
    // Verify Stathera invariant holds after parallel fill at multiple thresholds
    let mut tel = TestTelemetry::new("oracle_l4_stathera_post_fill");
    let limiter = Arc::new(AdaptiveRateLimiter::binance_default());
    let loader = HistoricalDataLoader::new("BTCUSDT");

    let anchor = loader.fetch_latest_aggtrade().await.unwrap();
    let from_id = anchor.agg_trade_id - 5000;
    let to_id = anchor.agg_trade_id - 1000;

    let trades = fetch_aggtrades_parallel("BTCUSDT", from_id, to_id, Some(limiter), None)
        .await
        .unwrap();

    for threshold in [250, 500, 750, 1000] {
        let bars = produce_bars(&trades, threshold);

        let stathera_violations: Vec<_> = bars
            .windows(2)
            .enumerate()
            .filter(|(_, w)| w[0].last_agg_trade_id + 1 != w[1].first_agg_trade_id)
            .map(|(i, w)| {
                serde_json::json!({
                    "bar_index": i,
                    "last_tid": w[0].last_agg_trade_id,
                    "next_first_tid": w[1].first_agg_trade_id,
                    "delta": w[1].first_agg_trade_id - w[0].last_agg_trade_id,
                })
            })
            .collect();

        tel.emit(
            "stathera_check",
            serde_json::json!({
                "threshold": threshold,
                "bar_count": bars.len(),
                "violations": stathera_violations.len(),
            }),
        );

        assert_eq!(
            stathera_violations.len(),
            0,
            "Stathera violations at threshold={threshold}: {stathera_violations:?}"
        );
    }
}
