//! Smoke tests for the real wgpu BFS and SSSP dispatch path.
//!
//! All tests in this module are gated on `#[cfg(feature = "gpu")]`.
//! On hosts without a GPU adapter (headless CI) every test that requires
//! GPU access detects the absence of an adapter at runtime and skips
//! gracefully — the test passes, not fails.
//!
//! ## Graph sizes
//! Each GPU test uses a graph with ≥ 4096 col_idx entries so that the
//! `GPU_EDGE_THRESHOLD` check in algorithms.rs is satisfied and the wgpu
//! dispatch path is actually exercised:
//! - BFS  : 100-vertex undirected (chain + chords) → 20,000+ col_idx entries
//! - BF   : 70-vertex complete digraph            → 4830 directed edges
//! - delta: same 70-vertex digraph as adjacency list

#[cfg(feature = "gpu")]
mod wgpu_graph_smoke {
    use scirs2_graph::gpu::algorithms::{
        gpu_bfs, gpu_sssp_bellman_ford, gpu_sssp_delta_stepping, GpuBfsConfig, GpuGraphBackend,
    };

    // ─────────────────────────────────────────────────────────────────────────
    // Helpers
    // ─────────────────────────────────────────────────────────────────────────

    /// Build an undirected CSR from an edge list.
    fn build_csr(n: usize, edges: &[(usize, usize)]) -> (Vec<usize>, Vec<usize>) {
        let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
        for &(u, v) in edges {
            adj[u].push(v);
            adj[v].push(u);
        }
        let mut row_ptr = vec![0usize; n + 1];
        for i in 0..n {
            row_ptr[i + 1] = row_ptr[i] + adj[i].len();
        }
        let col_idx: Vec<usize> = adj.into_iter().flatten().collect();
        (row_ptr, col_idx)
    }

    /// Detect a "no GPU adapter" error message and print a skip notice.
    /// Returns `true` if the error is adapter-related (test should be skipped).
    fn is_adapter_error(msg: &str) -> bool {
        msg.contains("adapter")
            || msg.contains("Adapter")
            || msg.contains("GPU")
            || msg.contains("no suitable")
            || msg.contains("no wgpu")
    }

    /// Build a 100-vertex band graph with half-width 32.
    ///
    /// Each vertex `i` connects to every `j` with `|i-j| <= 32`, stored as
    /// unique undirected edges. This yields 2 672 unique edges → 5 344
    /// col_idx entries (above GPU_EDGE_THRESHOLD=4096), while keeping a
    /// BFS diameter of ≈4 so the frontier is multi-level rather than trivially
    /// diameter-1.
    fn build_large_bfs_graph() -> (usize, Vec<(usize, usize)>) {
        let n = 100usize;
        let half_width = 32usize;
        let mut edges: Vec<(usize, usize)> = Vec::new();
        for i in 0..n {
            let lo = i + 1;
            let hi = (i + half_width + 1).min(n);
            for j in lo..hi {
                edges.push((i, j));
            }
        }
        (n, edges)
    }

    /// Build a 70-vertex complete directed graph (no self-loops) with
    /// deterministic positive weights.  Returns (row_ptr, col_idx, weights).
    ///
    /// Weight formula: `1.0 + ((i * 7 + j * 3) % 17) as f64 * 0.1`
    /// — always positive, finite, and varied enough for non-trivial shortest paths.
    ///
    /// Edge count: 70 × 69 = 4 830 (above GPU_EDGE_THRESHOLD).
    fn build_large_sssp_graph() -> (Vec<usize>, Vec<usize>, Vec<f64>) {
        let n = 70usize;
        let mut adj: Vec<Vec<(usize, f64)>> = vec![vec![]; n];
        for (i, row) in adj.iter_mut().enumerate() {
            for j in 0..n {
                if i != j {
                    let w = 1.0 + ((i * 7 + j * 3) % 17) as f64 * 0.1;
                    row.push((j, w));
                }
            }
        }
        let mut row_ptr = vec![0usize; n + 1];
        for i in 0..n {
            row_ptr[i + 1] = row_ptr[i] + adj[i].len();
        }
        let mut col_idx: Vec<usize> = Vec::new();
        let mut weights: Vec<f64> = Vec::new();
        for nbrs in &adj {
            for &(v, w) in nbrs {
                col_idx.push(v);
                weights.push(w);
            }
        }
        (row_ptr, col_idx, weights)
    }

