Skip to main content

termiflow/
graph.rs

1//! Graph data structures - Node, Edge, Graph, Subgraph
2
3use std::collections::{HashMap, HashSet};
4
5use crate::geom::EdgeRoute;
6use crate::style::BOX_HEIGHT;
7
8/// Node shape variants from Mermaid syntax
9#[derive(Debug, Clone, Copy, Default, PartialEq)]
10pub enum NodeShape {
11    #[default]
12    Rectangle, // [text] - default box
13    Rounded,          // (text) - rounded corners
14    Diamond,          // {text} - decision diamond
15    Circle,           // ((text)) - circular node
16    Stadium,          // ([text]) - pill/stadium shape
17    Asymmetric,       // >text] - flag shape
18    Parallelogram,    // [/text/] - parallelogram (lean right)
19    ParallelogramAlt, // [\text\] - parallelogram (lean left)
20    Trapezoid,        // [/text\] - trapezoid (wider top)
21    TrapezoidAlt,     // [\text/] - trapezoid (wider bottom)
22    Hexagon,          // {{text}} - hexagon
23    Database,         // [(text)] - cylinder/database
24    Subroutine,       // [[text]] - subroutine box
25    DoubleCircle,     // (((text))) - double circle (event/start)
26}
27
28/// Node in the graph (positioned after layout)
29#[derive(Debug, Clone)]
30pub struct Node {
31    pub id: String,
32    pub label: String,
33    /// Pre-measured label lines for rendering (optional; empty means "use label").
34    pub label_lines: Vec<String>,
35    pub shape: NodeShape, // Node shape from syntax
36    #[allow(dead_code)]
37    pub click_target: Option<String>, // Drill-down target from `click ID "file.md"`
38    pub x: usize,         // Column position (set by layout)
39    pub y: usize,         // Row position (set by layout)
40    pub width: usize,     // Calculated from label
41    pub height: usize,    // Box height in rows (default = BOX_HEIGHT)
42    pub rank: usize,      // Depth in graph (0 = root)
43}
44
45impl Node {
46    /// Create a new node with default rectangle shape
47    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
48        Self::with_shape(id, label, NodeShape::Rectangle)
49    }
50
51    /// Create a new node with a specific shape
52    pub fn with_shape(id: impl Into<String>, label: impl Into<String>, shape: NodeShape) -> Self {
53        let label = label.into();
54        Self {
55            id: id.into(),
56            width: crate::style::box_width(&label),
57            label,
58            label_lines: Vec::new(),
59            shape,
60            click_target: None,
61            x: 0,
62            y: 0,
63            height: BOX_HEIGHT,
64            rank: 0,
65        }
66    }
67
68    /// Visual center x-coordinate
69    #[inline]
70    pub fn center_x(&self) -> usize {
71        self.x + self.width / 2
72    }
73
74    /// Visual center y-coordinate
75    #[inline]
76    pub fn center_y(&self) -> usize {
77        let h = self.height.max(BOX_HEIGHT);
78        self.y + h / 2
79    }
80
81    #[inline]
82    pub fn bottom_y(&self) -> usize {
83        self.y + self.height.max(BOX_HEIGHT)
84    }
85}
86
87/// Visual/semantic kind of an edge, matching Mermaid flowchart syntax.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub enum EdgeKind {
90    #[default]
91    Arrow, // --> standard directed with arrowhead
92    Open,          // --- open link, no arrowhead
93    Thick,         // ==> heavy/bold shaft with arrowhead
94    Dotted,        // -.-> dashed shaft with arrowhead
95    Bidirectional, // <--> arrowheads on both ends
96    CircleEnd,     // --o circle end marker (non-directional)
97    CrossEnd,      // --x cross end marker (non-directional)
98}
99
100/// Edge connecting two nodes
101#[derive(Debug, Clone)]
102pub struct Edge {
103    pub from: String,          // Source node ID
104    pub to: String,            // Target node ID
105    pub label: Option<String>, // Optional edge label (from -->|label| syntax)
106    pub is_back_edge: bool,    // True if this edge creates a cycle
107    pub kind: EdgeKind,        // Visual/semantic kind of the edge
108}
109
110impl Edge {
111    pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
112        Self {
113            from: from.into(),
114            to: to.into(),
115            label: None,
116            is_back_edge: false,
117            kind: EdgeKind::Arrow,
118        }
119    }
120
121    pub fn with_label(
122        from: impl Into<String>,
123        to: impl Into<String>,
124        label: impl Into<String>,
125    ) -> Self {
126        Self {
127            from: from.into(),
128            to: to.into(),
129            label: Some(label.into()),
130            is_back_edge: false,
131            kind: EdgeKind::Arrow,
132        }
133    }
134}
135
136// ============================================================================
137// Subgraph Support
138// ============================================================================
139
140/// Rectangle for bounding boxes (used by subgraphs)
141#[derive(Debug, Clone, Default, PartialEq)]
142pub struct Rectangle {
143    pub x: usize,
144    pub y: usize,
145    pub width: usize,
146    pub height: usize,
147}
148
149impl Rectangle {
150    /// Create a new rectangle
151    pub fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
152        Self {
153            x,
154            y,
155            width,
156            height,
157        }
158    }
159
160    /// Check if this rectangle contains a point
161    #[inline]
162    pub fn contains(&self, x: usize, y: usize) -> bool {
163        x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
164    }
165
166    /// Check if this rectangle is valid (non-zero dimensions)
167    #[inline]
168    pub fn is_valid(&self) -> bool {
169        self.width > 0 && self.height > 0
170    }
171}
172
173/// Rendered subgraph title text.
174pub fn subgraph_title_text(title: &str) -> String {
175    format!(" {title} ")
176}
177
178/// Display width of the rendered subgraph title token.
179pub fn subgraph_title_len(title: &str) -> usize {
180    subgraph_title_text(title).chars().count()
181}
182
183/// Interior row that carries the subgraph title for the given orientation.
184pub fn subgraph_title_row(top_y: usize, height: usize, direction: Direction) -> usize {
185    if matches!(direction, Direction::BT) {
186        top_y + height.saturating_sub(2)
187    } else {
188        top_y.saturating_add(1)
189    }
190}
191
192/// Horizontal title origin inside a subgraph container for the given orientation.
193///
194/// Titles are anchored to the leading edge of the subgraph based on direction:
195/// TD/TB/LR anchor left, RL anchors right, and BT anchors bottom-left.
196pub fn subgraph_title_start_x(
197    left_x: usize,
198    width: usize,
199    title: &str,
200    direction: Direction,
201) -> Option<usize> {
202    let len = subgraph_title_len(title);
203    if len == 0 || len > width.saturating_sub(4) {
204        return None;
205    }
206
207    Some(match direction {
208        Direction::RL => left_x + width.saturating_sub(len + 2),
209        Direction::TD | Direction::TB | Direction::LR | Direction::BT => left_x.saturating_add(2),
210    })
211}
212
213/// Inclusive x-span of the rendered title token inside the subgraph container.
214pub fn subgraph_title_span(
215    left_x: usize,
216    width: usize,
217    title: &str,
218    direction: Direction,
219) -> Option<(usize, usize)> {
220    let start = subgraph_title_start_x(left_x, width, title, direction)?;
221    let end = start + subgraph_title_len(title).saturating_sub(1);
222    Some((start, end))
223}
224
225/// Subgraph grouping nodes together.
226///
227/// Subgraphs provide visual grouping of related nodes with:
228/// - Dashed border to distinguish from node boxes
229/// - Optional title displayed at the top
230/// - Automatic bounds calculation from contained nodes
231#[derive(Debug, Clone)]
232pub struct Subgraph {
233    /// Unique identifier for the subgraph
234    pub id: String,
235    /// Optional title shown in the subgraph border
236    pub title: Option<String>,
237    /// Parent subgraph ID when this subgraph is nested.
238    pub parent_id: Option<String>,
239    /// Child subgraph IDs in declaration order.
240    pub child_ids: Vec<String>,
241    /// Set of node IDs contained in this subgraph
242    pub node_ids: HashSet<String>,
243    /// Bounding box calculated during layout
244    pub bounds: Rectangle,
245    /// Inner bounds (content box; excludes gutters/padding)
246    pub inner_bounds: Rectangle,
247    /// Min/max rank of contained nodes (for layout ordering)
248    pub rank_range: (usize, usize),
249}
250
251impl Subgraph {
252    /// Create a new subgraph with optional title
253    pub fn new(id: impl Into<String>, title: Option<String>) -> Self {
254        Self {
255            id: id.into(),
256            title,
257            parent_id: None,
258            child_ids: Vec::new(),
259            node_ids: HashSet::new(),
260            bounds: Rectangle::default(),
261            inner_bounds: Rectangle::default(),
262            rank_range: (0, 0),
263        }
264    }
265
266    /// Check if this subgraph contains a node
267    #[inline]
268    pub fn contains_node(&self, node_id: &str) -> bool {
269        self.node_ids.contains(node_id)
270    }
271
272    /// Add a node to this subgraph
273    pub fn add_node(&mut self, node_id: impl Into<String>) {
274        self.node_ids.insert(node_id.into());
275    }
276
277    /// Add a child subgraph to this subgraph, preserving declaration order.
278    pub fn add_child(&mut self, child_id: impl Into<String>) {
279        let child_id = child_id.into();
280        if !self.child_ids.iter().any(|existing| existing == &child_id) {
281            self.child_ids.push(child_id);
282        }
283    }
284
285    /// Check if the subgraph has a title
286    #[inline]
287    pub fn has_title(&self) -> bool {
288        self.title.is_some()
289    }
290
291    /// Check if this subgraph has a parent.
292    #[inline]
293    pub fn has_parent(&self) -> bool {
294        self.parent_id.is_some()
295    }
296
297    /// Check if this subgraph has nested child subgraphs.
298    #[inline]
299    pub fn has_children(&self) -> bool {
300        !self.child_ids.is_empty()
301    }
302}
303
304/// Complete graph with nodes and edges
305#[derive(Debug, Clone, Default)]
306pub struct Graph {
307    pub nodes: Vec<Node>,
308    pub edges: Vec<Edge>,
309    pub direction: Direction,
310    pub warnings: Vec<String>,
311    /// Subgraphs for visual grouping. Nested parent/child structure may be present
312    /// even when later layout/render phases do not yet fully exploit it.
313    pub subgraphs: Vec<Subgraph>,
314    /// Maps node ID to its containing subgraph ID (if any)
315    pub node_subgraph: HashMap<String, String>,
316    /// Optional precomputed routes (kept for legacy/experimental spikes; the
317    /// main pipeline uses the deterministic waterfall layout + live routing)
318    pub edge_routes: HashMap<usize, EdgeRoute>,
319}
320
321/// Graph direction (from Mermaid `graph TD/LR/TB/BT`)
322#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
323pub enum Direction {
324    #[default]
325    TD, // Top-down (same as TB)
326    #[allow(dead_code)]
327    TB, // Top to bottom
328    LR, // Left to right
329    RL, // Right to left
330    BT, // Bottom to top
331}
332
333impl Graph {
334    pub fn new() -> Self {
335        Self::default()
336    }
337
338    pub fn get_node(&self, id: &str) -> Option<&Node> {
339        self.nodes.iter().find(|n| n.id == id)
340    }
341
342    pub fn get_node_mut(&mut self, id: &str) -> Option<&mut Node> {
343        self.nodes.iter_mut().find(|n| n.id == id)
344    }
345
346    pub fn has_cycles(&self) -> bool {
347        self.edges.iter().any(|e| e.is_back_edge)
348    }
349
350    pub fn add_node(&mut self, node: Node) {
351        if self.get_node(&node.id).is_none() {
352            self.nodes.push(node);
353        }
354    }
355
356    pub fn add_edge(&mut self, edge: Edge) {
357        self.edges.push(edge);
358    }
359
360    pub fn add_warning(&mut self, warning: String) {
361        self.warnings.push(warning);
362    }
363
364    // ========================================================================
365    // Subgraph Methods
366    // ========================================================================
367
368    /// Add a subgraph to the graph
369    pub fn add_subgraph(&mut self, subgraph: Subgraph) {
370        if self.get_subgraph(&subgraph.id).is_none() {
371            self.subgraphs.push(subgraph);
372        }
373    }
374
375    /// Get a subgraph by ID
376    pub fn get_subgraph(&self, id: &str) -> Option<&Subgraph> {
377        self.subgraphs.iter().find(|s| s.id == id)
378    }
379
380    /// Get a mutable reference to a subgraph by ID
381    pub fn get_subgraph_mut(&mut self, id: &str) -> Option<&mut Subgraph> {
382        self.subgraphs.iter_mut().find(|s| s.id == id)
383    }
384
385    /// Associate a node with a subgraph (tracks membership)
386    pub fn associate_node_with_subgraph(&mut self, node_id: &str, subgraph_id: &str) {
387        if let Some(previous_id) = self
388            .node_subgraph
389            .insert(node_id.to_string(), subgraph_id.to_string())
390            .filter(|previous_id| previous_id != subgraph_id)
391        {
392            if let Some(previous_subgraph) = self.get_subgraph_mut(&previous_id) {
393                previous_subgraph.node_ids.remove(node_id);
394            }
395        }
396        if let Some(subgraph) = self.get_subgraph_mut(subgraph_id) {
397            subgraph.add_node(node_id);
398        }
399    }
400
401    /// Get the subgraph containing a node (if any)
402    pub fn get_node_subgraph(&self, node_id: &str) -> Option<&str> {
403        self.node_subgraph.get(node_id).map(|s| s.as_str())
404    }
405
406    /// Return the node's subgraph ancestry from innermost to outermost.
407    pub fn node_subgraph_chain<'a>(&'a self, node_id: &str) -> Vec<&'a str> {
408        let mut chain = Vec::new();
409        let mut current = self.get_node_subgraph(node_id);
410        while let Some(current_id) = current {
411            chain.push(current_id);
412            current = self
413                .get_subgraph(current_id)
414                .and_then(|subgraph| subgraph.parent_id.as_deref());
415        }
416        chain
417    }
418
419    /// Return true when `ancestor_id` is a declared ancestor of `descendant_id`.
420    pub fn is_subgraph_ancestor(&self, ancestor_id: &str, descendant_id: &str) -> bool {
421        let mut current = self
422            .get_subgraph(descendant_id)
423            .and_then(|subgraph| subgraph.parent_id.as_deref());
424        while let Some(parent_id) = current {
425            if parent_id == ancestor_id {
426                return true;
427            }
428            current = self
429                .get_subgraph(parent_id)
430                .and_then(|subgraph| subgraph.parent_id.as_deref());
431        }
432        false
433    }
434
435    /// Return the subgraph borders an edge exits and enters.
436    ///
437    /// Each vector is ordered from innermost to outermost exclusive boundary,
438    /// stopping at the nearest common ancestor shared by the endpoints.
439    pub fn edge_boundary_crossings<'a>(
440        &'a self,
441        from_node_id: &str,
442        to_node_id: &str,
443    ) -> (Vec<&'a str>, Vec<&'a str>) {
444        let from_chain = self.node_subgraph_chain(from_node_id);
445        let to_chain = self.node_subgraph_chain(to_node_id);
446
447        let mut from_exclusive_len = from_chain.len();
448        let mut to_exclusive_len = to_chain.len();
449        while from_exclusive_len > 0
450            && to_exclusive_len > 0
451            && from_chain[from_exclusive_len - 1] == to_chain[to_exclusive_len - 1]
452        {
453            from_exclusive_len -= 1;
454            to_exclusive_len -= 1;
455        }
456
457        (
458            from_chain[..from_exclusive_len].to_vec(),
459            to_chain[..to_exclusive_len].to_vec(),
460        )
461    }
462
463    /// Check whether an edge crosses any subgraph boundary.
464    pub fn edge_crosses_subgraph_boundary(&self, from_node_id: &str, to_node_id: &str) -> bool {
465        let (exit_subgraphs, enter_subgraphs) =
466            self.edge_boundary_crossings(from_node_id, to_node_id);
467        !exit_subgraphs.is_empty() || !enter_subgraphs.is_empty()
468    }
469
470    /// Check whether a node belongs to a subgraph directly or through one of its
471    /// nested descendants.
472    pub fn is_node_in_subgraph_tree(&self, node_id: &str, subgraph_id: &str) -> bool {
473        self.node_subgraph_chain(node_id).contains(&subgraph_id)
474    }
475
476    /// Check if the graph has any subgraphs
477    #[inline]
478    pub fn has_subgraphs(&self) -> bool {
479        !self.subgraphs.is_empty()
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    // =========================================================================
488    // Node
489    // =========================================================================
490
491    #[test]
492    fn node_new_defaults() {
493        let n = Node::new("id", "label");
494        assert_eq!(n.id, "id");
495        assert_eq!(n.label, "label");
496        assert_eq!(n.shape, NodeShape::Rectangle);
497        assert_eq!(n.x, 0);
498        assert_eq!(n.y, 0);
499        assert_eq!(n.rank, 0);
500        assert_eq!(n.height, crate::style::BOX_HEIGHT);
501        assert!(n.label_lines.is_empty());
502        assert!(n.click_target.is_none());
503    }
504
505    #[test]
506    fn node_with_shape_stores_shape() {
507        let shapes = [
508            NodeShape::Diamond,
509            NodeShape::Circle,
510            NodeShape::Stadium,
511            NodeShape::Hexagon,
512            NodeShape::Database,
513            NodeShape::Subroutine,
514            NodeShape::DoubleCircle,
515            NodeShape::Asymmetric,
516            NodeShape::Parallelogram,
517            NodeShape::ParallelogramAlt,
518            NodeShape::Trapezoid,
519            NodeShape::TrapezoidAlt,
520        ];
521        for shape in shapes {
522            let n = Node::with_shape("x", "label", shape);
523            assert_eq!(n.shape, shape, "shape variant {shape:?} not stored");
524        }
525    }
526
527    #[test]
528    fn node_center_x_even_width() {
529        let mut n = Node::new("a", "");
530        n.x = 10;
531        n.width = 20;
532        assert_eq!(n.center_x(), 20); // 10 + 20/2
533    }
534
535    #[test]
536    fn node_center_x_odd_width() {
537        let mut n = Node::new("a", "");
538        n.x = 0;
539        n.width = 11;
540        assert_eq!(n.center_x(), 5); // 0 + 11/2 (integer)
541    }
542
543    #[test]
544    fn node_center_y_uses_height_max_box_height() {
545        let bh = crate::style::BOX_HEIGHT;
546        let mut n = Node::new("a", "");
547        n.y = 10;
548
549        // height < BOX_HEIGHT → uses BOX_HEIGHT
550        n.height = bh.saturating_sub(1).max(1);
551        assert_eq!(n.center_y(), 10 + bh / 2);
552
553        // height > BOX_HEIGHT → uses height
554        n.height = bh + 4;
555        assert_eq!(n.center_y(), 10 + (bh + 4) / 2);
556
557        // height == BOX_HEIGHT
558        n.height = bh;
559        assert_eq!(n.center_y(), 10 + bh / 2);
560    }
561
562    #[test]
563    fn node_bottom_y_enforces_min_height() {
564        let bh = crate::style::BOX_HEIGHT;
565        let mut n = Node::new("a", "");
566        n.y = 5;
567
568        // height < BOX_HEIGHT → bottom_y uses BOX_HEIGHT
569        n.height = 1;
570        assert_eq!(n.bottom_y(), 5 + bh);
571
572        // height > BOX_HEIGHT → bottom_y uses height
573        n.height = bh + 2;
574        assert_eq!(n.bottom_y(), 5 + bh + 2);
575    }
576
577    // =========================================================================
578    // Edge
579    // =========================================================================
580
581    #[test]
582    fn edge_new_defaults() {
583        let e = Edge::new("a", "b");
584        assert_eq!(e.from, "a");
585        assert_eq!(e.to, "b");
586        assert!(e.label.is_none());
587        assert!(!e.is_back_edge);
588        assert_eq!(e.kind, EdgeKind::Arrow);
589    }
590
591    #[test]
592    fn edge_with_label_stores_label() {
593        let e = Edge::with_label("x", "y", "hello");
594        assert_eq!(e.label, Some("hello".to_string()));
595        assert_eq!(e.from, "x");
596        assert_eq!(e.to, "y");
597        assert!(!e.is_back_edge);
598        assert_eq!(e.kind, EdgeKind::Arrow);
599    }
600
601    #[test]
602    fn edge_kind_default_is_arrow() {
603        assert_eq!(EdgeKind::default(), EdgeKind::Arrow);
604    }
605
606    // =========================================================================
607    // Rectangle
608    // =========================================================================
609
610    #[test]
611    fn rectangle_contains_inclusive_corners() {
612        let r = Rectangle::new(5, 10, 4, 3); // x=5..8, y=10..12
613        assert!(r.contains(5, 10)); // top-left
614        assert!(r.contains(8, 12)); // bottom-right (x+w-1, y+h-1)
615        assert!(!r.contains(9, 12)); // one past right
616        assert!(!r.contains(5, 13)); // one past bottom
617        assert!(!r.contains(4, 10)); // one before left
618        assert!(!r.contains(5, 9)); // one above top
619        assert!(r.contains(7, 11)); // interior
620    }
621
622    #[test]
623    fn rectangle_contains_zero_dimensions() {
624        // Zero width: nothing inside
625        let r = Rectangle::new(5, 5, 0, 5);
626        assert!(!r.contains(5, 5));
627
628        // Zero height: nothing inside
629        let r = Rectangle::new(5, 5, 5, 0);
630        assert!(!r.contains(5, 5));
631    }
632
633    #[test]
634    fn rectangle_is_valid() {
635        assert!(Rectangle::new(0, 0, 1, 1).is_valid());
636        assert!(Rectangle::new(5, 5, 10, 10).is_valid());
637        assert!(!Rectangle::new(0, 0, 0, 5).is_valid());
638        assert!(!Rectangle::new(0, 0, 5, 0).is_valid());
639        assert!(!Rectangle::new(0, 0, 0, 0).is_valid());
640    }
641
642    // =========================================================================
643    // Subgraph
644    // =========================================================================
645
646    #[test]
647    fn subgraph_new_empty() {
648        let sg = Subgraph::new("sg1", Some("My Group".to_string()));
649        assert_eq!(sg.id, "sg1");
650        assert_eq!(sg.title, Some("My Group".to_string()));
651        assert!(sg.parent_id.is_none());
652        assert!(sg.child_ids.is_empty());
653        assert!(sg.node_ids.is_empty());
654        assert!(!sg.bounds.is_valid());
655        assert_eq!(sg.rank_range, (0, 0));
656    }
657
658    #[test]
659    fn subgraph_no_title() {
660        let sg = Subgraph::new("sg", None);
661        assert!(!sg.has_title());
662        assert!(sg.title.is_none());
663    }
664
665    #[test]
666    fn subgraph_has_title() {
667        let sg = Subgraph::new("sg", Some("Title".to_string()));
668        assert!(sg.has_title());
669    }
670
671    #[test]
672    fn subgraph_tracks_children_without_duplicates() {
673        let mut sg = Subgraph::new("parent", None);
674        assert!(!sg.has_children());
675        assert!(!sg.has_parent());
676
677        sg.add_child("child");
678        sg.add_child("child");
679
680        assert!(sg.has_children());
681        assert_eq!(sg.child_ids, vec!["child".to_string()]);
682    }
683
684    #[test]
685    fn subgraph_add_and_contains_node() {
686        let mut sg = Subgraph::new("sg", None);
687        assert!(!sg.contains_node("n1"));
688        sg.add_node("n1");
689        assert!(sg.contains_node("n1"));
690        assert!(!sg.contains_node("n2"));
691
692        // Adding same node twice is idempotent (HashSet)
693        sg.add_node("n1");
694        assert_eq!(sg.node_ids.len(), 1);
695    }
696
697    #[test]
698    fn subgraph_contains_node_is_case_sensitive() {
699        let mut sg = Subgraph::new("sg", None);
700        sg.add_node("Node");
701        assert!(sg.contains_node("Node"));
702        assert!(!sg.contains_node("node"));
703    }
704
705    // =========================================================================
706    // Graph
707    // =========================================================================
708
709    #[test]
710    fn graph_new_is_empty() {
711        let g = Graph::new();
712        assert!(g.nodes.is_empty());
713        assert!(g.edges.is_empty());
714        assert!(g.warnings.is_empty());
715        assert!(g.subgraphs.is_empty());
716        assert!(!g.has_subgraphs());
717        assert!(!g.has_cycles());
718        assert_eq!(g.direction, Direction::TD);
719    }
720
721    #[test]
722    fn graph_add_node_and_get() {
723        let mut g = Graph::new();
724        g.add_node(Node::new("a", "Alpha"));
725        assert_eq!(g.nodes.len(), 1);
726        assert!(g.get_node("a").is_some());
727        assert_eq!(
728            g.get_node("a").expect("node 'a' was just added").label,
729            "Alpha"
730        );
731        assert!(g.get_node("b").is_none());
732    }
733
734    #[test]
735    fn graph_add_node_deduplicates_by_id() {
736        let mut g = Graph::new();
737        g.add_node(Node::new("a", "first"));
738        g.add_node(Node::new("a", "second")); // duplicate — should be skipped
739        assert_eq!(g.nodes.len(), 1);
740        assert_eq!(
741            g.get_node("a").expect("node 'a' was added first").label,
742            "first"
743        );
744    }
745
746    #[test]
747    fn graph_add_edge_no_dedup() {
748        let mut g = Graph::new();
749        g.add_edge(Edge::new("a", "b"));
750        g.add_edge(Edge::new("a", "b")); // duplicate allowed
751        assert_eq!(g.edges.len(), 2);
752    }
753
754    #[test]
755    fn graph_add_warning() {
756        let mut g = Graph::new();
757        g.add_warning("warn1".to_string());
758        g.add_warning("warn2".to_string());
759        assert_eq!(g.warnings, vec!["warn1", "warn2"]);
760    }
761
762    #[test]
763    fn graph_has_cycles_reflects_back_edges() {
764        let mut g = Graph::new();
765        g.add_edge(Edge::new("a", "b"));
766        assert!(!g.has_cycles());
767
768        let mut back = Edge::new("b", "a");
769        back.is_back_edge = true;
770        g.add_edge(back);
771        assert!(g.has_cycles());
772    }
773
774    #[test]
775    fn graph_add_subgraph_and_get() {
776        let mut g = Graph::new();
777        g.add_subgraph(Subgraph::new("sg1", None));
778        assert!(g.has_subgraphs());
779        assert!(g.get_subgraph("sg1").is_some());
780        assert!(g.get_subgraph("sg2").is_none());
781    }
782
783    #[test]
784    fn graph_add_subgraph_deduplicates_by_id() {
785        let mut g = Graph::new();
786        g.add_subgraph(Subgraph::new("sg", Some("First".to_string())));
787        g.add_subgraph(Subgraph::new("sg", Some("Second".to_string())));
788        assert_eq!(g.subgraphs.len(), 1);
789        assert_eq!(
790            g.get_subgraph("sg")
791                .expect("subgraph 'sg' was just added")
792                .title,
793            Some("First".to_string())
794        );
795    }
796
797    #[test]
798    fn graph_associate_node_with_subgraph() {
799        let mut g = Graph::new();
800        g.add_subgraph(Subgraph::new("sg", None));
801        g.add_node(Node::new("n1", "Node 1"));
802
803        g.associate_node_with_subgraph("n1", "sg");
804
805        assert_eq!(g.get_node_subgraph("n1"), Some("sg"));
806        assert!(g
807            .get_subgraph("sg")
808            .expect("subgraph 'sg' was just added")
809            .contains_node("n1"));
810    }
811
812    #[test]
813    fn graph_associate_node_with_subgraph_reassigns_membership() {
814        let mut g = Graph::new();
815        g.add_subgraph(Subgraph::new("outer", None));
816        g.add_subgraph(Subgraph::new("inner", None));
817        g.add_node(Node::new("n1", "Node 1"));
818
819        g.associate_node_with_subgraph("n1", "outer");
820        g.associate_node_with_subgraph("n1", "inner");
821
822        assert_eq!(g.get_node_subgraph("n1"), Some("inner"));
823        assert!(!g
824            .get_subgraph("outer")
825            .expect("outer subgraph should exist")
826            .contains_node("n1"));
827        assert!(g
828            .get_subgraph("inner")
829            .expect("inner subgraph should exist")
830            .contains_node("n1"));
831    }
832
833    #[test]
834    fn graph_get_node_subgraph_returns_none_for_unassociated() {
835        let mut g = Graph::new();
836        g.add_node(Node::new("n1", "Node 1"));
837        assert!(g.get_node_subgraph("n1").is_none());
838        assert!(g.get_node_subgraph("nonexistent").is_none());
839    }
840
841    #[test]
842    fn graph_is_node_in_subgraph_tree_checks_ancestor_chain() {
843        let mut g = Graph::new();
844        g.add_subgraph(Subgraph::new("outer", None));
845        g.add_subgraph(Subgraph::new("inner", None));
846        g.get_subgraph_mut("inner").unwrap().parent_id = Some("outer".to_string());
847        g.get_subgraph_mut("outer").unwrap().add_child("inner");
848        g.add_node(Node::new("n1", "Node 1"));
849        g.associate_node_with_subgraph("n1", "inner");
850
851        assert!(g.is_node_in_subgraph_tree("n1", "inner"));
852        assert!(g.is_node_in_subgraph_tree("n1", "outer"));
853        assert!(!g.is_node_in_subgraph_tree("n1", "missing"));
854        assert!(!g.is_node_in_subgraph_tree("missing-node", "outer"));
855    }
856
857    #[test]
858    fn graph_node_subgraph_chain_orders_inner_to_outer() {
859        let mut g = Graph::new();
860        g.add_subgraph(Subgraph::new("outer", None));
861        g.add_subgraph(Subgraph::new("inner", None));
862        g.get_subgraph_mut("inner").unwrap().parent_id = Some("outer".to_string());
863        g.get_subgraph_mut("outer").unwrap().add_child("inner");
864        g.add_node(Node::new("n1", "Node 1"));
865        g.associate_node_with_subgraph("n1", "inner");
866
867        assert_eq!(g.node_subgraph_chain("n1"), vec!["inner", "outer"]);
868    }
869
870    #[test]
871    fn graph_edge_boundary_crossings_child_to_parent_exit_only_child() {
872        let mut g = Graph::new();
873        g.add_subgraph(Subgraph::new("parent", None));
874        g.add_subgraph(Subgraph::new("child", None));
875        g.get_subgraph_mut("child").unwrap().parent_id = Some("parent".to_string());
876        g.get_subgraph_mut("parent").unwrap().add_child("child");
877        g.add_node(Node::new("inner", "Inner"));
878        g.add_node(Node::new("outer", "Outer"));
879        g.associate_node_with_subgraph("inner", "child");
880        g.associate_node_with_subgraph("outer", "parent");
881
882        let (exits, enters) = g.edge_boundary_crossings("inner", "outer");
883        assert_eq!(exits, vec!["child"]);
884        assert!(enters.is_empty());
885    }
886
887    #[test]
888    fn graph_edge_boundary_crossings_between_siblings_skip_common_parent() {
889        let mut g = Graph::new();
890        g.add_subgraph(Subgraph::new("parent", None));
891        g.add_subgraph(Subgraph::new("left", None));
892        g.add_subgraph(Subgraph::new("right", None));
893        g.get_subgraph_mut("left").unwrap().parent_id = Some("parent".to_string());
894        g.get_subgraph_mut("right").unwrap().parent_id = Some("parent".to_string());
895        g.get_subgraph_mut("parent").unwrap().add_child("left");
896        g.get_subgraph_mut("parent").unwrap().add_child("right");
897        g.add_node(Node::new("a", "A"));
898        g.add_node(Node::new("b", "B"));
899        g.associate_node_with_subgraph("a", "left");
900        g.associate_node_with_subgraph("b", "right");
901
902        let (exits, enters) = g.edge_boundary_crossings("a", "b");
903        assert_eq!(exits, vec!["left"]);
904        assert_eq!(enters, vec!["right"]);
905    }
906
907    #[test]
908    fn graph_edge_boundary_crossings_external_to_nested_include_all_entered_ancestors() {
909        let mut g = Graph::new();
910        g.add_subgraph(Subgraph::new("parent", None));
911        g.add_subgraph(Subgraph::new("child", None));
912        g.get_subgraph_mut("child").unwrap().parent_id = Some("parent".to_string());
913        g.get_subgraph_mut("parent").unwrap().add_child("child");
914        g.add_node(Node::new("outside", "Outside"));
915        g.add_node(Node::new("inside", "Inside"));
916        g.associate_node_with_subgraph("inside", "child");
917
918        let (exits, enters) = g.edge_boundary_crossings("outside", "inside");
919        assert!(exits.is_empty());
920        assert_eq!(enters, vec!["child", "parent"]);
921        assert!(g.edge_crosses_subgraph_boundary("outside", "inside"));
922    }
923
924    #[test]
925    fn graph_is_subgraph_ancestor_checks_parent_chain() {
926        let mut g = Graph::new();
927        g.add_subgraph(Subgraph::new("outer", Some("Outer".into())));
928        g.add_subgraph(Subgraph::new("inner", Some("Inner".into())));
929        g.add_subgraph(Subgraph::new("leaf", Some("Leaf".into())));
930
931        g.get_subgraph_mut("inner").unwrap().parent_id = Some("outer".to_string());
932        g.get_subgraph_mut("leaf").unwrap().parent_id = Some("inner".to_string());
933
934        assert!(g.is_subgraph_ancestor("outer", "inner"));
935        assert!(g.is_subgraph_ancestor("outer", "leaf"));
936        assert!(g.is_subgraph_ancestor("inner", "leaf"));
937        assert!(!g.is_subgraph_ancestor("leaf", "inner"));
938        assert!(!g.is_subgraph_ancestor("inner", "outer"));
939    }
940
941    #[test]
942    fn graph_get_node_mut_allows_mutation() {
943        let mut g = Graph::new();
944        g.add_node(Node::new("a", "Original"));
945        if let Some(n) = g.get_node_mut("a") {
946            n.label = "Modified".to_string();
947        }
948        assert_eq!(
949            g.get_node("a").expect("node 'a' was just added").label,
950            "Modified"
951        );
952    }
953
954    #[test]
955    fn graph_get_subgraph_mut_allows_mutation() {
956        let mut g = Graph::new();
957        g.add_subgraph(Subgraph::new("sg", None));
958        if let Some(sg) = g.get_subgraph_mut("sg") {
959            sg.title = Some("New Title".to_string());
960        }
961        assert_eq!(
962            g.get_subgraph("sg")
963                .expect("subgraph 'sg' was just added")
964                .title,
965            Some("New Title".to_string())
966        );
967    }
968
969    #[test]
970    fn direction_default_is_td() {
971        assert_eq!(Direction::default(), Direction::TD);
972    }
973
974    #[test]
975    fn node_shape_default_is_rectangle() {
976        assert_eq!(NodeShape::default(), NodeShape::Rectangle);
977    }
978}