//! Edge case validation for gap-fill against live Binance API (Issue #257, Task #42).
//!
//! Tests edge cases that can only be validated with real market data:
//! - Single-trade gaps
//! - Multi-symbol concurrent fills with shared rate limiter
//! - Large gaps with rate limiter budget tracking
//! - Dedup at page boundaries
//! - Empty range handling
//!
//! Run: cargo nextest run -p opendeviationbar-providers --test gap_fill_edge_cases --no-fail-fast

use std::sync::Arc;

use opendeviationbar_core::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();
    }
}

#[tokio::test]
async fn test_single_trade_gap_fill() {
    // Gap of exactly 1 trade — should use single-fetch path (no JoinSet)
    let mut tel = TestTelemetry::new("single_trade_gap_fill");
    let loader = HistoricalDataLoader::new("BTCUSDT");

    let anchor = loader.fetch_latest_aggtrade().await.unwrap();
    let from_id = anchor.agg_trade_id - 100;
    let to_id = from_id + 1; // Single trade

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

    tel.emit(
        "single_trade",
        serde_json::json!({
            "from_id": from_id,
            "to_id": to_id,
            "fetched": trades.len(),
            "tid": trades.first().map(|t| t.agg_trade_id),
        }),
    );

    assert_eq!(trades.len(), 1, "should fetch exactly 1 trade");
    assert_eq!(trades[0].agg_trade_id, from_id);
}

#[tokio::test]
async fn test_multi_symbol_concurrent_fill_shared_limiter() {
    // BTCUSDT + ETHUSDT gaps filled concurrently with a shared rate limiter
    let mut tel = TestTelemetry::new("multi_symbol_concurrent_fill");
    let limiter = Arc::new(AdaptiveRateLimiter::binance_default());

    let btc_loader = HistoricalDataLoader::new("BTCUSDT");
    let eth_loader = HistoricalDataLoader::new("ETHUSDT");

    let btc_anchor = btc_loader.fetch_latest_aggtrade().await.unwrap();
    let eth_anchor = eth_loader.fetch_latest_aggtrade().await.unwrap();

    let btc_from = btc_anchor.agg_trade_id - 1500;
    let btc_to = btc_anchor.agg_trade_id - 500;
    let eth_from = eth_anchor.agg_trade_id - 1500;
    let eth_to = eth_anchor.agg_trade_id - 500;

    // Launch both fills concurrently
    let (btc_result, eth_result) = tokio::join!(
        fetch_aggtrades_parallel("BTCUSDT", btc_from, btc_to, Some(limiter.clone()), None),
        fetch_aggtrades_parallel("ETHUSDT", eth_from, eth_to, Some(limiter.clone()), None),
    );

    let btc_trades = btc_result.unwrap();
    let eth_trades = eth_result.unwrap();

    tel.emit(
        "concurrent_fill",
        serde_json::json!({
            "btc_trades": btc_trades.len(),
            "eth_trades": eth_trades.len(),
            "limiter_utilization": limiter.utilization(),
        }),
    );

    // Both should have trades
    assert!(!btc_trades.is_empty(), "BTC fill should return trades");
    assert!(!eth_trades.is_empty(), "ETH fill should return trades");

    // Both should be contiguous
    for (name, trades) in [("BTCUSDT", &btc_trades), ("ETHUSDT", &eth_trades)] {
        for window in trades.windows(2) {
            assert_eq!(
                window[1].agg_trade_id,
                window[0].agg_trade_id + 1,
                "{name}: contiguity broken at {} -> {}",
                window[0].agg_trade_id,
                window[1].agg_trade_id
            );
        }
    }

    // Rate limiter should not be exhausted
    let util = limiter.utilization();
    assert!(
        util < 0.95,
        "rate limiter utilization too high after concurrent fills: {util:.2}"
    );
}

