use super::node::GraphNodeId;
use crate::{
    collections::{Graph, GraphNode, NodeType},
    graphs::transaction::TransactionResult,
    node::Node,
};
use std::collections::BTreeMap;

/// An enum representing either a single [GraphNode] or a collection of [GraphNode]'s
/// to be inserted into a [GraphAggregate].
///
/// We have this to ease the process of inserting nodes into the aggregate. Everything is tied
/// to a lifetime so we don't have to worry about cloning/owning nodes until the final graph is built -
/// that makes this super cheap to use.
#[derive(Clone, Copy)]
pub enum AggregateInsertValue<'a, T> {
    Single(&'a GraphNode<T>),
    Many(&'a [GraphNode<T>]),
}

impl<'a, T> From<&'a GraphNode<T>> for AggregateInsertValue<'a, T> {
    fn from(value: &'a GraphNode<T>) -> Self {
        AggregateInsertValue::Single(value)
    }
}

impl<'a, T> From<&'a [GraphNode<T>]> for AggregateInsertValue<'a, T> {
    fn from(value: &'a [GraphNode<T>]) -> AggregateInsertValue<'a, T> {
        AggregateInsertValue::Many(value)
    }
}

impl<'a, T> From<&'a Vec<GraphNode<T>>> for AggregateInsertValue<'a, T> {
    fn from(value: &'a Vec<GraphNode<T>>) -> AggregateInsertValue<'a, T> {
        AggregateInsertValue::Many(value.as_slice())
    }
}

pub struct AggregateBuildResult<T> {
    graph: Graph<T>,
    transaction_result: TransactionResult<T>,
}

impl<T: Clone> AggregateBuildResult<T> {
    pub fn into_graph(self) -> Graph<T> {
        let mut graph = self.graph;

        if self.transaction_result.is_invalid() {
            self.transaction_result.replay(&mut graph);
        }

        graph
    }

    pub fn unwrap(self) -> Graph<T> {
        if !self.transaction_result.is_valid() {
            panic!("Cannot unwrap an invalid graph build result.");
        }

        self.graph
    }
}

/// Building a [Graph<T>] can be a very complex task. Everything in this file exists
/// to simplify the process by allowing the user to do so in a declarative way.
///
/// The [ConnectionCommand] are a set of available ways we can connect different [GraphNode]'s together.
///
/// # Assumptions
/// * The first collection is the 'source' collection and the second collection is the 'target' collection.
/// * The target collection's [GraphNode]'s `Arity` is compatible with the [ConnectionCommand].
enum ConnectionCommand<'a, T> {
    /// Connects each `GraphNode` in the first collection to the corresponding `GraphNode` in the
    /// second collection.
    ///
    /// # Rules
    /// * The first collection must have the same number of `GraphNode`s as the second collection.
    /// * the `GraphNode`'s in the second collection must have an arity of either `Any` or `Exact(1)`.
    OneToOne(AggregateInsertValue<'a, T>, AggregateInsertValue<'a, T>),
    /// Connects each `GraphNode` in the first collection to all `GraphNode`s in the second collection.
    ///
    /// # Rules
    /// * The second collection must be of size `n` where `n` is a multiple of the size of the first
    ///   collection IE: `n % first.len() == 0`.
    /// * The `GraphNode`'s in the second collection must have an arity of either `Any` or `Exact(n / first.len())`.
    OneToMany(AggregateInsertValue<'a, T>, AggregateInsertValue<'a, T>),
    /// Connects all `GraphNode`s in the first collection to a single `GraphNode` in the second collection.
    ///
    /// # Rules
    /// * The first collection must be of size `n` where `n` is a multiple of the size of the second collection
    ///   IE: `n % second.len() == 0`.
    /// * The `GraphNode`'s in the first collection must have an arity of either `Any` or `Exact(n / second.len())`.
    ManyToOne(AggregateInsertValue<'a, T>, AggregateInsertValue<'a, T>),
    /// Connects all `GraphNode`s in the first collection to all `GraphNode`s in the second collection.
    ///
    /// # Rules
    /// * The `GraphNode`'s in the second collection must have an arity of either `Any` or `Exact(n)`
    ///   where `n` is the size of the first collection.
    AllToAll(AggregateInsertValue<'a, T>, AggregateInsertValue<'a, T>),
}

