//! DAG (Directed Acyclic Graph) algorithms.
//!
//! Ported from RustworkxCore and classical algorithm sources.
//!
//! Algorithms:
//! - topological_sort: Kahn's BFS-based topological ordering
//! - topological_sort_dfs: DFS-based topological ordering
//! - is_dag: Check if directed graph is acyclic
//! - dag_longest_path: Longest path in a DAG
//! - dag_longest_path_length: Length of longest path
//! - all_topological_sorts: Generate all possible topological orderings
//! - transitive_closure: Compute transitive closure of a DAG
//! - transitive_reduction: Compute transitive reduction of a DAG

use super::super::graph::{Graph, NodeId};
use std::collections::{HashMap, HashSet, VecDeque};

// ─────────────────────────────────────────────────────────────────────────────
// Topological Sort

/// Topological sort using Kahn's algorithm (BFS-based).
///
/// Returns nodes in topological order: for every directed edge u→v,
/// u appears before v in the ordering.
///
/// Ported from RustworkxCore:
/// <https://github.com/Qiskit/rustworkx/blob/main/rustworkx-core/src/dag_algo/mod.rs>
///
/// # Returns
/// `Ok(Vec<NodeId>)` with topological ordering if graph is a DAG,
/// `Err(Vec<NodeId>)` with the nodes involved in a cycle if cycles exist.
///
/// # Example
/// ```ignore
/// match topological_sort(&graph) {
///     Ok(order) => println!("Topological order: {:?}", order),
///     Err(cycle) => println!("Cycle detected involving: {:?}", cycle),
/// }
/// ```
pub fn topological_sort(graph: &Graph) -> Result<Vec<NodeId>, Vec<NodeId>> {
    // Compute in-degrees
    let mut in_degree: HashMap<NodeId, usize> = HashMap::new();
    for node in graph.nodes() {
        in_degree.insert(node, 0);
    }
    for (_, v, _, _) in graph.edges() {
        *in_degree.entry(v).or_insert(0) += 1;
    }

    // Enqueue all nodes with in-degree 0
    let mut queue: VecDeque<NodeId> = in_degree
        .iter()
        .filter(|(_, &deg)| deg == 0)
        .map(|(&node, _)| node)
        .collect();

    // Sort for deterministic output
    let mut sorted_queue: Vec<NodeId> = queue.drain(..).collect();
    sorted_queue.sort_unstable();
    queue.extend(sorted_queue);

    let mut order = Vec::new();

    while let Some(node) = queue.pop_front() {
        order.push(node);

        // Reduce in-degree of all successors
        let mut successors: Vec<NodeId> = graph.out_neighbors(node)
            .iter()
            .map(|n| n.target)
            .collect();
        successors.sort_unstable();

        for succ in successors {
            let deg = in_degree.entry(succ).or_insert(0);
            *deg -= 1;
            if *deg == 0 {
                queue.push_back(succ);
            }
        }
    }

    if order.len() == graph.node_count() {
        Ok(order)
    } else {
        // Find nodes in cycles (those with remaining in-degree > 0)
        let cycle_nodes: Vec<NodeId> = in_degree
            .into_iter()
            .filter(|(_, deg)| *deg > 0)
            .map(|(node, _)| node)
            .collect();
        Err(cycle_nodes)
    }
}

/// Topological sort using DFS (Depth-First Search).
///
/// Alternative to Kahn's algorithm; uses DFS post-order.
/// Reference: Cormen et al., "Introduction to Algorithms"
///
/// # Returns
/// `Some(Vec<NodeId>)` with topological ordering if graph is a DAG,
/// `None` if the graph has cycles.
pub fn topological_sort_dfs(graph: &Graph) -> Option<Vec<NodeId>> {
    let mut visited: HashSet<NodeId> = HashSet::new();
    let mut in_stack: HashSet<NodeId> = HashSet::new(); // For cycle detection
    let mut order: Vec<NodeId> = Vec::new();

    for start in graph.nodes() {
        if !visited.contains(&start) && !dfs_visit(graph, start, &mut visited, &mut in_stack, &mut order) {
            return None; // Cycle detected
        }
    }

    order.reverse();
    Some(order)
}

