// Copyright 2026 Gregorio Momm
// Licensed under the Apache License, Version 2.0
//
// Ported from RustworkxCore (IBM Qiskit) — Apache License 2.0
// Source: https://github.com/Qiskit/rustworkx/tree/main/rustworkx-core/src/connectivity
//
// Academic References:
// - Hopcroft, J., & Tarjan, R. (1973). "Algorithm 447: efficient algorithms for
//   graph manipulation." Communications of the ACM, 16(6), 372-378.
// - Johnson, D.B. (1975). "Finding all the elementary circuits of a directed graph."
//   SIAM Journal on Computing, 4(1), 77-84.
// - Stoer, M., & Wagner, F. (1997). "A simple min-cut algorithm."
//   Journal of the ACM, 44(4), 585-591.

//! Connectivity algorithms ported from RustworkxCore.
//!
//! - `articulation_points` — cut vertices (Hopcroft-Tarjan)
//! - `bridges` — bridge edges (Hopcroft-Tarjan)
//! - `chain_decomposition` — decompose into chains
//! - `core_number` — k-core decomposition
//! - `cycle_basis` — fundamental cycle basis
//! - `johnson_simple_cycles` — enumerate all elementary cycles (Johnson 1975)
//! - `stoer_wagner_min_cut` — global minimum cut (Stoer-Wagner 1997)
//! - `isolates` — find isolated (degree-0) nodes
//! - `all_simple_paths` — enumerate simple paths between two nodes
//! - `number_connected_components` — count components
//! - `node_connected_component` — component of a specific node

use std::collections::{HashMap, HashSet, VecDeque};

use super::super::graph::{Graph, NodeId};

// ─── Articulation Points ──────────────────────────────────────────────────────

/// Find all articulation points (cut vertices) in an undirected graph.
///
/// An articulation point is a vertex whose removal increases the number of
/// connected components. Uses iterative DFS with Hopcroft-Tarjan low-link values.
///
/// Runtime: O(n + m)
///
/// Reference: Hopcroft & Tarjan (1973)
pub fn articulation_points(graph: &Graph) -> Vec<NodeId> {
    let n = graph.upper_node_id_bound() as usize;
    let mut disc = vec![None::<u32>; n];
    let mut low = vec![0u32; n];
    let mut is_ap = vec![false; n];
    let mut timer = 0u32;

    for start in graph.nodes() {
        if disc[start as usize].is_some() {
            continue;
        }

        // Iterative DFS to avoid stack overflow on large graphs
        // Stack entries: (node, parent, neighbor_index, child_count)
        let mut stack: Vec<(NodeId, Option<NodeId>, usize, u32)> = Vec::new();
        stack.push((start, None, 0, 0));
        disc[start as usize] = Some(timer);
        low[start as usize] = timer;
        timer += 1;

        while let Some((node, parent, idx, children)) = stack.last_mut() {
            let node = *node;
            let parent = *parent;
            let neighbors: Vec<NodeId> = graph.out_neighbors(node).iter().map(|e| e.target).collect();

            if *idx < neighbors.len() {
                let next = neighbors[*idx];
                *idx += 1;

                if disc[next as usize].is_none() {
                    *children += 1;
                    disc[next as usize] = Some(timer);
                    low[next as usize] = timer;
                    timer += 1;
                    stack.push((next, Some(node), 0, 0));
                } else if Some(next) != parent {
                    let next_disc = disc[next as usize].unwrap();
                    if next_disc < low[node as usize] {
                        low[node as usize] = next_disc;
                    }
                }
            } else {
                // Done with this node — propagate low values up
                let children = *children;
                stack.pop();

                if let Some((par_node, par_parent, _, _)) = stack.last() {
                    let par = *par_node;
                    let par_is_root = par_parent.is_none();
                    if low[node as usize] < low[par as usize] {
                        low[par as usize] = low[node as usize];
                    }
                    // AP condition for non-root parent: low[child] >= disc[parent]
                    if !par_is_root && low[node as usize] >= disc[par as usize].unwrap() {
                        is_ap[par as usize] = true;
                    }
                }

                // Root AP condition: root has more than one child in DFS tree
                if parent.is_none() && children > 1 {
                    is_ap[node as usize] = true;
                }
            }
        }
    }

    graph.nodes().filter(|&n| is_ap[n as usize]).collect()
}

// ─── Bridges ─────────────────────────────────────────────────────────────────

/// Find all bridge edges in an undirected graph.
///
/// A bridge is an edge whose removal disconnects the graph.
/// Uses iterative DFS with Hopcroft-Tarjan low-link values.
///
/// Runtime: O(n + m)
///
/// Reference: Hopcroft & Tarjan (1973)
pub fn bridges(graph: &Graph) -> Vec<(NodeId, NodeId)> {
    let n = graph.upper_node_id_bound() as usize;
    let mut disc = vec![None::<u32>; n];
    let mut low = vec![0u32; n];
    let mut result = Vec::new();
    let mut timer = 0u32;

    for start in graph.nodes() {
        if disc[start as usize].is_some() {
            continue;
        }

        let mut stack: Vec<(NodeId, Option<NodeId>, usize)> = Vec::new();
        disc[start as usize] = Some(timer);
        low[start as usize] = timer;
        timer += 1;
        stack.push((start, None, 0));

        while let Some((node, parent, idx)) = stack.last_mut() {
            let node = *node;
            let parent = *parent;
            let neighbors: Vec<NodeId> = graph.out_neighbors(node).iter().map(|e| e.target).collect();

            if *idx < neighbors.len() {
                let next = neighbors[*idx];
                *idx += 1;

                if disc[next as usize].is_none() {
                    disc[next as usize] = Some(timer);
                    low[next as usize] = timer;
                    timer += 1;
                    stack.push((next, Some(node), 0));
                } else if Some(next) != parent {
                    let next_disc = disc[next as usize].unwrap();
                    if next_disc < low[node as usize] {
                        low[node as usize] = next_disc;
                    }
                }
            } else {
                stack.pop();
                if let Some((par_node, _, _)) = stack.last() {
                    let par = *par_node;
                    if low[node as usize] < low[par as usize] {
                        low[par as usize] = low[node as usize];
                    }
                    // Bridge condition: low[child] > disc[parent]
                    if low[node as usize] > disc[par as usize].unwrap() {
                        result.push((par, node));
                    }
                }
            }
        }
    }

    result
}

