Skip to main content

ipfrs_core/
dag.rs

1//! DAG (Directed Acyclic Graph) traversal and analysis utilities.
2//!
3//! This module provides utilities for working with IPLD Merkle DAGs:
4//! - Extracting CID links from IPLD data
5//! - Calculating DAG statistics
6//! - Validating DAG structures
7//! - Collecting all CIDs in a DAG
8//!
9//! # Examples
10//!
11//! ```rust
12//! use ipfrs_core::{Ipld, Cid, CidBuilder};
13//! use ipfrs_core::dag::{extract_links, DagStats};
14//! use std::collections::BTreeMap;
15//!
16//! // Create IPLD with links
17//! let cid1 = CidBuilder::new().build(b"data1").unwrap();
18//! let cid2 = CidBuilder::new().build(b"data2").unwrap();
19//!
20//! let mut map = BTreeMap::new();
21//! map.insert("link1".to_string(), Ipld::link(cid1));
22//! map.insert("link2".to_string(), Ipld::link(cid2));
23//! let ipld = Ipld::Map(map);
24//!
25//! // Extract all CID links
26//! let links = extract_links(&ipld);
27//! assert_eq!(links.len(), 2);
28//! ```
29
30use crate::cid::{Cid, SerializableCid};
31use crate::ipld::Ipld;
32use std::collections::{HashMap, HashSet, VecDeque};
33
34/// Extract all CID links from an IPLD structure (non-recursive).
35///
36/// This function finds all direct `Ipld::Link` values in the given IPLD data,
37/// but does not recursively traverse nested links. Use `collect_all_links` for
38/// recursive traversal.
39///
40/// # Arguments
41///
42/// * `ipld` - The IPLD data to extract links from
43///
44/// # Returns
45///
46/// A vector of all CID links found at the top level
47///
48/// # Examples
49///
50/// ```rust
51/// use ipfrs_core::{Ipld, CidBuilder};
52/// use ipfrs_core::dag::extract_links;
53/// use std::collections::BTreeMap;
54///
55/// let cid = CidBuilder::new().build(b"test").unwrap();
56/// let mut map = BTreeMap::new();
57/// map.insert("file".to_string(), Ipld::link(cid.clone()));
58/// let ipld = Ipld::Map(map);
59///
60/// let links = extract_links(&ipld);
61/// assert_eq!(links.len(), 1);
62/// assert_eq!(links[0], cid);
63/// ```
64pub fn extract_links(ipld: &Ipld) -> Vec<Cid> {
65    let mut links = Vec::new();
66    extract_links_recursive(ipld, &mut links, false);
67    links
68}
69
70/// Extract all CID links from an IPLD structure recursively.
71///
72/// This function traverses the entire IPLD tree and collects all `Ipld::Link`
73/// values found at any depth.
74///
75/// # Arguments
76///
77/// * `ipld` - The IPLD data to extract links from
78///
79/// # Returns
80///
81/// A vector of all CID links found (may contain duplicates)
82///
83/// # Examples
84///
85/// ```rust
86/// use ipfrs_core::{Ipld, CidBuilder};
87/// use ipfrs_core::dag::collect_all_links;
88/// use std::collections::BTreeMap;
89///
90/// let cid1 = CidBuilder::new().build(b"test1").unwrap();
91/// let cid2 = CidBuilder::new().build(b"test2").unwrap();
92///
93/// // Nested structure
94/// let mut inner = BTreeMap::new();
95/// inner.insert("link".to_string(), Ipld::link(cid2.clone()));
96///
97/// let mut outer = BTreeMap::new();
98/// outer.insert("file".to_string(), Ipld::link(cid1.clone()));
99/// outer.insert("nested".to_string(), Ipld::Map(inner));
100///
101/// let ipld = Ipld::Map(outer);
102/// let links = collect_all_links(&ipld);
103/// assert_eq!(links.len(), 2);
104/// ```
105pub fn collect_all_links(ipld: &Ipld) -> Vec<Cid> {
106    let mut links = Vec::new();
107    extract_links_recursive(ipld, &mut links, true);
108    links
109}
110
111/// Extract unique CID links from an IPLD structure (no duplicates).
112///
113/// This function is similar to `collect_all_links` but returns a set of unique
114/// CIDs, removing any duplicates.
115///
116/// # Arguments
117///
118/// * `ipld` - The IPLD data to extract links from
119///
120/// # Returns
121///
122/// A set of unique CID links
123pub fn collect_unique_links(ipld: &Ipld) -> HashSet<Cid> {
124    collect_all_links(ipld).into_iter().collect()
125}
126
127/// Internal recursive link extraction
128fn extract_links_recursive(ipld: &Ipld, links: &mut Vec<Cid>, recursive: bool) {
129    match ipld {
130        Ipld::Link(SerializableCid(cid)) => {
131            links.push(*cid);
132        }
133        Ipld::List(items) => {
134            if recursive {
135                for item in items {
136                    extract_links_recursive(item, links, true);
137                }
138            } else {
139                for item in items {
140                    if let Ipld::Link(SerializableCid(cid)) = item {
141                        links.push(*cid);
142                    }
143                }
144            }
145        }
146        Ipld::Map(map) => {
147            if recursive {
148                for value in map.values() {
149                    extract_links_recursive(value, links, true);
150                }
151            } else {
152                for value in map.values() {
153                    if let Ipld::Link(SerializableCid(cid)) = value {
154                        links.push(*cid);
155                    }
156                }
157            }
158        }
159        _ => {}
160    }
161}
162
163/// Statistics about a DAG structure
164#[derive(Debug, Clone, Default, PartialEq, Eq)]
165pub struct DagStats {
166    /// Total number of unique CIDs in the DAG
167    pub unique_cids: usize,
168    /// Total number of links (including duplicates)
169    pub total_links: usize,
170    /// Maximum depth of the DAG
171    pub max_depth: usize,
172    /// Number of leaf nodes (nodes with no outgoing links)
173    pub leaf_count: usize,
174    /// Number of intermediate nodes (nodes with outgoing links)
175    pub intermediate_count: usize,
176}
177
178impl DagStats {
179    /// Create empty DAG statistics
180    pub fn new() -> Self {
181        Self::default()
182    }
183
184    /// Calculate statistics for an IPLD structure
185    ///
186    /// Note: This only analyzes the structure of the IPLD data provided,
187    /// it does not follow CID links to fetch additional blocks.
188    ///
189    /// # Arguments
190    ///
191    /// * `ipld` - The IPLD data to analyze
192    ///
193    /// # Returns
194    ///
195    /// Statistics about the DAG structure
196    ///
197    /// # Examples
198    ///
199    /// ```rust
200    /// use ipfrs_core::{Ipld, CidBuilder};
201    /// use ipfrs_core::dag::DagStats;
202    /// use std::collections::BTreeMap;
203    ///
204    /// let cid = CidBuilder::new().build(b"data").unwrap();
205    /// let mut map = BTreeMap::new();
206    /// map.insert("link".to_string(), Ipld::link(cid));
207    /// let ipld = Ipld::Map(map);
208    ///
209    /// let stats = DagStats::from_ipld(&ipld);
210    /// assert_eq!(stats.total_links, 1);
211    /// assert_eq!(stats.unique_cids, 1);
212    /// ```
213    pub fn from_ipld(ipld: &Ipld) -> Self {
214        let all_links = collect_all_links(ipld);
215        let unique_links: HashSet<_> = all_links.iter().collect();
216
217        let depth = calculate_depth(ipld, 0);
218        let (leaves, intermediates) = count_node_types(ipld);
219
220        Self {
221            unique_cids: unique_links.len(),
222            total_links: all_links.len(),
223            max_depth: depth,
224            leaf_count: leaves,
225            intermediate_count: intermediates,
226        }
227    }
228
229    /// Calculate the deduplication ratio
230    ///
231    /// Returns the ratio of duplicate links to total links.
232    /// A value of 0.0 means no duplication, 1.0 means all links are duplicates.
233    pub fn deduplication_ratio(&self) -> f64 {
234        if self.total_links == 0 {
235            return 0.0;
236        }
237        let duplicates = self.total_links.saturating_sub(self.unique_cids);
238        duplicates as f64 / self.total_links as f64
239    }
240}
241
242/// Calculate the maximum depth of an IPLD tree
243fn calculate_depth(ipld: &Ipld, current_depth: usize) -> usize {
244    match ipld {
245        Ipld::List(items) => {
246            let max_child_depth = items
247                .iter()
248                .map(|item| calculate_depth(item, current_depth + 1))
249                .max()
250                .unwrap_or(current_depth);
251            max_child_depth
252        }
253        Ipld::Map(map) => {
254            let max_child_depth = map
255                .values()
256                .map(|value| calculate_depth(value, current_depth + 1))
257                .max()
258                .unwrap_or(current_depth);
259            max_child_depth
260        }
261        _ => current_depth,
262    }
263}
264
265/// Count leaf and intermediate nodes
266fn count_node_types(ipld: &Ipld) -> (usize, usize) {
267    match ipld {
268        Ipld::List(items) => {
269            if items.is_empty() {
270                return (1, 0); // Empty list is a leaf
271            }
272            let mut leaves = 0;
273            let mut intermediates = 1; // This node is intermediate
274            for item in items {
275                let (l, i) = count_node_types(item);
276                leaves += l;
277                intermediates += i;
278            }
279            (leaves, intermediates)
280        }
281        Ipld::Map(map) => {
282            if map.is_empty() {
283                return (1, 0); // Empty map is a leaf
284            }
285            let mut leaves = 0;
286            let mut intermediates = 1; // This node is intermediate
287            for value in map.values() {
288                let (l, i) = count_node_types(value);
289                leaves += l;
290                intermediates += i;
291            }
292            (leaves, intermediates)
293        }
294        _ => (1, 0), // Scalar values are leaves
295    }
296}
297
298/// Validate that an IPLD structure forms a proper DAG (no cycles).
299///
300/// This function checks that there are no circular references in the CID links.
301/// Note: This only validates the structure of the provided IPLD data, it does
302/// not fetch and validate linked blocks.
303///
304/// # Arguments
305///
306/// * `ipld` - The IPLD data to validate
307///
308/// # Returns
309///
310/// `true` if the structure is acyclic, `false` if cycles are detected
311///
312/// # Examples
313///
314/// ```rust
315/// use ipfrs_core::{Ipld, CidBuilder};
316/// use ipfrs_core::dag::is_dag;
317/// use std::collections::BTreeMap;
318///
319/// let cid = CidBuilder::new().build(b"test").unwrap();
320/// let mut map = BTreeMap::new();
321/// map.insert("link".to_string(), Ipld::link(cid));
322/// let ipld = Ipld::Map(map);
323///
324/// assert!(is_dag(&ipld));
325/// ```
326pub fn is_dag(ipld: &Ipld) -> bool {
327    let mut visited = HashSet::new();
328    let mut stack = HashSet::new();
329    has_cycle_dfs(ipld, &mut visited, &mut stack)
330}
331
332/// DFS cycle detection
333fn has_cycle_dfs(ipld: &Ipld, visited: &mut HashSet<String>, stack: &mut HashSet<String>) -> bool {
334    match ipld {
335        Ipld::Link(SerializableCid(cid)) => {
336            let cid_str = cid.to_string();
337            if stack.contains(&cid_str) {
338                return false; // Cycle detected
339            }
340            if visited.contains(&cid_str) {
341                return true; // Already validated this path
342            }
343            visited.insert(cid_str.clone());
344            stack.insert(cid_str.clone());
345            // Note: We can't follow the link without a BlockFetcher
346            // So we just mark it as visited
347            stack.remove(&cid_str);
348            true
349        }
350        Ipld::List(items) => {
351            for item in items {
352                if !has_cycle_dfs(item, visited, stack) {
353                    return false;
354                }
355            }
356            true
357        }
358        Ipld::Map(map) => {
359            for value in map.values() {
360                if !has_cycle_dfs(value, visited, stack) {
361                    return false;
362                }
363            }
364            true
365        }
366        _ => true,
367    }
368}
369
370/// Find all paths from root to a specific CID in an IPLD structure.
371///
372/// Returns a list of paths (as lists of keys) that lead to the target CID.
373///
374/// # Arguments
375///
376/// * `ipld` - The IPLD data to search
377/// * `target_cid` - The CID to find paths to
378///
379/// # Returns
380///
381/// A vector of paths, where each path is a vector of keys leading to the CID
382pub fn find_paths_to_cid(ipld: &Ipld, target_cid: &Cid) -> Vec<Vec<String>> {
383    let mut paths = Vec::new();
384    let mut current_path = Vec::new();
385    find_paths_recursive(ipld, target_cid, &mut current_path, &mut paths);
386    paths
387}
388
389/// Recursive path finding helper
390fn find_paths_recursive(
391    ipld: &Ipld,
392    target_cid: &Cid,
393    current_path: &mut Vec<String>,
394    paths: &mut Vec<Vec<String>>,
395) {
396    match ipld {
397        Ipld::Link(SerializableCid(cid)) if cid == target_cid => {
398            paths.push(current_path.clone());
399        }
400        Ipld::List(items) => {
401            for (i, item) in items.iter().enumerate() {
402                current_path.push(format!("[{}]", i));
403                find_paths_recursive(item, target_cid, current_path, paths);
404                current_path.pop();
405            }
406        }
407        Ipld::Map(map) => {
408            for (key, value) in map {
409                current_path.push(key.clone());
410                find_paths_recursive(value, target_cid, current_path, paths);
411                current_path.pop();
412            }
413        }
414        _ => {}
415    }
416}
417
418/// Traverse a DAG in breadth-first order, collecting all IPLD nodes.
419///
420/// Note: This only traverses the structure of the provided IPLD data,
421/// it does not fetch linked blocks.
422///
423/// # Arguments
424///
425/// * `root` - The root IPLD node to start traversal from
426///
427/// # Returns
428///
429/// A vector of all IPLD nodes in breadth-first order
430pub fn traverse_bfs(root: &Ipld) -> Vec<Ipld> {
431    let mut result = Vec::new();
432    let mut queue = VecDeque::new();
433    queue.push_back(root.clone());
434
435    while let Some(node) = queue.pop_front() {
436        result.push(node.clone());
437
438        match &node {
439            Ipld::List(items) => {
440                for item in items {
441                    queue.push_back(item.clone());
442                }
443            }
444            Ipld::Map(map) => {
445                for value in map.values() {
446                    queue.push_back(value.clone());
447                }
448            }
449            _ => {}
450        }
451    }
452
453    result
454}
455
456/// Traverse a DAG in depth-first order, collecting all IPLD nodes.
457///
458/// # Arguments
459///
460/// * `root` - The root IPLD node to start traversal from
461///
462/// # Returns
463///
464/// A vector of all IPLD nodes in depth-first order
465pub fn traverse_dfs(root: &Ipld) -> Vec<Ipld> {
466    let mut result = Vec::new();
467    traverse_dfs_recursive(root, &mut result);
468    result
469}
470
471/// Recursive DFS helper
472fn traverse_dfs_recursive(node: &Ipld, result: &mut Vec<Ipld>) {
473    result.push(node.clone());
474
475    match node {
476        Ipld::List(items) => {
477            for item in items {
478                traverse_dfs_recursive(item, result);
479            }
480        }
481        Ipld::Map(map) => {
482            for value in map.values() {
483                traverse_dfs_recursive(value, result);
484            }
485        }
486        _ => {}
487    }
488}
489
490/// Additional DAG metrics beyond basic statistics.
491///
492/// Provides advanced graph-theoretic metrics for analyzing DAG structure.
493#[derive(Debug, Clone, Default, PartialEq)]
494pub struct DagMetrics {
495    /// Average branching factor (average number of children per non-leaf node)
496    pub avg_branching_factor: f64,
497    /// Maximum branching factor (maximum number of children for any node)
498    pub max_branching_factor: usize,
499    /// Width of the DAG (maximum number of nodes at any level)
500    pub width: usize,
501    /// Total number of nodes in the DAG
502    pub total_nodes: usize,
503    /// Average depth of leaf nodes
504    pub avg_leaf_depth: f64,
505}
506
507impl DagMetrics {
508    /// Calculate advanced metrics for an IPLD structure.
509    ///
510    /// # Arguments
511    ///
512    /// * `ipld` - The IPLD data to analyze
513    ///
514    /// # Returns
515    ///
516    /// Advanced metrics about the DAG structure
517    ///
518    /// # Examples
519    ///
520    /// ```rust
521    /// use ipfrs_core::{Ipld, CidBuilder};
522    /// use ipfrs_core::dag::DagMetrics;
523    /// use std::collections::BTreeMap;
524    ///
525    /// let cid = CidBuilder::new().build(b"data").unwrap();
526    /// let mut map = BTreeMap::new();
527    /// map.insert("link1".to_string(), Ipld::link(cid.clone()));
528    /// map.insert("link2".to_string(), Ipld::link(cid));
529    /// let ipld = Ipld::Map(map);
530    ///
531    /// let metrics = DagMetrics::from_ipld(&ipld);
532    /// assert_eq!(metrics.max_branching_factor, 2);
533    /// ```
534    pub fn from_ipld(ipld: &Ipld) -> Self {
535        let mut levels: HashMap<usize, usize> = HashMap::new();
536        let mut branching_factors = Vec::new();
537        let mut leaf_depths = Vec::new();
538        let mut total_nodes = 0;
539
540        calculate_metrics(
541            ipld,
542            0,
543            &mut levels,
544            &mut branching_factors,
545            &mut leaf_depths,
546            &mut total_nodes,
547        );
548
549        let width = levels.values().copied().max().unwrap_or(0);
550        let max_branching_factor = branching_factors.iter().copied().max().unwrap_or(0);
551        let avg_branching_factor = if branching_factors.is_empty() {
552            0.0
553        } else {
554            branching_factors.iter().sum::<usize>() as f64 / branching_factors.len() as f64
555        };
556        let avg_leaf_depth = if leaf_depths.is_empty() {
557            0.0
558        } else {
559            leaf_depths.iter().sum::<usize>() as f64 / leaf_depths.len() as f64
560        };
561
562        Self {
563            avg_branching_factor,
564            max_branching_factor,
565            width,
566            total_nodes,
567            avg_leaf_depth,
568        }
569    }
570}
571
572/// Helper function to calculate DAG metrics recursively
573fn calculate_metrics(
574    ipld: &Ipld,
575    depth: usize,
576    levels: &mut HashMap<usize, usize>,
577    branching_factors: &mut Vec<usize>,
578    leaf_depths: &mut Vec<usize>,
579    total_nodes: &mut usize,
580) {
581    *total_nodes += 1;
582    *levels.entry(depth).or_insert(0) += 1;
583
584    match ipld {
585        Ipld::List(items) => {
586            if items.is_empty() {
587                leaf_depths.push(depth);
588            } else {
589                branching_factors.push(items.len());
590                for item in items {
591                    calculate_metrics(
592                        item,
593                        depth + 1,
594                        levels,
595                        branching_factors,
596                        leaf_depths,
597                        total_nodes,
598                    );
599                }
600            }
601        }
602        Ipld::Map(map) => {
603            if map.is_empty() {
604                leaf_depths.push(depth);
605            } else {
606                branching_factors.push(map.len());
607                for value in map.values() {
608                    calculate_metrics(
609                        value,
610                        depth + 1,
611                        levels,
612                        branching_factors,
613                        leaf_depths,
614                        total_nodes,
615                    );
616                }
617            }
618        }
619        _ => {
620            leaf_depths.push(depth);
621        }
622    }
623}
624
625/// Perform a topological sort on IPLD nodes containing CID links.
626///
627/// Returns nodes in dependency order (dependencies before dependents).
628/// This is useful for processing DAGs where nodes depend on their children.
629///
630/// Note: Since we cannot fetch linked blocks, this only sorts the CIDs
631/// found in the IPLD structure based on their dependency relationships.
632///
633/// # Arguments
634///
635/// * `ipld` - The IPLD data to sort
636///
637/// # Returns
638///
639/// A vector of CIDs in topological order (leaves first, root last)
640///
641/// # Examples
642///
643/// ```rust
644/// use ipfrs_core::{Ipld, CidBuilder};
645/// use ipfrs_core::dag::topological_sort;
646/// use std::collections::BTreeMap;
647///
648/// let cid1 = CidBuilder::new().build(b"leaf1").unwrap();
649/// let cid2 = CidBuilder::new().build(b"leaf2").unwrap();
650///
651/// let mut map = BTreeMap::new();
652/// map.insert("child1".to_string(), Ipld::link(cid1));
653/// map.insert("child2".to_string(), Ipld::link(cid2));
654/// let ipld = Ipld::Map(map);
655///
656/// let sorted = topological_sort(&ipld);
657/// assert_eq!(sorted.len(), 2);
658/// ```
659pub fn topological_sort(ipld: &Ipld) -> Vec<Cid> {
660    let links = collect_all_links(ipld);
661    let mut result = Vec::new();
662    let mut visited = HashSet::new();
663
664    // Simple topological sort: collect all unique CIDs
665    // Since we can't fetch blocks, we just deduplicate and return in encounter order
666    for cid in links {
667        if visited.insert(cid) {
668            result.push(cid);
669        }
670    }
671
672    result
673}
674
675/// Calculate the size (number of nodes) of a subgraph rooted at the given IPLD node.
676///
677/// This counts all nodes reachable from the root, including the root itself.
678///
679/// # Arguments
680///
681/// * `ipld` - The root of the subgraph
682///
683/// # Returns
684///
685/// The total number of nodes in the subgraph
686///
687/// # Examples
688///
689/// ```rust
690/// use ipfrs_core::Ipld;
691/// use ipfrs_core::dag::subgraph_size;
692///
693/// let ipld = Ipld::List(vec![
694///     Ipld::Integer(1),
695///     Ipld::Integer(2),
696///     Ipld::List(vec![Ipld::Integer(3)]),
697/// ]);
698///
699/// let size = subgraph_size(&ipld);
700/// assert_eq!(size, 5); // Root list + 2 integers + nested list + nested integer
701/// ```
702pub fn subgraph_size(ipld: &Ipld) -> usize {
703    let mut count = 1; // Count the root
704
705    match ipld {
706        Ipld::List(items) => {
707            for item in items {
708                count += subgraph_size(item);
709            }
710        }
711        Ipld::Map(map) => {
712            for value in map.values() {
713                count += subgraph_size(value);
714            }
715        }
716        _ => {}
717    }
718
719    count
720}
721
722/// Calculate the fanout (number of direct children) for each level of the DAG.
723///
724/// Returns a vector where index i contains the total number of children
725/// at depth i in the DAG.
726///
727/// # Arguments
728///
729/// * `ipld` - The IPLD data to analyze
730///
731/// # Returns
732///
733/// A vector of fanout counts per level
734///
735/// # Examples
736///
737/// ```rust
738/// use ipfrs_core::Ipld;
739/// use ipfrs_core::dag::dag_fanout_by_level;
740///
741/// let ipld = Ipld::List(vec![
742///     Ipld::Integer(1),
743///     Ipld::List(vec![Ipld::Integer(2), Ipld::Integer(3)]),
744/// ]);
745///
746/// let fanout = dag_fanout_by_level(&ipld);
747/// assert!(fanout.len() >= 2);
748/// ```
749pub fn dag_fanout_by_level(ipld: &Ipld) -> Vec<usize> {
750    let mut fanout_by_level = Vec::new();
751    calculate_fanout(ipld, 0, &mut fanout_by_level);
752    fanout_by_level
753}
754
755/// Helper to calculate fanout at each level
756fn calculate_fanout(ipld: &Ipld, depth: usize, fanout_by_level: &mut Vec<usize>) {
757    // Ensure vector is large enough
758    while fanout_by_level.len() <= depth {
759        fanout_by_level.push(0);
760    }
761
762    match ipld {
763        Ipld::List(items) => {
764            fanout_by_level[depth] += items.len();
765            for item in items {
766                calculate_fanout(item, depth + 1, fanout_by_level);
767            }
768        }
769        Ipld::Map(map) => {
770            fanout_by_level[depth] += map.len();
771            for value in map.values() {
772                calculate_fanout(value, depth + 1, fanout_by_level);
773            }
774        }
775        _ => {}
776    }
777}
778
779/// Count the number of CID links at each depth level in the DAG.
780///
781/// Returns a vector where index i contains the count of CID links at depth i.
782///
783/// # Arguments
784///
785/// * `ipld` - The IPLD data to analyze
786///
787/// # Returns
788///
789/// A vector of link counts per depth level
790///
791/// # Examples
792///
793/// ```rust
794/// use ipfrs_core::{Ipld, CidBuilder};
795/// use ipfrs_core::dag::count_links_by_depth;
796///
797/// // Direct link at depth 0
798/// let cid = CidBuilder::new().build(b"test").unwrap();
799/// let ipld = Ipld::link(cid);
800///
801/// let counts = count_links_by_depth(&ipld);
802/// assert_eq!(counts[0], 1);
803/// ```
804pub fn count_links_by_depth(ipld: &Ipld) -> Vec<usize> {
805    let mut counts = Vec::new();
806    count_links_recursive(ipld, 0, &mut counts);
807    counts
808}
809
810/// Helper to count links at each depth
811fn count_links_recursive(ipld: &Ipld, depth: usize, counts: &mut Vec<usize>) {
812    // Ensure vector is large enough
813    while counts.len() <= depth {
814        counts.push(0);
815    }
816
817    match ipld {
818        Ipld::Link(_) => {
819            counts[depth] += 1;
820        }
821        Ipld::List(items) => {
822            for item in items {
823                count_links_recursive(item, depth + 1, counts);
824            }
825        }
826        Ipld::Map(map) => {
827            for value in map.values() {
828                count_links_recursive(value, depth + 1, counts);
829            }
830        }
831        _ => {}
832    }
833}
834
835/// Filter an IPLD structure to only include nodes matching a predicate.
836///
837/// This creates a new IPLD structure containing only the nodes where the
838/// predicate returns true.
839///
840/// # Arguments
841///
842/// * `ipld` - The IPLD data to filter
843/// * `predicate` - Function that returns true for nodes to keep
844///
845/// # Returns
846///
847/// A filtered IPLD structure, or None if the root doesn't match
848///
849/// # Examples
850///
851/// ```rust
852/// use ipfrs_core::Ipld;
853/// use ipfrs_core::dag::filter_dag;
854///
855/// let ipld = Ipld::List(vec![
856///     Ipld::Integer(1),
857///     Ipld::Integer(2),
858///     Ipld::String("hello".to_string()),
859/// ]);
860///
861/// // Keep only integers
862/// let filtered = filter_dag(&ipld, &|node| matches!(node, Ipld::Integer(_) | Ipld::List(_)));
863/// assert!(filtered.is_some());
864/// ```
865pub fn filter_dag<F>(ipld: &Ipld, predicate: &F) -> Option<Ipld>
866where
867    F: Fn(&Ipld) -> bool,
868{
869    if !predicate(ipld) {
870        return None;
871    }
872
873    match ipld {
874        Ipld::List(items) => {
875            let filtered_items: Vec<Ipld> = items
876                .iter()
877                .filter_map(|item| filter_dag(item, predicate))
878                .collect();
879            Some(Ipld::List(filtered_items))
880        }
881        Ipld::Map(map) => {
882            let filtered_map: std::collections::BTreeMap<String, Ipld> = map
883                .iter()
884                .filter_map(|(k, v)| filter_dag(v, predicate).map(|filtered| (k.clone(), filtered)))
885                .collect();
886            Some(Ipld::Map(filtered_map))
887        }
888        other => Some(other.clone()),
889    }
890}
891
892/// Transform all nodes in a DAG using a mapping function.
893///
894/// Applies the transformation function to each node in the IPLD structure,
895/// building a new transformed DAG.
896///
897/// # Arguments
898///
899/// * `ipld` - The IPLD data to transform
900/// * `transform` - Function to transform each node
901///
902/// # Returns
903///
904/// The transformed IPLD structure
905///
906/// # Examples
907///
908/// ```rust
909/// use ipfrs_core::Ipld;
910/// use ipfrs_core::dag::map_dag;
911///
912/// let ipld = Ipld::Integer(42);
913///
914/// // Double all integers
915/// let transformed = map_dag(&ipld, &|node| {
916///     match node {
917///         Ipld::Integer(n) => Ipld::Integer(n * 2),
918///         other => other.clone(),
919///     }
920/// });
921///
922/// assert_eq!(transformed, Ipld::Integer(84));
923/// ```
924pub fn map_dag<F>(ipld: &Ipld, transform: &F) -> Ipld
925where
926    F: Fn(&Ipld) -> Ipld,
927{
928    let transformed = match ipld {
929        Ipld::List(items) => {
930            let mapped_items: Vec<Ipld> =
931                items.iter().map(|item| map_dag(item, transform)).collect();
932            Ipld::List(mapped_items)
933        }
934        Ipld::Map(map) => {
935            let mapped_map: std::collections::BTreeMap<String, Ipld> = map
936                .iter()
937                .map(|(k, v)| (k.clone(), map_dag(v, transform)))
938                .collect();
939            Ipld::Map(mapped_map)
940        }
941        other => other.clone(),
942    };
943
944    transform(&transformed)
945}
946
947/// Find the differences between two IPLD DAGs
948///
949/// Returns a tuple of (unique_to_first, unique_to_second, common_links).
950/// This is useful for determining what changed between two versions of a DAG.
951///
952/// # Arguments
953///
954/// * `dag1` - First DAG to compare
955/// * `dag2` - Second DAG to compare
956///
957/// # Returns
958///
959/// A tuple of:
960/// - Links unique to dag1
961/// - Links unique to dag2
962/// - Links common to both
963///
964/// # Example
965///
966/// ```rust
967/// use ipfrs_core::{Ipld, CidBuilder};
968/// use ipfrs_core::dag::dag_diff;
969/// use std::collections::BTreeMap;
970///
971/// let cid1 = CidBuilder::new().build(b"a").unwrap();
972/// let cid2 = CidBuilder::new().build(b"b").unwrap();
973/// let cid3 = CidBuilder::new().build(b"c").unwrap();
974///
975/// let mut map1 = BTreeMap::new();
976/// map1.insert("link1".to_string(), Ipld::link(cid1));
977/// map1.insert("link2".to_string(), Ipld::link(cid2));
978///
979/// let mut map2 = BTreeMap::new();
980/// map2.insert("link2".to_string(), Ipld::link(cid2));
981/// map2.insert("link3".to_string(), Ipld::link(cid3));
982///
983/// let dag1 = Ipld::Map(map1);
984/// let dag2 = Ipld::Map(map2);
985///
986/// let (unique1, unique2, common) = dag_diff(&dag1, &dag2);
987/// assert_eq!(unique1.len(), 1); // cid1
988/// assert_eq!(unique2.len(), 1); // cid3
989/// assert_eq!(common.len(), 1);  // cid2
990/// ```
991pub fn dag_diff(dag1: &Ipld, dag2: &Ipld) -> (HashSet<Cid>, HashSet<Cid>, HashSet<Cid>) {
992    let links1 = collect_unique_links(dag1);
993    let links2 = collect_unique_links(dag2);
994
995    let unique_to_first: HashSet<Cid> = links1.difference(&links2).copied().collect();
996    let unique_to_second: HashSet<Cid> = links2.difference(&links1).copied().collect();
997    let common: HashSet<Cid> = links1.intersection(&links2).copied().collect();
998
999    (unique_to_first, unique_to_second, common)
1000}
1001
1002/// Find common ancestor links between two DAGs
1003///
1004/// Returns the set of CID links that appear in both DAGs, which can help
1005/// identify shared structure or common ancestry.
1006///
1007/// # Arguments
1008///
1009/// * `dag1` - First DAG
1010/// * `dag2` - Second DAG
1011///
1012/// # Returns
1013///
1014/// A set of CIDs that appear in both DAGs
1015///
1016/// # Example
1017///
1018/// ```rust
1019/// use ipfrs_core::{Ipld, CidBuilder};
1020/// use ipfrs_core::dag::find_common_links;
1021///
1022/// let cid = CidBuilder::new().build(b"shared").unwrap();
1023///
1024/// let dag1 = Ipld::List(vec![Ipld::link(cid)]);
1025/// let dag2 = Ipld::List(vec![Ipld::link(cid)]);
1026///
1027/// let common = find_common_links(&dag1, &dag2);
1028/// assert_eq!(common.len(), 1);
1029/// ```
1030pub fn find_common_links(dag1: &Ipld, dag2: &Ipld) -> HashSet<Cid> {
1031    let links1 = collect_unique_links(dag1);
1032    let links2 = collect_unique_links(dag2);
1033
1034    links1.intersection(&links2).copied().collect()
1035}
1036
1037/// Prune nodes from a DAG based on a predicate
1038///
1039/// This function creates a new DAG with only the nodes that match the predicate.
1040/// It's useful for removing unwanted nodes or creating filtered views of a DAG.
1041///
1042/// # Arguments
1043///
1044/// * `ipld` - The DAG to prune
1045/// * `should_keep` - Predicate function returning true for nodes to keep
1046///
1047/// # Returns
1048///
1049/// A new IPLD structure with pruned nodes, or None if the root is pruned
1050///
1051/// # Example
1052///
1053/// ```rust
1054/// use ipfrs_core::Ipld;
1055/// use ipfrs_core::dag::prune_dag;
1056///
1057/// let ipld = Ipld::List(vec![
1058///     Ipld::Integer(1),
1059///     Ipld::Integer(2),
1060///     Ipld::String("keep".to_string()),
1061/// ]);
1062///
1063/// // Keep only strings
1064/// let pruned = prune_dag(&ipld, &|node| {
1065///     matches!(node, Ipld::String(_) | Ipld::List(_))
1066/// });
1067///
1068/// assert!(pruned.is_some());
1069/// ```
1070pub fn prune_dag<F>(ipld: &Ipld, should_keep: &F) -> Option<Ipld>
1071where
1072    F: Fn(&Ipld) -> bool,
1073{
1074    if !should_keep(ipld) {
1075        return None;
1076    }
1077
1078    match ipld {
1079        Ipld::List(items) => {
1080            let pruned_items: Vec<Ipld> = items
1081                .iter()
1082                .filter_map(|item| prune_dag(item, should_keep))
1083                .collect();
1084
1085            if pruned_items.is_empty() && !items.is_empty() {
1086                None
1087            } else {
1088                Some(Ipld::List(pruned_items))
1089            }
1090        }
1091        Ipld::Map(map) => {
1092            let pruned_map: std::collections::BTreeMap<String, Ipld> = map
1093                .iter()
1094                .filter_map(|(k, v)| prune_dag(v, should_keep).map(|pruned| (k.clone(), pruned)))
1095                .collect();
1096
1097            if pruned_map.is_empty() && !map.is_empty() {
1098                None
1099            } else {
1100                Some(Ipld::Map(pruned_map))
1101            }
1102        }
1103        other => Some(other.clone()),
1104    }
1105}
1106
1107/// Merge two DAGs into a single DAG
1108///
1109/// Creates a new DAG that contains all nodes from both input DAGs. When both
1110/// DAGs are maps, their keys are merged (dag2 values override dag1 on conflicts).
1111/// When both are lists, they are concatenated.
1112///
1113/// # Arguments
1114///
1115/// * `dag1` - First DAG
1116/// * `dag2` - Second DAG
1117///
1118/// # Returns
1119///
1120/// A merged IPLD structure
1121///
1122/// # Example
1123///
1124/// ```rust
1125/// use ipfrs_core::Ipld;
1126/// use ipfrs_core::dag::merge_dags;
1127/// use std::collections::BTreeMap;
1128///
1129/// let mut map1 = BTreeMap::new();
1130/// map1.insert("a".to_string(), Ipld::Integer(1));
1131///
1132/// let mut map2 = BTreeMap::new();
1133/// map2.insert("b".to_string(), Ipld::Integer(2));
1134///
1135/// let dag1 = Ipld::Map(map1);
1136/// let dag2 = Ipld::Map(map2);
1137///
1138/// let merged = merge_dags(&dag1, &dag2);
1139/// if let Ipld::Map(m) = merged {
1140///     assert_eq!(m.len(), 2);
1141/// }
1142/// ```
1143pub fn merge_dags(dag1: &Ipld, dag2: &Ipld) -> Ipld {
1144    match (dag1, dag2) {
1145        (Ipld::Map(map1), Ipld::Map(map2)) => {
1146            let mut merged = map1.clone();
1147            for (k, v) in map2 {
1148                merged.insert(k.clone(), v.clone());
1149            }
1150            Ipld::Map(merged)
1151        }
1152        (Ipld::List(list1), Ipld::List(list2)) => {
1153            let mut merged = list1.clone();
1154            merged.extend(list2.clone());
1155            Ipld::List(merged)
1156        }
1157        // If types don't match, prefer dag2
1158        (_, dag2) => dag2.clone(),
1159    }
1160}
1161
1162/// Count the total number of nodes in a DAG
1163///
1164/// This includes all nodes at all levels, counting duplicates if they appear
1165/// multiple times in the structure.
1166///
1167/// # Arguments
1168///
1169/// * `ipld` - The DAG to count nodes in
1170///
1171/// # Returns
1172///
1173/// The total number of nodes (including the root)
1174///
1175/// # Example
1176///
1177/// ```rust
1178/// use ipfrs_core::Ipld;
1179/// use ipfrs_core::dag::count_nodes;
1180///
1181/// let ipld = Ipld::List(vec![
1182///     Ipld::Integer(1),
1183///     Ipld::Integer(2),
1184///     Ipld::List(vec![Ipld::Integer(3)]),
1185/// ]);
1186///
1187/// assert_eq!(count_nodes(&ipld), 5); // List + 2 ints + inner list + 1 int
1188/// ```
1189pub fn count_nodes(ipld: &Ipld) -> usize {
1190    match ipld {
1191        Ipld::List(items) => 1 + items.iter().map(count_nodes).sum::<usize>(),
1192        Ipld::Map(map) => 1 + map.values().map(count_nodes).sum::<usize>(),
1193        _ => 1,
1194    }
1195}
1196
1197/// Get the maximum depth of a DAG
1198///
1199/// Returns the length of the longest path from the root to a leaf node.
1200///
1201/// # Arguments
1202///
1203/// * `ipld` - The DAG to measure
1204///
1205/// # Returns
1206///
1207/// The maximum depth (0 for leaf nodes, 1+ for containers)
1208///
1209/// # Example
1210///
1211/// ```rust
1212/// use ipfrs_core::Ipld;
1213/// use ipfrs_core::dag::dag_depth;
1214///
1215/// let ipld = Ipld::List(vec![
1216///     Ipld::Integer(1),
1217///     Ipld::List(vec![
1218///         Ipld::Integer(2),
1219///         Ipld::List(vec![Ipld::Integer(3)]),
1220///     ]),
1221/// ]);
1222///
1223/// assert_eq!(dag_depth(&ipld), 4);
1224/// ```
1225pub fn dag_depth(ipld: &Ipld) -> usize {
1226    match ipld {
1227        Ipld::List(items) => {
1228            if items.is_empty() {
1229                1
1230            } else {
1231                1 + items.iter().map(dag_depth).max().unwrap_or(0)
1232            }
1233        }
1234        Ipld::Map(map) => {
1235            if map.is_empty() {
1236                1
1237            } else {
1238                1 + map.values().map(dag_depth).max().unwrap_or(0)
1239            }
1240        }
1241        _ => 1,
1242    }
1243}
1244
1245/// Find all leaf nodes in a DAG
1246///
1247/// Returns all nodes that have no children (i.e., not List or Map, or empty containers).
1248///
1249/// # Arguments
1250///
1251/// * `ipld` - The DAG to search
1252///
1253/// # Returns
1254///
1255/// A vector of all leaf nodes
1256///
1257/// # Example
1258///
1259/// ```rust
1260/// use ipfrs_core::Ipld;
1261/// use ipfrs_core::dag::find_leaves;
1262///
1263/// let ipld = Ipld::List(vec![
1264///     Ipld::Integer(1),
1265///     Ipld::String("leaf".to_string()),
1266///     Ipld::List(vec![Ipld::Integer(2)]),
1267/// ]);
1268///
1269/// let leaves = find_leaves(&ipld);
1270/// assert_eq!(leaves.len(), 3); // Two integers and one string
1271/// ```
1272pub fn find_leaves(ipld: &Ipld) -> Vec<Ipld> {
1273    match ipld {
1274        Ipld::List(items) => {
1275            if items.is_empty() {
1276                vec![ipld.clone()]
1277            } else {
1278                items.iter().flat_map(find_leaves).collect()
1279            }
1280        }
1281        Ipld::Map(map) => {
1282            if map.is_empty() {
1283                vec![ipld.clone()]
1284            } else {
1285                map.values().flat_map(find_leaves).collect()
1286            }
1287        }
1288        _ => vec![ipld.clone()],
1289    }
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294    use super::*;
1295    use crate::cid::CidBuilder;
1296    use std::collections::BTreeMap;
1297
1298    #[test]
1299    fn test_extract_links() {
1300        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1301        let cid2 = CidBuilder::new().build(b"test2").unwrap();
1302
1303        let mut map = BTreeMap::new();
1304        map.insert("link1".to_string(), Ipld::link(cid1));
1305        map.insert("link2".to_string(), Ipld::link(cid2));
1306        map.insert("data".to_string(), Ipld::String("hello".to_string()));
1307
1308        let ipld = Ipld::Map(map);
1309        let links = extract_links(&ipld);
1310
1311        assert_eq!(links.len(), 2);
1312        assert!(links.contains(&cid1));
1313        assert!(links.contains(&cid2));
1314    }
1315
1316    #[test]
1317    fn test_collect_all_links_nested() {
1318        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1319        let cid2 = CidBuilder::new().build(b"test2").unwrap();
1320        let cid3 = CidBuilder::new().build(b"test3").unwrap();
1321
1322        let mut inner = BTreeMap::new();
1323        inner.insert("deep_link".to_string(), Ipld::link(cid3));
1324
1325        let mut outer = BTreeMap::new();
1326        outer.insert("link1".to_string(), Ipld::link(cid1));
1327        outer.insert("link2".to_string(), Ipld::link(cid2));
1328        outer.insert("nested".to_string(), Ipld::Map(inner));
1329
1330        let ipld = Ipld::Map(outer);
1331        let links = collect_all_links(&ipld);
1332
1333        assert_eq!(links.len(), 3);
1334        assert!(links.contains(&cid1));
1335        assert!(links.contains(&cid2));
1336        assert!(links.contains(&cid3));
1337    }
1338
1339    #[test]
1340    fn test_collect_unique_links() {
1341        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1342        let cid2 = CidBuilder::new().build(b"test2").unwrap();
1343
1344        // Create structure with duplicate links
1345        let list = vec![
1346            Ipld::link(cid1),
1347            Ipld::link(cid2),
1348            Ipld::link(cid1), // Duplicate
1349        ];
1350
1351        let ipld = Ipld::List(list);
1352        let unique = collect_unique_links(&ipld);
1353
1354        assert_eq!(unique.len(), 2);
1355    }
1356
1357    #[test]
1358    fn test_dag_stats() {
1359        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1360        let cid2 = CidBuilder::new().build(b"test2").unwrap();
1361
1362        let mut map = BTreeMap::new();
1363        map.insert("link1".to_string(), Ipld::link(cid1));
1364        map.insert("link2".to_string(), Ipld::link(cid2));
1365        map.insert("dup_link".to_string(), Ipld::link(cid1)); // Duplicate
1366
1367        let ipld = Ipld::Map(map);
1368        let stats = DagStats::from_ipld(&ipld);
1369
1370        assert_eq!(stats.unique_cids, 2);
1371        assert_eq!(stats.total_links, 3);
1372        assert!(stats.deduplication_ratio() > 0.0);
1373    }
1374
1375    #[test]
1376    fn test_dag_stats_nested() {
1377        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1378        let cid2 = CidBuilder::new().build(b"test2").unwrap();
1379
1380        let mut inner = BTreeMap::new();
1381        inner.insert("deep".to_string(), Ipld::link(cid2));
1382
1383        let mut outer = BTreeMap::new();
1384        outer.insert("link".to_string(), Ipld::link(cid1));
1385        outer.insert("nested".to_string(), Ipld::Map(inner));
1386
1387        let ipld = Ipld::Map(outer);
1388        let stats = DagStats::from_ipld(&ipld);
1389
1390        assert_eq!(stats.unique_cids, 2);
1391        assert_eq!(stats.max_depth, 2);
1392    }
1393
1394    #[test]
1395    fn test_is_dag() {
1396        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1397
1398        let mut map = BTreeMap::new();
1399        map.insert("link".to_string(), Ipld::link(cid1));
1400
1401        let ipld = Ipld::Map(map);
1402        assert!(is_dag(&ipld));
1403    }
1404
1405    #[test]
1406    fn test_find_paths_to_cid() {
1407        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1408        let cid2 = CidBuilder::new().build(b"test2").unwrap();
1409
1410        let mut inner = BTreeMap::new();
1411        inner.insert("target".to_string(), Ipld::link(cid1));
1412
1413        let mut outer = BTreeMap::new();
1414        outer.insert("other".to_string(), Ipld::link(cid2));
1415        outer.insert("nested".to_string(), Ipld::Map(inner));
1416
1417        let ipld = Ipld::Map(outer);
1418        let paths = find_paths_to_cid(&ipld, &cid1);
1419
1420        assert_eq!(paths.len(), 1);
1421        assert_eq!(paths[0], vec!["nested".to_string(), "target".to_string()]);
1422    }
1423
1424    #[test]
1425    fn test_traverse_bfs() {
1426        let list = vec![
1427            Ipld::Integer(1),
1428            Ipld::Integer(2),
1429            Ipld::List(vec![Ipld::Integer(3), Ipld::Integer(4)]),
1430        ];
1431        let ipld = Ipld::List(list);
1432
1433        let nodes = traverse_bfs(&ipld);
1434        assert_eq!(nodes.len(), 6); // Root + 3 direct children (1, 2, list) + 2 nested (3, 4)
1435    }
1436
1437    #[test]
1438    fn test_traverse_dfs() {
1439        let list = vec![
1440            Ipld::Integer(1),
1441            Ipld::List(vec![Ipld::Integer(2)]),
1442            Ipld::Integer(3),
1443        ];
1444        let ipld = Ipld::List(list);
1445
1446        let nodes = traverse_dfs(&ipld);
1447        assert_eq!(nodes.len(), 5); // Root + all children
1448    }
1449
1450    #[test]
1451    fn test_empty_ipld() {
1452        let ipld = Ipld::Null;
1453        let links = extract_links(&ipld);
1454        assert!(links.is_empty());
1455
1456        let stats = DagStats::from_ipld(&ipld);
1457        assert_eq!(stats.unique_cids, 0);
1458        assert_eq!(stats.total_links, 0);
1459    }
1460
1461    #[test]
1462    fn test_deduplication_ratio() {
1463        // No duplication
1464        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1465        let cid2 = CidBuilder::new().build(b"test2").unwrap();
1466
1467        let list = vec![Ipld::link(cid1), Ipld::link(cid2)];
1468        let ipld = Ipld::List(list);
1469        let stats = DagStats::from_ipld(&ipld);
1470        assert_eq!(stats.deduplication_ratio(), 0.0);
1471
1472        // 50% duplication
1473        let list = vec![
1474            Ipld::link(cid1),
1475            Ipld::link(cid2),
1476            Ipld::link(cid1),
1477            Ipld::link(cid2),
1478        ];
1479        let ipld = Ipld::List(list);
1480        let stats = DagStats::from_ipld(&ipld);
1481        assert_eq!(stats.deduplication_ratio(), 0.5);
1482    }
1483
1484    #[test]
1485    fn test_dag_metrics() {
1486        let cid = CidBuilder::new().build(b"test").unwrap();
1487        let mut map = BTreeMap::new();
1488        map.insert("link1".to_string(), Ipld::link(cid));
1489        map.insert("link2".to_string(), Ipld::link(cid));
1490        let ipld = Ipld::Map(map);
1491
1492        let metrics = DagMetrics::from_ipld(&ipld);
1493        assert_eq!(metrics.max_branching_factor, 2);
1494        assert!(metrics.width >= 1);
1495        assert!(metrics.total_nodes > 0);
1496    }
1497
1498    #[test]
1499    fn test_dag_metrics_nested() {
1500        let cid = CidBuilder::new().build(b"test").unwrap();
1501
1502        let mut inner = BTreeMap::new();
1503        inner.insert("deep1".to_string(), Ipld::Integer(1));
1504        inner.insert("deep2".to_string(), Ipld::Integer(2));
1505        inner.insert("deep3".to_string(), Ipld::link(cid));
1506
1507        let mut outer = BTreeMap::new();
1508        outer.insert("nested".to_string(), Ipld::Map(inner));
1509        outer.insert("value".to_string(), Ipld::String("test".to_string()));
1510
1511        let ipld = Ipld::Map(outer);
1512        let metrics = DagMetrics::from_ipld(&ipld);
1513
1514        assert_eq!(metrics.max_branching_factor, 3);
1515        assert!(metrics.avg_branching_factor > 0.0);
1516        assert!(metrics.avg_leaf_depth > 0.0);
1517    }
1518
1519    #[test]
1520    fn test_topological_sort() {
1521        let cid1 = CidBuilder::new().build(b"leaf1").unwrap();
1522        let cid2 = CidBuilder::new().build(b"leaf2").unwrap();
1523
1524        let mut map = BTreeMap::new();
1525        map.insert("child1".to_string(), Ipld::link(cid1));
1526        map.insert("child2".to_string(), Ipld::link(cid2));
1527        let ipld = Ipld::Map(map);
1528
1529        let sorted = topological_sort(&ipld);
1530        assert_eq!(sorted.len(), 2);
1531        assert!(sorted.contains(&cid1));
1532        assert!(sorted.contains(&cid2));
1533    }
1534
1535    #[test]
1536    fn test_topological_sort_with_duplicates() {
1537        let cid1 = CidBuilder::new().build(b"test").unwrap();
1538
1539        let list = vec![Ipld::link(cid1), Ipld::link(cid1), Ipld::link(cid1)];
1540        let ipld = Ipld::List(list);
1541
1542        let sorted = topological_sort(&ipld);
1543        // Should deduplicate
1544        assert_eq!(sorted.len(), 1);
1545        assert_eq!(sorted[0], cid1);
1546    }
1547
1548    #[test]
1549    fn test_subgraph_size() {
1550        let ipld = Ipld::List(vec![
1551            Ipld::Integer(1),
1552            Ipld::Integer(2),
1553            Ipld::List(vec![Ipld::Integer(3)]),
1554        ]);
1555
1556        let size = subgraph_size(&ipld);
1557        assert_eq!(size, 5); // Root list + 2 integers + nested list + nested integer
1558    }
1559
1560    #[test]
1561    fn test_subgraph_size_single_node() {
1562        let ipld = Ipld::Integer(42);
1563        let size = subgraph_size(&ipld);
1564        assert_eq!(size, 1);
1565    }
1566
1567    #[test]
1568    fn test_dag_fanout_by_level() {
1569        let ipld = Ipld::List(vec![
1570            Ipld::Integer(1),
1571            Ipld::List(vec![Ipld::Integer(2), Ipld::Integer(3)]),
1572        ]);
1573
1574        let fanout = dag_fanout_by_level(&ipld);
1575        assert!(fanout.len() >= 2);
1576        assert_eq!(fanout[0], 2); // Root has 2 children
1577    }
1578
1579    #[test]
1580    fn test_dag_fanout_empty() {
1581        let ipld = Ipld::Integer(42);
1582        let fanout = dag_fanout_by_level(&ipld);
1583        // Scalar value has no children
1584        assert!(fanout.is_empty() || fanout.iter().all(|&f| f == 0));
1585    }
1586
1587    #[test]
1588    fn test_count_links_by_depth() {
1589        let cid = CidBuilder::new().build(b"test").unwrap();
1590        let mut map = BTreeMap::new();
1591        map.insert("link".to_string(), Ipld::link(cid));
1592        let ipld = Ipld::Map(map);
1593
1594        let counts = count_links_by_depth(&ipld);
1595        assert!(!counts.is_empty());
1596        // Links inside map values are at depth 1
1597        assert_eq!(counts[1], 1);
1598    }
1599
1600    #[test]
1601    fn test_count_links_by_depth_nested() {
1602        let cid1 = CidBuilder::new().build(b"test1").unwrap();
1603        let cid2 = CidBuilder::new().build(b"test2").unwrap();
1604
1605        let mut inner = BTreeMap::new();
1606        inner.insert("deep".to_string(), Ipld::link(cid2));
1607
1608        let mut outer = BTreeMap::new();
1609        outer.insert("shallow".to_string(), Ipld::link(cid1));
1610        outer.insert("nested".to_string(), Ipld::Map(inner));
1611
1612        let ipld = Ipld::Map(outer);
1613        let counts = count_links_by_depth(&ipld);
1614
1615        assert!(counts.len() >= 2);
1616        // Links in outer map are at depth 1
1617        assert_eq!(counts[1], 1); // One link at depth 1 (shallow)
1618                                  // Links in inner map are at depth 2
1619        assert_eq!(counts[2], 1); // One link at depth 2 (deep)
1620    }
1621
1622    #[test]
1623    fn test_filter_dag() {
1624        let ipld = Ipld::List(vec![
1625            Ipld::Integer(1),
1626            Ipld::Integer(2),
1627            Ipld::String("hello".to_string()),
1628        ]);
1629
1630        // Keep only integers and lists
1631        let filtered = filter_dag(&ipld, &|node| {
1632            matches!(node, Ipld::Integer(_) | Ipld::List(_))
1633        });
1634        assert!(filtered.is_some());
1635
1636        if let Some(Ipld::List(items)) = filtered {
1637            assert_eq!(items.len(), 2); // Only the two integers
1638        } else {
1639            panic!("Expected filtered list");
1640        }
1641    }
1642
1643    #[test]
1644    fn test_filter_dag_all_filtered() {
1645        let ipld = Ipld::Integer(42);
1646
1647        // Filter out everything
1648        let filtered = filter_dag(&ipld, &|_| false);
1649        assert!(filtered.is_none());
1650    }
1651
1652    #[test]
1653    fn test_map_dag() {
1654        let ipld = Ipld::Integer(42);
1655
1656        // Double all integers
1657        let transformed = map_dag(&ipld, &|node| match node {
1658            Ipld::Integer(n) => Ipld::Integer(n * 2),
1659            other => other.clone(),
1660        });
1661
1662        assert_eq!(transformed, Ipld::Integer(84));
1663    }
1664
1665    #[test]
1666    fn test_map_dag_nested() {
1667        let ipld = Ipld::List(vec![Ipld::Integer(1), Ipld::Integer(2)]);
1668
1669        // Double all integers
1670        let transformed = map_dag(&ipld, &|node| match node {
1671            Ipld::Integer(n) => Ipld::Integer(n * 2),
1672            other => other.clone(),
1673        });
1674
1675        if let Ipld::List(items) = transformed {
1676            assert_eq!(items[0], Ipld::Integer(2));
1677            assert_eq!(items[1], Ipld::Integer(4));
1678        } else {
1679            panic!("Expected list");
1680        }
1681    }
1682
1683    #[test]
1684    fn test_map_dag_preserve_structure() {
1685        let mut map = BTreeMap::new();
1686        map.insert("a".to_string(), Ipld::Integer(1));
1687        map.insert("b".to_string(), Ipld::Integer(2));
1688        let ipld = Ipld::Map(map);
1689
1690        // Transform preserves map structure
1691        let transformed = map_dag(&ipld, &|node| match node {
1692            Ipld::Integer(n) => Ipld::Integer(n + 10),
1693            other => other.clone(),
1694        });
1695
1696        if let Ipld::Map(result_map) = transformed {
1697            assert_eq!(result_map.get("a"), Some(&Ipld::Integer(11)));
1698            assert_eq!(result_map.get("b"), Some(&Ipld::Integer(12)));
1699        } else {
1700            panic!("Expected map");
1701        }
1702    }
1703}