//! Distributed benchmark tests — Phase D (v4.4.0)
//!
//! Measures ingest throughput (nodes/sec) and query latency (P50/P95/P99)
//! for 1K, 100K, and 1M node social graphs across a 5-partition cluster.
//!
//! Also verifies:
//!  - Partition balance (each partition within ±10% of ideal share)
//!  - Cross-partition query overhead vs same-partition baseline
//!
//! Run with:
//!   cargo test --test test_distributed_benchmark --no-default-features \
//!              --features distributed -- --nocapture

use std::time::{Duration, Instant};

use indexmap::IndexMap;

use ocg::{
    CypherValue, PropertyValue, execute,
    distributed::graph::DistributedCluster,
    graph::{GraphBackend, NetworKitRustBackend},
};

type Cluster = DistributedCluster<NetworKitRustBackend>;

// ─── Timing helpers ─────────────────────────────────────────────────────────

/// Run `f` `iterations` times, collect wall-clock durations, return sorted vec.
fn measure_latency<F: FnMut() -> ()>(mut f: F, iterations: usize) -> Vec<Duration> {
    let mut samples: Vec<Duration> = (0..iterations)
        .map(|_| {
            let t = Instant::now();
            f();
            t.elapsed()
        })
        .collect();
    samples.sort();
    samples
}

fn percentile(sorted: &[Duration], pct: f64) -> Duration {
    if sorted.is_empty() {
        return Duration::ZERO;
    }
    let idx = ((sorted.len() as f64 * pct / 100.0) as usize).min(sorted.len() - 1);
    sorted[idx]
}

fn print_latency_table(label: &str, sorted: &[Duration]) {
    let p50 = percentile(sorted, 50.0);
    let p95 = percentile(sorted, 95.0);
    let p99 = percentile(sorted, 99.0);
    let min = sorted.first().copied().unwrap_or(Duration::ZERO);
    let max = sorted.last().copied().unwrap_or(Duration::ZERO);
    println!(
        "  {:<40} min={:>8.3}ms  p50={:>8.3}ms  p95={:>8.3}ms  p99={:>8.3}ms  max={:>8.3}ms",
        label,
        min.as_secs_f64() * 1000.0,
        p50.as_secs_f64() * 1000.0,
        p95.as_secs_f64() * 1000.0,
        p99.as_secs_f64() * 1000.0,
        max.as_secs_f64() * 1000.0,
    );
}

/// Build a cluster with `n` nodes labelled `Person` with `id` and `age`
/// properties. Returns (cluster, elapsed).
fn build_cluster(n: usize, num_partitions: u32) -> (Cluster, Duration) {
    let mut cluster = Cluster::new(num_partitions);
    let start = Instant::now();
    for i in 0..n {
        let mut props = IndexMap::new();
        props.insert("id".to_string(), PropertyValue::Integer(i as i64));
        props.insert("age".to_string(), PropertyValue::Integer((i % 80 + 20) as i64));
        cluster.create_node(vec!["Person"], props);
    }
    let elapsed = start.elapsed();
    (cluster, elapsed)
}

// ─── 1. Ingest throughput ───────────────────────────────────────────────────

fn ingest_benchmark(n: usize, num_partitions: u32) {
    let (cluster, elapsed) = build_cluster(n, num_partitions);
    let nodes_per_sec = n as f64 / elapsed.as_secs_f64();
    println!(
        "  Ingest {:>7} nodes / {} partitions  →  {:>10.0} nodes/sec  ({:.3}s total)",
        n,
        num_partitions,
        nodes_per_sec,
        elapsed.as_secs_f64(),
    );
    // Verify count.
    assert_eq!(cluster.total_node_count(), n);
}

#[test]
fn test_ingest_throughput() {
    println!("\n=== Ingest Throughput ===");
    ingest_benchmark(1_000, 5);
    ingest_benchmark(10_000, 5);
    ingest_benchmark(100_000, 5);
}