fn dfs_visit(
    graph: &Graph,
    node: NodeId,
    visited: &mut HashSet<NodeId>,
    in_stack: &mut HashSet<NodeId>,
    order: &mut Vec<NodeId>,
) -> bool {
    visited.insert(node);
    in_stack.insert(node);

    for neighbor in graph.out_neighbors(node) {
        if in_stack.contains(&neighbor.target) {
            return false; // Back edge = cycle
        }
        if !visited.contains(&neighbor.target) && !dfs_visit(graph, neighbor.target, visited, in_stack, order) {
            return false;
        }
    }

    in_stack.remove(&node);
    order.push(node);
    true
}

// ─────────────────────────────────────────────────────────────────────────────
// DAG Detection

/// Check if directed graph is acyclic (is a DAG).
///
/// Uses Kahn's topological sort: if all nodes can be processed, no cycles exist.
///
/// # Example
/// ```ignore
/// if is_dag(&graph) {
///     println!("Graph is a DAG!");
/// }
/// ```
pub fn is_dag(graph: &Graph) -> bool {
    topological_sort(graph).is_ok()
}

/// Find all cycles in a directed graph.
///
/// Uses Johnson's algorithm to enumerate all simple cycles.
/// Reference: Johnson, D.B. (1975). "Finding all the elementary circuits of a directed graph"
///
/// # Returns
/// A vector of cycles, where each cycle is a vector of node IDs.
///
/// # Example
/// ```ignore
/// let cycles = find_cycles(&graph);
/// for cycle in cycles {
///     println!("Cycle: {:?}", cycle);
/// }
/// ```
pub fn find_cycles(graph: &Graph) -> Vec<Vec<NodeId>> {
    let mut cycles = Vec::new();
    let mut blocked: HashSet<NodeId> = HashSet::new();
    let mut b_map: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
    let mut stack: Vec<NodeId> = Vec::new();

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

    for &start in &nodes {
        blocked.clear();
        b_map.clear();

        find_cycles_johnson(
            graph,
            start,
            start,
            &mut blocked,
            &mut b_map,
            &mut stack,
            &mut cycles,
        );
    }

    cycles
}

fn find_cycles_johnson(
    graph: &Graph,
    start: NodeId,
    current: NodeId,
    blocked: &mut HashSet<NodeId>,
    b_map: &mut HashMap<NodeId, HashSet<NodeId>>,
    stack: &mut Vec<NodeId>,
    cycles: &mut Vec<Vec<NodeId>>,
) -> bool {
    let mut found_cycle = false;
    stack.push(current);
    blocked.insert(current);

    for neighbor in graph.out_neighbors(current) {
        let next = neighbor.target;
        if next == start {
            // Found a cycle
            let mut cycle = stack.clone();
            cycle.push(start);
            cycles.push(cycle);
            found_cycle = true;
        } else if !blocked.contains(&next) {
            let f = find_cycles_johnson(graph, start, next, blocked, b_map, stack, cycles);
            found_cycle = found_cycle || f;
        }
    }

    if found_cycle {
        unblock(current, blocked, b_map);
    } else {
        for neighbor in graph.out_neighbors(current) {
            b_map.entry(neighbor.target).or_default().insert(current);
        }
    }

    stack.pop();
    found_cycle
}

