1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.item.node;
7   
8   import static org.junit.jupiter.api.Assertions.assertEquals;
9   import static org.junit.jupiter.api.Assertions.assertNotNull;
10  import static org.junit.jupiter.api.Assertions.assertNull;
11  import static org.junit.jupiter.api.Assertions.assertTrue;
12  
13  import org.junit.jupiter.api.Test;
14  
15  import java.net.URI;
16  import java.util.ArrayList;
17  import java.util.List;
18  import java.util.stream.Collectors;
19  
20  import dev.metaschema.core.mdm.IDMAssemblyNodeItem;
21  import dev.metaschema.core.mdm.IDMDocumentNodeItem;
22  import dev.metaschema.core.mdm.IDMFieldNodeItem;
23  import dev.metaschema.core.metapath.StaticContext;
24  import dev.metaschema.core.metapath.item.atomic.IStringItem;
25  import dev.metaschema.core.model.IAssemblyDefinition;
26  import dev.metaschema.core.model.IAssemblyInstance;
27  import dev.metaschema.core.model.IFieldInstance;
28  import dev.metaschema.core.model.IFlagInstance;
29  import dev.metaschema.core.model.IModule;
30  import dev.metaschema.core.model.ISource;
31  import dev.metaschema.core.qname.IEnhancedQName;
32  import dev.metaschema.core.testsupport.MockedModelTestSupport;
33  import dev.metaschema.core.testsupport.builder.IFieldBuilder;
34  import dev.metaschema.core.testsupport.builder.IModuleBuilder;
35  import dev.metaschema.core.util.ObjectUtils;
36  
37  /**
38   * Comprehensive tests for Metapath node item traversal and navigation.
39   */
40  class NodeItemTraversalTest {
41  
42    private static final String TEST_NAMESPACE = "http://example.com/ns/traversal-test";
43    private static final URI DOCUMENT_URI = URI.create("http://example.com/test-doc.xml");
44  
45    /**
46     * Test creating a document node item with a root assembly.
47     */
48    @Test
49    void testCreateDocumentNodeItem() {
50      MockedModelTestSupport mocking = new MockedModelTestSupport();
51      ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
52  
53      IModule module = IModuleBuilder.builder()
54          .namespace(TEST_NAMESPACE)
55          .shortName("traversal-test")
56          .version("1.0.0")
57          .source(source)
58          .assembly(mocking.assembly()
59              .name("root-assembly")
60              .rootName("root-assembly"))
61          .toModule();
62  
63      IAssemblyDefinition rootDef = module.getRootAssemblyDefinitions().iterator().next();
64      assertNotNull(rootDef, "Root assembly definition should exist");
65  
66      // Create a document node item
67      IDMDocumentNodeItem document = IDMDocumentNodeItem.newInstance(DOCUMENT_URI, rootDef);
68  
69      assertNotNull(document, "Document node item should be created");
70      assertEquals(INodeItem.NodeType.DOCUMENT, document.getNodeType(), "Should be a document node");
71      assertEquals(DOCUMENT_URI, document.getDocumentUri(), "Document URI should match");
72      assertNull(document.getParentNodeItem(), "Document should have no parent");
73  
74      IRootAssemblyNodeItem rootAssembly = document.getRootAssemblyNodeItem();
75      assertNotNull(rootAssembly, "Root assembly should exist");
76      assertEquals(INodeItem.NodeType.ASSEMBLY, rootAssembly.getNodeType(), "Should be an assembly node");
77      assertEquals(document, rootAssembly.getParentNodeItem(), "Root assembly parent should be the document");
78    }
79  
80    /**
81     * Test creating assembly node items with flags.
82     */
83    @Test
84    void testCreateAssemblyWithFlags() {
85      MockedModelTestSupport mocking = new MockedModelTestSupport();
86      ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
87  
88      IModule module = IModuleBuilder.builder()
89          .namespace(TEST_NAMESPACE)
90          .shortName("traversal-test")
91          .version("1.0.0")
92          .source(source)
93          .assembly(mocking.assembly()
94              .name("test-assembly")
95              .rootName("test-assembly")
96              .flags(List.of(
97                  mocking.flag().namespace(TEST_NAMESPACE).name("flag1"),
98                  mocking.flag().namespace(TEST_NAMESPACE).name("flag2"))))
99          .toModule();
100 
101     IAssemblyDefinition assemblyDef = module.getRootAssemblyDefinitions().iterator().next();
102     StaticContext staticContext = StaticContext.instance();
103     IDMAssemblyNodeItem assembly = IDMAssemblyNodeItem.newInstance(assemblyDef, staticContext);
104 
105     // Add flag values - find flag instances by name to ensure correct mapping
106     IFlagInstance flag1Instance = assemblyDef.getFlagInstanceByName(
107         IEnhancedQName.of(TEST_NAMESPACE, "flag1").getIndexPosition());
108     IFlagInstance flag2Instance = assemblyDef.getFlagInstanceByName(
109         IEnhancedQName.of(TEST_NAMESPACE, "flag2").getIndexPosition());
110     assertNotNull(flag1Instance, "flag1 instance should exist");
111     assertNotNull(flag2Instance, "flag2 instance should exist");
112 
113     assembly.newFlag(flag1Instance, IStringItem.valueOf("value1"));
114     assembly.newFlag(flag2Instance, IStringItem.valueOf("value2"));
115 
116     // Verify flags can be accessed
117     List<? extends IFlagNodeItem> flags = assembly.getFlags().stream().collect(Collectors.toList());
118     assertEquals(2, flags.size(), "Should have 2 flags");
119 
120     // Verify flag lookup by name
121     IFlagNodeItem flag1 = assembly.getFlagByName(IEnhancedQName.of(TEST_NAMESPACE, "flag1"));
122     assertNotNull(flag1, "Flag1 should be found by name");
123     assertEquals("value1", flag1.toAtomicItem().asString(), "Flag1 value should match");
124 
125     IFlagNodeItem flag2 = assembly.getFlagByName(IEnhancedQName.of(TEST_NAMESPACE, "flag2"));
126     assertNotNull(flag2, "Flag2 should be found by name");
127     assertEquals("value2", flag2.toAtomicItem().asString(), "Flag2 value should match");
128 
129     // Verify flag parent is the assembly
130     assertEquals(assembly, flag1.getParentNodeItem(), "Flag parent should be the assembly");
131     assertEquals(INodeItem.NodeType.FLAG, flag1.getNodeType(), "Should be a flag node");
132   }
133 
134   /**
135    * Test parent-child navigation between assembly and field nodes.
136    */
137   @Test
138   void testParentChildNavigation() {
139     MockedModelTestSupport mocking = new MockedModelTestSupport();
140     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
141 
142     IModule module = IModuleBuilder.builder()
143         .namespace(TEST_NAMESPACE)
144         .shortName("traversal-test")
145         .version("1.0.0")
146         .source(source)
147         .assembly(mocking.assembly()
148             .name("parent-assembly")
149             .rootName("parent-assembly")
150             .modelInstances(List.of(
151                 mocking.field().namespace(TEST_NAMESPACE).name("child-field"))))
152         .toModule();
153 
154     IAssemblyDefinition assemblyDef = module.getRootAssemblyDefinitions().iterator().next();
155     StaticContext staticContext = StaticContext.instance();
156     IDMAssemblyNodeItem assembly = IDMAssemblyNodeItem.newInstance(assemblyDef, staticContext);
157 
158     // Add a child field
159     List<IFieldInstance> fieldInstances = new ArrayList<>(assemblyDef.getFieldInstances());
160     assertEquals(1, fieldInstances.size(), "Should have 1 field instance");
161 
162     IDMFieldNodeItem field = assembly.newField(fieldInstances.get(0), IStringItem.valueOf("field-value"));
163 
164     // Test parent-child relationships
165     assertEquals(assembly, field.getParentNodeItem(), "Field parent should be the assembly");
166     assertEquals(assembly, field.getParentContentNodeItem(), "Field parent content node should be the assembly");
167 
168     List<? extends IModelNodeItem<?, ?>> modelItems = assembly.modelItems().collect(Collectors.toList());
169     assertEquals(1, modelItems.size(), "Assembly should have 1 child model item");
170     assertEquals(field, modelItems.get(0), "Child should be the field we added");
171 
172     assertEquals(INodeItem.NodeType.FIELD, field.getNodeType(), "Should be a field node");
173     assertEquals(INodeItem.NodeType.ASSEMBLY, assembly.getNodeType(), "Should be an assembly node");
174   }
175 
176   /**
177    * Test ancestor axis traversal.
178    */
179   @Test
180   void testAncestorAxis() {
181     MockedModelTestSupport mocking = new MockedModelTestSupport();
182     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
183 
184     IModule module = IModuleBuilder.builder()
185         .namespace(TEST_NAMESPACE)
186         .shortName("traversal-test")
187         .version("1.0.0")
188         .source(source)
189         .assembly(mocking.assembly()
190             .name("grandparent-assembly")
191             .rootName("grandparent-assembly")
192             .modelInstances(List.of(
193                 mocking.assemblyRef("child-assembly"))))
194         .assembly(mocking.assembly()
195             .name("child-assembly")
196             .modelInstances(List.of(
197                 mocking.field().namespace(TEST_NAMESPACE).name("grandchild-field"))))
198         .toModule();
199 
200     IAssemblyDefinition grandparentDef = module.getRootAssemblyDefinitions().iterator().next();
201     IDMDocumentNodeItem document = IDMDocumentNodeItem.newInstance(DOCUMENT_URI, grandparentDef);
202     IRootAssemblyNodeItem grandparent = document.getRootAssemblyNodeItem();
203 
204     // Add child assembly
205     IAssemblyInstance childAssemblyInstance = grandparentDef.getAssemblyInstances().iterator().next();
206     IDMAssemblyNodeItem child = ((IDMAssemblyNodeItem) grandparent).newAssembly(childAssemblyInstance);
207 
208     // Add grandchild field
209     IFieldInstance grandchildFieldInstance = child.getDefinition().getFieldInstances().iterator().next();
210     IDMFieldNodeItem grandchild = child.newField(grandchildFieldInstance, IStringItem.valueOf("test-value"));
211 
212     // Test ancestor axis from grandchild
213     // Note: ancestor() returns ancestors in document order (farthest to nearest)
214     List<? extends INodeItem> ancestors = grandchild.ancestor().collect(Collectors.toList());
215     assertEquals(3, ancestors.size(), "Grandchild should have 3 ancestors: document, grandparent, child");
216     // Verify ancestors are in document order (root first)
217     assertEquals(document, ancestors.get(0), "First ancestor should be the document (farthest)");
218     assertEquals(grandparent, ancestors.get(1), "Second ancestor should be the grandparent assembly");
219     assertEquals(child, ancestors.get(2), "Third ancestor should be the child assembly (nearest)");
220 
221     // Test ancestor-or-self axis
222     // Note: ancestorOrSelf() returns ancestors then self (self is last)
223     List<? extends INodeItem> ancestorsOrSelf = grandchild.ancestorOrSelf().collect(Collectors.toList());
224     assertEquals(4, ancestorsOrSelf.size(), "Should have 4 items: 3 ancestors + self");
225     assertEquals(grandchild, ancestorsOrSelf.get(3), "Last item should be self");
226   }
227 
228   /**
229    * Test descendant axis traversal.
230    */
231   @Test
232   void testDescendantAxis() {
233     MockedModelTestSupport mocking = new MockedModelTestSupport();
234     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
235 
236     IModule module = IModuleBuilder.builder()
237         .namespace(TEST_NAMESPACE)
238         .shortName("traversal-test")
239         .version("1.0.0")
240         .source(source)
241         .assembly(mocking.assembly()
242             .name("parent-assembly")
243             .rootName("parent-assembly")
244             .modelInstances(List.of(
245                 mocking.field().namespace(TEST_NAMESPACE).name("field1"),
246                 mocking.field().namespace(TEST_NAMESPACE).name("field2"),
247                 mocking.assemblyRef("child-assembly"))))
248         .assembly(mocking.assembly()
249             .name("child-assembly")
250             .modelInstances(List.of(
251                 mocking.field().namespace(TEST_NAMESPACE).name("nested-field"))))
252         .toModule();
253 
254     IAssemblyDefinition parentDef = module.getRootAssemblyDefinitions().iterator().next();
255     StaticContext staticContext = StaticContext.instance();
256     IDMAssemblyNodeItem parent = IDMAssemblyNodeItem.newInstance(parentDef, staticContext);
257 
258     // Add child fields and assembly - look up by name to ensure correct mapping
259     IFieldInstance field1Instance = parentDef.getModelInstances().stream()
260         .filter(instance -> instance instanceof IFieldInstance)
261         .map(instance -> (IFieldInstance) instance)
262         .filter(instance -> "field1".equals(instance.getName()))
263         .findFirst()
264         .orElseThrow(() -> new AssertionError("field1 instance not found"));
265     IFieldInstance field2Instance = parentDef.getModelInstances().stream()
266         .filter(instance -> instance instanceof IFieldInstance)
267         .map(instance -> (IFieldInstance) instance)
268         .filter(instance -> "field2".equals(instance.getName()))
269         .findFirst()
270         .orElseThrow(() -> new AssertionError("field2 instance not found"));
271 
272     parent.newField(field1Instance, IStringItem.valueOf("value1"));
273     parent.newField(field2Instance, IStringItem.valueOf("value2"));
274 
275     IAssemblyInstance childAssemblyInstance = parentDef.getAssemblyInstances().iterator().next();
276     IDMAssemblyNodeItem childAssembly = parent.newAssembly(childAssemblyInstance);
277 
278     IFieldInstance nestedFieldInstance = childAssembly.getDefinition().getFieldInstances().iterator().next();
279     childAssembly.newField(nestedFieldInstance, IStringItem.valueOf("nested-value"));
280 
281     // Test descendant axis - should find all descendants in depth-first order
282     List<? extends IModelNodeItem<?, ?>> descendants = parent.descendant().collect(Collectors.toList());
283     assertEquals(4, descendants.size(), "Should have 4 descendants: field1, field2, child-assembly, nested-field");
284 
285     // Count descendant types - the exact order depends on modelItems() ordering
286     long fieldCount = descendants.stream()
287         .filter(item -> item.getNodeType() == INodeItem.NodeType.FIELD)
288         .count();
289     long assemblyCount = descendants.stream()
290         .filter(item -> item.getNodeType() == INodeItem.NodeType.ASSEMBLY)
291         .count();
292     assertEquals(3, fieldCount, "Should have 3 field descendants (field1, field2, nested-field)");
293     assertEquals(1, assemblyCount, "Should have 1 assembly descendant (child-assembly)");
294 
295     // Verify the last field is the nested-field (child of the child-assembly)
296     // by checking its parent is an assembly
297     IModelNodeItem<?, ?> nestedField = descendants.stream()
298         .filter(item -> item.getNodeType() == INodeItem.NodeType.FIELD)
299         .filter(item -> item.getParentNodeItem() != parent)
300         .findFirst()
301         .orElse(null);
302     assertNotNull(nestedField, "Should have a nested field under child-assembly");
303     assertEquals(INodeItem.NodeType.ASSEMBLY, nestedField.getParentNodeItem().getNodeType(),
304         "Nested field's parent should be the child assembly");
305 
306     // Test descendant-or-self axis
307     List<? extends INodeItem> descendantsOrSelf = parent.descendantOrSelf().collect(Collectors.toList());
308     assertEquals(5, descendantsOrSelf.size(), "Should have 5 items: self + 4 descendants");
309     assertEquals(parent, descendantsOrSelf.get(0), "First item should be self");
310   }
311 
312   /**
313    * Test flag access on field and assembly nodes.
314    */
315   @Test
316   void testFlagAccessOnFieldsAndAssemblies() {
317     MockedModelTestSupport mocking = new MockedModelTestSupport();
318     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
319 
320     // Create field definition separately so we can configure flags on it properly
321     IFieldBuilder fieldBuilder = mocking.field()
322         .namespace(TEST_NAMESPACE)
323         .name("test-field")
324         .source(source)
325         .flags(List.of(
326             mocking.flag().namespace(TEST_NAMESPACE).name("field-flag").source(source)));
327 
328     IModule module = IModuleBuilder.builder()
329         .namespace(TEST_NAMESPACE)
330         .shortName("traversal-test")
331         .version("1.0.0")
332         .source(source)
333         .assembly(mocking.assembly()
334             .name("test-assembly")
335             .rootName("test-assembly")
336             .source(source)
337             .flags(List.of(
338                 mocking.flag().namespace(TEST_NAMESPACE).name("assembly-flag").source(source)))
339             .modelInstances(List.of(fieldBuilder)))
340         .toModule();
341 
342     IAssemblyDefinition assemblyDef = module.getRootAssemblyDefinitions().iterator().next();
343     StaticContext staticContext = StaticContext.instance();
344     IDMAssemblyNodeItem assembly = IDMAssemblyNodeItem.newInstance(assemblyDef, staticContext);
345 
346     // Add assembly flag
347     List<IFlagInstance> assemblyFlags = new ArrayList<>(assemblyDef.getFlagInstances());
348     assertEquals(1, assemblyFlags.size(), "Assembly should have 1 flag");
349     assembly.newFlag(assemblyFlags.get(0), IStringItem.valueOf("assembly-flag-value"));
350 
351     // Add field with flag
352     IFieldInstance fieldInstance = assemblyDef.getFieldInstances().iterator().next();
353     IDMFieldNodeItem field = assembly.newField(fieldInstance, IStringItem.valueOf("field-value"));
354 
355     List<IFlagInstance> fieldFlags = new ArrayList<>(fieldInstance.getDefinition().getFlagInstances());
356     assertEquals(1, fieldFlags.size(), "Field should have 1 flag");
357     field.newFlag(fieldFlags.get(0), IStringItem.valueOf("field-flag-value"));
358 
359     // Test flag access on assembly
360     List<? extends IFlagNodeItem> assemblyFlagItems = assembly.flags().collect(Collectors.toList());
361     assertEquals(1, assemblyFlagItems.size(), "Assembly should have 1 flag item");
362     assertEquals("assembly-flag-value", assemblyFlagItems.get(0).toAtomicItem().asString(),
363         "Assembly flag value should match");
364 
365     // Test flag access on field
366     List<? extends IFlagNodeItem> fieldFlagItems = field.flags().collect(Collectors.toList());
367     assertEquals(1, fieldFlagItems.size(), "Field should have 1 flag item");
368     assertEquals("field-flag-value", fieldFlagItems.get(0).toAtomicItem().asString(),
369         "Field flag value should match");
370   }
371 
372   /**
373    * Test node type identification using getNodeType().
374    */
375   @Test
376   void testNodeTypeIdentification() {
377     MockedModelTestSupport mocking = new MockedModelTestSupport();
378     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
379 
380     IModule module = IModuleBuilder.builder()
381         .namespace(TEST_NAMESPACE)
382         .shortName("traversal-test")
383         .version("1.0.0")
384         .source(source)
385         .assembly(mocking.assembly()
386             .name("test-assembly")
387             .rootName("test-assembly")
388             .flags(List.of(
389                 mocking.flag().namespace(TEST_NAMESPACE).name("test-flag")))
390             .modelInstances(List.of(
391                 mocking.field().namespace(TEST_NAMESPACE).name("test-field"))))
392         .toModule();
393 
394     IAssemblyDefinition assemblyDef = module.getRootAssemblyDefinitions().iterator().next();
395     IDMDocumentNodeItem document = IDMDocumentNodeItem.newInstance(DOCUMENT_URI, assemblyDef);
396     IRootAssemblyNodeItem assembly = document.getRootAssemblyNodeItem();
397 
398     // Add flag
399     List<IFlagInstance> flagInstances = new ArrayList<>(assemblyDef.getFlagInstances());
400     ((IDMAssemblyNodeItem) assembly).newFlag(flagInstances.get(0), IStringItem.valueOf("flag-value"));
401 
402     // Add field
403     IFieldInstance fieldInstance = assemblyDef.getFieldInstances().iterator().next();
404     IDMFieldNodeItem field = ((IDMAssemblyNodeItem) assembly).newField(fieldInstance,
405         IStringItem.valueOf("field-value"));
406 
407     // Get flag
408     IFlagNodeItem flag = assembly.getFlagByName(IEnhancedQName.of(TEST_NAMESPACE, "test-flag"));
409     assertNotNull(flag, "Flag should exist");
410 
411     // Test node types
412     assertEquals(INodeItem.NodeType.DOCUMENT, document.getNodeType(), "Should identify as document");
413     assertEquals(INodeItem.NodeType.ASSEMBLY, assembly.getNodeType(), "Should identify as assembly");
414     assertEquals(INodeItem.NodeType.FIELD, field.getNodeType(), "Should identify as field");
415     assertEquals(INodeItem.NodeType.FLAG, flag.getNodeType(), "Should identify as flag");
416   }
417 
418   /**
419    * Test Metapath generation from nodes using getMetapath().
420    */
421   @Test
422   void testMetapathGeneration() {
423     MockedModelTestSupport mocking = new MockedModelTestSupport();
424     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
425 
426     IModule module = IModuleBuilder.builder()
427         .namespace(TEST_NAMESPACE)
428         .shortName("traversal-test")
429         .version("1.0.0")
430         .source(source)
431         .assembly(mocking.assembly()
432             .name("root-assembly")
433             .rootName("root-assembly")
434             .modelInstances(List.of(
435                 mocking.field().namespace(TEST_NAMESPACE).name("child-field"))))
436         .toModule();
437 
438     IAssemblyDefinition assemblyDef = module.getRootAssemblyDefinitions().iterator().next();
439     IDMDocumentNodeItem document = IDMDocumentNodeItem.newInstance(DOCUMENT_URI, assemblyDef);
440     IRootAssemblyNodeItem assembly = document.getRootAssemblyNodeItem();
441 
442     // Add field
443     IFieldInstance fieldInstance = assemblyDef.getFieldInstances().iterator().next();
444     IDMFieldNodeItem field = ((IDMAssemblyNodeItem) assembly).newField(fieldInstance,
445         IStringItem.valueOf("field-value"));
446 
447     // Test Metapath generation
448     // Note: Document nodes may return empty string as they are the root
449     String documentPath = document.getMetapath();
450     assertNotNull(documentPath, "Document should have a metapath (even if empty)");
451 
452     String assemblyPath = assembly.getMetapath();
453     assertNotNull(assemblyPath, "Assembly should have a metapath");
454     // Root assembly path should reference the assembly name
455     assertTrue(assemblyPath.length() > 0, "Root assembly metapath should not be empty");
456 
457     String fieldPath = field.getMetapath();
458     assertNotNull(fieldPath, "Field should have a metapath");
459     assertTrue(fieldPath.length() > 0, "Field metapath should not be empty");
460 
461     // Field path should be longer than or equal to assembly path
462     // (it includes navigation from assembly to field)
463     assertTrue(fieldPath.length() >= assemblyPath.length(),
464         "Field metapath should be at least as long as assembly metapath");
465   }
466 
467   /**
468    * Test model items by name lookup.
469    */
470   @Test
471   void testModelItemsByName() {
472     MockedModelTestSupport mocking = new MockedModelTestSupport();
473     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
474 
475     IModule module = IModuleBuilder.builder()
476         .namespace(TEST_NAMESPACE)
477         .shortName("traversal-test")
478         .version("1.0.0")
479         .source(source)
480         .assembly(mocking.assembly()
481             .name("test-assembly")
482             .rootName("test-assembly")
483             .modelInstances(List.of(
484                 mocking.field().namespace(TEST_NAMESPACE).name("field1"),
485                 mocking.field().namespace(TEST_NAMESPACE).name("field2"))))
486         .toModule();
487 
488     IAssemblyDefinition assemblyDef = module.getRootAssemblyDefinitions().iterator().next();
489     StaticContext staticContext = StaticContext.instance();
490     IDMAssemblyNodeItem assembly = IDMAssemblyNodeItem.newInstance(assemblyDef, staticContext);
491 
492     // Add fields - look up by name to ensure correct mapping
493     IFieldInstance field1Instance = assemblyDef.getModelInstances().stream()
494         .filter(instance -> instance instanceof IFieldInstance)
495         .map(instance -> (IFieldInstance) instance)
496         .filter(instance -> "field1".equals(instance.getName()))
497         .findFirst()
498         .orElseThrow(() -> new AssertionError("field1 instance not found"));
499     IFieldInstance field2Instance = assemblyDef.getModelInstances().stream()
500         .filter(instance -> instance instanceof IFieldInstance)
501         .map(instance -> (IFieldInstance) instance)
502         .filter(instance -> "field2".equals(instance.getName()))
503         .findFirst()
504         .orElseThrow(() -> new AssertionError("field2 instance not found"));
505 
506     assembly.newField(field1Instance, IStringItem.valueOf("value1"));
507     assembly.newField(field2Instance, IStringItem.valueOf("value2"));
508 
509     // Test lookup by name
510     List<? extends IModelNodeItem<?, ?>> field1Items = assembly
511         .getModelItemsByName(IEnhancedQName.of(TEST_NAMESPACE, "field1"));
512     assertEquals(1, field1Items.size(), "Should find 1 item for field1");
513     assertEquals("value1", field1Items.get(0).toAtomicItem().asString(), "Field1 value should match");
514 
515     List<? extends IModelNodeItem<?, ?>> field2Items = assembly
516         .getModelItemsByName(IEnhancedQName.of(TEST_NAMESPACE, "field2"));
517     assertEquals(1, field2Items.size(), "Should find 1 item for field2");
518     assertEquals("value2", field2Items.get(0).toAtomicItem().asString(), "Field2 value should match");
519 
520     // Test lookup for non-existent name
521     List<? extends IModelNodeItem<?, ?>> nonExistentItems = assembly
522         .getModelItemsByName(IEnhancedQName.of(TEST_NAMESPACE, "non-existent"));
523     assertTrue(nonExistentItems.isEmpty(), "Should return empty list for non-existent name");
524   }
525 
526   /**
527    * Test complex assembly hierarchy with multiple levels.
528    */
529   @Test
530   void testComplexAssemblyHierarchy() {
531     MockedModelTestSupport mocking = new MockedModelTestSupport();
532     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
533 
534     IModule module = IModuleBuilder.builder()
535         .namespace(TEST_NAMESPACE)
536         .shortName("traversal-test")
537         .version("1.0.0")
538         .source(source)
539         .assembly(mocking.assembly()
540             .name("level1")
541             .rootName("level1")
542             .modelInstances(List.of(
543                 mocking.assemblyRef("level2"))))
544         .assembly(mocking.assembly()
545             .name("level2")
546             .modelInstances(List.of(
547                 mocking.assemblyRef("level3"))))
548         .assembly(mocking.assembly()
549             .name("level3")
550             .modelInstances(List.of(
551                 mocking.field().namespace(TEST_NAMESPACE).name("leaf-field"))))
552         .toModule();
553 
554     IAssemblyDefinition level1Def = module.getRootAssemblyDefinitions().iterator().next();
555     IDMDocumentNodeItem document = IDMDocumentNodeItem.newInstance(DOCUMENT_URI, level1Def);
556     IRootAssemblyNodeItem level1 = document.getRootAssemblyNodeItem();
557 
558     // Build level 2
559     IAssemblyInstance level2Instance = level1Def.getAssemblyInstances().iterator().next();
560     IDMAssemblyNodeItem level2 = ((IDMAssemblyNodeItem) level1).newAssembly(level2Instance);
561 
562     // Build level 3
563     IAssemblyInstance level3Instance = level2.getDefinition().getAssemblyInstances().iterator().next();
564     IDMAssemblyNodeItem level3 = level2.newAssembly(level3Instance);
565 
566     // Add leaf field
567     IFieldInstance leafFieldInstance = level3.getDefinition().getFieldInstances().iterator().next();
568     IDMFieldNodeItem leafField = level3.newField(leafFieldInstance, IStringItem.valueOf("leaf-value"));
569 
570     // Test navigation from leaf to root
571     assertEquals(level3, leafField.getParentNodeItem(), "Leaf field parent should be level3");
572     assertEquals(level2, level3.getParentNodeItem(), "Level3 parent should be level2");
573     assertEquals(level1, level2.getParentNodeItem(), "Level2 parent should be level1");
574     assertEquals(document, level1.getParentNodeItem(), "Level1 parent should be document");
575 
576     // Test descendant axis from root
577     List<? extends IModelNodeItem<?, ?>> allDescendants = level1.descendant().collect(Collectors.toList());
578     assertEquals(3, allDescendants.size(), "Should have 3 descendants from root: level2, level3, leaf-field");
579 
580     // Test ancestor axis from leaf
581     List<? extends INodeItem> allAncestors = leafField.ancestor().collect(Collectors.toList());
582     assertEquals(4, allAncestors.size(), "Should have 4 ancestors from leaf: level3, level2, level1, document");
583   }
584 
585   /**
586    * Test module node item creation and traversal.
587    */
588   @Test
589   void testModuleNodeItem() {
590     MockedModelTestSupport mocking = new MockedModelTestSupport();
591     ISource source = ISource.externalSource(ObjectUtils.notNull(URI.create(TEST_NAMESPACE)));
592 
593     IModule module = IModuleBuilder.builder()
594         .namespace(TEST_NAMESPACE)
595         .shortName("traversal-test")
596         .version("1.0.0")
597         .source(source)
598         .assembly(mocking.assembly()
599             .name("test-assembly")
600             .rootName("test-assembly"))
601         .toModule();
602 
603     // Create module node item
604     IModuleNodeItem moduleNode = INodeItemFactory.instance().newModuleNodeItem(module);
605 
606     assertNotNull(moduleNode, "Module node item should be created");
607     assertEquals(INodeItem.NodeType.MODULE, moduleNode.getNodeType(), "Should be a module node");
608     assertNull(moduleNode.getParentNodeItem(), "Module node should have no parent");
609     assertEquals(module, moduleNode.getModule(), "Module should match");
610 
611     // Module nodes should have model items for exported definitions
612     List<? extends IModelNodeItem<?, ?>> modelItems = moduleNode.modelItems().collect(Collectors.toList());
613     assertTrue(modelItems.size() > 0, "Module should have model items for exported definitions");
614   }
615 }