Skip to main content

plushie_renderer_engine/
tree.rs

1//! Retained UI tree.
2//!
3//! [`Tree`] holds the current root [`TreeNode`] and supports full
4//! replacement via [`snapshot`](Tree::snapshot) and incremental updates
5//! via [`apply_patch`](Tree::apply_patch). The renderer reads the tree
6//! during `view()` to produce iced widgets; the host mutates it by
7//! sending Snapshot and Patch messages.
8
9use std::collections::{HashMap, HashSet};
10
11use plushie_core::protocol::{PatchOp, TreeNode};
12use plushie_widget_sdk::shared_state::MAX_TREE_DEPTH;
13
14/// Retained tree store. Holds the current root node (if any) and supports
15/// full replacement (snapshot) and incremental patch application.
16///
17/// Maintains an internal `id_index` mapping each node ID to the
18/// child-index path that addresses it from the root. The index is
19/// rebuilt on every snapshot and after every `apply_patch` call so
20/// `find_by_id` is O(path-depth) instead of O(tree-size). Empty IDs
21/// (legal for unaddressable nodes) are not indexed; under duplicate
22/// IDs the first depth-first occurrence wins, matching the recursive
23/// scan it replaces.
24#[derive(Debug, Default)]
25pub struct Tree {
26    root: Option<TreeNode>,
27    id_index: HashMap<String, Vec<usize>>,
28}
29
30impl Tree {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Replace the entire tree with a new root (snapshot).
36    ///
37    /// The tree is always accepted (the renderer needs UI even if the tree
38    /// has problems). Returns `Ok(())` if all node IDs are unique, or
39    /// `Err` with a list of `"id (type_name)"` strings for each duplicate.
40    /// The caller should emit a protocol error so the host can fix the bug.
41    pub fn snapshot(&mut self, root: TreeNode) -> Result<(), Vec<String>> {
42        self.root = Some(root);
43        self.rebuild_id_index();
44        // Validate after setting: the tree is accepted regardless, but
45        // duplicates are reported as errors.
46        if let Some(root) = self.root.as_ref() {
47            validate_unique_ids(root)
48        } else {
49            Ok(())
50        }
51    }
52
53    /// Return a reference to the current root, if any.
54    pub fn root(&self) -> Option<&TreeNode> {
55        self.root.as_ref()
56    }
57
58    /// Return a mutable reference to the current root, if any. Used
59    /// by transforms that drive the shared [`tree_walk`] walker, which
60    /// takes `&mut TreeNode` even for read-only passes.
61    ///
62    /// [`tree_walk`]: plushie_core::tree_walk
63    pub fn root_mut(&mut self) -> Option<&mut TreeNode> {
64        self.root.as_mut()
65    }
66
67    /// Find a window node by its window ID, searching the entire tree recursively.
68    pub fn find_window(&self, window_id: &str) -> Option<&TreeNode> {
69        let root = self.root.as_ref()?;
70        find_window_recursive(root, window_id, 0)
71    }
72
73    /// Collect the IDs of all window nodes in the tree (recursive search).
74    pub fn window_ids(&self) -> Vec<String> {
75        let Some(root) = self.root.as_ref() else {
76            return Vec::new();
77        };
78        let mut ids = Vec::new();
79        collect_window_ids_recursive(root, &mut ids, 0);
80        ids
81    }
82
83    /// Find a node by ID. Goes through the `id_index` for O(depth)
84    /// lookup instead of a full depth-first scan.
85    pub fn find_by_id(&self, node_id: &str) -> Option<&TreeNode> {
86        let root = self.root.as_ref()?;
87        let path = self.id_index.get(node_id)?;
88        navigate(root, path).ok()
89    }
90
91    /// Returns the type name of the node with the given ID, if found.
92    pub fn find_by_type(&self, node_id: &str) -> Option<&str> {
93        self.find_by_id(node_id).map(|n| n.type_name.as_str())
94    }
95
96    fn rebuild_id_index(&mut self) {
97        self.id_index.clear();
98        if let Some(root) = self.root.as_ref() {
99            let mut path = Vec::new();
100            collect_id_index(root, &mut path, &mut self.id_index, 0);
101        }
102    }
103
104    /// Validate protocol ordering for a sequence of patch operations.
105    ///
106    /// The wire protocol orders structural ops so indices stay meaningful:
107    /// removes first, updates/replacements next, inserts last. Removes and
108    /// inserts are ordered only within the same parent path.
109    pub fn validate_patch_order(ops: &[PatchOp]) -> Result<(), String> {
110        validate_patch_order(ops)
111    }
112
113    /// Apply a sequence of patch operations to the tree.
114    ///
115    /// The caller is responsible for validating protocol ordering before
116    /// applying renderer-owned patch sequences. Operations are applied
117    /// sequentially. If one operation fails, it is skipped with a warning
118    /// and subsequent operations are still applied. This preserves existing
119    /// best-effort handling for malformed or stale individual ops.
120    /// Applies patch operations and returns any removed nodes that had an
121    /// "exit" prop (for exit animation ghost promotion).
122    pub fn apply_patch(&mut self, ops: Vec<PatchOp>) -> Vec<(String, usize, TreeNode)> {
123        log::debug!("applying patch: {} ops", ops.len());
124        let mut exit_nodes = Vec::new();
125        for op in ops {
126            // Check for exit nodes before removal
127            if op.op == "remove_child"
128                && let Some(root) = self.root.as_ref()
129                && let Ok(parent) = navigate(root, &op.path)
130            {
131                let index = op
132                    .rest
133                    .get("index")
134                    .and_then(|v| v.as_u64())
135                    .unwrap_or(u64::MAX) as usize;
136                if index < parent.children.len() {
137                    let child = &parent.children[index];
138                    if child.props.get("exit").is_some() {
139                        exit_nodes.push((parent.id.clone(), index, child.clone()));
140                    }
141                }
142            }
143            if let Err(e) = self.apply_op(&op) {
144                if matches!(e, PatchApplyError::NoTree) {
145                    log::debug!("failed to apply patch op {:?}: {}", op.op, e);
146                } else {
147                    log::warn!("failed to apply patch op {:?}: {}", op.op, e);
148                }
149            }
150        }
151        // Index paths can shift under insert_child/remove_child and
152        // get rewritten under replace_node. Rebuilding once per
153        // apply_patch keeps incremental complexity off the inner
154        // loop and matches the snapshot path's index ownership.
155        self.rebuild_id_index();
156        exit_nodes
157    }
158
159    fn apply_op(&mut self, op: &PatchOp) -> Result<(), PatchApplyError> {
160        let root = self.root.as_mut().ok_or(PatchApplyError::NoTree)?;
161
162        match op.op.as_str() {
163            "replace_node" => {
164                let node = op.rest.get("node").ok_or_else(|| {
165                    PatchApplyError::invalid("replace_node: missing 'node' field")
166                })?;
167                let new_node: TreeNode = serde_json::from_value(node.clone()).map_err(|e| {
168                    PatchApplyError::invalid(format!("replace_node: invalid node: {e}"))
169                })?;
170
171                if op.path.is_empty() {
172                    // Replace root
173                    *root = new_node;
174                } else {
175                    let parent = navigate_mut(root, &op.path[..op.path.len() - 1])
176                        .map_err(PatchApplyError::invalid)?;
177                    let idx = *op.path.last().unwrap();
178                    if idx < parent.children.len() {
179                        parent.children[idx] = new_node;
180                    } else {
181                        return Err(PatchApplyError::invalid(format!(
182                            "replace_node: index {idx} out of bounds"
183                        )));
184                    }
185                }
186                Ok(())
187            }
188            "update_props" => {
189                let target = navigate_mut(root, &op.path).map_err(PatchApplyError::invalid)?;
190                let props = op.rest.get("props").ok_or_else(|| {
191                    PatchApplyError::invalid("update_props: missing 'props' field")
192                })?;
193
194                if !props.is_object() {
195                    log::warn!("update_props: patch props is not an object: {}", props);
196                    return Ok(());
197                }
198                let target_map = target.props.as_prop_map_mut();
199                let patch_map = props.as_object().unwrap();
200                for (k, v) in patch_map {
201                    if v.is_null() {
202                        target_map.remove(k);
203                    } else {
204                        target_map.insert(
205                            k.clone(),
206                            plushie_core::protocol::PropValue::from(v.clone()),
207                        );
208                    }
209                }
210                Ok(())
211            }
212            "insert_child" => {
213                let parent = navigate_mut(root, &op.path).map_err(PatchApplyError::invalid)?;
214                let index = op
215                    .rest
216                    .get("index")
217                    .and_then(|v| v.as_u64())
218                    .ok_or_else(|| {
219                        PatchApplyError::invalid("insert_child: missing or invalid 'index'")
220                    })? as usize;
221                let node = op.rest.get("node").ok_or_else(|| {
222                    PatchApplyError::invalid("insert_child: missing 'node' field")
223                })?;
224                let new_node: TreeNode = serde_json::from_value(node.clone()).map_err(|e| {
225                    PatchApplyError::invalid(format!("insert_child: invalid node: {e}"))
226                })?;
227
228                if index <= parent.children.len() {
229                    parent.children.insert(index, new_node);
230                } else {
231                    log::warn!(
232                        "insert_child: index {index} is beyond children length {}, appending instead",
233                        parent.children.len()
234                    );
235                    parent.children.push(new_node);
236                }
237                Ok(())
238            }
239            "remove_child" => {
240                let parent = navigate_mut(root, &op.path).map_err(PatchApplyError::invalid)?;
241                let index = op
242                    .rest
243                    .get("index")
244                    .and_then(|v| v.as_u64())
245                    .ok_or_else(|| {
246                        PatchApplyError::invalid("remove_child: missing or invalid 'index'")
247                    })? as usize;
248
249                if index < parent.children.len() {
250                    parent.children.remove(index);
251                    Ok(())
252                } else {
253                    Err(PatchApplyError::invalid(format!(
254                        "remove_child: index {index} out of bounds (len={})",
255                        parent.children.len()
256                    )))
257                }
258            }
259            other => {
260                plushie_core::diagnostics::error(plushie_core::Diagnostic::UnknownPatchOp {
261                    op: other.to_string(),
262                    payload: patch_op_payload(op),
263                });
264                Ok(())
265            }
266        }
267    }
268}
269
270#[derive(Debug, PartialEq, Eq)]
271enum PatchApplyError {
272    NoTree,
273    Invalid(String),
274}
275
276impl PatchApplyError {
277    fn invalid(message: impl Into<String>) -> Self {
278        Self::Invalid(message.into())
279    }
280}
281
282impl std::fmt::Display for PatchApplyError {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        match self {
285            Self::NoTree => f.write_str("no tree to patch"),
286            Self::Invalid(message) => f.write_str(message),
287        }
288    }
289}
290
291fn patch_op_payload(op: &PatchOp) -> serde_json::Value {
292    serde_json::to_value(op).unwrap_or_else(|_| {
293        serde_json::json!({
294            "op": op.op,
295            "path": op.path,
296            "rest": op.rest
297        })
298    })
299}
300
301#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
302enum PatchPhase {
303    Remove,
304    Middle,
305    Insert,
306}
307
308enum PatchOrderOp<'a> {
309    Remove { path: &'a [usize], index: usize },
310    Middle { parent_path: &'a [usize] },
311    Insert { path: &'a [usize], index: usize },
312}
313
314#[derive(Debug)]
315struct ParentPatchOrder {
316    phase: PatchPhase,
317    last_remove: Option<usize>,
318    last_insert: Option<usize>,
319}
320
321impl Default for ParentPatchOrder {
322    fn default() -> Self {
323        Self {
324            phase: PatchPhase::Remove,
325            last_remove: None,
326            last_insert: None,
327        }
328    }
329}
330
331fn validate_patch_order(ops: &[PatchOp]) -> Result<(), String> {
332    let mut parent_orders: HashMap<Vec<usize>, ParentPatchOrder> = HashMap::new();
333
334    for (op_index, op) in ops.iter().enumerate() {
335        let Some(order_op) = classify_patch_order_op(op) else {
336            continue;
337        };
338
339        match order_op {
340            PatchOrderOp::Remove { path, index } => {
341                let order = parent_orders.entry(path.to_vec()).or_default();
342                if order.phase > PatchPhase::Remove {
343                    return Err(format!(
344                        "patch op at index {op_index} is remove_child, but removes must appear before update_props, replace_node, and insert_child"
345                    ));
346                }
347                if let Some(previous) = order.last_remove
348                    && index > previous
349                {
350                    return Err(format!(
351                        "patch op at index {op_index} removes child {index} from parent path {path:?}, but remove_child ops for the same parent must not increase child indices"
352                    ));
353                }
354                order.last_remove = Some(index);
355            }
356            PatchOrderOp::Middle { parent_path } => {
357                let order = parent_orders.entry(parent_path.to_vec()).or_default();
358                if order.phase == PatchPhase::Insert {
359                    return Err(format!(
360                        "patch op at index {op_index} is update_props or replace_node, but updates and replacements must appear before insert_child"
361                    ));
362                }
363                order.phase = PatchPhase::Middle;
364            }
365            PatchOrderOp::Insert { path, index } => {
366                let order = parent_orders.entry(path.to_vec()).or_default();
367                order.phase = PatchPhase::Insert;
368                if let Some(previous) = order.last_insert
369                    && index < previous
370                {
371                    return Err(format!(
372                        "patch op at index {op_index} inserts child {index} into parent path {path:?}, but insert_child ops for the same parent must not decrease child indices"
373                    ));
374                }
375                order.last_insert = Some(index);
376            }
377        }
378    }
379
380    Ok(())
381}
382
383fn classify_patch_order_op(op: &PatchOp) -> Option<PatchOrderOp<'_>> {
384    match op.op.as_str() {
385        "remove_child" => op
386            .rest
387            .get("index")
388            .and_then(|value| value.as_u64())
389            .map(|index| PatchOrderOp::Remove {
390                path: &op.path,
391                index: index as usize,
392            }),
393        "update_props" if op.rest.get("props").is_some_and(|props| props.is_object()) => op
394            .path
395            .split_last()
396            .map(|(_, parent_path)| PatchOrderOp::Middle { parent_path }),
397        "replace_node"
398            if op
399                .rest
400                .get("node")
401                .is_some_and(|node| serde_json::from_value::<TreeNode>(node.clone()).is_ok()) =>
402        {
403            op.path
404                .split_last()
405                .map(|(_, parent_path)| PatchOrderOp::Middle { parent_path })
406        }
407        "insert_child" => op
408            .rest
409            .get("index")
410            .and_then(|value| value.as_u64())
411            .and_then(|index| {
412                op.rest.get("node").and_then(|node| {
413                    serde_json::from_value::<TreeNode>(node.clone())
414                        .is_ok()
415                        .then_some(PatchOrderOp::Insert {
416                            path: &op.path,
417                            index: index as usize,
418                        })
419                })
420            }),
421        _ => None,
422    }
423}
424
425/// Walk the tree depth-first and record the path to each non-empty
426/// node ID. Mirrors the original `find_by_id` semantics: when an ID
427/// appears more than once (which the validator already flags as a
428/// host bug), only the first depth-first occurrence is indexed.
429fn collect_id_index(
430    node: &TreeNode,
431    path: &mut Vec<usize>,
432    out: &mut HashMap<String, Vec<usize>>,
433    depth: usize,
434) {
435    if depth > MAX_TREE_DEPTH {
436        return;
437    }
438    if !node.id.is_empty() {
439        out.entry(node.id.clone()).or_insert_with(|| path.clone());
440    }
441    for (idx, child) in node.children.iter().enumerate() {
442        path.push(idx);
443        collect_id_index(child, path, out, depth + 1);
444        path.pop();
445    }
446}
447
448fn find_window_recursive<'a>(
449    node: &'a TreeNode,
450    window_id: &str,
451    depth: usize,
452) -> Option<&'a TreeNode> {
453    if depth > MAX_TREE_DEPTH {
454        plushie_core::diagnostics::warn(plushie_core::Diagnostic::TreeDepthExceeded {
455            id: node.id.clone(),
456            max_depth: MAX_TREE_DEPTH,
457        });
458        return None;
459    }
460    if node.type_name == "window" && node.id == window_id {
461        return Some(node);
462    }
463    for child in &node.children {
464        if let Some(found) = find_window_recursive(child, window_id, depth + 1) {
465            return Some(found);
466        }
467    }
468    None
469}
470
471fn collect_window_ids_recursive(node: &TreeNode, ids: &mut Vec<String>, depth: usize) {
472    if depth > MAX_TREE_DEPTH {
473        plushie_core::diagnostics::warn(plushie_core::Diagnostic::TreeDepthExceeded {
474            id: node.id.clone(),
475            max_depth: MAX_TREE_DEPTH,
476        });
477        return;
478    }
479    if node.type_name == "window" {
480        ids.push(node.id.clone());
481    }
482    for child in &node.children {
483        collect_window_ids_recursive(child, ids, depth + 1);
484    }
485}
486
487/// Maximum number of duplicate IDs collected per validation pass
488/// before short-circuiting.
489///
490/// A pathological tree (legitimate bug or hostile host) can legally
491/// contain millions of nodes under the 64 MiB wire cap. Reporting
492/// every duplicate burns CPU and memory walking the rest of the
493/// tree; one `too_many_duplicate_ids` diagnostic is more useful than
494/// a list that no one will read.
495const MAX_DUPLICATE_IDS: usize = 100;
496
497/// Walk the tree and check that all node IDs are unique.
498///
499/// Returns `Ok(())` if no duplicates are found, or `Err` with a list of
500/// `"id (type_name)"` strings for each ID that appears more than once.
501/// Empty IDs are skipped (some internal nodes may not have meaningful IDs).
502///
503/// The traversal short-circuits after [`MAX_DUPLICATE_IDS`] duplicates
504/// have been collected; a single summary entry (`too_many_duplicates`)
505/// is appended so the caller knows the list is capped.
506fn validate_unique_ids(root: &TreeNode) -> Result<(), Vec<String>> {
507    let mut seen = HashSet::new();
508    let mut duplicates = Vec::new();
509    let mut summary_emitted = false;
510    collect_duplicate_ids(root, &mut seen, &mut duplicates, &mut summary_emitted, 0);
511    if duplicates.is_empty() {
512        Ok(())
513    } else {
514        Err(duplicates)
515    }
516}
517
518fn collect_duplicate_ids(
519    node: &TreeNode,
520    seen: &mut HashSet<String>,
521    duplicates: &mut Vec<String>,
522    summary_emitted: &mut bool,
523    depth: usize,
524) {
525    if depth > MAX_TREE_DEPTH {
526        return;
527    }
528    if duplicates.len() >= MAX_DUPLICATE_IDS {
529        if !*summary_emitted {
530            let diag = plushie_core::Diagnostic::TooManyDuplicates {
531                limit: MAX_DUPLICATE_IDS,
532            };
533            duplicates.push(diag.to_string());
534            *summary_emitted = true;
535        }
536        return;
537    }
538    if !node.id.is_empty() && !seen.insert(node.id.clone()) {
539        duplicates.push(format!("{} ({})", node.id, node.type_name));
540    }
541    for child in &node.children {
542        collect_duplicate_ids(child, seen, duplicates, summary_emitted, depth + 1);
543    }
544}
545
546/// Navigate to a node at the given path of child indices.
547fn navigate<'a>(root: &'a TreeNode, path: &[usize]) -> Result<&'a TreeNode, String> {
548    let mut current = root;
549    for &idx in path {
550        if idx < current.children.len() {
551            current = &current.children[idx];
552        } else {
553            return Err(format!(
554                "path navigation: index {idx} out of bounds (len={})",
555                current.children.len()
556            ));
557        }
558    }
559    Ok(current)
560}
561
562fn navigate_mut<'a>(root: &'a mut TreeNode, path: &[usize]) -> Result<&'a mut TreeNode, String> {
563    let mut current = root;
564    for &idx in path {
565        if idx < current.children.len() {
566            current = &mut current.children[idx];
567        } else {
568            return Err(format!(
569                "path navigation: index {idx} out of bounds (len={})",
570                current.children.len()
571            ));
572        }
573    }
574    Ok(current)
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use plushie_core::protocol::PatchOp;
581    use plushie_widget_sdk::testing::{node, node_with_children, node_with_props};
582    use serde_json::json;
583
584    fn make_patch_op(op: &str, path: Vec<usize>, rest: serde_json::Value) -> PatchOp {
585        // Deserialize from JSON to get proper PatchOp with flattened rest
586        let mut obj = serde_json::Map::new();
587        obj.insert("op".to_string(), json!(op));
588        obj.insert("path".to_string(), json!(path));
589        if let Some(map) = rest.as_object() {
590            for (k, v) in map {
591                obj.insert(k.clone(), v.clone());
592            }
593        }
594        serde_json::from_value(serde_json::Value::Object(obj)).unwrap()
595    }
596
597    // -----------------------------------------------------------------------
598    // Tree basics
599    // -----------------------------------------------------------------------
600
601    #[test]
602    fn new_tree_is_empty() {
603        let tree = Tree::new();
604        assert!(tree.root().is_none());
605    }
606
607    #[test]
608    fn default_tree_is_empty() {
609        let tree = Tree::default();
610        assert!(tree.root().is_none());
611    }
612
613    #[test]
614    fn snapshot_sets_root() {
615        let mut tree = Tree::new();
616        let _ = tree.snapshot(node("root", "column"));
617        assert!(tree.root().is_some());
618        assert_eq!(tree.root().unwrap().id, "root");
619        assert_eq!(tree.root().unwrap().type_name, "column");
620    }
621
622    #[test]
623    fn snapshot_replaces_previous_root() {
624        let mut tree = Tree::new();
625        let _ = tree.snapshot(node("first", "column"));
626        let _ = tree.snapshot(node("second", "row"));
627        assert_eq!(tree.root().unwrap().id, "second");
628        assert_eq!(tree.root().unwrap().type_name, "row");
629    }
630
631    #[test]
632    fn snapshot_preserves_children() {
633        let mut tree = Tree::new();
634        let root = node_with_children(
635            "root",
636            "column",
637            vec![node("a", "text"), node("b", "button")],
638        );
639        let _ = tree.snapshot(root);
640        assert_eq!(tree.root().unwrap().children.len(), 2);
641        assert_eq!(tree.root().unwrap().children[0].id, "a");
642        assert_eq!(tree.root().unwrap().children[1].id, "b");
643    }
644
645    // -----------------------------------------------------------------------
646    // find_window
647    // -----------------------------------------------------------------------
648
649    #[test]
650    fn find_window_at_root() {
651        let mut tree = Tree::new();
652        let _ = tree.snapshot(node("main", "window"));
653        let found = tree.find_window("main");
654        assert!(found.is_some());
655        assert_eq!(found.unwrap().id, "main");
656        assert_eq!(found.unwrap().type_name, "window");
657    }
658
659    #[test]
660    fn find_window_root_wrong_id() {
661        let mut tree = Tree::new();
662        let _ = tree.snapshot(node("main", "window"));
663        assert!(tree.find_window("other").is_none());
664    }
665
666    #[test]
667    fn find_window_in_children() {
668        let mut tree = Tree::new();
669        let root = node_with_children(
670            "root",
671            "column",
672            vec![node("win1", "window"), node("win2", "window")],
673        );
674        let _ = tree.snapshot(root);
675        assert!(tree.find_window("win1").is_some());
676        assert!(tree.find_window("win2").is_some());
677        assert_eq!(tree.find_window("win1").unwrap().id, "win1");
678    }
679
680    #[test]
681    fn find_window_not_found() {
682        let mut tree = Tree::new();
683        let _ = tree.snapshot(node("root", "column"));
684        assert!(tree.find_window("nope").is_none());
685    }
686
687    #[test]
688    fn find_window_on_empty_tree() {
689        let tree = Tree::new();
690        assert!(tree.find_window("anything").is_none());
691    }
692
693    #[test]
694    fn find_window_ignores_non_window_children() {
695        let mut tree = Tree::new();
696        let root = node_with_children(
697            "root",
698            "column",
699            vec![
700                node("btn", "button"),
701                node("win", "window"),
702                node("txt", "text"),
703            ],
704        );
705        let _ = tree.snapshot(root);
706        assert!(tree.find_window("btn").is_none());
707        assert!(tree.find_window("txt").is_none());
708        assert!(tree.find_window("win").is_some());
709    }
710
711    #[test]
712    fn find_window_searches_grandchildren() {
713        let mut tree = Tree::new();
714        let root = node_with_children(
715            "root",
716            "column",
717            vec![node_with_children(
718                "inner",
719                "row",
720                vec![node("deep_win", "window")],
721            )],
722        );
723        let _ = tree.snapshot(root);
724        let found = tree.find_window("deep_win");
725        assert!(found.is_some());
726        assert_eq!(found.unwrap().id, "deep_win");
727    }
728
729    #[test]
730    fn find_window_deeply_nested() {
731        let mut tree = Tree::new();
732        let root = node_with_children(
733            "root",
734            "column",
735            vec![node_with_children(
736                "l1",
737                "row",
738                vec![node_with_children(
739                    "l2",
740                    "column",
741                    vec![node_with_children(
742                        "l3",
743                        "row",
744                        vec![node("buried_win", "window")],
745                    )],
746                )],
747            )],
748        );
749        let _ = tree.snapshot(root);
750        let found = tree.find_window("buried_win");
751        assert!(found.is_some());
752        assert_eq!(found.unwrap().id, "buried_win");
753    }
754
755    #[test]
756    fn window_ids_finds_nested_windows() {
757        let mut tree = Tree::new();
758        let root = node_with_children(
759            "root",
760            "column",
761            vec![
762                node("w1", "window"),
763                node_with_children("inner", "row", vec![node("w2", "window")]),
764            ],
765        );
766        let _ = tree.snapshot(root);
767        let ids = tree.window_ids();
768        assert_eq!(ids.len(), 2);
769        assert!(ids.contains(&"w1".to_string()));
770        assert!(ids.contains(&"w2".to_string()));
771    }
772
773    // -----------------------------------------------------------------------
774    // window_ids
775    // -----------------------------------------------------------------------
776
777    #[test]
778    fn window_ids_when_root_is_window() {
779        let mut tree = Tree::new();
780        let _ = tree.snapshot(node("main", "window"));
781        let ids = tree.window_ids();
782        assert_eq!(ids, vec!["main".to_string()]);
783    }
784
785    #[test]
786    fn window_ids_collects_child_windows() {
787        let mut tree = Tree::new();
788        let root = node_with_children(
789            "root",
790            "column",
791            vec![
792                node("w1", "window"),
793                node("w2", "window"),
794                node("w3", "window"),
795            ],
796        );
797        let _ = tree.snapshot(root);
798        let ids = tree.window_ids();
799        assert_eq!(ids.len(), 3);
800        assert!(ids.contains(&"w1".to_string()));
801        assert!(ids.contains(&"w2".to_string()));
802        assert!(ids.contains(&"w3".to_string()));
803    }
804
805    #[test]
806    fn window_ids_skips_non_windows() {
807        let mut tree = Tree::new();
808        let root = node_with_children(
809            "root",
810            "column",
811            vec![
812                node("w1", "window"),
813                node("btn", "button"),
814                node("w2", "window"),
815            ],
816        );
817        let _ = tree.snapshot(root);
818        let ids = tree.window_ids();
819        assert_eq!(ids.len(), 2);
820        assert!(!ids.contains(&"btn".to_string()));
821    }
822
823    #[test]
824    fn window_ids_empty_when_no_windows() {
825        let mut tree = Tree::new();
826        let _ = tree.snapshot(node("root", "column"));
827        assert!(tree.window_ids().is_empty());
828    }
829
830    #[test]
831    fn window_ids_empty_on_empty_tree() {
832        let tree = Tree::new();
833        assert!(tree.window_ids().is_empty());
834    }
835
836    // -----------------------------------------------------------------------
837    // apply_patch: replace_node
838    // -----------------------------------------------------------------------
839
840    #[test]
841    fn patch_replace_root() {
842        let mut tree = Tree::new();
843        let _ = tree.snapshot(node("old", "column"));
844        let op = make_patch_op(
845            "replace_node",
846            vec![],
847            json!({
848                "node": {"id": "new", "type": "row", "props": {}, "children": []}
849            }),
850        );
851        tree.apply_patch(vec![op]);
852        assert_eq!(tree.root().unwrap().id, "new");
853        assert_eq!(tree.root().unwrap().type_name, "row");
854    }
855
856    #[test]
857    fn patch_replace_child() {
858        let mut tree = Tree::new();
859        let root = node_with_children(
860            "root",
861            "column",
862            vec![node("a", "text"), node("b", "button")],
863        );
864        let _ = tree.snapshot(root);
865        let op = make_patch_op(
866            "replace_node",
867            vec![1],
868            json!({
869                "node": {"id": "c", "type": "text", "props": {"content": "replaced"}, "children": []}
870            }),
871        );
872        tree.apply_patch(vec![op]);
873        assert_eq!(tree.root().unwrap().children[1].id, "c");
874        assert_eq!(
875            tree.root().unwrap().children[1].props.to_value()["content"],
876            "replaced"
877        );
878    }
879
880    #[test]
881    fn patch_replace_nested_child() {
882        let mut tree = Tree::new();
883        let root = node_with_children(
884            "root",
885            "column",
886            vec![node_with_children(
887                "row",
888                "row",
889                vec![node("inner", "text")],
890            )],
891        );
892        let _ = tree.snapshot(root);
893        let op = make_patch_op(
894            "replace_node",
895            vec![0, 0],
896            json!({
897                "node": {"id": "replaced", "type": "button", "props": {}, "children": []}
898            }),
899        );
900        tree.apply_patch(vec![op]);
901        assert_eq!(tree.root().unwrap().children[0].children[0].id, "replaced");
902        assert_eq!(
903            tree.root().unwrap().children[0].children[0].type_name,
904            "button"
905        );
906    }
907
908    #[test]
909    fn patch_replace_out_of_bounds_does_not_panic() {
910        let mut tree = Tree::new();
911        let _ = tree.snapshot(node("root", "column"));
912        let op = make_patch_op(
913            "replace_node",
914            vec![5],
915            json!({
916                "node": {"id": "x", "type": "text", "props": {}, "children": []}
917            }),
918        );
919        // Should report the malformed op but not panic.
920        tree.apply_patch(vec![op]);
921        // Root is unchanged
922        assert_eq!(tree.root().unwrap().id, "root");
923    }
924
925    // -----------------------------------------------------------------------
926    // apply_patch: update_props
927    // -----------------------------------------------------------------------
928
929    #[test]
930    fn patch_update_props_on_root() {
931        let mut tree = Tree::new();
932        let _ = tree.snapshot(node_with_props("root", "column", json!({"spacing": 5})));
933        let op = make_patch_op(
934            "update_props",
935            vec![],
936            json!({
937                "props": {"spacing": 10, "padding": 20}
938            }),
939        );
940        tree.apply_patch(vec![op]);
941        assert_eq!(tree.root().unwrap().props.to_value()["spacing"], 10);
942        assert_eq!(tree.root().unwrap().props.to_value()["padding"], 20);
943    }
944
945    #[test]
946    fn patch_update_props_removes_null_keys() {
947        let mut tree = Tree::new();
948        let _ = tree.snapshot(node_with_props(
949            "root",
950            "text",
951            json!({"content": "hi", "size": 14}),
952        ));
953        let op = make_patch_op(
954            "update_props",
955            vec![],
956            json!({
957                "props": {"size": null}
958            }),
959        );
960        tree.apply_patch(vec![op]);
961        assert_eq!(tree.root().unwrap().props.to_value()["content"], "hi");
962        assert!(tree.root().unwrap().props.get("size").is_none());
963    }
964
965    #[test]
966    fn patch_update_props_on_child() {
967        let mut tree = Tree::new();
968        let root = node_with_children(
969            "root",
970            "column",
971            vec![node_with_props("txt", "text", json!({"content": "old"}))],
972        );
973        let _ = tree.snapshot(root);
974        let op = make_patch_op(
975            "update_props",
976            vec![0],
977            json!({
978                "props": {"content": "new"}
979            }),
980        );
981        tree.apply_patch(vec![op]);
982        assert_eq!(
983            tree.root().unwrap().children[0].props.to_value()["content"],
984            "new"
985        );
986    }
987
988    #[test]
989    fn patch_update_props_non_object_target_props_does_not_panic() {
990        let mut tree = Tree::new();
991        // A non-object props value collapses to an empty map on construction;
992        // the merge then proceeds normally, inserting the patch keys.
993        let _ = tree.snapshot(node_with_props("root", "text", json!("not an object")));
994        let op = make_patch_op(
995            "update_props",
996            vec![],
997            json!({
998                "props": {"content": "new"}
999            }),
1000        );
1001        tree.apply_patch(vec![op]);
1002        assert_eq!(tree.root().unwrap().props.get_str("content"), Some("new"));
1003    }
1004
1005    #[test]
1006    fn patch_update_props_non_object_patch_props_does_not_panic() {
1007        let mut tree = Tree::new();
1008        let _ = tree.snapshot(node_with_props("root", "text", json!({"content": "hi"})));
1009        // Patch props is a string, not an object
1010        let op = make_patch_op(
1011            "update_props",
1012            vec![],
1013            json!({
1014                "props": "not an object"
1015            }),
1016        );
1017        tree.apply_patch(vec![op]);
1018        // Props unchanged: the merge was skipped
1019        assert_eq!(tree.root().unwrap().props.to_value()["content"], "hi");
1020    }
1021
1022    // -----------------------------------------------------------------------
1023    // apply_patch: insert_child
1024    // -----------------------------------------------------------------------
1025
1026    #[test]
1027    fn patch_insert_child_at_beginning() {
1028        let mut tree = Tree::new();
1029        let root = node_with_children("root", "column", vec![node("a", "text")]);
1030        let _ = tree.snapshot(root);
1031        let op = make_patch_op(
1032            "insert_child",
1033            vec![],
1034            json!({
1035                "index": 0,
1036                "node": {"id": "b", "type": "button", "props": {}, "children": []}
1037            }),
1038        );
1039        tree.apply_patch(vec![op]);
1040        assert_eq!(tree.root().unwrap().children.len(), 2);
1041        assert_eq!(tree.root().unwrap().children[0].id, "b");
1042        assert_eq!(tree.root().unwrap().children[1].id, "a");
1043    }
1044
1045    #[test]
1046    fn patch_insert_child_at_end() {
1047        let mut tree = Tree::new();
1048        let root = node_with_children("root", "column", vec![node("a", "text")]);
1049        let _ = tree.snapshot(root);
1050        let op = make_patch_op(
1051            "insert_child",
1052            vec![],
1053            json!({
1054                "index": 1,
1055                "node": {"id": "b", "type": "button", "props": {}, "children": []}
1056            }),
1057        );
1058        tree.apply_patch(vec![op]);
1059        assert_eq!(tree.root().unwrap().children.len(), 2);
1060        assert_eq!(tree.root().unwrap().children[1].id, "b");
1061    }
1062
1063    #[test]
1064    fn patch_insert_child_beyond_length_appends() {
1065        let mut tree = Tree::new();
1066        let _ = tree.snapshot(node("root", "column"));
1067        let op = make_patch_op(
1068            "insert_child",
1069            vec![],
1070            json!({
1071                "index": 99,
1072                "node": {"id": "x", "type": "text", "props": {}, "children": []}
1073            }),
1074        );
1075        tree.apply_patch(vec![op]);
1076        assert_eq!(tree.root().unwrap().children.len(), 1);
1077        assert_eq!(tree.root().unwrap().children[0].id, "x");
1078    }
1079
1080    #[test]
1081    fn patch_insert_child_into_nested_parent() {
1082        let mut tree = Tree::new();
1083        let root = node_with_children(
1084            "root",
1085            "column",
1086            vec![node_with_children(
1087                "row",
1088                "row",
1089                vec![node("existing", "text")],
1090            )],
1091        );
1092        let _ = tree.snapshot(root);
1093        let op = make_patch_op(
1094            "insert_child",
1095            vec![0],
1096            json!({
1097                "index": 0,
1098                "node": {"id": "new", "type": "button", "props": {}, "children": []}
1099            }),
1100        );
1101        tree.apply_patch(vec![op]);
1102        let row = &tree.root().unwrap().children[0];
1103        assert_eq!(row.children.len(), 2);
1104        assert_eq!(row.children[0].id, "new");
1105        assert_eq!(row.children[1].id, "existing");
1106    }
1107
1108    // -----------------------------------------------------------------------
1109    // apply_patch: remove_child
1110    // -----------------------------------------------------------------------
1111
1112    #[test]
1113    fn patch_remove_child() {
1114        let mut tree = Tree::new();
1115        let root = node_with_children(
1116            "root",
1117            "column",
1118            vec![node("a", "text"), node("b", "button"), node("c", "text")],
1119        );
1120        let _ = tree.snapshot(root);
1121        let op = make_patch_op("remove_child", vec![], json!({"index": 1}));
1122        tree.apply_patch(vec![op]);
1123        assert_eq!(tree.root().unwrap().children.len(), 2);
1124        assert_eq!(tree.root().unwrap().children[0].id, "a");
1125        assert_eq!(tree.root().unwrap().children[1].id, "c");
1126    }
1127
1128    #[test]
1129    fn patch_remove_child_first() {
1130        let mut tree = Tree::new();
1131        let root = node_with_children(
1132            "root",
1133            "column",
1134            vec![node("a", "text"), node("b", "button")],
1135        );
1136        let _ = tree.snapshot(root);
1137        let op = make_patch_op("remove_child", vec![], json!({"index": 0}));
1138        tree.apply_patch(vec![op]);
1139        assert_eq!(tree.root().unwrap().children.len(), 1);
1140        assert_eq!(tree.root().unwrap().children[0].id, "b");
1141    }
1142
1143    #[test]
1144    fn patch_remove_child_last() {
1145        let mut tree = Tree::new();
1146        let root = node_with_children(
1147            "root",
1148            "column",
1149            vec![node("a", "text"), node("b", "button")],
1150        );
1151        let _ = tree.snapshot(root);
1152        let op = make_patch_op("remove_child", vec![], json!({"index": 1}));
1153        tree.apply_patch(vec![op]);
1154        assert_eq!(tree.root().unwrap().children.len(), 1);
1155        assert_eq!(tree.root().unwrap().children[0].id, "a");
1156    }
1157
1158    #[test]
1159    fn patch_remove_child_out_of_bounds_does_not_panic() {
1160        let mut tree = Tree::new();
1161        let _ = tree.snapshot(node("root", "column"));
1162        let op = make_patch_op("remove_child", vec![], json!({"index": 0}));
1163        // Should report the malformed op but not panic.
1164        tree.apply_patch(vec![op]);
1165        assert!(tree.root().unwrap().children.is_empty());
1166    }
1167
1168    // -----------------------------------------------------------------------
1169    // apply_patch: unknown op
1170    // -----------------------------------------------------------------------
1171
1172    #[test]
1173    fn patch_unknown_op_does_not_panic() {
1174        let mut tree = Tree::new();
1175        let mut root = node("root", "column");
1176        root.children.push(node("existing", "text"));
1177        let _ = tree.snapshot(root);
1178        let unknown = make_patch_op("frobnicate", vec![], json!({}));
1179        let valid = make_patch_op(
1180            "insert_child",
1181            vec![],
1182            json!({
1183                "index": 1,
1184                "node": node("child", "text")
1185            }),
1186        );
1187
1188        tree.apply_patch(vec![unknown, valid]);
1189
1190        assert_eq!(tree.root().unwrap().id, "root");
1191        assert_eq!(tree.root().unwrap().children.len(), 2);
1192        assert_eq!(tree.root().unwrap().children[0].id, "existing");
1193        assert_eq!(tree.root().unwrap().children[1].id, "child");
1194    }
1195
1196    #[test]
1197    fn unknown_patch_payload_preserves_flattened_fields() {
1198        let unknown = make_patch_op(
1199            "frobnicate",
1200            vec![1, 2],
1201            json!({
1202                "index": 3,
1203                "extra": {"answer": 42}
1204            }),
1205        );
1206
1207        let payload = patch_op_payload(&unknown);
1208
1209        assert_eq!(payload["op"], "frobnicate");
1210        assert_eq!(payload["path"], json!([1, 2]));
1211        assert_eq!(payload["index"], 3);
1212        assert_eq!(payload["extra"], json!({"answer": 42}));
1213    }
1214
1215    // -----------------------------------------------------------------------
1216    // apply_patch: multiple ops in sequence
1217    // -----------------------------------------------------------------------
1218
1219    #[test]
1220    fn patch_multiple_ops_applied_in_order() {
1221        let mut tree = Tree::new();
1222        let _ = tree.snapshot(node("root", "column"));
1223
1224        let ops = vec![
1225            make_patch_op(
1226                "insert_child",
1227                vec![],
1228                json!({
1229                    "index": 0,
1230                    "node": {"id": "a", "type": "text", "props": {}, "children": []}
1231                }),
1232            ),
1233            make_patch_op(
1234                "insert_child",
1235                vec![],
1236                json!({
1237                    "index": 1,
1238                    "node": {"id": "b", "type": "text", "props": {}, "children": []}
1239                }),
1240            ),
1241            make_patch_op(
1242                "insert_child",
1243                vec![],
1244                json!({
1245                    "index": 1,
1246                    "node": {"id": "c", "type": "text", "props": {}, "children": []}
1247                }),
1248            ),
1249        ];
1250        tree.apply_patch(ops);
1251        let children = &tree.root().unwrap().children;
1252        assert_eq!(children.len(), 3);
1253        assert_eq!(children[0].id, "a");
1254        assert_eq!(children[1].id, "c");
1255        assert_eq!(children[2].id, "b");
1256    }
1257
1258    // -----------------------------------------------------------------------
1259    // apply_patch on empty tree
1260    // -----------------------------------------------------------------------
1261
1262    #[test]
1263    fn patch_on_empty_tree_does_not_panic() {
1264        let mut tree = Tree::new();
1265        let op = make_patch_op(
1266            "replace_node",
1267            vec![],
1268            json!({
1269                "node": {"id": "x", "type": "text", "props": {}, "children": []}
1270            }),
1271        );
1272        tree.apply_patch(vec![op]);
1273        // Still empty: the op should fail gracefully
1274        assert!(tree.root().is_none());
1275    }
1276
1277    // -----------------------------------------------------------------------
1278    // navigate_mut edge cases (tested indirectly through patch ops)
1279    // -----------------------------------------------------------------------
1280
1281    #[test]
1282    fn patch_deep_path_navigation() {
1283        let mut tree = Tree::new();
1284        let root = node_with_children(
1285            "root",
1286            "column",
1287            vec![node_with_children(
1288                "r0",
1289                "row",
1290                vec![node_with_children(
1291                    "r0c0",
1292                    "column",
1293                    vec![node("deep", "text")],
1294                )],
1295            )],
1296        );
1297        let _ = tree.snapshot(root);
1298        let op = make_patch_op(
1299            "update_props",
1300            vec![0, 0, 0],
1301            json!({
1302                "props": {"content": "updated deep"}
1303            }),
1304        );
1305        tree.apply_patch(vec![op]);
1306        let deep = &tree.root().unwrap().children[0].children[0].children[0];
1307        assert_eq!(deep.props.to_value()["content"], "updated deep");
1308    }
1309
1310    #[test]
1311    fn patch_invalid_path_does_not_panic() {
1312        let mut tree = Tree::new();
1313        let _ = tree.snapshot(node("root", "column"));
1314        let op = make_patch_op(
1315            "update_props",
1316            vec![0, 1, 2],
1317            json!({
1318                "props": {"x": 1}
1319            }),
1320        );
1321        tree.apply_patch(vec![op]);
1322        // Root unchanged
1323        assert_eq!(tree.root().unwrap().id, "root");
1324    }
1325
1326    // -----------------------------------------------------------------------
1327    // Malformed patch operations (error paths)
1328    // -----------------------------------------------------------------------
1329
1330    #[test]
1331    fn patch_replace_node_missing_node_field_does_not_panic() {
1332        let mut tree = Tree::new();
1333        let _ = tree.snapshot(node("root", "column"));
1334        // replace_node without the required "node" field
1335        let op = make_patch_op("replace_node", vec![], json!({}));
1336        tree.apply_patch(vec![op]);
1337        // Tree should be unchanged
1338        assert_eq!(tree.root().unwrap().id, "root");
1339    }
1340
1341    #[test]
1342    fn patch_replace_node_invalid_node_json_does_not_panic() {
1343        let mut tree = Tree::new();
1344        let _ = tree.snapshot(node("root", "column"));
1345        // "node" is present but not a valid TreeNode (missing required fields)
1346        let op = make_patch_op("replace_node", vec![], json!({"node": {"garbage": true}}));
1347        tree.apply_patch(vec![op]);
1348        assert_eq!(tree.root().unwrap().id, "root");
1349    }
1350
1351    #[test]
1352    fn patch_update_props_missing_props_field_does_not_panic() {
1353        let mut tree = Tree::new();
1354        let _ = tree.snapshot(node_with_props("root", "text", json!({"content": "hi"})));
1355        let op = make_patch_op("update_props", vec![], json!({}));
1356        tree.apply_patch(vec![op]);
1357        // Props unchanged: the missing "props" field is handled gracefully
1358        assert_eq!(tree.root().unwrap().props.to_value()["content"], "hi");
1359    }
1360
1361    #[test]
1362    fn patch_insert_child_missing_index_does_not_panic() {
1363        let mut tree = Tree::new();
1364        let _ = tree.snapshot(node("root", "column"));
1365        let op = make_patch_op(
1366            "insert_child",
1367            vec![],
1368            json!({
1369                "node": {"id": "x", "type": "text", "props": {}, "children": []}
1370            }),
1371        );
1372        tree.apply_patch(vec![op]);
1373        // No child inserted because index is missing
1374        assert!(tree.root().unwrap().children.is_empty());
1375    }
1376
1377    #[test]
1378    fn patch_insert_child_missing_node_does_not_panic() {
1379        let mut tree = Tree::new();
1380        let _ = tree.snapshot(node("root", "column"));
1381        let op = make_patch_op("insert_child", vec![], json!({"index": 0}));
1382        tree.apply_patch(vec![op]);
1383        assert!(tree.root().unwrap().children.is_empty());
1384    }
1385
1386    #[test]
1387    fn patch_remove_child_missing_index_does_not_panic() {
1388        let mut tree = Tree::new();
1389        let root = node_with_children("root", "column", vec![node("a", "text")]);
1390        let _ = tree.snapshot(root);
1391        let op = make_patch_op("remove_child", vec![], json!({}));
1392        tree.apply_patch(vec![op]);
1393        // Child should still be present: the op failed gracefully
1394        assert_eq!(tree.root().unwrap().children.len(), 1);
1395    }
1396
1397    // -----------------------------------------------------------------------
1398    // Multi-op patch tests
1399    // -----------------------------------------------------------------------
1400
1401    #[test]
1402    fn patch_multi_op_mixed_types() {
1403        let mut tree = Tree::new();
1404        let root = node_with_children(
1405            "root",
1406            "column",
1407            vec![
1408                node_with_props("a", "text", json!({"content": "hello"})),
1409                node("b", "button"),
1410            ],
1411        );
1412        let _ = tree.snapshot(root);
1413
1414        let ops = vec![
1415            // Insert a third child at index 2
1416            make_patch_op(
1417                "insert_child",
1418                vec![],
1419                json!({
1420                    "index": 2,
1421                    "node": {"id": "c", "type": "text", "props": {"content": "new"}, "children": []}
1422                }),
1423            ),
1424            // Remove the first child (index 0 = "a")
1425            make_patch_op("remove_child", vec![], json!({"index": 0})),
1426            // Update props on current index 0 (was "b", now shifted to front)
1427            make_patch_op(
1428                "update_props",
1429                vec![0],
1430                json!({"props": {"label": "updated"}}),
1431            ),
1432        ];
1433        tree.apply_patch(ops);
1434
1435        let children = &tree.root().unwrap().children;
1436        assert_eq!(children.len(), 2);
1437        assert_eq!(children[0].id, "b");
1438        assert_eq!(children[0].props.to_value()["label"], "updated");
1439        assert_eq!(children[1].id, "c");
1440    }
1441
1442    #[test]
1443    fn patch_remove_shifts_indices() {
1444        let mut tree = Tree::new();
1445        let root = node_with_children(
1446            "root",
1447            "column",
1448            vec![
1449                node("first", "text"),
1450                node("second", "button"),
1451                node("third", "text"),
1452            ],
1453        );
1454        let _ = tree.snapshot(root);
1455
1456        let ops = vec![
1457            // Remove child at index 0 ("first")
1458            make_patch_op("remove_child", vec![], json!({"index": 0})),
1459            // Replace node at index 0, which is now "second" after removal
1460            make_patch_op(
1461                "replace_node",
1462                vec![0],
1463                json!({
1464                    "node": {"id": "replaced", "type": "row", "props": {}, "children": []}
1465                }),
1466            ),
1467        ];
1468        tree.apply_patch(ops);
1469
1470        let children = &tree.root().unwrap().children;
1471        assert_eq!(children.len(), 2);
1472        assert_eq!(children[0].id, "replaced");
1473        assert_eq!(children[0].type_name, "row");
1474        assert_eq!(children[1].id, "third");
1475    }
1476
1477    #[test]
1478    fn patch_bad_middle_op_continues() {
1479        let mut tree = Tree::new();
1480        let root = node_with_children(
1481            "root",
1482            "column",
1483            vec![
1484                node_with_props("a", "text", json!({"content": "original"})),
1485                node("b", "button"),
1486            ],
1487        );
1488        let _ = tree.snapshot(root);
1489
1490        let ops = vec![
1491            // First op: update props on child "a" (valid)
1492            make_patch_op(
1493                "update_props",
1494                vec![0],
1495                json!({"props": {"content": "changed"}}),
1496            ),
1497            // Second op: invalid path (out of bounds)
1498            make_patch_op(
1499                "update_props",
1500                vec![99, 0],
1501                json!({"props": {"content": "nope"}}),
1502            ),
1503            // Third op: update props on child "b" (valid)
1504            make_patch_op(
1505                "update_props",
1506                vec![1],
1507                json!({"props": {"label": "click me"}}),
1508            ),
1509        ];
1510        tree.apply_patch(ops);
1511
1512        let children = &tree.root().unwrap().children;
1513        assert_eq!(children[0].props.to_value()["content"], "changed");
1514        assert_eq!(children[1].props.to_value()["label"], "click me");
1515    }
1516
1517    // -----------------------------------------------------------------------
1518    // validate_unique_ids / snapshot duplicate detection
1519    // -----------------------------------------------------------------------
1520
1521    #[test]
1522    fn snapshot_unique_ids_returns_ok() {
1523        let mut tree = Tree::new();
1524        let root = node_with_children(
1525            "root",
1526            "column",
1527            vec![node("a", "text"), node("b", "button")],
1528        );
1529        assert!(tree.snapshot(root).is_ok());
1530    }
1531
1532    #[test]
1533    fn snapshot_duplicate_ids_returns_err() {
1534        let mut tree = Tree::new();
1535        let root = node_with_children(
1536            "root",
1537            "column",
1538            vec![node("dupe", "text"), node("dupe", "button")],
1539        );
1540        let result = tree.snapshot(root);
1541        assert!(result.is_err());
1542        let dupes = result.unwrap_err();
1543        assert_eq!(dupes.len(), 1);
1544        assert!(dupes[0].contains("dupe"));
1545    }
1546
1547    #[test]
1548    fn snapshot_duplicate_ids_still_accepts_tree() {
1549        let mut tree = Tree::new();
1550        let root = node_with_children(
1551            "root",
1552            "column",
1553            vec![node("dupe", "text"), node("dupe", "button")],
1554        );
1555        let _ = tree.snapshot(root);
1556        // Tree was still accepted despite duplicates
1557        assert!(tree.root().is_some());
1558        assert_eq!(tree.root().unwrap().children.len(), 2);
1559    }
1560
1561    #[test]
1562    fn snapshot_multiple_duplicate_ids() {
1563        let mut tree = Tree::new();
1564        let root = node_with_children(
1565            "root",
1566            "column",
1567            vec![
1568                node("a", "text"),
1569                node("a", "text"),
1570                node("b", "button"),
1571                node("b", "button"),
1572            ],
1573        );
1574        let result = tree.snapshot(root);
1575        assert!(result.is_err());
1576        let dupes = result.unwrap_err();
1577        assert_eq!(dupes.len(), 2);
1578    }
1579
1580    #[test]
1581    fn snapshot_empty_ids_are_ignored() {
1582        let mut tree = Tree::new();
1583        let root = node_with_children("root", "column", vec![node("", "text"), node("", "text")]);
1584        // Empty IDs should not be flagged as duplicates
1585        assert!(tree.snapshot(root).is_ok());
1586    }
1587
1588    #[test]
1589    fn duplicate_collection_short_circuits_past_cap() {
1590        // Build a flat tree with MAX_DUPLICATE_IDS + 50 duplicated IDs.
1591        // The caller must still see an error, but the list is capped
1592        // and a summary entry tells downstream code the list was cut
1593        // short.
1594        let over = super::MAX_DUPLICATE_IDS + 50;
1595        let mut children = Vec::with_capacity(over * 2);
1596        for _ in 0..over {
1597            children.push(node("shared", "text"));
1598            children.push(node("shared", "text"));
1599        }
1600        let root = node_with_children("root", "column", children);
1601        let mut tree = Tree::new();
1602        let dupes = tree.snapshot(root).unwrap_err();
1603
1604        // List should be capped at MAX_DUPLICATE_IDS duplicate entries
1605        // plus the one summary entry.
1606        assert_eq!(dupes.len(), super::MAX_DUPLICATE_IDS + 1);
1607        assert!(
1608            dupes
1609                .last()
1610                .is_some_and(|s| s.contains("too_many_duplicates")),
1611            "expected summary entry, got {:?}",
1612            dupes.last()
1613        );
1614    }
1615
1616    #[test]
1617    fn find_window_returns_none_beyond_max_depth() {
1618        // Build a chain deeper than MAX_TREE_DEPTH (256).
1619        let mut deepest = node("deep_win", "window");
1620        for i in 0..MAX_TREE_DEPTH + 10 {
1621            deepest = node_with_children(&format!("n{i}"), "column", vec![deepest]);
1622        }
1623        let mut tree = Tree::new();
1624        let _ = tree.snapshot(deepest);
1625
1626        // The window at depth > 256 should not be found.
1627        assert!(
1628            tree.find_window("deep_win").is_none(),
1629            "window beyond MAX_TREE_DEPTH should not be reachable"
1630        );
1631    }
1632
1633    #[test]
1634    fn window_ids_skips_windows_beyond_max_depth() {
1635        let mut deepest = node("deep_win", "window");
1636        for i in 0..MAX_TREE_DEPTH + 10 {
1637            deepest = node_with_children(&format!("n{i}"), "column", vec![deepest]);
1638        }
1639        let mut tree = Tree::new();
1640        let _ = tree.snapshot(deepest);
1641
1642        let ids = tree.window_ids();
1643        assert!(
1644            !ids.contains(&"deep_win".to_string()),
1645            "window beyond MAX_TREE_DEPTH should not appear in window_ids"
1646        );
1647    }
1648
1649    // -----------------------------------------------------------------------
1650    // id_index: find_by_id across snapshot + patch
1651    // -----------------------------------------------------------------------
1652
1653    #[test]
1654    fn find_by_id_finds_root() {
1655        let mut tree = Tree::new();
1656        let _ = tree.snapshot(node("root", "column"));
1657        let found = tree.find_by_id("root").unwrap();
1658        assert_eq!(found.id, "root");
1659    }
1660
1661    #[test]
1662    fn find_by_id_finds_nested_descendant() {
1663        let mut tree = Tree::new();
1664        let root = node_with_children(
1665            "root",
1666            "column",
1667            vec![node_with_children("row", "row", vec![node("deep", "text")])],
1668        );
1669        let _ = tree.snapshot(root);
1670        let found = tree.find_by_id("deep").unwrap();
1671        assert_eq!(found.id, "deep");
1672        assert_eq!(found.type_name, "text");
1673    }
1674
1675    #[test]
1676    fn find_by_id_returns_none_for_missing() {
1677        let mut tree = Tree::new();
1678        let _ = tree.snapshot(node("root", "column"));
1679        assert!(tree.find_by_id("ghost").is_none());
1680    }
1681
1682    #[test]
1683    fn find_by_id_skips_empty_ids() {
1684        let mut tree = Tree::new();
1685        let root = node_with_children("root", "column", vec![node("", "text"), node("a", "text")]);
1686        let _ = tree.snapshot(root);
1687        // Empty ID is not indexed, but a real one is reachable.
1688        assert!(tree.find_by_id("").is_none());
1689        assert!(tree.find_by_id("a").is_some());
1690    }
1691
1692    #[test]
1693    fn find_by_id_first_match_wins_under_duplicates() {
1694        let mut tree = Tree::new();
1695        // Two nodes share an ID: the first one (depth-first) is the
1696        // visible one through the index. The validator will report
1697        // the duplicate, but the lookup must remain stable.
1698        let root = node_with_children(
1699            "root",
1700            "column",
1701            vec![
1702                node_with_props("dupe", "text", json!({"content": "first"})),
1703                node_with_props("dupe", "button", json!({"content": "second"})),
1704            ],
1705        );
1706        let _ = tree.snapshot(root);
1707        let found = tree.find_by_id("dupe").unwrap();
1708        assert_eq!(found.type_name, "text");
1709        assert_eq!(found.props.to_value()["content"], "first");
1710    }
1711
1712    #[test]
1713    fn id_index_reflects_inserts_and_removes() {
1714        let mut tree = Tree::new();
1715        let _ = tree.snapshot(node("root", "column"));
1716        assert!(tree.find_by_id("root").is_some());
1717
1718        // Insert "a" at index 0; insert_child shifts no existing
1719        // index because parent is empty.
1720        tree.apply_patch(vec![make_patch_op(
1721            "insert_child",
1722            vec![],
1723            json!({
1724                "index": 0,
1725                "node": {"id": "a", "type": "text", "props": {}, "children": []}
1726            }),
1727        )]);
1728        assert_eq!(tree.find_by_id("a").unwrap().id, "a");
1729
1730        // Insert "b" at index 0, pushing "a" to index 1. The index
1731        // entry for "a" must follow the shift.
1732        tree.apply_patch(vec![make_patch_op(
1733            "insert_child",
1734            vec![],
1735            json!({
1736                "index": 0,
1737                "node": {"id": "b", "type": "text", "props": {}, "children": []}
1738            }),
1739        )]);
1740        assert_eq!(tree.find_by_id("a").unwrap().id, "a");
1741        assert_eq!(tree.find_by_id("b").unwrap().id, "b");
1742
1743        // Update a prop on "a" (now at child index 1) and confirm
1744        // the index lookup still reaches the right node.
1745        tree.apply_patch(vec![make_patch_op(
1746            "update_props",
1747            vec![1],
1748            json!({"props": {"content": "updated"}}),
1749        )]);
1750        assert_eq!(
1751            tree.find_by_id("a").unwrap().props.to_value()["content"],
1752            "updated"
1753        );
1754
1755        // Remove "b" at index 0; "a" shifts back to index 0. The
1756        // index for "b" disappears; "a" still resolves.
1757        tree.apply_patch(vec![make_patch_op(
1758            "remove_child",
1759            vec![],
1760            json!({"index": 0}),
1761        )]);
1762        assert!(tree.find_by_id("b").is_none());
1763        let a = tree.find_by_id("a").unwrap();
1764        assert_eq!(a.id, "a");
1765        assert_eq!(a.props.to_value()["content"], "updated");
1766
1767        // Replace "a" with "c"; the old ID drops from the index,
1768        // the new one appears.
1769        tree.apply_patch(vec![make_patch_op(
1770            "replace_node",
1771            vec![0],
1772            json!({
1773                "node": {"id": "c", "type": "button", "props": {}, "children": []}
1774            }),
1775        )]);
1776        assert!(tree.find_by_id("a").is_none());
1777        assert_eq!(tree.find_by_id("c").unwrap().type_name, "button");
1778    }
1779
1780    #[test]
1781    fn id_index_indexes_descendants_added_via_replace() {
1782        let mut tree = Tree::new();
1783        let _ = tree.snapshot(node("root", "column"));
1784        // Replace root with a richer subtree; every nested ID
1785        // should be reachable through find_by_id afterwards.
1786        tree.apply_patch(vec![make_patch_op(
1787            "replace_node",
1788            vec![],
1789            json!({
1790                "node": {
1791                    "id": "new_root",
1792                    "type": "column",
1793                    "props": {},
1794                    "children": [
1795                        {"id": "kid_a", "type": "text", "props": {}, "children": []},
1796                        {
1797                            "id": "kid_b",
1798                            "type": "row",
1799                            "props": {},
1800                            "children": [
1801                                {"id": "grand", "type": "text", "props": {}, "children": []}
1802                            ]
1803                        }
1804                    ]
1805                }
1806            }),
1807        )]);
1808        assert!(tree.find_by_id("root").is_none());
1809        assert_eq!(tree.find_by_id("new_root").unwrap().type_name, "column");
1810        assert_eq!(tree.find_by_id("kid_a").unwrap().type_name, "text");
1811        assert_eq!(tree.find_by_id("kid_b").unwrap().type_name, "row");
1812        assert_eq!(tree.find_by_id("grand").unwrap().type_name, "text");
1813    }
1814}