// ─── Core Number (k-core decomposition) ──────────────────────────────────────

/// Compute the core number for each node (k-core decomposition).
///
/// The core number of a node is the largest k such that the node belongs
/// to the k-core subgraph (where every node has degree ≥ k).
///
/// Runtime: O(m)
///
/// Reference: Batagelj & Zaversnik (2003)
pub fn core_number(graph: &Graph) -> HashMap<NodeId, usize> {
    let mut degree: HashMap<NodeId, usize> = graph
        .nodes()
        .map(|n| (n, graph.degree(n)))
        .collect();

    let max_deg = degree.values().copied().max().unwrap_or(0);

    // Bucket sort by degree
    let mut buckets: Vec<Vec<NodeId>> = vec![Vec::new(); max_deg + 1];
    for (&node, &deg) in &degree {
        buckets[deg].push(node);
    }

    let mut core = HashMap::new();
    let mut processed = HashSet::new();

    for k in 0..=max_deg {
        // Process all nodes currently in this bucket
        let mut i = 0;
        while i < buckets[k].len() {
            let node = buckets[k][i];
            i += 1;

            if processed.contains(&node) {
                continue;
            }
            processed.insert(node);
            core.insert(node, k);

            // Reduce degree of neighbors
            for nbr in graph.out_neighbors(node).iter().map(|e| e.target) {
                if processed.contains(&nbr) {
                    continue;
                }
                let deg = degree[&nbr];
                if deg > k {
                    *degree.get_mut(&nbr).unwrap() = deg - 1;
                    buckets[deg - 1].push(nbr);
                }
            }
        }
    }

    core
}

// ─── Cycle Basis ─────────────────────────────────────────────────────────────

/// Find a fundamental cycle basis for an undirected graph.
///
/// Returns a minimum cycle basis (set of independent cycles that span
/// the cycle space). Uses DFS spanning tree approach.
///
/// Runtime: O((n + m)²) worst case
pub fn cycle_basis(graph: &Graph) -> Vec<Vec<NodeId>> {
    let n = graph.upper_node_id_bound() as usize;
    let mut visited = vec![false; n];
    let mut parent = vec![None::<NodeId>; n];
    let mut cycles = Vec::new();

    for start in graph.nodes() {
        if visited[start as usize] {
            continue;
        }

        let mut stack = vec![start];
        visited[start as usize] = true;

        while let Some(node) = stack.pop() {
            for nbr in graph.out_neighbors(node).iter().map(|e| e.target) {
                if !visited[nbr as usize] {
                    visited[nbr as usize] = true;
                    parent[nbr as usize] = Some(node);
                    stack.push(nbr);
                } else if parent[node as usize] != Some(nbr) {
                    // Back edge — extract cycle
                    let mut cycle = vec![nbr, node];
                    let mut cur = node;
                    while let Some(p) = parent[cur as usize] {
                        if p == nbr {
                            break;
                        }
                        cycle.push(p);
                        cur = p;
                    }
                    // Deduplicate: only store if we haven't seen this edge yet
                    let (u, v) = (node.min(nbr), node.max(nbr));
                    if !cycles.iter().any(|c: &Vec<NodeId>| {
                        c.contains(&u) && c.contains(&v) && c.len() == cycle.len()
                    }) {
                        cycles.push(cycle);
                    }
                }
            }
        }
    }

    cycles
}

// ─── Johnson Simple Cycles ────────────────────────────────────────────────────

/// Find all elementary (simple) cycles in a directed graph — Johnson's algorithm.
///
/// Returns all elementary circuits of a directed graph. A circuit is a closed
/// path where no node is repeated.
///
/// Runtime: O((n + m)(c + 1)) where c = number of circuits
///
/// Reference: Johnson, D.B. (1975)
pub fn johnson_simple_cycles(graph: &Graph) -> Vec<Vec<NodeId>> {
    let n = graph.upper_node_id_bound() as usize;
    let mut all_cycles = Vec::new();

    let nodes: Vec<NodeId> = graph.nodes().collect();

    let mut blocked = vec![false; n];
    let mut b_set: Vec<HashSet<NodeId>> = vec![HashSet::new(); n];
    let mut stack: Vec<NodeId> = Vec::new();

    fn unblock(node: NodeId, blocked: &mut Vec<bool>, b_set: &mut Vec<HashSet<NodeId>>) {
        blocked[node as usize] = false;
        let to_unblock: Vec<NodeId> = b_set[node as usize].drain().collect();
        for w in to_unblock {
            if blocked[w as usize] {
                unblock(w, blocked, b_set);
            }
        }
    }

    fn circuit(
        v: NodeId,
        s: NodeId,
        graph: &Graph,
        blocked: &mut Vec<bool>,
        b_set: &mut Vec<HashSet<NodeId>>,
        stack: &mut Vec<NodeId>,
        cycles: &mut Vec<Vec<NodeId>>,
        subgraph_nodes: &HashSet<NodeId>,
    ) -> bool {
        let mut found = false;
        stack.push(v);
        blocked[v as usize] = true;

        for w in graph.out_neighbors(v).iter().map(|e| e.target) {
            if !subgraph_nodes.contains(&w) {
                continue;
            }
            if w == s {
                // Found a cycle
                let mut cycle = stack.clone();
                cycle.push(s);
                cycles.push(cycle);
                found = true;
            } else if !blocked[w as usize] {
                if circuit(w, s, graph, blocked, b_set, stack, cycles, subgraph_nodes) {
                    found = true;
                }
            }
        }

        if found {
            unblock(v, blocked, b_set);
        } else {
            for w in graph.out_neighbors(v).iter().map(|e| e.target) {
                if subgraph_nodes.contains(&w) {
                    b_set[w as usize].insert(v);
                }
            }
        }

        stack.pop();
        found
    }

    for (i, &s) in nodes.iter().enumerate() {
        // Build subgraph with nodes >= s
        let subgraph_nodes: HashSet<NodeId> = nodes[i..].iter().copied().collect();

        // Reset blocked/b_set for subgraph nodes
        for &node in &subgraph_nodes {
            blocked[node as usize] = false;
            b_set[node as usize].clear();
        }

        circuit(
            s,
            s,
            graph,
            &mut blocked,
            &mut b_set,
            &mut stack,
            &mut all_cycles,
            &subgraph_nodes,
        );
    }

    all_cycles
}

// ─── Stoer-Wagner Min Cut ─────────────────────────────────────────────────────

