hypen_engine/reconcile/
diff.rs

1use super::{InstanceTree, Patch};
2use crate::ir::{Element, NodeId, Value};
3use crate::reactive::DependencyGraph;
4use indexmap::IndexMap;
5
6/// Reconcile an element tree against the instance tree and generate patches
7pub fn reconcile(
8    tree: &mut InstanceTree,
9    element: &Element,
10    parent_id: Option<NodeId>,
11    state: &serde_json::Value,
12    dependencies: &mut DependencyGraph,
13) -> Vec<Patch> {
14    let mut patches = Vec::new();
15
16    // For initial render, just create the tree
17    if tree.root().is_none() {
18        let node_id = create_tree(tree, element, parent_id, state, &mut patches, true, dependencies);
19        tree.set_root(node_id);
20        return patches;
21    }
22
23    // Incremental update: reconcile root against existing tree
24    if let Some(root_id) = tree.root() {
25        reconcile_node(tree, root_id, element, state, &mut patches, dependencies);
26    }
27
28    patches
29}
30
31/// Create a new subtree from an element
32pub fn create_tree(
33    tree: &mut InstanceTree,
34    element: &Element,
35    parent_id: Option<NodeId>,
36    state: &serde_json::Value,
37    patches: &mut Vec<Patch>,
38    is_root: bool,
39    dependencies: &mut DependencyGraph,
40) -> NodeId {
41    // Special handling for iterable elements (List, Grid, etc.)
42    // An element is iterable if it has BOTH:
43    // 1. prop "0" with a Binding (like @state.items) - not a static value
44    // 2. children (the template to repeat for each item)
45    // This distinguishes iterables from:
46    // - Text elements (prop "0" but no children)
47    // - Route/Link elements (prop "0" with static path, not a binding)
48    if let Some(Value::Binding(_)) = element.props.get("0") {
49        if !element.children.is_empty() {
50            return create_list_tree(tree, element, parent_id, state, patches, is_root, dependencies);
51        }
52    }
53
54    // Create the node
55    let node_id = tree.create_node(element, state);
56
57    // Register dependencies for this node
58    for value in element.props.values() {
59        match value {
60            Value::Binding(binding) => {
61                dependencies.add_dependency(node_id, binding);
62            }
63            Value::TemplateString { bindings, .. } => {
64                // Register all bindings in the template string
65                for binding in bindings {
66                    dependencies.add_dependency(node_id, binding);
67                }
68            }
69            _ => {}
70        }
71    }
72
73    // Generate Create patch
74    let node = tree.get(node_id).unwrap();
75    
76    // For lazy elements, add child component name as a prop
77    let mut props = node.props.clone();
78    if let Some(Value::Static(val)) = element.props.get("__lazy") {
79        if val.as_bool().unwrap_or(false) && !element.children.is_empty() {
80            // Store the first child's element type so renderer knows what component to load
81            let child_component = &element.children[0].element_type;
82            props.insert("__lazy_child".to_string(), serde_json::json!(child_component));
83        }
84    }
85    
86    patches.push(Patch::create(
87        node_id,
88        node.element_type.clone(),
89        props,
90    ));
91
92    // Event handling is now done at the renderer level
93
94    // If there's a parent, insert this node
95    if let Some(parent) = parent_id {
96        tree.add_child(parent, node_id, None);
97        patches.push(Patch::insert(parent, node_id, None));
98    } else if is_root {
99        // This is the root node - insert into "root" container
100        patches.push(Patch::insert_root(node_id));
101    }
102
103    // Create children (skip if element is marked as lazy)
104    let is_lazy = element.props.get("__lazy")
105        .and_then(|v| {
106            if let Value::Static(val) = v {
107                val.as_bool()
108            } else {
109                None
110            }
111        })
112        .unwrap_or(false);
113    
114    if !is_lazy {
115        for child_element in &element.children {
116            create_tree(tree, child_element, Some(node_id), state, patches, false, dependencies);
117        }
118    }
119
120    node_id
121}
122
123/// Create an iterable element (List, Grid, etc.) that iterates over an array in state
124/// Iterable elements are identified by having prop "0" with an array binding
125fn create_list_tree(
126    tree: &mut InstanceTree,
127    element: &Element,
128    parent_id: Option<NodeId>,
129    state: &serde_json::Value,
130    patches: &mut Vec<Patch>,
131    is_root: bool,
132    dependencies: &mut DependencyGraph,
133) -> NodeId {
134    // Get the array binding from first prop (prop "0")
135    let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
136        evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
137    } else {
138        serde_json::Value::Array(vec![])
139    };
140
141    // Create a container element - use the original element type (List, Grid, etc.)
142    // but remove the "0" prop since it's only for iteration, not rendering
143    let mut list_element = Element::new(&element.element_type);
144    for (key, value) in &element.props {
145        // Skip prop "0" - it's only for array binding, not for rendering
146        if key != "0" {
147            list_element.props.insert(key.clone(), value.clone());
148        }
149    }
150    
151    let node_id = tree.create_node(&list_element, state);
152
153    // Register the List node as depending on the array binding
154    // This is critical for reactive updates when the array changes
155    if let Some(Value::Binding(binding)) = element.props.get("0") {
156        dependencies.add_dependency(node_id, binding);
157    }
158
159    // Store the original element template for re-reconciliation
160    // This allows us to rebuild the list when the array changes
161    if let Some(node) = tree.get_mut(node_id) {
162        node.raw_props = element.props.clone();
163        node.element_template = Some(element.clone());
164    }
165
166    // Generate Create patch for container
167    let node = tree.get(node_id).unwrap();
168    patches.push(Patch::create(
169        node_id,
170        node.element_type.clone(),
171        node.props.clone(),
172    ));
173
174    // Insert container
175    if let Some(parent) = parent_id {
176        tree.add_child(parent, node_id, None);
177        patches.push(Patch::insert(parent, node_id, None));
178    } else if is_root {
179        patches.push(Patch::insert_root(node_id));
180    }
181
182    // If array is empty, return early
183    if let serde_json::Value::Array(items) = &array {
184        // Create children for each item in the array
185        for (index, item) in items.iter().enumerate() {
186            for child_template in &element.children {
187                // Clone child and replace ${item.x} bindings with actual item data
188                let child_with_item = replace_item_bindings(child_template, item, index);
189                create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
190            }
191        }
192    }
193
194    node_id
195}
196
197/// Navigate a nested path in a JSON value (e.g., "images.0" or "category.name")
198/// Supports both object keys and array indices
199fn navigate_item_path<'a>(item: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
200    let mut current = item;
201
202    for segment in path.split('.') {
203        // Try to parse as array index first
204        if let Ok(index) = segment.parse::<usize>() {
205            current = current.get(index)?;
206        } else {
207            // Otherwise treat as object key
208            current = current.get(segment)?;
209        }
210    }
211
212    Some(current)
213}
214
215/// Replace ${item.x} bindings with actual item values
216pub fn replace_item_bindings(element: &Element, item: &serde_json::Value, index: usize) -> Element {
217    let mut new_element = element.clone();
218    
219    // Replace bindings in props
220    let mut new_props = IndexMap::new();
221    for (key, value) in &element.props {
222        let new_value = match value {
223            Value::Binding(binding) => {
224                // Check if binding starts with "item"
225                if binding.path.first().map(|s| s.as_str()) == Some("item") {
226                    // Extract path after "item." (skip first segment)
227                    let prop_path = binding.path[1..].join(".");
228
229                    // Navigate nested path (e.g., "images.0" -> item["images"][0])
230                    if let Some(val) = navigate_item_path(item, &prop_path) {
231                        Value::Static(val.clone())
232                    } else {
233                        value.clone()
234                    }
235                } else {
236                    value.clone()
237                }
238            }
239            Value::Static(serde_json::Value::String(s)) => {
240                // Check if static string contains ${item.property} pattern
241                if s.contains("${item.") {
242                    let mut result = s.clone();
243
244                    // Find and replace all ${item.path} patterns
245                    // Simple parser: look for ${item. and find the matching }
246                    let mut pos = 0;
247                    while let Some(start) = result[pos..].find("${item.") {
248                        let abs_start = pos + start;
249                        if let Some(end) = result[abs_start..].find('}') {
250                            let abs_end = abs_start + end;
251                            // Extract the path between ${item. and }
252                            let path = &result[abs_start + 7..abs_end]; // 7 = "${item.".len()
253
254                            if let Some(val) = navigate_item_path(item, path) {
255                                let pattern = format!("${{item.{}}}", path);
256                                let replacement = match val {
257                                    serde_json::Value::String(s) => s.clone(),
258                                    serde_json::Value::Number(n) => n.to_string(),
259                                    serde_json::Value::Bool(b) => b.to_string(),
260                                    _ => serde_json::to_string(val).unwrap_or_default(),
261                                };
262                                result = result.replace(&pattern, &replacement);
263                                pos = 0; // Start over after replacement
264                            } else {
265                                pos = abs_end + 1; // Move past this pattern
266                            }
267                        } else {
268                            break; // No closing brace found
269                        }
270                    }
271
272                    Value::Static(serde_json::Value::String(result))
273                } else {
274                    value.clone()
275                }
276            }
277            _ => value.clone(),
278        };
279        new_props.insert(key.clone(), new_value);
280    }
281    new_element.props = new_props;
282    
283    // Add a key for reconciliation (use array index)
284    new_element.key = Some(format!("list-item-{}", index));
285    
286    // Recursively replace in children
287    new_element.children = element.children.iter()
288        .map(|child| replace_item_bindings(child, item, index))
289        .collect();
290    
291    new_element
292}
293
294/// Reconcile a single node with a new element
295pub fn reconcile_node(
296    tree: &mut InstanceTree,
297    node_id: NodeId,
298    element: &Element,
299    state: &serde_json::Value,
300    patches: &mut Vec<Patch>,
301    dependencies: &mut DependencyGraph,
302) {
303    let node = tree.get(node_id).cloned();
304    if node.is_none() {
305        return;
306    }
307    let node = node.unwrap();
308
309    // Special handling for iterable elements (List, Grid, etc.)
310    // An element is iterable if it has BOTH prop "0" (array binding) AND children (template)
311    // This distinguishes iterables from Text elements, which also use prop "0" but have no children
312    let is_iterable = element.props.get("0").is_some() && !element.children.is_empty();
313    
314    if is_iterable {
315        // Get the array binding from first prop (prop "0")
316        let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
317            evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
318        } else {
319            serde_json::Value::Array(vec![])
320        };
321
322        // Regenerate iterable children
323        if let serde_json::Value::Array(items) = &array {
324            let old_children = node.children.clone();
325            
326            // Calculate expected number of children
327            let expected_children_count = items.len() * element.children.len();
328            
329            // Remove old children if count doesn't match
330            if old_children.len() != expected_children_count {
331                for &old_child_id in &old_children {
332                    patches.push(Patch::remove(old_child_id));
333                }
334                
335                // Clear children from node
336                if let Some(node) = tree.get_mut(node_id) {
337                    node.children.clear();
338                }
339                
340                // Create new children
341                for (index, item) in items.iter().enumerate() {
342                    for child_template in &element.children {
343                        let child_with_item = replace_item_bindings(child_template, item, index);
344                        create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
345                    }
346                }
347            } else {
348                // Reconcile existing children
349                let mut child_index = 0;
350                for (item_index, item) in items.iter().enumerate() {
351                    for child_template in &element.children {
352                        if let Some(&old_child_id) = old_children.get(child_index) {
353                            let child_with_item = replace_item_bindings(child_template, item, item_index);
354                            reconcile_node(tree, old_child_id, &child_with_item, state, patches, dependencies);
355                        }
356                        child_index += 1;
357                    }
358                }
359            }
360        }
361        
362        return; // Done with List reconciliation
363    }
364
365    // If element type changed, replace the entire subtree
366    if node.element_type != element.element_type {
367        replace_subtree(tree, node_id, &node, element, state, patches, dependencies);
368        return;
369    }
370
371    // Register dependencies for this node (same as create_tree)
372    for value in element.props.values() {
373        if let Value::Binding(binding) = value {
374            dependencies.add_dependency(node_id, binding);
375        }
376    }
377
378    // Diff props
379    let new_props = resolve_props(&element.props, state);
380    let prop_patches = diff_props(node_id, &node.props, &new_props);
381    patches.extend(prop_patches);
382
383    // Update node props in tree
384    if let Some(node) = tree.get_mut(node_id) {
385        node.props = new_props.clone();
386        node.raw_props = element.props.clone();
387    }
388
389    // Reconcile children (skip if element is marked as lazy)
390    let is_lazy = element.props.get("__lazy")
391        .and_then(|v| {
392            if let Value::Static(val) = v {
393                val.as_bool()
394            } else {
395                None
396            }
397        })
398        .unwrap_or(false);
399    
400    if !is_lazy {
401        let old_children = node.children.clone();
402        let new_children = &element.children;
403
404        // Simple strategy: match children by index
405        for (i, new_child_element) in new_children.iter().enumerate() {
406            if let Some(&old_child_id) = old_children.get(i) {
407                // Reconcile existing child
408                reconcile_node(tree, old_child_id, new_child_element, state, patches, dependencies);
409            } else {
410                // Create new child
411                let new_child_id = create_tree(tree, new_child_element, Some(node_id), state, patches, false, dependencies);
412                if let Some(node) = tree.get_mut(node_id) {
413                    node.children.push(new_child_id);
414                }
415            }
416        }
417
418        // Remove extra old children
419        if old_children.len() > new_children.len() {
420            for &old_child_id in &old_children[new_children.len()..] {
421                patches.push(Patch::remove(old_child_id));
422                // TODO: Remove from tree
423            }
424        }
425    }
426}
427
428/// Replace an entire subtree when element types don't match.
429/// This removes the old node and its descendants, then creates a new subtree in its place.
430fn replace_subtree(
431    tree: &mut InstanceTree,
432    old_node_id: NodeId,
433    old_node: &super::InstanceNode,
434    new_element: &Element,
435    state: &serde_json::Value,
436    patches: &mut Vec<Patch>,
437    dependencies: &mut DependencyGraph,
438) {
439    let parent_id = old_node.parent;
440
441    // Remember the position of the old node in its parent's children list
442    let old_position = if let Some(pid) = parent_id {
443        tree.get(pid).and_then(|parent| {
444            parent.children.iter().position(|&id| id == old_node_id)
445        })
446    } else {
447        None
448    };
449
450    // Collect all node IDs in the old subtree (depth-first, children before parents)
451    let ids_to_remove = collect_subtree_ids(tree, old_node_id);
452
453    // Generate Remove patches and clear dependencies in one pass
454    for &id in &ids_to_remove {
455        patches.push(Patch::remove(id));
456        dependencies.remove_node(id);
457    }
458
459    // Remove old node from parent's children list
460    if let Some(pid) = parent_id {
461        if let Some(parent) = tree.get_mut(pid) {
462            parent.children.retain(|&id| id != old_node_id);
463        }
464    }
465
466    // Remove the old subtree from the tree
467    tree.remove(old_node_id);
468
469    // Create the new subtree
470    let is_root = parent_id.is_none();
471    let new_node_id = create_tree(tree, new_element, parent_id, state, patches, is_root, dependencies);
472
473    // If this was the root, update it
474    if is_root {
475        tree.set_root(new_node_id);
476    } else if let Some(pid) = parent_id {
477        if let Some(pos) = old_position {
478            if let Some(parent) = tree.get_mut(pid) {
479                let current_len = parent.children.len();
480                // create_tree appended at end, so new node is at current_len - 1
481                // We want it at position `pos`
482                if pos < current_len - 1 {
483                    // Pop from end (O(1)) and insert at correct position (O(n))
484                    let new_id = parent.children.pop().unwrap();
485                    parent.children.insert(pos, new_id);
486
487                    // Get the next sibling for the Move patch
488                    let next_sibling = parent.children.get(pos + 1).copied();
489                    patches.push(Patch::move_node(pid, new_node_id, next_sibling));
490                }
491                // If pos == current_len - 1, it's already in the right place (was last child)
492            }
493        }
494    }
495}
496
497/// Collect all node IDs in a subtree (post-order: children before parents)
498/// Uses iterative approach to avoid stack overflow on deep trees
499fn collect_subtree_ids(tree: &InstanceTree, root_id: NodeId) -> Vec<NodeId> {
500    let mut result = Vec::new();
501    let mut stack: Vec<(NodeId, bool)> = vec![(root_id, false)];
502
503    while let Some((node_id, children_processed)) = stack.pop() {
504        if children_processed {
505            // Children already processed, add this node
506            result.push(node_id);
507        } else {
508            // Push self back with flag, then push children
509            stack.push((node_id, true));
510            if let Some(node) = tree.get(node_id) {
511                // Push children in reverse order so they're processed left-to-right
512                for &child_id in node.children.iter().rev() {
513                    stack.push((child_id, false));
514                }
515            }
516        }
517    }
518
519    result
520}
521
522/// Resolve props by evaluating bindings against state
523fn resolve_props(props: &IndexMap<String, crate::ir::Value>, state: &serde_json::Value) -> IndexMap<String, serde_json::Value> {
524    let mut resolved = IndexMap::new();
525
526    for (key, value) in props {
527        let resolved_value = match value {
528            crate::ir::Value::Static(v) => v.clone(),
529            crate::ir::Value::Binding(binding) => {
530                // Evaluate binding against state using parsed path
531                evaluate_binding(binding, state).unwrap_or(serde_json::Value::Null)
532            }
533            crate::ir::Value::TemplateString { template, bindings } => {
534                // Interpolate all bindings in the template string
535                let mut result = template.clone();
536                for binding in bindings {
537                    let binding_placeholder = format!("${{state.{}}}", binding.full_path());
538                    let value = evaluate_binding(binding, state)
539                        .map(|v| match v {
540                            serde_json::Value::String(s) => s,
541                            other => other.to_string(),
542                        })
543                        .unwrap_or_default();
544                    result = result.replace(&binding_placeholder, &value);
545                }
546                serde_json::Value::String(result)
547            }
548            crate::ir::Value::Action(action) => {
549                // Actions are serialized with @ prefix for renderer to detect
550                serde_json::Value::String(format!("@{}", action))
551            }
552        };
553        resolved.insert(key.clone(), resolved_value);
554    }
555
556    resolved
557}
558
559/// Evaluate a binding against state using its parsed path
560fn evaluate_binding(binding: &crate::reactive::Binding, state: &serde_json::Value) -> Option<serde_json::Value> {
561    let mut current = state;
562
563    for segment in &binding.path {
564        current = current.get(segment)?;
565    }
566
567    Some(current.clone())
568}
569
570/// Reconcile children using keys for efficient updates
571fn reconcile_keyed_children(
572    tree: &mut InstanceTree,
573    parent_id: NodeId,
574    old_children: &[NodeId],
575    new_children: &[Element],
576    state: &serde_json::Value,
577    dependencies: &mut DependencyGraph,
578) -> Vec<Patch> {
579    let mut patches = Vec::new();
580
581    // Build a map of old children by key
582    let mut old_keyed: IndexMap<String, NodeId> = IndexMap::new();
583    let mut old_unkeyed: Vec<NodeId> = Vec::new();
584
585    for &child_id in old_children {
586        if let Some(node) = tree.get(child_id) {
587            if let Some(key) = &node.key {
588                old_keyed.insert(key.clone(), child_id);
589            } else {
590                old_unkeyed.push(child_id);
591            }
592        }
593    }
594
595    let mut new_child_ids: Vec<NodeId> = Vec::new();
596    let mut unkeyed_idx = 0;
597
598    // First pass: match children by key or create new ones
599    for new_element in new_children {
600        let new_key = new_element.key.as_ref();
601
602        let child_id = if let Some(key) = new_key {
603            // Try to find matching keyed child
604            if let Some(&old_id) = old_keyed.get(key) {
605                // Found a match - reconcile it
606                reconcile_node(tree, old_id, new_element, state, &mut patches, dependencies);
607                old_keyed.shift_remove(key); // Mark as used
608                old_id
609            } else {
610                // No match - create new child
611                create_tree(tree, new_element, Some(parent_id), state, &mut patches, false, dependencies)
612            }
613        } else {
614            // Unkeyed child - try to reuse unkeyed old child
615            if let Some(&old_id) = old_unkeyed.get(unkeyed_idx) {
616                reconcile_node(tree, old_id, new_element, state, &mut patches, dependencies);
617                unkeyed_idx += 1;
618                old_id
619            } else {
620                // No unkeyed child available - create new
621                create_tree(tree, new_element, Some(parent_id), state, &mut patches, false, dependencies)
622            }
623        };
624
625        new_child_ids.push(child_id);
626    }
627
628    // Second pass: generate move/insert patches using LIS algorithm
629    // Build list of old positions for children that were reused
630    let mut old_positions: Vec<Option<usize>> = Vec::new();
631    for &new_id in &new_child_ids {
632        let old_pos = old_children.iter().position(|&old_id| old_id == new_id);
633        old_positions.push(old_pos);
634    }
635
636    // Find longest increasing subsequence to minimize moves
637    let lis = longest_increasing_subsequence(&old_positions);
638
639    // Update parent's children list
640    if let Some(parent_node) = tree.get_mut(parent_id) {
641        parent_node.children = new_child_ids.clone();
642    }
643
644    // Generate move/insert patches for children not in LIS
645    for (new_idx, &child_id) in new_child_ids.iter().enumerate() {
646        // Check if this child needs to be moved
647        if !lis.contains(&new_idx) {
648            // Calculate before_id (the next child in the list, or None for last position)
649            let before_id = new_child_ids.get(new_idx + 1).copied();
650            patches.push(Patch::move_node(parent_id, child_id, before_id));
651        }
652    }
653
654    // Third pass: remove old children that weren't reused
655    for old_id in old_keyed.values() {
656        patches.push(Patch::remove(*old_id));
657    }
658    for &old_id in &old_unkeyed[unkeyed_idx..] {
659        patches.push(Patch::remove(old_id));
660    }
661
662    patches
663}
664
665/// Find the longest increasing subsequence (LIS) indices
666/// This is used to minimize DOM moves during reconciliation
667/// Returns indices of elements that are already in correct relative order
668fn longest_increasing_subsequence(positions: &[Option<usize>]) -> Vec<usize> {
669    // Extract valid positions with their indices
670    let valid: Vec<(usize, usize)> = positions
671        .iter()
672        .enumerate()
673        .filter_map(|(idx, &pos)| pos.map(|p| (idx, p)))
674        .collect();
675
676    if valid.is_empty() {
677        return Vec::new();
678    }
679
680    // Use dynamic programming to find LIS
681    let n = valid.len();
682    let mut dp: Vec<usize> = vec![1; n];
683    let mut prev: Vec<Option<usize>> = vec![None; n];
684
685    for i in 1..n {
686        for j in 0..i {
687            if valid[j].1 < valid[i].1 && dp[j] + 1 > dp[i] {
688                dp[i] = dp[j] + 1;
689                prev[i] = Some(j);
690            }
691        }
692    }
693
694    // Find the index with maximum LIS length
695    let mut max_idx = 0;
696    for i in 1..n {
697        if dp[i] > dp[max_idx] {
698            max_idx = i;
699        }
700    }
701
702    // Reconstruct the LIS indices
703    let mut result = Vec::new();
704    let mut idx = Some(max_idx);
705    while let Some(i) = idx {
706        result.push(valid[i].0);
707        idx = prev[i];
708    }
709    result.reverse();
710
711    result
712}
713
714/// Diff two sets of props and generate SetProp patches
715pub fn diff_props(
716    node_id: NodeId,
717    old_props: &IndexMap<String, serde_json::Value>,
718    new_props: &IndexMap<String, serde_json::Value>,
719) -> Vec<Patch> {
720    let mut patches = Vec::new();
721
722    // Check for changed or new props
723    for (key, new_value) in new_props {
724        if old_props.get(key) != Some(new_value) {
725            patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
726        }
727    }
728
729    // Note: We don't remove old props, just set new ones
730    // Platforms should handle prop removal if needed
731
732    patches
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738    use crate::ir::Value;
739    use serde_json::json;
740
741    #[test]
742    fn test_create_simple_tree() {
743        use crate::reactive::DependencyGraph;
744
745        let mut tree = InstanceTree::new();
746        let mut patches = Vec::new();
747        let mut dependencies = DependencyGraph::new();
748
749        let element = Element::new("Column")
750            .with_child(Element::new("Text").with_prop("text", Value::Static(json!("Hello"))));
751
752        let state = json!({});
753        create_tree(&mut tree, &element, None, &state, &mut patches, true, &mut dependencies);
754
755        // Should create 2 nodes (Column + Text) + 2 Inserts (root + child)
756        // Create Column, Insert Column into root, Create Text, Insert Text into Column
757        assert_eq!(patches.len(), 4);
758
759        // Verify root insert patch exists
760        let root_insert = patches.iter().find(|p| {
761            matches!(p, Patch::Insert { parent_id, .. } if parent_id == "root")
762        });
763        assert!(root_insert.is_some(), "Root insert patch should exist");
764    }
765
766    #[test]
767    fn test_diff_props() {
768        let node_id = NodeId::default();
769        let old = indexmap::indexmap! {
770            "color".to_string() => json!("red"),
771            "size".to_string() => json!(16),
772        };
773        let new = indexmap::indexmap! {
774            "color".to_string() => json!("blue"),
775            "size".to_string() => json!(16),
776        };
777
778        let patches = diff_props(node_id, &old, &new);
779
780        // Only color changed
781        assert_eq!(patches.len(), 1);
782    }
783
784    #[test]
785    fn test_longest_increasing_subsequence_basic() {
786        // Test case: [0, 1, 2, 3] - already in order, should select all
787        let positions = vec![Some(0), Some(1), Some(2), Some(3)];
788        let lis = longest_increasing_subsequence(&positions);
789        assert_eq!(lis, vec![0, 1, 2, 3]);
790    }
791
792    #[test]
793    fn test_longest_increasing_subsequence_reverse() {
794        // Test case: [3, 2, 1, 0] - reversed, should select only one element
795        let positions = vec![Some(3), Some(2), Some(1), Some(0)];
796        let lis = longest_increasing_subsequence(&positions);
797        assert_eq!(lis.len(), 1);
798    }
799
800    #[test]
801    fn test_longest_increasing_subsequence_mixed() {
802        // Test case: [0, 3, 1, 2] - should select [0, 1, 2]
803        let positions = vec![Some(0), Some(3), Some(1), Some(2)];
804        let lis = longest_increasing_subsequence(&positions);
805        assert_eq!(lis.len(), 3);
806        // Should contain indices of 0, 1, 2 in the original array
807        assert!(lis.contains(&0));
808        assert!(lis.contains(&2));
809        assert!(lis.contains(&3));
810    }
811
812    #[test]
813    fn test_longest_increasing_subsequence_with_new_items() {
814        // Test case: [None, Some(0), None, Some(1)] - new items mixed with existing
815        let positions = vec![None, Some(0), None, Some(1)];
816        let lis = longest_increasing_subsequence(&positions);
817        assert_eq!(lis, vec![1, 3]); // Only indices with existing positions
818    }
819
820    #[test]
821    fn test_longest_increasing_subsequence_empty() {
822        let positions: Vec<Option<usize>> = vec![];
823        let lis = longest_increasing_subsequence(&positions);
824        assert_eq!(lis, Vec::<usize>::new());
825    }
826
827    #[test]
828    fn test_longest_increasing_subsequence_all_new() {
829        // All new items (no old positions)
830        let positions = vec![None, None, None];
831        let lis = longest_increasing_subsequence(&positions);
832        assert_eq!(lis, Vec::<usize>::new());
833    }
834
835    #[test]
836    fn test_reconcile_keyed_children_basic() {
837        let mut tree = InstanceTree::new();
838        let mut dependencies = DependencyGraph::new();
839        let state = json!({});
840
841        // Create a parent node
842        let parent = Element::new("Column");
843        let parent_id = tree.create_node(&parent, &state);
844
845        // Create old children
846        let mut old_child_1 = Element::new("Text").with_prop("text", Value::Static(json!("Item 1")));
847        old_child_1.key = Some("item-1".to_string());
848        let old_id_1 = tree.create_node(&old_child_1, &state);
849
850        let mut old_child_2 = Element::new("Text").with_prop("text", Value::Static(json!("Item 2")));
851        old_child_2.key = Some("item-2".to_string());
852        let old_id_2 = tree.create_node(&old_child_2, &state);
853
854        tree.add_child(parent_id, old_id_1, None);
855        tree.add_child(parent_id, old_id_2, None);
856
857        // New children in reversed order
858        let mut new_child_2 = Element::new("Text").with_prop("text", Value::Static(json!("Item 2")));
859        new_child_2.key = Some("item-2".to_string());
860
861        let mut new_child_1 = Element::new("Text").with_prop("text", Value::Static(json!("Item 1")));
862        new_child_1.key = Some("item-1".to_string());
863
864        let new_children = vec![new_child_2, new_child_1];
865        let old_children = vec![old_id_1, old_id_2];
866
867        let patches = reconcile_keyed_children(&mut tree, parent_id, &old_children, &new_children, &state, &mut dependencies);
868
869        // Should have move patches but no create/remove patches (children are reused)
870        let move_count = patches.iter().filter(|p| matches!(p, Patch::Move { .. })).count();
871        let create_count = patches.iter().filter(|p| matches!(p, Patch::Create { .. })).count();
872        let remove_count = patches.iter().filter(|p| matches!(p, Patch::Remove { .. })).count();
873
874        assert_eq!(create_count, 0, "Should not create new children");
875        assert_eq!(remove_count, 0, "Should not remove any children");
876        assert!(move_count > 0, "Should have move patches for reordering");
877    }
878
879    #[test]
880    fn test_reconcile_keyed_children_add_remove() {
881        let mut tree = InstanceTree::new();
882        let mut dependencies = DependencyGraph::new();
883        let state = json!({});
884
885        let parent = Element::new("Column");
886        let parent_id = tree.create_node(&parent, &state);
887
888        // Old: item-1, item-2
889        let mut old_child_1 = Element::new("Text");
890        old_child_1.key = Some("item-1".to_string());
891        let old_id_1 = tree.create_node(&old_child_1, &state);
892
893        let mut old_child_2 = Element::new("Text");
894        old_child_2.key = Some("item-2".to_string());
895        let old_id_2 = tree.create_node(&old_child_2, &state);
896
897        tree.add_child(parent_id, old_id_1, None);
898        tree.add_child(parent_id, old_id_2, None);
899
900        // New: item-2, item-3 (removed item-1, added item-3)
901        let mut new_child_2 = Element::new("Text");
902        new_child_2.key = Some("item-2".to_string());
903
904        let mut new_child_3 = Element::new("Text");
905        new_child_3.key = Some("item-3".to_string());
906
907        let new_children = vec![new_child_2, new_child_3];
908        let old_children = vec![old_id_1, old_id_2];
909
910        let patches = reconcile_keyed_children(&mut tree, parent_id, &old_children, &new_children, &state, &mut dependencies);
911
912        let create_count = patches.iter().filter(|p| matches!(p, Patch::Create { .. })).count();
913        let remove_count = patches.iter().filter(|p| matches!(p, Patch::Remove { .. })).count();
914
915        assert_eq!(create_count, 1, "Should create item-3");
916        assert_eq!(remove_count, 1, "Should remove item-1");
917    }
918}