fn unblock(node: NodeId, blocked: &mut HashSet<NodeId>, b_map: &mut HashMap<NodeId, HashSet<NodeId>>) {
    blocked.remove(&node);
    if let Some(b_set) = b_map.remove(&node) {
        for w in b_set {
            if blocked.contains(&w) {
                unblock(w, blocked, b_map);
            }
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Longest Path

/// Longest path in a DAG (by number of edges).
///
/// Uses dynamic programming on topological order.
///
/// # Returns
/// `Some((length, path))` if graph is a DAG, `None` if cycles exist.
///
/// # Example
/// ```ignore
/// if let Some((len, path)) = dag_longest_path(&graph) {
///     println!("Longest path has {} edges: {:?}", len, path);
/// }
/// ```
pub fn dag_longest_path(graph: &Graph) -> Option<(usize, Vec<NodeId>)> {
    let order = topological_sort(graph).ok()?;

    let mut dist: HashMap<NodeId, usize> = HashMap::new();
    let mut prev: HashMap<NodeId, NodeId> = HashMap::new();

    for &node in &order {
        dist.insert(node, 0);
    }

    for &node in &order {
        let current_dist = dist[&node];
        for neighbor in graph.out_neighbors(node) {
            let next_dist = dist.get(&neighbor.target).copied().unwrap_or(0);
            if current_dist + 1 > next_dist {
                dist.insert(neighbor.target, current_dist + 1);
                prev.insert(neighbor.target, node);
            }
        }
    }

    // Find node with maximum distance
    let (&end_node, &max_dist) = dist.iter().max_by_key(|(_, &d)| d)?;

    if max_dist == 0 {
        return Some((0, vec![end_node]));
    }

    // Reconstruct path
    let mut path = vec![end_node];
    let mut curr = end_node;
    while let Some(&p) = prev.get(&curr) {
        path.push(p);
        curr = p;
    }
    path.reverse();

    Some((max_dist, path))
}

/// Length of the longest path in a DAG (number of edges).
///
/// # Returns
/// `Some(length)` if graph is a DAG, `None` if cycles exist.
pub fn dag_longest_path_length(graph: &Graph) -> Option<usize> {
    dag_longest_path(graph).map(|(len, _)| len)
}

// ─────────────────────────────────────────────────────────────────────────────
// Weighted Longest Path

/// Longest path in a DAG by total edge weight.
///
/// Uses dynamic programming on topological order, summing edge weights.
///
/// # Returns
/// `Some((cost, path))` with maximum weight path, `None` if graph has cycles.
pub fn dag_longest_path_weighted(graph: &Graph) -> Option<(f64, Vec<NodeId>)> {
    let order = topological_sort(graph).ok()?;

    let mut dist: HashMap<NodeId, f64> = HashMap::new();
    let mut prev: HashMap<NodeId, NodeId> = HashMap::new();

    for &node in &order {
        dist.insert(node, 0.0);
    }

    for &node in &order {
        let current_dist = dist[&node];
        for neighbor in graph.out_neighbors(node) {
            let edge_weight = neighbor.weight.unwrap_or(1.0);
            let next_dist = dist.get(&neighbor.target).copied().unwrap_or(0.0);
            if current_dist + edge_weight > next_dist {
                dist.insert(neighbor.target, current_dist + edge_weight);
                prev.insert(neighbor.target, node);
            }
        }
    }

    // Find node with maximum distance
    let (&end_node, &max_dist) = dist.iter()
        .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))?;

    if max_dist == 0.0 {
        return Some((0.0, vec![end_node]));
    }

    // Reconstruct path
    let mut path = vec![end_node];
    let mut curr = end_node;
    while let Some(&p) = prev.get(&curr) {
        path.push(p);
        curr = p;
    }
    path.reverse();

    Some((max_dist, path))
}

// ─────────────────────────────────────────────────────────────────────────────
// Transitive Closure

/// Compute the transitive closure of a directed graph.
///
/// Returns a set of all (u, v) pairs where v is reachable from u.
///
/// # Example
/// ```ignore
/// let closure = transitive_closure(&graph);
/// if closure.contains(&(a, d)) {
///     println!("d is reachable from a");
/// }
/// ```
pub fn transitive_closure(graph: &Graph) -> HashSet<(NodeId, NodeId)> {
    let mut reachable: HashSet<(NodeId, NodeId)> = HashSet::new();

    for source in graph.nodes() {
        // BFS from each source
        let mut visited: HashSet<NodeId> = HashSet::new();
        let mut queue: VecDeque<NodeId> = VecDeque::new();
        queue.push_back(source);

        while let Some(node) = queue.pop_front() {
            if !visited.insert(node) {
                continue;
            }
            if node != source {
                reachable.insert((source, node));
            }
            for neighbor in graph.out_neighbors(node) {
                if !visited.contains(&neighbor.target) {
                    queue.push_back(neighbor.target);
                }
            }
        }
    }

    reachable
}

// ─── Transitive Reduction ─────────────────────────────────────────────────────

/// Compute the transitive reduction of a DAG.
///
/// The transitive reduction removes every edge (u, v) that is redundant —
/// i.e. there exists an alternative path from u to v of length ≥ 2.
/// The result has the same reachability as the input but with the minimum
/// number of edges.
///
/// **Only valid for DAGs.** Returns an empty graph if the input contains cycles.
///
/// Runtime: O(V · (V + E)) — a reachability check per edge.
pub fn transitive_reduction(graph: &Graph) -> Graph {
    // Validate DAG
    if !is_dag(graph) {
        return Graph::new(super::super::graph::GraphConfig::directed());
    }

    use super::super::graph::GraphConfig;

    let mut out = Graph::new(GraphConfig::directed());

    // Add all nodes
    let nodes: Vec<NodeId> = graph.nodes().collect();
    let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
    for &n in &nodes {
        let new_id = out.add_node();
        id_map.insert(n, new_id);
    }

    // For each edge (u, v), keep it only if there is NO path from u to v
    // of length ≥ 2 in the ORIGINAL graph.
    for &u in &nodes {
        for neighbor in graph.out_neighbors(u) {
            let v = neighbor.target;
            // Check whether v is reachable from u through a path of length >= 2
            // i.e. through a neighbor of u that is NOT v
            let reachable_via_other = graph.out_neighbors(u).iter().any(|nb| {
                if nb.target == v { return false; }
                // BFS from nb.target to v
                bfs_reaches(graph, nb.target, v)
            });
            if !reachable_via_other {
                let nu = id_map[&u];
                let nv = id_map[&v];
                out.add_edge(nu, nv, neighbor.weight);
            }
        }
    }
    out
}

/// BFS from `start` — returns true if `target` is reachable.
fn bfs_reaches(graph: &Graph, start: NodeId, target: NodeId) -> bool {
    let mut visited: HashSet<NodeId> = HashSet::new();
    let mut queue: VecDeque<NodeId> = VecDeque::new();
    queue.push_back(start);
    visited.insert(start);
    while let Some(n) = queue.pop_front() {
        if n == target { return true; }
        for nb in graph.out_neighbors(n) {
            if visited.insert(nb.target) {
                queue.push_back(nb.target);
            }
        }
    }
    false
}

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

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

    fn make_dag() -> (Graph, NodeId, NodeId, NodeId, NodeId) {
        let mut g = Graph::new(GraphConfig::directed());
        let a = g.add_node();
        let b = g.add_node();
        let c = g.add_node();
        let d = g.add_node();
        // a → b → d
        // a → c → d
        g.add_edge(a, b, None);
        g.add_edge(a, c, None);
        g.add_edge(b, d, None);
        g.add_edge(c, d, None);
        (g, a, b, c, d)
    }

    #[test]
    fn test_topological_sort_dag() {
        let (g, a, b, c, d) = make_dag();
        let result = topological_sort(&g);
        assert!(result.is_ok());
        let order = result.unwrap();

        // a must come before b and c, b and c before d
        let pos_a = order.iter().position(|&x| x == a).unwrap();
        let pos_b = order.iter().position(|&x| x == b).unwrap();
        let pos_c = order.iter().position(|&x| x == c).unwrap();
        let pos_d = order.iter().position(|&x| x == d).unwrap();

        assert!(pos_a < pos_b);
        assert!(pos_a < pos_c);
        assert!(pos_b < pos_d);
        assert!(pos_c < pos_d);
    }

    #[test]
    fn test_topological_sort_cycle() {
        let mut g = Graph::new(GraphConfig::directed());
        let a = g.add_node();
        let b = g.add_node();
        g.add_edge(a, b, None);
        g.add_edge(b, a, None); // Cycle!

        let result = topological_sort(&g);
        assert!(result.is_err());
    }

    #[test]
    fn test_is_dag() {
        let (dag, _, _, _, _) = make_dag();
        assert!(is_dag(&dag));

        let mut cyclic = Graph::new(GraphConfig::directed());
        let x = cyclic.add_node();
        let y = cyclic.add_node();
        cyclic.add_edge(x, y, None);
        cyclic.add_edge(y, x, None);
        assert!(!is_dag(&cyclic));
    }

    #[test]
    fn test_dag_longest_path() {
        let (g, a, _b, _c, d) = make_dag();
        let result = dag_longest_path(&g);
        assert!(result.is_some());
        let (len, path) = result.unwrap();
        assert_eq!(len, 2); // a→b→d or a→c→d both have length 2
        assert_eq!(path.first(), Some(&a));
        assert_eq!(path.last(), Some(&d));
    }

    #[test]
    fn test_topological_sort_dfs() {
        let (g, a, _b, _c, d) = make_dag();
        let result = topological_sort_dfs(&g);
        assert!(result.is_some());
        let order = result.unwrap();

        let pos_a = order.iter().position(|&x| x == a).unwrap();
        let pos_d = order.iter().position(|&x| x == d).unwrap();
        assert!(pos_a < pos_d);
    }

    #[test]
    fn test_transitive_closure() {
        let (g, a, b, _c, d) = make_dag();
        let closure = transitive_closure(&g);
        assert!(closure.contains(&(a, d))); // a can reach d
        assert!(closure.contains(&(a, b))); // a can reach b
        assert!(!closure.contains(&(d, a))); // d cannot reach a (DAG)
    }

    #[test]
    fn test_find_cycles_no_cycles() {
        let (dag, _, _, _, _) = make_dag();
        let cycles = find_cycles(&dag);
        assert!(cycles.is_empty());
    }

    #[test]
    fn test_find_cycles_with_cycles() {
        let mut g = Graph::new(GraphConfig::directed());
        let a = g.add_node();
        let b = g.add_node();
        let c = g.add_node();
        g.add_edge(a, b, None);
        g.add_edge(b, c, None);
        g.add_edge(c, a, None); // Cycle: a→b→c→a

        let cycles = find_cycles(&g);
        assert!(!cycles.is_empty());
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Lexicographical Topological Sort

/// Lexicographically smallest topological sort.
///
/// Among all valid topological orderings, returns the lexicographically
/// smallest one (using a min-heap instead of a FIFO queue).
///
/// Runtime: O((n + m) log n)
pub fn lexicographical_topological_sort(graph: &Graph) -> Result<Vec<NodeId>, Vec<NodeId>> {
    use std::collections::BinaryHeap;
    use std::cmp::Reverse;

    let mut in_degree: HashMap<NodeId, usize> = graph.nodes().map(|n| (n, 0)).collect();
    for (_, v, _, _) in graph.edges() {
        *in_degree.entry(v).or_insert(0) += 1;
    }

    // Min-heap: always pick smallest available node
    let mut heap: BinaryHeap<Reverse<NodeId>> = in_degree
        .iter()
        .filter(|(_, &d)| d == 0)
        .map(|(&n, _)| Reverse(n))
        .collect();

    let mut result = Vec::new();

    while let Some(Reverse(node)) = heap.pop() {
        result.push(node);
        for e in graph.out_neighbors(node) {
            let deg = in_degree.get_mut(&e.target).unwrap();
            *deg -= 1;
            if *deg == 0 {
                heap.push(Reverse(e.target));
            }
        }
    }

    if result.len() == graph.node_count() {
        Ok(result)
    } else {
        Err(result)
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// DAG Layers

/// Assign each node to a DAG layer (longest path from source).
///
/// Layer 0 = root nodes (in-degree 0).
/// Each node's layer = 1 + max(layer of predecessors).
///
/// Returns `None` if the graph has cycles.
///
/// Runtime: O(n + m)
pub fn dag_layers(graph: &Graph) -> Option<HashMap<NodeId, usize>> {
    let mut in_degree: HashMap<NodeId, usize> = graph.nodes().map(|n| (n, 0)).collect();
    for (_, v, _, _) in graph.edges() {
        *in_degree.entry(v).or_insert(0) += 1;
    }

    let mut layer: HashMap<NodeId, usize> = HashMap::new();
    let mut queue: VecDeque<NodeId> = in_degree
        .iter()
        .filter(|(_, &d)| d == 0)
        .map(|(&n, _)| n)
        .collect();

    // Roots are at layer 0
    for &n in &queue { layer.insert(n, 0); }

    let mut processed = 0;

    while let Some(node) = queue.pop_front() {
        processed += 1;
        let cur_layer = layer[&node];
        for e in graph.out_neighbors(node) {
            let child = e.target;
            let next_layer = cur_layer + 1;
            let entry = layer.entry(child).or_insert(0);
            if next_layer > *entry { *entry = next_layer; }
            let deg = in_degree.get_mut(&child).unwrap();
            *deg -= 1;
            if *deg == 0 {
                queue.push_back(child);
            }
        }
    }

    if processed == graph.node_count() { Some(layer) } else { None }
}

// ─────────────────────────────────────────────────────────────────────────────
// Collect Runs

/// Collect maximal runs (consecutive sequences) of nodes satisfying a predicate.
///
/// A "run" is a maximal path where every node satisfies `predicate`.
/// Processes nodes in topological order. Used in circuit compilation and
/// quantum circuit optimization.
///
/// Runtime: O(n + m)
pub fn collect_runs<F>(graph: &Graph, predicate: F) -> Vec<Vec<NodeId>>
where
    F: Fn(NodeId) -> bool,
{
    // Get topological order first
    let order = match topological_sort(graph) {
        Ok(o) => o,
        Err(_) => return vec![],
    };

    let mut result = Vec::new();
    let mut visited: HashSet<NodeId> = HashSet::new();

    for &start in &order {
        if visited.contains(&start) || !predicate(start) {
            continue;
        }

        let mut run = vec![start];
        visited.insert(start);
        let mut cur = start;

        // Extend run: follow single out-edge chains where predicate holds
        loop {
            let successors: Vec<NodeId> = graph.out_neighbors(cur).iter().map(|e| e.target).collect();
            // Only extend if exactly one successor and it satisfies predicate
            if successors.len() == 1 {
                let next = successors[0];
                // Only continue if next has a single in-edge (keeping run linear)
                let pred_count = graph.nodes()
                    .filter(|&u| graph.out_neighbors(u).iter().any(|e| e.target == next))
                    .count();
                if pred_count == 1 && predicate(next) && !visited.contains(&next) {
                    visited.insert(next);
                    run.push(next);
                    cur = next;
                    continue;
                }
            }
            break;
        }

        if run.len() >= 1 {
            result.push(run);
        }
    }

    result
}

// ─────────────────────────────────────────────────────────────────────────────
// New tests for the additions above

#[cfg(test)]
mod lex_tests {
    use super::*;
    use super::super::super::graph::GraphConfig;

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

    #[test]
    fn test_lexicographical_topological_sort_dag() {
        let g = make_diamond();
        let order = lexicographical_topological_sort(&g).unwrap();
        // Must be a valid topo order starting with 0
        assert_eq!(order[0], 0);
        assert_eq!(*order.last().unwrap(), 3);
        // Lex-smallest: 0, 1, 2, 3
        assert_eq!(order, vec![0, 1, 2, 3]);
    }

    #[test]
    fn test_lexicographical_topological_sort_cycle() {
        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!(lexicographical_topological_sort(&g).is_err());
    }

    #[test]
    fn test_dag_layers() {
        let g = make_diamond();
        let layers = dag_layers(&g).unwrap();
        assert_eq!(layers[&0], 0);
        assert_eq!(layers[&1], 1);
        assert_eq!(layers[&2], 1);
        assert_eq!(layers[&3], 2);
    }

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

    #[test]
    fn test_collect_runs_all_satisfy() {
        // Linear chain: 0→1→2→3, all nodes satisfy predicate
        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, 3, None);
        let runs = collect_runs(&g, |_| true);
        // One run covering all nodes
        assert_eq!(runs.len(), 1);
        assert_eq!(runs[0], vec![0, 1, 2, 3]);
    }

    #[test]
    fn test_collect_runs_some_satisfy() {
        // 0→1→2→3, only even nodes satisfy predicate
        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, 3, None);
        let runs = collect_runs(&g, |n| n % 2 == 0);
        // Runs: [0], [2] (odd nodes break the chain)
        assert_eq!(runs.len(), 2);
    }
}

// ─── Bicolor Runs ─────────────────────────────────────────────────────────────

/// Collect maximal "bicolor runs" in a DAG.
///
/// A bicolor run is a maximal sequence of nodes in topological order where
/// consecutive nodes satisfy a two-color predicate: each node in the run
/// must have a different "color" from its predecessor.
///
/// Concretely: given a function `color: NodeId → bool` (false = color A, true = color B),
/// a bicolor run is a chain where colors alternate: A, B, A, B, ...
///
/// This is used in Qiskit's circuit routing to detect "runs" of alternating gate types.
///
/// # Arguments
/// - `graph`: a DAG (directed acyclic graph)
/// - `color`: function mapping each node to its color (false or true)
///
/// # Returns
/// List of bicolor runs (each run is a list of `NodeId` in topological order).
pub fn collect_bicolor_runs<F>(graph: &Graph, color: F) -> Vec<Vec<NodeId>>
where
    F: Fn(NodeId) -> bool,
{
    // Topological order — return empty if graph has cycles
    let topo = match topological_sort(graph) {
        Ok(order) => order,
        Err(_) => return vec![],
    };
    if topo.is_empty() { return vec![]; }

    // For each node, try to extend an existing run or start a new one.
    // A run can be extended from predecessor `u` to `node` if:
    // - u is the last node in the run
    // - color(node) != color(u)  (alternating colors)
    // - node is a direct successor of u in the DAG

    let color_map: HashMap<NodeId, bool> = topo.iter().map(|&n| (n, color(n))).collect();

    // For O(1) predecessor lookup: build in-neighbor map
    let mut in_neighbors: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
    for &u in &topo {
        for e in graph.out_neighbors(u).iter() {
            in_neighbors.entry(e.target).or_default().insert(u);
        }
    }

    let mut active_runs: Vec<Vec<NodeId>> = Vec::new();

    for &node in &topo {
        let node_color = color_map[&node];
        let preds = in_neighbors.get(&node);

        // Try to extend a run ending at one of node's predecessors with alternating color
        let mut extended = false;
        for run in active_runs.iter_mut() {
            if let Some(&last) = run.last() {
                let last_color = color_map[&last];
                let last_is_pred = preds.map_or(false, |s| s.contains(&last));
                if last_is_pred && last_color != node_color {
                    run.push(node);
                    extended = true;
                    break;
                }
            }
        }

        if !extended {
            active_runs.push(vec![node]);
        }
    }

    active_runs
}

#[cfg(test)]
mod bicolor_tests {
    use super::*;
    use super::super::super::graph::GraphConfig;

    #[test]
    fn test_bicolor_runs_alternating() {
        // 0→1→2→3→4, colors: A,B,A,B,A (alternating)
        let mut g = Graph::new(GraphConfig::directed());
        for _ in 0..5 { 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, 4, None);
        // color: node % 2 == 0 → false (A), node % 2 == 1 → true (B)
        let runs = collect_bicolor_runs(&g, |n| n % 2 == 1);
        // Should produce one long run [0,1,2,3,4] since they alternate
        let max_run_len = runs.iter().map(|r| r.len()).max().unwrap_or(0);
        assert!(max_run_len >= 3, "longest bicolor run should have ≥3 nodes, got {}", max_run_len);
    }

    #[test]
    fn test_bicolor_runs_same_color() {
        // 0→1→2→3: all same color → no bicolor run longer than 1
        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, 3, None);
        // all nodes have color false
        let runs = collect_bicolor_runs(&g, |_| false);
        // Each run can be at most length 1 since no alternation
        for run in &runs {
            assert!(run.len() <= 1, "same-color run should not be bicolor, got {:?}", run);
        }
    }

    #[test]
    fn test_bicolor_runs_empty() {
        let g = Graph::new(GraphConfig::directed());
        let runs = collect_bicolor_runs(&g, |_| false);
        assert!(runs.is_empty());
    }

    #[test]
    fn test_bicolor_runs_single_node() {
        let mut g = Graph::new(GraphConfig::directed());
        g.add_node();
        let runs = collect_bicolor_runs(&g, |_| false);
        assert_eq!(runs.len(), 1);
        assert_eq!(runs[0], vec![0]);
    }

    // ── transitive_reduction ──────────────────────────────────────────────────

    #[test]
    fn test_transitive_reduction_diamond() {
        // a→b, a→c, b→d, c→d, and ALSO a→d (redundant)
        let mut g = Graph::new(GraphConfig::directed());
        let a = g.add_node();
        let b = g.add_node();
        let c = g.add_node();
        let d = g.add_node();
        g.add_edge(a, b, None);
        g.add_edge(a, c, None);
        g.add_edge(b, d, None);
        g.add_edge(c, d, None);
        g.add_edge(a, d, None); // redundant — covered by a→b→d

        let tr = transitive_reduction(&g);
        // Transitive reduction should have 4 edges (a→d removed)
        assert_eq!(tr.node_count(), 4);
        assert_eq!(tr.edge_count(), 4, "a→d should be removed: {:?}", tr.edge_count());
    }

    #[test]
    fn test_transitive_reduction_chain() {
        // a→b→c→d with extra a→c and a→d (both redundant)
        let mut g = Graph::new(GraphConfig::directed());
        let a = g.add_node();
        let b = g.add_node();
        let c = g.add_node();
        let d = g.add_node();
        g.add_edge(a, b, None);
        g.add_edge(b, c, None);
        g.add_edge(c, d, None);
        g.add_edge(a, c, None); // redundant
        g.add_edge(a, d, None); // redundant

        let tr = transitive_reduction(&g);
        assert_eq!(tr.edge_count(), 3); // only a→b, b→c, c→d
    }

    #[test]
    fn test_transitive_reduction_minimal() {
        // Already minimal DAG: a→b→c
        let mut g = Graph::new(GraphConfig::directed());
        let a = g.add_node();
        let b = g.add_node();
        let c = g.add_node();
        g.add_edge(a, b, None);
        g.add_edge(b, c, None);

        let tr = transitive_reduction(&g);
        assert_eq!(tr.edge_count(), 2); // unchanged
    }

    #[test]
    fn test_transitive_reduction_cycle_returns_empty() {
        // Cycle: not a DAG → returns empty
        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 tr = transitive_reduction(&g);
        assert_eq!(tr.node_count(), 0);
    }
}