/// Global minimum cut using the Stoer-Wagner algorithm.
///
/// Finds the minimum cut (minimum weight set of edges whose removal disconnects
/// the graph). Works on undirected weighted graphs.
///
/// Runtime: O(n³) or O(nm + n² log n) with priority queue
///
/// Reference: Stoer & Wagner (1997)
///
/// Returns (cut_weight, partition_a, partition_b).
pub fn stoer_wagner_min_cut(
    graph: &Graph,
) -> Option<(f64, Vec<NodeId>, Vec<NodeId>)> {
    let nodes: Vec<NodeId> = graph.nodes().collect();
    let n = nodes.len();
    if n < 2 {
        return None;
    }

    // Build adjacency map (symmetric, each undirected edge counted once)
    let mut adj: HashMap<(NodeId, NodeId), f64> = HashMap::new();
    for u in graph.nodes() {
        for e in graph.out_neighbors(u) {
            if u < e.target { // Only count each undirected edge once
                let w = e.weight.unwrap_or(1.0);
                *adj.entry((u, e.target)).or_insert(0.0) += w;
            }
        }
    }

    // Track merged super-nodes. Each super-node -> original nodes it contains
    let mut merged: HashMap<NodeId, Vec<NodeId>> = nodes.iter().map(|&n| (n, vec![n])).collect();
    let mut active: HashSet<NodeId> = nodes.iter().copied().collect();

    let mut best_cut = f64::INFINITY;
    let mut best_partition: Vec<NodeId> = Vec::new();

    // Repeat n-1 times
    while active.len() > 1 {
        // Minimum cut phase: find s and t (last two added in max-adjacency order)
        let mut in_a: HashSet<NodeId> = HashSet::new();
        let mut w: HashMap<NodeId, f64> = active.iter().map(|&n| (n, 0.0)).collect();

        let start = *active.iter().next().unwrap();
        in_a.insert(start);
        let mut last = start;
        let mut second_last = start;

        for _ in 1..active.len() {
            // Add most tightly connected node
            second_last = last;

            // Update w for neighbors of last
            for (&(u, v), &weight) in &adj {
                let other = if u == last && active.contains(&v) && !in_a.contains(&v) {
                    Some(v)
                } else if v == last && active.contains(&u) && !in_a.contains(&u) {
                    Some(u)
                } else {
                    None
                };
                if let Some(o) = other {
                    *w.get_mut(&o).unwrap() += weight;
                }
            }

            // Pick maximum w node not in A
            last = *active
                .iter()
                .filter(|n| !in_a.contains(n))
                .max_by(|&&a, &&b| w[&a].partial_cmp(&w[&b]).unwrap())
                .unwrap();

            in_a.insert(last);
        }

        // Cut of the phase = w(t)
        let cut_value = w[&last];
        if cut_value < best_cut {
            best_cut = cut_value;
            best_partition = merged[&last].clone();
        }

        // Merge last into second_last
        let last_nodes = merged.remove(&last).unwrap();
        merged.get_mut(&second_last).unwrap().extend(last_nodes);
        active.remove(&last);

        // Update adjacency: combine last's edges into second_last's
        let edges: Vec<((NodeId, NodeId), f64)> = adj
            .iter()
            .filter(|&(&(u, v), _)| u == last || v == last)
            .map(|(&k, &w)| (k, w))
            .collect();

        for ((u, v), weight) in edges {
            adj.remove(&(u, v));
            let other = if u == last { v } else { u };
            if other == second_last || !active.contains(&other) {
                continue;
            }
            let key = (other.min(second_last), other.max(second_last));
            *adj.entry(key).or_insert(0.0) += weight;
        }
    }

    let all_nodes: Vec<NodeId> = nodes.clone();
    let partition_b: Vec<NodeId> = all_nodes
        .iter()
        .copied()
        .filter(|n| !best_partition.contains(n))
        .collect();

    Some((best_cut, best_partition, partition_b))
}

// ─── Isolates ─────────────────────────────────────────────────────────────────

/// Find all isolated nodes (nodes with degree 0).
///
/// Runtime: O(n)
pub fn isolates(graph: &Graph) -> Vec<NodeId> {
    graph.nodes().filter(|&n| graph.degree(n) == 0).collect()
}

// ─── All Simple Paths ─────────────────────────────────────────────────────────

/// Find all simple paths from `source` to `target`.
///
/// A simple path visits each node at most once.
/// Optionally limit by `cutoff` (max path length).
///
/// Runtime: O(n! / (n-k)!) worst case
pub fn all_simple_paths(
    graph: &Graph,
    source: NodeId,
    target: NodeId,
    cutoff: Option<usize>,
) -> Vec<Vec<NodeId>> {
    let mut results = Vec::new();
    let max_len = cutoff.unwrap_or(usize::MAX / 2);

    let mut stack: Vec<(NodeId, Vec<NodeId>, HashSet<NodeId>)> = Vec::new();
    let mut visited = HashSet::new();
    visited.insert(source);
    stack.push((source, vec![source], visited));

    while let Some((node, path, visited)) = stack.pop() {
        let edges_so_far = path.len() - 1; // edges traversed to reach `node`
        for nbr in graph.out_neighbors(node).iter().map(|e| e.target) {
            if nbr == target && edges_so_far + 1 <= max_len {
                let mut full_path = path.clone();
                full_path.push(nbr);
                results.push(full_path);
            } else if nbr != target && !visited.contains(&nbr) && edges_so_far + 1 < max_len {
                let mut new_path = path.clone();
                new_path.push(nbr);
                let mut new_visited = visited.clone();
                new_visited.insert(nbr);
                stack.push((nbr, new_path, new_visited));
            }
        }
    }

    results
}

// ─── All-pairs simple paths ────────────────────────────────────────────────────

/// Enumerate all simple paths between every ordered pair of distinct nodes.
///
/// This calls [`all_simple_paths`] for every (source, target) pair.  The result
/// is a `HashMap` from `(source, target)` to the list of simple paths.
///
/// # Parameters
/// - `cutoff` — optional maximum path length (number of edges)
///
/// # Warning
/// Exponential in the number of paths — only practical for small graphs or with
/// a small `cutoff`.
pub fn all_pairs_all_simple_paths(
    graph: &Graph,
    cutoff: Option<usize>,
) -> HashMap<(NodeId, NodeId), Vec<Vec<NodeId>>> {
    let nodes: Vec<NodeId> = graph.nodes().collect();
    let mut result: HashMap<(NodeId, NodeId), Vec<Vec<NodeId>>> = HashMap::new();
    for &src in &nodes {
        for &tgt in &nodes {
            if src == tgt { continue; }
            let paths = all_simple_paths(graph, src, tgt, cutoff);
            if !paths.is_empty() {
                result.insert((src, tgt), paths);
            }
        }
    }
    result
}