#[tokio::test]
async fn test_exact_page_boundary_gap() {
    // Gap of exactly 1000 trades (one full page) — boundary case
    let mut tel = TestTelemetry::new("exact_page_boundary");
    let loader = HistoricalDataLoader::new("BTCUSDT");

    let anchor = loader.fetch_latest_aggtrade().await.unwrap();
    let from_id = anchor.agg_trade_id - 2000;
    let to_id = from_id + 1000; // Exactly one page

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

    tel.emit(
        "page_boundary",
        serde_json::json!({
            "from_id": from_id,
            "to_id": to_id,
            "fetched": trades.len(),
        }),
    );

    // Should use single-fetch path (total_trades == PAGE_SIZE)
    assert!(!trades.is_empty());
    assert_eq!(trades.first().unwrap().agg_trade_id, from_id);

    // Contiguity
    for window in trades.windows(2) {
        assert_eq!(window[1].agg_trade_id, window[0].agg_trade_id + 1);
    }
}

#[tokio::test]
async fn test_cross_page_boundary_dedup() {
    // 1001 trades — forces 2 pages, tests dedup at boundary
    let mut tel = TestTelemetry::new("cross_page_dedup");
    let loader = HistoricalDataLoader::new("BTCUSDT");

    let anchor = loader.fetch_latest_aggtrade().await.unwrap();
    let from_id = anchor.agg_trade_id - 3000;
    let to_id = from_id + 1001; // Just over one page

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

    tel.emit(
        "cross_page",
        serde_json::json!({
            "from_id": from_id,
            "to_id": to_id,
            "fetched": trades.len(),
        }),
    );

    assert!(!trades.is_empty());

    // No duplicates
    let unique_ids: std::collections::HashSet<i64> = trades.iter().map(|t| t.agg_trade_id).collect();
    assert_eq!(
        unique_ids.len(),
        trades.len(),
        "duplicate trade IDs found after dedup"
    );

    // Contiguity
    for window in trades.windows(2) {
        assert_eq!(
            window[1].agg_trade_id,
            window[0].agg_trade_id + 1,
            "contiguity broken at {} -> {}",
            window[0].agg_trade_id,
            window[1].agg_trade_id
        );
    }
}

#[tokio::test]
async fn test_bars_from_gap_filled_trades_stathera() {
    // Produce bars from gap-filled trades and verify Stathera invariant
    let mut tel = TestTelemetry::new("bars_stathera_from_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 - 3000;
    let to_id = anchor.agg_trade_id - 500;

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

    assert!(!trades.is_empty(), "should fetch trades");

    // Process through all production thresholds
    for threshold in [250, 500, 750, 1000] {
        let mut processor = OpenDeviationBarProcessor::new(threshold).unwrap();
        let mut bars = Vec::new();
        for trade in &trades {
            if let Ok(Some(bar)) = processor.process_single_trade(trade) {
                bars.push(bar);
            }
        }

        tel.emit(
            "threshold_bars",
            serde_json::json!({
                "threshold": threshold,
                "bars": bars.len(),
            }),
        );

        // Stathera: bar[i].last_tid + 1 == bar[i+1].first_tid
        for window in bars.windows(2) {
            assert_eq!(
                window[0].last_agg_trade_id + 1,
                window[1].first_agg_trade_id,
                "Stathera violation at threshold={threshold}: {}+1 != {}",
                window[0].last_agg_trade_id,
                window[1].first_agg_trade_id
            );
        }
    }
}

#[tokio::test]
async fn test_gap_fill_from_id_beyond_latest() {
    // from_id beyond the latest trade — should return empty (graceful termination)
    let mut tel = TestTelemetry::new("from_id_beyond_latest");
    let loader = HistoricalDataLoader::new("BTCUSDT");

    let anchor = loader.fetch_latest_aggtrade().await.unwrap();
    let future_id = anchor.agg_trade_id + 1_000_000; // Way beyond latest

    let trades = fetch_aggtrades_parallel(
        "BTCUSDT",
        future_id,
        future_id + 100,
        None,
        None,
    )
    .await
    .unwrap();

    tel.emit(
        "beyond_latest",
        serde_json::json!({
            "future_id": future_id,
            "fetched": trades.len(),
        }),
    );

    assert!(
        trades.is_empty(),
        "fetching beyond latest should return empty, got {} trades",
        trades.len()
    );
}
