// Latency benchmark for order operations against AsterDex testnet.
//
// Measures wall-clock round-trip time for place_order, cancel_order, modify_order,
// and the full place→modify→cancel lifecycle using real network calls.
//
// Run:
//   cargo test --test aster_timing -- --include-ignored --test-threads=1 --nocapture
//
// IMPORTANT:
//   --nocapture  : shows timing output (suppressed by default)
//   --test-threads=1 : prevents EIP-712 nonce collisions across test cases

use std::time::{Duration, Instant};
use asterdex_sdk::{CancelOrderParams, ModifyOrderParams, PlaceOrderParams, FuturesClient};

/// Number of iterations per benchmark — balances statistical confidence with testnet rate limits.
const N: usize = 5;

/// Pause between consecutive API calls to stay well within testnet rate limits.
const INTER_CALL_MS: u64 = 300;

// ---------------------------------------------------------------------------
// Infrastructure
// ---------------------------------------------------------------------------

fn build_client() -> FuturesClient {
    let _ = dotenvy::dotenv();
    FuturesClient::from_env().expect("FuturesClient::from_env() failed — check .env credentials")
}

/// Compute min, max, mean, median and p95 from a sorted slice of `Duration`.
struct Stats {
    min: Duration,
    max: Duration,
    mean: Duration,
    median: Duration,
    p95: Duration,
}

fn compute_stats(samples: &[Duration]) -> Stats {
    let mut sorted = samples.to_vec();
    sorted.sort_unstable();
    let n = sorted.len();
    let sum: Duration = sorted.iter().sum();
    Stats {
        min: sorted[0],
        max: sorted[n - 1],
        mean: sum / n as u32,
        median: sorted[n / 2],
        p95: sorted[(n * 95 / 100).min(n - 1)],
    }
}