// ─── Number Connected Components ─────────────────────────────────────────────

/// Count the number of connected components in an undirected graph.
///
/// Runtime: O(n + m)
pub fn number_connected_components(graph: &Graph) -> usize {
    let n = graph.upper_node_id_bound() as usize;
    let mut visited = vec![false; n];
    let mut count = 0;

    for start in graph.nodes() {
        if visited[start as usize] {
            continue;
        }
        count += 1;
        let mut queue = VecDeque::new();
        queue.push_back(start);
        visited[start as usize] = true;
        while let Some(node) = queue.pop_front() {
            for nbr in graph.out_neighbors(node).iter().map(|e| e.target) {
                if !visited[nbr as usize] {
                    visited[nbr as usize] = true;
                    queue.push_back(nbr);
                }
            }
        }
    }

    count
}

/// Get all nodes in the same connected component as `node`.
///
/// Runtime: O(n + m)
pub fn node_connected_component(graph: &Graph, node: NodeId) -> Vec<NodeId> {
    let n = graph.upper_node_id_bound() as usize;
    let mut visited = vec![false; n];
    let mut component = Vec::new();
    let mut queue = VecDeque::new();

    visited[node as usize] = true;
    queue.push_back(node);

    while let Some(cur) = queue.pop_front() {
        component.push(cur);
        for nbr in graph.out_neighbors(cur).iter().map(|e| e.target) {
            if !visited[nbr as usize] {
                visited[nbr as usize] = true;
                queue.push_back(nbr);
            }
        }
    }

    component
}

// ─── Connectivity Predicates ──────────────────────────────────────────────────

/// Return `true` if the undirected graph is connected (one component).
///
/// For a directed graph, treats all edges as undirected (weak connectivity).
/// Runtime: O(n + m)
pub fn is_connected(graph: &Graph) -> bool {
    let node_count = graph.nodes().count();
    if node_count == 0 {
        return true;
    }
    let start = match graph.nodes().next() {
        Some(n) => n,
        None => return true,
    };
    node_connected_component(graph, start).len() == node_count
}

/// Return `true` if the directed graph is strongly connected.
///
/// Uses two BFS passes: forward from a start node, then backward (reversing
/// all edges). If both reach every node the graph is strongly connected.
/// Runtime: O(n + m)
pub fn is_strongly_connected(graph: &Graph) -> bool {
    let node_count = graph.nodes().count();
    if node_count == 0 {
        return true;
    }
    let start = match graph.nodes().next() {
        Some(n) => n,
        None => return true,
    };
    let n = graph.upper_node_id_bound() as usize;

    // Forward BFS
    let mut fwd = vec![false; n];
    let mut queue = VecDeque::new();
    fwd[start as usize] = true;
    queue.push_back(start);
    while let Some(u) = queue.pop_front() {
        for e in graph.out_neighbors(u).iter() {
            if !fwd[e.target as usize] {
                fwd[e.target as usize] = true;
                queue.push_back(e.target);
            }
        }
    }
    if fwd.iter().enumerate().any(|(i, &v)| !v && graph.nodes().any(|n| n as usize == i)) {
        return false;
    }

    // Backward BFS (reverse edges)
    // Build reverse adjacency
    let mut rev: Vec<Vec<NodeId>> = vec![Vec::new(); n];
    for u in graph.nodes() {
        for e in graph.out_neighbors(u).iter() {
            rev[e.target as usize].push(u);
        }
    }
    let mut bwd = vec![false; n];
    bwd[start as usize] = true;
    queue.push_back(start);
    while let Some(u) = queue.pop_front() {
        for &v in &rev[u as usize] {
            if !bwd[v as usize] {
                bwd[v as usize] = true;
                queue.push_back(v);
            }
        }
    }
    graph.nodes().all(|n| bwd[n as usize])
}

/// Return `true` if the directed graph is weakly connected.
///
/// Treats all directed edges as undirected and checks connectivity.
/// Runtime: O(n + m)
pub fn is_weakly_connected(graph: &Graph) -> bool {
    is_connected(graph)
}

/// Return `true` if the graph is bipartite (2-colorable).
///
/// Uses BFS 2-coloring. Works for directed and undirected graphs
/// (directed edges are ignored for the bipartite check; the underlying
/// undirected structure is checked).
/// Runtime: O(n + m)
pub fn is_bipartite(graph: &Graph) -> bool {
    let n = graph.upper_node_id_bound() as usize;
    let mut color: Vec<Option<bool>> = vec![None; n];

    for start in graph.nodes() {
        if color[start as usize].is_some() {
            continue;
        }
        color[start as usize] = Some(false);
        let mut queue = VecDeque::new();
        queue.push_back(start);
        while let Some(u) = queue.pop_front() {
            let u_color = color[u as usize].unwrap();
            for e in graph.out_neighbors(u).iter() {
                let v = e.target;
                match color[v as usize] {
                    None => {
                        color[v as usize] = Some(!u_color);
                        queue.push_back(v);
                    }
                    Some(c) if c == u_color => return false,
                    _ => {}
                }
            }
        }
    }
    true
}

/// Return the number of strongly connected components in a directed graph.
///
/// Runtime: O(n + m)
pub fn number_strongly_connected_components(graph: &Graph) -> usize {
    use super::components::StronglyConnectedComponents;
    StronglyConnectedComponents::run(graph).num_components
}

/// Return the number of weakly connected components in a directed graph.
///
/// Treats all edges as undirected. Runtime: O(n + m)
pub fn number_weakly_connected_components(graph: &Graph) -> usize {
    number_connected_components(graph)
}