// ─── 2. Partition balance ───────────────────────────────────────────────────

/// After round-robin ingest, each partition must hold between
/// `ideal * (1 - tolerance)` and `ideal * (1 + tolerance)` nodes.
fn assert_balanced(n: usize, num_partitions: u32, tolerance: f64) {
    let (cluster, _) = build_cluster(n, num_partitions);
    let ideal = n as f64 / num_partitions as f64;
    let lo = (ideal * (1.0 - tolerance)).floor() as usize;
    let hi = (ideal * (1.0 + tolerance)).ceil() as usize;

    println!(
        "  Balance check: {} nodes / {} partitions  (ideal={:.1}, allowed [{}, {}])",
        n, num_partitions, ideal, lo, hi
    );
    for pid in 0..num_partitions {
        let cnt = cluster.partition_node_count(pid);
        println!("    partition {pid}: {cnt} nodes");
        assert!(
            cnt >= lo && cnt <= hi,
            "partition {pid} has {cnt} nodes, expected [{lo}, {hi}] (tolerance={tolerance_pct:.0}%)",
            tolerance_pct = tolerance * 100.0
        );
    }
}

#[test]
fn test_partition_balance_1k() {
    println!("\n=== Partition Balance (1K nodes, 5 partitions) ===");
    assert_balanced(1_000, 5, 0.10);
}

#[test]
fn test_partition_balance_10k() {
    println!("\n=== Partition Balance (10K nodes, 5 partitions) ===");
    assert_balanced(10_000, 5, 0.10);
}

// ─── 3. Query latency ───────────────────────────────────────────────────────

/// Query latency: `MATCH (n:Person) RETURN count(n)` on a pre-built cluster.
#[test]
fn test_count_query_latency_1k() {
    println!("\n=== Count Query Latency (1K nodes, 5 partitions, 100 runs) ===");
    let (mut cluster, _) = build_cluster(1_000, 5);
    let samples = measure_latency(
        || {
            let r = execute(&mut cluster, "MATCH (n:Person) RETURN count(n) AS c").unwrap();
            let _ = r.rows.first();
        },
        100,
    );
    print_latency_table("MATCH (n:Person) RETURN count(n)", &samples);

    // Sanity: p99 should be well under 1 second for 1K nodes in-process.
    assert!(
        percentile(&samples, 99.0) < Duration::from_secs(1),
        "p99 latency exceeded 1s for 1K node count query"
    );
}

#[test]
fn test_count_query_latency_10k() {
    println!("\n=== Count Query Latency (10K nodes, 5 partitions, 50 runs) ===");
    let (mut cluster, _) = build_cluster(10_000, 5);
    let samples = measure_latency(
        || {
            let _ = execute(&mut cluster, "MATCH (n:Person) RETURN count(n) AS c").unwrap();
        },
        50,
    );
    print_latency_table("MATCH (n:Person) RETURN count(n)", &samples);

    assert!(
        percentile(&samples, 99.0) < Duration::from_secs(5),
        "p99 latency exceeded 5s for 10K node count query"
    );
}

/// Aggregation latency: `MATCH (n:Person) RETURN avg(n.age)`.
#[test]
fn test_aggregation_latency_1k() {
    println!("\n=== Aggregation Latency (1K nodes, avg(age), 100 runs) ===");
    let (mut cluster, _) = build_cluster(1_000, 5);
    let samples = measure_latency(
        || {
            let _ = execute(&mut cluster, "MATCH (n:Person) RETURN avg(n.age) AS a").unwrap();
        },
        100,
    );
    print_latency_table("MATCH (n:Person) RETURN avg(n.age)", &samples);
}