/// Represents a relationship between two [GraphNode]'s where the `source_id` is the [GraphNode]'s
/// id that is incoming, or giving its value to the `target_id` [GraphNode].
struct Relationship<'a> {
    source_id: &'a GraphNodeId,
    target_id: &'a GraphNodeId,
}

/// The [GraphAggregate] struct is a builder for [Graph] that allows you to build a [Graph]
/// in an declarative way. It allows you to build a [Graph] by connecting [GraphNode]'s together in
/// a multitude of ways.
///
/// We do this by maintaining a map of nodes with an aggregate of thir relationships or how they are
/// connected to each other. Because of the nature of this aggregate, verifying the correctness of a
/// [Graph] can truly only be done after building the final [Graph].
#[derive(Default)]
pub struct GraphAggregate<'a, T> {
    nodes: BTreeMap<&'a GraphNodeId, &'a GraphNode<T>>,
    node_order: BTreeMap<usize, &'a GraphNodeId>,
    relationships: Vec<Relationship<'a>>,
}

impl<'a, T: Clone> GraphAggregate<'a, T> {
    pub fn new() -> Self {
        GraphAggregate {
            nodes: BTreeMap::new(),
            node_order: BTreeMap::new(),
            relationships: Vec::new(),
        }
    }

    pub fn build(&self) -> Graph<T> {
        self.try_build().into_graph()
    }

    pub fn try_build(&self) -> AggregateBuildResult<T> {
        let mut id_index_map = BTreeMap::new();
        let mut graph = Graph::<T>::default();

        let result = graph.try_modify(|mut trans| {
            for (index, node_id) in self.node_order.values().enumerate() {
                let node = self.nodes[node_id];

                trans.push((index, node.node_type(), node.value().clone(), node.arity()));
                id_index_map.insert(*node_id, index);
            }

            for rel in self.relationships.iter() {
                let source_idx = id_index_map[rel.source_id];
                let target_idx = id_index_map[rel.target_id];

                trans.attach(source_idx, target_idx);
            }

            trans.try_commit()
        });

        AggregateBuildResult {
            graph,
            transaction_result: result,
        }
    }

    pub fn cycle<F>(mut self, one: F) -> Self
    where
        F: Into<AggregateInsertValue<'a, T>>,
    {
        let group = one.into();
        self.connect(ConnectionCommand::OneToOne(group.clone(), group));
        self
    }

    pub fn one_to_one<F, S>(mut self, one: F, two: S) -> Self
    where
        F: Into<AggregateInsertValue<'a, T>>,
        S: Into<AggregateInsertValue<'a, T>>,
    {
        self.connect(ConnectionCommand::OneToOne(one.into(), two.into()));
        self
    }

    pub fn one_to_many<F, S>(mut self, one: F, two: S) -> Self
    where
        F: Into<AggregateInsertValue<'a, T>>,
        S: Into<AggregateInsertValue<'a, T>>,
    {
        self.connect(ConnectionCommand::OneToMany(one.into(), two.into()));
        self
    }

    pub fn many_to_one<F, S>(mut self, one: F, two: S) -> Self
    where
        F: Into<AggregateInsertValue<'a, T>>,
        S: Into<AggregateInsertValue<'a, T>>,
    {
        self.connect(ConnectionCommand::ManyToOne(one.into(), two.into()));
        self
    }

    pub fn all_to_all<F, S>(mut self, one: F, two: S) -> Self
    where
        F: Into<AggregateInsertValue<'a, T>>,
        S: Into<AggregateInsertValue<'a, T>>,
    {
        self.connect(ConnectionCommand::AllToAll(one.into(), two.into()));
        self
    }