// ─── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::graph::backends::networkit_rust::graph::GraphConfig;

    fn path_graph(n: usize) -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..n { g.add_node(); }
        for i in 0..(n as NodeId - 1) { g.add_edge(i, i + 1, None); }
        g
    }

    fn cycle_graph_undirected(n: usize) -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..n { g.add_node(); }
        for i in 0..(n as NodeId) { g.add_edge(i, (i + 1) % n as NodeId, None); }
        g
    }

    // ── articulation_points ───────────────────────────────────────────────────

    #[test]
    fn test_articulation_points_path() {
        // In a path 0-1-2-3, middle nodes (1,2) are articulation points
        let g = path_graph(4);
        let mut aps = articulation_points(&g);
        aps.sort();
        assert_eq!(aps, vec![1, 2]);
    }

    #[test]
    fn test_articulation_points_cycle() {
        // Cycle has no articulation points
        let g = cycle_graph_undirected(4);
        let aps = articulation_points(&g);
        assert!(aps.is_empty(), "cycle should have no APs, got {:?}", aps);
    }

    #[test]
    fn test_articulation_points_star() {
        // Star: center is an AP
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..5 { g.add_node(); }
        for i in 1..5u64 { g.add_edge(0, i, None); }
        let aps = articulation_points(&g);
        assert_eq!(aps, vec![0]);
    }

    // ── bridges ───────────────────────────────────────────────────────────────

    #[test]
    fn test_bridges_path() {
        // Every edge in a path is a bridge
        let g = path_graph(4);
        let mut brs = bridges(&g);
        brs.sort();
        assert_eq!(brs.len(), 3);
    }

    #[test]
    fn test_bridges_cycle() {
        // Cycle has no bridges
        let g = cycle_graph_undirected(4);
        let brs = bridges(&g);
        assert!(brs.is_empty(), "cycle should have no bridges, got {:?}", brs);
    }

    // ── core_number ───────────────────────────────────────────────────────────

    #[test]
    fn test_core_number_path() {
        let g = path_graph(4);
        let cores = core_number(&g);
        // Endpoints have degree 1 → core 1; inner nodes have degree 2 → core 1
        // In a path all nodes are in 1-core (no 2-core since removing any 1-degree node works)
        for &v in cores.values() {
            assert!(v <= 1);
        }
    }

    #[test]
    fn test_core_number_complete() {
        // K4: every node has degree 3, so all are in 3-core
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        for i in 0..4u64 { for j in (i+1)..4 { g.add_edge(i, j, None); } }
        let cores = core_number(&g);
        for (&node, &k) in &cores {
            assert_eq!(k, 3, "node {} should have core 3, got {}", node, k);
        }
    }

    // ── cycle_basis ───────────────────────────────────────────────────────────

    #[test]
    fn test_cycle_basis_cycle() {
        let g = cycle_graph_undirected(4);
        let basis = cycle_basis(&g);
        // A 4-cycle should produce 1 fundamental cycle
        assert!(!basis.is_empty());
    }

    #[test]
    fn test_cycle_basis_tree() {
        // A tree has no cycles
        let g = path_graph(4);
        let basis = cycle_basis(&g);
        assert!(basis.is_empty(), "tree should have empty cycle basis");
    }

    // ── johnson_simple_cycles ─────────────────────────────────────────────────

    #[test]
    fn test_johnson_simple_cycles_directed_triangle() {
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(2, 0, None);
        let cycles = johnson_simple_cycles(&g);
        // Should find the single elementary cycle 0→1→2→0
        assert!(!cycles.is_empty());
    }

    #[test]
    fn test_johnson_simple_cycles_dag() {
        // DAG has no cycles
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        let cycles = johnson_simple_cycles(&g);
        assert!(cycles.is_empty());
    }

    // ── stoer_wagner_min_cut ──────────────────────────────────────────────────

    #[test]
    fn test_stoer_wagner_simple() {
        // Two triangles connected by a single edge: min cut = 1
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..6 { g.add_node(); }
        // Triangle 1
        g.add_edge(0, 1, Some(1.0));
        g.add_edge(1, 2, Some(1.0));
        g.add_edge(2, 0, Some(1.0));
        // Triangle 2
        g.add_edge(3, 4, Some(1.0));
        g.add_edge(4, 5, Some(1.0));
        g.add_edge(5, 3, Some(1.0));
        // Bridge
        g.add_edge(2, 3, Some(1.0));

        let (cut, _a, _b) = stoer_wagner_min_cut(&g).unwrap();
        assert!((cut - 1.0).abs() < 1e-9, "expected cut=1, got {}", cut);
    }

    // ── isolates ──────────────────────────────────────────────────────────────

    #[test]
    fn test_isolates() {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None);
        // Nodes 2 and 3 are isolated
        let mut iso = isolates(&g);
        iso.sort();
        assert_eq!(iso, vec![2, 3]);
    }

    // ── all_simple_paths ──────────────────────────────────────────────────────

    #[test]
    fn test_all_simple_paths() {
        let g = path_graph(4); // 0-1-2-3
        let paths = all_simple_paths(&g, 0, 3, None);
        // In a path graph there is exactly one simple path from 0 to 3
        assert_eq!(paths.len(), 1);
        assert_eq!(paths[0], vec![0, 1, 2, 3]);
    }

    #[test]
    fn test_all_simple_paths_cutoff() {
        // Complete graph K4 — many paths from 0 to 3
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        for i in 0..4u64 { for j in (i+1)..4 { g.add_edge(i, j, None); } }
        // With cutoff=1 only direct edge 0→3
        let paths = all_simple_paths(&g, 0, 3, Some(1));
        assert!(paths.iter().all(|p| p.len() <= 2));
    }

    // ── all_pairs_all_simple_paths ────────────────────────────────────────────

    #[test]
    fn test_all_pairs_all_simple_paths_triangle() {
        // Triangle 0-1-2: every pair has paths
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None); g.add_edge(0, 2, None);
        let ap = all_pairs_all_simple_paths(&g, None);
        // 3 ordered pairs × 2 paths each (direct + through third node)
        assert_eq!(ap.len(), 6);
    }

    #[test]
    fn test_all_pairs_all_simple_paths_disconnected() {
        // Two disjoint edges: 0-1 and 2-3; no cross paths
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(2, 3, None);
        let ap = all_pairs_all_simple_paths(&g, None);
        // Only (0,1), (1,0), (2,3), (3,2)
        assert_eq!(ap.len(), 4);
    }

    // ── number_connected_components / node_connected_component ────────────────

    #[test]
    fn test_number_connected_components() {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..6 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(3, 4, None);
        // Node 5 is isolated
        assert_eq!(number_connected_components(&g), 3);
    }

    #[test]
    fn test_node_connected_component() {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..6 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(3, 4, None);
        let mut comp = node_connected_component(&g, 0);
        comp.sort();
        assert_eq!(comp, vec![0, 1, 2]);
    }
}

