use std::collections::HashSet;
use std::hash::Hash;

pub fn would_create_cycle<Node, Neighbors, Iter>(
    from: &Node,
    to: &Node,
    mut neighbors: Neighbors,
) -> bool
where
    Node: Clone + Eq + Hash,
    Neighbors: FnMut(&Node) -> Iter,
    Iter: IntoIterator<Item = Node>,
{
    if from == to {
        return true;
    }
    has_path(to, from, &mut neighbors)
}

fn has_path<Node, Neighbors, Iter>(start: &Node, target: &Node, neighbors: &mut Neighbors) -> bool
where
    Node: Clone + Eq + Hash,
    Neighbors: FnMut(&Node) -> Iter,
    Iter: IntoIterator<Item = Node>,
{
    let mut visited = HashSet::new();
    has_path_inner(start, target, neighbors, &mut visited)
}

fn has_path_inner<Node, Neighbors, Iter>(
    current: &Node,
    target: &Node,
    neighbors: &mut Neighbors,
    visited: &mut HashSet<Node>,
) -> bool
where
    Node: Clone + Eq + Hash,
    Neighbors: FnMut(&Node) -> Iter,
    Iter: IntoIterator<Item = Node>,
{
    if current == target {
        return true;
    }
    if !visited.insert(current.clone()) {
        return false;
    }
    for next in neighbors(current) {
        if has_path_inner(&next, target, neighbors, visited) {
            return true;
        }
    }
    false
}

#[cfg(test)]
mod tests {
    use super::would_create_cycle;
    use std::collections::HashMap;

    #[derive(Clone, Debug, PartialEq, Eq, Hash)]
    enum Node {
        TrackIn,
        TrackOut,
        PluginA,
        PluginB,
    }

    #[test]
    fn detects_track_cycle() {
        let graph = HashMap::from([
            ("A".to_string(), vec!["B".to_string()]),
            ("B".to_string(), vec!["C".to_string()]),
            ("C".to_string(), vec![]),
        ]);

        let from = "C".to_string();
        let to = "A".to_string();
        assert!(would_create_cycle(&from, &to, |node: &String| {
            graph.get(node).cloned().unwrap_or_default()
        }));
    }

    #[test]
    fn detects_plugin_cycle() {
        let graph = HashMap::from([
            (Node::TrackIn, vec![Node::PluginA]),
            (Node::PluginA, vec![Node::PluginB]),
            (Node::PluginB, vec![Node::TrackOut]),
            (Node::TrackOut, vec![]),
        ]);

        assert!(would_create_cycle(&Node::PluginB, &Node::PluginA, |node| {
            graph.get(node).cloned().unwrap_or_default()
        }));
    }

    #[test]
    fn allows_acyclic_edge() {
        let graph = HashMap::from([
            (Node::TrackIn, vec![Node::PluginA]),
            (Node::PluginA, vec![Node::TrackOut]),
            (Node::TrackOut, vec![]),
        ]);

        assert!(!would_create_cycle(
            &Node::TrackIn,
            &Node::TrackOut,
            |node| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn allows_edge_between_disconnected_subgraphs() {
        let graph = HashMap::from([
            ("A".to_string(), vec!["B".to_string()]),
            ("B".to_string(), vec![]),
            ("X".to_string(), vec!["Y".to_string()]),
            ("Y".to_string(), vec![]),
        ]);

        assert!(!would_create_cycle(
            &"A".to_string(),
            &"X".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn self_edge_is_always_a_cycle() {
        let graph = HashMap::<String, Vec<String>>::new();
        let node = "A".to_string();
        assert!(would_create_cycle(&node, &node, |current: &String| {
            graph.get(current).cloned().unwrap_or_default()
        }));
    }

    #[test]
    fn detects_long_cycle() {
        let graph = HashMap::from([
            ("A".to_string(), vec!["B".to_string()]),
            ("B".to_string(), vec!["C".to_string()]),
            ("C".to_string(), vec!["D".to_string()]),
            ("D".to_string(), vec!["E".to_string()]),
            ("E".to_string(), vec!["A".to_string()]),
        ]);

        assert!(would_create_cycle(
            &"E".to_string(),
            &"A".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn allows_connecting_sibling_nodes() {
        let graph = HashMap::from([
            ("A".to_string(), vec!["B".to_string()]),
            ("B".to_string(), vec![]),
            ("C".to_string(), vec!["D".to_string()]),
            ("D".to_string(), vec![]),
        ]);

        assert!(!would_create_cycle(
            &"B".to_string(),
            &"C".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn diamond_graph_connecting_new_node() {
        let graph = HashMap::from([
            ("A".to_string(), vec!["B".to_string(), "C".to_string()]),
            ("B".to_string(), vec!["D".to_string()]),
            ("C".to_string(), vec!["D".to_string()]),
            ("D".to_string(), vec![]),
        ]);

        assert!(!would_create_cycle(
            &"E".to_string(),
            &"D".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn empty_graph_allows_any_edge() {
        let graph: HashMap<String, Vec<String>> = HashMap::new();

        assert!(!would_create_cycle(
            &"A".to_string(),
            &"B".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn detects_cycle_in_bidirectional_graph() {
        let graph = HashMap::from([
            ("A".to_string(), vec!["B".to_string()]),
            ("B".to_string(), vec!["A".to_string()]),
        ]);

        assert!(would_create_cycle(
            &"B".to_string(),
            &"A".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn handles_missing_nodes() {
        let graph = HashMap::from([
            ("A".to_string(), vec!["B".to_string()]),
            ("B".to_string(), vec!["C".to_string()]),
        ]);

        assert!(!would_create_cycle(
            &"X".to_string(),
            &"A".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn single_node_no_edges() {
        let graph = HashMap::from([("A".to_string(), vec![])]);

        assert!(would_create_cycle(
            &"A".to_string(),
            &"A".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }

    #[test]
    fn connecting_to_new_node_from_diamond() {
        let graph = HashMap::from([
            ("A".to_string(), vec!["B".to_string(), "C".to_string()]),
            ("B".to_string(), vec!["D".to_string()]),
            ("C".to_string(), vec!["D".to_string()]),
            ("D".to_string(), vec!["E".to_string()]),
            ("E".to_string(), vec![]),
        ]);

        assert!(!would_create_cycle(
            &"F".to_string(),
            &"E".to_string(),
            |node: &String| { graph.get(node).cloned().unwrap_or_default() }
        ));
    }
}