/// Print a formatted timing report section.
fn print_report(label: &str, note: &str, samples: &[Duration]) {
    let stats = compute_stats(samples);
    println!();
    println!("  {label}  {note}");
    print!("  Samples: ");
    for (i, d) in samples.iter().enumerate() {
        if i > 0 { print!("  "); }
        print!("{:>5}ms", d.as_millis());
    }
    println!();
    println!(
        "  Stats:   min={:>4}ms  max={:>4}ms  mean={:>4}ms  median={:>4}ms  p95={:>4}ms",
        stats.min.as_millis(),
        stats.max.as_millis(),
        stats.mean.as_millis(),
        stats.median.as_millis(),
        stats.p95.as_millis(),
    );
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Place a BTCUSDT LIMIT BUY at the given price (string) and return the order_id.
async fn place_at(client: &FuturesClient, price: &str) -> i64 {
    client
        .place_order(PlaceOrderParams {
            symbol: "BTCUSDT".to_string(),
            side: "BUY".to_string(),
            order_type: "LIMIT".to_string(),
            quantity: "0.001".to_string(),
            price: Some(price.to_string()),
            time_in_force: Some("GTC".to_string()),
            ..Default::default()
        })
        .await
        .expect("place_order failed")
        .data
        .order_id
}

/// Cancel an order by ID, ignoring -2011 (already gone).
async fn cancel(client: &FuturesClient, order_id: i64) {
    let _ = client
        .cancel_order(CancelOrderParams {
            symbol: "BTCUSDT".to_string(),
            order_id: Some(order_id),
            ..Default::default()
        })
        .await;
}

async fn delay() {
    tokio::time::sleep(Duration::from_millis(INTER_CALL_MS)).await;
}

// ---------------------------------------------------------------------------
// Benchmark 1 — place_order latency
// ---------------------------------------------------------------------------

/// Measure round-trip latency of `place_order` over N iterations.
///
/// Each call places a BTCUSDT LIMIT BUY far below market so the order rests
/// in the book. All orders are cancelled after measurement.
#[tokio::test]
#[ignore]
async fn bench_place_order() {
    let client = build_client();
    let mut timings = Vec::with_capacity(N);
    let mut order_ids = Vec::with_capacity(N);

    println!();
    println!("========================================");
    println!(" bench_place_order  (n={N})");
    println!("========================================");

    for i in 0..N {
        let price = format!("{}", 10_000 + i as u64);

        let t = Instant::now();
        let order_id = place_at(&client, &price).await;
        let elapsed = t.elapsed();

        println!("  run {:>2}: {:>5}ms  (order_id={})", i + 1, elapsed.as_millis(), order_id);
        timings.push(elapsed);
        order_ids.push(order_id);
        delay().await;
    }

    print_report("place_order", "(1 REST round trip)", &timings);

    // Cleanup
    for oid in order_ids {
        cancel(&client, oid).await;
        delay().await;
    }
}

// ---------------------------------------------------------------------------
// Benchmark 2 — cancel_order latency
// ---------------------------------------------------------------------------

/// Measure round-trip latency of `cancel_order` over N iterations.
///
/// All orders are pre-placed (untimed) before the cancel measurement starts,
/// so the place latency does not inflate the cancel numbers.
#[tokio::test]
#[ignore]
async fn bench_cancel_order() {
    let client = build_client();
    let mut timings = Vec::with_capacity(N);

    println!();
    println!("========================================");
    println!(" bench_cancel_order  (n={N})");
    println!("========================================");

    // Pre-place all orders (untimed)
    let mut order_ids = Vec::with_capacity(N);
    for i in 0..N {
        let price = format!("{}", 10_100 + i as u64);
        let oid = place_at(&client, &price).await;
        order_ids.push(oid);
        delay().await;
    }

    // Now measure cancel latency
    for (i, order_id) in order_ids.iter().enumerate() {
        let t = Instant::now();
        cancel(&client, *order_id).await;
        let elapsed = t.elapsed();
        println!("  run {:>2}: {:>5}ms  (cancelled order_id={})", i + 1, elapsed.as_millis(), order_id);
        timings.push(elapsed);
        delay().await;
    }

    print_report("cancel_order", "(1 REST round trip)", &timings);
}

// ---------------------------------------------------------------------------
// Benchmark 3 — modify_order latency
// ---------------------------------------------------------------------------

/// Measure round-trip latency of `modify_order` over N iterations.
///
/// `modify_order` is implemented as cancel + re-place (2 REST round trips)
/// because the testnet does not expose a native `PUT /fapi/v3/order` endpoint.
/// The timing includes both calls.
#[tokio::test]
#[ignore]
async fn bench_modify_order() {
    let client = build_client();
    let mut timings = Vec::with_capacity(N);

    println!();
    println!("========================================");
    println!(" bench_modify_order  (n={N})");
    println!("========================================");
    println!("  Note: modify = cancel + re-place (2 round trips, no native PUT endpoint)");

    for i in 0..N {
        // Pre-place (untimed)
        let place_price = format!("{}", 10_200 + i as u64);
        let order_id = place_at(&client, &place_price).await;
        delay().await;

        // Measure modify (cancel + re-place)
        let modify_price = format!("{}", 10_500 + i as u64);
        let t = Instant::now();
        let new_order = client
            .modify_order(ModifyOrderParams {
                symbol: "BTCUSDT".to_string(),
                quantity: Some("0.001".to_string()),
                price: Some(modify_price),
                order_id: Some(order_id),
                ..Default::default()
            })
            .await
            .expect("modify_order failed");
        let elapsed = t.elapsed();

        println!(
            "  run {:>2}: {:>5}ms  (orig={}, replacement={})",
            i + 1,
            elapsed.as_millis(),
            order_id,
            new_order.data.order_id
        );
        timings.push(elapsed);

        // Cancel replacement
        cancel(&client, new_order.data.order_id).await;
        delay().await;
    }

    print_report("modify_order", "(2 REST round trips: cancel + re-place)", &timings);
}

// ---------------------------------------------------------------------------
// Benchmark 4 — Full order lifecycle latency
// ---------------------------------------------------------------------------

/// Measure end-to-end latency of a complete order lifecycle:
/// place → modify → cancel.
///
/// This represents the total time a trader waits when submitting, amending,
/// and withdrawing an order in a single strategy cycle.
///
/// Round trips: 4 (place + cancel_orig + re-place + cancel_replacement)
#[tokio::test]
#[ignore]
async fn bench_full_lifecycle() {
    let client = build_client();
    let mut timings = Vec::with_capacity(N);

    println!();
    println!("========================================");
    println!(" bench_full_lifecycle  (n={N})");
    println!("========================================");
    println!("  Sequence: place → modify → cancel  (4 round trips total)");

    for i in 0..N {
        let place_price  = format!("{}", 10_300 + i as u64);
        let modify_price = format!("{}", 10_600 + i as u64);

        let t = Instant::now();

        // 1. Place
        let order_id = place_at(&client, &place_price).await;

        // 2. Modify (cancel + re-place)
        let new_order = client
            .modify_order(ModifyOrderParams {
                symbol: "BTCUSDT".to_string(),
                quantity: Some("0.001".to_string()),
                price: Some(modify_price),
                order_id: Some(order_id),
                ..Default::default()
            })
            .await
            .expect("modify_order failed in lifecycle");

        // 3. Cancel
        cancel(&client, new_order.data.order_id).await;

        let elapsed = t.elapsed();
        println!("  run {:>2}: {:>5}ms", i + 1, elapsed.as_millis());
        timings.push(elapsed);

        delay().await;
    }

    print_report(
        "full_lifecycle",
        "(place + modify + cancel = 4 round trips)",
        &timings,
    );
}

// ---------------------------------------------------------------------------
// Benchmark 5 — Summary report (runs all 4 benchmarks, prints a combined table)
// ---------------------------------------------------------------------------

/// Run all 4 benchmarks and print a combined summary table.
///
/// This is the recommended entry point for a full timing report:
///
/// ```
/// cargo test --test aster_timing bench_summary -- --include-ignored --nocapture
/// ```
#[tokio::test]
#[ignore]
async fn bench_summary() {
    let client = build_client();

    println!();
    println!("╔══════════════════════════════════════════════════════════════════╗");
    println!("║          AsterDex Order Timing Benchmark  (n={N} per operation)         ║");
    println!("║          Testnet: https://fapi.asterdex-testnet.com              ║");
    println!("╚══════════════════════════════════════════════════════════════════╝");
    println!();

    // ---- place_order ----
    let mut place_timings = Vec::with_capacity(N);
    let mut order_ids = Vec::with_capacity(N);
    for i in 0..N {
        let price = format!("{}", 10_000 + i as u64);
        let t = Instant::now();
        let oid = place_at(&client, &price).await;
        place_timings.push(t.elapsed());
        order_ids.push(oid);
        delay().await;
    }

    // ---- cancel_order ---- (cancel orders placed above)
    let mut cancel_timings = Vec::with_capacity(N);
    for oid in &order_ids {
        let t = Instant::now();
        cancel(&client, *oid).await;
        cancel_timings.push(t.elapsed());
        delay().await;
    }

    // ---- modify_order ----
    let mut modify_timings = Vec::with_capacity(N);
    for i in 0..N {
        let price_orig   = format!("{}", 10_200 + i as u64);
        let price_modify = format!("{}", 10_500 + i as u64);

        let oid = place_at(&client, &price_orig).await;
        delay().await;

        let t = Instant::now();
        let new_order = client
            .modify_order(ModifyOrderParams {
                symbol: "BTCUSDT".to_string(),
                quantity: Some("0.001".to_string()),
                price: Some(price_modify),
                order_id: Some(oid),
                ..Default::default()
            })
            .await
            .expect("modify_order failed in summary");
        modify_timings.push(t.elapsed());

        cancel(&client, new_order.data.order_id).await;
        delay().await;
    }

    // ---- full lifecycle ----
    let mut lifecycle_timings = Vec::with_capacity(N);
    for i in 0..N {
        let price_orig   = format!("{}", 10_300 + i as u64);
        let price_modify = format!("{}", 10_600 + i as u64);

        let t = Instant::now();
        let oid = place_at(&client, &price_orig).await;
        let new_order = client
            .modify_order(ModifyOrderParams {
                symbol: "BTCUSDT".to_string(),
                quantity: Some("0.001".to_string()),
                price: Some(price_modify),
                order_id: Some(oid),
                ..Default::default()
            })
            .await
            .expect("modify_order failed in lifecycle summary");
        cancel(&client, new_order.data.order_id).await;
        lifecycle_timings.push(t.elapsed());

        delay().await;
    }

    // ---- Print combined table ----
    let p = compute_stats(&place_timings);
    let c = compute_stats(&cancel_timings);
    let m = compute_stats(&modify_timings);
    let l = compute_stats(&lifecycle_timings);

    println!(
        "  {:<22}  {:>6}  {:>6}  {:>6}  {:>8}  {:>6}  {}",
        "Operation", "min", "max", "mean", "median", "p95", "round trips"
    );
    println!("  {}", "-".repeat(75));
    println!(
        "  {:<22}  {:>5}ms  {:>5}ms  {:>5}ms  {:>7}ms  {:>5}ms  {}",
        "place_order",
        p.min.as_millis(), p.max.as_millis(), p.mean.as_millis(),
        p.median.as_millis(), p.p95.as_millis(), "1"
    );
    println!(
        "  {:<22}  {:>5}ms  {:>5}ms  {:>5}ms  {:>7}ms  {:>5}ms  {}",
        "cancel_order",
        c.min.as_millis(), c.max.as_millis(), c.mean.as_millis(),
        c.median.as_millis(), c.p95.as_millis(), "1"
    );
    println!(
        "  {:<22}  {:>5}ms  {:>5}ms  {:>5}ms  {:>7}ms  {:>5}ms  {} (cancel + re-place)",
        "modify_order",
        m.min.as_millis(), m.max.as_millis(), m.mean.as_millis(),
        m.median.as_millis(), m.p95.as_millis(), "2"
    );
    println!(
        "  {:<22}  {:>5}ms  {:>5}ms  {:>5}ms  {:>7}ms  {:>5}ms  {} (place+modify+cancel)",
        "full_lifecycle",
        l.min.as_millis(), l.max.as_millis(), l.mean.as_millis(),
        l.median.as_millis(), l.p95.as_millis(), "4"
    );
    println!();

    // Raw samples for reference
    println!("  Raw samples (ms):");
    let label_width = 22usize;
    let print_row = |label: &str, samples: &[Duration]| {
        print!("    {:<label_width$} ", label);
        for d in samples {
            print!("{:>5}  ", d.as_millis());
        }
        println!();
    };
    print_row("place_order",   &place_timings);
    print_row("cancel_order",  &cancel_timings);
    print_row("modify_order",  &modify_timings);
    print_row("full_lifecycle",&lifecycle_timings);
    println!();
}