// ─── Chain Decomposition ──────────────────────────────────────────────────────

/// Decompose the graph into chains.
///
/// A chain decomposition partitions the edges into chains (sequences of edges
/// where each edge shares an endpoint with the next). This is related to
/// DFS back-edge structure.
///
/// The algorithm runs DFS and groups back-edges with their tree-path predecessors.
/// Each chain starts at a back-edge and follows tree-path edges back to the root
/// of the subtree.
///
/// Returns a list of chains, each chain being an ordered list of edges (u, v).
///
/// Reference: Schmidt, J.M. (2013). "A simple test on 2-vertex- and 2-edge-connectivity."
/// Information Processing Letters, 113(7), 241-244.
pub fn chain_decomposition(graph: &Graph) -> Vec<Vec<(NodeId, NodeId)>> {
    let mut chains: Vec<Vec<(NodeId, NodeId)>> = Vec::new();
    let mut visited: HashSet<NodeId> = HashSet::new();
    // DFS depth and parent tracking
    let mut depth: HashMap<NodeId, usize> = HashMap::new();
    let mut parent: HashMap<NodeId, NodeId> = HashMap::new();
    let mut used_back_edges: HashSet<(NodeId, NodeId)> = HashSet::new();

    fn dfs_chain(
        graph: &Graph,
        u: NodeId,
        d: usize,
        visited: &mut HashSet<NodeId>,
        depth: &mut HashMap<NodeId, usize>,
        parent: &mut HashMap<NodeId, NodeId>,
        chains: &mut Vec<Vec<(NodeId, NodeId)>>,
        used_back_edges: &mut HashSet<(NodeId, NodeId)>,
    ) {
        visited.insert(u);
        depth.insert(u, d);
        for e in graph.out_neighbors(u).iter() {
            let v = e.target;
            if !visited.contains(&v) {
                parent.insert(v, u);
                dfs_chain(graph, v, d + 1, visited, depth, parent, chains, used_back_edges);
            } else if parent.get(&u).copied() != Some(v) {
                // Back edge u → v (v is ancestor of u)
                let key = if depth[&u] > depth[&v] { (v, u) } else { (u, v) };
                if used_back_edges.insert(key) {
                    // Build chain: trace path from u to v using parent links
                    let mut chain: Vec<(NodeId, NodeId)> = vec![(u, v)];
                    let mut cur = u;
                    while cur != v {
                        let par = match parent.get(&cur) { Some(&p) => p, None => break };
                        chain.push((cur, par));
                        cur = par;
                    }
                    chains.push(chain);
                }
            }
        }
    }

    for start in graph.nodes() {
        if !visited.contains(&start) {
            dfs_chain(graph, start, 0, &mut visited, &mut depth, &mut parent, &mut chains, &mut used_back_edges);
        }
    }

    chains
}

// ─── Find a Single Cycle ─────────────────────────────────────────────────────

/// Find a single cycle in the graph, if one exists.
///
/// Returns `Some(cycle)` where `cycle` is a list of nodes forming a cycle,
/// or `None` if the graph is acyclic.
///
/// Uses DFS and detects the first back-edge found.
///
/// Runtime: O(n + m)
pub fn find_cycle(graph: &Graph) -> Option<Vec<NodeId>> {
    let mut visited: HashSet<NodeId> = HashSet::new();
    let mut in_stack: HashMap<NodeId, usize> = HashMap::new(); // node → stack index
    let mut stack: Vec<NodeId> = Vec::new();

    fn dfs_cycle(
        graph: &Graph,
        u: NodeId,
        parent: Option<NodeId>,
        visited: &mut HashSet<NodeId>,
        in_stack: &mut HashMap<NodeId, usize>,
        stack: &mut Vec<NodeId>,
    ) -> Option<Vec<NodeId>> {
        visited.insert(u);
        let stack_idx = stack.len();
        in_stack.insert(u, stack_idx);
        stack.push(u);

        for e in graph.out_neighbors(u).iter() {
            let v = e.target;
            if Some(v) == parent { continue; } // Skip tree edge back to parent (undirected)
            if in_stack.contains_key(&v) {
                // Found back-edge u → v; extract cycle
                let v_idx = in_stack[&v];
                return Some(stack[v_idx..].to_vec());
            }
            if !visited.contains(&v) {
                if let Some(cycle) = dfs_cycle(graph, v, Some(u), visited, in_stack, stack) {
                    return Some(cycle);
                }
            }
        }

        stack.pop();
        in_stack.remove(&u);
        None
    }

    for start in graph.nodes() {
        if !visited.contains(&start) {
            if let Some(cycle) = dfs_cycle(graph, start, None, &mut visited, &mut in_stack, &mut stack) {
                return Some(cycle);
            }
        }
    }
    None
}

// ─── Tests for chain_decomposition and find_cycle ─────────────────────────────

#[cfg(test)]
mod chain_cycle_tests {
    use super::*;
    use crate::graph::backends::networkit_rust::graph::GraphConfig;

    fn triangle() -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(2, 0, None);
        g
    }

    fn tree4() -> Graph {
        // Tree: 0-1, 0-2, 1-3 (no cycles)
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(0, 2, None);
        g.add_edge(1, 3, None);
        g
    }

    // ── chain_decomposition ───────────────────────────────────────────────────

    #[test]
    fn test_chain_decomposition_tree() {
        // Tree has no back-edges → no chains
        let g = tree4();
        let chains = chain_decomposition(&g);
        assert!(chains.is_empty(), "tree has no chains, got {:?}", chains);
    }

    #[test]
    fn test_chain_decomposition_triangle() {
        // Triangle has one back-edge → one chain
        let g = triangle();
        let chains = chain_decomposition(&g);
        assert!(!chains.is_empty(), "triangle should have at least one chain");
    }

    #[test]
    fn test_chain_decomposition_empty() {
        let g = Graph::new(GraphConfig::simple());
        let chains = chain_decomposition(&g);
        assert!(chains.is_empty());
    }

    #[test]
    fn test_chain_decomposition_path() {
        // Path has no back-edges
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(2, 3, None);
        let chains = chain_decomposition(&g);
        assert!(chains.is_empty(), "path has no chains");
    }

    // ── find_cycle ────────────────────────────────────────────────────────────

    #[test]
    fn test_find_cycle_triangle() {
        let g = triangle();
        let cycle = find_cycle(&g).expect("triangle has a cycle");
        assert!(cycle.len() >= 3, "cycle in triangle has ≥3 nodes: {:?}", cycle);
    }

    #[test]
    fn test_find_cycle_tree() {
        // Tree is acyclic
        let g = tree4();
        assert!(find_cycle(&g).is_none(), "tree should have no cycle");
    }

    #[test]
    fn test_find_cycle_path() {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..5 { g.add_node(); }
        for i in 0..4u64 { g.add_edge(i, i+1, None); }
        assert!(find_cycle(&g).is_none(), "path should have no cycle");
    }

    #[test]
    fn test_find_cycle_empty() {
        let g = Graph::new(GraphConfig::simple());
        assert!(find_cycle(&g).is_none());
    }

    #[test]
    fn test_find_cycle_k4() {
        // K4 has many cycles
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        for i in 0..4u64 { for j in (i+1)..4 { g.add_edge(i, j, None); } }
        let cycle = find_cycle(&g).expect("K4 has cycles");
        assert!(cycle.len() >= 3);
    }
}