    pub fn insert<G: Into<AggregateInsertValue<'a, T>>>(mut self, collection: G) -> Self {
        let group = collection.into();
        self.attach(&group);
        self
    }

    pub fn layer<F>(&self, collections: Vec<F>) -> Self
    where
        F: Into<AggregateInsertValue<'a, T>>,
    {
        let mut conn = GraphAggregate::new();
        let inserts = collections
            .into_iter()
            .map(|c| c.into())
            .collect::<Vec<AggregateInsertValue<'a, T>>>();

        let mut previous = &inserts[0];

        for collection in inserts.iter() {
            conn.attach(collection);
        }

        for coll in inserts.iter().skip(1) {
            conn = conn.one_to_one(previous.clone(), coll.clone());
            previous = coll;
        }

        conn
    }

    fn connect(&mut self, connection: ConnectionCommand<'a, T>) {
        if let ConnectionCommand::OneToOne(one, two) = &connection {
            let one_ids = self.attach(&one);
            let two_ids = self.attach(&two);

            self.one_to_one_connect(&one_ids, &two_ids);
        } else if let ConnectionCommand::OneToMany(one, two) = &connection {
            let one_ids = self.attach(&one);
            let two_ids = self.attach(&two);

            self.one_to_many_connect(&one_ids, &two_ids);
        } else if let ConnectionCommand::ManyToOne(one, two) = &connection {
            let one_ids = self.attach(&one);
            let two_ids = self.attach(&two);

            self.many_to_one_connect(&one_ids, &two_ids);
        } else if let ConnectionCommand::AllToAll(one, two) = &connection {
            let one_ids = self.attach(&one);
            let two_ids = self.attach(&two);

            self.all_to_all_connect(&one_ids, &two_ids);
        }
    }

    fn attach(&mut self, group: &AggregateInsertValue<'a, T>) -> Vec<&'a GraphNodeId> {
        match group {
            AggregateInsertValue::Single(node) => {
                if !self.nodes.contains_key(node.id()) {
                    let ordinal = self.node_order.len();
                    self.nodes.insert(node.id(), node);
                    self.node_order.insert(ordinal, node.id());
                }

                vec![node.id()]
            }
            AggregateInsertValue::Many(nodes) => {
                let mut indexes = Vec::with_capacity(nodes.len());
                for node in nodes.iter() {
                    let node_id = node.id();
                    indexes.push(node_id);

                    if !self.nodes.contains_key(node_id) {
                        let ordinal = self.node_order.len();

                        self.nodes.insert(node_id, node);
                        self.node_order.insert(ordinal, node_id);

                        nodes
                            .iter()
                            .filter(|item| node.outgoing().contains(&item.index()))
                            .for_each(|item| {
                                self.relationships.push(Relationship {
                                    source_id: node.id(),
                                    target_id: item.id(),
                                });
                            });
                    }
                }

                indexes
            }
        }
    }

    fn one_to_one_connect(&mut self, one: &[&'a GraphNodeId], two: &[&'a GraphNodeId]) {
        let one_outputs = self.get_outputs(&one);
        let two_inputs = self.get_inputs(&two);

        if one_outputs.len() != two_inputs.len() {
            panic!("OneToOne - Both groups must be of the same size.");
        }

        for (one, two) in one_outputs.into_iter().zip(two_inputs.into_iter()) {
            self.relationships.push(Relationship {
                source_id: one.id(),
                target_id: two.id(),
            });
        }
    }

    fn one_to_many_connect(&mut self, one: &[&'a GraphNodeId], two: &[&'a GraphNodeId]) {
        let one_outputs = self.get_outputs(one);
        let two_inputs = self.get_inputs(two);

        if two_inputs.len() % one_outputs.len() != 0 {
            panic!("OneToMany - TwoGroup inputs must be a multiple of OneGroup outputs.");
        }

        for targets in two_inputs.chunks(one_outputs.len()) {
            for (source, target) in one_outputs.iter().zip(targets.iter()) {
                self.relationships.push(Relationship {
                    source_id: source.id(),
                    target_id: target.id(),
                });
            }
        }
    }

    fn many_to_one_connect(&mut self, one: &[&'a GraphNodeId], two: &[&'a GraphNodeId]) {
        let one_outputs = self.get_outputs(one);
        let two_inputs = self.get_inputs(two);

        if one_outputs.len() % two_inputs.len() != 0 {
            panic!("ManyToOne - OneGroup outputs must be a multiple of TwoGroup inputs.");
        }

        for sources in one_outputs.chunks(two_inputs.len()) {
            for (source, target) in sources.iter().zip(two_inputs.iter()) {
                self.relationships.push(Relationship {
                    source_id: source.id(),
                    target_id: target.id(),
                });
            }
        }
    }

    fn all_to_all_connect(&mut self, one: &[&'a GraphNodeId], two: &[&'a GraphNodeId]) {
        let one_outputs = self.get_outputs(one);
        let two_inputs = self.get_inputs(two);

        for source in one_outputs {
            for target in two_inputs.iter() {
                self.relationships.push(Relationship {
                    source_id: source.id(),
                    target_id: target.id(),
                });
            }
        }
    }

    fn get_outputs(&self, ids: &[&'a GraphNodeId]) -> Vec<&'a GraphNode<T>> {
        let collection = ids
            .iter()
            .map(|id| self.nodes[id])
            .collect::<Vec<&'a GraphNode<T>>>();

        let outputs = collection
            .iter()
            .enumerate()
            .skip_while(|(_, node)| !node.outgoing().is_empty())
            .filter_map(|(idx, _)| collection.get(idx).cloned())
            .collect::<Vec<&GraphNode<T>>>();

        if !outputs.is_empty() {
            return outputs;
        }

        let recurrent_outputs = collection
            .iter()
            .enumerate()
            .filter(|(_, node)| {
                node.outgoing().len() == 1
                    && node.is_recurrent()
                    && node.node_type() == NodeType::Vertex
            })
            .filter_map(|(idx, _)| collection.get(idx).cloned())
            .collect::<Vec<&GraphNode<T>>>();

        if !recurrent_outputs.is_empty() {
            return recurrent_outputs;
        }

        collection
            .iter()
            .enumerate()
            .filter(|(_, node)| node.incoming().is_empty())
            .filter_map(|(idx, _)| collection.get(idx).cloned())
            .collect::<Vec<&GraphNode<T>>>()
    }

    fn get_inputs(&self, ids: &[&'a GraphNodeId]) -> Vec<&'a GraphNode<T>> {
        let collection = ids
            .iter()
            .map(|id| self.nodes[id])
            .collect::<Vec<&'a GraphNode<T>>>();

        let inputs = collection
            .iter()
            .enumerate()
            .take_while(|(_, node)| node.incoming().is_empty())
            .filter_map(|(idx, _)| collection.get(idx).cloned())
            .collect::<Vec<&GraphNode<T>>>();

        if !inputs.is_empty() {
            return inputs;
        }

        let recurrent_inputs = collection
            .iter()
            .enumerate()
            .filter(|(_, node)| {
                node.outgoing().len() == 1
                    && node.is_recurrent()
                    && node.node_type() == NodeType::Vertex
            })
            .filter_map(|(idx, _)| collection.get(idx).cloned())
            .collect::<Vec<&GraphNode<T>>>();

        if !recurrent_inputs.is_empty() {
            return recurrent_inputs;
        }

        collection
            .iter()
            .enumerate()
            .filter(|(_, node)| node.outgoing().is_empty())
            .filter_map(|(idx, _)| collection.get(idx).cloned())
            .collect::<Vec<&GraphNode<T>>>()
    }
}

#[cfg(test)]
mod tests {
    use crate::{GraphAggregate, GraphNode, Node, NodeType};
    use radiate_core::Valid;
    use std::panic;

    fn should_panic<F: FnOnce() -> () + panic::UnwindSafe>(f: F) {
        let does_panic = panic::catch_unwind(f).is_err();
        assert!(does_panic, "Expected function to panic, but it did not.");
    }

    #[test]
    fn test_graph_aggregate_one_to_one() {
        let graph = GraphAggregate::new()
            .one_to_one(
                &vec![
                    GraphNode::new(0, NodeType::Input, 0),
                    GraphNode::new(1, NodeType::Input, 1),
                ],
                &vec![
                    GraphNode::new(0, NodeType::Output, 0),
                    GraphNode::new(1, NodeType::Output, 1),
                ],
            )
            .build();

        let input_nodes = graph.inputs();
        let output_nodes = graph.outputs();

        let input_one = &graph[0];
        let input_two = &graph[1];
        let output_one = &graph[2];
        let output_two = &graph[3];

        assert_eq!(input_one.index(), 0);
        assert_eq!(input_two.index(), 1);
        assert_eq!(output_one.index(), 2);
        assert_eq!(output_two.index(), 3);

        for (in_node, out_node) in input_nodes.zip(output_nodes) {
            assert!(in_node.outgoing().contains(&out_node.index()));
            assert!(out_node.incoming().contains(&in_node.index()));

            assert_eq!(in_node.outgoing().len(), 1);
            assert_eq!(in_node.incoming().len(), 0);
            assert_eq!(out_node.incoming().len(), 1);
            assert_eq!(out_node.outgoing().len(), 0);
        }

        assert!(graph.is_valid());
    }

    #[test]
    fn test_graph_aggregate_one_to_many() {
        let graph = GraphAggregate::new()
            .one_to_many(
                &vec![
                    GraphNode::new(0, NodeType::Input, 0),
                    GraphNode::new(1, NodeType::Input, 1),
                ],
                &vec![
                    GraphNode::new(2, NodeType::Output, 0),
                    GraphNode::new(3, NodeType::Output, 1),
                    GraphNode::new(4, NodeType::Output, 2),
                    GraphNode::new(5, NodeType::Output, 3),
                ],
            )
            .build();

        let input_one = &graph[0];
        let input_two = &graph[1];

        let output_one = &graph[2];
        let output_two = &graph[3];
        let output_three = &graph[4];
        let output_four = &graph[5];

        assert!(input_one.outgoing().contains(&output_one.index()));
        assert!(input_one.outgoing().contains(&output_three.index()));
        assert!(input_two.outgoing().contains(&output_two.index()));
        assert!(input_two.outgoing().contains(&output_four.index()));
        assert!(output_one.incoming().contains(&input_one.index()));
        assert!(output_two.incoming().contains(&input_two.index()));
        assert!(output_three.incoming().contains(&input_one.index()));
        assert!(output_four.incoming().contains(&input_two.index()));

        assert_eq!(input_one.outgoing().len(), 2);
        assert_eq!(input_two.outgoing().len(), 2);
        assert_eq!(output_one.incoming().len(), 1);
        assert_eq!(output_two.incoming().len(), 1);
        assert_eq!(output_three.incoming().len(), 1);
        assert_eq!(output_four.incoming().len(), 1);

        assert!(graph.is_valid());
    }

    #[test]
    fn test_graph_aggregate_many_to_one() {
        let graph = GraphAggregate::new()
            .many_to_one(
                &vec![
                    GraphNode::new(0, NodeType::Input, 0),
                    GraphNode::new(123, NodeType::Input, 1),
                    GraphNode::new(111, NodeType::Input, 2),
                    GraphNode::new(3, NodeType::Input, 3),
                ],
                &vec![
                    GraphNode::new(10000, NodeType::Output, 0),
                    GraphNode::new(123, NodeType::Output, 1),
                ],
            )
            .build();

        let input_one = &graph[0];
        let input_two = &graph[1];
        let input_three = &graph[2];
        let input_four = &graph[3];

        let output_one = &graph[4];
        let output_two = &graph[5];

        assert_eq!(input_one.index(), 0);
        assert_eq!(input_two.index(), 1);
        assert_eq!(input_three.index(), 2);
        assert_eq!(input_four.index(), 3);
        assert_eq!(output_one.index(), 4);
        assert_eq!(output_two.index(), 5);

        assert!(input_one.outgoing().contains(&output_one.index()));
        assert!(input_three.outgoing().contains(&output_one.index()));
        assert!(input_two.outgoing().contains(&output_two.index()));
        assert!(input_four.outgoing().contains(&output_two.index()));
        assert!(output_one.incoming().contains(&input_one.index()));
        assert!(output_one.incoming().contains(&input_three.index()));
        assert!(output_two.incoming().contains(&input_two.index()));
        assert!(output_two.incoming().contains(&input_four.index()));

        assert_eq!(input_one.outgoing().len(), 1);
        assert_eq!(input_two.outgoing().len(), 1);
        assert_eq!(input_three.outgoing().len(), 1);
        assert_eq!(input_four.outgoing().len(), 1);
        assert_eq!(output_one.incoming().len(), 2);
        assert_eq!(output_two.incoming().len(), 2);

        assert!(graph.is_valid());
    }

    #[test]
    fn test_graph_aggregate_all_to_all() {
        let graph = GraphAggregate::new()
            .all_to_all(
                &vec![
                    GraphNode::new(0, NodeType::Input, 0),
                    GraphNode::new(1, NodeType::Input, 1),
                ],
                &vec![
                    GraphNode::new(2, NodeType::Output, 0),
                    GraphNode::new(3, NodeType::Output, 1),
                    GraphNode::new(4, NodeType::Output, 2),
                ],
            )
            .build();

        let input_one = &graph[0];
        let input_two = &graph[1];

        let output_one = &graph[2];
        let output_two = &graph[3];
        let output_three = &graph[4];

        assert!(input_one.outgoing().contains(&output_one.index()));
        assert!(input_one.outgoing().contains(&output_two.index()));
        assert!(input_one.outgoing().contains(&output_three.index()));
        assert!(input_two.outgoing().contains(&output_one.index()));
        assert!(input_two.outgoing().contains(&output_two.index()));
        assert!(input_two.outgoing().contains(&output_three.index()));

        assert!(output_one.incoming().contains(&input_one.index()));
        assert!(output_one.incoming().contains(&input_two.index()));
        assert!(output_two.incoming().contains(&input_one.index()));
        assert!(output_two.incoming().contains(&input_two.index()));
        assert!(output_three.incoming().contains(&input_one.index()));
        assert!(output_three.incoming().contains(&input_two.index()));

        assert_eq!(input_one.outgoing().len(), 3);
        assert_eq!(input_two.outgoing().len(), 3);
        assert_eq!(output_one.incoming().len(), 2);
        assert_eq!(output_two.incoming().len(), 2);
        assert_eq!(output_three.incoming().len(), 2);

        assert!(graph.is_valid());
    }

    #[test]
    fn test_graph_aggregate_cycle() {
        let graph = GraphAggregate::new()
            .cycle(&vec![
                GraphNode::new(0, NodeType::Vertex, 0),
                GraphNode::new(1, NodeType::Vertex, 1),
            ])
            .build();

        let node_one = &graph[0];
        let node_two = &graph[1];

        assert!(node_one.outgoing().contains(&node_one.index()));
        assert!(node_one.incoming().contains(&node_one.index()));
        assert!(node_one.is_recurrent());
        assert!(node_one.node_type() == NodeType::Vertex);

        assert!(node_two.outgoing().contains(&node_two.index()));
        assert!(node_two.incoming().contains(&node_two.index()));
        assert!(node_two.is_recurrent());
        assert!(node_two.node_type() == NodeType::Vertex);

        assert!(graph.is_valid());
    }

    #[test]
    fn test_graph_aggregate_layer() {
        let graph = GraphAggregate::new()
            .layer(vec![
                &vec![
                    GraphNode::new(0, NodeType::Input, "ONE"),
                    GraphNode::new(1, NodeType::Input, "TWO"),
                ],
                &vec![
                    GraphNode::new(2, NodeType::Vertex, "THREE"),
                    GraphNode::new(3, NodeType::Vertex, "FOUR"),
                ],
                &vec![
                    GraphNode::new(4, NodeType::Output, "FIVE"),
                    GraphNode::new(5, NodeType::Output, "SIX"),
                ],
            ])
            .build();

        assert!(graph.is_valid());

        let input_one = &graph[0];
        let input_two = &graph[1];
        let vertex_one = &graph[2];
        let vertex_two = &graph[3];
        let output_one = &graph[4];
        let output_two = &graph[5];

        assert!(input_one.outgoing().contains(&vertex_one.index()));
        assert!(input_two.outgoing().contains(&vertex_two.index()));
        assert!(vertex_one.incoming().contains(&input_one.index()));
        assert!(vertex_two.incoming().contains(&input_two.index()));
        assert!(vertex_one.outgoing().contains(&output_one.index()));
        assert!(vertex_two.outgoing().contains(&output_two.index()));
        assert!(output_one.incoming().contains(&vertex_one.index()));
        assert!(output_two.incoming().contains(&vertex_two.index()));
    }

    #[test]
    fn one_to_one_size_mismatch_panics() {
        should_panic(|| {
            let _ = GraphAggregate::new()
                .one_to_one(
                    &vec![GraphNode::new(0, NodeType::Input, 0)],
                    &vec![
                        GraphNode::new(1, NodeType::Output, 0),
                        GraphNode::new(2, NodeType::Output, 1),
                    ],
                )
                .build();
        });
    }

    #[test]
    fn one_to_many_invalid_multiple_panics() {
        should_panic(|| {
            let _ = GraphAggregate::new()
                .one_to_many(
                    &vec![
                        GraphNode::new(0, NodeType::Input, 0),
                        GraphNode::new(1, NodeType::Input, 1),
                    ],
                    &vec![
                        // 3 is NOT a multiple of 2 -> should panic
                        GraphNode::new(2, NodeType::Output, 0),
                        GraphNode::new(3, NodeType::Output, 1),
                        GraphNode::new(4, NodeType::Output, 2),
                    ],
                )
                .build();
        });
    }

    #[test]
    fn many_to_one_invalid_multiple_panics() {
        should_panic(|| {
            let _ = GraphAggregate::new()
                .many_to_one(
                    &vec![
                        GraphNode::new(0, NodeType::Input, 0),
                        GraphNode::new(1, NodeType::Input, 1),
                        GraphNode::new(2, NodeType::Input, 2),
                    ],
                    &vec![
                        // 3 (one) % 2 (two) != 0 -> panic
                        GraphNode::new(10, NodeType::Output, 0),
                        GraphNode::new(11, NodeType::Output, 1),
                    ],
                )
                .build();
        });
    }

    #[test]
    fn duplicate_insert_does_not_duplicate_nodes_or_edges() {
        let inputs = vec![
            GraphNode::new(0, NodeType::Input, "A"),
            GraphNode::new(1, NodeType::Input, "B"),
        ];
        let outputs = vec![
            GraphNode::new(2, NodeType::Output, "C"),
            GraphNode::new(3, NodeType::Output, "D"),
        ];

        let graph = GraphAggregate::new()
            .insert(&inputs) // first insert
            .insert(&inputs) // duplicate insert
            .one_to_one(&inputs, &outputs)
            .build();

        // Expect 4 nodes only
        assert_eq!(graph.len(), 4);

        // And exactly the two one-to-one edges
        let out0 = &graph[0].outgoing();
        let out1 = &graph[1].outgoing();
        assert_eq!(out0.len(), 1);
        assert_eq!(out1.len(), 1);
        assert!(out0.contains(&2));
        assert!(out1.contains(&3));
    }

    #[test]
    fn node_index_order_is_deterministic_by_insert_order() {
        // Insert order: inputs then outputs; indices should follow that order.
        let inputs = vec![
            GraphNode::new(10, NodeType::Input, "i0"),
            GraphNode::new(11, NodeType::Input, "i1"),
        ];
        let outputs = vec![
            GraphNode::new(20, NodeType::Output, "o0"),
            GraphNode::new(21, NodeType::Output, "o1"),
        ];

        let g1 = GraphAggregate::new()
            .insert(&inputs)
            .insert(&outputs)
            .build();

        assert_eq!(g1[0].node_type(), NodeType::Input);
        assert_eq!(g1[1].node_type(), NodeType::Input);
        assert_eq!(g1[2].node_type(), NodeType::Output);
        assert_eq!(g1[3].node_type(), NodeType::Output);
    }

    #[test]
    fn get_inputs_fallback_paths_are_respected() {
        // Make outputs with no incoming/outgoing to trigger input-fallback
        let outs = vec![
            GraphNode::new(100, NodeType::Output, "o0"),
            GraphNode::new(101, NodeType::Output, "o1"),
        ];

        let g = GraphAggregate::new()
            .one_to_one(
                &vec![
                    GraphNode::new(0, NodeType::Input, "i0"),
                    GraphNode::new(1, NodeType::Input, "i1"),
                ],
                &outs,
            )
            .build();

        // Inputs 0..1 connect to 2..3
        assert!(g[0].outgoing().contains(&2));
        assert!(g[1].outgoing().contains(&3));
        assert!(g[2].incoming().contains(&0));
        assert!(g[3].incoming().contains(&1));
        assert!(g.is_valid());
    }

    #[test]
    fn all_to_all_edge_counts_match() {
        let ins = vec![
            GraphNode::new(0, NodeType::Input, 0),
            GraphNode::new(1, NodeType::Input, 1),
            GraphNode::new(2, NodeType::Input, 2),
        ];
        let outs = vec![
            GraphNode::new(3, NodeType::Output, 0),
            GraphNode::new(4, NodeType::Output, 1),
        ];

        let g = GraphAggregate::new().all_to_all(&ins, &outs).build();

        // Each input connects to all outputs
        for i in 0..ins.len() {
            assert_eq!(g[i].outgoing().len(), outs.len());
        }

        // Each output receives from all inputs
        for j in 0..outs.len() {
            assert_eq!(g[ins.len() + j].incoming().len(), ins.len());
        }

        assert!(g.is_valid());
    }

    #[test]
    fn cycle_builds_self_loops_only() {
        let verts = vec![
            GraphNode::new(0, NodeType::Vertex, 0),
            GraphNode::new(1, NodeType::Vertex, 1),
            GraphNode::new(2, NodeType::Vertex, 2),
        ];

        let g = GraphAggregate::new().cycle(&verts).build();

        for i in 0..verts.len() {
            assert!(g[i].outgoing().contains(&i));
            assert!(g[i].incoming().contains(&i));
            assert_eq!(g[i].outgoing().len(), 1);
            assert_eq!(g[i].incoming().len(), 1);
        }
        assert!(g.is_valid());
    }

    #[test]
    fn one_to_many_chunking_is_correct() {
        // 2 inputs -> 6 outputs (chunks of 2)
        let ins = vec![
            GraphNode::new(0, NodeType::Input, "a"),
            GraphNode::new(1, NodeType::Input, "b"),
        ];
        let outs = vec![
            GraphNode::new(2, NodeType::Output, "o0"),
            GraphNode::new(3, NodeType::Output, "o1"),
            GraphNode::new(4, NodeType::Output, "o2"),
            GraphNode::new(5, NodeType::Output, "o3"),
            GraphNode::new(6, NodeType::Output, "o4"),
            GraphNode::new(7, NodeType::Output, "o5"),
        ];

        let g = GraphAggregate::new().one_to_many(&ins, &outs).build();

        // Expect pairs: (0->2,1->3),(0->4,1->5),(0->6,1->7)
        for (_, chunk_start) in (2..=6).step_by(2).enumerate() {
            let i0 = 0;
            let i1 = 1;
            let o0 = chunk_start;
            let o1 = chunk_start + 1;

            assert!(g[i0].outgoing().contains(&o0));
            assert!(g[i1].outgoing().contains(&o1));
            assert!(g[o0].incoming().contains(&i0));
            assert!(g[o1].incoming().contains(&i1));
        }
        assert!(g.is_valid());
    }

    #[test]
    fn many_to_one_chunking_is_correct() {
        // 6 inputs -> 2 outputs (chunks of 2)
        let ins = vec![
            GraphNode::new(0, NodeType::Input, "a"),
            GraphNode::new(1, NodeType::Input, "b"),
            GraphNode::new(2, NodeType::Input, "c"),
            GraphNode::new(3, NodeType::Input, "d"),
            GraphNode::new(4, NodeType::Input, "e"),
            GraphNode::new(5, NodeType::Input, "f"),
        ];
        let outs = vec![
            GraphNode::new(6, NodeType::Output, "o0"),
            GraphNode::new(7, NodeType::Output, "o1"),
        ];

        let g = GraphAggregate::new().many_to_one(&ins, &outs).build();

        // Expect pairs of inputs mapping to both outputs by chunks of outs.len()
        // Chunks: [0,1] -> [6,7]; [2,3] -> [6,7]; [4,5] -> [6,7]
        for pair in &[(0, 1), (2, 3), (4, 5)] {
            assert!(g[pair.0].outgoing().contains(&6));
            assert!(g[pair.1].outgoing().contains(&7));
            assert!(g[6].incoming().contains(&pair.0));
            assert!(g[7].incoming().contains(&pair.1));
        }
        assert!(g.is_valid());
    }
}
