//! Integration tests for BSP distributed algorithms.
//!
//! These tests verify correctness of the BSP superstep implementations
//! (PageRank, BFS, SSSP) by running the full multi-superstep iteration
//! in-process against a single-partition graph and comparing results
//! to the expected values computed analytically.

#[cfg(feature = "distributed")]
mod bsp_tests {
    use std::collections::HashMap;
    use ocg::distributed::network::superstep::{
        SuperstepRequest, algorithms,
    };

    // ── Helper ────────────────────────────────────────────────────────────

    /// Run a BSP algorithm in-process for multiple supersteps.
    ///
    /// Returns the aggregated `local_results` from the final convergence step.
    fn run_algorithm(
        algo: &str,
        params: HashMap<String, f64>,
        node_ids: &[u64],
        edges: &[(u64, u64, f64)],
        max_supersteps: u32,
    ) -> HashMap<u64, f64> {
        let mut messages: HashMap<u64, Vec<f64>> = HashMap::new();
        let mut all_results: HashMap<u64, f64> = HashMap::new();

        for superstep in 0..max_supersteps {
            let req = SuperstepRequest {
                algo: algo.to_string(),
                superstep,
                params: params.clone(),
                messages: messages.clone(),
            };

            let resp = algorithms::dispatch(&req, node_ids, edges).expect("dispatch failed");

            // Accumulate results
            all_results.extend(resp.local_results.clone());

            if resp.converged {
                break;
            }

            // Route outgoing messages (single partition — all messages stay local)
            messages = HashMap::new();
            for msg in resp.outgoing {
                messages.entry(msg.dst_vertex).or_default().push(msg.value);
            }
        }

        all_results
    }

    // ── BFS tests ─────────────────────────────────────────────────────────

    /// Simple chain: 0 → 1 → 2 → 3
    #[test]
    fn test_bfs_chain_graph() {
        // Nodes: 0, 1, 2, 3
        // Edges: 0→1, 1→2, 2→3
        let node_ids = vec![0u64, 1, 2, 3];
        let edges = vec![(0u64, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0)];
        let params: HashMap<String, f64> = [("source".to_string(), 0.0)].into();

        let result = run_algorithm("bfs", params, &node_ids, &edges, 10);

        // BFS from 0: distances should be 0, 1, 2, 3
        assert_eq!(result.get(&0), Some(&0.0), "source should have level 0");
        assert_eq!(result.get(&1), Some(&1.0), "level of 1 should be 1");
        assert_eq!(result.get(&2), Some(&2.0), "level of 2 should be 2");
        assert_eq!(result.get(&3), Some(&3.0), "level of 3 should be 3");
    }

    /// Star graph: 0 → 1, 0 → 2, 0 → 3
    #[test]
    fn test_bfs_star_graph() {
        let node_ids = vec![0u64, 1, 2, 3];
        let edges = vec![(0u64, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0)];
        let params: HashMap<String, f64> = [("source".to_string(), 0.0)].into();

        let result = run_algorithm("bfs", params, &node_ids, &edges, 5);

        assert_eq!(result.get(&0), Some(&0.0));
        assert_eq!(result.get(&1), Some(&1.0));
        assert_eq!(result.get(&2), Some(&1.0));
        assert_eq!(result.get(&3), Some(&1.0));
    }

    /// BFS from a leaf in a star (source is not node 0)
    #[test]
    fn test_bfs_unreachable_nodes() {
        // 0 → 1 → 2; node 3 is isolated
        let node_ids = vec![0u64, 1, 2, 3];
        let edges = vec![(0u64, 1, 1.0), (1, 2, 1.0)];
        let params: HashMap<String, f64> = [("source".to_string(), 0.0)].into();

        let result = run_algorithm("bfs", params, &node_ids, &edges, 10);

        assert_eq!(result.get(&0), Some(&0.0));
        assert_eq!(result.get(&1), Some(&1.0));
        assert_eq!(result.get(&2), Some(&2.0));
        // Node 3 is unreachable — should not appear in results
        assert!(result.get(&3).is_none(), "unreachable node should not appear");
    }

    // ── SSSP tests ────────────────────────────────────────────────────────

    /// SSSP on a weighted chain: 0 -1→ 1 -2→ 2 -3→ 3
    #[test]
    fn test_sssp_weighted_chain() {
        let node_ids = vec![0u64, 1, 2, 3];
        let edges = vec![(0u64, 1, 1.0), (1, 2, 2.0), (2, 3, 3.0)];
        let params: HashMap<String, f64> = [("source".to_string(), 0.0)].into();

        let result = run_algorithm("sssp", params, &node_ids, &edges, 10);

        assert!((result[&0] - 0.0).abs() < 1e-9);
        assert!((result[&1] - 1.0).abs() < 1e-9);
        assert!((result[&2] - 3.0).abs() < 1e-9);
        assert!((result[&3] - 6.0).abs() < 1e-9);
    }