// ── Connectivity predicate tests ───────────────────────────────────────────────

#[cfg(test)]
mod predicate_tests {
    use super::*;
    use crate::graph::backends::networkit_rust::graph::GraphConfig;

    fn path4() -> Graph {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        for i in 0..3u64 { g.add_edge(i, i + 1, None); }
        g
    }

    fn two_component() -> Graph {
        // 0-1  2-3 (disconnected)
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(2, 3, None);
        g
    }

    // ── is_connected ─────────────────────────────────────────────────────────

    #[test]
    fn test_is_connected_path() {
        assert!(is_connected(&path4()));
    }

    #[test]
    fn test_is_connected_disconnected() {
        assert!(!is_connected(&two_component()));
    }

    #[test]
    fn test_is_connected_empty() {
        let g = Graph::new(GraphConfig::simple());
        assert!(is_connected(&g));
    }

    #[test]
    fn test_is_connected_single_node() {
        let mut g = Graph::new(GraphConfig::simple());
        g.add_node();
        assert!(is_connected(&g));
    }

    // ── is_strongly_connected ────────────────────────────────────────────────

    #[test]
    fn test_is_strongly_connected_cycle() {
        // Directed cycle 0→1→2→0 is strongly connected
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(2, 0, None);
        assert!(is_strongly_connected(&g));
    }

    #[test]
    fn test_is_strongly_connected_path() {
        // Directed path 0→1→2 is NOT strongly connected
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        assert!(!is_strongly_connected(&g));
    }

    #[test]
    fn test_is_strongly_connected_empty() {
        let g = Graph::new(GraphConfig::directed());
        assert!(is_strongly_connected(&g));
    }

    // ── is_bipartite ─────────────────────────────────────────────────────────

    #[test]
    fn test_is_bipartite_path() {
        // Any path is bipartite
        assert!(is_bipartite(&path4()));
    }

    #[test]
    fn test_is_bipartite_even_cycle() {
        // Even cycle is bipartite
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        for i in 0..4u64 { g.add_edge(i, (i + 1) % 4, None); }
        assert!(is_bipartite(&g));
    }

    #[test]
    fn test_is_bipartite_odd_cycle() {
        // Triangle is NOT bipartite
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(2, 0, None);
        assert!(!is_bipartite(&g));
    }

    #[test]
    fn test_is_bipartite_complete_bipartite() {
        // K_{2,3}: two sides {0,1} and {2,3,4}
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..5 { g.add_node(); }
        for u in 0..2u64 { for v in 2..5u64 { g.add_edge(u, v, None); } }
        assert!(is_bipartite(&g));
    }

    // ── number_strongly_connected_components ─────────────────────────────────

    #[test]
    fn test_number_scc_cycle_plus_isolated() {
        // Directed cycle 0→1→2→0 plus isolated node 3
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(1, 2, None);
        g.add_edge(2, 0, None);
        // Node 3 is its own SCC
        assert_eq!(number_strongly_connected_components(&g), 2);
    }

    #[test]
    fn test_number_weakly_connected() {
        assert_eq!(number_weakly_connected_components(&two_component()), 2);
    }
}

// ─── has_path ─────────────────────────────────────────────────────────────────

/// Return `true` if there is a path from `source` to `target`.
///
/// For undirected graphs any edge can be traversed in either direction.
/// Uses BFS from source; stops as soon as target is found.
/// Runtime: O(n + m)
pub fn has_path(graph: &Graph, source: NodeId, target: NodeId) -> bool {
    if source == target { return true; }
    let n = graph.upper_node_id_bound() as usize;
    let mut visited = vec![false; n];
    let mut queue = VecDeque::new();
    visited[source as usize] = true;
    queue.push_back(source);
    while let Some(u) = queue.pop_front() {
        for e in graph.out_neighbors(u).iter() {
            let v = e.target;
            if v == target { return true; }
            if !visited[v as usize] {
                visited[v as usize] = true;
                queue.push_back(v);
            }
        }
    }
    false
}

// ─── biconnected_components ───────────────────────────────────────────────────