/// Property filter latency: `MATCH (n:Person) WHERE n.age > 50 RETURN count(n)`.
#[test]
fn test_filter_query_latency_1k() {
    println!("\n=== Filter Query Latency (1K nodes, WHERE age > 50, 100 runs) ===");
    let (mut cluster, _) = build_cluster(1_000, 5);
    let samples = measure_latency(
        || {
            let _ = execute(
                &mut cluster,
                "MATCH (n:Person) WHERE n.age > 50 RETURN count(n) AS c",
            )
            .unwrap();
        },
        100,
    );
    print_latency_table("MATCH (n:Person) WHERE n.age > 50", &samples);
}

// ─── 4. Cross-partition vs same-partition edge query overhead ───────────────

/// Build a cluster where half the edges are cross-partition.
/// Compares MATCH query time on a same-partition subgraph vs full cluster.
#[test]
fn test_cross_partition_overhead() {
    println!("\n=== Cross-partition Edge Query Overhead (500 nodes, 2 partitions) ===");

    let mut cluster = Cluster::new(2);

    // Create 500 nodes — 250 on p0, 250 on p1 (round-robin).
    let mut node_ids: Vec<u64> = Vec::with_capacity(500);
    for i in 0..500i64 {
        let mut props = IndexMap::new();
        props.insert("i".to_string(), PropertyValue::Integer(i));
        node_ids.push(cluster.create_node(vec!["V"], props));
    }

    // Create 200 cross-partition edges (even idx → odd idx).
    let mut edge_count = 0usize;
    for k in 0..200usize {
        let a = node_ids[k * 2];       // p0 node
        let b = node_ids[k * 2 + 1];   // p1 node
        cluster.create_relationship(a, b, "LINK", IndexMap::new()).unwrap();
        edge_count += 1;
    }
    assert_eq!(edge_count, 200);

    // Measure: traverse all edges.
    let samples_cross = measure_latency(
        || {
            let _ = execute(&mut cluster, "MATCH (a:V)-[:LINK]->(b:V) RETURN count(*) AS c").unwrap();
        },
        50,
    );
    print_latency_table("MATCH cross-partition -[:LINK]->", &samples_cross);

    // Measure: count nodes on p0 only (same-partition, no edge traversal).
    let samples_local = measure_latency(
        || {
            let _ = execute(&mut cluster, "MATCH (n:V) RETURN count(n) AS c").unwrap();
        },
        50,
    );
    print_latency_table("MATCH (n:V) count (all partitions)", &samples_local);

    println!(
        "  Cross-partition p50 overhead vs local: {:.2}x",
        percentile(&samples_cross, 50.0).as_secs_f64()
            / percentile(&samples_local, 50.0).as_secs_f64().max(0.000001)
    );
}

// ─── 5. Correctness after large ingest ──────────────────────────────────────

/// After ingesting 10K nodes, aggregation must return the exact expected value.
#[test]
fn test_large_ingest_correctness() {
    let n = 10_000usize;
    let (mut cluster, _) = build_cluster(n, 5);

    let r = execute(&mut cluster, "MATCH (n:Person) RETURN count(n) AS c").unwrap();
    let cnt = r
        .rows
        .first()
        .and_then(|row| row.get("c"))
        .and_then(|v| if let CypherValue::Integer(i) = v { Some(*i) } else { None })
        .unwrap_or(0);

    assert_eq!(cnt as usize, n, "count(*) after large ingest must equal {n}");

    // Sum of ids 0..n = n*(n-1)/2.
    let expected_sum = (n as i64 * (n as i64 - 1)) / 2;
    let r2 = execute(&mut cluster, "MATCH (n:Person) RETURN sum(n.id) AS s").unwrap();
    let got_sum = r2
        .rows
        .first()
        .and_then(|row| row.get("s"))
        .and_then(|v| if let CypherValue::Integer(i) = v { Some(*i) } else { None })
        .unwrap_or(-1);
    assert_eq!(
        got_sum, expected_sum,
        "sum(n.id) after large ingest: expected {expected_sum}, got {got_sum}"
    );
    println!(
        "\n  Correctness check {n} nodes: count={cnt}, sum={got_sum} (expected {expected_sum}) ✓"
    );
}