    /// SSSP: diamond graph (two paths)
    ///
    ///   0 -1→ 1 -1→ 3
    ///   0 -5→ 2 -1→ 3
    ///
    /// Shortest path from 0 to 3 is 0→1→3 = 2
    #[test]
    fn test_sssp_diamond_shortest_path() {
        let node_ids = vec![0u64, 1, 2, 3];
        let edges = vec![
            (0u64, 1, 1.0), // 0→1 cost 1
            (1, 3, 1.0),    // 1→3 cost 1
            (0, 2, 5.0),    // 0→2 cost 5
            (2, 3, 1.0),    // 2→3 cost 1
        ];
        let params: HashMap<String, f64> = [("source".to_string(), 0.0)].into();

        let result = run_algorithm("sssp", params, &node_ids, &edges, 10);

        assert!((result[&0] - 0.0).abs() < 1e-9, "dist(0)=0");
        assert!((result[&1] - 1.0).abs() < 1e-9, "dist(1)=1");
        assert!((result[&2] - 5.0).abs() < 1e-9, "dist(2)=5");
        assert!((result[&3] - 2.0).abs() < 1e-9, "dist(3)=2 via 0→1→3");
    }

    // ── PageRank tests ────────────────────────────────────────────────────

    /// PageRank on a single self-loop node: rank should converge to 1.0
    #[test]
    fn test_pagerank_single_node_self_loop() {
        let node_ids = vec![0u64];
        let edges = vec![(0u64, 0, 1.0)]; // self-loop
        let params: HashMap<String, f64> = [
            ("damping".to_string(), 0.85),
            ("n_total".to_string(), 1.0),
            ("max_iters".to_string(), 20.0),
        ].into();

        let result = run_algorithm("pagerank", params, &node_ids, &edges, 25);

        assert!(result.contains_key(&0), "node 0 should have a rank");
        let rank = result[&0];
        assert!(rank > 0.0, "rank should be positive");
        assert!(rank <= 1.5, "rank should be bounded");
    }

    /// PageRank on two nodes that link to each other: ranks should be equal
    #[test]
    fn test_pagerank_two_node_mutual() {
        // 0 ↔ 1
        let node_ids = vec![0u64, 1];
        let edges = vec![(0u64, 1, 1.0), (1, 0, 1.0)];
        let params: HashMap<String, f64> = [
            ("damping".to_string(), 0.85),
            ("n_total".to_string(), 2.0),
            ("max_iters".to_string(), 30.0),
        ].into();

        let result = run_algorithm("pagerank", params, &node_ids, &edges, 35);

        assert!(result.contains_key(&0) && result.contains_key(&1));
        let diff = (result[&0] - result[&1]).abs();
        assert!(diff < 0.1, "mutual link graph should have equal ranks; diff={}", diff);
    }

    /// PageRank: a "hub" node that all others point to should have higher rank
    #[test]
    fn test_pagerank_hub_has_highest_rank() {
        // 1 → 0, 2 → 0, 3 → 0, 0 → 1 (0 is the hub)
        let node_ids = vec![0u64, 1, 2, 3];
        let edges = vec![
            (1u64, 0, 1.0),
            (2, 0, 1.0),
            (3, 0, 1.0),
            (0, 1, 1.0),
        ];
        let params: HashMap<String, f64> = [
            ("damping".to_string(), 0.85),
            ("n_total".to_string(), 4.0),
            ("max_iters".to_string(), 50.0),
        ].into();

        let result = run_algorithm("pagerank", params, &node_ids, &edges, 55);

        // Node 0 receives from 1, 2, 3 so should have highest rank
        let rank_0 = result.get(&0).copied().unwrap_or(0.0);
        let rank_1 = result.get(&1).copied().unwrap_or(0.0);
        let rank_2 = result.get(&2).copied().unwrap_or(0.0);
        let rank_3 = result.get(&3).copied().unwrap_or(0.0);

        assert!(rank_0 > rank_2, "hub (0) should outrank leaf (2); {} vs {}", rank_0, rank_2);
        assert!(rank_0 > rank_3, "hub (0) should outrank leaf (3); {} vs {}", rank_0, rank_3);
        // Node 1 is directly fed by the hub, could be comparable
        let _ = (rank_1, rank_3); // not checking relative order of leaves
    }

    // ── Algorithm dispatch tests ──────────────────────────────────────────

    #[test]
    fn test_dispatch_unknown_algo_is_error() {
        let node_ids = vec![0u64];
        let edges: Vec<(u64, u64, f64)> = vec![];
        let req = SuperstepRequest {
            algo: "does_not_exist".to_string(),
            superstep: 0,
            params: HashMap::new(),
            messages: HashMap::new(),
        };
        let result = algorithms::dispatch(&req, &node_ids, &edges);
        assert!(result.is_err(), "unknown algorithm should return an error");
    }

    #[test]
    fn test_dispatch_case_insensitive() {
        let node_ids = vec![0u64, 1];
        let edges = vec![(0u64, 1, 1.0)];
        let req = SuperstepRequest {
            algo: "BFS".to_string(), // uppercase
            superstep: 0,
            params: [("source".to_string(), 0.0)].into(),
            messages: HashMap::new(),
        };
        let result = algorithms::dispatch(&req, &node_ids, &edges);
        assert!(result.is_ok(), "BFS (uppercase) should be dispatched correctly");
    }
}