    /// Same 70-vertex complete digraph as adjacency list for delta-stepping.
    fn build_large_sssp_adj() -> Vec<Vec<(usize, f64)>> {
        let n = 70usize;
        let mut adj: Vec<Vec<(usize, f64)>> = vec![vec![]; n];
        for (i, row) in adj.iter_mut().enumerate() {
            for j in 0..n {
                if i != j {
                    let w = 1.0 + ((i * 7 + j * 3) % 17) as f64 * 0.1;
                    row.push((j, w));
                }
            }
        }
        adj
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 1 — BFS: GPU result matches CPU, or skip gracefully
    // ─────────────────────────────────────────────────────────────────────────

    /// 100-vertex graph with chain + multiple chords (>10 000 col_idx entries).
    /// GPU BFS distances must match the CpuParallel reference exactly.
    /// On no-adapter hosts, skips gracefully without failing the test.
    #[test]
    fn bfs_gpu_matches_cpu_or_skips() {
        let (n, edges) = build_large_bfs_graph();
        let (row_ptr, col_idx) = build_csr(n, &edges);

        // Sanity: graph is large enough to exercise GPU path
        assert!(
            col_idx.len() >= 4096,
            "col_idx.len()={} must be ≥ 4096 to exercise GPU threshold",
            col_idx.len()
        );

        // CPU reference
        let cpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::CpuParallel,
            chunk_size: 1024,
        };
        let cpu_dist = gpu_bfs(&row_ptr, &col_idx, 0, &cpu_config).expect("cpu bfs failed");

        // GPU attempt
        let gpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::Gpu,
            chunk_size: 1024,
        };
        match gpu_bfs(&row_ptr, &col_idx, 0, &gpu_config) {
            Err(e) => {
                let msg = e.to_string();
                if is_adapter_error(&msg) {
                    println!("bfs_gpu_matches_cpu_or_skips: no wgpu adapter — skipping ({msg})");
                } else {
                    panic!("Unexpected error in GPU BFS: {e}");
                }
            }
            Ok(gpu_dist) => {
                assert_eq!(
                    gpu_dist.len(),
                    cpu_dist.len(),
                    "distance vector length mismatch"
                );
                for v in 0..n {
                    assert_eq!(
                        gpu_dist[v], cpu_dist[v],
                        "vertex {v}: gpu={} cpu={}",
                        gpu_dist[v], cpu_dist[v]
                    );
                }
                println!(
                    "bfs_gpu_matches_cpu_or_skips: GPU distances match CPU reference \
                     for all {n} vertices (col_idx.len={})",
                    col_idx.len()
                );
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 2 — Bellman-Ford SSSP: GPU result matches CPU within f32 tolerance
    // ─────────────────────────────────────────────────────────────────────────

    /// 70-vertex complete directed graph (4 830 edges, above GPU threshold).
    /// GPU Bellman-Ford distances must match the CpuParallel reference within
    /// 1e-5 (f32 precision from the GPU path).
    /// Skips gracefully on no-adapter hosts.
    #[test]
    fn sssp_bellman_ford_gpu_matches_cpu_or_skips() {
        let (row_ptr, col_idx, weights) = build_large_sssp_graph();
        let n = row_ptr.len() - 1;

        assert!(
            col_idx.len() >= 4096,
            "col_idx.len()={} must be ≥ 4096 to exercise GPU threshold",
            col_idx.len()
        );

        // CPU reference (sequential path via CpuParallel backend)
        let cpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::CpuParallel,
            chunk_size: 1024,
        };
        let cpu_dist = gpu_sssp_bellman_ford(&row_ptr, &col_idx, &weights, 0, &cpu_config)
            .expect("cpu sssp failed");

        // GPU attempt
        let gpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::Gpu,
            chunk_size: 1024,
        };
        match gpu_sssp_bellman_ford(&row_ptr, &col_idx, &weights, 0, &gpu_config) {
            Err(e) => {
                let msg = e.to_string();
                if is_adapter_error(&msg) {
                    println!(
                        "sssp_bellman_ford_gpu_matches_cpu_or_skips: no wgpu adapter — skipping ({msg})"
                    );
                } else {
                    panic!("Unexpected error in GPU Bellman-Ford: {e}");
                }
            }
            Ok(gpu_dist) => {
                assert_eq!(gpu_dist.len(), n, "distance vector length mismatch");
                for v in 0..n {
                    let cpu_v = cpu_dist[v];
                    let gpu_v = gpu_dist[v];
                    if cpu_v.is_infinite() {
                        assert!(gpu_v.is_infinite(), "vertex {v}: cpu=inf but gpu={gpu_v}");
                    } else {
                        assert!(
                            (cpu_v - gpu_v).abs() < 1e-4,
                            "vertex {v}: cpu={cpu_v} gpu={gpu_v} diff={}",
                            (cpu_v - gpu_v).abs()
                        );
                    }
                }
                println!(
                    "sssp_bellman_ford_gpu_matches_cpu_or_skips: GPU distances match CPU within \
                     1e-4 for all {n} vertices ({} edges)",
                    col_idx.len()
                );
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 3 — CpuParallel is honest: actually calls parallel_bfs_atomic
    // ─────────────────────────────────────────────────────────────────────────

    /// Call `gpu_bfs` with `GpuGraphBackend::CpuParallel` on a 1000-vertex graph.
    /// Assert: no panic, correct BFS distances (matches the sequential reference).
    #[test]
    fn cpu_parallel_bfs_actually_parallel() {
        let n = 1000usize;
        // Build a grid-like graph: chain + cross edges every 10 vertices
        let mut edges: Vec<(usize, usize)> = (0..(n - 1)).map(|i| (i, i + 1)).collect();
        for i in (0..n).step_by(10) {
            if i + 10 < n {
                edges.push((i, i + 10));
            }
        }
        let (row_ptr, col_idx) = build_csr(n, &edges);

        let config = GpuBfsConfig {
            backend: GpuGraphBackend::CpuParallel,
            chunk_size: 1024,
        };
        let dist = gpu_bfs(&row_ptr, &col_idx, 0, &config).expect("cpu parallel bfs failed");

        assert_eq!(
            dist.len(),
            n,
            "distance vector must have one entry per vertex"
        );
        // Vertex 0 must be at distance 0
        assert_eq!(dist[0], 0, "source distance must be 0");
        // All vertices must be reachable (graph is connected)
        for (v, &d) in dist.iter().enumerate().take(n) {
            assert!(
                d != usize::MAX,
                "vertex {v} must be reachable in a connected graph"
            );
        }
        // Check monotonicity along chain (within same row, distance should be reasonable)
        // dist[10] <= dist[1]+1 (via cross edge 0-10) — just check upper bound
        assert!(
            dist[n - 1] < n,
            "max distance must be less than n in this graph"
        );

        println!(
            "cpu_parallel_bfs_actually_parallel: 1000-vertex graph traversed correctly (max dist={})",
            dist.iter().filter(|&&d| d != usize::MAX).max().copied().unwrap_or(0)
        );
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 4 — Delta-stepping: matches Bellman-Ford within 1e-4
    // ─────────────────────────────────────────────────────────────────────────

    /// Helper: compute Bellman-Ford reference via CpuParallel from adjacency list.
    fn bf_ref_from_adj(adj: &[Vec<(usize, f64)>], source: usize) -> Vec<f64> {
        use scirs2_graph::gpu::algorithms::gpu_sssp_bellman_ford;
        let n = adj.len();
        let mut row_ptr = vec![0usize; n + 1];
        for (i, nbrs) in adj.iter().enumerate() {
            row_ptr[i + 1] = row_ptr[i] + nbrs.len();
        }
        let mut col_idx = Vec::new();
        let mut weights = Vec::new();
        for nbrs in adj {
            for &(v, w) in nbrs {
                col_idx.push(v);
                weights.push(w);
            }
        }
        let cpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::CpuParallel,
            chunk_size: 1024,
        };
        gpu_sssp_bellman_ford(&row_ptr, &col_idx, &weights, source, &cpu_config)
            .expect("reference Bellman-Ford failed")
    }

    /// 70-vertex complete digraph (same structure as Test 2, in adjacency-list form).
    /// GPU delta-stepping result must equal the CpuParallel Bellman-Ford reference
    /// within 1e-4.  Skips gracefully on no-adapter hosts.
    #[test]
    fn delta_stepping_matches_bellman_ford_or_skips() {
        let adj = build_large_sssp_adj();
        let n = adj.len();

        // Count total edges for the threshold assertion
        let total_edges: usize = adj.iter().map(|v| v.len()).sum();
        assert!(
            total_edges >= 4096,
            "total_edges={total_edges} must be ≥ 4096 to exercise GPU threshold"
        );

        // BF reference via CpuParallel (build CSR from adj)
        let mut row_ptr = vec![0usize; n + 1];
        for (i, nbrs) in adj.iter().enumerate() {
            row_ptr[i + 1] = row_ptr[i] + nbrs.len();
        }
        let mut col_idx = Vec::new();
        let mut weights = Vec::new();
        for nbrs in &adj {
            for &(v, w) in nbrs {
                col_idx.push(v);
                weights.push(w);
            }
        }

        let cpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::CpuParallel,
            chunk_size: 1024,
        };
        let bf_ref = gpu_sssp_bellman_ford(&row_ptr, &col_idx, &weights, 0, &cpu_config)
            .expect("reference Bellman-Ford failed");

        // Delta-stepping GPU attempt
        let gpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::Gpu,
            chunk_size: 1024,
        };
        match gpu_sssp_delta_stepping(&adj, 0, 1.0, &gpu_config) {
            Err(e) => {
                let msg = e.to_string();
                if is_adapter_error(&msg) {
                    println!(
                        "delta_stepping_matches_bellman_ford_or_skips: no wgpu adapter — skipping ({msg})"
                    );
                } else {
                    panic!("Unexpected error in GPU delta-stepping: {e}");
                }
            }
            Ok(ds_dist) => {
                assert_eq!(ds_dist.len(), n, "distance vector length mismatch");
                for v in 0..n {
                    let ref_v = bf_ref[v];
                    let ds_v = ds_dist[v];
                    if ref_v.is_infinite() {
                        assert!(ds_v.is_infinite(), "vertex {v}: bf=inf but delta={ds_v}");
                    } else {
                        assert!(
                            (ref_v - ds_v).abs() < 1e-4,
                            "vertex {v}: bf={ref_v} delta={ds_v} diff={}",
                            (ref_v - ds_v).abs()
                        );
                    }
                }
                println!(
                    "delta_stepping_matches_bellman_ford_or_skips: delta-stepping matches \
                     Bellman-Ford within 1e-4 for all {n} vertices ({total_edges} edges)"
                );
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 5 — True WGSL delta-stepping: matches Bellman-Ford for large graph
    // ─────────────────────────────────────────────────────────────────────────

    /// Verifies that the true bucket-based WGSL delta-stepping (light+apply kernels)
    /// produces distances equal to Bellman-Ford within 1e-4.
    /// Uses the same 70-vertex complete digraph as Test 4 (4830 edges, ≥4096 threshold).
    /// Skips gracefully when no wgpu adapter is available.
    #[test]
    fn delta_stepping_true_wgsl_matches_bellman_ford_or_skips() {
        let adj = build_large_sssp_adj();
        let n = adj.len();
        let total_edges: usize = adj.iter().map(|v| v.len()).sum();

        // Verify edge count meets GPU threshold
        assert!(
            total_edges >= 4096,
            "graph must have ≥ 4096 edges, got {total_edges}"
        );

        // Bellman-Ford CPU reference
        let bf_ref = bf_ref_from_adj(&adj, 0);

        // GPU delta-stepping with default delta=1.0
        let gpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::Gpu,
            chunk_size: 1024,
        };
        match gpu_sssp_delta_stepping(&adj, 0, 1.0, &gpu_config) {
            Err(e) => {
                let msg = e.to_string();
                if is_adapter_error(&msg) {
                    println!(
                        "delta_stepping_true_wgsl_matches_bellman_ford_or_skips: \
                         no wgpu adapter — skipping ({msg})"
                    );
                } else {
                    panic!("Unexpected error in true WGSL delta-stepping: {e}");
                }
            }
            Ok(ds_dist) => {
                assert_eq!(ds_dist.len(), n, "distance vector length mismatch");
                for v in 0..n {
                    let ref_v = bf_ref[v];
                    let ds_v = ds_dist[v];
                    if ref_v.is_infinite() {
                        assert!(
                            ds_v.is_infinite(),
                            "vertex {v}: bf=inf but true_delta={ds_v}"
                        );
                    } else {
                        assert!(
                            (ref_v - ds_v).abs() < 1e-4,
                            "vertex {v}: bf={ref_v} true_delta={ds_v} diff={}",
                            (ref_v - ds_v).abs()
                        );
                    }
                }
                println!(
                    "delta_stepping_true_wgsl_matches_bellman_ford_or_skips: \
                     true WGSL delta-stepping matches BF within 1e-4 \
                     for all {n} vertices ({total_edges} edges)"
                );
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Test 6 — Delta-stepping with varied delta values (heavy-edge convergence)
    // ─────────────────────────────────────────────────────────────────────────

    /// Tests delta-stepping with delta=0.5 (all edges become heavy — weights ≥ 1.0)
    /// and delta=10.0 (all edges become light). Both should produce the same distances
    /// as Bellman-Ford.
    ///
    /// With delta=0.5, the heavy-edge convergence flag is exercised: if the heavy
    /// kernel did not set the flag, the loop would exit after one iteration and leave
    /// multi-hop distances at INF.
    /// Skips gracefully when no wgpu adapter is available.
    #[test]
    fn delta_stepping_with_varied_delta_or_skips() {
        let adj = build_large_sssp_adj();
        let n = adj.len();

        // Bellman-Ford CPU reference
        let bf_ref = bf_ref_from_adj(&adj, 0);

        let gpu_config = GpuBfsConfig {
            backend: GpuGraphBackend::Gpu,
            chunk_size: 1024,
        };

        // Test both delta values; skip both if no adapter
        for &delta in &[0.5f64, 10.0f64] {
            match gpu_sssp_delta_stepping(&adj, 0, delta, &gpu_config) {
                Err(e) => {
                    let msg = e.to_string();
                    if is_adapter_error(&msg) {
                        println!(
                            "delta_stepping_with_varied_delta_or_skips(delta={delta}): \
                             no wgpu adapter — skipping ({msg})"
                        );
                        // If no adapter for one, no adapter for the other; exit early
                        return;
                    } else {
                        panic!("Unexpected error (delta={delta}): {e}");
                    }
                }
                Ok(ds_dist) => {
                    assert_eq!(
                        ds_dist.len(),
                        n,
                        "delta={delta}: distance vector length mismatch"
                    );
                    for v in 0..n {
                        let ref_v = bf_ref[v];
                        let ds_v = ds_dist[v];
                        if ref_v.is_infinite() {
                            assert!(
                                ds_v.is_infinite(),
                                "delta={delta} vertex {v}: bf=inf but delta_step={ds_v}"
                            );
                        } else {
                            assert!(
                                (ref_v - ds_v).abs() < 1e-4,
                                "delta={delta} vertex {v}: bf={ref_v} delta_step={ds_v} diff={}",
                                (ref_v - ds_v).abs()
                            );
                        }
                    }
                    println!(
                        "delta_stepping_with_varied_delta_or_skips(delta={delta}): \
                         distances match BF within 1e-4 for all {n} vertices"
                    );
                }
            }
        }
    }
}
