1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.format;
7   
8   import static org.junit.jupiter.api.Assertions.assertEquals;
9   import static org.junit.jupiter.api.Assertions.assertNotNull;
10  import static org.mockito.Mockito.doReturn;
11  import static org.mockito.Mockito.mock;
12  
13  import org.junit.jupiter.api.BeforeEach;
14  import org.junit.jupiter.api.DisplayName;
15  import org.junit.jupiter.api.Nested;
16  import org.junit.jupiter.api.Test;
17  
18  import java.net.URI;
19  
20  import dev.metaschema.core.metapath.item.atomic.IStringItem;
21  import dev.metaschema.core.metapath.item.node.IAssemblyInstanceGroupedNodeItem;
22  import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
23  import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
24  import dev.metaschema.core.metapath.item.node.IFieldNodeItem;
25  import dev.metaschema.core.metapath.item.node.IFlagNodeItem;
26  import dev.metaschema.core.metapath.item.node.IModuleNodeItem;
27  import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
28  import dev.metaschema.core.model.IAssemblyInstance;
29  import dev.metaschema.core.model.IFieldInstance;
30  import dev.metaschema.core.model.XmlGroupAsBehavior;
31  import dev.metaschema.core.qname.IEnhancedQName;
32  import dev.metaschema.core.testsupport.mocking.MockNodeItemFactory;
33  import dev.metaschema.core.util.CollectionUtil;
34  
35  /**
36   * Unit tests for {@link XPathFormatter}.
37   */
38  class XPathFormatterTest {
39  
40    private static final String TEST_NS = "http://example.com/test";
41    private static final String EMPTY_NS = "";
42  
43    private XPathFormatter formatter;
44    private MockNodeItemFactory mockFactory;
45  
46    @BeforeEach
47    void setUp() {
48      formatter = new XPathFormatter();
49      mockFactory = new MockNodeItemFactory();
50    }
51  
52    @Nested
53    @DisplayName("Document Node Formatting")
54    class DocumentNodeTests {
55  
56      @Test
57      @DisplayName("formatDocument returns empty string")
58      void testFormatDocumentReturnsEmptyString() {
59        IDocumentNodeItem document = mock(IDocumentNodeItem.class);
60  
61        String result = formatter.formatDocument(document);
62  
63        assertEquals("", result);
64      }
65    }
66  
67    @Nested
68    @DisplayName("Module Node Formatting")
69    class ModuleNodeTests {
70  
71      @Test
72      @DisplayName("formatMetaschema returns empty string")
73      void testFormatMetaschemaReturnsEmptyString() {
74        IModuleNodeItem module = mock(IModuleNodeItem.class);
75  
76        String result = formatter.formatMetaschema(module);
77  
78        assertEquals("", result);
79      }
80    }
81  
82    @Nested
83    @DisplayName("Root Assembly Formatting")
84    class RootAssemblyTests {
85  
86      @Test
87      @DisplayName("formatRootAssembly with namespace returns EQName format")
88      void testFormatRootAssemblyWithNamespace() {
89        IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "catalog");
90        IRootAssemblyNodeItem root = mock(IRootAssemblyNodeItem.class);
91  
92        doReturn(qname).when(root).getQName();
93  
94        String result = formatter.formatRootAssembly(root);
95  
96        assertEquals("Q{" + TEST_NS + "}catalog", result);
97      }
98  
99      @Test
100     @DisplayName("formatRootAssembly without namespace returns local name only")
101     void testFormatRootAssemblyWithoutNamespace() {
102       IEnhancedQName qname = IEnhancedQName.of(EMPTY_NS, "catalog");
103       IRootAssemblyNodeItem root = mock(IRootAssemblyNodeItem.class);
104 
105       doReturn(qname).when(root).getQName();
106 
107       String result = formatter.formatRootAssembly(root);
108 
109       assertEquals("catalog", result);
110     }
111   }
112 
113   @Nested
114   @DisplayName("Assembly Formatting")
115   class AssemblyTests {
116 
117     @Test
118     @DisplayName("formatAssembly UNGROUPED returns EQName with position")
119     void testFormatAssemblyUngrouped() {
120       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "control");
121       IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
122       IAssemblyInstance instance = mock(IAssemblyInstance.class);
123 
124       doReturn(qname).when(assembly).getQName();
125       doReturn(instance).when(assembly).getInstance();
126       doReturn(2).when(assembly).getPosition();
127       doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
128 
129       String result = formatter.formatAssembly(assembly);
130 
131       assertEquals("Q{" + TEST_NS + "}control[2]", result);
132     }
133 
134     @Test
135     @DisplayName("formatAssembly GROUPED returns wrapper + EQName with position")
136     void testFormatAssemblyGrouped() {
137       IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "control");
138       IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "controls");
139       IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
140       IAssemblyInstance instance = mock(IAssemblyInstance.class);
141 
142       doReturn(elementQname).when(assembly).getQName();
143       doReturn(instance).when(assembly).getInstance();
144       doReturn(1).when(assembly).getPosition();
145       doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
146       doReturn(wrapperQname).when(instance).getEffectiveXmlGroupAsQName();
147 
148       String result = formatter.formatAssembly(assembly);
149 
150       assertEquals("Q{" + TEST_NS + "}controls[1]/Q{" + TEST_NS + "}control[1]", result);
151     }
152 
153     @Test
154     @DisplayName("formatAssembly without instance returns EQName with position")
155     void testFormatAssemblyWithoutInstance() {
156       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "control");
157       IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
158 
159       doReturn(qname).when(assembly).getQName();
160       doReturn(null).when(assembly).getInstance();
161       doReturn(1).when(assembly).getPosition();
162 
163       String result = formatter.formatAssembly(assembly);
164 
165       assertEquals("Q{" + TEST_NS + "}control[1]", result);
166     }
167 
168     @Test
169     @DisplayName("formatAssembly without namespace returns local name with position")
170     void testFormatAssemblyWithoutNamespace() {
171       IEnhancedQName qname = IEnhancedQName.of(EMPTY_NS, "control");
172       IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
173       IAssemblyInstance instance = mock(IAssemblyInstance.class);
174 
175       doReturn(qname).when(assembly).getQName();
176       doReturn(instance).when(assembly).getInstance();
177       doReturn(3).when(assembly).getPosition();
178       doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
179 
180       String result = formatter.formatAssembly(assembly);
181 
182       assertEquals("control[3]", result);
183     }
184 
185     @Test
186     @DisplayName("formatAssembly GROUPED with high position keeps wrapper at [1]")
187     void testFormatAssemblyGroupedWrapperAlwaysPositionOne() {
188       IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "control");
189       IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "controls");
190       IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
191       IAssemblyInstance instance = mock(IAssemblyInstance.class);
192 
193       doReturn(elementQname).when(assembly).getQName();
194       doReturn(instance).when(assembly).getInstance();
195       doReturn(5).when(assembly).getPosition(); // 5th element in the group
196       doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
197       doReturn(wrapperQname).when(instance).getEffectiveXmlGroupAsQName();
198 
199       String result = formatter.formatAssembly(assembly);
200 
201       // Wrapper is always [1], element position varies
202       assertEquals("Q{" + TEST_NS + "}controls[1]/Q{" + TEST_NS + "}control[5]", result);
203     }
204 
205     @Test
206     @DisplayName("formatAssembly GROUPED with null wrapper QName falls back to element only")
207     void testFormatAssemblyGroupedNullWrapper() {
208       IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "control");
209       IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
210       IAssemblyInstance instance = mock(IAssemblyInstance.class);
211 
212       doReturn(elementQname).when(assembly).getQName();
213       doReturn(instance).when(assembly).getInstance();
214       doReturn(1).when(assembly).getPosition();
215       doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
216       doReturn(null).when(instance).getEffectiveXmlGroupAsQName(); // null wrapper
217 
218       String result = formatter.formatAssembly(assembly);
219 
220       // Should gracefully handle null wrapper
221       assertEquals("Q{" + TEST_NS + "}control[1]", result);
222     }
223   }
224 
225   @Nested
226   @DisplayName("Grouped Assembly Instance Formatting")
227   class GroupedAssemblyInstanceTests {
228 
229     @Test
230     @DisplayName("formatAssembly(IAssemblyInstanceGroupedNodeItem) UNGROUPED")
231     void testFormatGroupedAssemblyInstanceUngrouped() {
232       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "control");
233       IAssemblyInstanceGroupedNodeItem assembly = mock(IAssemblyInstanceGroupedNodeItem.class);
234       IAssemblyInstance instance = mock(IAssemblyInstance.class);
235 
236       doReturn(qname).when(assembly).getQName();
237       doReturn(instance).when(assembly).getInstance();
238       doReturn(2).when(assembly).getPosition();
239       doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
240 
241       String result = formatter.formatAssembly(assembly);
242 
243       assertEquals("Q{" + TEST_NS + "}control[2]", result);
244     }
245 
246     @Test
247     @DisplayName("formatAssembly(IAssemblyInstanceGroupedNodeItem) GROUPED")
248     void testFormatGroupedAssemblyInstanceGrouped() {
249       IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "control");
250       IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "controls");
251       IAssemblyInstanceGroupedNodeItem assembly = mock(IAssemblyInstanceGroupedNodeItem.class);
252       IAssemblyInstance instance = mock(IAssemblyInstance.class);
253 
254       doReturn(elementQname).when(assembly).getQName();
255       doReturn(instance).when(assembly).getInstance();
256       doReturn(3).when(assembly).getPosition();
257       doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
258       doReturn(wrapperQname).when(instance).getEffectiveXmlGroupAsQName();
259 
260       String result = formatter.formatAssembly(assembly);
261 
262       assertEquals("Q{" + TEST_NS + "}controls[1]/Q{" + TEST_NS + "}control[3]", result);
263     }
264   }
265 
266   @Nested
267   @DisplayName("Field Formatting")
268   class FieldTests {
269 
270     @Test
271     @DisplayName("formatField UNGROUPED returns EQName with position")
272     void testFormatFieldUngrouped() {
273       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "title");
274       IFieldNodeItem field = mock(IFieldNodeItem.class);
275       IFieldInstance instance = mock(IFieldInstance.class);
276 
277       doReturn(qname).when(field).getQName();
278       doReturn(instance).when(field).getInstance();
279       doReturn(1).when(field).getPosition();
280       doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
281 
282       String result = formatter.formatField(field);
283 
284       assertEquals("Q{" + TEST_NS + "}title[1]", result);
285     }
286 
287     @Test
288     @DisplayName("formatField GROUPED returns wrapper + EQName with position")
289     void testFormatFieldGrouped() {
290       IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "prop");
291       IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "props");
292       IFieldNodeItem field = mock(IFieldNodeItem.class);
293       IFieldInstance instance = mock(IFieldInstance.class);
294 
295       doReturn(elementQname).when(field).getQName();
296       doReturn(instance).when(field).getInstance();
297       doReturn(2).when(field).getPosition();
298       doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
299       doReturn(wrapperQname).when(instance).getEffectiveXmlGroupAsQName();
300 
301       String result = formatter.formatField(field);
302 
303       assertEquals("Q{" + TEST_NS + "}props[1]/Q{" + TEST_NS + "}prop[2]", result);
304     }
305 
306     @Test
307     @DisplayName("formatField without instance returns EQName with position")
308     void testFormatFieldWithoutInstance() {
309       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "value");
310       IFieldNodeItem field = mock(IFieldNodeItem.class);
311 
312       doReturn(qname).when(field).getQName();
313       doReturn(null).when(field).getInstance();
314       doReturn(1).when(field).getPosition();
315 
316       String result = formatter.formatField(field);
317 
318       assertEquals("Q{" + TEST_NS + "}value[1]", result);
319     }
320   }
321 
322   @Nested
323   @DisplayName("Flag Formatting")
324   class FlagTests {
325 
326     @Test
327     @DisplayName("formatFlag with namespace returns @EQName")
328     void testFormatFlagWithNamespace() {
329       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "id");
330       IFlagNodeItem flag = mock(IFlagNodeItem.class);
331 
332       doReturn(qname).when(flag).getQName();
333 
334       String result = formatter.formatFlag(flag);
335 
336       assertEquals("@Q{" + TEST_NS + "}id", result);
337     }
338 
339     @Test
340     @DisplayName("formatFlag without namespace returns @localname")
341     void testFormatFlagWithoutNamespace() {
342       IEnhancedQName qname = IEnhancedQName.of(EMPTY_NS, "id");
343       IFlagNodeItem flag = mock(IFlagNodeItem.class);
344 
345       doReturn(qname).when(flag).getQName();
346 
347       String result = formatter.formatFlag(flag);
348 
349       assertEquals("@id", result);
350     }
351   }
352 
353   @Nested
354   @DisplayName("Special Characters and Edge Cases")
355   class SpecialCharacterTests {
356 
357     @Test
358     @DisplayName("formatAssembly with hyphenated name")
359     void testFormatAssemblyHyphenatedName() {
360       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "my-control-element");
361       IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
362       IAssemblyInstance instance = mock(IAssemblyInstance.class);
363 
364       doReturn(qname).when(assembly).getQName();
365       doReturn(instance).when(assembly).getInstance();
366       doReturn(1).when(assembly).getPosition();
367       doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
368 
369       String result = formatter.formatAssembly(assembly);
370 
371       assertEquals("Q{" + TEST_NS + "}my-control-element[1]", result);
372     }
373 
374     @Test
375     @DisplayName("formatAssembly with underscore name")
376     void testFormatAssemblyUnderscoreName() {
377       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "my_control_element");
378       IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
379       IAssemblyInstance instance = mock(IAssemblyInstance.class);
380 
381       doReturn(qname).when(assembly).getQName();
382       doReturn(instance).when(assembly).getInstance();
383       doReturn(1).when(assembly).getPosition();
384       doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
385 
386       String result = formatter.formatAssembly(assembly);
387 
388       assertEquals("Q{" + TEST_NS + "}my_control_element[1]", result);
389     }
390 
391     @Test
392     @DisplayName("formatFlag with numeric suffix in name")
393     void testFormatFlagNumericSuffix() {
394       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "id2");
395       IFlagNodeItem flag = mock(IFlagNodeItem.class);
396 
397       doReturn(qname).when(flag).getQName();
398 
399       String result = formatter.formatFlag(flag);
400 
401       assertEquals("@Q{" + TEST_NS + "}id2", result);
402     }
403   }
404 
405   @Nested
406   @DisplayName("Multiple Siblings Tests")
407   class MultipleSiblingsTests {
408 
409     @Test
410     @DisplayName("Multiple assemblies at different positions")
411     void testMultipleAssembliesAtDifferentPositions() {
412       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "control");
413 
414       // Create three assemblies with positions 1, 2, 3
415       for (int position = 1; position <= 3; position++) {
416         IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
417         IAssemblyInstance instance = mock(IAssemblyInstance.class);
418 
419         doReturn(qname).when(assembly).getQName();
420         doReturn(instance).when(assembly).getInstance();
421         doReturn(position).when(assembly).getPosition();
422         doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
423 
424         String result = formatter.formatAssembly(assembly);
425 
426         assertEquals("Q{" + TEST_NS + "}control[" + position + "]", result);
427       }
428     }
429 
430     @Test
431     @DisplayName("Multiple fields at different positions")
432     void testMultipleFieldsAtDifferentPositions() {
433       IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "prop");
434 
435       // Create three fields with positions 1, 2, 3
436       for (int position = 1; position <= 3; position++) {
437         IFieldNodeItem field = mock(IFieldNodeItem.class);
438         IFieldInstance instance = mock(IFieldInstance.class);
439 
440         doReturn(qname).when(field).getQName();
441         doReturn(instance).when(field).getInstance();
442         doReturn(position).when(field).getPosition();
443         doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
444 
445         String result = formatter.formatField(field);
446 
447         assertEquals("Q{" + TEST_NS + "}prop[" + position + "]", result);
448       }
449     }
450   }
451 
452   @Nested
453   @DisplayName("Full Path Formatting")
454   class FullPathTests {
455 
456     @Test
457     @DisplayName("format produces complete XPath from document to flag")
458     void testFormatCompletePath() {
459       IEnhancedQName rootQname = IEnhancedQName.of(TEST_NS, "catalog");
460       IEnhancedQName assemblyQname = IEnhancedQName.of(TEST_NS, "control");
461       IEnhancedQName flagQname = IEnhancedQName.of(TEST_NS, "id");
462 
463       IFlagNodeItem flag = mockFactory.flag(flagQname, IStringItem.valueOf("ac-1"));
464       IAssemblyNodeItem assembly = mockFactory.assembly(
465           assemblyQname,
466           CollectionUtil.singletonList(flag),
467           CollectionUtil.emptyList());
468 
469       // Set up instance for assembly
470       IAssemblyInstance assemblyInstance = mock(IAssemblyInstance.class);
471       doReturn(assemblyInstance).when(assembly).getInstance();
472       doReturn(XmlGroupAsBehavior.UNGROUPED).when(assemblyInstance).getXmlGroupAsBehavior();
473 
474       // Create document to establish parent hierarchy for formatting
475       assertNotNull(mockFactory.document(
476           URI.create("https://example.com/catalog.xml"),
477           rootQname,
478           CollectionUtil.emptyList(),
479           CollectionUtil.singletonList(assembly)));
480 
481       // Format from flag - the path should traverse up through parent nodes
482       String result = formatter.format(flag);
483 
484       // Expected: /Q{ns}catalog/Q{ns}control[1]/@Q{ns}id
485       assertEquals("/Q{" + TEST_NS + "}catalog/Q{" + TEST_NS + "}control[1]/@Q{" + TEST_NS + "}id", result);
486     }
487 
488     @Test
489     @DisplayName("format with nested assemblies produces correct path")
490     void testFormatNestedAssemblies() {
491       IEnhancedQName rootQname = IEnhancedQName.of(TEST_NS, "catalog");
492       IEnhancedQName groupQname = IEnhancedQName.of(TEST_NS, "group");
493       IEnhancedQName controlQname = IEnhancedQName.of(TEST_NS, "control");
494       IEnhancedQName flagQname = IEnhancedQName.of(TEST_NS, "id");
495 
496       IFlagNodeItem flag = mockFactory.flag(flagQname, IStringItem.valueOf("ac-1"));
497 
498       IAssemblyNodeItem control = mockFactory.assembly(
499           controlQname,
500           CollectionUtil.singletonList(flag),
501           CollectionUtil.emptyList());
502       IAssemblyInstance controlInstance = mock(IAssemblyInstance.class);
503       doReturn(controlInstance).when(control).getInstance();
504       doReturn(XmlGroupAsBehavior.UNGROUPED).when(controlInstance).getXmlGroupAsBehavior();
505 
506       IAssemblyNodeItem group = mockFactory.assembly(
507           groupQname,
508           CollectionUtil.emptyList(),
509           CollectionUtil.singletonList(control));
510       IAssemblyInstance groupInstance = mock(IAssemblyInstance.class);
511       doReturn(groupInstance).when(group).getInstance();
512       doReturn(XmlGroupAsBehavior.UNGROUPED).when(groupInstance).getXmlGroupAsBehavior();
513 
514       // Create document to establish parent hierarchy for formatting
515       assertNotNull(mockFactory.document(
516           URI.create("https://example.com/catalog.xml"),
517           rootQname,
518           CollectionUtil.emptyList(),
519           CollectionUtil.singletonList(group)));
520 
521       String result = formatter.format(flag);
522 
523       assertEquals(
524           "/Q{" + TEST_NS + "}catalog/Q{" + TEST_NS + "}group[1]/Q{" + TEST_NS + "}control[1]/@Q{" + TEST_NS + "}id",
525           result);
526     }
527 
528     @Test
529     @DisplayName("format with GROUPED assembly in path")
530     void testFormatWithGroupedAssemblyInPath() {
531       IEnhancedQName rootQname = IEnhancedQName.of(TEST_NS, "catalog");
532       IEnhancedQName controlQname = IEnhancedQName.of(TEST_NS, "control");
533       IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "controls");
534       IEnhancedQName flagQname = IEnhancedQName.of(TEST_NS, "id");
535 
536       IFlagNodeItem flag = mockFactory.flag(flagQname, IStringItem.valueOf("ac-1"));
537 
538       IAssemblyNodeItem control = mockFactory.assembly(
539           controlQname,
540           CollectionUtil.singletonList(flag),
541           CollectionUtil.emptyList());
542       IAssemblyInstance controlInstance = mock(IAssemblyInstance.class);
543       doReturn(controlInstance).when(control).getInstance();
544       doReturn(XmlGroupAsBehavior.GROUPED).when(controlInstance).getXmlGroupAsBehavior();
545       doReturn(wrapperQname).when(controlInstance).getEffectiveXmlGroupAsQName();
546 
547       // Create document to establish parent hierarchy for formatting
548       assertNotNull(mockFactory.document(
549           URI.create("https://example.com/catalog.xml"),
550           rootQname,
551           CollectionUtil.emptyList(),
552           CollectionUtil.singletonList(control)));
553 
554       String result = formatter.format(flag);
555 
556       assertEquals(
557           "/Q{" + TEST_NS + "}catalog/Q{" + TEST_NS + "}controls[1]/Q{" + TEST_NS + "}control[1]/@Q{" + TEST_NS + "}id",
558           result);
559     }
560 
561     @Test
562     @DisplayName("format document node produces single slash")
563     void testFormatDocumentNode() {
564       IEnhancedQName rootQname = IEnhancedQName.of(TEST_NS, "catalog");
565 
566       IDocumentNodeItem document = mockFactory.document(
567           URI.create("https://example.com/catalog.xml"),
568           rootQname,
569           CollectionUtil.emptyList(),
570           CollectionUtil.emptyList());
571 
572       String result = formatter.format(document);
573 
574       // Document alone should produce empty string (the leading "/" comes from
575       // joining)
576       assertEquals("", result);
577     }
578   }
579 }