/// Decompose an undirected graph into its biconnected components.
///
/// A biconnected component is a maximal subgraph with no articulation point.
/// Returns a list of components, each expressed as a set of edges `(u, v)`.
///
/// Uses the standard DFS-based algorithm (Hopcroft-Tarjan).
/// Runtime: O(n + m)
pub fn biconnected_components(graph: &Graph) -> Vec<Vec<(NodeId, NodeId)>> {
    let n = graph.upper_node_id_bound() as usize;
    let mut disc   = vec![None::<u32>; n];
    let mut low    = vec![0u32; n];
    let mut timer  = 0u32;
    let mut stack: Vec<(NodeId, NodeId)> = Vec::new(); // edge stack
    let mut components: Vec<Vec<(NodeId, NodeId)>> = Vec::new();

    for start in graph.nodes() {
        if disc[start as usize].is_some() { continue; }
        // Iterative DFS
        // State: (node, parent, child_iterator_index)
        let mut dfs: Vec<(NodeId, NodeId, usize)> = vec![(start, NodeId::MAX, 0)];
        disc[start as usize] = Some(timer);
        low[start as usize]  = timer;
        timer += 1;

        while let Some((u, parent, child_idx)) = dfs.last_mut() {
            let u = *u;
            let parent = *parent;
            let neighbors: Vec<NodeId> = graph.out_neighbors(u).iter().map(|e| e.target).collect();

            if *child_idx < neighbors.len() {
                let v = neighbors[*child_idx];
                *child_idx += 1;

                if disc[v as usize].is_none() {
                    // Tree edge
                    disc[v as usize] = Some(timer);
                    low[v as usize]  = timer;
                    timer += 1;
                    stack.push((u, v));
                    dfs.push((v, u, 0));
                } else if v != parent && disc[v as usize].unwrap() < disc[u as usize].unwrap() {
                    // Back edge
                    if disc[v as usize].unwrap() < low[u as usize] {
                        low[u as usize] = disc[v as usize].unwrap();
                    }
                    stack.push((u, v));
                }
            } else {
                dfs.pop();
                if let Some(&(pu, _, _)) = dfs.last() {
                    // Propagate low
                    if low[u as usize] < low[pu as usize] {
                        low[pu as usize] = low[u as usize];
                    }
                    // If pu is an articulation point w.r.t. u, pop component
                    if low[u as usize] >= disc[pu as usize].unwrap() {
                        let mut comp = Vec::new();
                        while let Some(&top) = stack.last() {
                            if top == (pu, u) || top == (u, pu) {
                                comp.push(stack.pop().unwrap());
                                break;
                            }
                            comp.push(stack.pop().unwrap());
                        }
                        if !comp.is_empty() {
                            components.push(comp);
                        }
                    }
                } else if !stack.is_empty() {
                    // Root: collect remaining stack as one component
                    let comp = stack.drain(..).collect();
                    components.push(comp);
                }
            }
        }
    }

    components
}

// ─── Tree / Forest predicates ─────────────────────────────────────────────────

/// Return `true` if the undirected graph is a tree.
///
/// A tree is a connected acyclic undirected graph.
/// Equivalently: n nodes, exactly n−1 edges, and connected.
pub fn is_tree(graph: &Graph) -> bool {
    let n = graph.node_count();
    if n == 0 { return true; }
    // A tree has exactly n-1 edges and is connected
    graph.edge_count() == n - 1 && is_connected(graph)
}

/// Return `true` if the undirected graph is a forest.
///
/// A forest is an acyclic undirected graph (union of trees).
/// Equivalently: each connected component is a tree.
pub fn is_forest(graph: &Graph) -> bool {
    // Forest ↔ acyclic ↔ edges = nodes - number_of_components
    let n = graph.node_count();
    if n == 0 { return true; }
    let c = number_connected_components(graph);
    graph.edge_count() == n - c
}

#[cfg(test)]
mod biconnected_has_path_tests {
    use super::*;
    use crate::graph::backends::networkit_rust::graph::GraphConfig;

    // ── has_path ──────────────────────────────────────────────────────────────

    #[test]
    fn test_has_path_connected() {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None); g.add_edge(2, 3, None);
        assert!(has_path(&g, 0, 3));
        assert!(has_path(&g, 3, 0));
    }

    #[test]
    fn test_has_path_disconnected() {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None);
        g.add_edge(2, 3, None);
        assert!(!has_path(&g, 0, 3));
    }

    #[test]
    fn test_has_path_self() {
        let mut g = Graph::new(GraphConfig::simple());
        g.add_node();
        assert!(has_path(&g, 0, 0));
    }

    #[test]
    fn test_has_path_directed() {
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None);
        assert!(has_path(&g, 0, 2));
        assert!(!has_path(&g, 2, 0)); // directed: no reverse path
    }

    // ── biconnected_components ────────────────────────────────────────────────

    #[test]
    fn test_biconnected_triangle() {
        // K3 is biconnected: one component with all 3 edges
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None); g.add_edge(0, 2, None);
        let bcs = biconnected_components(&g);
        assert_eq!(bcs.len(), 1);
        assert_eq!(bcs[0].len(), 3);
    }

    #[test]
    fn test_biconnected_path() {
        // Path 0-1-2-3: each edge is its own biconnected component
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None); g.add_edge(2, 3, None);
        let bcs = biconnected_components(&g);
        assert_eq!(bcs.len(), 3, "path has 3 bcc: {:?}", bcs);
    }

    #[test]
    fn test_biconnected_two_triangles_sharing_vertex() {
        // Two K3 sharing node 2: 0-1-2-0 and 2-3-4-2
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..5 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None); g.add_edge(0, 2, None);
        g.add_edge(2, 3, None); g.add_edge(3, 4, None); g.add_edge(2, 4, None);
        let bcs = biconnected_components(&g);
        assert_eq!(bcs.len(), 2, "two triangles sharing a vertex → 2 bcc: {:?}", bcs);
    }

    #[test]
    fn test_biconnected_empty() {
        let g = Graph::new(GraphConfig::simple());
        assert!(biconnected_components(&g).is_empty());
    }

    // ── is_tree ───────────────────────────────────────────────────────────────

    #[test]
    fn test_is_tree_path() {
        // Path 0-1-2-3 is a tree
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None); g.add_edge(2, 3, None);
        assert!(is_tree(&g));
    }

    #[test]
    fn test_is_tree_single_node() {
        let mut g = Graph::new(GraphConfig::simple());
        g.add_node();
        assert!(is_tree(&g));
    }

    #[test]
    fn test_is_not_tree_cycle() {
        // C4 has n edges, not n-1
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None);
        g.add_edge(2, 3, None); g.add_edge(3, 0, None);
        assert!(!is_tree(&g));
    }

    #[test]
    fn test_is_not_tree_disconnected() {
        // Two disconnected edges: 0-1, 2-3 (disconnected)
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..4 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(2, 3, None);
        assert!(!is_tree(&g));
    }

    // ── is_forest ─────────────────────────────────────────────────────────────

    #[test]
    fn test_is_forest_two_trees() {
        // Two disjoint paths: 0-1-2 and 3-4
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..5 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None);
        g.add_edge(3, 4, None);
        assert!(is_forest(&g));
    }

    #[test]
    fn test_is_forest_cycle_not_forest() {
        let mut g = Graph::new(GraphConfig::simple());
        for _ in 0..3 { g.add_node(); }
        g.add_edge(0, 1, None); g.add_edge(1, 2, None); g.add_edge(0, 2, None);
        assert!(!is_forest(&g));
    }

    #[test]
    fn test_is_forest_empty() {
        let g = Graph::new(GraphConfig::simple());
        assert!(is_forest(&g));
    }
}
