netrun_sim/
graph.rs

1//! Graph topology types for flow-based development networks.
2//!
3//! This module defines the static structure of a network: nodes, ports, edges,
4//! and the conditions that govern packet flow (salvo conditions).
5
6use std::collections::{HashMap, HashSet};
7
8/// The name of a port on a node.
9pub type PortName = String;
10
11/// Specifies the capacity of a port (how many packets it can hold).
12#[derive(Debug, Clone)]
13pub enum PortSlotSpec {
14    /// Port can hold unlimited packets.
15    Infinite,
16    /// Port can hold at most this many packets.
17    Finite(u64),
18}
19
20/// A port on a node where packets can enter or exit.
21#[derive(Debug, Clone)]
22pub struct Port {
23    /// The capacity specification for this port.
24    pub slots_spec: PortSlotSpec,
25}
26
27/// A predicate on the state of a port, used in salvo conditions.
28///
29/// These predicates test the current packet count at a port against
30/// various conditions like empty, full, or numeric comparisons.
31#[derive(Debug, Clone)]
32pub enum PortState {
33    /// Port has zero packets.
34    Empty,
35    /// Port is at capacity (always false for infinite ports).
36    Full,
37    /// Port has at least one packet.
38    NonEmpty,
39    /// Port is below capacity (always true for infinite ports).
40    NonFull,
41    /// Port has exactly this many packets.
42    Equals(u64),
43    /// Port has fewer than this many packets.
44    LessThan(u64),
45    /// Port has more than this many packets.
46    GreaterThan(u64),
47    /// Port has at most this many packets.
48    EqualsOrLessThan(u64),
49    /// Port has at least this many packets.
50    EqualsOrGreaterThan(u64),
51}
52
53/// A boolean expression over port states, used to define when salvos can trigger.
54///
55/// This forms a simple expression tree that can combine port state checks
56/// with logical operators (And, Or, Not).
57#[derive(Debug, Clone)]
58pub enum SalvoConditionTerm {
59    /// Check if a specific port matches a state predicate.
60    Port { port_name: String, state: PortState },
61    /// All sub-terms must be true.
62    And(Vec<Self>),
63    /// At least one sub-term must be true.
64    Or(Vec<Self>),
65    /// The sub-term must be false.
66    Not(Box<Self>),
67}
68
69/// Evaluates a salvo condition term against the current port packet counts.
70///
71/// # Arguments
72/// * `term` - The condition term to evaluate
73/// * `port_packet_counts` - Map of port names to their current packet counts
74/// * `ports` - Map of port names to their definitions (needed for capacity checks)
75///
76/// # Returns
77/// `true` if the condition is satisfied, `false` otherwise.
78pub fn evaluate_salvo_condition(
79    term: &SalvoConditionTerm,
80    port_packet_counts: &HashMap<PortName, u64>,
81    ports: &HashMap<PortName, Port>,
82) -> bool {
83    match term {
84        SalvoConditionTerm::Port { port_name, state } => {
85            let count = *port_packet_counts.get(port_name).unwrap_or(&0);
86            let port = ports.get(port_name);
87
88            match state {
89                PortState::Empty => count == 0,
90                PortState::Full => match port {
91                    Some(p) => match p.slots_spec {
92                        PortSlotSpec::Infinite => false, // Infinite port can never be full
93                        PortSlotSpec::Finite(max) => count >= max,
94                    },
95                    None => false,
96                },
97                PortState::NonEmpty => count > 0,
98                PortState::NonFull => match port {
99                    Some(p) => match p.slots_spec {
100                        PortSlotSpec::Infinite => true, // Infinite port is always non-full
101                        PortSlotSpec::Finite(max) => count < max,
102                    },
103                    None => true,
104                },
105                PortState::Equals(n) => count == *n,
106                PortState::LessThan(n) => count < *n,
107                PortState::GreaterThan(n) => count > *n,
108                PortState::EqualsOrLessThan(n) => count <= *n,
109                PortState::EqualsOrGreaterThan(n) => count >= *n,
110            }
111        }
112        SalvoConditionTerm::And(terms) => {
113            terms.iter().all(|t| evaluate_salvo_condition(t, port_packet_counts, ports))
114        }
115        SalvoConditionTerm::Or(terms) => {
116            terms.iter().any(|t| evaluate_salvo_condition(t, port_packet_counts, ports))
117        }
118        SalvoConditionTerm::Not(inner) => {
119            !evaluate_salvo_condition(inner, port_packet_counts, ports)
120        }
121    }
122}
123
124/// The name of a salvo condition.
125pub type SalvoConditionName = String;
126
127/// A condition that defines when packets can trigger an epoch or be sent.
128///
129/// Salvo conditions are attached to nodes and control the flow of packets:
130/// - **Input salvo conditions**: Define when packets at input ports can trigger a new epoch
131/// - **Output salvo conditions**: Define when packets at output ports can be sent out
132#[derive(Debug, Clone)]
133pub struct SalvoCondition {
134    /// Maximum number of times this condition can trigger per epoch.
135    /// For input salvo conditions, this must be 1.
136    /// For output salvo conditions, 0 means unlimited.
137    pub max_salvos: u64,
138    /// The ports whose packets are included when this condition triggers.
139    pub ports: Vec<PortName>,
140    /// The boolean condition that must be satisfied for this salvo to trigger.
141    pub term: SalvoConditionTerm,
142}
143
144/// Extracts all port names referenced in a SalvoConditionTerm.
145fn collect_ports_from_term(term: &SalvoConditionTerm, ports: &mut HashSet<PortName>) {
146    match term {
147        SalvoConditionTerm::Port { port_name, .. } => {
148            ports.insert(port_name.clone());
149        }
150        SalvoConditionTerm::And(terms) | SalvoConditionTerm::Or(terms) => {
151            for t in terms {
152                collect_ports_from_term(t, ports);
153            }
154        }
155        SalvoConditionTerm::Not(inner) => {
156            collect_ports_from_term(inner, ports);
157        }
158    }
159}
160
161/// Errors that can occur during graph validation
162#[derive(Debug, Clone, PartialEq, thiserror::Error)]
163pub enum GraphValidationError {
164    /// Edge references a node that doesn't exist
165    #[error("edge {edge_source} -> {edge_target} references non-existent node '{missing_node}'")]
166    EdgeReferencesNonexistentNode {
167        edge_source: PortRef,
168        edge_target: PortRef,
169        missing_node: NodeName,
170    },
171    /// Edge references a port that doesn't exist on the node
172    #[error("edge {edge_source} -> {edge_target} references non-existent port {missing_port}")]
173    EdgeReferencesNonexistentPort {
174        edge_source: PortRef,
175        edge_target: PortRef,
176        missing_port: PortRef,
177    },
178    /// Edge source is not an output port
179    #[error("edge source {edge_source} must be an output port")]
180    EdgeSourceNotOutputPort {
181        edge_source: PortRef,
182        edge_target: PortRef,
183    },
184    /// Edge target is not an input port
185    #[error("edge target {edge_target} must be an input port")]
186    EdgeTargetNotInputPort {
187        edge_source: PortRef,
188        edge_target: PortRef,
189    },
190    /// SalvoCondition.ports references a port that doesn't exist
191    #[error("{condition_type} salvo condition '{condition_name}' on node '{node_name}' references non-existent port '{missing_port}'", condition_type = if *is_input_condition { "input" } else { "output" })]
192    SalvoConditionReferencesNonexistentPort {
193        node_name: NodeName,
194        condition_name: SalvoConditionName,
195        is_input_condition: bool,
196        missing_port: PortName,
197    },
198    /// SalvoCondition.term references a port that doesn't exist
199    #[error("{condition_type} salvo condition '{condition_name}' on node '{node_name}' has term referencing non-existent port '{missing_port}'", condition_type = if *is_input_condition { "input" } else { "output" })]
200    SalvoConditionTermReferencesNonexistentPort {
201        node_name: NodeName,
202        condition_name: SalvoConditionName,
203        is_input_condition: bool,
204        missing_port: PortName,
205    },
206    /// Input salvo condition has max_salvos != 1
207    #[error("input salvo condition '{condition_name}' on node '{node_name}' has max_salvos={max_salvos}, but must be 1")]
208    InputSalvoConditionInvalidMaxSalvos {
209        node_name: NodeName,
210        condition_name: SalvoConditionName,
211        max_salvos: u64,
212    },
213    /// Duplicate edge (same source and target)
214    #[error("duplicate edge: {edge_source} -> {edge_target}")]
215    DuplicateEdge {
216        edge_source: PortRef,
217        edge_target: PortRef,
218    },
219}
220
221/// The name of a node in the graph.
222pub type NodeName = String;
223
224/// A processing unit in the graph with input and output ports.
225///
226/// Nodes are the fundamental building blocks of a flow-based network.
227/// They have:
228/// - Input ports where packets arrive
229/// - Output ports where packets are sent
230/// - Input salvo conditions that define when arriving packets trigger an epoch
231/// - Output salvo conditions that define when output packets can be sent
232#[derive(Debug, Clone)]
233pub struct Node {
234    /// The unique name of this node.
235    pub name: NodeName,
236    /// Input ports where packets can arrive.
237    pub in_ports: HashMap<PortName, Port>,
238    /// Output ports where packets can be sent.
239    pub out_ports: HashMap<PortName, Port>,
240    /// Conditions that trigger new epochs when satisfied by packets at input ports.
241    pub in_salvo_conditions: HashMap<SalvoConditionName, SalvoCondition>,
242    /// Conditions that must be satisfied to send packets from output ports.
243    pub out_salvo_conditions: HashMap<SalvoConditionName, SalvoCondition>,
244}
245
246/// Whether a port is for input or output.
247#[derive(Debug, Clone, PartialEq, Eq, Hash)]
248pub enum PortType {
249    /// An input port (packets flow into the node).
250    Input,
251    /// An output port (packets flow out of the node).
252    Output,
253}
254
255/// A reference to a specific port on a specific node.
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257pub struct PortRef {
258    /// The name of the node containing this port.
259    pub node_name: NodeName,
260    /// Whether this is an input or output port.
261    pub port_type: PortType,
262    /// The name of the port on the node.
263    pub port_name: PortName,
264}
265
266impl std::fmt::Display for PortRef {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        let port_type_str = match self.port_type {
269            PortType::Input => "in",
270            PortType::Output => "out",
271        };
272        write!(f, "{}.{}.{}", self.node_name, port_type_str, self.port_name)
273    }
274}
275
276/// A connection between two ports in the graph.
277///
278/// Edges connect output ports to input ports, allowing packets to flow
279/// between nodes.
280#[derive(Debug, Clone, PartialEq, Eq, Hash)]
281pub struct Edge {
282    /// The output port where this edge originates.
283    pub source: PortRef,
284    /// The input port where this edge terminates.
285    pub target: PortRef,
286}
287
288impl std::fmt::Display for Edge {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        write!(f, "{} -> {}", self.source, self.target)
291    }
292}
293
294/// The static topology of a flow-based network.
295///
296/// A Graph defines the structure of a network: which nodes exist, how they're
297/// connected, and what conditions govern packet flow. The graph is immutable
298/// after creation.
299///
300/// # Example
301///
302/// ```
303/// use netrun_sim::graph::{Graph, Node, Edge, PortRef, PortType, Port, PortSlotSpec};
304/// use std::collections::HashMap;
305///
306/// // Create a simple A -> B graph
307/// let node_a = Node {
308///     name: "A".to_string(),
309///     in_ports: HashMap::new(),
310///     out_ports: [("out".to_string(), Port { slots_spec: PortSlotSpec::Infinite })].into(),
311///     in_salvo_conditions: HashMap::new(),
312///     out_salvo_conditions: HashMap::new(),
313/// };
314/// let node_b = Node {
315///     name: "B".to_string(),
316///     in_ports: [("in".to_string(), Port { slots_spec: PortSlotSpec::Infinite })].into(),
317///     out_ports: HashMap::new(),
318///     in_salvo_conditions: HashMap::new(),
319///     out_salvo_conditions: HashMap::new(),
320/// };
321///
322/// let edge = Edge {
323///     source: PortRef { node_name: "A".to_string(), port_type: PortType::Output, port_name: "out".to_string() },
324///     target: PortRef { node_name: "B".to_string(), port_type: PortType::Input, port_name: "in".to_string() },
325/// };
326///
327/// let graph = Graph::new(vec![node_a, node_b], vec![edge]);
328/// assert!(graph.validate().is_empty());
329/// ```
330#[derive(Debug, Clone)]
331pub struct Graph {
332    nodes: HashMap<NodeName, Node>,
333    edges: HashSet<Edge>,
334    edges_by_tail: HashMap<PortRef, Edge>,
335    edges_by_head: HashMap<PortRef, Edge>,
336}
337
338impl Graph {
339    /// Creates a new Graph from a list of nodes and edges.
340    ///
341    /// Builds internal indexes for efficient edge lookups by source (tail) and target (head) ports.
342    pub fn new(nodes: Vec<Node>, edges: Vec<Edge>) -> Self {
343        let nodes_map: HashMap<NodeName, Node> = nodes
344            .into_iter()
345            .map(|node| (node.name.clone(), node))
346            .collect();
347
348        let mut edges_set: HashSet<Edge> = HashSet::new();
349        let mut edges_by_tail: HashMap<PortRef, Edge> = HashMap::new();
350        let mut edges_by_head: HashMap<PortRef, Edge> = HashMap::new();
351
352        for edge in edges {
353            edges_by_tail.insert(edge.source.clone(), edge.clone());
354            edges_by_head.insert(edge.target.clone(), edge.clone());
355            edges_set.insert(edge);
356        }
357
358        Graph {
359            nodes: nodes_map,
360            edges: edges_set,
361            edges_by_tail,
362            edges_by_head,
363        }
364    }
365
366    /// Returns a reference to all nodes in the graph, keyed by name.
367    pub fn nodes(&self) -> &HashMap<NodeName, Node> { &self.nodes }
368
369    /// Returns a reference to all edges in the graph.
370    pub fn edges(&self) -> &HashSet<Edge> { &self.edges }
371
372    /// Returns the edge that has the given output port as its source (tail).
373    pub fn get_edge_by_tail(&self, output_port_ref: &PortRef) -> Option<&Edge> {
374        self.edges_by_tail.get(output_port_ref)
375    }
376
377    /// Returns the edge that has the given input port as its target (head).
378    pub fn get_edge_by_head(&self, input_port_ref: &PortRef) -> Option<&Edge> {
379        self.edges_by_head.get(input_port_ref)
380    }
381
382    /// Validates the graph structure.
383    ///
384    /// Returns a list of all validation errors found. An empty list means the graph is valid.
385    pub fn validate(&self) -> Vec<GraphValidationError> {
386        let mut errors = Vec::new();
387
388        // Track seen edges to detect duplicates
389        let mut seen_edges: HashSet<(&PortRef, &PortRef)> = HashSet::new();
390
391        // Validate edges
392        for edge in &self.edges {
393            let source = &edge.source;
394            let target = &edge.target;
395
396            // Check for duplicate edges
397            if !seen_edges.insert((source, target)) {
398                errors.push(GraphValidationError::DuplicateEdge {
399                    edge_source: source.clone(),
400                    edge_target: target.clone(),
401                });
402                continue;
403            }
404
405            // Validate source node exists
406            let source_node = match self.nodes.get(&source.node_name) {
407                Some(node) => node,
408                None => {
409                    errors.push(GraphValidationError::EdgeReferencesNonexistentNode {
410                        edge_source: source.clone(),
411                        edge_target: target.clone(),
412                        missing_node: source.node_name.clone(),
413                    });
414                    continue;
415                }
416            };
417
418            // Validate target node exists
419            let target_node = match self.nodes.get(&target.node_name) {
420                Some(node) => node,
421                None => {
422                    errors.push(GraphValidationError::EdgeReferencesNonexistentNode {
423                        edge_source: source.clone(),
424                        edge_target: target.clone(),
425                        missing_node: target.node_name.clone(),
426                    });
427                    continue;
428                }
429            };
430
431            // Validate source is an output port
432            if source.port_type != PortType::Output {
433                errors.push(GraphValidationError::EdgeSourceNotOutputPort {
434                    edge_source: source.clone(),
435                    edge_target: target.clone(),
436                });
437            } else if !source_node.out_ports.contains_key(&source.port_name) {
438                errors.push(GraphValidationError::EdgeReferencesNonexistentPort {
439                    edge_source: source.clone(),
440                    edge_target: target.clone(),
441                    missing_port: source.clone(),
442                });
443            }
444
445            // Validate target is an input port
446            if target.port_type != PortType::Input {
447                errors.push(GraphValidationError::EdgeTargetNotInputPort {
448                    edge_source: source.clone(),
449                    edge_target: target.clone(),
450                });
451            } else if !target_node.in_ports.contains_key(&target.port_name) {
452                errors.push(GraphValidationError::EdgeReferencesNonexistentPort {
453                    edge_source: source.clone(),
454                    edge_target: target.clone(),
455                    missing_port: target.clone(),
456                });
457            }
458        }
459
460        // Validate nodes and their salvo conditions
461        for (node_name, node) in &self.nodes {
462            // Validate input salvo conditions
463            for (cond_name, condition) in &node.in_salvo_conditions {
464                // Input salvo conditions must have max_salvos == 1
465                if condition.max_salvos != 1 {
466                    errors.push(GraphValidationError::InputSalvoConditionInvalidMaxSalvos {
467                        node_name: node_name.clone(),
468                        condition_name: cond_name.clone(),
469                        max_salvos: condition.max_salvos,
470                    });
471                }
472
473                // Validate ports in condition.ports exist as input ports
474                for port_name in &condition.ports {
475                    if !node.in_ports.contains_key(port_name) {
476                        errors.push(GraphValidationError::SalvoConditionReferencesNonexistentPort {
477                            node_name: node_name.clone(),
478                            condition_name: cond_name.clone(),
479                            is_input_condition: true,
480                            missing_port: port_name.clone(),
481                        });
482                    }
483                }
484
485                // Validate ports in condition.term exist as input ports
486                let mut term_ports = HashSet::new();
487                collect_ports_from_term(&condition.term, &mut term_ports);
488                for port_name in term_ports {
489                    if !node.in_ports.contains_key(&port_name) {
490                        errors.push(GraphValidationError::SalvoConditionTermReferencesNonexistentPort {
491                            node_name: node_name.clone(),
492                            condition_name: cond_name.clone(),
493                            is_input_condition: true,
494                            missing_port: port_name,
495                        });
496                    }
497                }
498            }
499
500            // Validate output salvo conditions
501            for (cond_name, condition) in &node.out_salvo_conditions {
502                // Validate ports in condition.ports exist as output ports
503                for port_name in &condition.ports {
504                    if !node.out_ports.contains_key(port_name) {
505                        errors.push(GraphValidationError::SalvoConditionReferencesNonexistentPort {
506                            node_name: node_name.clone(),
507                            condition_name: cond_name.clone(),
508                            is_input_condition: false,
509                            missing_port: port_name.clone(),
510                        });
511                    }
512                }
513
514                // Validate ports in condition.term exist as output ports
515                let mut term_ports = HashSet::new();
516                collect_ports_from_term(&condition.term, &mut term_ports);
517                for port_name in term_ports {
518                    if !node.out_ports.contains_key(&port_name) {
519                        errors.push(GraphValidationError::SalvoConditionTermReferencesNonexistentPort {
520                            node_name: node_name.clone(),
521                            condition_name: cond_name.clone(),
522                            is_input_condition: false,
523                            missing_port: port_name,
524                        });
525                    }
526                }
527            }
528        }
529
530        errors
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    // Helper functions for tests
539    fn simple_port() -> Port {
540        Port { slots_spec: PortSlotSpec::Infinite }
541    }
542
543    fn simple_node(name: &str, in_ports: Vec<&str>, out_ports: Vec<&str>) -> Node {
544        let in_ports_map: HashMap<PortName, Port> = in_ports
545            .iter()
546            .map(|p| (p.to_string(), simple_port()))
547            .collect();
548        let out_ports_map: HashMap<PortName, Port> = out_ports
549            .iter()
550            .map(|p| (p.to_string(), simple_port()))
551            .collect();
552
553        // Default input salvo condition
554        let mut in_salvo_conditions = HashMap::new();
555        if !in_ports.is_empty() {
556            in_salvo_conditions.insert(
557                "default".to_string(),
558                SalvoCondition {
559                    max_salvos: 1,
560                    ports: in_ports.iter().map(|s| s.to_string()).collect(),
561                    term: SalvoConditionTerm::Port {
562                        port_name: in_ports[0].to_string(),
563                        state: PortState::NonEmpty,
564                    },
565                },
566            );
567        }
568
569        Node {
570            name: name.to_string(),
571            in_ports: in_ports_map,
572            out_ports: out_ports_map,
573            in_salvo_conditions,
574            out_salvo_conditions: HashMap::new(),
575        }
576    }
577
578    fn edge(src_node: &str, src_port: &str, tgt_node: &str, tgt_port: &str) -> Edge {
579        Edge {
580            source: PortRef {
581                node_name: src_node.to_string(),
582                port_type: PortType::Output,
583                port_name: src_port.to_string(),
584            },
585            target: PortRef {
586                node_name: tgt_node.to_string(),
587                port_type: PortType::Input,
588                port_name: tgt_port.to_string(),
589            },
590        }
591    }
592
593    #[test]
594    fn test_valid_graph_passes_validation() {
595        let nodes = vec![
596            simple_node("A", vec![], vec!["out"]),
597            simple_node("B", vec!["in"], vec![]),
598        ];
599        let edges = vec![edge("A", "out", "B", "in")];
600        let graph = Graph::new(nodes, edges);
601
602        let errors = graph.validate();
603        assert!(errors.is_empty(), "Expected no errors, got: {:?}", errors);
604    }
605
606    #[test]
607    fn test_edge_references_nonexistent_source_node() {
608        let nodes = vec![
609            simple_node("B", vec!["in"], vec![]),
610        ];
611        // Edge from nonexistent node "A"
612        let edges = vec![edge("A", "out", "B", "in")];
613        let graph = Graph::new(nodes, edges);
614
615        let errors = graph.validate();
616        assert_eq!(errors.len(), 1);
617        match &errors[0] {
618            GraphValidationError::EdgeReferencesNonexistentNode { missing_node, .. } => {
619                assert_eq!(missing_node, "A");
620            }
621            _ => panic!("Expected EdgeReferencesNonexistentNode, got: {:?}", errors[0]),
622        }
623    }
624
625    #[test]
626    fn test_edge_references_nonexistent_target_node() {
627        let nodes = vec![
628            simple_node("A", vec![], vec!["out"]),
629        ];
630        // Edge to nonexistent node "B"
631        let edges = vec![edge("A", "out", "B", "in")];
632        let graph = Graph::new(nodes, edges);
633
634        let errors = graph.validate();
635        assert_eq!(errors.len(), 1);
636        match &errors[0] {
637            GraphValidationError::EdgeReferencesNonexistentNode { missing_node, .. } => {
638                assert_eq!(missing_node, "B");
639            }
640            _ => panic!("Expected EdgeReferencesNonexistentNode, got: {:?}", errors[0]),
641        }
642    }
643
644    #[test]
645    fn test_edge_references_nonexistent_source_port() {
646        let nodes = vec![
647            simple_node("A", vec![], vec!["out"]),
648            simple_node("B", vec!["in"], vec![]),
649        ];
650        // Edge from nonexistent port "wrong_port"
651        let edges = vec![edge("A", "wrong_port", "B", "in")];
652        let graph = Graph::new(nodes, edges);
653
654        let errors = graph.validate();
655        assert_eq!(errors.len(), 1);
656        match &errors[0] {
657            GraphValidationError::EdgeReferencesNonexistentPort { missing_port, .. } => {
658                assert_eq!(missing_port.port_name, "wrong_port");
659            }
660            _ => panic!("Expected EdgeReferencesNonexistentPort, got: {:?}", errors[0]),
661        }
662    }
663
664    #[test]
665    fn test_edge_references_nonexistent_target_port() {
666        let nodes = vec![
667            simple_node("A", vec![], vec!["out"]),
668            simple_node("B", vec!["in"], vec![]),
669        ];
670        // Edge to nonexistent port "wrong_port"
671        let edges = vec![edge("A", "out", "B", "wrong_port")];
672        let graph = Graph::new(nodes, edges);
673
674        let errors = graph.validate();
675        assert_eq!(errors.len(), 1);
676        match &errors[0] {
677            GraphValidationError::EdgeReferencesNonexistentPort { missing_port, .. } => {
678                assert_eq!(missing_port.port_name, "wrong_port");
679            }
680            _ => panic!("Expected EdgeReferencesNonexistentPort, got: {:?}", errors[0]),
681        }
682    }
683
684    #[test]
685    fn test_edge_source_must_be_output_port() {
686        let nodes = vec![
687            simple_node("A", vec!["in"], vec!["out"]),
688            simple_node("B", vec!["in"], vec![]),
689        ];
690        // Edge from input port (wrong type)
691        let edges = vec![Edge {
692            source: PortRef {
693                node_name: "A".to_string(),
694                port_type: PortType::Input, // Wrong!
695                port_name: "in".to_string(),
696            },
697            target: PortRef {
698                node_name: "B".to_string(),
699                port_type: PortType::Input,
700                port_name: "in".to_string(),
701            },
702        }];
703        let graph = Graph::new(nodes, edges);
704
705        let errors = graph.validate();
706        assert!(errors.iter().any(|e| matches!(e, GraphValidationError::EdgeSourceNotOutputPort { .. })));
707    }
708
709    #[test]
710    fn test_edge_target_must_be_input_port() {
711        let nodes = vec![
712            simple_node("A", vec![], vec!["out"]),
713            simple_node("B", vec!["in"], vec!["out"]),
714        ];
715        // Edge to output port (wrong type)
716        let edges = vec![Edge {
717            source: PortRef {
718                node_name: "A".to_string(),
719                port_type: PortType::Output,
720                port_name: "out".to_string(),
721            },
722            target: PortRef {
723                node_name: "B".to_string(),
724                port_type: PortType::Output, // Wrong!
725                port_name: "out".to_string(),
726            },
727        }];
728        let graph = Graph::new(nodes, edges);
729
730        let errors = graph.validate();
731        assert!(errors.iter().any(|e| matches!(e, GraphValidationError::EdgeTargetNotInputPort { .. })));
732    }
733
734    #[test]
735    fn test_input_salvo_condition_must_have_max_salvos_1() {
736        let mut node = simple_node("A", vec!["in"], vec![]);
737        // Set max_salvos to something other than 1
738        node.in_salvo_conditions.get_mut("default").unwrap().max_salvos = 2;
739
740        let graph = Graph::new(vec![node], vec![]);
741
742        let errors = graph.validate();
743        assert_eq!(errors.len(), 1);
744        match &errors[0] {
745            GraphValidationError::InputSalvoConditionInvalidMaxSalvos { max_salvos, .. } => {
746                assert_eq!(*max_salvos, 2);
747            }
748            _ => panic!("Expected InputSalvoConditionInvalidMaxSalvos, got: {:?}", errors[0]),
749        }
750    }
751
752    #[test]
753    fn test_salvo_condition_ports_must_exist() {
754        let mut node = simple_node("A", vec!["in"], vec![]);
755        // Reference nonexistent port in condition.ports
756        node.in_salvo_conditions.get_mut("default").unwrap().ports = vec!["nonexistent".to_string()];
757
758        let graph = Graph::new(vec![node], vec![]);
759
760        let errors = graph.validate();
761        assert!(errors.iter().any(|e| matches!(
762            e,
763            GraphValidationError::SalvoConditionReferencesNonexistentPort { missing_port, .. }
764            if missing_port == "nonexistent"
765        )));
766    }
767
768    #[test]
769    fn test_salvo_condition_term_ports_must_exist() {
770        let mut node = simple_node("A", vec!["in"], vec![]);
771        // Reference nonexistent port in condition.term
772        node.in_salvo_conditions.get_mut("default").unwrap().term = SalvoConditionTerm::Port {
773            port_name: "nonexistent".to_string(),
774            state: PortState::NonEmpty,
775        };
776
777        let graph = Graph::new(vec![node], vec![]);
778
779        let errors = graph.validate();
780        assert!(errors.iter().any(|e| matches!(
781            e,
782            GraphValidationError::SalvoConditionTermReferencesNonexistentPort { missing_port, .. }
783            if missing_port == "nonexistent"
784        )));
785    }
786
787    #[test]
788    fn test_output_salvo_condition_ports_must_exist() {
789        let mut node = simple_node("A", vec![], vec!["out"]);
790        // Add output salvo condition referencing nonexistent port
791        node.out_salvo_conditions.insert(
792            "test".to_string(),
793            SalvoCondition {
794                max_salvos: 0,
795                ports: vec!["nonexistent".to_string()],
796                term: SalvoConditionTerm::Port {
797                    port_name: "out".to_string(),
798                    state: PortState::NonEmpty,
799                },
800            },
801        );
802
803        let graph = Graph::new(vec![node], vec![]);
804
805        let errors = graph.validate();
806        assert!(errors.iter().any(|e| matches!(
807            e,
808            GraphValidationError::SalvoConditionReferencesNonexistentPort {
809                is_input_condition: false,
810                missing_port,
811                ..
812            } if missing_port == "nonexistent"
813        )));
814    }
815
816    #[test]
817    fn test_complex_salvo_condition_term_validation() {
818        let mut node = simple_node("A", vec!["in1", "in2"], vec![]);
819        // Create complex term with And/Or/Not that references nonexistent port
820        node.in_salvo_conditions.get_mut("default").unwrap().term = SalvoConditionTerm::And(vec![
821            SalvoConditionTerm::Port {
822                port_name: "in1".to_string(),
823                state: PortState::NonEmpty,
824            },
825            SalvoConditionTerm::Or(vec![
826                SalvoConditionTerm::Port {
827                    port_name: "in2".to_string(),
828                    state: PortState::NonEmpty,
829                },
830                SalvoConditionTerm::Not(Box::new(SalvoConditionTerm::Port {
831                    port_name: "nonexistent".to_string(), // This should be caught
832                    state: PortState::Empty,
833                })),
834            ]),
835        ]);
836
837        let graph = Graph::new(vec![node], vec![]);
838
839        let errors = graph.validate();
840        assert!(errors.iter().any(|e| matches!(
841            e,
842            GraphValidationError::SalvoConditionTermReferencesNonexistentPort { missing_port, .. }
843            if missing_port == "nonexistent"
844        )));
845    }
846
847    #[test]
848    fn test_empty_graph_is_valid() {
849        let graph = Graph::new(vec![], vec![]);
850        let errors = graph.validate();
851        assert!(errors.is_empty());
852    }
853
854    #[test]
855    fn test_node_without_ports_is_valid() {
856        let node = Node {
857            name: "A".to_string(),
858            in_ports: HashMap::new(),
859            out_ports: HashMap::new(),
860            in_salvo_conditions: HashMap::new(),
861            out_salvo_conditions: HashMap::new(),
862        };
863        let graph = Graph::new(vec![node], vec![]);
864        let errors = graph.validate();
865        assert!(errors.is_empty());
866    }
867}