Skip to main content

mig_bo4e/
engine.rs

1//! Mapping engine — loads TOML definitions and provides bidirectional conversion.
2//!
3//! Supports nested group paths (e.g., "SG4.SG5") for navigating the assembled tree
4//! and provides `map_forward` / `map_reverse` for full entity conversion.
5
6use std::collections::{BTreeMap, HashMap, HashSet};
7use std::path::Path;
8
9use mig_assembly::assembler::{
10    AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
11};
12use mig_types::schema::mig::MigSchema;
13use mig_types::segment::OwnedSegment;
14
15use crate::definition::{FieldMapping, MappingDefinition};
16use crate::error::MappingError;
17use crate::segment_structure::SegmentStructure;
18
19/// The mapping engine holds all loaded mapping definitions
20/// and provides methods for bidirectional conversion.
21pub struct MappingEngine {
22    definitions: Vec<MappingDefinition>,
23    segment_structure: Option<SegmentStructure>,
24    code_lookup: Option<crate::code_lookup::CodeLookup>,
25    /// Transaction-root SG id (e.g. "SG4" for UTILMD), when the engine is
26    /// operating at transaction scope. Child entities whose parent group
27    /// equals this id are left at the top level of the forward-mapped JSON
28    /// rather than being nested — SG4 is the transaction envelope, so
29    /// entities inside it (Marktlokation, Geschaeftspartner, …) are peers of
30    /// the transaction metadata, not sub-objects of it.
31    ///
32    /// Nesting still applies to other parent groups: e.g. Kontakt (SG2.SG3)
33    /// remains nested under Marktteilnehmer (SG2) because SG2 is a
34    /// message-level group, not the transaction root.
35    transaction_group: Option<String>,
36    /// PID currently being processed (e.g., "55002"). Used to suppress codelist
37    /// decoration of self-referential PID-identifier fields (e.g., RFF+Z13's
38    /// d1154 in PID 55002 has only "55002" as an allowed value).
39    current_pid: Option<String>,
40    /// The shared code-list tables a definition's `code_list` names. One `Arc`
41    /// per mappings tree, shared by every engine built from it -- a format
42    /// version builds a couple of thousand engines and the tables are the same
43    /// for all of them.
44    code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
45    /// Forward mapping writes each code as it stands on the wire instead of
46    /// through its rule's table (see [`MappingEngine::with_raw_codes`]).
47    raw_codes: bool,
48}
49
50impl MappingEngine {
51    /// Create an empty engine with no definitions (for unit testing).
52    pub fn new_empty() -> Self {
53        Self {
54            definitions: Vec::new(),
55            segment_structure: None,
56            code_lookup: None,
57            transaction_group: None,
58            current_pid: None,
59            raw_codes: false,
60            code_lists: std::sync::Arc::new(crate::code_lists::CodeLists::default()),
61        }
62    }
63
64    /// Load all TOML mapping files from a directory.
65    pub fn load(dir: &Path) -> Result<Self, MappingError> {
66        let mut definitions = Vec::new();
67
68        let mut entries: Vec<_> = std::fs::read_dir(dir)?.filter_map(|e| e.ok()).collect();
69        entries.sort_by_key(|e| e.file_name());
70
71        for entry in entries {
72            let path = entry.path();
73            if path.extension().map(|e| e == "toml").unwrap_or(false) {
74                let content = std::fs::read_to_string(&path)?;
75                let def = MappingDefinition::from_toml_str(&content).map_err(|message| {
76                    MappingError::TomlParse {
77                        file: path.display().to_string(),
78                        message,
79                    }
80                })?;
81                definitions.push(def);
82            }
83        }
84
85        // Emission order comes from `meta.order` where a definition states it,
86        // and from the filename otherwise — which is what every file relies on
87        // today, via the `_30_12_` prefix convention. `sort_by_key` is stable,
88        // so definitions without the key keep their filename order exactly, and
89        // `u32::MAX` puts them after any that opt in.
90        definitions.sort_by_key(|d| d.meta.order.unwrap_or(u32::MAX));
91
92        Ok(Self {
93            definitions,
94            segment_structure: None,
95            code_lookup: None,
96            transaction_group: None,
97            current_pid: None,
98            raw_codes: false,
99            code_lists: crate::code_lists::CodeLists::discover(dir),
100        })
101    }
102
103    /// Load message-level and transaction-level TOML mappings from separate directories.
104    ///
105    /// Returns `(message_engine, transaction_engine)` where:
106    /// - `message_engine` maps SG2/SG3/root-level definitions (shared across PIDs)
107    /// - `transaction_engine` maps SG4+ definitions (PID-specific)
108    pub fn load_split(
109        message_dir: &Path,
110        transaction_dir: &Path,
111    ) -> Result<(Self, Self), MappingError> {
112        let msg_engine = Self::load(message_dir)?;
113        let tx_engine = Self::load(transaction_dir)?;
114        Ok((msg_engine, tx_engine))
115    }
116
117    /// Load TOML mapping files from multiple directories into a single engine.
118    ///
119    /// Useful for combining message-level and transaction-level mappings
120    /// when a single engine with all definitions is needed.
121    pub fn load_merged(dirs: &[&Path]) -> Result<Self, MappingError> {
122        let mut definitions = Vec::new();
123        for dir in dirs {
124            let engine = Self::load(dir)?;
125            definitions.extend(engine.definitions);
126        }
127        Ok(Self {
128            definitions,
129            segment_structure: None,
130            code_lookup: None,
131            transaction_group: None,
132            current_pid: None,
133            raw_codes: false,
134            code_lists: crate::code_lists::CodeLists::discover(
135                dirs.first().copied().unwrap_or(Path::new("")),
136            ),
137        })
138    }
139
140    /// Load transaction-level mappings with common template inheritance.
141    ///
142    /// 1. Loads all `.toml` from `common_dir`
143    /// 2. Filters: keeps only definitions whose `source_path` exists in the PID schema
144    /// 3. Loads all `.toml` from `pid_dir`
145    /// 4. For each PID definition, if a common definition has matching
146    ///    `(source_group, discriminator)`, replaces the common one (file-level replacement)
147    /// 5. Merges both sets: common first, then PID additions
148    pub fn load_with_common(
149        common_dir: &Path,
150        pid_dir: &Path,
151        schema_index: &crate::pid_schema_index::PidSchemaIndex,
152    ) -> Result<Self, MappingError> {
153        let mut common_defs = Self::load(common_dir)?.definitions;
154
155        // Filter common defs by schema — keep only groups that exist in this PID
156        common_defs.retain(|d| {
157            d.meta
158                .source_path
159                .as_deref()
160                .map(|sp| schema_index.has_group(sp))
161                .unwrap_or(true)
162        });
163
164        let pid_defs = Self::load(pid_dir)?.definitions;
165
166        // Build set of PID override keys: (source_group_normalized, discriminator)
167        // Normalizations applied:
168        // 1. Strip positional indices from source_group: "SG4.SG5:1" → "SG4.SG5"
169        // 2. Strip occurrence indices from discriminator: "RFF.c506.d1153=TN#0" → "RFF.c506.d1153=TN"
170        let normalize_sg = |sg: &str| -> String {
171            sg.split('.')
172                .map(|part| part.split(':').next().unwrap_or(part))
173                .collect::<Vec<_>>()
174                .join(".")
175        };
176        let pid_keys: HashSet<(String, Option<String>)> = pid_defs
177            .iter()
178            .flat_map(|d| {
179                let sg = normalize_sg(&d.meta.source_group);
180                let disc = d.meta.discriminator.clone();
181                let mut keys = vec![(sg.clone(), disc.clone())];
182                // If discriminator has occurrence index (#N), also add base form
183                if let Some(ref disc_str) = disc {
184                    if let Some(base) = disc_str.rsplit_once('#') {
185                        if base.1.chars().all(|c| c.is_ascii_digit()) {
186                            keys.push((sg, Some(base.0.to_string())));
187                        }
188                    }
189                }
190                keys
191            })
192            .collect();
193
194        // Remove common defs that are overridden by PID defs
195        common_defs.retain(|d| {
196            let key = (
197                normalize_sg(&d.meta.source_group),
198                d.meta.discriminator.clone(),
199            );
200            !pid_keys.contains(&key)
201        });
202
203        // Combine: common first, then PID
204        let mut definitions = common_defs;
205        definitions.extend(pid_defs);
206
207        Ok(Self {
208            definitions,
209            segment_structure: None,
210            code_lookup: None,
211            transaction_group: None,
212            current_pid: None,
213            raw_codes: false,
214            code_lists: crate::code_lists::CodeLists::discover(pid_dir),
215        })
216    }
217
218    /// Load common definitions only (no per-PID dir), filtered by schema index.
219    ///
220    /// Used for PIDs that have no per-PID directory but can use shared common/ definitions.
221    pub fn load_common_only(
222        common_dir: &Path,
223        schema_index: &crate::pid_schema_index::PidSchemaIndex,
224    ) -> Result<Self, MappingError> {
225        let mut common_defs = Self::load(common_dir)?.definitions;
226
227        // Filter common defs by schema — keep only groups that exist in this PID
228        common_defs.retain(|d| {
229            d.meta
230                .source_path
231                .as_deref()
232                .map(|sp| schema_index.has_group(sp))
233                .unwrap_or(true)
234        });
235
236        Ok(Self {
237            definitions: common_defs,
238            segment_structure: None,
239            code_lookup: None,
240            transaction_group: None,
241            current_pid: None,
242            raw_codes: false,
243            code_lists: crate::code_lists::CodeLists::discover(common_dir),
244        })
245    }
246
247    /// Load message + transaction engines with common template inheritance.
248    ///
249    /// Returns `(message_engine, transaction_engine)` where the transaction engine
250    /// inherits shared templates from `common_dir`, filtered by the PID schema.
251    pub fn load_split_with_common(
252        message_dir: &Path,
253        common_dir: &Path,
254        transaction_dir: &Path,
255        schema_index: &crate::pid_schema_index::PidSchemaIndex,
256    ) -> Result<(Self, Self), MappingError> {
257        let msg_engine = Self::load(message_dir)?;
258        let tx_engine = Self::load_with_common(common_dir, transaction_dir, schema_index)?;
259        Ok((msg_engine, tx_engine))
260    }
261
262    /// Create an engine from an already-parsed list of definitions.
263    /// Whether any definition names a shared code list.
264    fn names_a_code_list(definitions: &[MappingDefinition]) -> bool {
265        definitions.iter().any(|d| {
266            d.fields.values().any(|f| {
267                matches!(f, FieldMapping::Structured(s)
268                    if s.code_list.is_some() || s.also_code_list.is_some())
269            })
270        })
271    }
272
273    /// The table a structured mapping translates through: its own inline
274    /// `enum_map`, or the shared list its `code_list` names.
275    ///
276    /// Returning a reference rather than resolving at load time is what keeps
277    /// the tables out of the compiled cache: a definition serialises the name,
278    /// not 94 entries, in each of the files that use it.
279    fn table<'a>(
280        &'a self,
281        inline: Option<&'a BTreeMap<String, String>>,
282        named: Option<&str>,
283    ) -> Option<&'a BTreeMap<String, String>> {
284        self.code_lists.resolve(inline, named)
285    }
286
287    /// Attach shared code lists to an engine built from cached definitions.
288    pub fn with_code_lists(
289        mut self,
290        code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
291    ) -> Self {
292        self.code_lists = code_lists;
293        self
294    }
295
296    /// The shared tables this engine resolves `code_list` names against.
297    pub fn code_lists(&self) -> &std::sync::Arc<crate::code_lists::CodeLists> {
298        &self.code_lists
299    }
300
301    /// Build from cached definitions, with the shared tables their `code_list`
302    /// names resolve against.
303    pub fn from_definitions_with_code_lists(
304        code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
305        definitions: Vec<MappingDefinition>,
306    ) -> Self {
307        // The assertion belongs here too, not only in `from_definitions`:
308        // `DataBundle::load` called *this* constructor with an empty `Arc` for
309        // months of work, so checking only the other one meant the check could
310        // not see the one path that actually shipped.
311        debug_assert!(
312            !(code_lists.is_empty() && Self::names_a_code_list(&definitions)),
313            "definitions name a shared code list but the supplied tables are \
314             empty — whatever produced them (a bundle, a cache) is not carrying \
315             them, and every code they translate will reach the output raw"
316        );
317        // Built directly rather than through `from_definitions`, whose debug
318        // assertion is precisely "nobody supplied the tables" -- routing the
319        // correct call through it would fire on every translated definition.
320        Self {
321            definitions,
322            segment_structure: None,
323            code_lookup: None,
324            transaction_group: None,
325            current_pid: None,
326            raw_codes: false,
327            code_lists,
328        }
329    }
330
331    pub fn from_definitions(definitions: Vec<MappingDefinition>) -> Self {
332        // A definition that names a code list is useless without the tables:
333        // the name resolves to nothing and the EDIFACT code reaches the output
334        // raw, which reads as "the guide lists no codes here" rather than as
335        // the wiring mistake it is. It has happened twice -- once in the API,
336        // once in the test harness -- so say so where it happens instead of
337        // letting a wrong value travel.
338        debug_assert!(
339            !Self::names_a_code_list(&definitions),
340            "definitions name a shared code list but none were supplied — build \
341             this engine with `from_definitions_with_code_lists`, or the codes \
342             they translate will reach the output untranslated"
343        );
344        Self {
345            definitions,
346            segment_structure: None,
347            code_lookup: None,
348            transaction_group: None,
349            current_pid: None,
350            raw_codes: false,
351            code_lists: std::sync::Arc::new(crate::code_lists::CodeLists::default()),
352        }
353    }
354
355    /// Save definitions to a cache file.
356    ///
357    /// Only the `definitions` are serialized — `segment_structure` and `code_lookup`
358    /// must be re-attached after loading from cache. Paths in the definitions are
359    /// already resolved to numeric indices, so no `PathResolver` is needed at load time.
360    pub fn save_cached(&self, path: &Path) -> Result<(), MappingError> {
361        let encoded =
362            serde_json::to_vec(&self.definitions).map_err(|e| MappingError::CacheWrite {
363                path: path.display().to_string(),
364                message: e.to_string(),
365            })?;
366        if let Some(parent) = path.parent() {
367            std::fs::create_dir_all(parent)?;
368        }
369        std::fs::write(path, encoded)?;
370        Ok(())
371    }
372
373    /// Load from cache if available, otherwise fall back to TOML directory.
374    ///
375    /// When loading from cache, PathResolver is NOT needed (paths pre-resolved).
376    /// When falling back to TOML, the caller should chain `.with_path_resolver()`.
377    pub fn load_cached_or_toml(cache_path: &Path, toml_dir: &Path) -> Result<Self, MappingError> {
378        if cache_path.exists() {
379            Self::load_cached(cache_path)
380        } else {
381            Self::load(toml_dir)
382        }
383    }
384
385    /// Load definitions from a cache file.
386    ///
387    /// Returns an engine with only `definitions` populated. Attach `segment_structure`
388    /// and `code_lookup` via the builder methods if needed.
389    pub fn load_cached(path: &Path) -> Result<Self, MappingError> {
390        let bytes = std::fs::read(path)?;
391        let definitions: Vec<MappingDefinition> =
392            serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
393                path: path.display().to_string(),
394                message: e.to_string(),
395            })?;
396        Ok(Self {
397            definitions,
398            segment_structure: None,
399            code_lookup: None,
400            transaction_group: None,
401            current_pid: None,
402            raw_codes: false,
403            code_lists: crate::code_lists::CodeLists::discover(path),
404        })
405    }
406
407    /// Attach a MIG-derived segment structure for trailing element padding.
408    ///
409    /// When set, `map_reverse` pads each segment's elements up to the
410    /// MIG-defined count, ensuring trailing empty elements are preserved.
411    pub fn with_segment_structure(mut self, ss: SegmentStructure) -> Self {
412        self.segment_structure = Some(ss);
413        self
414    }
415
416    /// Attach a code lookup for enriching code-type field values.
417    ///
418    /// When set, fields that map to code-type elements in the PID schema
419    /// are emitted as `{"code": "Z15", "meaning": "Ja"}` objects instead of plain strings.
420    pub fn with_code_lookup(mut self, cl: crate::code_lookup::CodeLookup) -> Self {
421        self.code_lookup = Some(cl);
422        self
423    }
424
425    /// Declare which PID this engine is currently processing.
426    ///
427    /// When combined with [`with_code_lookup`](Self::with_code_lookup), code
428    /// fields whose only allowed value equals the PID itself (Class C in the
429    /// 2026-04-28 audit — e.g., RFF+Z13's d1154 in PID 55002 enumerates only
430    /// `55002`) are emitted as plain strings instead of being decorated with
431    /// `{code, meaning, enum}` and a dedup-suffixed enum name.
432    pub fn with_pid(mut self, pid: impl Into<String>) -> Self {
433        self.current_pid = Some(pid.into());
434        self
435    }
436
437    /// Write codes as they stand on the wire in the forward direction.
438    ///
439    /// By default a code with a table (`enum_map`, `code_list`) is written as
440    /// its name — `NAD+Z65` as `"partnerrolle": "kundeDesLf"` — and an
441    /// `also_target` field receives the second value the code carries. Names
442    /// belong to the release that wrote them; codes do not. With raw codes each
443    /// element is written once, as its code, and no `also_target` field is
444    /// derived; enrichment still adds the `meaning`. The reverse direction
445    /// accepts raw codes, so the output renders the same message.
446    pub fn with_raw_codes(mut self, raw: bool) -> Self {
447        self.raw_codes = raw;
448        self
449    }
450
451    /// Attach a path resolver to normalize EDIFACT ID paths to numeric indices.
452    ///
453    /// This allows TOML mapping files to use named paths like `loc.c517.d3225`
454    /// instead of numeric indices like `loc.1.0`. Resolution happens once at
455    /// load time — the engine hot path is completely unchanged.
456    pub fn with_path_resolver(mut self, resolver: crate::path_resolver::PathResolver) -> Self {
457        for def in &mut self.definitions {
458            def.normalize_paths(&resolver);
459        }
460        self
461    }
462
463    /// Declare the transaction-root SG id (e.g. `"SG4"` for UTILMD).
464    ///
465    /// When set, entities whose parent group equals this id are not nested
466    /// into their parent in the forward-mapped JSON. See the
467    /// [`transaction_group`](Self#structfield.transaction_group-1) field doc
468    /// on `MappingEngine` for the full rationale.
469    pub fn with_transaction_group(mut self, tx: impl Into<String>) -> Self {
470        self.transaction_group = Some(tx.into());
471        self
472    }
473
474    /// Add definitions to an already-built engine, keeping everything else it
475    /// carries (code lookup, segment structure, PID, transaction group).
476    ///
477    /// Used to widen a message-level engine into one flat engine over a whole
478    /// variant — what APERAK and CONTRL are converted with, since they have no
479    /// message/transaction split in the v2 `convert` route. A definition whose
480    /// `(entity, source_group, source_path, discriminator, parent_field)` the
481    /// engine already has is skipped, so the same rule reached through two
482    /// PIDs is added once.
483    ///
484    /// The definitions are taken as they are; run them through
485    /// [`with_path_resolver`](Self::with_path_resolver) first if their paths
486    /// are still named.
487    pub fn extend_definitions(mut self, defs: impl IntoIterator<Item = MappingDefinition>) -> Self {
488        fn key(d: &MappingDefinition) -> (String, String, String, String, String) {
489            (
490                d.meta.entity.clone(),
491                d.meta.source_group.clone(),
492                d.meta.source_path.clone().unwrap_or_default(),
493                d.meta.discriminator.clone().unwrap_or_default(),
494                d.meta.parent_field.clone().unwrap_or_default(),
495            )
496        }
497        let mut seen: std::collections::HashSet<_> = self.definitions.iter().map(key).collect();
498        for def in defs {
499            if seen.insert(key(&def)) {
500                self.definitions.push(def);
501            }
502        }
503        self
504    }
505
506    /// Get all loaded definitions.
507    pub fn definitions(&self) -> &[MappingDefinition] {
508        &self.definitions
509    }
510
511    /// Find a definition by entity name.
512    /// The entity's own definition. `parent_field` children (which carry their
513    /// parent's entity name and are mapped inside the parent's instance) are
514    /// skipped — they are not a definition *of* the entity.
515    pub fn definition_for_entity(&self, entity: &str) -> Option<&MappingDefinition> {
516        self.definitions.iter().find(|d| {
517            d.meta.entity == entity
518                && d.meta.parent_field.is_none()
519                && !is_bound_child(&self.definitions, d)
520        })
521    }
522
523    // ── Forward mapping: tree → BO4E ──
524
525    /// Extract a field value from an assembled tree using a mapping path.
526    ///
527    /// `group_path` supports dotted notation for nested groups (e.g., "SG4.SG5").
528    /// Parent groups default to repetition 0; `repetition` applies to the leaf group.
529    ///
530    /// Path format: "segment.composite.data_element" e.g., "loc.c517.d3225"
531    pub fn extract_field(
532        &self,
533        tree: &AssembledTree,
534        group_path: &str,
535        path: &str,
536        repetition: usize,
537    ) -> Option<String> {
538        let instance = Self::resolve_group_instance(tree, group_path, repetition)?;
539        Self::extract_from_instance(instance, path)
540    }
541
542    /// Navigate a potentially nested group path to find a group instance.
543    ///
544    /// For "SG4.SG5", finds SG4\[0\] then SG5 at the given repetition within it.
545    /// For "SG8", finds SG8 at the given repetition in the top-level groups.
546    ///
547    /// Supports intermediate repetition with colon syntax: "SG4.SG8:1.SG10"
548    /// means SG4\[0\] → SG8\[1\] → SG10\[repetition\]. Without a colon suffix,
549    /// intermediate groups default to repetition 0.
550    pub fn resolve_group_instance<'a>(
551        tree: &'a AssembledTree,
552        group_path: &str,
553        repetition: usize,
554    ) -> Option<&'a AssembledGroupInstance> {
555        let parts: Vec<&str> = group_path.split('.').collect();
556
557        let (first_id, first_rep) = parse_group_spec(parts[0]);
558        let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
559
560        if parts.len() == 1 {
561            // Single part — use the explicit rep from spec or the `repetition` param
562            let rep = first_rep.unwrap_or(repetition);
563            return first_group.repetitions.get(rep);
564        }
565
566        // Navigate through groups; intermediate parts default to rep 0
567        // unless explicitly specified via `:N` suffix
568        let mut current_instance = first_group.repetitions.get(first_rep.unwrap_or(0))?;
569
570        for (i, part) in parts[1..].iter().enumerate() {
571            let (group_id, explicit_rep) = parse_group_spec(part);
572            let child_group = current_instance
573                .child_groups
574                .iter()
575                .find(|g| g.group_id == group_id)?;
576
577            if i == parts.len() - 2 {
578                // Last part — use explicit rep, or fall back to `repetition`
579                let rep = explicit_rep.unwrap_or(repetition);
580                return child_group.repetitions.get(rep);
581            }
582            // Intermediate — use explicit rep or 0
583            current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
584        }
585
586        None
587    }
588
589    /// Navigate the assembled tree using a source_path with qualifier suffixes.
590    ///
591    /// Source paths like `"sg4.sg8_z98.sg10"` encode qualifiers inline:
592    /// `sg8_z98` means "find the SG8 repetition whose entry segment has qualifier Z98".
593    /// Parts without underscores (e.g., `sg4`, `sg10`) use the first repetition.
594    ///
595    /// Returns `None` if any part of the path can't be resolved.
596    pub fn resolve_by_source_path<'a>(
597        tree: &'a AssembledTree,
598        source_path: &str,
599    ) -> Option<&'a AssembledGroupInstance> {
600        let parts: Vec<&str> = source_path.split('.').collect();
601        if parts.is_empty() {
602            return None;
603        }
604
605        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
606        let first_group = tree
607            .groups
608            .iter()
609            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))?;
610
611        let mut current_instance = if let Some(q) = first_qualifier {
612            find_rep_by_entry_qualifier(&first_group.repetitions, q)?
613        } else {
614            first_group.repetitions.first()?
615        };
616
617        if parts.len() == 1 {
618            return Some(current_instance);
619        }
620
621        for part in &parts[1..] {
622            let (group_id, qualifier) = parse_source_path_part(part);
623            let child_group = current_instance
624                .child_groups
625                .iter()
626                .find(|g| g.group_id.eq_ignore_ascii_case(group_id))?;
627
628            current_instance = if let Some(q) = qualifier {
629                find_rep_by_entry_qualifier(&child_group.repetitions, q)?
630            } else {
631                child_group.repetitions.first()?
632            };
633        }
634
635        Some(current_instance)
636    }
637
638    /// Resolve ALL matching instances for a source_path, returning a Vec.
639    ///
640    /// Like `resolve_by_source_path` but returns all repetitions matching
641    /// at any level, not just the first.  For example, if there are two SG5
642    /// reps with LOC+Z17, `resolve_all_by_source_path(tree, "sg4.sg5_z17")`
643    /// returns both.  For deeper paths like "sg4.sg8_zf3.sg10", if there are
644    /// two SG8 reps with ZF3, it returns SG10 children from both.
645    pub fn resolve_all_by_source_path<'a>(
646        tree: &'a AssembledTree,
647        source_path: &str,
648    ) -> Vec<&'a AssembledGroupInstance> {
649        let parts: Vec<&str> = source_path.split('.').collect();
650        if parts.is_empty() {
651            return vec![];
652        }
653
654        // First part: match against top-level groups
655        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
656        let first_group = match tree
657            .groups
658            .iter()
659            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
660        {
661            Some(g) => g,
662            None => return vec![],
663        };
664
665        let mut current_instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
666            find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
667        } else {
668            first_group.repetitions.iter().collect()
669        };
670
671        // Navigate remaining parts, branching at each level when multiple
672        // instances match a qualifier (e.g., two SG8 reps with ZF3).
673        for part in &parts[1..] {
674            let (group_id, qualifier) = parse_source_path_part(part);
675            let mut next_instances = Vec::new();
676
677            for instance in &current_instances {
678                if let Some(child_group) = instance
679                    .child_groups
680                    .iter()
681                    .find(|g| g.group_id.eq_ignore_ascii_case(group_id))
682                {
683                    if let Some(q) = qualifier {
684                        next_instances.extend(find_all_reps_by_entry_qualifier(
685                            &child_group.repetitions,
686                            q,
687                        ));
688                    } else {
689                        next_instances.extend(child_group.repetitions.iter());
690                    }
691                }
692            }
693
694            current_instances = next_instances;
695        }
696
697        current_instances
698    }
699
700    /// Like `resolve_all_by_source_path` but also returns the direct parent
701    /// rep index that each leaf instance came from. The "direct parent" is the
702    /// group one level above the leaf in the path.
703    ///
704    /// For `"sg2.sg3"`: parent is the SG2 rep index.
705    /// For `"sg17.sg36.sg40"`: parent is the SG36 rep index (not SG17).
706    ///
707    /// For single-level paths, all indices are 0.
708    ///
709    /// Compute child rep indices for the leaf group in a source_path.
710    /// E.g., for "sg29.sg30", returns the position of each matched SG30 rep
711    /// within its parent SG29's SG30 child group.
712    fn compute_child_indices(
713        tree: &AssembledTree,
714        source_path: &str,
715        indexed: &[(usize, &AssembledGroupInstance)],
716    ) -> Vec<usize> {
717        let parts: Vec<&str> = source_path.split('.').collect();
718        if parts.len() < 2 {
719            return vec![];
720        }
721        // Navigate to the parent level and find the child group
722        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
723        let first_group = match tree
724            .groups
725            .iter()
726            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
727        {
728            Some(g) => g,
729            None => return vec![],
730        };
731        let parent_reps: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
732            find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
733        } else {
734            first_group.repetitions.iter().collect()
735        };
736        // For 2-level paths (sg29.sg30), find the child group in the parent
737        let (child_id, _child_qualifier) = parse_source_path_part(parts[parts.len() - 1]);
738        let mut result = Vec::new();
739        for (_, inst) in indexed {
740            // Find which rep index this instance is at in the child group
741            let mut found = false;
742            for parent in &parent_reps {
743                if let Some(child_group) = parent
744                    .child_groups
745                    .iter()
746                    .find(|g| g.group_id.eq_ignore_ascii_case(child_id))
747                {
748                    if let Some(pos) = child_group
749                        .repetitions
750                        .iter()
751                        .position(|r| std::ptr::eq(r, *inst))
752                    {
753                        result.push(pos);
754                        found = true;
755                        break;
756                    }
757                }
758            }
759            if !found {
760                result.push(usize::MAX); // fallback
761            }
762        }
763        result
764    }
765
766    /// Returns `Vec<(parent_rep_index, &AssembledGroupInstance)>`.
767    pub fn resolve_all_with_parent_indices<'a>(
768        tree: &'a AssembledTree,
769        source_path: &str,
770    ) -> Vec<(usize, &'a AssembledGroupInstance)> {
771        let parts: Vec<&str> = source_path.split('.').collect();
772        if parts.is_empty() {
773            return vec![];
774        }
775
776        // First part: match against top-level groups
777        let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
778        let first_group = match tree
779            .groups
780            .iter()
781            .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
782        {
783            Some(g) => g,
784            None => return vec![],
785        };
786
787        // If single-level path, just return instances with index 0
788        if parts.len() == 1 {
789            let instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
790                find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
791            } else {
792                first_group.repetitions.iter().collect()
793            };
794            return instances.into_iter().map(|i| (0, i)).collect();
795        }
796
797        // Multi-level: navigate tracking (parent_rep_idx, instance) at each level.
798        // At intermediate levels, parent_rep_idx is updated to the current rep's
799        // position within its group. At the leaf level, the parent_rep_idx from
800        // the previous level is preserved — giving us the DIRECT parent index.
801        let first_reps: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = first_qualifier {
802            let matching = find_all_reps_by_entry_qualifier(&first_group.repetitions, q);
803            let mut result = Vec::new();
804            for m in matching {
805                let idx = first_group
806                    .repetitions
807                    .iter()
808                    .position(|r| std::ptr::eq(r, m))
809                    .unwrap_or(0);
810                result.push((idx, m));
811            }
812            result
813        } else {
814            first_group.repetitions.iter().enumerate().collect()
815        };
816
817        let mut current: Vec<(usize, &AssembledGroupInstance)> = first_reps;
818        let remaining = &parts[1..];
819
820        for (level, part) in remaining.iter().enumerate() {
821            let is_leaf = level == remaining.len() - 1;
822            let (group_id, qualifier) = parse_source_path_part(part);
823            let mut next: Vec<(usize, &AssembledGroupInstance)> = Vec::new();
824
825            for (prev_parent_idx, instance) in &current {
826                if let Some(child_group) = instance
827                    .child_groups
828                    .iter()
829                    .find(|g| g.group_id.eq_ignore_ascii_case(group_id))
830                {
831                    let matching: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = qualifier
832                    {
833                        let filtered =
834                            find_all_reps_by_entry_qualifier(&child_group.repetitions, q);
835                        filtered
836                            .into_iter()
837                            .map(|m| {
838                                let idx = child_group
839                                    .repetitions
840                                    .iter()
841                                    .position(|r| std::ptr::eq(r, m))
842                                    .unwrap_or(0);
843                                (idx, m)
844                            })
845                            .collect()
846                    } else {
847                        child_group.repetitions.iter().enumerate().collect()
848                    };
849
850                    for (rep_idx, child_rep) in matching {
851                        if is_leaf {
852                            // At the leaf: keep the parent index from the previous level
853                            next.push((*prev_parent_idx, child_rep));
854                        } else {
855                            // At intermediate: pass down the current rep index
856                            next.push((rep_idx, child_rep));
857                        }
858                    }
859                }
860            }
861
862            current = next;
863        }
864
865        current
866    }
867
868    /// Extract a field from a group instance by path.
869    ///
870    /// Supports qualifier-based segment selection with `tag[qualifier]` syntax:
871    /// - `"dtm.0.1"` → first DTM segment, elements\[0\]\[1\]
872    /// - `"dtm[92].0.1"` → DTM where elements\[0\]\[0\] == "92", then elements\[0\]\[1\]
873    pub fn extract_from_instance(instance: &AssembledGroupInstance, path: &str) -> Option<String> {
874        let parts: Vec<&str> = path.split('.').collect();
875        if parts.is_empty() {
876            return None;
877        }
878
879        // Parse segment tag, optional qualifier, and occurrence index:
880        // "dtm[92]" → ("DTM", Some("92"), 0), "rff[Z34,1]" → ("RFF", Some("Z34"), 1)
881        let (segment_tag, qualifier, occurrence) = parse_tag_qualifier(parts[0]);
882
883        let segment = if let Some(q) = qualifier {
884            instance
885                .segments
886                .iter()
887                .filter(|s| {
888                    s.tag.eq_ignore_ascii_case(&segment_tag)
889                        && s.elements
890                            .first()
891                            .and_then(|e| e.first())
892                            .map(|v| v.as_str())
893                            == Some(q)
894                })
895                .nth(occurrence)?
896        } else {
897            instance
898                .segments
899                .iter()
900                .filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
901                .nth(occurrence)?
902        };
903
904        Self::resolve_field_path(segment, &parts[1..])
905    }
906
907    /// Extract ALL matching values from a group instance for a collect-all path.
908    ///
909    /// Used with wildcard occurrence syntax `tag[qualifier,*]` to collect values
910    /// from every segment matching the qualifier, not just the Nth one.
911    /// Returns a `Vec<String>` of all extracted values in segment order.
912    pub fn extract_all_from_instance(instance: &AssembledGroupInstance, path: &str) -> Vec<String> {
913        let parts: Vec<&str> = path.split('.').collect();
914        if parts.is_empty() {
915            return vec![];
916        }
917
918        let (segment_tag, qualifier, _) = parse_tag_qualifier(parts[0]);
919
920        let matching_segments: Vec<&AssembledSegment> = if let Some(q) = qualifier {
921            instance
922                .segments
923                .iter()
924                .filter(|s| {
925                    s.tag.eq_ignore_ascii_case(&segment_tag)
926                        && s.elements
927                            .first()
928                            .and_then(|e| e.first())
929                            .map(|v| v.as_str())
930                            == Some(q)
931                })
932                .collect()
933        } else {
934            instance
935                .segments
936                .iter()
937                .filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
938                .collect()
939        };
940
941        matching_segments
942            .into_iter()
943            .filter_map(|seg| Self::resolve_field_path(seg, &parts[1..]))
944            .collect()
945    }
946
947    /// Map all fields in a definition from the assembled tree to a BO4E JSON object.
948    ///
949    /// `group_path` is the definition's `source_group` (may be dotted, e.g., "SG4.SG5").
950    /// An empty `source_group` maps root-level segments (BGM, DTM, etc.).
951    /// Returns a flat JSON object with target field names as keys.
952    pub fn map_forward(
953        &self,
954        tree: &AssembledTree,
955        def: &MappingDefinition,
956        repetition: usize,
957    ) -> serde_json::Value {
958        self.map_forward_inner(tree, def, repetition, true)
959    }
960
961    /// Inner implementation with enrichment control.
962    fn map_forward_inner(
963        &self,
964        tree: &AssembledTree,
965        def: &MappingDefinition,
966        repetition: usize,
967        enrich_codes: bool,
968    ) -> serde_json::Value {
969        let mut result = serde_json::Map::new();
970
971        // Root-level mapping: source_group is empty → use tree's own segments.
972        // Include all root segments (both pre-group and post-group, e.g., summary
973        // MOA after UNS+S in REMADV) plus any inter_group_segments (e.g., UNS+S
974        // consumed between groups by the assembler).
975        if def.meta.source_group.is_empty() {
976            let mut all_root_segs = tree.segments.clone();
977            for segs in tree.inter_group_segments.values() {
978                all_root_segs.extend(segs.iter().cloned());
979            }
980            let root_instance = AssembledGroupInstance {
981                segments: all_root_segs,
982                child_groups: vec![],
983                entry_mig_number: None,
984                variant_mig_numbers: vec![],
985                skipped_segments: Vec::new(),
986                skipped_positions: Vec::new(),
987            };
988            self.extract_fields_from_instance(&root_instance, def, &mut result, enrich_codes);
989            return serde_json::Value::Object(result);
990        }
991
992        // Try source_path-based resolution when:
993        //   1. source_path has qualifier suffixes (e.g., "sg4.sg8_z98.sg10")
994        //   2. source_group has no explicit :N indices (those take priority)
995        // This allows definitions without positional indices to navigate via
996        // entry-segment qualifiers (e.g., SEQ qualifier Z98).
997        let instance = if let Some(ref sp) = def.meta.source_path {
998            if has_source_path_qualifiers(sp) && !def.meta.source_group.contains(':') {
999                Self::resolve_by_source_path(tree, sp).or_else(|| {
1000                    Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
1001                })
1002            } else {
1003                Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
1004            }
1005        } else {
1006            Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
1007        };
1008
1009        if let Some(instance) = instance {
1010            // repeat_on_tag: iterate over all segments of that tag, producing an array
1011            if let Some(ref tag) = def.meta.repeat_on_tag {
1012                let matching: Vec<_> = instance
1013                    .segments
1014                    .iter()
1015                    .filter(|s| s.tag.eq_ignore_ascii_case(tag))
1016                    .collect();
1017
1018                if matching.len() > 1 {
1019                    let mut arr = Vec::new();
1020                    for seg in &matching {
1021                        let sub_instance = AssembledGroupInstance {
1022                            segments: vec![(*seg).clone()],
1023                            child_groups: vec![],
1024                            entry_mig_number: None,
1025                            variant_mig_numbers: vec![],
1026                            skipped_segments: Vec::new(),
1027                            skipped_positions: Vec::new(),
1028                        };
1029                        let mut elem_result = serde_json::Map::new();
1030                        self.extract_fields_from_instance(
1031                            &sub_instance,
1032                            def,
1033                            &mut elem_result,
1034                            enrich_codes,
1035                        );
1036                        if !elem_result.is_empty() {
1037                            arr.push(serde_json::Value::Object(elem_result));
1038                        }
1039                    }
1040                    if !arr.is_empty() {
1041                        return serde_json::Value::Array(arr);
1042                    }
1043                }
1044            }
1045
1046            self.extract_fields_from_instance(instance, def, &mut result, enrich_codes);
1047        }
1048
1049        serde_json::Value::Object(result)
1050    }
1051
1052    /// Extract all fields from an instance into a result map.
1053    ///
1054    /// When a `code_lookup` is configured, code-type fields are emitted as
1055    /// `{"code": "E01", "meaning": "..."}` objects. Data-type fields remain plain strings.
1056    fn extract_fields_from_instance(
1057        &self,
1058        instance: &AssembledGroupInstance,
1059        def: &MappingDefinition,
1060        result: &mut serde_json::Map<String, serde_json::Value>,
1061        enrich_codes: bool,
1062    ) {
1063        for (path, field_mapping) in &def.fields {
1064            let (target, enum_map) = match field_mapping {
1065                FieldMapping::Simple(t) => (t.as_str(), None),
1066                FieldMapping::Structured(s) => (
1067                    s.target.as_str(),
1068                    self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
1069                ),
1070                FieldMapping::Nested(_) => continue,
1071            };
1072            if target.is_empty() {
1073                continue;
1074            }
1075            if let Some((list, sub)) = list_target(target) {
1076                self.extract_list_field(
1077                    instance,
1078                    def,
1079                    path,
1080                    list,
1081                    sub,
1082                    enum_map,
1083                    enrich_codes,
1084                    result,
1085                );
1086                continue;
1087            }
1088            if let Some(val) = Self::extract_from_instance(instance, path) {
1089                // Dual decomposition: one EDIFACT code also feeds a second BO4E
1090                // field (e.g. the NAD qualifier carries both partnerrolle and
1091                // datenqualitaet). Without this the code cannot be recovered in
1092                // reverse, because several codes share the primary value.
1093                if let FieldMapping::Structured(s) = field_mapping {
1094                    if let (false, Some(also), Some(also_map)) = (
1095                        self.raw_codes,
1096                        s.also_target.as_deref(),
1097                        self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
1098                    ) {
1099                        if let Some(also_val) = also_map.get(&val) {
1100                            set_nested_value(result, also, also_val.clone());
1101                        }
1102                    }
1103                }
1104
1105                let mapped_val = match enum_map {
1106                    Some(map) if !self.raw_codes => {
1107                        map.get(&val).cloned().unwrap_or_else(|| val.clone())
1108                    }
1109                    _ => val.clone(),
1110                };
1111
1112                // Enrich code fields with meaning from PID schema
1113                if enrich_codes {
1114                    if let (Some(ref code_lookup), Some(ref source_path)) =
1115                        (&self.code_lookup, &def.meta.source_path)
1116                    {
1117                        let parts: Vec<&str> = path.split('.').collect();
1118                        let (seg_tag, path_qualifier, _occ) = parse_tag_qualifier(parts[0]);
1119                        let (element_idx, component_idx) =
1120                            Self::parse_element_component(&parts[1..]);
1121                        let disc_qualifier = Self::discriminator_qualifier_for_tag(def, &seg_tag);
1122                        let q = disc_qualifier.as_deref();
1123
1124                        if let Some(codes) = code_lookup.enrichment_codes(
1125                            source_path,
1126                            &seg_tag,
1127                            path_qualifier,
1128                            q,
1129                            element_idx,
1130                            component_idx,
1131                        ) {
1132                            // Class C: PID self-reference — emit a plain string,
1133                            // skipping {code, meaning, enum} decoration when the
1134                            // schema's only allowed value at this position is the
1135                            // PID itself.
1136                            if let Some(ref pid) = self.current_pid {
1137                                if codes.len() == 1 && codes.contains_key(pid.as_str()) {
1138                                    set_nested_value(result, target, mapped_val);
1139                                    continue;
1140                                }
1141                            }
1142
1143                            // Look up the original EDIFACT value for enrichment,
1144                            // since schema codes use raw values (e.g., "293")
1145                            // not enum_map targets (e.g., "BDEW").
1146                            let enrichment = codes.get(&val);
1147                            let meaning = enrichment
1148                                .map(|e| serde_json::Value::String(e.meaning.clone()))
1149                                .unwrap_or(serde_json::Value::Null);
1150
1151                            let mut obj = serde_json::Map::new();
1152                            obj.insert("code".into(), serde_json::json!(mapped_val));
1153                            obj.insert("meaning".into(), meaning);
1154                            if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
1155                                obj.insert("enum".into(), serde_json::json!(enum_key));
1156                            }
1157                            let enriched = serde_json::Value::Object(obj);
1158                            set_nested_value_json(result, target, enriched);
1159                            continue;
1160                        }
1161                    }
1162                }
1163
1164                set_nested_value(result, target, mapped_val);
1165            }
1166        }
1167
1168        // Also for `parent_field` children: they can be parents of deeper
1169        // `parent_field` definitions (SG15 → SG17 → SG18).
1170        if !instance.child_groups.is_empty() {
1171            self.extract_nested_children(instance, def, result, enrich_codes);
1172        }
1173    }
1174
1175    /// Forward half of a list target (`werte[].code`): one element of the array
1176    /// per segment the path matches — `cav[*,*]` every CAV, `cav[Z30,*]` every
1177    /// CAV+Z30 — in wire order. A positional slot (`cav[*,1]` → `wert2`) names
1178    /// a position, and the SG10 CAVs are identified by their code, not their
1179    /// place: an absent first CAV moved the second into the first's field, and
1180    /// CAVs beyond the last slot were dropped. The list keeps every CAV with
1181    /// its own code.
1182    #[allow(clippy::too_many_arguments)]
1183    fn extract_list_field(
1184        &self,
1185        instance: &AssembledGroupInstance,
1186        def: &MappingDefinition,
1187        path: &str,
1188        list: &str,
1189        sub: &str,
1190        enum_map: Option<&std::collections::BTreeMap<String, String>>,
1191        enrich_codes: bool,
1192        result: &mut serde_json::Map<String, serde_json::Value>,
1193    ) {
1194        let parts: Vec<&str> = path.split('.').collect();
1195        if parts.len() < 2 {
1196            return;
1197        }
1198        let (seg_tag, qualifier, _) = parse_tag_qualifier(parts[0]);
1199        let segments: Vec<&AssembledSegment> = instance
1200            .segments
1201            .iter()
1202            .filter(|s| {
1203                s.tag.eq_ignore_ascii_case(&seg_tag)
1204                    && qualifier.map_or(true, |q| {
1205                        s.elements
1206                            .first()
1207                            .and_then(|e| e.first())
1208                            .map(|v| v.as_str())
1209                            == Some(q)
1210                    })
1211            })
1212            .collect();
1213        if segments.is_empty() {
1214            return;
1215        }
1216        let items = result
1217            .entry(list.to_string())
1218            .or_insert_with(|| serde_json::Value::Array(Vec::new()));
1219        let Some(items) = items.as_array_mut() else {
1220            return;
1221        };
1222        while items.len() < segments.len() {
1223            items.push(serde_json::Value::Object(serde_json::Map::new()));
1224        }
1225        let codes = if enrich_codes {
1226            match (&self.code_lookup, &def.meta.source_path) {
1227                (Some(lookup), Some(source_path)) => {
1228                    let (element_idx, component_idx) = Self::parse_element_component(&parts[1..]);
1229                    let disc = Self::discriminator_qualifier_for_tag(def, &seg_tag);
1230                    lookup
1231                        .enrichment_codes(
1232                            source_path,
1233                            &seg_tag,
1234                            qualifier,
1235                            disc.as_deref(),
1236                            element_idx,
1237                            component_idx,
1238                        )
1239                        .cloned()
1240                }
1241                _ => None,
1242            }
1243        } else {
1244            None
1245        };
1246        for (item, segment) in items.iter_mut().zip(segments) {
1247            let Some(val) = Self::resolve_field_path(segment, &parts[1..]) else {
1248                continue;
1249            };
1250            let mapped = match enum_map {
1251                Some(map) if !self.raw_codes => {
1252                    map.get(&val).cloned().unwrap_or_else(|| val.clone())
1253                }
1254                _ => val.clone(),
1255            };
1256            let value = match &codes {
1257                Some(codes) => {
1258                    let enrichment = codes.get(&val);
1259                    let mut obj = serde_json::Map::new();
1260                    obj.insert("code".into(), serde_json::json!(mapped));
1261                    obj.insert(
1262                        "meaning".into(),
1263                        enrichment
1264                            .map(|e| serde_json::Value::String(e.meaning.clone()))
1265                            .unwrap_or(serde_json::Value::Null),
1266                    );
1267                    if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
1268                        obj.insert("enum".into(), serde_json::json!(enum_key));
1269                    }
1270                    serde_json::Value::Object(obj)
1271                }
1272                None => serde_json::Value::String(mapped),
1273            };
1274            if let Some(obj) = item.as_object_mut() {
1275                set_nested_value_json(obj, sub, value);
1276            }
1277        }
1278    }
1279
1280    /// Forward half of `[meta] parent_field`: map the child groups of `instance`
1281    /// (the parent group repetition `def` was just extracted from) into array
1282    /// fields of the same object. Placement is instance-local — a child can only
1283    /// land in the object produced from the group repetition that contains it.
1284    fn extract_nested_children(
1285        &self,
1286        instance: &AssembledGroupInstance,
1287        def: &MappingDefinition,
1288        result: &mut serde_json::Map<String, serde_json::Value>,
1289        enrich_codes: bool,
1290    ) {
1291        for child in self
1292            .definitions
1293            .iter()
1294            .filter(|c| is_nested_child_of(c, def))
1295        {
1296            if nested_parent_qualifier(child).is_some_and(|q| !entry_qualifier_matches(instance, q))
1297            {
1298                continue;
1299            }
1300            let (leaf_id, leaf_qualifier) = nested_child_leaf(child);
1301            let Some(group) = instance
1302                .child_groups
1303                .iter()
1304                .find(|g| g.group_id.eq_ignore_ascii_case(&leaf_id))
1305            else {
1306                continue;
1307            };
1308            let reps: Vec<&AssembledGroupInstance> = match leaf_qualifier {
1309                Some(q) => find_all_reps_by_entry_qualifier(&group.repetitions, q),
1310                None => group.repetitions.iter().collect(),
1311            };
1312
1313            let mut items: Vec<serde_json::Value> = Vec::new();
1314            let mut push_item = |sub: &AssembledGroupInstance| {
1315                let mut obj = serde_json::Map::new();
1316                self.extract_fields_from_instance(sub, child, &mut obj, enrich_codes);
1317                if !obj.is_empty() {
1318                    items.push(serde_json::Value::Object(obj));
1319                }
1320            };
1321            for rep in reps {
1322                let repeat_tag = child
1323                    .meta
1324                    .repeat_on_tag
1325                    .as_deref()
1326                    .filter(|tag| rep.segments.iter().any(|s| s.tag.eq_ignore_ascii_case(tag)));
1327                let Some(tag) = repeat_tag else {
1328                    push_item(rep);
1329                    continue;
1330                };
1331                // One element per repeating segment; the group's other segments
1332                // (e.g. the CTA entry segment) are visible to every element.
1333                let shared: Vec<AssembledSegment> = rep
1334                    .segments
1335                    .iter()
1336                    .filter(|s| !s.tag.eq_ignore_ascii_case(tag))
1337                    .cloned()
1338                    .collect();
1339                for seg in rep
1340                    .segments
1341                    .iter()
1342                    .filter(|s| s.tag.eq_ignore_ascii_case(tag))
1343                {
1344                    let mut segments = shared.clone();
1345                    segments.push(seg.clone());
1346                    push_item(&AssembledGroupInstance {
1347                        segments,
1348                        child_groups: vec![],
1349                        entry_mig_number: None,
1350                        variant_mig_numbers: vec![],
1351                        skipped_segments: Vec::new(),
1352                        skipped_positions: Vec::new(),
1353                    });
1354                }
1355            }
1356            if items.is_empty() {
1357                continue;
1358            }
1359            let field = child.meta.parent_field.as_deref().unwrap_or_default();
1360            match result.get_mut(field) {
1361                Some(serde_json::Value::Array(existing)) => existing.extend(items),
1362                _ => {
1363                    result.insert(field.to_string(), serde_json::Value::Array(items));
1364                }
1365            }
1366        }
1367
1368        // Bound children write into this same object (see `is_bound_child_of`).
1369        for child in self
1370            .definitions
1371            .iter()
1372            .filter(|c| is_bound_child_of(c, def))
1373        {
1374            for rep in bound_child_reps(instance, child) {
1375                self.extract_fields_from_instance(rep, child, result, enrich_codes);
1376            }
1377        }
1378    }
1379
1380    /// Reverse half of `[meta] parent_field`: emit the elements of
1381    /// `bo4e_value[parent_field]` as child group(s) of `instance`, the parent
1382    /// group repetition just rebuilt from that same object.
1383    fn reverse_nested_children(
1384        &self,
1385        bo4e_value: &serde_json::Value,
1386        def: &MappingDefinition,
1387        instance: &mut AssembledGroupInstance,
1388    ) {
1389        let mut handled_fields: Vec<&str> = Vec::new();
1390        for child in self
1391            .definitions
1392            .iter()
1393            .filter(|c| is_nested_child_of(c, def))
1394        {
1395            let field = child.meta.parent_field.as_deref().unwrap_or_default();
1396            if handled_fields.contains(&field) {
1397                continue;
1398            }
1399            if nested_parent_qualifier(child)
1400                .is_some_and(|q| !rebuilt_entry_qualifier_matches(instance, def, q))
1401            {
1402                continue;
1403            }
1404            let elements: Vec<&serde_json::Value> = match bo4e_value.get(field) {
1405                Some(serde_json::Value::Array(arr)) => arr.iter().collect(),
1406                Some(serde_json::Value::Null) | None => continue,
1407                Some(other) => vec![other],
1408            };
1409
1410            let mut reps: Vec<AssembledGroupInstance> = Vec::new();
1411            if let Some(tag) = child.meta.repeat_on_tag.as_deref() {
1412                // All elements share one group repetition: the non-repeating
1413                // segments (entry segment) once, then one repeating segment each.
1414                let mut merged: Option<AssembledGroupInstance> = None;
1415                for element in elements {
1416                    let sub = self.map_reverse_single(element, child);
1417                    if sub.segments.is_empty() {
1418                        continue;
1419                    }
1420                    match merged.as_mut() {
1421                        None => merged = Some(sub),
1422                        Some(m) => m.segments.extend(
1423                            sub.segments
1424                                .into_iter()
1425                                .filter(|s| s.tag.eq_ignore_ascii_case(tag)),
1426                        ),
1427                    }
1428                }
1429                reps.extend(merged);
1430            } else {
1431                for element in elements {
1432                    let mut sub = self.map_reverse_single(element, child);
1433                    if sub.segments.is_empty() {
1434                        continue;
1435                    }
1436                    // Grandchildren nested in this element (multi-level nesting).
1437                    self.reverse_nested_children(element, child, &mut sub);
1438                    reps.push(sub);
1439                }
1440            }
1441            if reps.is_empty() {
1442                continue;
1443            }
1444            handled_fields.push(field);
1445
1446            let (leaf_id, _) = nested_child_leaf(child);
1447            push_child_reps(instance, leaf_id, reps);
1448        }
1449
1450        // Bound children are rebuilt from this same object, one repetition
1451        // each, under the repetition just rebuilt from it.
1452        for child in self
1453            .definitions
1454            .iter()
1455            .filter(|c| is_bound_child_of(c, def))
1456        {
1457            let mut sub = self.map_reverse_single(bo4e_value, child);
1458            if sub.segments.is_empty() {
1459                continue;
1460            }
1461            self.reverse_nested_children(bo4e_value, child, &mut sub);
1462            let (leaf_id, _) = nested_child_leaf(child);
1463            push_child_reps(instance, leaf_id, vec![sub]);
1464        }
1465    }
1466
1467    /// Extract the discriminator's qualifier value from a definition's `[meta]`.
1468    ///
1469    /// `discriminator` strings look like `"RFF.0.0=Z13"` (numeric, post path-resolution)
1470    /// or `"RFF.c506.d1153=TN"` (named, pre-resolution). The qualifier is the
1471    /// substring after the first `=`. Returns `None` when no discriminator is set
1472    /// or the format is unexpected.
1473    pub(crate) fn discriminator_qualifier(def: &MappingDefinition) -> Option<String> {
1474        def.meta
1475            .discriminator
1476            .as_deref()
1477            .and_then(|d| d.split_once('=').map(|(_, v)| v.to_string()))
1478    }
1479
1480    /// The discriminator value when the discriminator selects on `segment_tag`
1481    /// itself (`RFF.0.0=Z13` for an RFF field). A discriminator on another segment
1482    /// (`SEQ.0.0=Z98` for a CCI field) says nothing about which variant of
1483    /// `segment_tag` a field reads.
1484    pub(crate) fn discriminator_qualifier_for_tag(
1485        def: &MappingDefinition,
1486        segment_tag: &str,
1487    ) -> Option<String> {
1488        let (lhs, value) = def.meta.discriminator.as_deref()?.split_once('=')?;
1489        let disc_tag = lhs.split('.').next().unwrap_or(lhs);
1490        disc_tag
1491            .eq_ignore_ascii_case(segment_tag)
1492            .then(|| value.to_string())
1493    }
1494
1495    /// Map a PID struct field's segments to BO4E JSON.
1496    ///
1497    /// `segments` are the `OwnedSegment`s from a PID wrapper field.
1498    /// Converts to `AssembledSegment` format for compatibility with existing
1499    /// field extraction logic, then applies the definition's field mappings.
1500    pub fn map_forward_from_segments(
1501        &self,
1502        segments: &[OwnedSegment],
1503        def: &MappingDefinition,
1504    ) -> serde_json::Value {
1505        let assembled_segments: Vec<AssembledSegment> = segments
1506            .iter()
1507            .map(|s| AssembledSegment {
1508                tag: s.id.clone(),
1509                elements: s.elements.clone(),
1510                mig_number: None,
1511                segment_number: Some(s.segment_number),
1512            })
1513            .collect();
1514
1515        let instance = AssembledGroupInstance {
1516            segments: assembled_segments,
1517            child_groups: vec![],
1518            entry_mig_number: None,
1519            variant_mig_numbers: vec![],
1520            skipped_segments: Vec::new(),
1521            skipped_positions: Vec::new(),
1522        };
1523
1524        let mut result = serde_json::Map::new();
1525        self.extract_fields_from_instance(&instance, def, &mut result, true);
1526        serde_json::Value::Object(result)
1527    }
1528
1529    // ── Reverse mapping: BO4E → tree ──
1530
1531    /// Map a BO4E JSON object back to an assembled group instance.
1532    ///
1533    /// Uses the definition's field mappings to populate segment elements.
1534    /// Fields with `default` values are used when no BO4E value is present
1535    /// (useful for fixed qualifiers like LOC qualifier "Z16").
1536    ///
1537    /// Supports:
1538    /// - Named paths: `"d3227"` → element\[0\]\[0\], `"c517.d3225"` → element\[1\]\[0\]
1539    /// - Numeric index: `"0"` → element\[0\]\[0\], `"1.2"` → element\[1\]\[2\]
1540    /// - Qualifier selection: `"dtm[92].0.1"` → DTM segment with qualifier "92"
1541    pub fn map_reverse(
1542        &self,
1543        bo4e_value: &serde_json::Value,
1544        def: &MappingDefinition,
1545    ) -> AssembledGroupInstance {
1546        // repeat_on_tag + array input: reverse each element independently, merge segments
1547        if def.meta.repeat_on_tag.is_some() {
1548            if let Some(arr) = bo4e_value.as_array() {
1549                let mut all_segments = Vec::new();
1550                for elem in arr {
1551                    let sub = self.map_reverse_single(elem, def);
1552                    all_segments.extend(sub.segments);
1553                }
1554                return AssembledGroupInstance {
1555                    segments: all_segments,
1556                    child_groups: vec![],
1557                    entry_mig_number: None,
1558                    variant_mig_numbers: vec![],
1559                    skipped_segments: Vec::new(),
1560                    skipped_positions: Vec::new(),
1561                };
1562            }
1563        }
1564        let mut instance = self.map_reverse_single(bo4e_value, def);
1565        if def.meta.parent_field.is_some() {
1566            return instance;
1567        }
1568        if !instance.segments.is_empty() {
1569            self.reverse_nested_children(bo4e_value, def, &mut instance);
1570            return instance;
1571        }
1572        // Nothing of the parent's own but its constants: still a repetition
1573        // when a child group of it carries data (a `MarktlokationDaten` holding
1574        // only `haushaltskunde`). The child needs its parent's entry segment,
1575        // which the phantom check would otherwise drop with the constants.
1576        // The children are judged against that rebuilt entry segment: a child
1577        // qualified on its parent's variant (`sg4.sg8_z98.sg10`) matches only
1578        // a `SEQ+Z98`, never an empty probe.
1579        let mut with_constants = self.map_reverse_single_inner(bo4e_value, def, true);
1580        if with_constants.segments.is_empty() {
1581            return instance;
1582        }
1583        self.reverse_nested_children(bo4e_value, def, &mut with_constants);
1584        if with_constants.child_groups.is_empty() && !self.stands_for_bare_group(bo4e_value, def) {
1585            return instance;
1586        }
1587        with_constants
1588    }
1589
1590    /// Whether `bo4e_value` is the `{}` the forward pass writes for a group
1591    /// present without data, and `def` the group it stands for.
1592    ///
1593    /// MSCONS 13017/13019 name the Meldepunkt by its ID (`LOC+172+<ID>`) or,
1594    /// with an SG7 RFF+AGK, by nothing (`LOC+172`): the rule maps
1595    /// `lokationsId`, so without this the bare LOC+172 would be judged a
1596    /// phantom. `{}` says which group it was only when one group writes the
1597    /// entity: with several, it would bring back every one of them.
1598    fn stands_for_bare_group(
1599        &self,
1600        bo4e_value: &serde_json::Value,
1601        def: &MappingDefinition,
1602    ) -> bool {
1603        bo4e_value.as_object().is_some_and(|o| o.is_empty())
1604            && self
1605                .definitions
1606                .iter()
1607                .filter(|d| d.meta.entity == def.meta.entity && d.meta.parent_field.is_none())
1608                .all(|d| d.meta.source_path == def.meta.source_path)
1609    }
1610
1611    fn map_reverse_single(
1612        &self,
1613        bo4e_value: &serde_json::Value,
1614        def: &MappingDefinition,
1615    ) -> AssembledGroupInstance {
1616        self.map_reverse_single_inner(bo4e_value, def, false)
1617    }
1618
1619    /// `keep_constants`: emit the definition's constants even when none of its
1620    /// data fields resolved (the parent of a child group with data).
1621    fn map_reverse_single_inner(
1622        &self,
1623        bo4e_value: &serde_json::Value,
1624        def: &MappingDefinition,
1625        keep_constants: bool,
1626    ) -> AssembledGroupInstance {
1627        // Collect (segment_key, element_index, component_index, value) tuples.
1628        // segment_key includes qualifier for disambiguation: "DTM" or "DTM[92]".
1629        let mut field_values: Vec<(String, String, usize, usize, String)> =
1630            Vec::with_capacity(def.fields.len());
1631
1632        // Track whether any field with a non-empty target resolved to an actual
1633        // BO4E value.  When a definition has data fields but none resolved to
1634        // values, only defaults (qualifiers) would be emitted — producing phantom
1635        // segments for groups not present in the original EDIFACT message.
1636        // Definitions with ONLY qualifier/default fields (no data targets) are
1637        // "container" definitions (e.g., SEQ entry segments) and are always kept.
1638        let mut has_real_data = false;
1639        let mut has_data_fields = false;
1640        // Per-segment phantom tracking: segments with data fields but no resolved
1641        // data are phantoms — their entries should be removed from field_values.
1642        let mut seg_has_data_field: HashSet<String> = HashSet::new();
1643        let mut seg_has_real_data: HashSet<String> = HashSet::new();
1644        let mut injected_qualifiers: HashSet<String> = HashSet::new();
1645        // List-target fields (`werte[].code`), emitted after the loop item by
1646        // item so the segments come out in the list's order.
1647        type ListField<'a> = (
1648            &'a str,
1649            &'a str,
1650            String,
1651            Option<String>,
1652            usize,
1653            usize,
1654            Option<&'a std::collections::BTreeMap<String, String>>,
1655        );
1656        let mut list_fields: Vec<ListField<'_>> = Vec::new();
1657
1658        for (path, field_mapping) in &def.fields {
1659            let (target, default, enum_map, when_filled, also_target, also_enum_map) =
1660                match field_mapping {
1661                    FieldMapping::Simple(t) => (t.as_str(), None, None, None, None, None),
1662                    FieldMapping::Structured(s) => (
1663                        s.target.as_str(),
1664                        s.default.as_ref(),
1665                        self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
1666                        s.when_filled.as_ref(),
1667                        s.also_target.as_deref(),
1668                        self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
1669                    ),
1670                    FieldMapping::Nested(_) => continue,
1671                };
1672
1673            let parts: Vec<&str> = path.split('.').collect();
1674            if parts.len() < 2 {
1675                continue;
1676            }
1677
1678            let (seg_tag, qualifier, _occ) = parse_tag_qualifier(parts[0]);
1679            // Use the raw first part as segment key to group fields by segment instance.
1680            // Indexed qualifiers like "RFF[Z34,1]" produce a distinct key from "RFF[Z34]".
1681            let seg_key = parts[0].to_uppercase();
1682            let sub_path = &parts[1..];
1683
1684            // Determine (element_idx, component_idx) from path
1685            let (element_idx, component_idx) = if let Ok(ei) = sub_path[0].parse::<usize>() {
1686                let ci = if sub_path.len() > 1 {
1687                    sub_path[1].parse::<usize>().unwrap_or(0)
1688                } else {
1689                    0
1690                };
1691                (ei, ci)
1692            } else {
1693                match sub_path.len() {
1694                    1 => (0, 0),
1695                    2 => (1, 0),
1696                    _ => continue,
1697                }
1698            };
1699
1700            if let Some((list, sub)) = list_target(target) {
1701                list_fields.push((
1702                    list,
1703                    sub,
1704                    seg_tag.clone(),
1705                    qualifier.map(str::to_string),
1706                    element_idx,
1707                    component_idx,
1708                    enum_map,
1709                ));
1710                continue;
1711            }
1712
1713            // Try BO4E value first, fall back to default
1714            let val = if target.is_empty() {
1715                match (default, when_filled) {
1716                    // has when_filled → conditional injection
1717                    (Some(d), Some(fields)) => {
1718                        let any_filled = fields.iter().any(|f| field_is_filled(bo4e_value, f));
1719                        if any_filled {
1720                            // A successful when_filled check confirms real data
1721                            // exists — prevent phantom suppression.
1722                            has_real_data = true;
1723                            Some(d.clone())
1724                        } else {
1725                            None
1726                        }
1727                    }
1728                    // no when_filled → unconditional (backward compat)
1729                    (Some(d), None) => Some(d.clone()),
1730                    (None, _) => None,
1731                }
1732            } else {
1733                has_data_fields = true;
1734                seg_has_data_field.insert(seg_key.clone());
1735                let bo4e_val = self.populate_field(bo4e_value, target);
1736                if bo4e_val.is_some() {
1737                    has_real_data = true;
1738                    seg_has_real_data.insert(seg_key.clone());
1739                }
1740                // Apply reverse enum_map: BO4E value → EDIFACT value
1741                let mapped_val = match (bo4e_val, enum_map) {
1742                    (Some(v), Some(map)) => {
1743                        // Dual decomposition (`also_target`): one EDIFACT code was
1744                        // split across two BO4E fields, so neither alone identifies
1745                        // it. Find the code both maps agree on; several codes share
1746                        // a `partnerrolle` and are told apart only by the second
1747                        // field. Falls back to the single-map lookup when the
1748                        // second field is absent or no code matches both.
1749                        let joint = match (also_target, also_enum_map) {
1750                            (Some(also), Some(also_map)) => {
1751                                self.populate_field(bo4e_value, also).and_then(|also_v| {
1752                                    map.iter()
1753                                        .find(|(code, bo4e_v)| {
1754                                            *bo4e_v == &v && also_map.get(*code) == Some(&also_v)
1755                                        })
1756                                        .map(|(code, _)| code.clone())
1757                                })
1758                            }
1759                            _ => None,
1760                        };
1761                        joint
1762                            .or_else(|| {
1763                                // Reverse lookup: find EDIFACT key for BO4E value
1764                                map.iter()
1765                                    .find(|(_, bo4e_v)| *bo4e_v == &v)
1766                                    .map(|(edifact_k, _)| edifact_k.clone())
1767                            })
1768                            .or(Some(v))
1769                    }
1770                    (v, _) => v,
1771                };
1772                mapped_val.or_else(|| default.cloned())
1773            };
1774
1775            if let Some(val) = val {
1776                field_values.push((
1777                    seg_key.clone(),
1778                    seg_tag.clone(),
1779                    element_idx,
1780                    component_idx,
1781                    val,
1782                ));
1783            }
1784
1785            // If there's a qualifier, also inject it at elements[0][0]
1786            if let Some(q) = qualifier {
1787                if injected_qualifiers.insert(seg_key.clone()) {
1788                    field_values.push((seg_key, seg_tag, 0, 0, q.to_string()));
1789                }
1790            }
1791        }
1792
1793        // Reverse half of list targets: element i of the array becomes the i-th
1794        // segment (`CAV[*,i]`, or `CAV[Q,i]` with the qualifier written back).
1795        let longest = list_fields
1796            .iter()
1797            .filter_map(|(list, ..)| bo4e_value.get(*list).and_then(|v| v.as_array()))
1798            .map(|a| a.len())
1799            .max()
1800            .unwrap_or(0);
1801        if !list_fields.is_empty() {
1802            has_data_fields = true;
1803        }
1804        for i in 0..longest {
1805            for (list, sub, seg_tag, qualifier, element_idx, component_idx, enum_map) in
1806                &list_fields
1807            {
1808                let key = match qualifier {
1809                    Some(q) => format!("{seg_tag}[{q},{i}]"),
1810                    None => format!("{seg_tag}[*,{i}]"),
1811                };
1812                seg_has_data_field.insert(key.clone());
1813                let Some(item) = bo4e_value
1814                    .get(*list)
1815                    .and_then(|v| v.as_array())
1816                    .and_then(|a| a.get(i))
1817                else {
1818                    continue;
1819                };
1820                let Some(value) = self.populate_field(item, sub) else {
1821                    continue;
1822                };
1823                let value = match enum_map {
1824                    Some(map) => map
1825                        .iter()
1826                        .find(|(_, name)| **name == value)
1827                        .map(|(code, _)| code.clone())
1828                        .unwrap_or(value),
1829                    None => value,
1830                };
1831                has_real_data = true;
1832                seg_has_real_data.insert(key.clone());
1833                field_values.push((
1834                    key.clone(),
1835                    seg_tag.clone(),
1836                    *element_idx,
1837                    *component_idx,
1838                    value,
1839                ));
1840                if let Some(q) = qualifier {
1841                    if injected_qualifiers.insert(key.clone()) {
1842                        field_values.push((key, seg_tag.clone(), 0, 0, q.clone()));
1843                    }
1844                }
1845            }
1846        }
1847
1848        // Per-segment phantom prevention for qualified segments: remove entries
1849        // for segments using tag[qualifier] syntax (e.g., FTX[ACB], DTM[Z07])
1850        // that have data fields but none resolved to actual BO4E values.  This
1851        // prevents phantom segments when a definition maps multiple segment types
1852        // and optional qualified segments are not in the original message.
1853        // Unqualified segments (plain tags like SEQ, IDE) are always kept — they
1854        // are typically entry/mandatory segments of their group.
1855        field_values.retain(|(seg_key, _, _, _, _)| {
1856            if !seg_key.contains('[') {
1857                return true; // unqualified segments always kept
1858            }
1859            !seg_has_data_field.contains(seg_key) || seg_has_real_data.contains(seg_key)
1860        });
1861
1862        // If the definition has data fields but none resolved to actual BO4E values,
1863        // return an empty instance to prevent phantom segments for groups not
1864        // present in the original EDIFACT message.  Definitions with only
1865        // qualifier/default fields (has_data_fields=false) are always kept.
1866        if has_data_fields && !has_real_data && !keep_constants {
1867            return AssembledGroupInstance {
1868                segments: vec![],
1869                child_groups: vec![],
1870                entry_mig_number: None,
1871                variant_mig_numbers: vec![],
1872                skipped_segments: Vec::new(),
1873                skipped_positions: Vec::new(),
1874            };
1875        }
1876
1877        // Build segments with elements/components in correct positions.
1878        // Group by segment_key to create separate segments for "DTM[92]" vs "DTM[93]".
1879        let mut segments: Vec<AssembledSegment> = Vec::with_capacity(field_values.len());
1880        let mut seen_keys: HashMap<String, usize> = HashMap::new();
1881
1882        for (seg_key, seg_tag, element_idx, component_idx, val) in &field_values {
1883            let seg = if let Some(&pos) = seen_keys.get(seg_key) {
1884                &mut segments[pos]
1885            } else {
1886                let pos = segments.len();
1887                seen_keys.insert(seg_key.clone(), pos);
1888                segments.push(AssembledSegment {
1889                    tag: seg_tag.clone(),
1890                    elements: vec![],
1891                    mig_number: None,
1892                    segment_number: None,
1893                });
1894                &mut segments[pos]
1895            };
1896
1897            while seg.elements.len() <= *element_idx {
1898                seg.elements.push(vec![]);
1899            }
1900            while seg.elements[*element_idx].len() <= *component_idx {
1901                seg.elements[*element_idx].push(String::new());
1902            }
1903            seg.elements[*element_idx][*component_idx] = val.clone();
1904        }
1905
1906        // Pad intermediate empty elements: any [] between position 0 and the last
1907        // populated position becomes [""] so the EDIFACT renderer emits the `+` separator.
1908        for seg in &mut segments {
1909            let last_populated = seg.elements.iter().rposition(|e| !e.is_empty());
1910            if let Some(last_idx) = last_populated {
1911                for i in 0..last_idx {
1912                    if seg.elements[i].is_empty() {
1913                        seg.elements[i] = vec![String::new()];
1914                    }
1915                }
1916            }
1917        }
1918
1919        // MIG-aware trailing padding: extend each segment to the MIG-defined element count.
1920        if let Some(ref ss) = self.segment_structure {
1921            for seg in &mut segments {
1922                if let Some(expected) = ss.element_count(&seg.tag) {
1923                    while seg.elements.len() < expected {
1924                        seg.elements.push(vec![String::new()]);
1925                    }
1926                }
1927            }
1928        }
1929
1930        AssembledGroupInstance {
1931            segments,
1932            child_groups: vec![],
1933            entry_mig_number: None,
1934            variant_mig_numbers: vec![],
1935            skipped_segments: Vec::new(),
1936            skipped_positions: Vec::new(),
1937        }
1938    }
1939
1940    /// Resolve a field path within a segment to extract a value.
1941    ///
1942    /// Two path conventions are supported:
1943    ///
1944    /// **Named paths** (backward compatible):
1945    /// - 1-part `"d3227"` → elements\[0\]\[0\]
1946    /// - 2-part `"c517.d3225"` → elements\[1\]\[0\]
1947    ///
1948    /// **Numeric index paths** (for multi-component access):
1949    /// - `"0"` → elements\[0\]\[0\]
1950    /// - `"1.0"` → elements\[1\]\[0\]
1951    /// - `"1.2"` → elements\[1\]\[2\]
1952    fn resolve_field_path(segment: &AssembledSegment, path: &[&str]) -> Option<String> {
1953        if path.is_empty() {
1954            return None;
1955        }
1956
1957        // Numeric paths only: index-based resolution.
1958        if let Ok(element_idx) = path[0].parse::<usize>() {
1959            let component_idx = if path.len() > 1 {
1960                path[1].parse::<usize>().unwrap_or(0)
1961            } else {
1962                0
1963            };
1964            return segment
1965                .elements
1966                .get(element_idx)?
1967                .get(component_idx)
1968                .filter(|v| !v.is_empty())
1969                .cloned();
1970        }
1971
1972        // Non-numeric path[0] indicates an EDIFACT ID path that the PathResolver
1973        // failed to normalize (e.g. composite/element absent from any loaded PID
1974        // schema). Returning None lets the field be omitted from output instead
1975        // of silently guessing element index 1, which previously surfaced
1976        // unrelated data (e.g. NAD c819.d3229 read as c082.d3039 / rollencodenummer).
1977        None
1978    }
1979
1980    /// Parse element and component indices from path parts after the segment tag.
1981    /// E.g., ["2"] -> (2, 0), ["0", "3"] -> (0, 3), ["1", "0"] -> (1, 0)
1982    pub(crate) fn parse_element_component(parts: &[&str]) -> (usize, usize) {
1983        if parts.is_empty() {
1984            return (0, 0);
1985        }
1986        let element_idx = parts[0].parse::<usize>().unwrap_or(0);
1987        let component_idx = if parts.len() > 1 {
1988            parts[1].parse::<usize>().unwrap_or(0)
1989        } else {
1990            0
1991        };
1992        (element_idx, component_idx)
1993    }
1994
1995    /// Extract a value from a BO4E JSON object by target field name.
1996    /// Supports dotted paths like "nested.field_name".
1997    pub fn populate_field(
1998        &self,
1999        bo4e_value: &serde_json::Value,
2000        target_field: &str,
2001    ) -> Option<String> {
2002        let mut current = bo4e_value;
2003        for part in target_field.split('.') {
2004            current = current.get(part)?;
2005        }
2006        // Handle enriched code objects: {"code": "Z15", "meaning": "..."}
2007        if let Some(code) = current.get("code").and_then(|v| v.as_str()) {
2008            return Some(code.to_string());
2009        }
2010        current.as_str().map(|s| s.to_string())
2011    }
2012
2013    /// Build a segment from BO4E values using the reverse mapping.
2014    pub fn build_segment_from_bo4e(
2015        &self,
2016        bo4e_value: &serde_json::Value,
2017        segment_tag: &str,
2018        target_field: &str,
2019    ) -> AssembledSegment {
2020        let value = self.populate_field(bo4e_value, target_field);
2021        let elements = if let Some(val) = value {
2022            vec![vec![val]]
2023        } else {
2024            vec![]
2025        };
2026        AssembledSegment {
2027            tag: segment_tag.to_uppercase(),
2028            elements,
2029            mig_number: None,
2030            segment_number: None,
2031        }
2032    }
2033
2034    // ── Multi-entity forward mapping ──
2035
2036    /// Parse a discriminator string (e.g., "SEQ.0.0=Z79") and find the matching
2037    /// repetition index within the given group path.
2038    ///
2039    /// Discriminator format: `"TAG.element_idx.component_idx=expected_value"`
2040    /// Scans all repetitions of the leaf group and returns the first rep index
2041    /// where the entry segment matches.
2042    pub fn resolve_repetition(
2043        tree: &AssembledTree,
2044        group_path: &str,
2045        discriminator: &str,
2046    ) -> Option<usize> {
2047        let (spec, expected) = discriminator.split_once('=')?;
2048        let parts: Vec<&str> = spec.split('.').collect();
2049        if parts.len() != 3 {
2050            return None;
2051        }
2052        let tag = parts[0];
2053        let element_idx: usize = parts[1].parse().ok()?;
2054        let component_idx: usize = parts[2].parse().ok()?;
2055
2056        // Navigate to the parent and get the leaf group with all its repetitions
2057        let path_parts: Vec<&str> = group_path.split('.').collect();
2058
2059        let leaf_group = if path_parts.len() == 1 {
2060            let (group_id, _) = parse_group_spec(path_parts[0]);
2061            tree.groups.iter().find(|g| g.group_id == group_id)?
2062        } else {
2063            // Navigate to the parent instance, then find the leaf group
2064            let parent_parts = &path_parts[..path_parts.len() - 1];
2065            let mut current_instance = {
2066                let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
2067                let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
2068                first_group.repetitions.get(first_rep.unwrap_or(0))?
2069            };
2070            for part in &parent_parts[1..] {
2071                let (group_id, explicit_rep) = parse_group_spec(part);
2072                let child_group = current_instance
2073                    .child_groups
2074                    .iter()
2075                    .find(|g| g.group_id == group_id)?;
2076                current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
2077            }
2078            let (leaf_id, _) = parse_group_spec(path_parts.last()?);
2079            current_instance
2080                .child_groups
2081                .iter()
2082                .find(|g| g.group_id == leaf_id)?
2083        };
2084
2085        // Scan all repetitions for the matching discriminator
2086        let expected_values: Vec<&str> = expected.split('|').collect();
2087        for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
2088            let matches = instance.segments.iter().any(|s| {
2089                s.tag.eq_ignore_ascii_case(tag)
2090                    && s.elements
2091                        .get(element_idx)
2092                        .and_then(|e| e.get(component_idx))
2093                        .map(|v| expected_values.iter().any(|ev| v == ev))
2094                        .unwrap_or(false)
2095            });
2096            if matches {
2097                return Some(rep_idx);
2098            }
2099        }
2100
2101        None
2102    }
2103
2104    /// Like `resolve_repetition`, but returns ALL matching rep indices instead of just the first.
2105    ///
2106    /// This is used for multi-Zeitscheibe support where multiple SG6 reps may match
2107    /// the same discriminator (e.g., multiple RFF+Z49 time slices).
2108    pub fn resolve_all_repetitions(
2109        tree: &AssembledTree,
2110        group_path: &str,
2111        discriminator: &str,
2112    ) -> Vec<usize> {
2113        let Some((spec, expected)) = discriminator.split_once('=') else {
2114            return Vec::new();
2115        };
2116        let parts: Vec<&str> = spec.split('.').collect();
2117        if parts.len() != 3 {
2118            return Vec::new();
2119        }
2120        let tag = parts[0];
2121        let element_idx: usize = match parts[1].parse() {
2122            Ok(v) => v,
2123            Err(_) => return Vec::new(),
2124        };
2125        let component_idx: usize = match parts[2].parse() {
2126            Ok(v) => v,
2127            Err(_) => return Vec::new(),
2128        };
2129
2130        // Navigate to the parent and get the leaf group with all its repetitions
2131        let path_parts: Vec<&str> = group_path.split('.').collect();
2132
2133        let leaf_group = if path_parts.len() == 1 {
2134            let (group_id, _) = parse_group_spec(path_parts[0]);
2135            match tree.groups.iter().find(|g| g.group_id == group_id) {
2136                Some(g) => g,
2137                None => return Vec::new(),
2138            }
2139        } else {
2140            let parent_parts = &path_parts[..path_parts.len() - 1];
2141            let mut current_instance = {
2142                let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
2143                let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
2144                    Some(g) => g,
2145                    None => return Vec::new(),
2146                };
2147                match first_group.repetitions.get(first_rep.unwrap_or(0)) {
2148                    Some(i) => i,
2149                    None => return Vec::new(),
2150                }
2151            };
2152            for part in &parent_parts[1..] {
2153                let (group_id, explicit_rep) = parse_group_spec(part);
2154                let child_group = match current_instance
2155                    .child_groups
2156                    .iter()
2157                    .find(|g| g.group_id == group_id)
2158                {
2159                    Some(g) => g,
2160                    None => return Vec::new(),
2161                };
2162                current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
2163                    Some(i) => i,
2164                    None => return Vec::new(),
2165                };
2166            }
2167            let (leaf_id, _) = match path_parts.last() {
2168                Some(p) => parse_group_spec(p),
2169                None => return Vec::new(),
2170            };
2171            match current_instance
2172                .child_groups
2173                .iter()
2174                .find(|g| g.group_id == leaf_id)
2175            {
2176                Some(g) => g,
2177                None => return Vec::new(),
2178            }
2179        };
2180
2181        // Parse optional occurrence index from expected value: "TN#1" → ("TN", Some(1))
2182        let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
2183
2184        // Collect ALL matching rep indices
2185        let expected_values: Vec<&str> = expected_raw.split('|').collect();
2186        let mut result = Vec::new();
2187        for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
2188            let matches = instance.segments.iter().any(|s| {
2189                s.tag.eq_ignore_ascii_case(tag)
2190                    && s.elements
2191                        .get(element_idx)
2192                        .and_then(|e| e.get(component_idx))
2193                        .map(|v| expected_values.iter().any(|ev| v == ev))
2194                        .unwrap_or(false)
2195            });
2196            if matches {
2197                result.push(rep_idx);
2198            }
2199        }
2200
2201        // If occurrence index specified, return only that match
2202        if let Some(occ) = occurrence {
2203            result.into_iter().nth(occ).into_iter().collect()
2204        } else {
2205            result
2206        }
2207    }
2208
2209    /// Resolve a discriminated instance using source_path for parent navigation.
2210    ///
2211    /// Like `resolve_repetition` + `resolve_group_instance`, but navigates to the
2212    /// parent group via source_path qualifier suffixes. Returns the matching instance
2213    /// directly (not just a rep index) to avoid re-navigation in `map_forward_inner`.
2214    ///
2215    /// For example, `source_path = "sg4.sg8_z98.sg10"` with `discriminator = "CCI.2.0=ZB3"`
2216    /// navigates to the SG8 instance with SEQ qualifier Z98, then finds the SG10 rep
2217    /// where CCI element 2 component 0 equals "ZB3".
2218    /// Map all definitions against a tree, returning a JSON object with entity names as keys.
2219    ///
2220    /// For each definition:
2221    /// - Has discriminator → find matching rep via `resolve_repetition`, map single instance
2222    /// - Root-level (empty source_group) → map rep 0 as single object
2223    /// - No discriminator, 1 rep in tree → map as single object
2224    /// - No discriminator, multiple reps in tree → map ALL reps into a JSON array
2225    ///
2226    /// When multiple definitions share the same `entity` name, their fields are
2227    /// deep-merged into a single JSON object. This allows related TOML files
2228    /// (e.g., LOC location + SEQ info + SG10 characteristics) to contribute
2229    /// fields to the same BO4E entity.
2230    pub fn map_all_forward(&self, tree: &AssembledTree) -> serde_json::Value {
2231        self.map_all_forward_inner(tree, true).0
2232    }
2233
2234    /// Like [`map_all_forward`](Self::map_all_forward) but with explicit
2235    /// `enrich_codes` control (when `false`, code fields are plain strings
2236    /// instead of `{"code": …, "meaning": …}` objects).
2237    pub fn map_all_forward_enriched(
2238        &self,
2239        tree: &AssembledTree,
2240        enrich_codes: bool,
2241    ) -> serde_json::Value {
2242        self.map_all_forward_inner(tree, enrich_codes).0
2243    }
2244
2245    /// Inner implementation with enrichment control.
2246    ///
2247    /// Returns `(json_value, nesting_info)`, where `nesting_info` maps entity
2248    /// keys to the parent rep index for each child element (used by the reverse
2249    /// mapper to distribute nested group children among their parent reps).
2250    fn map_all_forward_inner(
2251        &self,
2252        tree: &AssembledTree,
2253        enrich_codes: bool,
2254    ) -> (
2255        serde_json::Value,
2256        std::collections::HashMap<String, Vec<usize>>,
2257    ) {
2258        self.map_all_forward_inner_with_tx(tree, enrich_codes, self.transaction_group.as_deref())
2259    }
2260
2261    /// Like `map_all_forward_inner` but with an explicit transaction-group
2262    /// override. Used by `map_interchange`, which knows the tx group even when
2263    /// the caller-supplied tx_engine wasn't built with `with_transaction_group`.
2264    fn map_all_forward_inner_with_tx(
2265        &self,
2266        tree: &AssembledTree,
2267        enrich_codes: bool,
2268        tx_group_override: Option<&str>,
2269    ) -> (
2270        serde_json::Value,
2271        std::collections::HashMap<String, Vec<usize>>,
2272    ) {
2273        let mut result = serde_json::Map::new();
2274        let mut nesting_info: std::collections::HashMap<String, Vec<usize>> =
2275            std::collections::HashMap::new();
2276        // Source groups that have written each entity key so far.
2277        let mut contributors: std::collections::HashMap<String, Vec<String>> =
2278            std::collections::HashMap::new();
2279
2280        for def in &self.definitions {
2281            // `parent_field` and bound children are mapped inside their parent's
2282            // instance (see `extract_nested_children`), never as top-level entities.
2283            if def.meta.parent_field.is_some() || is_bound_child(&self.definitions, def) {
2284                continue;
2285            }
2286            let entity = &def.meta.entity;
2287
2288            let bo4e = if let Some(ref disc) = def.meta.discriminator {
2289                // Has discriminator — resolve to matching rep(s).
2290                // Use source_path navigation when qualifiers are present
2291                // (e.g., "sg4.sg8_z98.sg10" navigates to Z98's SG10 reps,
2292                //  "sg4.sg5_z17" finds all LOC+Z17 when there are multiple).
2293                let use_source_path = def
2294                    .meta
2295                    .source_path
2296                    .as_ref()
2297                    .is_some_and(|sp| has_source_path_qualifiers(sp));
2298                if use_source_path {
2299                    // Navigate via source_path, then filter by discriminator.
2300                    let sp = def.meta.source_path.as_deref().unwrap();
2301                    let all_instances = Self::resolve_all_by_source_path(tree, sp);
2302                    // Apply discriminator filter to resolved instances (respects #N occurrence)
2303                    let instances: Vec<_> = if let Some(matcher) = DiscriminatorMatcher::parse(disc)
2304                    {
2305                        matcher.filter_instances(all_instances)
2306                    } else {
2307                        all_instances
2308                    };
2309                    let extract = |instance: &AssembledGroupInstance| {
2310                        let mut r = serde_json::Map::new();
2311                        self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
2312                        serde_json::Value::Object(r)
2313                    };
2314                    match instances.len() {
2315                        0 => None,
2316                        1 => Some(extract(instances[0])),
2317                        _ => Some(serde_json::Value::Array(
2318                            instances.iter().map(|i| extract(i)).collect(),
2319                        )),
2320                    }
2321                } else {
2322                    let reps = Self::resolve_all_repetitions(tree, &def.meta.source_group, disc);
2323                    match reps.len() {
2324                        0 => None,
2325                        1 => Some(self.map_forward_inner(tree, def, reps[0], enrich_codes)),
2326                        _ => Some(serde_json::Value::Array(
2327                            reps.iter()
2328                                .map(|&rep| self.map_forward_inner(tree, def, rep, enrich_codes))
2329                                .collect(),
2330                        )),
2331                    }
2332                }
2333            } else if def.meta.source_group.is_empty() {
2334                // Root-level mapping — always single object
2335                Some(self.map_forward_inner(tree, def, 0, enrich_codes))
2336            } else if def.meta.source_path.as_ref().is_some_and(|sp| {
2337                has_source_path_qualifiers(sp) || def.meta.source_group.contains('.')
2338            }) {
2339                // Multi-level source path — navigate via source_path to collect all
2340                // instances across all parent repetitions. Handles both qualified
2341                // paths (e.g., "sg4.sg8_zd7.sg10") and unqualified paths (e.g.,
2342                // "sg17.sg36.sg40") where multiple parent reps each have children.
2343                let sp = def.meta.source_path.as_deref().unwrap();
2344                let mut indexed = Self::resolve_all_with_parent_indices(tree, sp);
2345
2346                // When the LAST part of source_path has no qualifier (e.g., "sg29.sg30"),
2347                // exclude reps that match a qualified sibling definition's qualifier
2348                // (e.g., "sg29.sg30_z35"). This prevents double-extraction when both
2349                // qualified and unqualified definitions target the same group.
2350                if let Some(last_part) = sp.rsplit('.').next() {
2351                    if !last_part.contains('_') {
2352                        // Collect qualifiers from sibling definitions that share the
2353                        // same base group name. E.g., for "sg29.sg30", only match
2354                        // "sg29.sg30_z35" (same base "sg30"), NOT "sg29.sg31_z35".
2355                        let base_prefix = if let Some(parent) = sp.rsplit_once('.') {
2356                            format!("{}.", parent.0)
2357                        } else {
2358                            String::new()
2359                        };
2360                        let sibling_qualifiers: Vec<String> = self
2361                            .definitions
2362                            .iter()
2363                            .filter_map(|d| d.meta.source_path.as_deref())
2364                            .filter(|other_sp| {
2365                                *other_sp != sp
2366                                    && other_sp.starts_with(&base_prefix)
2367                                    && other_sp.split('.').count() == sp.split('.').count()
2368                            })
2369                            .filter_map(|other_sp| {
2370                                let other_last = other_sp.rsplit('.').next()?;
2371                                // Only match siblings with the same base group name
2372                                // e.g., "sg30_z35" has base "sg30", must match "sg30"
2373                                let (base, q) = other_last.split_once('_')?;
2374                                if base == last_part {
2375                                    Some(q.to_string())
2376                                } else {
2377                                    None
2378                                }
2379                            })
2380                            .collect();
2381
2382                        if !sibling_qualifiers.is_empty() {
2383                            indexed.retain(|(_, inst)| {
2384                                let entry_qual = inst
2385                                    .segments
2386                                    .first()
2387                                    .and_then(|seg| seg.elements.first())
2388                                    .and_then(|el| el.first())
2389                                    .map(|v| v.to_lowercase());
2390                                // Keep reps whose entry qualifier does NOT match
2391                                // any sibling's qualifier
2392                                !entry_qual.is_some_and(|q| {
2393                                    sibling_qualifiers.iter().any(|sq| {
2394                                        sq.split('_').any(|part| part.eq_ignore_ascii_case(&q))
2395                                    })
2396                                })
2397                            });
2398                        }
2399                    }
2400                }
2401                let extract = |instance: &AssembledGroupInstance| {
2402                    let mut r = serde_json::Map::new();
2403                    self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
2404                    serde_json::Value::Object(r)
2405                };
2406                // Track parent rep indices for nesting reconstruction.
2407                // Key by source_path (not entity or source_group) so that definitions
2408                // at different depths or with different qualifiers don't collide.
2409                // e.g., "sg5.sg8_z41.sg9" vs "sg5.sg8_z42.sg9" are distinct keys.
2410                if def.meta.source_group.contains('.') && !indexed.is_empty() {
2411                    if let Some(sp) = &def.meta.source_path {
2412                        let parent_indices: Vec<usize> =
2413                            indexed.iter().map(|(idx, _)| *idx).collect();
2414                        nesting_info.entry(sp.clone()).or_insert(parent_indices);
2415
2416                        // Also store child rep indices (position within the leaf group)
2417                        // for depth-1 reverse placement. Key: "{sp}#child".
2418                        let child_key = format!("{sp}#child");
2419                        if let std::collections::hash_map::Entry::Vacant(e) =
2420                            nesting_info.entry(child_key)
2421                        {
2422                            let child_indices: Vec<usize> =
2423                                Self::compute_child_indices(tree, sp, &indexed);
2424                            if !child_indices.is_empty() {
2425                                e.insert(child_indices);
2426                            }
2427                        }
2428                    }
2429                }
2430                match indexed.len() {
2431                    0 => None,
2432                    1 => Some(extract(indexed[0].1)),
2433                    _ => Some(serde_json::Value::Array(
2434                        indexed.iter().map(|(_, i)| extract(i)).collect(),
2435                    )),
2436                }
2437            } else {
2438                let num_reps = Self::count_repetitions(tree, &def.meta.source_group);
2439                if num_reps <= 1 {
2440                    Some(self.map_forward_inner(tree, def, 0, enrich_codes))
2441                } else {
2442                    // Multiple reps, no discriminator — map all into array
2443                    let mut items = Vec::with_capacity(num_reps);
2444                    for rep in 0..num_reps {
2445                        items.push(self.map_forward_inner(tree, def, rep, enrich_codes));
2446                    }
2447                    Some(serde_json::Value::Array(items))
2448                }
2449            };
2450
2451            if let Some(bo4e) = bo4e {
2452                let key = to_camel_case(entity);
2453                match def.meta.target_list.as_deref() {
2454                    Some(list_field) => append_to_list_field(&mut result, &key, list_field, bo4e),
2455                    None => {
2456                        // Keep both on a shape mismatch only for sibling groups:
2457                        // no earlier contributor of this entity is this group's
2458                        // ancestor or descendant (see `merge_entity`).
2459                        let group = def
2460                            .meta
2461                            .source_path
2462                            .clone()
2463                            .unwrap_or_else(|| def.meta.source_group.to_lowercase());
2464                        let seen = contributors.entry(key.clone()).or_default();
2465                        let nested = seen.iter().any(|other: &String| {
2466                            group.starts_with(&format!("{other}."))
2467                                || other.starts_with(&format!("{group}."))
2468                        });
2469                        seen.push(group);
2470                        merge_entity(&mut result, &key, bo4e, !nested);
2471                    }
2472                }
2473            }
2474        }
2475
2476        // Post-process: nest child entities under their parent entities.
2477        // E.g., Kontakt (source_group="SG2.SG3") moves under Marktteilnehmer (source_group="SG2").
2478        // Children whose parent group is the transaction root (e.g. SG4 for UTILMD) are
2479        // left at the top level — see MappingEngine::transaction_group.
2480        nest_child_entities_in_result(
2481            &mut result,
2482            &self.definitions,
2483            &nesting_info,
2484            tx_group_override,
2485        );
2486
2487        (serde_json::Value::Object(result), nesting_info)
2488    }
2489
2490    /// Reverse-map a BO4E entity map back to an AssembledTree.
2491    ///
2492    /// For each definition:
2493    /// 1. Look up entity in input by `meta.entity` name
2494    /// 2. If entity value is an array, map each element as a separate group repetition
2495    /// 3. Place results by `source_group`: `""` → root segments, `"SGn"` → groups
2496    ///
2497    /// This is the inverse of `map_all_forward()`.
2498    pub fn map_all_reverse(
2499        &self,
2500        entities: &serde_json::Value,
2501        nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
2502    ) -> AssembledTree {
2503        self.map_all_reverse_with_mig(entities, nesting_info, None)
2504    }
2505
2506    /// [`map_all_reverse`](Self::map_all_reverse) with the PID-filtered MIG,
2507    /// which decides the parent of a nested child entity the BO4E JSON does
2508    /// not link to a parent (see the nesting step below).
2509    pub fn map_all_reverse_with_mig(
2510        &self,
2511        entities: &serde_json::Value,
2512        nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
2513        mig: Option<&MigSchema>,
2514    ) -> AssembledTree {
2515        let mut root_segments: Vec<AssembledSegment> = Vec::new();
2516        let mut groups: Vec<AssembledGroup> = Vec::new();
2517        // Track parent rep indices for child entities extracted from map-keyed
2518        // or array parents.  Used as fallback when nesting_info is empty.
2519        let mut inferred_nesting: std::collections::HashMap<String, Vec<usize>> =
2520            std::collections::HashMap::new();
2521
2522        for def in &self.definitions {
2523            // `parent_field` and bound children are reversed with their parent
2524            // object (see `reverse_nested_children`).
2525            if def.meta.parent_field.is_some() || is_bound_child(&self.definitions, def) {
2526                continue;
2527            }
2528            let entity_key = to_camel_case(&def.meta.entity);
2529
2530            // Look up entity value — first at top level, then nested under parent.
2531            // `_extracted` keeps the owned value alive for the borrow below.
2532            let _extracted: Option<serde_json::Value>;
2533            let entity_value = if let Some(list_field) = def.meta.target_list.as_deref() {
2534                // `target_list`: this definition's data is not the entity object,
2535                // it is the elements of a list field on it. Handing the array
2536                // straight to the array branch below turns each element back into
2537                // one group repetition, which is the exact inverse of the forward
2538                // "one repetition -> one element" rule.
2539                match entities.get(&entity_key).and_then(|e| e.get(list_field)) {
2540                    Some(v) if v.is_array() => {
2541                        _extracted = None;
2542                        v
2543                    }
2544                    _ => continue,
2545                }
2546            } else if let Some(v) = entities.get(&entity_key) {
2547                _extracted = None;
2548                v
2549            } else if def.meta.source_group.contains('.') {
2550                // Child entity not at top level — try extracting from parent entity
2551                match extract_child_from_parent_with_indices(entities, &self.definitions, def) {
2552                    Some((v, parent_indices)) => {
2553                        // Record inferred parent rep indices for nesting distribution
2554                        if let Some(sp) = def.meta.source_path.as_deref() {
2555                            inferred_nesting
2556                                .entry(sp.to_string())
2557                                .or_insert(parent_indices);
2558                        }
2559                        _extracted = Some(v);
2560                        _extracted.as_ref().unwrap()
2561                    }
2562                    None => continue,
2563                }
2564            } else {
2565                continue;
2566            };
2567
2568            // Support map-keyed entities from typed PID format.
2569            // E.g., geschaeftspartner: {"Z04": {name1: "..."}} with discriminator NAD.0.0=Z04.
2570            // Extract inner value using discriminator's qualifier value as key,
2571            // and inject the qualifier into the inner object so companion fields find it.
2572            //
2573            // Also handles non-discriminated maps (e.g., marktteilnehmer: {"MS": {...}, "MR": {...}})
2574            // by converting them to arrays of inner values.
2575            let unwrapped: Option<serde_json::Value>;
2576            let entity_value = if entity_value.is_object() && !entity_value.is_array() {
2577                if let Some(disc_value) = def
2578                    .meta
2579                    .discriminator
2580                    .as_deref()
2581                    .and_then(|d| d.split_once('='))
2582                    .map(|(_, v)| v)
2583                {
2584                    // Discriminated definition: try to extract map key matching qualifier
2585                    if let Some(inner) = entity_value.get(disc_value) {
2586                        let mut injected = inner.clone();
2587                        // Find the field that maps to the discriminator's EDIFACT path
2588                        // and inject the map key as that field's value (e.g., nadQualifier = "Z04")
2589                        if let Some(qualifier_field) =
2590                            find_qualifier_companion_field(&self.definitions, &def.meta.entity)
2591                        {
2592                            if let Some(obj) = injected.as_object_mut() {
2593                                let entry = obj
2594                                    .entry(qualifier_field)
2595                                    .or_insert(serde_json::Value::Null);
2596                                if entry.is_null() {
2597                                    *entry = serde_json::Value::String(disc_value.to_string());
2598                                }
2599                            }
2600                        }
2601                        unwrapped = Some(injected);
2602                        unwrapped.as_ref().unwrap()
2603                    } else {
2604                        entity_value
2605                    }
2606                } else if is_map_keyed_object(entity_value) {
2607                    // Non-discriminated definition: convert map to array
2608                    // e.g., marktteilnehmer: {"MS": {...}, "MR": {...}} → [{...}, {...}]
2609                    // Inject each map key into its inner object using the companion field
2610                    // that maps to the discriminator path (if identifiable from other defs).
2611                    let map = entity_value.as_object().unwrap();
2612                    let arr: Vec<serde_json::Value> = map
2613                        .iter()
2614                        .map(|(key, val)| {
2615                            let mut item = val.clone();
2616                            // Try to find a qualifier companion field from peer definitions
2617                            // that share this entity name and have a discriminator
2618                            if let Some(obj) = item.as_object_mut() {
2619                                if let Some(qualifier_field) = find_qualifier_companion_field(
2620                                    &self.definitions,
2621                                    &def.meta.entity,
2622                                ) {
2623                                    let entry = obj
2624                                        .entry(qualifier_field)
2625                                        .or_insert(serde_json::Value::Null);
2626                                    if entry.is_null() {
2627                                        *entry = serde_json::Value::String(key.clone());
2628                                    }
2629                                }
2630                            }
2631                            item
2632                        })
2633                        .collect();
2634                    unwrapped = Some(serde_json::Value::Array(arr));
2635                    unwrapped.as_ref().unwrap()
2636                } else {
2637                    entity_value
2638                }
2639            } else {
2640                entity_value
2641            };
2642
2643            // Determine target group from source_group (use leaf part after last dot)
2644            let leaf_group = def
2645                .meta
2646                .source_group
2647                .rsplit('.')
2648                .next()
2649                .unwrap_or(&def.meta.source_group);
2650
2651            if def.meta.source_group.is_empty() {
2652                // Root-level: reverse into root segments
2653                let instance = self.map_reverse(entity_value, def);
2654                root_segments.extend(instance.segments);
2655            } else if self.transaction_group.as_deref() == Some(def.meta.source_group.as_str()) {
2656                // The input is one transaction: every definition of the
2657                // transaction group fills its one instance (Prozessdaten's IDE
2658                // and STS beside the Freitext's FTX). One repetition per
2659                // definition would split a transaction into several.
2660                let items: Vec<&serde_json::Value> = match entity_value.as_array() {
2661                    Some(arr) => arr.iter().collect(),
2662                    None => vec![entity_value],
2663                };
2664                let group = match groups.iter().position(|g| g.group_id == leaf_group) {
2665                    Some(i) => &mut groups[i],
2666                    None => {
2667                        groups.push(AssembledGroup {
2668                            group_id: leaf_group.to_string(),
2669                            repetitions: Vec::new(),
2670                        });
2671                        groups.last_mut().expect("pushed")
2672                    }
2673                };
2674                if group.repetitions.is_empty() {
2675                    group.repetitions.push(AssembledGroupInstance {
2676                        segments: Vec::new(),
2677                        child_groups: Vec::new(),
2678                        entry_mig_number: None,
2679                        variant_mig_numbers: Vec::new(),
2680                        skipped_segments: Vec::new(),
2681                        skipped_positions: Vec::new(),
2682                    });
2683                }
2684                let transaction = &mut group.repetitions[0];
2685                for item in items {
2686                    let instance = self.map_reverse(item, def);
2687                    transaction.segments.extend(instance.segments);
2688                    for child in instance.child_groups {
2689                        match transaction
2690                            .child_groups
2691                            .iter_mut()
2692                            .find(|g| g.group_id == child.group_id)
2693                        {
2694                            Some(existing) => existing.repetitions.extend(child.repetitions),
2695                            None => transaction.child_groups.push(child),
2696                        }
2697                    }
2698                }
2699            } else if entity_value.is_array() {
2700                // Array entity: each element becomes a group repetition
2701                let arr = entity_value.as_array().unwrap();
2702                let reps: Vec<_> = arr.iter().map(|item| self.map_reverse(item, def)).collect();
2703
2704                // Merge into existing group or create new one
2705                if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2706                    existing.repetitions.extend(reps);
2707                } else {
2708                    groups.push(AssembledGroup {
2709                        group_id: leaf_group.to_string(),
2710                        repetitions: reps,
2711                    });
2712                }
2713            } else {
2714                // Single object: one repetition
2715                let instance = self.map_reverse(entity_value, def);
2716
2717                if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2718                    existing.repetitions.push(instance);
2719                } else {
2720                    groups.push(AssembledGroup {
2721                        group_id: leaf_group.to_string(),
2722                        repetitions: vec![instance],
2723                    });
2724                }
2725            }
2726        }
2727
2728        // Post-process: move nested groups under their parent repetitions.
2729        // Definitions with multi-level source_group (e.g., "SG2.SG3") produce
2730        // top-level groups that must be nested inside their parent group.
2731        // Children are distributed sequentially among parent reps (child[i] → parent[i])
2732        // matching the forward mapper's extraction order.
2733        let nested_specs: Vec<(String, String)> = self
2734            .definitions
2735            .iter()
2736            .filter(|def| def.meta.parent_field.is_none())
2737            .filter_map(|def| {
2738                let parts: Vec<&str> = def.meta.source_group.split('.').collect();
2739                if parts.len() > 1 {
2740                    Some((parts[0].to_string(), parts[parts.len() - 1].to_string()))
2741                } else {
2742                    None
2743                }
2744            })
2745            .collect();
2746        for (parent_id, child_id) in &nested_specs {
2747            // Only nest if both parent and child exist at the top level
2748            let has_parent = groups.iter().any(|g| g.group_id == *parent_id);
2749            let has_child = groups.iter().any(|g| g.group_id == *child_id);
2750            if has_parent && has_child {
2751                let child_idx = groups.iter().position(|g| g.group_id == *child_id).unwrap();
2752                let child_group = groups.remove(child_idx);
2753                let parent = groups
2754                    .iter_mut()
2755                    .find(|g| g.group_id == *parent_id)
2756                    .unwrap();
2757                // Distribute child reps among parent reps using nesting info
2758                // if available, falling back to all-under-first when not.
2759                // Nesting info is keyed by source_path (e.g., "sg2.sg3").
2760                let child_source_path = self
2761                    .definitions
2762                    .iter()
2763                    .find(|d| {
2764                        let parts: Vec<&str> = d.meta.source_group.split('.').collect();
2765                        d.meta.parent_field.is_none()
2766                            && parts.len() > 1
2767                            && parts[parts.len() - 1] == *child_id
2768                    })
2769                    .and_then(|d| d.meta.source_path.as_deref());
2770                let distribution = child_source_path.and_then(|key| {
2771                    nesting_info
2772                        .and_then(|ni| ni.get(key))
2773                        .or_else(|| inferred_nesting.get(key))
2774                });
2775                // Without a link from the JSON, the parent follows from the MIG:
2776                // the first repetition (in MIG variant order) whose variant
2777                // defines this child group — e.g. the SG2 NAD+MS repetition for
2778                // the sender's SG3 contact. Not "the first array element": BO4E
2779                // carries no ordering information.
2780                let unlinked_target = mig
2781                    .and_then(|m| {
2782                        mig_assembly::repetition_order::preferred_parent_repetition(
2783                            parent,
2784                            &m.segment_groups,
2785                            child_id,
2786                        )
2787                    })
2788                    .unwrap_or(0);
2789                for (i, child_rep) in child_group.repetitions.into_iter().enumerate() {
2790                    let target_idx = distribution
2791                        .and_then(|dist| dist.get(i))
2792                        .copied()
2793                        .unwrap_or(unlinked_target);
2794
2795                    if let Some(target_rep) = parent.repetitions.get_mut(target_idx) {
2796                        if let Some(existing) = target_rep
2797                            .child_groups
2798                            .iter_mut()
2799                            .find(|g| g.group_id == *child_id)
2800                        {
2801                            existing.repetitions.push(child_rep);
2802                        } else {
2803                            target_rep.child_groups.push(AssembledGroup {
2804                                group_id: child_id.clone(),
2805                                repetitions: vec![child_rep],
2806                            });
2807                        }
2808                    }
2809                }
2810            }
2811        }
2812
2813        let post_group_start = root_segments.len();
2814        AssembledTree {
2815            segments: root_segments,
2816            groups,
2817            post_group_start,
2818            inter_group_segments: std::collections::BTreeMap::new(),
2819        }
2820    }
2821
2822    /// Count the number of repetitions available for a group path in the tree.
2823    fn count_repetitions(tree: &AssembledTree, group_path: &str) -> usize {
2824        let parts: Vec<&str> = group_path.split('.').collect();
2825
2826        let (first_id, first_rep) = parse_group_spec(parts[0]);
2827        let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
2828            Some(g) => g,
2829            None => return 0,
2830        };
2831
2832        if parts.len() == 1 {
2833            return first_group.repetitions.len();
2834        }
2835
2836        // Navigate to parent, then count leaf group reps
2837        let mut current_instance = match first_group.repetitions.get(first_rep.unwrap_or(0)) {
2838            Some(i) => i,
2839            None => return 0,
2840        };
2841
2842        for (i, part) in parts[1..].iter().enumerate() {
2843            let (group_id, explicit_rep) = parse_group_spec(part);
2844            let child_group = match current_instance
2845                .child_groups
2846                .iter()
2847                .find(|g| g.group_id == group_id)
2848            {
2849                Some(g) => g,
2850                None => return 0,
2851            };
2852
2853            if i == parts.len() - 2 {
2854                // Last part — return rep count
2855                return child_group.repetitions.len();
2856            }
2857            current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
2858                Some(i) => i,
2859                None => return 0,
2860            };
2861        }
2862
2863        0
2864    }
2865
2866    /// Translate an assembled tree into BO4E, without code enrichment.
2867    ///
2868    /// This is the translation proper: every code field is a plain string, as it
2869    /// appears in the EDIFACT message. Enrichment (`{code, meaning, enum}`) is a
2870    /// display concern and is applied separately by [`Self::enrich_bo4e_types`],
2871    /// so a caller that does not need it never pays for it and never has to
2872    /// strip it back out.
2873    pub fn translate_edifact_to_bo4e(
2874        msg_engine: &MappingEngine,
2875        tx_engine: &MappingEngine,
2876        tree: &AssembledTree,
2877        transaction_group: &str,
2878    ) -> crate::model::MappedMessage {
2879        Self::map_interchange_inner(msg_engine, tx_engine, tree, transaction_group, false)
2880    }
2881
2882    /// Decorate code fields of an already-translated message in place.
2883    ///
2884    /// Replaces the plain string at each code position with
2885    /// `{"code": …, "meaning": …, "enum": …}`. Needs a [`CodeLookup`] on the
2886    /// engines; without one this is a no-op, which is why CI — which never
2887    /// attaches a lookup — sees the unenriched shape.
2888    ///
2889    /// Works from the mapping definitions rather than from the EDIFACT tree: a
2890    /// definition knows both where a value came from (`source_path` plus the
2891    /// segment/element coordinates of the field) and where it went (`target`),
2892    /// which is all the lookup needs. The original EDIFACT value is recovered by
2893    /// inverting `enum_map` the same way the reverse mapper does, including the
2894    /// `also_target` disambiguation for codes that share a primary value.
2895    pub fn enrich_bo4e_types(
2896        msg_engine: &MappingEngine,
2897        tx_engine: &MappingEngine,
2898        mapped: &mut crate::model::MappedMessage,
2899    ) {
2900        msg_engine.enrich_entities(&mut mapped.stammdaten);
2901        for tx in &mut mapped.transaktionen {
2902            tx_engine.enrich_entities(&mut tx.stammdaten);
2903        }
2904
2905        // The metadata slots hold mapped entities too. The forward pass splits
2906        // them out of `stammdaten`, so walking `stammdaten` alone no longer
2907        // reaches them — and their code fields would silently stay plain.
2908        msg_engine.enrich_named_entity(
2909            &mut mapped.nachricht_meta,
2910            crate::model::MSG_METADATA_ENTITY,
2911        );
2912        for tx in &mut mapped.transaktionen {
2913            tx_engine
2914                .enrich_named_entity(&mut tx.transaktionsdaten, crate::model::TX_METADATA_ENTITY);
2915        }
2916    }
2917
2918    /// Apply the code sites of one named entity to a value holding that entity.
2919    ///
2920    /// The entity-map walk keys on the enclosing object's field name; a metadata
2921    /// slot has no such name, so the entity is named explicitly here.
2922    fn enrich_named_entity(&self, value: &mut serde_json::Value, entity_key: &str) {
2923        if self.code_lookup.is_none() || value.is_null() {
2924            return;
2925        }
2926        let sites = self.code_sites();
2927        if let Some(entity_sites) = sites.get(entity_key) {
2928            Self::apply_sites(self, value, entity_sites);
2929        }
2930    }
2931
2932    /// Apply this engine's code enrichment to one entity map.
2933    ///
2934    /// Entities are located by key at any depth, because the forward pass moves
2935    /// them after extraction: `nest_child_entities_in_result` puts children
2936    /// under their parents.
2937    fn enrich_entities(&self, value: &mut serde_json::Value) {
2938        if self.code_lookup.is_none() {
2939            return;
2940        }
2941        let sites: HashMap<String, Vec<CodeSite<'_>>> = self.code_sites();
2942        if sites.is_empty() {
2943            return;
2944        }
2945        Self::walk_and_enrich(self, value, &sites);
2946    }
2947
2948    /// Every code-field position this engine's definitions write to, grouped by
2949    /// the entity key the value ends up under.
2950    fn code_sites(&self) -> HashMap<String, Vec<CodeSite<'_>>> {
2951        let Some(ref code_lookup) = self.code_lookup else {
2952            return HashMap::new();
2953        };
2954        let mut sites: HashMap<String, Vec<CodeSite<'_>>> = HashMap::new();
2955
2956        for def in &self.definitions {
2957            let Some(ref source_path) = def.meta.source_path else {
2958                continue;
2959            };
2960            let entity_key = to_camel_case(&def.meta.entity);
2961
2962            for (path, field_mapping) in &def.fields {
2963                let (target, enum_map, also_target, also_enum_map) = match field_mapping {
2964                    FieldMapping::Simple(t) => (t.as_str(), None, None, None),
2965                    FieldMapping::Structured(s) => (
2966                        s.target.as_str(),
2967                        self.table(s.enum_map.as_ref(), s.code_list.as_deref()),
2968                        s.also_target.as_deref(),
2969                        self.table(s.also_enum_map.as_ref(), s.also_code_list.as_deref()),
2970                    ),
2971                    FieldMapping::Nested(_) => continue,
2972                };
2973                if target.is_empty() {
2974                    continue;
2975                }
2976
2977                let parts: Vec<&str> = path.split('.').collect();
2978                let (seg_tag, path_qualifier, _occ) = parse_tag_qualifier(parts[0]);
2979                let (element_idx, component_idx) = Self::parse_element_component(&parts[1..]);
2980                // Same predicate, and the same two qualifiers, as the pre-split
2981                // path in `extract_fields_from_instance`: the field key's own
2982                // qualifier selects the schema variant, the discriminator's only
2983                // where the key has none. Asking with one merged qualifier — as
2984                // this did — calls `cav[Z30]`'s device number a code field and
2985                // decorates it, which the pre-split path never did.
2986                let disc_qualifier = Self::discriminator_qualifier_for_tag(def, &seg_tag);
2987                if code_lookup
2988                    .enrichment_codes(
2989                        source_path,
2990                        &seg_tag,
2991                        path_qualifier,
2992                        disc_qualifier.as_deref(),
2993                        element_idx,
2994                        component_idx,
2995                    )
2996                    .is_none()
2997                {
2998                    continue;
2999                }
3000
3001                sites.entry(entity_key.clone()).or_default().push(CodeSite {
3002                    target,
3003                    parent_field: def.meta.parent_field.as_deref(),
3004                    source_path,
3005                    seg_tag,
3006                    path_qualifier: path_qualifier.map(str::to_string),
3007                    disc_qualifier,
3008                    element_idx,
3009                    component_idx,
3010                    enum_map,
3011                    also_target,
3012                    also_enum_map,
3013                });
3014            }
3015        }
3016        sites
3017    }
3018
3019    /// Descend through the result, enriching every object that sits under a key
3020    /// naming an entity this engine maps.
3021    fn walk_and_enrich(
3022        engine: &MappingEngine,
3023        value: &mut serde_json::Value,
3024        sites: &HashMap<String, Vec<CodeSite<'_>>>,
3025    ) {
3026        match value {
3027            serde_json::Value::Object(map) => {
3028                for (key, child) in map.iter_mut() {
3029                    if let Some(entity_sites) = sites.get(key.as_str()) {
3030                        Self::apply_sites(engine, child, entity_sites);
3031                    }
3032                    Self::walk_and_enrich(engine, child, sites);
3033                }
3034            }
3035            serde_json::Value::Array(items) => {
3036                for item in items.iter_mut() {
3037                    Self::walk_and_enrich(engine, item, sites);
3038                }
3039            }
3040            _ => {}
3041        }
3042    }
3043
3044    /// Apply one entity's code sites to an entity value (an object, or an array
3045    /// of them when the group repeats).
3046    fn apply_sites(engine: &MappingEngine, value: &mut serde_json::Value, sites: &[CodeSite<'_>]) {
3047        match value {
3048            serde_json::Value::Array(items) => {
3049                for item in items.iter_mut() {
3050                    Self::apply_sites(engine, item, sites);
3051                }
3052            }
3053            serde_json::Value::Object(_) => {
3054                for site in sites {
3055                    match site.parent_field {
3056                        None => engine.enrich_one(value, site),
3057                        Some(field) => {
3058                            if let Some(nested) = value.get_mut(field) {
3059                                Self::apply_nested_site(engine, nested, site);
3060                            }
3061                        }
3062                    }
3063                }
3064            }
3065            _ => {}
3066        }
3067    }
3068
3069    /// Apply one nested site to every element of the `parent_field` array.
3070    fn apply_nested_site(
3071        engine: &MappingEngine,
3072        value: &mut serde_json::Value,
3073        site: &CodeSite<'_>,
3074    ) {
3075        match value {
3076            serde_json::Value::Array(items) => {
3077                for item in items.iter_mut() {
3078                    Self::apply_nested_site(engine, item, site);
3079                }
3080            }
3081            serde_json::Value::Object(_) => engine.enrich_one(value, site),
3082            _ => {}
3083        }
3084    }
3085
3086    /// Enrich a single position, if it currently holds a plain string.
3087    fn enrich_one(&self, entity: &mut serde_json::Value, site: &CodeSite<'_>) {
3088        // A list target (`werte[].code`) enriches the key in every element.
3089        if let Some((list, sub)) = list_target(site.target) {
3090            if let Some(items) = entity.get_mut(list).and_then(|v| v.as_array_mut()) {
3091                let element_site = CodeSite {
3092                    target: sub,
3093                    ..site.clone()
3094                };
3095                for item in items {
3096                    self.enrich_one(item, &element_site);
3097                }
3098            }
3099            return;
3100        }
3101        let Some(ref code_lookup) = self.code_lookup else {
3102            return;
3103        };
3104        // Already an object means another definition enriched this position.
3105        let Some(mapped_val) = Self::read_plain_string(entity, site.target) else {
3106            return;
3107        };
3108
3109        // Recover the EDIFACT value: the schema's codes are raw ("293"), while
3110        // the JSON holds the enum_map target ("BDEW").
3111        let raw = match site.enum_map {
3112            None => mapped_val.clone(),
3113            Some(map) => {
3114                let joint = match (site.also_target, site.also_enum_map) {
3115                    (Some(also), Some(also_map)) => {
3116                        Self::read_plain_string(entity, also).and_then(|also_v| {
3117                            map.iter()
3118                                .find(|(code, bo4e_v)| {
3119                                    *bo4e_v == &mapped_val && also_map.get(*code) == Some(&also_v)
3120                                })
3121                                .map(|(code, _)| code.clone())
3122                        })
3123                    }
3124                    _ => None,
3125                };
3126                joint
3127                    .or_else(|| {
3128                        map.iter()
3129                            .find(|(_, bo4e_v)| *bo4e_v == &mapped_val)
3130                            .map(|(code, _)| code.clone())
3131                    })
3132                    .unwrap_or_else(|| mapped_val.clone())
3133            }
3134        };
3135
3136        let Some(codes) = code_lookup.enrichment_codes(
3137            site.source_path,
3138            &site.seg_tag,
3139            site.path_qualifier.as_deref(),
3140            site.disc_qualifier.as_deref(),
3141            site.element_idx,
3142            site.component_idx,
3143        ) else {
3144            return;
3145        };
3146
3147        // Class C: PID self-reference stays a plain string.
3148        if let Some(ref pid) = self.current_pid {
3149            if codes.len() == 1 && codes.contains_key(pid.as_str()) {
3150                return;
3151            }
3152        }
3153
3154        let enrichment = codes.get(&raw);
3155        let meaning = enrichment
3156            .map(|e| serde_json::Value::String(e.meaning.clone()))
3157            .unwrap_or(serde_json::Value::Null);
3158
3159        let mut obj = serde_json::Map::new();
3160        obj.insert("code".into(), serde_json::json!(mapped_val));
3161        obj.insert("meaning".into(), meaning);
3162        if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
3163            obj.insert("enum".into(), serde_json::json!(enum_key));
3164        }
3165
3166        if let serde_json::Value::Object(map) = entity {
3167            set_nested_value_json(map, site.target, serde_json::Value::Object(obj));
3168        }
3169    }
3170
3171    /// The string at a dotted target path, or `None` when it is absent or has
3172    /// already been replaced by an enrichment object.
3173    fn read_plain_string(entity: &serde_json::Value, target: &str) -> Option<String> {
3174        let mut current = entity;
3175        for part in target.split('.') {
3176            current = current.get(part)?;
3177        }
3178        current.as_str().map(str::to_string)
3179    }
3180
3181    /// Map an assembled tree into message-level and transaction-level results.
3182    ///
3183    /// - `msg_engine`: MappingEngine loaded with message-level definitions (SG2, SG3, root segments)
3184    /// - `tx_engine`: MappingEngine loaded with transaction-level definitions (relative to SG4)
3185    /// - `tree`: The assembled tree for one message
3186    /// - `transaction_group`: The group ID that represents transactions (e.g., "SG4")
3187    ///
3188    /// Returns a `MappedMessage` with message stammdaten and per-transaction results.
3189    pub fn map_interchange(
3190        msg_engine: &MappingEngine,
3191        tx_engine: &MappingEngine,
3192        tree: &AssembledTree,
3193        transaction_group: &str,
3194        enrich_codes: bool,
3195    ) -> crate::model::MappedMessage {
3196        let mut mapped =
3197            Self::translate_edifact_to_bo4e(msg_engine, tx_engine, tree, transaction_group);
3198        if enrich_codes {
3199            Self::enrich_bo4e_types(msg_engine, tx_engine, &mut mapped);
3200        }
3201        mapped
3202    }
3203
3204    /// The translation itself, with enrichment still inlined in the extraction.
3205    ///
3206    /// Retained so the split can be proven equivalent: `map_interchange_inner`
3207    /// with `enrich_codes = true` must produce exactly what
3208    /// `translate_edifact_to_bo4e` followed by `enrich_bo4e_types` produces.
3209    /// See `enrich_split_parity_test`.
3210    /// Test-only door onto the pre-split path, so the parity gate can compare
3211    /// the two. Not part of the public pipeline.
3212    #[doc(hidden)]
3213    pub fn map_interchange_inner_for_test(
3214        msg_engine: &MappingEngine,
3215        tx_engine: &MappingEngine,
3216        tree: &AssembledTree,
3217        transaction_group: &str,
3218        enrich_codes: bool,
3219    ) -> crate::model::MappedMessage {
3220        Self::map_interchange_inner(msg_engine, tx_engine, tree, transaction_group, enrich_codes)
3221    }
3222
3223    pub(crate) fn map_interchange_inner(
3224        msg_engine: &MappingEngine,
3225        tx_engine: &MappingEngine,
3226        tree: &AssembledTree,
3227        transaction_group: &str,
3228        enrich_codes: bool,
3229    ) -> crate::model::MappedMessage {
3230        // Map message-level entities (also captures nesting info)
3231        let (stammdaten, nesting_info) = msg_engine.map_all_forward_inner(tree, enrich_codes);
3232
3233        // Find the transaction group and map each repetition
3234        let transaktionen = tree
3235            .groups
3236            .iter()
3237            .find(|g| g.group_id == transaction_group)
3238            .map(|sg| {
3239                sg.repetitions
3240                    .iter()
3241                    .map(|instance| {
3242                        // Wrap the instance in its group so that definitions with
3243                        // source_group paths like "SG4.SG5" can resolve correctly.
3244                        let wrapped_tree = AssembledTree {
3245                            segments: vec![],
3246                            groups: vec![AssembledGroup {
3247                                group_id: transaction_group.to_string(),
3248                                repetitions: vec![instance.clone()],
3249                            }],
3250                            post_group_start: 0,
3251                            inter_group_segments: std::collections::BTreeMap::new(),
3252                        };
3253
3254                        // Pass the transaction_group into the tx_engine so its direct
3255                        // children (Marktlokation etc.) stay top-level peers of
3256                        // Prozessdaten rather than nested under it.
3257                        let (tx_result, tx_nesting) = tx_engine.map_all_forward_inner_with_tx(
3258                            &wrapped_tree,
3259                            enrich_codes,
3260                            Some(transaction_group),
3261                        );
3262
3263                        // Split the transaction's own metadata out of its
3264                        // business objects. The engine maps `Prozessdaten` like
3265                        // any other entity; it just does not belong among the
3266                        // BOs once mapped.
3267                        let mut tx_result = tx_result;
3268                        let transaktionsdaten = crate::model::take_entity(
3269                            &mut tx_result,
3270                            crate::model::TX_METADATA_ENTITY,
3271                        );
3272
3273                        crate::model::MappedTransaktion {
3274                            stammdaten: tx_result,
3275                            transaktionsdaten,
3276                            nesting_info: tx_nesting,
3277                        }
3278                    })
3279                    .collect()
3280            })
3281            .unwrap_or_default();
3282
3283        // Same split one level up: `Nachricht` is metadata about the message.
3284        let mut stammdaten = stammdaten;
3285        let nachricht_meta =
3286            crate::model::take_entity(&mut stammdaten, crate::model::MSG_METADATA_ENTITY);
3287
3288        crate::model::MappedMessage {
3289            stammdaten,
3290            nachricht_meta,
3291            transaktionen,
3292            nesting_info,
3293            inter_group_segments: tree.inter_group_segments.clone(),
3294        }
3295    }
3296
3297    /// Reverse-map a `MappedMessage` back to an `AssembledTree`.
3298    ///
3299    /// Two-engine approach mirroring `map_interchange()`:
3300    /// - `msg_engine` handles message-level stammdaten → SG2/SG3 groups
3301    /// - `tx_engine` handles per-transaction stammdaten → SG4 instances
3302    ///
3303    /// All entities (including prozessdaten/nachricht) are in `tx.stammdaten`.
3304    /// Results are merged into one `AssembledGroupInstance` per transaction,
3305    /// collected into an SG4 `AssembledGroup`, then combined with message-level groups.
3306    pub fn map_interchange_reverse(
3307        msg_engine: &MappingEngine,
3308        tx_engine: &MappingEngine,
3309        mapped: &crate::model::MappedMessage,
3310        transaction_group: &str,
3311        filtered_mig: Option<&MigSchema>,
3312    ) -> AssembledTree {
3313        // Step 1: Reverse message-level stammdaten.
3314        //
3315        // The message's metadata entity goes back in here first: the forward
3316        // pass split `Nachricht` out into its own slot, but the definitions
3317        // resolve against one flat entity map, so without this the BGM/DTM
3318        // segments it feeds cannot be rebuilt. Clone only when there is
3319        // metadata to restore — keeps the common path zero-copy.
3320        let _owned_msg: Option<serde_json::Value>;
3321        let msg_stammdaten = if !mapped.nachricht_meta.is_null() {
3322            let mut merged = mapped.stammdaten.clone();
3323            crate::model::restore_entity(
3324                &mut merged,
3325                crate::model::MSG_METADATA_ENTITY,
3326                &mapped.nachricht_meta,
3327            );
3328            _owned_msg = Some(merged);
3329            _owned_msg.as_ref().unwrap()
3330        } else {
3331            _owned_msg = None;
3332            &mapped.stammdaten
3333        };
3334
3335        let msg_tree = msg_engine.map_all_reverse_with_mig(
3336            msg_stammdaten,
3337            if mapped.nesting_info.is_empty() {
3338                None
3339            } else {
3340                Some(&mapped.nesting_info)
3341            },
3342            filtered_mig,
3343        );
3344
3345        // Step 2: Build transaction instances from each Transaktion
3346        let mut sg4_reps: Vec<AssembledGroupInstance> = Vec::new();
3347
3348        // Collect all definitions with their relative paths and sort by depth.
3349        // Shallower paths (SG8) must be processed before deeper ones (SG8:0.SG10)
3350        // so that parent group repetitions exist before children are added.
3351        struct DefWithMeta<'a> {
3352            def: &'a MappingDefinition,
3353            relative: String,
3354            depth: usize,
3355        }
3356
3357        let mut sorted_defs: Vec<DefWithMeta> = tx_engine
3358            .definitions
3359            .iter()
3360            // `parent_field` and bound children are reversed with their parent
3361            // object (see `reverse_nested_children`).
3362            .filter(|def| {
3363                def.meta.parent_field.is_none() && !is_bound_child(&tx_engine.definitions, def)
3364            })
3365            .map(|def| {
3366                let relative = strip_tx_group_prefix(&def.meta.source_group, transaction_group);
3367                let depth = if relative.is_empty() {
3368                    0
3369                } else {
3370                    relative.chars().filter(|c| *c == '.').count() + 1
3371                };
3372                DefWithMeta {
3373                    def,
3374                    relative,
3375                    depth,
3376                }
3377            })
3378            .collect();
3379
3380        // Build parent source_path → rep_index map from deeper definitions.
3381        // SG10 defs like "SG4.SG8:0.SG10" with source_path "sg4.sg8_z79.sg10"
3382        // tell us that the SG8 def with source_path "sg4.sg8_z79" should be rep 0.
3383        let mut parent_rep_map: std::collections::HashMap<String, usize> =
3384            std::collections::HashMap::new();
3385        for dm in &sorted_defs {
3386            if dm.depth >= 2 {
3387                let parts: Vec<&str> = dm.relative.split('.').collect();
3388                let (_, parent_rep) = parse_group_spec(parts[0]);
3389                if let Some(rep_idx) = parent_rep {
3390                    if let Some(sp) = &dm.def.meta.source_path {
3391                        if let Some((parent_path, _)) = sp.rsplit_once('.') {
3392                            parent_rep_map
3393                                .entry(parent_path.to_string())
3394                                .or_insert(rep_idx);
3395                        }
3396                    }
3397                }
3398            }
3399        }
3400
3401        // Augment shallow definitions with explicit rep indices from the map,
3402        // but only for single-rep cases (no multi-rep — those use dynamic tracking).
3403        for dm in &mut sorted_defs {
3404            if dm.depth == 1 && !dm.relative.contains(':') {
3405                if let Some(sp) = &dm.def.meta.source_path {
3406                    if let Some(rep_idx) = parent_rep_map.get(sp.as_str()) {
3407                        dm.relative = format!("{}:{}", dm.relative, rep_idx);
3408                    }
3409                }
3410            }
3411        }
3412
3413        // Sort: shallower depth first, so SG8 defs create reps before SG8:N.SG10 defs.
3414        // Within same depth, sort by MIG group position (if available) for correct emission order,
3415        // falling back to alphabetical relative path for deterministic ordering.
3416        //
3417        // For variant groups (SG8 with Z01/Z03/Z07 etc.), use per-variant MIG positions
3418        // extracted from each definition's source_path qualifier suffix (e.g., "sg4.sg8_z01" → "Z01").
3419        if let Some(mig) = filtered_mig {
3420            let mig_order = build_reverse_mig_group_order(mig, transaction_group);
3421            sorted_defs.sort_by(|a, b| {
3422                a.depth.cmp(&b.depth).then_with(|| {
3423                    let a_id = a.relative.split(':').next().unwrap_or(&a.relative);
3424                    let b_id = b.relative.split(':').next().unwrap_or(&b.relative);
3425                    // Try per-variant lookup from source_path (e.g., "sg4.sg8_z01" → "SG8_Z01")
3426                    let a_pos = variant_mig_position(a.def, a_id, &mig_order);
3427                    let b_pos = variant_mig_position(b.def, b_id, &mig_order);
3428                    a_pos.cmp(&b_pos).then(a.relative.cmp(&b.relative))
3429                })
3430            });
3431        } else {
3432            sorted_defs.sort_by(|a, b| a.depth.cmp(&b.depth).then(a.relative.cmp(&b.relative)));
3433        }
3434
3435        for tx in &mapped.transaktionen {
3436            let mut root_segs: Vec<AssembledSegment> = Vec::new();
3437            let mut child_groups: Vec<AssembledGroup> = Vec::new();
3438
3439            // `transaktionsdaten` is merged back for the same reason as the
3440            // message's metadata above — the definitions expect one flat map.
3441            let _owned_tx: Option<serde_json::Value>;
3442            let tx_stammdaten: &serde_json::Value = if !tx.transaktionsdaten.is_null() {
3443                let mut merged = tx.stammdaten.clone();
3444                crate::model::restore_entity(
3445                    &mut merged,
3446                    crate::model::TX_METADATA_ENTITY,
3447                    &tx.transaktionsdaten,
3448                );
3449                _owned_tx = Some(merged);
3450                _owned_tx.as_ref().unwrap()
3451            } else {
3452                _owned_tx = None;
3453                &tx.stammdaten
3454            };
3455
3456            // Track source_path → repetition indices for parent groups (top-down).
3457            // Built during depth-1 processing, used by depth-2+ defs without
3458            // explicit rep indices to find their correct parent via source_path.
3459            // Vec<usize> supports multi-rep parents (e.g., two SG8+ZF3 reps).
3460            let mut source_path_to_rep: std::collections::HashMap<String, Vec<usize>> =
3461                std::collections::HashMap::new();
3462
3463            for dm in &sorted_defs {
3464                // Determine the BO4E value to reverse-map from.
3465                // Check top level first, then nested under parent entity.
3466                let entity_key = to_camel_case(&dm.def.meta.entity);
3467                let _tx_extracted: Option<serde_json::Value>;
3468                let bo4e_value = if let Some(v) = tx_stammdaten.get(&entity_key) {
3469                    _tx_extracted = None;
3470                    v
3471                } else if dm.def.meta.source_group.contains('.') {
3472                    match extract_child_from_parent(tx_stammdaten, &tx_engine.definitions, dm.def) {
3473                        Some(v) => {
3474                            _tx_extracted = Some(v);
3475                            _tx_extracted.as_ref().unwrap()
3476                        }
3477                        None => continue,
3478                    }
3479                } else {
3480                    continue;
3481                };
3482
3483                // Support map-keyed entities from typed PID format (same logic as map_all_reverse).
3484                let unwrapped_value: Option<serde_json::Value>;
3485                let bo4e_value = if bo4e_value.is_object() && !bo4e_value.is_array() {
3486                    if let Some(disc_value) = dm
3487                        .def
3488                        .meta
3489                        .discriminator
3490                        .as_deref()
3491                        .and_then(|d| d.split_once('='))
3492                        .map(|(_, v)| v)
3493                    {
3494                        if let Some(inner) = bo4e_value.get(disc_value) {
3495                            let mut injected = inner.clone();
3496                            if let Some(qualifier_field) = find_qualifier_companion_field(
3497                                &tx_engine.definitions,
3498                                &dm.def.meta.entity,
3499                            ) {
3500                                if let Some(obj) = injected.as_object_mut() {
3501                                    obj.entry(qualifier_field).or_insert_with(|| {
3502                                        serde_json::Value::String(disc_value.to_string())
3503                                    });
3504                                }
3505                            }
3506                            unwrapped_value = Some(injected);
3507                            unwrapped_value.as_ref().unwrap()
3508                        } else {
3509                            bo4e_value
3510                        }
3511                    } else if is_map_keyed_object(bo4e_value) {
3512                        let map = bo4e_value.as_object().unwrap();
3513                        let arr: Vec<serde_json::Value> = map
3514                            .iter()
3515                            .map(|(key, val)| {
3516                                let mut item = val.clone();
3517                                if let Some(obj) = item.as_object_mut() {
3518                                    if let Some(qualifier_field) = find_qualifier_companion_field(
3519                                        &tx_engine.definitions,
3520                                        &dm.def.meta.entity,
3521                                    ) {
3522                                        let entry = obj
3523                                            .entry(qualifier_field)
3524                                            .or_insert(serde_json::Value::Null);
3525                                        if entry.is_null() {
3526                                            *entry = serde_json::Value::String(key.clone());
3527                                        }
3528                                    }
3529                                }
3530                                item
3531                            })
3532                            .collect();
3533                        unwrapped_value = Some(serde_json::Value::Array(arr));
3534                        unwrapped_value.as_ref().unwrap()
3535                    } else {
3536                        bo4e_value
3537                    }
3538                } else {
3539                    bo4e_value
3540                };
3541
3542                // Handle array entities: each element becomes a separate group rep.
3543                // This supports both the NAD/SG12 pattern (multiple qualifiers) and
3544                // the multi-rep pattern (e.g., two LOC+Z17 Messlokationen).
3545                let items: Vec<&serde_json::Value> = if bo4e_value.is_array() {
3546                    bo4e_value.as_array().unwrap().iter().collect()
3547                } else {
3548                    vec![bo4e_value]
3549                };
3550
3551                for (item_idx, item) in items.iter().enumerate() {
3552                    let instance = tx_engine.map_reverse(item, dm.def);
3553
3554                    // Skip empty instances (definition had no real BO4E data)
3555                    if instance.segments.is_empty() && instance.child_groups.is_empty() {
3556                        continue;
3557                    }
3558
3559                    if dm.relative.is_empty() {
3560                        // The definition maps the transaction group itself
3561                        // (CONTRL's SG1, UTILMD's SG4): its segments are the
3562                        // instance's own root segments. Children it nested with
3563                        // `parent_field` (CONTRL SG1.SG2, the UCS/UCD errors of
3564                        // this checked message) are already built as child
3565                        // groups of that instance and must travel with it.
3566                        root_segs.extend(instance.segments);
3567                        for child in instance.child_groups {
3568                            match child_groups
3569                                .iter_mut()
3570                                .find(|g| g.group_id == child.group_id)
3571                            {
3572                                Some(existing) => existing.repetitions.extend(child.repetitions),
3573                                None => child_groups.push(child),
3574                            }
3575                        }
3576                    } else {
3577                        // For depth-2+ defs without explicit rep index, resolve
3578                        // parent rep from source_path matching (qualifier-based).
3579                        // item_idx selects the correct parent rep for multi-rep entities.
3580                        let effective_relative = if dm.depth >= 2 {
3581                            // Multi-rep: strip hardcoded parent :N indices so
3582                            // resolve_child_relative uses source_path lookup instead.
3583                            let rel = if items.len() > 1 {
3584                                strip_all_rep_indices(&dm.relative)
3585                            } else {
3586                                dm.relative.clone()
3587                            };
3588                            // Use tx nesting info for multi-rep arrays, BUT skip it
3589                            // when source_path is present and resolves to a single
3590                            // parent rep. In that case, nesting_info indices (from the
3591                            // original tree) may not match the reverse tree's rep layout.
3592                            // resolve_child_relative uses reverse-tree source_path_to_rep
3593                            // which is always correct.
3594                            let skip_nesting = dm
3595                                .def
3596                                .meta
3597                                .source_path
3598                                .as_ref()
3599                                .and_then(|sp| sp.rsplit_once('.'))
3600                                .and_then(|(parent_path, _)| source_path_to_rep.get(parent_path))
3601                                .is_some_and(|reps| reps.len() == 1);
3602                            let nesting_idx = if items.len() > 1 && !skip_nesting {
3603                                dm.def
3604                                    .meta
3605                                    .source_path
3606                                    .as_ref()
3607                                    .and_then(|sp| tx.nesting_info.get(sp))
3608                                    .and_then(|dist| dist.get(item_idx))
3609                                    .copied()
3610                            } else {
3611                                None
3612                            };
3613                            if let Some(parent_rep) = nesting_idx {
3614                                // Direct placement using known nesting distribution
3615                                let parts: Vec<&str> = rel.split('.').collect();
3616                                let parent_id = parts[0].split(':').next().unwrap_or(parts[0]);
3617                                let rest = parts[1..].join(".");
3618                                format!("{}:{}.{}", parent_id, parent_rep, rest)
3619                            } else {
3620                                resolve_child_relative(
3621                                    &rel,
3622                                    dm.def.meta.source_path.as_deref(),
3623                                    &source_path_to_rep,
3624                                    item_idx,
3625                                )
3626                            }
3627                        } else if dm.depth == 1 {
3628                            // Depth-1: use nesting_info child indices for correct
3629                            // rep placement (preserves original interleaving order).
3630                            let child_key = dm
3631                                .def
3632                                .meta
3633                                .source_path
3634                                .as_ref()
3635                                .map(|sp| format!("{sp}#child"));
3636                            if let Some(child_indices) =
3637                                child_key.as_ref().and_then(|ck| tx.nesting_info.get(ck))
3638                            {
3639                                if let Some(&target) = child_indices.get(item_idx) {
3640                                    if target != usize::MAX {
3641                                        let base =
3642                                            dm.relative.split(':').next().unwrap_or(&dm.relative);
3643                                        format!("{}:{}", base, target)
3644                                    } else {
3645                                        dm.relative.clone()
3646                                    }
3647                                } else if items.len() > 1 && item_idx > 0 {
3648                                    strip_rep_index(&dm.relative)
3649                                } else {
3650                                    dm.relative.clone()
3651                                }
3652                            } else if items.len() > 1 && item_idx > 0 {
3653                                strip_rep_index(&dm.relative)
3654                            } else {
3655                                dm.relative.clone()
3656                            }
3657                        } else if items.len() > 1 && item_idx > 0 {
3658                            // Multi-rep entity with hardcoded :N index: first item uses
3659                            // the original index, subsequent items append (strip :N).
3660                            strip_rep_index(&dm.relative)
3661                        } else {
3662                            dm.relative.clone()
3663                        };
3664
3665                        let rep_used =
3666                            place_in_groups(&mut child_groups, &effective_relative, instance);
3667
3668                        // Track source_path → rep_index for depth-1 (parent) defs
3669                        if dm.depth == 1 {
3670                            if let Some(sp) = &dm.def.meta.source_path {
3671                                source_path_to_rep
3672                                    .entry(sp.clone())
3673                                    .or_default()
3674                                    .push(rep_used);
3675                            }
3676                        }
3677                    }
3678                }
3679            }
3680
3681            sg4_reps.push(AssembledGroupInstance {
3682                segments: root_segs,
3683                child_groups,
3684                entry_mig_number: None,
3685                variant_mig_numbers: vec![],
3686                skipped_segments: Vec::new(),
3687                skipped_positions: Vec::new(),
3688            });
3689        }
3690
3691        // Step 3: Combine message tree with transaction group.
3692        // Move UNS section separator from root segments to inter_group_segments.
3693        // UNS+D (detail) goes BEFORE the tx group (MSCONS: header/detail boundary).
3694        // UNS+S (summary) goes AFTER the tx group (ORDERS: detail/summary boundary).
3695        // Any segments that follow UNS in the sequence (e.g., summary MOA in REMADV)
3696        // are also placed in inter_group_segments alongside UNS.
3697        let mut root_segments = Vec::new();
3698        let mut uns_segments = Vec::new();
3699        let mut uns_is_summary = false;
3700        let mut found_uns = false;
3701        for seg in msg_tree.segments {
3702            if seg.tag == "UNS" {
3703                // Check if this is UNS+S (summary separator) vs UNS+D (detail separator)
3704                uns_is_summary = seg
3705                    .elements
3706                    .first()
3707                    .and_then(|el| el.first())
3708                    .map(|v| v == "S")
3709                    .unwrap_or(false);
3710                uns_segments.push(seg);
3711                found_uns = true;
3712            } else if found_uns {
3713                // Segments after UNS belong in the same inter_group position
3714                uns_segments.push(seg);
3715            } else {
3716                root_segments.push(seg);
3717            }
3718        }
3719
3720        let pre_group_count = root_segments.len();
3721        let mut all_groups = msg_tree.groups;
3722        let mut inter_group = msg_tree.inter_group_segments;
3723
3724        // Helper: parse SG number from group_id (e.g., "SG26" → 26).
3725        let sg_num = |id: &str| -> usize {
3726            id.strip_prefix("SG")
3727                .and_then(|n| n.parse::<usize>().ok())
3728                .unwrap_or(0)
3729        };
3730
3731        if !sg4_reps.is_empty() {
3732            if uns_is_summary {
3733                // UNS+S: place AFTER the transaction group (detail/summary boundary)
3734                all_groups.push(AssembledGroup {
3735                    group_id: transaction_group.to_string(),
3736                    repetitions: sg4_reps,
3737                });
3738                if !uns_segments.is_empty() {
3739                    // Sort groups by SG number so the disassembler emits them
3740                    // in MIG order.  Insert UNS right after the tx_group —
3741                    // any groups with higher SG numbers (e.g., SG50/SG52 in
3742                    // INVOIC) are post-UNS summary groups.
3743                    all_groups.sort_by_key(|g| sg_num(&g.group_id));
3744                    let tx_num = sg_num(transaction_group);
3745                    let uns_pos = all_groups
3746                        .iter()
3747                        .rposition(|g| sg_num(&g.group_id) <= tx_num)
3748                        .map(|i| i + 1)
3749                        .unwrap_or(all_groups.len());
3750                    inter_group.insert(uns_pos, uns_segments);
3751                }
3752            } else {
3753                // UNS+D: place BEFORE the transaction group (header/detail boundary)
3754                if !uns_segments.is_empty() {
3755                    inter_group.insert(all_groups.len(), uns_segments);
3756                }
3757                all_groups.push(AssembledGroup {
3758                    group_id: transaction_group.to_string(),
3759                    repetitions: sg4_reps,
3760                });
3761            }
3762        } else if !uns_segments.is_empty() {
3763            if transaction_group.is_empty() {
3764                // Truly message-only (tx_group=""): UNS is a section separator.
3765                // UNS+S (summary) goes AFTER all groups — e.g., ORDCHG UNS+S
3766                // follows SG1 (NAD+CTA+COM) groups.
3767                // UNS+D (detail) goes BEFORE groups.
3768                all_groups.sort_by_key(|g| sg_num(&g.group_id));
3769                if uns_is_summary {
3770                    inter_group.insert(all_groups.len(), uns_segments);
3771                } else {
3772                    inter_group.insert(0, uns_segments);
3773                }
3774            } else {
3775                // Has a tx_group but no tx reps (e.g., INVOIC PID 31004
3776                // Storno — no SG26 data).  Sort groups and insert UNS after
3777                // the last group with SG number ≤ tx_group number.
3778                all_groups.sort_by_key(|g| sg_num(&g.group_id));
3779                let tx_num = sg_num(transaction_group);
3780                let uns_pos = all_groups
3781                    .iter()
3782                    .rposition(|g| sg_num(&g.group_id) <= tx_num)
3783                    .map(|i| i + 1)
3784                    .unwrap_or(all_groups.len());
3785                inter_group.insert(uns_pos, uns_segments);
3786            }
3787        }
3788
3789        // Restore inter_group_segments captured during forward mapping
3790        // (e.g. PID-foreign top-level segments preserved by the assembler's
3791        // skip-unknown mode — see `Assembler::assemble_generic`). Without
3792        // this, BO4E forward + reverse drops anything not represented in a
3793        // TOML mapping definition. We append rather than overwrite so the
3794        // UNS placement computed above survives — same-key collisions are
3795        // rare in practice (UNS goes at well-known positions).
3796        for (k, segs) in &mapped.inter_group_segments {
3797            if segs.is_empty() {
3798                continue;
3799            }
3800            let existing_tags: std::collections::HashSet<String> = inter_group
3801                .get(k)
3802                .map(|v| v.iter().map(|s| s.tag.clone()).collect())
3803                .unwrap_or_default();
3804            for seg in segs {
3805                if existing_tags.contains(&seg.tag) {
3806                    continue;
3807                }
3808                inter_group.entry(*k).or_default().push(seg.clone());
3809            }
3810        }
3811
3812        let mut tree = AssembledTree {
3813            segments: root_segments,
3814            groups: all_groups,
3815            post_group_start: pre_group_count,
3816            inter_group_segments: inter_group,
3817        };
3818
3819        // Order repetitions of same-ID group variants (SG2 NAD+MS / NAD+MR,
3820        // SG12 NAD+Z07 / NAD+Z08, SG10 CCI variants, …) by MIG variant order.
3821        // The reps above were appended in definition order and, within one
3822        // definition, in the order of the BO4E JSON array — which carries no
3823        // ordering information. The transaction group itself keeps its order:
3824        // the order of transactions is data.
3825        if let Some(mig) = filtered_mig {
3826            mig_assembly::repetition_order::sort_repetitions_by_mig_variant(
3827                &mut tree,
3828                mig,
3829                (!transaction_group.is_empty()).then_some(transaction_group),
3830            );
3831        }
3832        tree
3833    }
3834
3835    /// Build an assembled group from BO4E values and a definition.
3836    pub fn build_group_from_bo4e(
3837        &self,
3838        bo4e_value: &serde_json::Value,
3839        def: &MappingDefinition,
3840    ) -> AssembledGroup {
3841        let instance = self.map_reverse(bo4e_value, def);
3842        let leaf_group = def
3843            .meta
3844            .source_group
3845            .rsplit('.')
3846            .next()
3847            .unwrap_or(&def.meta.source_group);
3848
3849        AssembledGroup {
3850            group_id: leaf_group.to_string(),
3851            repetitions: vec![instance],
3852        }
3853    }
3854
3855    /// Forward-map an assembled tree to a typed interchange.
3856    ///
3857    /// Runs the dynamic mapping pipeline, wraps the result with metadata,
3858    /// then converts via JSON serialization into the caller's typed structs.
3859    ///
3860    /// - `M`: message-level stammdaten type (e.g., `Pid55001MsgStammdaten`)
3861    /// - `T`: transaction-level stammdaten type (e.g., `Pid55001TxStammdaten`)
3862    pub fn map_interchange_typed<M, T>(
3863        msg_engine: &MappingEngine,
3864        tx_engine: &MappingEngine,
3865        tree: &AssembledTree,
3866        tx_group: &str,
3867        enrich_codes: bool,
3868        nachrichtendaten: crate::model::Nachrichtendaten,
3869        interchangedaten: crate::model::Interchangedaten,
3870    ) -> Result<crate::model::Interchange<M, T>, serde_json::Error>
3871    where
3872        M: serde::de::DeserializeOwned,
3873        T: serde::de::DeserializeOwned,
3874    {
3875        let mapped = Self::map_interchange(msg_engine, tx_engine, tree, tx_group, enrich_codes);
3876        let nachricht = mapped.into_dynamic_nachricht(nachrichtendaten);
3877        let dynamic = crate::model::DynamicInterchange {
3878            interchangedaten,
3879            nachrichten: vec![nachricht],
3880        };
3881        let value = serde_json::to_value(&dynamic)?;
3882        serde_json::from_value(value)
3883    }
3884
3885    /// Reverse-map a typed interchange nachricht back to an assembled tree.
3886    ///
3887    /// Serializes the typed struct to JSON, then runs the dynamic reverse pipeline.
3888    ///
3889    /// - `M`: message-level stammdaten type
3890    /// - `T`: transaction-level stammdaten type
3891    pub fn map_interchange_reverse_typed<M, T>(
3892        msg_engine: &MappingEngine,
3893        tx_engine: &MappingEngine,
3894        nachricht: &crate::model::Nachricht<M, T>,
3895        tx_group: &str,
3896    ) -> Result<AssembledTree, serde_json::Error>
3897    where
3898        M: serde::Serialize,
3899        T: serde::Serialize,
3900    {
3901        // The reverse resolves definitions against one flat entity map, so both
3902        // metadata slots go back where the mappings expect to find them.
3903        let mut stammdaten = serde_json::to_value(&nachricht.stammdaten)?;
3904        crate::model::restore_message_metadata(&mut stammdaten, &nachricht.nachrichtendaten);
3905        let transaktionen: Vec<crate::model::MappedTransaktion> = nachricht
3906            .transaktionen
3907            .iter()
3908            .map(|t| {
3909                Ok(crate::model::MappedTransaktion {
3910                    stammdaten: serde_json::to_value(t)?,
3911                    transaktionsdaten: serde_json::Value::Null,
3912                    nesting_info: Default::default(),
3913                })
3914            })
3915            .collect::<Result<Vec<_>, serde_json::Error>>()?;
3916        let mapped = crate::model::MappedMessage {
3917            stammdaten,
3918            nachricht_meta: serde_json::Value::Null,
3919            transaktionen,
3920            nesting_info: Default::default(),
3921            inter_group_segments: Default::default(),
3922        };
3923        Ok(Self::map_interchange_reverse(
3924            msg_engine, tx_engine, &mapped, tx_group, None,
3925        ))
3926    }
3927}
3928
3929/// Parse a group path part with optional repetition: "SG8:1" → ("SG8", Some(1)).
3930/// Parse a source_path part into (group_id, optional_qualifier).
3931///
3932/// `"sg8_z98"` → `("sg8", Some("z98"))`
3933/// `"sg4"` → `("sg4", None)`
3934/// `"sg10"` → `("sg10", None)`
3935fn parse_source_path_part(part: &str) -> (&str, Option<&str>) {
3936    // Find the first underscore that separates group from qualifier.
3937    // Source path parts look like "sg8_z98", "sg4", "sg10", "sg12_z04".
3938    // The group ID is always "sgN", so the underscore after the digits is the separator.
3939    if let Some(pos) = part.find('_') {
3940        let group = &part[..pos];
3941        let qualifier = &part[pos + 1..];
3942        if !qualifier.is_empty() {
3943            return (group, Some(qualifier));
3944        }
3945    }
3946    (part, None)
3947}
3948
3949/// Build a map from group ID (e.g., "SG5", "SG8") to its position index
3950/// within the transaction group's nested_groups Vec.
3951/// Used by `map_interchange_reverse` to sort definitions in MIG order.
3952///
3953/// For variant groups (same ID with variant_code set, e.g., SG8 with Z01, Z03, Z07),
3954/// stores per-variant positions (e.g., "SG8_Z01" → 0, "SG8_Z03" → 1) so that
3955/// definitions are sorted in MIG XML order rather than alphabetical qualifier order.
3956fn build_reverse_mig_group_order(mig: &MigSchema, tx_group_id: &str) -> HashMap<String, usize> {
3957    let mut order = HashMap::new();
3958    if let Some(tg) = mig.segment_groups.iter().find(|g| g.id == tx_group_id) {
3959        for (i, nested) in tg.nested_groups.iter().enumerate() {
3960            // For variant groups, store per-variant key (e.g., "SG8_Z01" → i)
3961            if let Some(ref vc) = nested.variant_code {
3962                let variant_key = format!("{}_{}", nested.id, vc.to_uppercase());
3963                order.insert(variant_key, i);
3964            }
3965            // Always store base group ID for fallback
3966            order.entry(nested.id.clone()).or_insert(i);
3967        }
3968    }
3969    order
3970}
3971
3972/// Extract the MIG position for a definition, using per-variant lookup when possible.
3973///
3974/// For a definition with source_path "sg4.sg8_z01", extracts the variant qualifier "Z01"
3975/// and looks up "SG8_Z01" in the MIG order map. Falls back to the base group ID (e.g., "SG8")
3976/// if no variant qualifier is found or if the per-variant key isn't in the map.
3977fn variant_mig_position(
3978    def: &MappingDefinition,
3979    base_group_id: &str,
3980    mig_order: &HashMap<String, usize>,
3981) -> usize {
3982    // Try to extract variant qualifier from source_path.
3983    // source_path like "sg4.sg8_z01" or "sg4.sg8_z01.sg10" — we want the part matching base_group_id.
3984    if let Some(ref sp) = def.meta.source_path {
3985        // Find the path segment matching the base group (e.g., "sg8_z01" for base "SG8")
3986        let base_lower = base_group_id.to_lowercase();
3987        for part in sp.split('.') {
3988            if part.starts_with(&base_lower)
3989                || part.starts_with(base_group_id.to_lowercase().as_str())
3990            {
3991                // Extract qualifier suffix: "sg8_z01" → "z01"
3992                if let Some(underscore_pos) = part.find('_') {
3993                    let qualifier = &part[underscore_pos + 1..];
3994                    let variant_key = format!("{}_{}", base_group_id, qualifier.to_uppercase());
3995                    if let Some(&pos) = mig_order.get(&variant_key) {
3996                        return pos;
3997                    }
3998                }
3999            }
4000        }
4001    }
4002    // Fallback to base group position
4003    mig_order.get(base_group_id).copied().unwrap_or(usize::MAX)
4004}
4005
4006/// Find a group repetition whose entry segment has a matching qualifier.
4007///
4008/// The entry segment is the first segment in the instance (e.g., SEQ for SG8).
4009/// The qualifier is matched against `elements[0][0]` (case-insensitive).
4010fn find_rep_by_entry_qualifier<'a>(
4011    reps: &'a [AssembledGroupInstance],
4012    qualifier: &str,
4013) -> Option<&'a AssembledGroupInstance> {
4014    // Support compound qualifiers like "za1_za2" — match any part.
4015    let parts: Vec<&str> = qualifier.split('_').collect();
4016    reps.iter().find(|inst| {
4017        inst.segments.first().is_some_and(|seg| {
4018            seg.elements
4019                .first()
4020                .and_then(|e| e.first())
4021                .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
4022        })
4023    })
4024}
4025
4026/// Find ALL repetitions whose entry segment qualifier matches (case-insensitive).
4027fn find_all_reps_by_entry_qualifier<'a>(
4028    reps: &'a [AssembledGroupInstance],
4029    qualifier: &str,
4030) -> Vec<&'a AssembledGroupInstance> {
4031    // Support compound qualifiers like "za1_za2" — match any part.
4032    let parts: Vec<&str> = qualifier.split('_').collect();
4033    reps.iter()
4034        .filter(|inst| {
4035            inst.segments.first().is_some_and(|seg| {
4036                seg.elements
4037                    .first()
4038                    .and_then(|e| e.first())
4039                    .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
4040            })
4041        })
4042        .collect()
4043}
4044
4045/// Check if a source_path contains qualifier suffixes (e.g., "sg8_z98").
4046fn has_source_path_qualifiers(source_path: &str) -> bool {
4047    source_path.split('.').any(|part| {
4048        if let Some(pos) = part.find('_') {
4049            pos < part.len() - 1
4050        } else {
4051            false
4052        }
4053    })
4054}
4055
4056fn parse_group_spec(part: &str) -> (&str, Option<usize>) {
4057    if let Some(colon_pos) = part.find(':') {
4058        let id = &part[..colon_pos];
4059        let rep = part[colon_pos + 1..].parse::<usize>().ok();
4060        (id, rep)
4061    } else {
4062        (part, None)
4063    }
4064}
4065
4066/// Strip the transaction group prefix from a source_group path.
4067///
4068/// Given `source_group = "SG4.SG8:0.SG10"` and `tx_group = "SG4"`,
4069/// returns `"SG8:0.SG10"`.
4070/// Given `source_group = "SG4"` and `tx_group = "SG4"`, returns `""`.
4071fn strip_tx_group_prefix(source_group: &str, tx_group: &str) -> String {
4072    if source_group == tx_group || source_group.is_empty() {
4073        String::new()
4074    } else if let Some(rest) = source_group.strip_prefix(tx_group) {
4075        rest.strip_prefix('.').unwrap_or(rest).to_string()
4076    } else {
4077        source_group.to_string()
4078    }
4079}
4080
4081/// Place a reverse-mapped group instance into the correct nesting position.
4082///
4083/// `relative_path` is the group path relative to the transaction group:
4084/// - `"SG5"` → top-level child group
4085/// - `"SG8:0.SG10"` → SG10 inside SG8 repetition 0
4086///
4087/// Returns the repetition index used at the first nesting level.
4088fn place_in_groups(
4089    groups: &mut Vec<AssembledGroup>,
4090    relative_path: &str,
4091    instance: AssembledGroupInstance,
4092) -> usize {
4093    let parts: Vec<&str> = relative_path.split('.').collect();
4094
4095    if parts.len() == 1 {
4096        // Leaf group: "SG5", "SG8", "SG12", or with explicit index "SG8:0"
4097        let (id, rep) = parse_group_spec(parts[0]);
4098
4099        // Find or create the group
4100        let group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == id) {
4101            g
4102        } else {
4103            groups.push(AssembledGroup {
4104                group_id: id.to_string(),
4105                repetitions: vec![],
4106            });
4107            groups.last_mut().unwrap()
4108        };
4109
4110        if let Some(rep_idx) = rep {
4111            // Explicit index: place at specific position, merging into existing
4112            while group.repetitions.len() <= rep_idx {
4113                group.repetitions.push(AssembledGroupInstance {
4114                    segments: vec![],
4115                    child_groups: vec![],
4116                    entry_mig_number: None,
4117                    variant_mig_numbers: vec![],
4118                    skipped_segments: Vec::new(),
4119                    skipped_positions: Vec::new(),
4120                });
4121            }
4122            group.repetitions[rep_idx]
4123                .segments
4124                .extend(instance.segments);
4125            group.repetitions[rep_idx]
4126                .child_groups
4127                .extend(instance.child_groups);
4128            rep_idx
4129        } else {
4130            // No index: append new repetition
4131            let pos = group.repetitions.len();
4132            group.repetitions.push(instance);
4133            pos
4134        }
4135    } else {
4136        // Nested path: e.g., "SG8:0.SG10" → place SG10 inside SG8 rep 0
4137        let (parent_id, parent_rep) = parse_group_spec(parts[0]);
4138        let rep_idx = parent_rep.unwrap_or(0);
4139
4140        // Find or create the parent group
4141        let parent_group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == parent_id) {
4142            g
4143        } else {
4144            groups.push(AssembledGroup {
4145                group_id: parent_id.to_string(),
4146                repetitions: vec![],
4147            });
4148            groups.last_mut().unwrap()
4149        };
4150
4151        // Ensure the target repetition exists (extend with empty instances if needed)
4152        while parent_group.repetitions.len() <= rep_idx {
4153            parent_group.repetitions.push(AssembledGroupInstance {
4154                segments: vec![],
4155                child_groups: vec![],
4156                entry_mig_number: None,
4157                variant_mig_numbers: vec![],
4158                skipped_segments: Vec::new(),
4159                skipped_positions: Vec::new(),
4160            });
4161        }
4162
4163        let remaining = parts[1..].join(".");
4164        place_in_groups(
4165            &mut parent_group.repetitions[rep_idx].child_groups,
4166            &remaining,
4167            instance,
4168        );
4169        rep_idx
4170    }
4171}
4172
4173/// Resolve the effective relative path for a child definition (depth >= 2).
4174///
4175/// If the child's relative already has an explicit parent rep index (e.g., "SG8:5.SG10"),
4176/// use it as-is. Otherwise, use the `source_path` to look up the parent's actual
4177/// repetition index from `source_path_to_rep`.
4178///
4179/// `item_idx` selects which parent rep to use when the parent created multiple reps
4180/// (e.g., two SG8 reps with ZF3 → item_idx 0 picks the first, 1 picks the second).
4181///
4182/// Example: relative = "SG8.SG10", source_path = "sg4.sg8_zf3.sg10"
4183/// → looks up "sg4.sg8_zf3" in map → finds reps [3, 4] → item_idx=1 → returns "SG8:4.SG10"
4184fn resolve_child_relative(
4185    relative: &str,
4186    source_path: Option<&str>,
4187    source_path_to_rep: &std::collections::HashMap<String, Vec<usize>>,
4188    item_idx: usize,
4189) -> String {
4190    let parts: Vec<&str> = relative.split('.').collect();
4191    if parts.is_empty() {
4192        return relative.to_string();
4193    }
4194
4195    // If first part already has explicit index, keep as-is
4196    let (parent_id, parent_rep) = parse_group_spec(parts[0]);
4197    if parent_rep.is_some() {
4198        return relative.to_string();
4199    }
4200
4201    // Try to resolve from source_path: extract parent path and look up its rep
4202    if let Some(sp) = source_path {
4203        if let Some((parent_path, _child)) = sp.rsplit_once('.') {
4204            // Exact match first.
4205            if let Some(rep_indices) = source_path_to_rep.get(parent_path) {
4206                let rep_idx = rep_indices
4207                    .get(item_idx)
4208                    .or_else(|| rep_indices.last())
4209                    .copied()
4210                    .unwrap_or(0);
4211                let rest = parts[1..].join(".");
4212                return format!("{}:{}.{}", parent_id, rep_idx, rest);
4213            }
4214            // Fallback: variant wildcard. When TOMLs use a flat parent path
4215            // like "sg4" but the schema splits it into variants (e.g. sg4_su,
4216            // sg4_z10..z21), union the reps from every matching variant so a
4217            // per-item iteration can place each child under its own parent.
4218            // `PidSchemaIndex::has_group` already accepts this style for
4219            // forward mapping — reverse mapping needs the same or children
4220            // from all-but-one variant get dropped (PARTIN 12 SG4 reps).
4221            let prefix = format!("{}_", parent_path);
4222            let mut unioned: Vec<usize> = source_path_to_rep
4223                .iter()
4224                .filter(|(k, _)| k.starts_with(&prefix))
4225                .flat_map(|(_, v)| v.iter().copied())
4226                .collect();
4227            if !unioned.is_empty() {
4228                unioned.sort_unstable();
4229                unioned.dedup();
4230                let rep_idx = unioned
4231                    .get(item_idx)
4232                    .or_else(|| unioned.last())
4233                    .copied()
4234                    .unwrap_or(0);
4235                let rest = parts[1..].join(".");
4236                return format!("{}:{}.{}", parent_id, rep_idx, rest);
4237            }
4238        }
4239    }
4240
4241    // No resolution possible, keep original
4242    relative.to_string()
4243}
4244
4245/// Parsed discriminator for filtering assembled group instances.
4246///
4247/// Discriminator format: "TAG.element_idx.component_idx=VALUE" or
4248/// "TAG.element_idx.component_idx=VAL1|VAL2" (pipe-separated multi-value).
4249/// E.g., "LOC.0.0=Z17" → match LOC segments where elements[0][0] == "Z17"
4250/// E.g., "RFF.0.0=Z49|Z53" → match RFF where elements[0][0] is Z49 OR Z53
4251struct DiscriminatorMatcher<'a> {
4252    tag: &'a str,
4253    element_idx: usize,
4254    component_idx: usize,
4255    expected_values: Vec<&'a str>,
4256    /// Optional occurrence index: `#N` selects the Nth match among instances.
4257    occurrence: Option<usize>,
4258}
4259
4260impl<'a> DiscriminatorMatcher<'a> {
4261    fn parse(disc: &'a str) -> Option<Self> {
4262        let (spec, expected) = disc.split_once('=')?;
4263        let parts: Vec<&str> = spec.split('.').collect();
4264        if parts.len() != 3 {
4265            return None;
4266        }
4267        let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
4268        Some(Self {
4269            tag: parts[0],
4270            element_idx: parts[1].parse().ok()?,
4271            component_idx: parts[2].parse().ok()?,
4272            expected_values: expected_raw.split('|').collect(),
4273            occurrence,
4274        })
4275    }
4276
4277    fn matches(&self, instance: &AssembledGroupInstance) -> bool {
4278        instance.segments.iter().any(|s| {
4279            s.tag.eq_ignore_ascii_case(self.tag)
4280                && s.elements
4281                    .get(self.element_idx)
4282                    .and_then(|e| e.get(self.component_idx))
4283                    .map(|v| self.expected_values.iter().any(|ev| v == ev))
4284                    .unwrap_or(false)
4285        })
4286    }
4287
4288    /// Filter instances, respecting the occurrence index if present.
4289    fn filter_instances<'b>(
4290        &self,
4291        instances: Vec<&'b AssembledGroupInstance>,
4292    ) -> Vec<&'b AssembledGroupInstance> {
4293        let matching: Vec<_> = instances
4294            .into_iter()
4295            .filter(|inst| self.matches(inst))
4296            .collect();
4297        if let Some(occ) = self.occurrence {
4298            matching.into_iter().nth(occ).into_iter().collect()
4299        } else {
4300            matching
4301        }
4302    }
4303}
4304
4305/// Parse an optional occurrence index from a discriminator expected value.
4306///
4307/// `"TN#1"` → `("TN", Some(1))` — select the 2nd matching rep
4308/// `"TN"`   → `("TN", None)` — select all matching reps
4309/// `"Z13|Z14#0"` → `("Z13|Z14", Some(0))` — first match among Z13 or Z14
4310fn parse_discriminator_occurrence(expected: &str) -> (&str, Option<usize>) {
4311    if let Some(hash_pos) = expected.rfind('#') {
4312        if let Ok(occ) = expected[hash_pos + 1..].parse::<usize>() {
4313            return (&expected[..hash_pos], Some(occ));
4314        }
4315    }
4316    (expected, None)
4317}
4318
4319/// Strip explicit rep index from a relative path: "SG5:4" → "SG5", "SG8:3" → "SG8".
4320/// Used for multi-rep entities where subsequent items should append rather than
4321/// merge into the same rep position.
4322fn strip_rep_index(relative: &str) -> String {
4323    let (id, _) = parse_group_spec(relative);
4324    id.to_string()
4325}
4326
4327/// Strip all explicit rep indices from a multi-part relative path:
4328/// "SG8:3.SG10" → "SG8.SG10", "SG8:3.SG10:0" → "SG8.SG10".
4329/// Used for multi-rep depth-2+ entities so resolve_child_relative uses
4330/// source_path lookup instead of hardcoded indices.
4331pub(crate) fn strip_all_rep_indices(relative: &str) -> String {
4332    relative
4333        .split('.')
4334        .map(|part| {
4335            let (id, _) = parse_group_spec(part);
4336            id
4337        })
4338        .collect::<Vec<_>>()
4339        .join(".")
4340}
4341
4342// ── Nested child groups (`[meta] parent_field`) ──
4343
4344/// Whether `child` is a `parent_field` definition nested directly below `parent`
4345/// (which may itself be a `parent_field` definition — nesting can span several
4346/// group levels, one `parent_field` per level):
4347/// same entity, `source_group` exactly one level deeper, and (when both carry a
4348/// `source_path`) a structurally compatible parent path. Qualifiers on the parent
4349/// part are compared only when both sides specify one; the instance-level check
4350/// is [`nested_parent_qualifier`] + [`entry_qualifier_matches`].
4351pub fn is_nested_child_of(child: &MappingDefinition, parent: &MappingDefinition) -> bool {
4352    if child.meta.parent_field.is_none() || child.meta.entity != parent.meta.entity {
4353        return false;
4354    }
4355    let child_sg = strip_all_rep_indices(&child.meta.source_group);
4356    let parent_sg = strip_all_rep_indices(&parent.meta.source_group);
4357    match child_sg.rsplit_once('.') {
4358        Some((head, _)) if head.eq_ignore_ascii_case(&parent_sg) => {}
4359        _ => return false,
4360    }
4361    let (Some(child_sp), Some(parent_sp)) = (
4362        child.meta.source_path.as_deref(),
4363        parent.meta.source_path.as_deref(),
4364    ) else {
4365        return true;
4366    };
4367    let Some((child_parent_sp, _)) = child_sp.rsplit_once('.') else {
4368        return false;
4369    };
4370    let child_parts: Vec<&str> = child_parent_sp.split('.').collect();
4371    let parent_parts: Vec<&str> = parent_sp.split('.').collect();
4372    child_parts.len() == parent_parts.len()
4373        && child_parts.iter().zip(&parent_parts).all(|(c, p)| {
4374            let (c_id, c_q) = parse_source_path_part(c);
4375            let (p_id, p_q) = parse_source_path_part(p);
4376            c_id.eq_ignore_ascii_case(p_id)
4377                && match (c_q, p_q) {
4378                    (Some(cq), Some(pq)) => cq.eq_ignore_ascii_case(pq),
4379                    _ => true,
4380                }
4381        })
4382}
4383
4384/// Whether `child` is bound to the repetitions of `parent`: a definition without
4385/// `parent_field` whose `source_path` lies exactly one group below `parent`'s,
4386/// both writing the same entity (e.g. a CCI rule on `sg4.sg8_z01.sg10` writing
4387/// `regelzone` into the `MarktlokationDaten` of `sg4.sg8_z01`).
4388///
4389/// Its fields land in the object mapped from the parent repetition that
4390/// contains the child group, and are rendered under that same repetition. Both
4391/// were paired by array position before: with several SG8 repetitions and
4392/// SG10s under only some of them, a child's fields landed in the wrong object
4393/// and were rendered under the wrong SEQ. BO4E carries no positions; the
4394/// object a field sits in is the only link there is.
4395///
4396/// Only parents at least two groups deep bind (`sg4.sg8_z01`, not `sg4`): a
4397/// transaction-root or message-level group is one object anyway.
4398pub fn is_bound_child_of(child: &MappingDefinition, parent: &MappingDefinition) -> bool {
4399    if std::ptr::eq(child, parent)
4400        || child.meta.parent_field.is_some()
4401        || parent.meta.parent_field.is_some()
4402        || child.meta.target_list.is_some()
4403        || parent.meta.target_list.is_some()
4404        || child.meta.entity != parent.meta.entity
4405    {
4406        return false;
4407    }
4408    let (Some(child_sp), Some(parent_sp)) = (
4409        child.meta.source_path.as_deref(),
4410        parent.meta.source_path.as_deref(),
4411    ) else {
4412        return false;
4413    };
4414    parent_sp.contains('.')
4415        && child_sp
4416            .rsplit_once('.')
4417            .is_some_and(|(head, _)| head.eq_ignore_ascii_case(parent_sp))
4418}
4419
4420/// Whether `def` is bound to a parent definition among `definitions` (see
4421/// [`is_bound_child_of`]) and so mapped inside its parent, never on its own.
4422pub fn is_bound_child(definitions: &[MappingDefinition], def: &MappingDefinition) -> bool {
4423    definitions.iter().any(|p| is_bound_child_of(def, p))
4424}
4425
4426/// Entry qualifier the parent group instance must carry for a nested child
4427/// definition to apply (e.g. `"z08"` for `source_path = "sg4.sg12_z08.sg13"`).
4428fn nested_parent_qualifier(child: &MappingDefinition) -> Option<&str> {
4429    let (parent_path, _) = child.meta.source_path.as_deref()?.rsplit_once('.')?;
4430    let last = parent_path.rsplit('.').next()?;
4431    parse_source_path_part(last).1
4432}
4433
4434/// Leaf group id and optional entry qualifier of a nested child definition
4435/// (e.g. `("SG13", None)` for `source_group = "SG4.SG12.SG13"`).
4436fn nested_child_leaf(child: &MappingDefinition) -> (String, Option<&str>) {
4437    let leaf_group = strip_all_rep_indices(
4438        child
4439            .meta
4440            .source_group
4441            .rsplit('.')
4442            .next()
4443            .unwrap_or(&child.meta.source_group),
4444    );
4445    let leaf_qualifier = child
4446        .meta
4447        .source_path
4448        .as_deref()
4449        .and_then(|sp| sp.rsplit('.').next())
4450        .and_then(|part| parse_source_path_part(part).1);
4451    (leaf_group, leaf_qualifier)
4452}
4453
4454/// The repetitions of `instance`'s child group that the bound `child` maps:
4455/// the group named by its `source_path` leaf, narrowed by the leaf's qualifier
4456/// and by the definition's discriminator.
4457fn bound_child_reps<'i>(
4458    instance: &'i AssembledGroupInstance,
4459    child: &MappingDefinition,
4460) -> Vec<&'i AssembledGroupInstance> {
4461    let (leaf_id, leaf_qualifier) = nested_child_leaf(child);
4462    let Some(group) = instance
4463        .child_groups
4464        .iter()
4465        .find(|g| g.group_id.eq_ignore_ascii_case(&leaf_id))
4466    else {
4467        return Vec::new();
4468    };
4469    let reps: Vec<&AssembledGroupInstance> = match leaf_qualifier {
4470        Some(q) => find_all_reps_by_entry_qualifier(&group.repetitions, q),
4471        None => group.repetitions.iter().collect(),
4472    };
4473    match child
4474        .meta
4475        .discriminator
4476        .as_deref()
4477        .and_then(DiscriminatorMatcher::parse)
4478    {
4479        Some(matcher) => matcher.filter_instances(reps),
4480        None => reps,
4481    }
4482}
4483
4484/// Append `reps` to `instance`'s child group `leaf_id`, creating it if absent.
4485fn push_child_reps(
4486    instance: &mut AssembledGroupInstance,
4487    leaf_id: String,
4488    reps: Vec<AssembledGroupInstance>,
4489) {
4490    match instance
4491        .child_groups
4492        .iter_mut()
4493        .find(|g| g.group_id.eq_ignore_ascii_case(&leaf_id))
4494    {
4495        Some(group) => group.repetitions.extend(reps),
4496        None => instance.child_groups.push(AssembledGroup {
4497            group_id: leaf_id,
4498            repetitions: reps,
4499        }),
4500    }
4501}
4502
4503/// Whether the instance's entry segment (its first segment) carries `qualifier`
4504/// at `elements[0][0]`. Compound qualifiers (`"z53_z54"`) match any part.
4505fn entry_qualifier_matches(instance: &AssembledGroupInstance, qualifier: &str) -> bool {
4506    segment_qualifier_matches(instance.segments.first(), qualifier)
4507}
4508
4509/// [`entry_qualifier_matches`] for a group repetition rebuilt by the reverse
4510/// mapping from `def`. Its segments follow the order of `def`'s fields, so the
4511/// entry segment need not come first (e.g. `PIA` listed before `SEQ`). When
4512/// `def` has a discriminator, its segment tag names the entry segment.
4513fn rebuilt_entry_qualifier_matches(
4514    instance: &AssembledGroupInstance,
4515    def: &MappingDefinition,
4516    qualifier: &str,
4517) -> bool {
4518    let entry_tag = def
4519        .meta
4520        .discriminator
4521        .as_deref()
4522        .and_then(|d| d.split('.').next())
4523        .filter(|tag| !tag.is_empty());
4524    let entry = match entry_tag {
4525        Some(tag) => instance
4526            .segments
4527            .iter()
4528            .find(|s| s.tag.eq_ignore_ascii_case(tag)),
4529        None => instance.segments.first(),
4530    };
4531    segment_qualifier_matches(entry, qualifier)
4532}
4533
4534fn segment_qualifier_matches(segment: Option<&AssembledSegment>, qualifier: &str) -> bool {
4535    segment
4536        .and_then(|seg| seg.elements.first())
4537        .and_then(|e| e.first())
4538        .is_some_and(|v| qualifier.split('_').any(|q| v.eq_ignore_ascii_case(q)))
4539}
4540
4541/// Whether a `when_filled` guard's field carries data: a string (or an enriched
4542/// `{code, …}` object), or a non-empty array or object. A group whose content
4543/// sits in nested children (`parent_field`, e.g. `zuordnungen` from SG10) has
4544/// nothing else to name — its entry segment must still be written when they
4545/// are there, or the group cannot be rendered from data the AHB allows.
4546fn field_is_filled(bo4e_value: &serde_json::Value, field: &str) -> bool {
4547    let mut current = bo4e_value;
4548    for part in field.split('.') {
4549        match current.get(part) {
4550            Some(v) => current = v,
4551            None => return false,
4552        }
4553    }
4554    match current {
4555        serde_json::Value::String(s) => !s.is_empty(),
4556        serde_json::Value::Array(a) => !a.is_empty(),
4557        serde_json::Value::Object(o) => !o.is_empty(),
4558        serde_json::Value::Number(_) | serde_json::Value::Bool(_) => true,
4559        serde_json::Value::Null => false,
4560    }
4561}
4562
4563/// A list target `name[].sub` → `("name", "sub")`: the field writes `sub` of
4564/// one element of the array `name` per matching segment.
4565pub(crate) fn list_target(target: &str) -> Option<(&str, &str)> {
4566    let (list, sub) = target.split_once("[].")?;
4567    (!list.is_empty() && !sub.is_empty()).then_some((list, sub))
4568}
4569
4570/// Parse a segment tag with optional qualifier and occurrence index.
4571///
4572/// - `"dtm[92]"`    → `("DTM", Some("92"), 0)` — first (default) occurrence
4573/// - `"rff[Z34,1]"` → `("RFF", Some("Z34"), 1)` — second occurrence (0-indexed)
4574/// - `"rff[Z34,*]"` → `("RFF", Some("Z34"), 0)` — wildcard occurrence
4575/// - `"rff"`         → `("RFF", None, 0)`
4576pub(crate) fn parse_tag_qualifier(tag_part: &str) -> (String, Option<&str>, usize) {
4577    if let Some(bracket_start) = tag_part.find('[') {
4578        let tag = tag_part[..bracket_start].to_uppercase();
4579        let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
4580        if let Some(comma_pos) = inner.find(',') {
4581            let qualifier = &inner[..comma_pos];
4582            let index = inner[comma_pos + 1..].parse::<usize>().unwrap_or(0);
4583            // "*" wildcard means no qualifier filter — positional access only
4584            if qualifier == "*" {
4585                (tag, None, index)
4586            } else {
4587                (tag, Some(qualifier), index)
4588            }
4589        } else {
4590            (tag, Some(inner), 0)
4591        }
4592    } else {
4593        (tag_part.to_uppercase(), None, 0)
4594    }
4595}
4596
4597/// Deep-merge a BO4E value into the result map.
4598///
4599/// If the entity already exists as an object, new fields are merged in
4600/// (existing fields are NOT overwritten). This allows multiple TOML
4601/// definitions with the same `entity` name to contribute fields to one object.
4602pub fn deep_merge_insert(
4603    result: &mut serde_json::Map<String, serde_json::Value>,
4604    entity: &str,
4605    bo4e: serde_json::Value,
4606) {
4607    merge_entity(result, entity, bo4e, false);
4608}
4609
4610/// [`deep_merge_insert`], choosing what happens when the two values do not
4611/// line up (an array meets an object, or arrays of different lengths):
4612/// `keep_both` appends them as separate repetitions, otherwise the new value
4613/// replaces the old.
4614///
4615/// Keeping both is right for *sibling* groups sharing an entity — two LOC+Z16
4616/// (an array) and one LOC+Z22 (an object) are three `Marktlokation`
4617/// repetitions, and replacing dropped both MaLos without an error. It is wrong
4618/// for a group and its own flat child rule (an SG8 and its SG10 on one entity
4619/// without `parent_field`): their values are one repetition's fields, and
4620/// appending the child rows would turn each into an SG8 repetition of its own.
4621fn merge_entity(
4622    result: &mut serde_json::Map<String, serde_json::Value>,
4623    entity: &str,
4624    bo4e: serde_json::Value,
4625    keep_both: bool,
4626) {
4627    if let Some(existing) = result.get_mut(entity) {
4628        // Array + Array: element-wise merge (same entity from multiple TOML defs,
4629        // each producing an array for multi-rep groups like two LOC+Z17).
4630        if let (Some(existing_arr), Some(new_arr)) =
4631            (existing.as_array().map(|a| a.len()), bo4e.as_array())
4632        {
4633            if existing_arr == new_arr.len() {
4634                let existing_arr = existing.as_array_mut().unwrap();
4635                for (existing_elem, new_elem) in existing_arr.iter_mut().zip(new_arr) {
4636                    if let (Some(existing_map), Some(new_map)) =
4637                        (existing_elem.as_object_mut(), new_elem.as_object())
4638                    {
4639                        for (k, v) in new_map {
4640                            if let Some(existing_v) = existing_map.get_mut(k) {
4641                                if let (Some(existing_inner), Some(new_inner)) =
4642                                    (existing_v.as_object_mut(), v.as_object())
4643                                {
4644                                    for (ik, iv) in new_inner {
4645                                        existing_inner
4646                                            .entry(ik.clone())
4647                                            .or_insert_with(|| iv.clone());
4648                                    }
4649                                }
4650                            } else {
4651                                existing_map.insert(k.clone(), v.clone());
4652                            }
4653                        }
4654                    }
4655                }
4656                return;
4657            }
4658        }
4659        // Object + Object: field-level merge
4660        if let (Some(existing_map), serde_json::Value::Object(new_map)) =
4661            (existing.as_object_mut(), &bo4e)
4662        {
4663            for (k, v) in new_map {
4664                if let Some(existing_v) = existing_map.get_mut(k) {
4665                    // Recursively merge nested objects (e.g., companion types)
4666                    if let (Some(existing_inner), Some(new_inner)) =
4667                        (existing_v.as_object_mut(), v.as_object())
4668                    {
4669                        for (ik, iv) in new_inner {
4670                            existing_inner
4671                                .entry(ik.clone())
4672                                .or_insert_with(|| iv.clone());
4673                        }
4674                    }
4675                    // Don't overwrite existing scalar/array values
4676                } else {
4677                    existing_map.insert(k.clone(), v.clone());
4678                }
4679            }
4680            return;
4681        }
4682        if !keep_both {
4683            result.insert(entity.to_string(), bo4e);
4684            return;
4685        }
4686        // Shapes that do not line up are different repetitions: keep them all.
4687        let existing_items = match std::mem::take(existing) {
4688            serde_json::Value::Array(items) => items,
4689            other => vec![other],
4690        };
4691        let new_items = match bo4e {
4692            serde_json::Value::Array(items) => items,
4693            other => vec![other],
4694        };
4695        *existing = serde_json::Value::Array(existing_items.into_iter().chain(new_items).collect());
4696        return;
4697    }
4698    result.insert(entity.to_string(), bo4e);
4699}
4700
4701/// Append a definition's per-repetition output to a **list-valued field** on an
4702/// entity — the write half of `MappingMeta::target_list`.
4703///
4704/// This cannot go through `deep_merge_insert`, which documents that it does
4705/// "not overwrite existing scalar/array values". That rule is right for ordinary
4706/// fields and wrong here: a list field is the one place where several
4707/// definitions are *expected* to contribute to the same key (four separate
4708/// `Obis*` definitions all feed `zaehlwerke`), and under `deep_merge_insert`
4709/// every contribution after the first would be dropped without a trace.
4710///
4711/// Empty elements are skipped so an absent optional group does not leave a
4712/// `[{}]` behind, which would reverse into a phantom segment.
4713fn append_to_list_field(
4714    result: &mut serde_json::Map<String, serde_json::Value>,
4715    entity: &str,
4716    list_field: &str,
4717    bo4e: serde_json::Value,
4718) {
4719    let mut items = match bo4e {
4720        serde_json::Value::Array(a) => a,
4721        other => vec![other],
4722    };
4723    items.retain(|v| !v.as_object().is_some_and(|o| o.is_empty()));
4724    if items.is_empty() {
4725        return;
4726    }
4727    let entry = result
4728        .entry(entity.to_string())
4729        .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
4730    // An entity carrying a list field is a single object. If it is already an
4731    // array, some other definition made it multi-rep and the two shapes are
4732    // incompatible — leave it alone rather than corrupt it silently.
4733    let Some(obj) = entry.as_object_mut() else {
4734        return;
4735    };
4736    match obj.get_mut(list_field).and_then(|v| v.as_array_mut()) {
4737        Some(existing) => existing.extend(items),
4738        None => {
4739            obj.insert(list_field.to_string(), serde_json::Value::Array(items));
4740        }
4741    }
4742}
4743
4744/// Convert a PascalCase name to camelCase by lowering the first character.
4745///
4746/// E.g., `"Ansprechpartner"` → `"ansprechpartner"`,
4747/// `"AnsprechpartnerEdifact"` → `"ansprechpartnerEdifact"`,
4748/// `"ProduktpaketPriorisierung"` → `"produktpaketPriorisierung"`.
4749/// Detect whether a JSON object looks like a map-keyed entity (typed PID format).
4750///
4751/// Map-keyed objects have short uppercase/alphanumeric keys that look like qualifier
4752/// codes (e.g., `{"Z04": {...}, "Z09": {...}}` or `{"MS": {...}, "MR": {...}}`),
4753/// as opposed to normal field-name objects (e.g., `{"name1": "...", "adresse": {...}}`).
4754fn is_map_keyed_object(value: &serde_json::Value) -> bool {
4755    let Some(obj) = value.as_object() else {
4756        return false;
4757    };
4758    if obj.is_empty() {
4759        return false;
4760    }
4761    // All keys must be short (≤5 chars), uppercase/digit only, and all values must be objects
4762    obj.iter().all(|(k, v)| {
4763        k.len() <= 5
4764            && k.chars()
4765                .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
4766            && v.is_object()
4767    })
4768}
4769
4770/// Find the BO4E companion field name used for the qualifier/discriminator
4771/// across definitions that share the same entity name.
4772///
4773/// For example, if `Geschaeftspartner` has a definition with discriminator
4774/// `NAD.0.0=Z04` and companion field `nad.0.0 → nadQualifier`, this returns
4775/// `Some("nadQualifier")`.
4776///
4777/// Used to inject map keys into inner objects when converting map-keyed entities.
4778fn find_qualifier_companion_field(
4779    definitions: &[crate::definition::MappingDefinition],
4780    entity: &str,
4781) -> Option<String> {
4782    for def in definitions {
4783        if def.meta.entity != *entity || def.meta.parent_field.is_some() {
4784            continue;
4785        }
4786        let disc = def.meta.discriminator.as_deref()?;
4787        let (disc_path, _) = disc.split_once('=')?;
4788        let disc_path_lower = disc_path.to_lowercase();
4789
4790        // Search [fields] for the qualifier field (e.g., Marktteilnehmer has
4791        // "marktrolle" in [fields]).
4792        for (path, mapping) in &def.fields {
4793            let cf_path = path.to_lowercase();
4794            let matches = cf_path == disc_path_lower || format!("{}.0", cf_path) == disc_path_lower;
4795            if matches {
4796                let target = match mapping {
4797                    FieldMapping::Simple(t) => t.as_str(),
4798                    FieldMapping::Structured(s) => s.target.as_str(),
4799                    FieldMapping::Nested(_) => continue,
4800                };
4801                if !target.is_empty() {
4802                    return Some(target.to_string());
4803                }
4804            }
4805        }
4806    }
4807    None
4808}
4809
4810/// Extract a child entity from its parent entity in the reverse mapping input.
4811///
4812/// When a child entity (e.g., Kontakt with source_group="SG2.SG3") isn't found
4813/// at the top level, look inside the parent entity (e.g., Marktteilnehmer with
4814/// source_group="SG2") for a nested field matching the child's camelCase name.
4815///
4816/// For map-keyed parents ({"MS": {...}, "MR": {...}}), collects child values
4817/// from all inner objects that have the field, returning them as an array.
4818fn extract_child_from_parent(
4819    entities: &serde_json::Value,
4820    definitions: &[MappingDefinition],
4821    child_def: &MappingDefinition,
4822) -> Option<serde_json::Value> {
4823    extract_child_from_parent_with_indices(entities, definitions, child_def).map(|(v, _)| v)
4824}
4825
4826/// Like `extract_child_from_parent`, but also returns the parent rep indices
4827/// from which each child was extracted.  This allows the nesting distribution
4828/// to place child groups under the correct parent rep even when `nesting_info`
4829/// is unavailable (e.g., typed struct / manual JSON construction).
4830fn extract_child_from_parent_with_indices(
4831    entities: &serde_json::Value,
4832    definitions: &[MappingDefinition],
4833    child_def: &MappingDefinition,
4834) -> Option<(serde_json::Value, Vec<usize>)> {
4835    let parts: Vec<&str> = child_def.meta.source_group.split('.').collect();
4836    if parts.len() < 2 {
4837        return None;
4838    }
4839    let parent_group = parts[0];
4840    let parent_def = definitions
4841        .iter()
4842        .find(|d| d.meta.source_group == parent_group && d.meta.entity != child_def.meta.entity)?;
4843    let parent_key = to_camel_case(&parent_def.meta.entity);
4844    let child_key = to_camel_case(&child_def.meta.entity);
4845    let parent_value = entities.get(&parent_key)?;
4846
4847    // Map-keyed parent: collect child from each inner object
4848    if let Some(parent_map) = parent_value.as_object() {
4849        if is_map_keyed_value(parent_map) {
4850            let mut children: Vec<serde_json::Value> = Vec::new();
4851            let mut indices: Vec<usize> = Vec::new();
4852            for (i, (_key, inner)) in parent_map.iter().enumerate() {
4853                if let Some(child) = inner.get(&child_key) {
4854                    if !child.is_null() {
4855                        children.push(child.clone());
4856                        indices.push(i);
4857                    }
4858                }
4859            }
4860            return match children.len() {
4861                0 => None,
4862                1 => Some((children.into_iter().next().unwrap(), indices)),
4863                _ => Some((serde_json::Value::Array(children), indices)),
4864            };
4865        }
4866    }
4867
4868    // Array parent: collect child from each element
4869    if let Some(parent_arr) = parent_value.as_array() {
4870        let mut children: Vec<serde_json::Value> = Vec::new();
4871        let mut indices: Vec<usize> = Vec::new();
4872        for (i, item) in parent_arr.iter().enumerate() {
4873            if let Some(child) = item.get(&child_key) {
4874                if !child.is_null() {
4875                    children.push(child.clone());
4876                    indices.push(i);
4877                }
4878            }
4879        }
4880        return match children.len() {
4881            0 => None,
4882            1 => Some((children.into_iter().next().unwrap(), indices)),
4883            _ => Some((serde_json::Value::Array(children), indices)),
4884        };
4885    }
4886
4887    // Single parent object — always index 0
4888    let child = parent_value.get(&child_key)?;
4889    if child.is_null() {
4890        return None;
4891    }
4892    Some((child.clone(), vec![0]))
4893}
4894
4895/// Move child entities under their parent entities in the forward-mapped result.
4896///
4897/// For each definition with a dotted `source_group` (e.g., "SG2.SG3"), finds the
4898/// parent definition (e.g., "SG2") and moves the child entity from the top-level
4899/// result into the parent entity as a nested field.
4900fn nest_child_entities_in_result(
4901    result: &mut serde_json::Map<String, serde_json::Value>,
4902    definitions: &[MappingDefinition],
4903    nesting_info: &std::collections::HashMap<String, Vec<usize>>,
4904    transaction_group: Option<&str>,
4905) {
4906    let nesting_pairs = child_entity_nesting_pairs(definitions, transaction_group);
4907
4908    for (_parent_group, parent_entity, child_entity, child_source_path) in nesting_pairs {
4909        let parent_key = to_camel_case(&parent_entity);
4910        let child_key = to_camel_case(&child_entity);
4911
4912        // Remove child from top level (if present)
4913        let child_value = match result.remove(&child_key) {
4914            Some(v) => v,
4915            None => continue,
4916        };
4917
4918        // Get parent value.
4919        // If the parent is a plain array (not map-keyed), nesting would silently
4920        // place the child into arbitrary array elements. Skip and leave the child
4921        // at the top level where the reverse mapper can find it.
4922        let Some(parent_value) = result.get_mut(&parent_key) else {
4923            // Parent doesn't exist — put child back
4924            result.insert(child_key, child_value);
4925            continue;
4926        };
4927        if parent_value.is_array() {
4928            result.insert(child_key, child_value);
4929            continue;
4930        }
4931
4932        // Get the nesting distribution (which parent rep each child rep belongs to)
4933        let distribution = child_source_path
4934            .as_deref()
4935            .and_then(|sp| nesting_info.get(sp));
4936
4937        // Normalize child to a list of (index, value) pairs
4938        let child_items: Vec<(usize, &serde_json::Value)> = match &child_value {
4939            serde_json::Value::Array(arr) => arr.iter().enumerate().collect(),
4940            other => vec![(0, other)],
4941        };
4942
4943        // Helper: insert or append child value into a parent object field.
4944        // First call inserts the value; subsequent calls convert to array and append.
4945        let insert_or_append = |obj: &mut serde_json::Map<String, serde_json::Value>,
4946                                key: &str,
4947                                val: &serde_json::Value| {
4948            match obj.get_mut(key) {
4949                Some(existing) => {
4950                    // Convert single value to array, then push
4951                    if !existing.is_array() {
4952                        let prev = existing.take();
4953                        *existing = serde_json::Value::Array(vec![prev]);
4954                    }
4955                    if let Some(arr) = existing.as_array_mut() {
4956                        arr.push(val.clone());
4957                    }
4958                }
4959                None => {
4960                    obj.insert(key.to_string(), val.clone());
4961                }
4962            }
4963        };
4964
4965        // Handle parent as map-keyed object: {"MS": {...}, "MR": {...}}
4966        if let Some(parent_map) = parent_value.as_object_mut() {
4967            if is_map_keyed_value(parent_map) {
4968                // Map keys in insertion order correspond to rep indices
4969                let keys: Vec<String> = parent_map.keys().cloned().collect();
4970                for (i, child_item) in &child_items {
4971                    let target_idx = distribution
4972                        .and_then(|dist| dist.get(*i))
4973                        .copied()
4974                        .unwrap_or(0);
4975                    if let Some(key) = keys.get(target_idx) {
4976                        if let Some(inner) = parent_map.get_mut(key).and_then(|v| v.as_object_mut())
4977                        {
4978                            insert_or_append(inner, &child_key, child_item);
4979                        }
4980                    }
4981                }
4982                continue;
4983            }
4984        }
4985
4986        // Handle parent as array
4987        if let Some(parent_arr) = parent_value.as_array_mut() {
4988            for (i, child_item) in &child_items {
4989                let target_idx = distribution
4990                    .and_then(|dist| dist.get(*i))
4991                    .copied()
4992                    .unwrap_or(0);
4993                if let Some(parent_obj) = parent_arr
4994                    .get_mut(target_idx)
4995                    .and_then(|v| v.as_object_mut())
4996                {
4997                    insert_or_append(parent_obj, &child_key, child_item);
4998                }
4999            }
5000            continue;
5001        }
5002
5003        // Handle parent as single object
5004        if let Some(parent_obj) = parent_value.as_object_mut() {
5005            for (_i, child_item) in &child_items {
5006                insert_or_append(parent_obj, &child_key, child_item);
5007            }
5008            continue;
5009        }
5010
5011        // Fallback: put child back at top level
5012        result.insert(child_key, child_value);
5013    }
5014}
5015
5016/// Parent/child entity pairs the forward mapping nests (see
5017/// [`nest_child_entities_in_result`]): `(parent_group, parent_entity,
5018/// child_entity, child_source_path)`.
5019///
5020/// A child entity (dotted `source_group`, e.g. `SG2.SG3` Kontakt) is moved into
5021/// the object of the entity mapped from its parent group (e.g. `SG2`
5022/// Marktteilnehmer) — unless the parent group is the transaction root, the
5023/// child also has a definition at the parent level (same-entity enrichment), or
5024/// the parent maps a dotted field of the child's name.
5025pub(crate) fn child_entity_nesting_pairs(
5026    definitions: &[MappingDefinition],
5027    transaction_group: Option<&str>,
5028) -> Vec<(String, String, String, Option<String>)> {
5029    // Collect parent→child relationships from definitions.
5030    // parent_group → (parent_entity, child_entity, child_source_path)
5031    let mut nesting_pairs: Vec<(String, String, String, Option<String>)> = Vec::new();
5032    for def in definitions {
5033        let parts: Vec<&str> = def.meta.source_group.split('.').collect();
5034        if parts.len() < 2 || def.meta.parent_field.is_some() {
5035            continue;
5036        }
5037        let parent_group = parts[0];
5038        // Skip nesting when the parent group is the transaction root. SG4 in UTILMD
5039        // IS the transaction — its direct children (Marktlokation, Geschaeftspartner,
5040        // ProduktpaketDaten, …) are peers of the transaction metadata (Prozessdaten),
5041        // not sub-objects of it. Nesting still applies to other parents (e.g. SG2.SG3
5042        // Kontakt stays nested under SG2 Marktteilnehmer).
5043        if transaction_group.is_some_and(|tx| tx == parent_group) {
5044            continue;
5045        }
5046        let child_entity = def.meta.entity.clone();
5047        // Skip if the child entity also has a definition at the parent group level.
5048        // E.g., Prozessdaten at SG4.SG6 enriches Prozessdaten at SG4 via deep_merge —
5049        // this is same-entity enrichment, not a parent-child nesting relationship.
5050        let child_has_parent_level_def = definitions
5051            .iter()
5052            .any(|d| d.meta.source_group == parent_group && d.meta.entity == child_entity);
5053        if child_has_parent_level_def {
5054            continue;
5055        }
5056        // Find the parent definition (a different entity at the parent group level)
5057        let parent_entity = definitions
5058            .iter()
5059            .find(|d| d.meta.source_group == parent_group && d.meta.entity != child_entity)
5060            .map(|d| d.meta.entity.clone());
5061        if let Some(ref parent_entity) = parent_entity {
5062            // Skip nesting if the parent definition has a dotted field target
5063            // that creates a sub-object with the same name as the child entity.
5064            // E.g., Prozessdaten has "zeitscheibe.referenz" which creates
5065            // prozessdaten.zeitscheibe — collides with nesting Zeitscheibe entity.
5066            let child_key_lc = to_camel_case(&child_entity);
5067            let parent_defs: Vec<_> = definitions
5068                .iter()
5069                .filter(|d| d.meta.entity == *parent_entity)
5070                .collect();
5071            let has_conflicting_field = parent_defs.iter().any(|pd| {
5072                pd.fields.values().any(|fm| {
5073                    let target = match fm {
5074                        crate::definition::FieldMapping::Simple(t) => t.as_str(),
5075                        crate::definition::FieldMapping::Structured(s) => s.target.as_str(),
5076                        crate::definition::FieldMapping::Nested(_) => "",
5077                    };
5078                    target.starts_with(&child_key_lc)
5079                        && target.get(child_key_lc.len()..child_key_lc.len() + 1) == Some(".")
5080                })
5081            });
5082            if has_conflicting_field {
5083                continue;
5084            }
5085            // Avoid duplicates
5086            if nesting_pairs
5087                .iter()
5088                .any(|(_, pe, ce, _)| *pe == *parent_entity && *ce == child_entity)
5089            {
5090                continue;
5091            }
5092            nesting_pairs.push((
5093                parent_group.to_string(),
5094                parent_entity.clone(),
5095                child_entity,
5096                def.meta.source_path.clone(),
5097            ));
5098        }
5099    }
5100
5101    nesting_pairs
5102}
5103
5104/// Check if a JSON map looks like a map-keyed entity (short uppercase/code keys → objects).
5105fn is_map_keyed_value(map: &serde_json::Map<String, serde_json::Value>) -> bool {
5106    if map.is_empty() {
5107        return false;
5108    }
5109    map.values().all(|v| v.is_object())
5110        && map.keys().all(|k| {
5111            k.len() <= 5
5112                || k.chars()
5113                    .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
5114        })
5115}
5116
5117/// One code-field position recovered from a mapping definition: where the value
5118/// came from in EDIFACT, and where it landed in BO4E.
5119#[derive(Clone)]
5120struct CodeSite<'a> {
5121    target: &'a str,
5122    /// `[meta] parent_field`: the site is a key of an element of that array on
5123    /// the entity, not a key of the entity. Without this the split enrichment
5124    /// looks for the target directly on the carrier and finds nothing, while
5125    /// the pre-split path enriched it at write time — the two would disagree on
5126    /// every nested rule.
5127    parent_field: Option<&'a str>,
5128    source_path: &'a str,
5129    seg_tag: String,
5130    /// The qualifier on the field key itself (`cav[Z30]...`).
5131    path_qualifier: Option<String>,
5132    /// The qualifier the definition's discriminator pins for this tag.
5133    disc_qualifier: Option<String>,
5134    element_idx: usize,
5135    component_idx: usize,
5136    enum_map: Option<&'a std::collections::BTreeMap<String, String>>,
5137    also_target: Option<&'a str>,
5138    also_enum_map: Option<&'a std::collections::BTreeMap<String, String>>,
5139}
5140
5141pub(crate) fn to_camel_case(name: &str) -> String {
5142    let mut chars = name.chars();
5143    match chars.next() {
5144        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
5145        None => String::new(),
5146    }
5147}
5148
5149/// Set a value in a nested JSON map using a dotted path.
5150/// E.g., "address.city" sets `{"address": {"city": "value"}}`.
5151fn set_nested_value(map: &mut serde_json::Map<String, serde_json::Value>, path: &str, val: String) {
5152    set_nested_value_json(map, path, serde_json::Value::String(val));
5153}
5154
5155/// Like `set_nested_value` but accepts a `serde_json::Value` instead of a `String`.
5156fn set_nested_value_json(
5157    map: &mut serde_json::Map<String, serde_json::Value>,
5158    path: &str,
5159    val: serde_json::Value,
5160) {
5161    if let Some((prefix, leaf)) = path.rsplit_once('.') {
5162        let mut current = map;
5163        for part in prefix.split('.') {
5164            let entry = current
5165                .entry(part.to_string())
5166                .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
5167            current = entry.as_object_mut().expect("expected object in path");
5168        }
5169        current.insert(leaf.to_string(), val);
5170    } else {
5171        map.insert(path.to_string(), val);
5172    }
5173}
5174
5175/// Precompiled cache for a single format-version/variant (e.g., FV2504/UTILMD_Strom).
5176///
5177/// Contains all engines with paths pre-resolved, ready for immediate use.
5178/// Loading one `VariantCache` file replaces thousands of individual `.bin` reads.
5179#[derive(serde::Serialize, serde::Deserialize)]
5180pub struct VariantCache {
5181    /// Message-level definitions (shared across PIDs).
5182    pub message_defs: Vec<MappingDefinition>,
5183    /// Per-PID transaction definitions (key: "pid_55001").
5184    pub transaction_defs: BTreeMap<String, Vec<MappingDefinition>>,
5185    /// Per-PID combined definitions (key: "pid_55001").
5186    pub combined_defs: BTreeMap<String, Vec<MappingDefinition>>,
5187    /// Per-PID code lookups (key: "pid_55001"). Cached to avoid reading schema JSONs at load time.
5188    #[serde(default)]
5189    pub code_lookups: BTreeMap<String, crate::code_lookup::CodeLookup>,
5190    /// Parsed MIG schema — cached to avoid re-parsing MIG XML at startup.
5191    #[serde(default)]
5192    pub mig_schema: Option<mig_types::schema::mig::MigSchema>,
5193    /// Segment element counts derived from MIG — cached for reverse mapping padding.
5194    #[serde(default)]
5195    pub segment_structure: Option<crate::segment_structure::SegmentStructure>,
5196    /// The shared code-list tables the definitions' `code_list` names resolve
5197    /// against. Not part of the cache file: the tables live once beside it, so
5198    /// `load` finds them and every engine this cache builds inherits them.
5199    /// Without that a translated code reaches the output raw.
5200    #[serde(skip)]
5201    pub code_lists: std::sync::Arc<crate::code_lists::CodeLists>,
5202    /// Per-PID AHB segment numbers (key: "pid_55001"). Used for MIG filtering at runtime.
5203    /// Eliminates the need to parse AHB XML files at startup.
5204    #[serde(default)]
5205    pub pid_segment_numbers: BTreeMap<String, Vec<String>>,
5206    /// Per-PID field requirements (key: "pid_55001"). Built from PID schema + TOML definitions.
5207    /// Used by `validate_pid()` to check field completeness.
5208    #[serde(default)]
5209    pub pid_requirements: BTreeMap<String, crate::pid_requirements::PidRequirements>,
5210    /// Per-PID pre-built AHB workflow (key: "pid_55001"). The EDIFACT-side rulebook
5211    /// (segment-path keyed), twin of `pid_requirements` (BO4E-entity keyed). Built at
5212    /// compile-mappings from the PID schema JSON so downstream consumers can run full
5213    /// raw-EDIFACT validation (`Mapper::validate_edifact`) without the schema files.
5214    #[serde(default)]
5215    pub pid_ahb_workflows: BTreeMap<String, ahb_types::AhbWorkflow>,
5216    /// Per-PID transaction group ID (key: "pid_55001", value: "SG4").
5217    /// Derived from the common `source_group` prefix of transaction definitions.
5218    /// Empty string for message-only variants (e.g., ORDCHG).
5219    #[serde(default)]
5220    pub tx_groups: BTreeMap<String, String>,
5221}
5222
5223impl VariantCache {
5224    /// Save this variant cache to a single JSON file.
5225    pub fn save(&self, path: &Path) -> Result<(), MappingError> {
5226        let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
5227            path: path.display().to_string(),
5228            message: e.to_string(),
5229        })?;
5230        if let Some(parent) = path.parent() {
5231            std::fs::create_dir_all(parent)?;
5232        }
5233        std::fs::write(path, encoded)?;
5234        Ok(())
5235    }
5236
5237    /// Load a variant cache from a single JSON file.
5238    pub fn load(path: &Path) -> Result<Self, MappingError> {
5239        let bytes = std::fs::read(path)?;
5240        let mut cache: Self =
5241            serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
5242                path: path.display().to_string(),
5243                message: e.to_string(),
5244            })?;
5245        cache.code_lists = crate::code_lists::CodeLists::discover(path);
5246        Ok(cache)
5247    }
5248
5249    /// Get the transaction group for a PID (e.g., "SG4" for UTILMD PIDs).
5250    /// Returns `None` if the PID is not in this variant.
5251    /// Returns `Some("")` for message-only variants (no transaction group).
5252    pub fn tx_group(&self, pid: &str) -> Option<&str> {
5253        self.tx_groups
5254            .get(&format!("pid_{pid}"))
5255            .map(|s| s.as_str())
5256    }
5257
5258    /// Build a `MappingEngine` from the message-level definitions, attaching
5259    /// the per-PID code lookup so forward mapping enriches code fields with
5260    /// `{ code, meaning, enum }` objects.
5261    pub fn msg_engine(&self, pid: &str) -> MappingEngine {
5262        let mut eng = MappingEngine::from_definitions_with_code_lists(
5263            std::sync::Arc::clone(&self.code_lists),
5264            self.message_defs.clone(),
5265        )
5266        .with_pid(pid);
5267        if let Some(cl) = self.code_lookups.get(&format!("pid_{pid}")) {
5268            eng = eng.with_code_lookup(cl.clone());
5269        }
5270        eng
5271    }
5272
5273    /// Build a `MappingEngine` from the transaction-level definitions for a PID,
5274    /// attaching the per-PID code lookup. Returns `None` if the PID is not in
5275    /// this variant.
5276    pub fn tx_engine(&self, pid: &str) -> Option<MappingEngine> {
5277        self.transaction_defs
5278            .get(&format!("pid_{pid}"))
5279            .map(|defs| {
5280                let mut eng = MappingEngine::from_definitions_with_code_lists(
5281                    std::sync::Arc::clone(&self.code_lists),
5282                    defs.clone(),
5283                )
5284                .with_pid(pid);
5285                if let Some(cl) = self.code_lookups.get(&format!("pid_{pid}")) {
5286                    eng = eng.with_code_lookup(cl.clone());
5287                }
5288                eng
5289            })
5290    }
5291
5292    /// Get a PID-filtered MIG schema.
5293    /// Returns `None` if no MIG schema or no segment numbers for this PID.
5294    ///
5295    /// Falls back to the empty-PID workflow's segment numbers when the AHB
5296    /// has no Pruefidentifikator attribute (e.g., APERAK — one workflow for
5297    /// all BGM doc codes). This lets `from_edifact` work for variants whose
5298    /// AHB doesn't enumerate per-PID segment numbers.
5299    pub fn filtered_mig(&self, pid: &str) -> Option<mig_types::schema::mig::MigSchema> {
5300        let mig = self.mig_schema.as_ref()?;
5301        let numbers = self
5302            .pid_segment_numbers
5303            .get(&format!("pid_{pid}"))
5304            .or_else(|| self.pid_segment_numbers.get("pid_"))?;
5305        let number_set: std::collections::HashSet<String> = numbers.iter().cloned().collect();
5306        Some(mig_assembly::pid_filter::filter_mig_for_pid(
5307            mig,
5308            &number_set,
5309        ))
5310    }
5311
5312    /// The PID's view of the MIG with group variants kept apart, for the
5313    /// structure check (see `mig_assembly::structure_check`).
5314    pub fn pid_mig_unmerged(&self, pid: &str) -> Option<mig_types::schema::mig::MigSchema> {
5315        let mig = self.mig_schema.as_ref()?;
5316        let numbers = self
5317            .pid_segment_numbers
5318            .get(&format!("pid_{pid}"))
5319            .or_else(|| self.pid_segment_numbers.get("pid_"))?;
5320        let number_set: std::collections::HashSet<String> = numbers.iter().cloned().collect();
5321        Some(mig_assembly::pid_filter::filter_mig_for_pid_unmerged(
5322            mig,
5323            &number_set,
5324        ))
5325    }
5326}
5327
5328/// Bundled data for a single format version (e.g., FV2504).
5329///
5330/// Contains all VariantCaches for every message type in that FV,
5331/// serialized as one bincode file for distribution via GitHub releases.
5332#[derive(serde::Serialize, serde::Deserialize)]
5333pub struct DataBundle {
5334    pub format_version: String,
5335    pub bundle_version: u32,
5336    pub variants: BTreeMap<String, VariantCache>,
5337    /// PID-agnostic BO4E type catalog (parsed from `bo4e-german` source).
5338    ///
5339    /// Populated by the bundle generator at compile-mappings time. Older bundles
5340    /// without this field deserialize to an empty catalog.
5341    #[serde(default)]
5342    pub bo4e_catalog: crate::bo4e_catalog::Bo4eCatalog,
5343
5344    /// The crate version that produced this bundle.
5345    ///
5346    /// Distinct from [`bundle_version`](Self::bundle_version), which guards the
5347    /// serialisation *format* and has been unchanged for many releases — a
5348    /// bundle can satisfy it while its mappings, schemas and code lists come
5349    /// from another era. That is not a hypothetical: a bundle five months old
5350    /// passed the format check, loaded cleanly, and rendered 12% of a message
5351    /// with no error (issue #158).
5352    ///
5353    /// `None` for bundles produced before this field existed, which is itself
5354    /// evidence of age.
5355    #[serde(default, skip_serializing_if = "Option::is_none")]
5356    pub built_by: Option<String>,
5357    /// The shared code-list tables the definitions' `code_list` names resolve
5358    /// against.
5359    ///
5360    /// Carried IN the bundle, unlike `VariantCache`, which is written beside a
5361    /// copy of `code_lists.toml` and repairs itself from it on load. A bundle
5362    /// is a bare `.bin` fetched into `~/.edifact/data` with nothing beside it,
5363    /// so a bundle that does not carry its tables cannot resolve a single
5364    /// name: forward, the EDIFACT code reaches the output untranslated;
5365    /// reverse, the BO4E name is written into the EDIFACT slot verbatim. The
5366    /// deduplication that made naming worth doing does not argue against this
5367    /// -- there is one bundle per format version, so the tables appear once.
5368    #[serde(default)]
5369    pub code_lists: crate::code_lists::CodeLists,
5370}
5371
5372impl DataBundle {
5373    pub const CURRENT_VERSION: u32 = 2;
5374
5375    /// The release a bundle built now belongs to, and the one a bundle must
5376    /// have been built by to be read.
5377    ///
5378    /// Taken from `mig-bo4e` rather than from whichever crate produces or
5379    /// consumes a bundle: `mig-bo4e` carries the workspace version, which is
5380    /// what the release process stamps, while `automapper-generator` versions
5381    /// itself separately. Reading it from the producer gave `0.1.0` against a
5382    /// consumer expecting `0.1.1` — a mismatch that is an artefact of where the
5383    /// constant was read, not of the data.
5384    pub const PRODUCING_VERSION: &'static str = env!("CARGO_PKG_VERSION");
5385
5386    pub fn variant(&self, name: &str) -> Option<&VariantCache> {
5387        self.variants.get(name)
5388    }
5389
5390    pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MappingError> {
5391        let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
5392            path: "<stream>".to_string(),
5393            message: e.to_string(),
5394        })?;
5395        writer.write_all(&encoded).map_err(MappingError::Io)
5396    }
5397
5398    pub fn read_from<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
5399        let mut bytes = Vec::new();
5400        reader.read_to_end(&mut bytes).map_err(MappingError::Io)?;
5401        serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
5402            path: "<stream>".to_string(),
5403            message: e.to_string(),
5404        })
5405    }
5406
5407    pub fn read_from_checked<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
5408        let mut bundle = Self::read_from(reader)?;
5409        // Every engine this bundle builds resolves names through its variant's
5410        // `Arc`, which `#[serde(skip)]` left empty on the way in.
5411        let shared = std::sync::Arc::new(std::mem::take(&mut bundle.code_lists));
5412        for variant in bundle.variants.values_mut() {
5413            variant.code_lists = std::sync::Arc::clone(&shared);
5414        }
5415        bundle.code_lists = (*shared).clone();
5416        if bundle.bundle_version != Self::CURRENT_VERSION {
5417            return Err(MappingError::CacheRead {
5418                path: "<stream>".to_string(),
5419                message: format!(
5420                    "Incompatible bundle version {}, expected version {}. \
5421                     Run `edifact-data update` to fetch compatible bundles.",
5422                    bundle.bundle_version,
5423                    Self::CURRENT_VERSION
5424                ),
5425            });
5426        }
5427        Ok(bundle)
5428    }
5429
5430    pub fn save(&self, path: &Path) -> Result<(), MappingError> {
5431        if let Some(parent) = path.parent() {
5432            std::fs::create_dir_all(parent)?;
5433        }
5434        let mut file = std::fs::File::create(path).map_err(MappingError::Io)?;
5435        self.write_to(&mut file)
5436    }
5437
5438    pub fn load(path: &Path) -> Result<Self, MappingError> {
5439        let mut file = std::fs::File::open(path).map_err(MappingError::Io)?;
5440        Self::read_from_checked(&mut file)
5441    }
5442}
5443
5444#[cfg(test)]
5445mod variant_cache_helper_tests {
5446    use super::*;
5447
5448    fn make_test_cache() -> VariantCache {
5449        let mut tx_groups = BTreeMap::new();
5450        tx_groups.insert("pid_55001".to_string(), "SG4".to_string());
5451        tx_groups.insert("pid_21007".to_string(), "SG14".to_string());
5452
5453        let mut transaction_defs = BTreeMap::new();
5454        transaction_defs.insert("pid_55001".to_string(), vec![]);
5455        transaction_defs.insert("pid_21007".to_string(), vec![]);
5456
5457        VariantCache {
5458            code_lists: Default::default(),
5459            message_defs: vec![],
5460            transaction_defs,
5461            combined_defs: BTreeMap::new(),
5462            code_lookups: BTreeMap::new(),
5463            mig_schema: None,
5464            segment_structure: None,
5465            pid_segment_numbers: BTreeMap::new(),
5466            pid_requirements: BTreeMap::new(),
5467            pid_ahb_workflows: BTreeMap::new(),
5468            tx_groups,
5469        }
5470    }
5471
5472    #[test]
5473    fn test_tx_group_returns_correct_group() {
5474        let vc = make_test_cache();
5475        assert_eq!(vc.tx_group("55001").unwrap(), "SG4");
5476        assert_eq!(vc.tx_group("21007").unwrap(), "SG14");
5477    }
5478
5479    #[test]
5480    fn test_tx_group_unknown_pid_returns_none() {
5481        let vc = make_test_cache();
5482        assert!(vc.tx_group("99999").is_none());
5483    }
5484
5485    #[test]
5486    fn test_msg_engine_returns_engine() {
5487        let vc = make_test_cache();
5488        let engine = vc.msg_engine("55001");
5489        assert_eq!(engine.definitions().len(), 0);
5490    }
5491
5492    #[test]
5493    fn test_tx_engine_returns_engine_for_known_pid() {
5494        let vc = make_test_cache();
5495        assert!(vc.tx_engine("55001").is_some());
5496    }
5497
5498    #[test]
5499    fn test_tx_engine_returns_none_for_unknown_pid() {
5500        let vc = make_test_cache();
5501        assert!(vc.tx_engine("99999").is_none());
5502    }
5503
5504    /// Build a cache whose every map holds many keys. Each call creates fresh
5505    /// `HashMap`s (fresh random hash seeds), so an order-dependent serializer
5506    /// produces different bytes on different calls.
5507    fn make_populated_cache() -> VariantCache {
5508        let pids: Vec<String> = (0..40).map(|i| format!("pid_{}", 55000 + i * 7)).collect();
5509        let schema: serde_json::Value = serde_json::from_str(include_str!(
5510            "../../mig-types/src/generated/fv2504/utilmd/pids/pid_55001_schema.json"
5511        ))
5512        .unwrap();
5513        let code_lookup = crate::code_lookup::CodeLookup::from_schema_value(&schema);
5514        let element_counts: serde_json::Map<String, serde_json::Value> = (0..40)
5515            .map(|i| (format!("T{i:02}"), serde_json::json!(i)))
5516            .collect();
5517        let segment_structure: SegmentStructure =
5518            serde_json::from_value(serde_json::json!({ "element_counts": element_counts }))
5519                .unwrap();
5520        let ubs: serde_json::Map<String, serde_json::Value> = (0..40)
5521            .map(|i| (format!("UB{i}"), serde_json::json!({ "Ref": i })))
5522            .collect();
5523        let workflow: ahb_types::AhbWorkflow = serde_json::from_value(serde_json::json!({
5524            "pruefidentifikator": "55001",
5525            "description": "",
5526            "communication_direction": null,
5527            "fields": [],
5528            "ub_definitions": ubs,
5529        }))
5530        .unwrap();
5531
5532        let mut vc = make_test_cache();
5533        vc.segment_structure = Some(segment_structure);
5534        for pid in &pids {
5535            vc.transaction_defs.insert(pid.clone(), vec![]);
5536            vc.combined_defs.insert(pid.clone(), vec![]);
5537            vc.code_lookups.insert(pid.clone(), code_lookup.clone());
5538            vc.pid_segment_numbers
5539                .insert(pid.clone(), vec!["00001".to_string()]);
5540            vc.pid_ahb_workflows.insert(pid.clone(), workflow.clone());
5541            vc.tx_groups.insert(pid.clone(), "SG4".to_string());
5542        }
5543        vc
5544    }
5545
5546    /// Enrichment of qualified field paths uses the codes of the segment variant
5547    /// the field reads, never those of a sibling variant of the same tag.
5548    #[test]
5549    fn test_enrichment_uses_codes_of_the_path_qualifier_variant() {
5550        let comp = |sub: u64, id: &str, codes: Option<serde_json::Value>| match codes {
5551            Some(c) => serde_json::json!({"sub_index": sub, "id": id, "type": "code", "codes": c}),
5552            None => serde_json::json!({"sub_index": sub, "id": id, "type": "data"}),
5553        };
5554        let code = |v: &str, n: &str| serde_json::json!([{"value": v, "name": n}]);
5555        let seg = |tag: &str, composite: &str, comps: Vec<serde_json::Value>| serde_json::json!({"id": tag, "elements": [{"index": 0, "composite": composite, "components": comps}]});
5556        let schema = serde_json::json!({"fields": {"sg15": {"segments": [
5557            seg("RFF", "C506", vec![comp(0, "1153", Some(code("Z13", "PID"))), comp(1, "1154", Some(code("21037", "RD / NB-Bewertung")))]),
5558            seg("RFF", "C506", vec![comp(0, "1153", Some(code("ACW", "Referenz"))), comp(1, "1154", None)]),
5559            seg("CAV", "C889", vec![comp(0, "7111", Some(code("Z91", "Z91"))), comp(1, "7110", Some(code("A", "Alpha")))]),
5560            seg("CAV", "C889", vec![comp(0, "7111", Some(code("ZF0", "ZF0"))), comp(1, "7110", Some(code("C", "Gamma")))]),
5561        ]}}});
5562        let engine = MappingEngine::new_empty()
5563            .with_code_lookup(crate::code_lookup::CodeLookup::from_schema_value(&schema));
5564        let def = MappingDefinition::from_toml_str(
5565            r#"
5566[meta]
5567entity = "Status"
5568bo4e_type = "Status"
5569source_group = "SG15"
5570source_path = "sg15"
5571discriminator = "RFF.0.0=Z13"
5572
5573[fields]
5574"rff.0.1" = "pruefidentifikator"
5575"rff[ACW].0.1" = "referenz"
5576"cav[Z91].0.1" = "z91Wert"
5577"cav[ZF0].0.1" = "zf0Wert"
5578"#,
5579        )
5580        .unwrap();
5581        let segment = |tag: &str, elements: &[&[&str]]| OwnedSegment {
5582            id: tag.to_string(),
5583            elements: elements
5584                .iter()
5585                .map(|e| e.iter().map(|c| c.to_string()).collect())
5586                .collect(),
5587            segment_number: 1,
5588        };
5589        let json = engine.map_forward_from_segments(
5590            &[
5591                segment("RFF", &[&["Z13", "21037"]]),
5592                segment("RFF", &[&["ACW", "REF-1"]]),
5593                segment("CAV", &[&["Z91", "C"]]),
5594                segment("CAV", &[&["ZF0", "C"]]),
5595            ],
5596            &def,
5597        );
5598        assert_eq!(
5599            json["referenz"],
5600            serde_json::json!("REF-1"),
5601            "RFF+ACW d1154 is data; RFF+Z13's codes must not apply: {json}"
5602        );
5603        assert_eq!(json["pruefidentifikator"]["meaning"], "RD / NB-Bewertung");
5604        assert_eq!(
5605            json["z91Wert"]["meaning"],
5606            serde_json::Value::Null,
5607            "'C' is a CAV+ZF0 code, unknown to CAV+Z91: {json}"
5608        );
5609        assert_eq!(json["zf0Wert"]["meaning"], "Gamma");
5610    }
5611
5612    /// A list target keeps every CAV with its own code, in wire order, and
5613    /// writes them back the same way — an absent first CAV no longer shifts
5614    /// the others, and a third CAV has somewhere to go.
5615    #[test]
5616    fn list_target_reads_and_writes_every_repetition_in_order() {
5617        let engine = MappingEngine::new_empty();
5618        let def = MappingDefinition::from_toml_str(
5619            r#"
5620[meta]
5621entity = "Zuordnung"
5622bo4e_type = "Zuordnung"
5623source_group = "SG10"
5624source_path = "sg10"
5625
5626[fields]
5627"cci.2.0" = "merkmal.code"
5628"cav[*,*].0.0" = "werte[].code"
5629"cav[*,*].0.3" = "werte[].text"
5630"cav[Z30,*].0.3" = "geraetenummern[].nummer"
5631"#,
5632        )
5633        .unwrap();
5634        let segment = |tag: &str, elements: &[&[&str]]| OwnedSegment {
5635            id: tag.to_string(),
5636            elements: elements
5637                .iter()
5638                .map(|e| e.iter().map(|c| c.to_string()).collect())
5639                .collect(),
5640            segment_number: 1,
5641        };
5642        let json = engine.map_forward_from_segments(
5643            &[
5644                segment("CCI", &[&[""], &[""], &["ZB3"]]),
5645                segment("CAV", &[&["Z90", "", "", "UENB"]]),
5646                segment("CAV", &[&["Z91", "", "", "MSB"]]),
5647                segment("CAV", &[&["Z30", "", "", "W1"]]),
5648                segment("CAV", &[&["Z30", "", "", "W2"]]),
5649            ],
5650            &def,
5651        );
5652        assert_eq!(
5653            json["werte"],
5654            serde_json::json!([
5655                {"code": "Z90", "text": "UENB"},
5656                {"code": "Z91", "text": "MSB"},
5657                {"code": "Z30", "text": "W1"},
5658                {"code": "Z30", "text": "W2"},
5659            ]),
5660            "{json}"
5661        );
5662        assert_eq!(
5663            json["geraetenummern"],
5664            serde_json::json!([{"nummer": "W1"}, {"nummer": "W2"}])
5665        );
5666
5667        // Back: one CAV per element, in the list's order.
5668        let only_werte = serde_json::json!({
5669            "merkmal": {"code": "ZB3"},
5670            "werte": [{"text": "UENB", "code": "Z90"}, {"code": "Z91", "text": "MSB"}],
5671        });
5672        let instance = engine.map_reverse(&only_werte, &def);
5673        let cavs: Vec<Vec<String>> = instance
5674            .segments
5675            .iter()
5676            .filter(|s| s.tag == "CAV")
5677            .map(|s| s.elements[0].clone())
5678            .collect();
5679        assert_eq!(
5680            cavs,
5681            vec![
5682                vec![
5683                    "Z90".to_string(),
5684                    String::new(),
5685                    String::new(),
5686                    "UENB".to_string()
5687                ],
5688                vec![
5689                    "Z91".to_string(),
5690                    String::new(),
5691                    String::new(),
5692                    "MSB".to_string()
5693                ],
5694            ]
5695        );
5696    }
5697
5698    #[test]
5699    fn test_variant_cache_serialization_is_deterministic() {
5700        let reference = serde_json::to_vec(&make_populated_cache()).unwrap();
5701        for _ in 0..5 {
5702            let again = serde_json::to_vec(&make_populated_cache()).unwrap();
5703            assert!(
5704                reference == again,
5705                "VariantCache serialization must not depend on HashMap iteration order"
5706            );
5707        }
5708    }
5709
5710    #[test]
5711    fn test_variant_cache_serializes_map_keys_sorted() {
5712        use indexmap::IndexMap;
5713        use serde::de::IgnoredAny;
5714
5715        #[derive(serde::Deserialize)]
5716        struct ProbeWorkflow {
5717            ub_definitions: IndexMap<String, IgnoredAny>,
5718        }
5719        #[derive(serde::Deserialize)]
5720        struct ProbeStructure {
5721            element_counts: IndexMap<String, usize>,
5722        }
5723        #[derive(serde::Deserialize)]
5724        struct Probe {
5725            transaction_defs: IndexMap<String, IgnoredAny>,
5726            combined_defs: IndexMap<String, IgnoredAny>,
5727            code_lookups: IndexMap<String, IndexMap<String, IgnoredAny>>,
5728            segment_structure: ProbeStructure,
5729            pid_segment_numbers: IndexMap<String, IgnoredAny>,
5730            pid_requirements: IndexMap<String, IgnoredAny>,
5731            pid_ahb_workflows: IndexMap<String, ProbeWorkflow>,
5732            tx_groups: IndexMap<String, String>,
5733        }
5734        fn assert_sorted<'a>(what: &str, keys: impl Iterator<Item = &'a String>) {
5735            let keys: Vec<&String> = keys.collect();
5736            let mut sorted = keys.clone();
5737            sorted.sort();
5738            assert_eq!(keys, sorted, "{what} keys must serialize in sorted order");
5739        }
5740
5741        let json = serde_json::to_string(&make_populated_cache()).unwrap();
5742        let probe: Probe = serde_json::from_str(&json).unwrap();
5743        assert_sorted("transaction_defs", probe.transaction_defs.keys());
5744        assert_sorted("combined_defs", probe.combined_defs.keys());
5745        assert_sorted("code_lookups", probe.code_lookups.keys());
5746        let lookup = probe.code_lookups.values().next().unwrap();
5747        assert!(lookup.len() > 10, "fixture lookup should have many entries");
5748        assert_sorted("code_lookup entries", lookup.keys());
5749        assert_sorted(
5750            "segment_structure",
5751            probe.segment_structure.element_counts.keys(),
5752        );
5753        assert_sorted("pid_segment_numbers", probe.pid_segment_numbers.keys());
5754        assert_sorted("pid_requirements", probe.pid_requirements.keys());
5755        assert_sorted("pid_ahb_workflows", probe.pid_ahb_workflows.keys());
5756        let wf = probe.pid_ahb_workflows.values().next().unwrap();
5757        assert_sorted("ub_definitions", wf.ub_definitions.keys());
5758        assert_sorted("tx_groups", probe.tx_groups.keys());
5759    }
5760
5761    #[test]
5762    fn test_data_bundle_serializes_variants_sorted() {
5763        use indexmap::IndexMap;
5764        use serde::de::IgnoredAny;
5765
5766        #[derive(serde::Deserialize)]
5767        struct Probe {
5768            variants: IndexMap<String, IgnoredAny>,
5769        }
5770        let variants: BTreeMap<String, VariantCache> = (0..20)
5771            .map(|i| (format!("VARIANT_{i:02}"), make_test_cache()))
5772            .collect();
5773        let bundle = DataBundle {
5774            format_version: "FV2504".to_string(),
5775            bundle_version: DataBundle::CURRENT_VERSION,
5776            built_by: Some(DataBundle::PRODUCING_VERSION.to_string()),
5777            variants,
5778            bo4e_catalog: Default::default(),
5779            code_lists: Default::default(),
5780        };
5781        let mut bytes = Vec::new();
5782        bundle.write_to(&mut bytes).unwrap();
5783        let probe: Probe = serde_json::from_slice(&bytes).unwrap();
5784        let keys: Vec<&String> = probe.variants.keys().collect();
5785        let mut sorted = keys.clone();
5786        sorted.sort();
5787        assert_eq!(keys, sorted);
5788    }
5789}
5790
5791#[cfg(test)]
5792mod tests {
5793    use super::*;
5794    use crate::definition::{MappingDefinition, MappingMeta, StructuredFieldMapping};
5795    use indexmap::IndexMap;
5796
5797    fn make_def(fields: IndexMap<String, FieldMapping>) -> MappingDefinition {
5798        MappingDefinition {
5799            meta: MappingMeta {
5800                entity: "Test".to_string(),
5801                bo4e_type: "Test".to_string(),
5802                source_group: "SG4".to_string(),
5803                source_path: None,
5804                discriminator: None,
5805                repeat_on_tag: None,
5806                parent_field: None,
5807                target_list: None,
5808                order: None,
5809            },
5810            fields,
5811            complex_handlers: None,
5812        }
5813    }
5814
5815    #[test]
5816    fn test_map_interchange_single_transaction_backward_compat() {
5817        use mig_assembly::assembler::*;
5818
5819        // Single SG4 with SG5 — the common case for current PID 55001 fixtures
5820        let tree = AssembledTree {
5821            segments: vec![
5822                AssembledSegment {
5823                    tag: "UNH".to_string(),
5824                    elements: vec![vec!["001".to_string()]],
5825                    mig_number: None,
5826                    segment_number: None,
5827                },
5828                AssembledSegment {
5829                    tag: "BGM".to_string(),
5830                    elements: vec![vec!["E01".to_string()], vec!["DOC001".to_string()]],
5831                    mig_number: None,
5832                    segment_number: None,
5833                },
5834            ],
5835            groups: vec![
5836                AssembledGroup {
5837                    group_id: "SG2".to_string(),
5838                    repetitions: vec![AssembledGroupInstance {
5839                        segments: vec![AssembledSegment {
5840                            tag: "NAD".to_string(),
5841                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
5842                            mig_number: None,
5843                            segment_number: None,
5844                        }],
5845                        child_groups: vec![],
5846                        entry_mig_number: None,
5847                        variant_mig_numbers: vec![],
5848                        skipped_segments: vec![],
5849                        skipped_positions: Vec::new(),
5850                    }],
5851                },
5852                AssembledGroup {
5853                    group_id: "SG4".to_string(),
5854                    repetitions: vec![AssembledGroupInstance {
5855                        segments: vec![AssembledSegment {
5856                            tag: "IDE".to_string(),
5857                            elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
5858                            mig_number: None,
5859                            segment_number: None,
5860                        }],
5861                        child_groups: vec![AssembledGroup {
5862                            group_id: "SG5".to_string(),
5863                            repetitions: vec![AssembledGroupInstance {
5864                                segments: vec![AssembledSegment {
5865                                    tag: "LOC".to_string(),
5866                                    elements: vec![
5867                                        vec!["Z16".to_string()],
5868                                        vec!["DE000111222333".to_string()],
5869                                    ],
5870                                    mig_number: None,
5871                                    segment_number: None,
5872                                }],
5873                                child_groups: vec![],
5874                                entry_mig_number: None,
5875                                variant_mig_numbers: vec![],
5876                                skipped_segments: vec![],
5877                                skipped_positions: Vec::new(),
5878                            }],
5879                        }],
5880                        entry_mig_number: None,
5881                        variant_mig_numbers: vec![],
5882                        skipped_segments: vec![],
5883                        skipped_positions: Vec::new(),
5884                    }],
5885                },
5886            ],
5887            post_group_start: 2,
5888            inter_group_segments: std::collections::BTreeMap::new(),
5889        };
5890
5891        // Empty message engine (no message-level defs for this test)
5892        let msg_engine = MappingEngine::from_definitions(vec![]);
5893
5894        // Transaction defs
5895        let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
5896        tx_fields.insert(
5897            "ide.1".to_string(),
5898            FieldMapping::Simple("vorgangId".to_string()),
5899        );
5900        let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
5901        malo_fields.insert(
5902            "loc.1".to_string(),
5903            FieldMapping::Simple("marktlokationsId".to_string()),
5904        );
5905
5906        let tx_engine = MappingEngine::from_definitions(vec![
5907            MappingDefinition {
5908                meta: MappingMeta {
5909                    entity: "Prozessdaten".to_string(),
5910                    bo4e_type: "Prozessdaten".to_string(),
5911                    source_group: "SG4".to_string(),
5912                    source_path: None,
5913                    discriminator: None,
5914                    repeat_on_tag: None,
5915                    parent_field: None,
5916                    target_list: None,
5917                    order: None,
5918                },
5919                fields: tx_fields,
5920                complex_handlers: None,
5921            },
5922            MappingDefinition {
5923                meta: MappingMeta {
5924                    entity: "Marktlokation".to_string(),
5925                    bo4e_type: "Marktlokation".to_string(),
5926                    source_group: "SG4.SG5".to_string(),
5927                    source_path: None,
5928                    discriminator: None,
5929                    repeat_on_tag: None,
5930                    parent_field: None,
5931                    target_list: None,
5932                    order: None,
5933                },
5934                fields: malo_fields,
5935                complex_handlers: None,
5936            },
5937        ]);
5938
5939        let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
5940
5941        assert_eq!(result.transaktionen.len(), 1);
5942        assert_eq!(
5943            result.transaktionen[0].transaktionsdaten["vorgangId"]
5944                .as_str()
5945                .unwrap(),
5946            "TX001"
5947        );
5948        // Marktlokation (SG4.SG5) stays top-level — SG4 IS the transaction root,
5949        // so Marktlokation is a peer of Prozessdaten, not a child of it.
5950        assert_eq!(
5951            result.transaktionen[0].stammdaten["marktlokation"]["marktlokationsId"]
5952                .as_str()
5953                .unwrap(),
5954            "DE000111222333"
5955        );
5956    }
5957
5958    #[test]
5959    fn test_map_reverse_pads_intermediate_empty_elements() {
5960        // NAD+Z09+++Muster:Max — positions 0 and 3 populated, 1 and 2 should become [""]
5961        let mut fields = IndexMap::new();
5962        fields.insert(
5963            "nad.0".to_string(),
5964            FieldMapping::Structured(StructuredFieldMapping {
5965                target: String::new(),
5966                transform: None,
5967                when: None,
5968                default: Some("Z09".to_string()),
5969                enum_map: None,
5970                code_list: None,
5971                also_code_list: None,
5972                when_filled: None,
5973                also_target: None,
5974                also_enum_map: None,
5975            }),
5976        );
5977        fields.insert(
5978            "nad.3.0".to_string(),
5979            FieldMapping::Simple("name".to_string()),
5980        );
5981        fields.insert(
5982            "nad.3.1".to_string(),
5983            FieldMapping::Simple("vorname".to_string()),
5984        );
5985
5986        let def = make_def(fields);
5987        let engine = MappingEngine::from_definitions(vec![]);
5988
5989        let bo4e = serde_json::json!({
5990            "name": "Muster",
5991            "vorname": "Max"
5992        });
5993
5994        let instance = engine.map_reverse(&bo4e, &def);
5995        assert_eq!(instance.segments.len(), 1);
5996
5997        let nad = &instance.segments[0];
5998        assert_eq!(nad.tag, "NAD");
5999        assert_eq!(nad.elements.len(), 4);
6000        assert_eq!(nad.elements[0], vec!["Z09"]);
6001        // Intermediate positions 1 and 2 should be padded to [""]
6002        assert_eq!(nad.elements[1], vec![""]);
6003        assert_eq!(nad.elements[2], vec![""]);
6004        assert_eq!(nad.elements[3][0], "Muster");
6005        assert_eq!(nad.elements[3][1], "Max");
6006    }
6007
6008    #[test]
6009    fn test_map_reverse_no_padding_when_contiguous() {
6010        // DTM+92:20250531:303 — all three components in element 0, no gaps
6011        let mut fields = IndexMap::new();
6012        fields.insert(
6013            "dtm.0.0".to_string(),
6014            FieldMapping::Structured(StructuredFieldMapping {
6015                target: String::new(),
6016                transform: None,
6017                when: None,
6018                default: Some("92".to_string()),
6019                enum_map: None,
6020                code_list: None,
6021                also_code_list: None,
6022                when_filled: None,
6023                also_target: None,
6024                also_enum_map: None,
6025            }),
6026        );
6027        fields.insert(
6028            "dtm.0.1".to_string(),
6029            FieldMapping::Simple("value".to_string()),
6030        );
6031        fields.insert(
6032            "dtm.0.2".to_string(),
6033            FieldMapping::Structured(StructuredFieldMapping {
6034                target: String::new(),
6035                transform: None,
6036                when: None,
6037                default: Some("303".to_string()),
6038                enum_map: None,
6039                code_list: None,
6040                also_code_list: None,
6041                when_filled: None,
6042                also_target: None,
6043                also_enum_map: None,
6044            }),
6045        );
6046
6047        let def = make_def(fields);
6048        let engine = MappingEngine::from_definitions(vec![]);
6049
6050        let bo4e = serde_json::json!({ "value": "20250531" });
6051
6052        let instance = engine.map_reverse(&bo4e, &def);
6053        let dtm = &instance.segments[0];
6054        // Single element with 3 components — no intermediate padding needed
6055        assert_eq!(dtm.elements.len(), 1);
6056        assert_eq!(dtm.elements[0], vec!["92", "20250531", "303"]);
6057    }
6058
6059    #[test]
6060    fn test_map_message_level_extracts_sg2_only() {
6061        use mig_assembly::assembler::*;
6062
6063        // Build a tree with SG2 (message-level) and SG4 (transaction-level)
6064        let tree = AssembledTree {
6065            segments: vec![
6066                AssembledSegment {
6067                    tag: "UNH".to_string(),
6068                    elements: vec![vec!["001".to_string()]],
6069                    mig_number: None,
6070                    segment_number: None,
6071                },
6072                AssembledSegment {
6073                    tag: "BGM".to_string(),
6074                    elements: vec![vec!["E01".to_string()]],
6075                    mig_number: None,
6076                    segment_number: None,
6077                },
6078            ],
6079            groups: vec![
6080                AssembledGroup {
6081                    group_id: "SG2".to_string(),
6082                    repetitions: vec![AssembledGroupInstance {
6083                        segments: vec![AssembledSegment {
6084                            tag: "NAD".to_string(),
6085                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
6086                            mig_number: None,
6087                            segment_number: None,
6088                        }],
6089                        child_groups: vec![],
6090                        entry_mig_number: None,
6091                        variant_mig_numbers: vec![],
6092                        skipped_segments: vec![],
6093                        skipped_positions: Vec::new(),
6094                    }],
6095                },
6096                AssembledGroup {
6097                    group_id: "SG4".to_string(),
6098                    repetitions: vec![AssembledGroupInstance {
6099                        segments: vec![AssembledSegment {
6100                            tag: "IDE".to_string(),
6101                            elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
6102                            mig_number: None,
6103                            segment_number: None,
6104                        }],
6105                        child_groups: vec![],
6106                        entry_mig_number: None,
6107                        variant_mig_numbers: vec![],
6108                        skipped_segments: vec![],
6109                        skipped_positions: Vec::new(),
6110                    }],
6111                },
6112            ],
6113            post_group_start: 2,
6114            inter_group_segments: std::collections::BTreeMap::new(),
6115        };
6116
6117        // Message-level definition maps SG2
6118        let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
6119        msg_fields.insert(
6120            "nad.0".to_string(),
6121            FieldMapping::Simple("marktrolle".to_string()),
6122        );
6123        msg_fields.insert(
6124            "nad.1".to_string(),
6125            FieldMapping::Simple("rollencodenummer".to_string()),
6126        );
6127        let msg_def = MappingDefinition {
6128            meta: MappingMeta {
6129                entity: "Marktteilnehmer".to_string(),
6130                bo4e_type: "Marktteilnehmer".to_string(),
6131                source_group: "SG2".to_string(),
6132                source_path: None,
6133                discriminator: None,
6134                repeat_on_tag: None,
6135                parent_field: None,
6136                target_list: None,
6137                order: None,
6138            },
6139            fields: msg_fields,
6140            complex_handlers: None,
6141        };
6142
6143        let engine = MappingEngine::from_definitions(vec![msg_def.clone()]);
6144        let result = engine.map_all_forward(&tree);
6145
6146        // Should contain Marktteilnehmer from SG2
6147        assert!(result.get("marktteilnehmer").is_some());
6148        let mt = &result["marktteilnehmer"];
6149        assert_eq!(mt["marktrolle"].as_str().unwrap(), "MS");
6150        assert_eq!(mt["rollencodenummer"].as_str().unwrap(), "9900123");
6151    }
6152
6153    #[test]
6154    fn test_map_transaction_scoped_to_sg4_instance() {
6155        use mig_assembly::assembler::*;
6156
6157        // Build a tree with SG4 containing SG5 (LOC+Z16)
6158        let tree = AssembledTree {
6159            segments: vec![
6160                AssembledSegment {
6161                    tag: "UNH".to_string(),
6162                    elements: vec![vec!["001".to_string()]],
6163                    mig_number: None,
6164                    segment_number: None,
6165                },
6166                AssembledSegment {
6167                    tag: "BGM".to_string(),
6168                    elements: vec![vec!["E01".to_string()]],
6169                    mig_number: None,
6170                    segment_number: None,
6171                },
6172            ],
6173            groups: vec![AssembledGroup {
6174                group_id: "SG4".to_string(),
6175                repetitions: vec![AssembledGroupInstance {
6176                    segments: vec![AssembledSegment {
6177                        tag: "IDE".to_string(),
6178                        elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
6179                        mig_number: None,
6180                        segment_number: None,
6181                    }],
6182                    child_groups: vec![AssembledGroup {
6183                        group_id: "SG5".to_string(),
6184                        repetitions: vec![AssembledGroupInstance {
6185                            segments: vec![AssembledSegment {
6186                                tag: "LOC".to_string(),
6187                                elements: vec![
6188                                    vec!["Z16".to_string()],
6189                                    vec!["DE000111222333".to_string()],
6190                                ],
6191                                mig_number: None,
6192                                segment_number: None,
6193                            }],
6194                            child_groups: vec![],
6195                            entry_mig_number: None,
6196                            variant_mig_numbers: vec![],
6197                            skipped_segments: vec![],
6198                            skipped_positions: Vec::new(),
6199                        }],
6200                    }],
6201                    entry_mig_number: None,
6202                    variant_mig_numbers: vec![],
6203                    skipped_segments: vec![],
6204                    skipped_positions: Vec::new(),
6205                }],
6206            }],
6207            post_group_start: 2,
6208            inter_group_segments: std::collections::BTreeMap::new(),
6209        };
6210
6211        // Transaction-level definitions: prozessdaten (root of SG4) + marktlokation (SG5)
6212        let mut proz_fields: IndexMap<String, FieldMapping> = IndexMap::new();
6213        proz_fields.insert(
6214            "ide.1".to_string(),
6215            FieldMapping::Simple("vorgangId".to_string()),
6216        );
6217        let proz_def = MappingDefinition {
6218            meta: MappingMeta {
6219                entity: "Prozessdaten".to_string(),
6220                bo4e_type: "Prozessdaten".to_string(),
6221                source_group: "".to_string(), // Root-level within transaction sub-tree
6222                source_path: None,
6223                discriminator: None,
6224                repeat_on_tag: None,
6225                parent_field: None,
6226                target_list: None,
6227                order: None,
6228            },
6229            fields: proz_fields,
6230            complex_handlers: None,
6231        };
6232
6233        let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
6234        malo_fields.insert(
6235            "loc.1".to_string(),
6236            FieldMapping::Simple("marktlokationsId".to_string()),
6237        );
6238        let malo_def = MappingDefinition {
6239            meta: MappingMeta {
6240                entity: "Marktlokation".to_string(),
6241                bo4e_type: "Marktlokation".to_string(),
6242                source_group: "SG5".to_string(), // Relative to SG4, not "SG4.SG5"
6243                source_path: None,
6244                discriminator: None,
6245                repeat_on_tag: None,
6246                parent_field: None,
6247                target_list: None,
6248                order: None,
6249            },
6250            fields: malo_fields,
6251            complex_handlers: None,
6252        };
6253
6254        let tx_engine = MappingEngine::from_definitions(vec![proz_def, malo_def]);
6255
6256        // Scope to the SG4 instance and map
6257        let sg4 = &tree.groups[0]; // SG4 group
6258        let sg4_instance = &sg4.repetitions[0];
6259        let sub_tree = sg4_instance.as_assembled_tree();
6260
6261        let result = tx_engine.map_all_forward(&sub_tree);
6262
6263        // Should contain Prozessdaten from SG4 root segments
6264        assert_eq!(
6265            result["prozessdaten"]["vorgangId"].as_str().unwrap(),
6266            "TX001"
6267        );
6268
6269        // Should contain Marktlokation from SG5 within SG4
6270        assert_eq!(
6271            result["marktlokation"]["marktlokationsId"]
6272                .as_str()
6273                .unwrap(),
6274            "DE000111222333"
6275        );
6276    }
6277
6278    #[test]
6279    fn test_map_interchange_produces_full_hierarchy() {
6280        use mig_assembly::assembler::*;
6281
6282        // Build a tree with SG2 (message-level) and SG4 with two repetitions (two transactions)
6283        let tree = AssembledTree {
6284            segments: vec![
6285                AssembledSegment {
6286                    tag: "UNH".to_string(),
6287                    elements: vec![vec!["001".to_string()]],
6288                    mig_number: None,
6289                    segment_number: None,
6290                },
6291                AssembledSegment {
6292                    tag: "BGM".to_string(),
6293                    elements: vec![vec!["E01".to_string()]],
6294                    mig_number: None,
6295                    segment_number: None,
6296                },
6297            ],
6298            groups: vec![
6299                AssembledGroup {
6300                    group_id: "SG2".to_string(),
6301                    repetitions: vec![AssembledGroupInstance {
6302                        segments: vec![AssembledSegment {
6303                            tag: "NAD".to_string(),
6304                            elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
6305                            mig_number: None,
6306                            segment_number: None,
6307                        }],
6308                        child_groups: vec![],
6309                        entry_mig_number: None,
6310                        variant_mig_numbers: vec![],
6311                        skipped_segments: vec![],
6312                        skipped_positions: Vec::new(),
6313                    }],
6314                },
6315                AssembledGroup {
6316                    group_id: "SG4".to_string(),
6317                    repetitions: vec![
6318                        AssembledGroupInstance {
6319                            segments: vec![AssembledSegment {
6320                                tag: "IDE".to_string(),
6321                                elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
6322                                mig_number: None,
6323                                segment_number: None,
6324                            }],
6325                            child_groups: vec![],
6326                            entry_mig_number: None,
6327                            variant_mig_numbers: vec![],
6328                            skipped_segments: vec![],
6329                            skipped_positions: Vec::new(),
6330                        },
6331                        AssembledGroupInstance {
6332                            segments: vec![AssembledSegment {
6333                                tag: "IDE".to_string(),
6334                                elements: vec![vec!["24".to_string()], vec!["TX002".to_string()]],
6335                                mig_number: None,
6336                                segment_number: None,
6337                            }],
6338                            child_groups: vec![],
6339                            entry_mig_number: None,
6340                            variant_mig_numbers: vec![],
6341                            skipped_segments: vec![],
6342                            skipped_positions: Vec::new(),
6343                        },
6344                    ],
6345                },
6346            ],
6347            post_group_start: 2,
6348            inter_group_segments: std::collections::BTreeMap::new(),
6349        };
6350
6351        // Message-level definitions
6352        let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
6353        msg_fields.insert(
6354            "nad.0".to_string(),
6355            FieldMapping::Simple("marktrolle".to_string()),
6356        );
6357        let msg_defs = vec![MappingDefinition {
6358            meta: MappingMeta {
6359                entity: "Marktteilnehmer".to_string(),
6360                bo4e_type: "Marktteilnehmer".to_string(),
6361                source_group: "SG2".to_string(),
6362                source_path: None,
6363                discriminator: None,
6364                repeat_on_tag: None,
6365                parent_field: None,
6366                target_list: None,
6367                order: None,
6368            },
6369            fields: msg_fields,
6370            complex_handlers: None,
6371        }];
6372
6373        // Transaction-level definitions (source_group includes SG4 prefix)
6374        let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
6375        tx_fields.insert(
6376            "ide.1".to_string(),
6377            FieldMapping::Simple("vorgangId".to_string()),
6378        );
6379        let tx_defs = vec![MappingDefinition {
6380            meta: MappingMeta {
6381                entity: "Prozessdaten".to_string(),
6382                bo4e_type: "Prozessdaten".to_string(),
6383                source_group: "SG4".to_string(),
6384                source_path: None,
6385                discriminator: None,
6386                repeat_on_tag: None,
6387                parent_field: None,
6388                target_list: None,
6389                order: None,
6390            },
6391            fields: tx_fields,
6392            complex_handlers: None,
6393        }];
6394
6395        let msg_engine = MappingEngine::from_definitions(msg_defs);
6396        let tx_engine = MappingEngine::from_definitions(tx_defs);
6397
6398        let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
6399
6400        // Message-level stammdaten
6401        assert!(result.stammdaten["marktteilnehmer"].is_object());
6402        assert_eq!(
6403            result.stammdaten["marktteilnehmer"]["marktrolle"]
6404                .as_str()
6405                .unwrap(),
6406            "MS"
6407        );
6408
6409        // Two transactions
6410        assert_eq!(result.transaktionen.len(), 2);
6411        assert_eq!(
6412            result.transaktionen[0].transaktionsdaten["vorgangId"]
6413                .as_str()
6414                .unwrap(),
6415            "TX001"
6416        );
6417        assert_eq!(
6418            result.transaktionen[1].transaktionsdaten["vorgangId"]
6419                .as_str()
6420                .unwrap(),
6421            "TX002"
6422        );
6423    }
6424
6425    #[test]
6426    fn test_map_reverse_with_segment_structure_pads_trailing() {
6427        // STS+7++E01 — position 0 and 2 populated, MIG says 5 elements
6428        let mut fields = IndexMap::new();
6429        fields.insert(
6430            "sts.0".to_string(),
6431            FieldMapping::Structured(StructuredFieldMapping {
6432                target: String::new(),
6433                transform: None,
6434                when: None,
6435                default: Some("7".to_string()),
6436                enum_map: None,
6437                code_list: None,
6438                also_code_list: None,
6439                when_filled: None,
6440                also_target: None,
6441                also_enum_map: None,
6442            }),
6443        );
6444        fields.insert(
6445            "sts.2".to_string(),
6446            FieldMapping::Simple("grund".to_string()),
6447        );
6448
6449        let def = make_def(fields);
6450
6451        // Build a SegmentStructure manually via BTreeMap
6452        let mut counts = std::collections::BTreeMap::new();
6453        counts.insert("STS".to_string(), 5usize);
6454        let ss = SegmentStructure {
6455            element_counts: counts,
6456        };
6457
6458        let engine = MappingEngine::from_definitions(vec![]).with_segment_structure(ss);
6459
6460        let bo4e = serde_json::json!({ "grund": "E01" });
6461
6462        let instance = engine.map_reverse(&bo4e, &def);
6463        let sts = &instance.segments[0];
6464        // Should have 5 elements: pos 0 = ["7"], pos 1 = [""] (intermediate pad),
6465        // pos 2 = ["E01"], pos 3 = [""] (trailing pad), pos 4 = [""] (trailing pad)
6466        assert_eq!(sts.elements.len(), 5);
6467        assert_eq!(sts.elements[0], vec!["7"]);
6468        assert_eq!(sts.elements[1], vec![""]);
6469        assert_eq!(sts.elements[2], vec!["E01"]);
6470        assert_eq!(sts.elements[3], vec![""]);
6471        assert_eq!(sts.elements[4], vec![""]);
6472    }
6473
6474    #[test]
6475    fn test_resolve_child_relative_with_source_path() {
6476        let mut map: std::collections::HashMap<String, Vec<usize>> =
6477            std::collections::HashMap::new();
6478        map.insert("sg4.sg8_ze1".to_string(), vec![6]);
6479        map.insert("sg4.sg8_z98".to_string(), vec![0]);
6480
6481        // Child without explicit index → resolved from source_path
6482        assert_eq!(
6483            resolve_child_relative("SG8.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
6484            "SG8:6.SG10"
6485        );
6486
6487        // Child with explicit index → kept as-is
6488        assert_eq!(
6489            resolve_child_relative("SG8:3.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
6490            "SG8:3.SG10"
6491        );
6492
6493        // Source path not in map → kept as-is
6494        assert_eq!(
6495            resolve_child_relative("SG8.SG10", Some("sg4.sg8_unknown.sg10"), &map, 0),
6496            "SG8.SG10"
6497        );
6498
6499        // No source_path → kept as-is
6500        assert_eq!(
6501            resolve_child_relative("SG8.SG10", None, &map, 0),
6502            "SG8.SG10"
6503        );
6504
6505        // SG9 also works
6506        assert_eq!(
6507            resolve_child_relative("SG8.SG9", Some("sg4.sg8_z98.sg9"), &map, 0),
6508            "SG8:0.SG9"
6509        );
6510
6511        // Multi-rep parent: item_idx selects the correct parent rep
6512        map.insert("sg4.sg8_zf3".to_string(), vec![3, 4]);
6513        assert_eq!(
6514            resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 0),
6515            "SG8:3.SG10"
6516        );
6517        assert_eq!(
6518            resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 1),
6519            "SG8:4.SG10"
6520        );
6521    }
6522
6523    #[test]
6524    fn test_place_in_groups_returns_rep_index() {
6525        let mut groups: Vec<AssembledGroup> = Vec::new();
6526
6527        // Append (no index) → returns position 0
6528        let instance = AssembledGroupInstance {
6529            segments: vec![],
6530            child_groups: vec![],
6531            entry_mig_number: None,
6532            variant_mig_numbers: vec![],
6533            skipped_segments: vec![],
6534            skipped_positions: Vec::new(),
6535        };
6536        assert_eq!(place_in_groups(&mut groups, "SG8", instance), 0);
6537
6538        // Append again → returns position 1
6539        let instance = AssembledGroupInstance {
6540            segments: vec![],
6541            child_groups: vec![],
6542            entry_mig_number: None,
6543            variant_mig_numbers: vec![],
6544            skipped_segments: vec![],
6545            skipped_positions: Vec::new(),
6546        };
6547        assert_eq!(place_in_groups(&mut groups, "SG8", instance), 1);
6548
6549        // Explicit index → returns that index
6550        let instance = AssembledGroupInstance {
6551            segments: vec![],
6552            child_groups: vec![],
6553            entry_mig_number: None,
6554            variant_mig_numbers: vec![],
6555            skipped_segments: vec![],
6556            skipped_positions: Vec::new(),
6557        };
6558        assert_eq!(place_in_groups(&mut groups, "SG8:5", instance), 5);
6559    }
6560
6561    #[test]
6562    fn test_resolve_by_source_path() {
6563        use mig_assembly::assembler::*;
6564
6565        // Build a tree: SG4[0] → SG8 with two reps (Z98 and ZD7) → each has SG10
6566        let tree = AssembledTree {
6567            segments: vec![],
6568            groups: vec![AssembledGroup {
6569                group_id: "SG4".to_string(),
6570                repetitions: vec![AssembledGroupInstance {
6571                    segments: vec![],
6572                    child_groups: vec![AssembledGroup {
6573                        group_id: "SG8".to_string(),
6574                        repetitions: vec![
6575                            AssembledGroupInstance {
6576                                segments: vec![AssembledSegment {
6577                                    tag: "SEQ".to_string(),
6578                                    elements: vec![vec!["Z98".to_string()]],
6579                                    mig_number: None,
6580                                    segment_number: None,
6581                                }],
6582                                child_groups: vec![AssembledGroup {
6583                                    group_id: "SG10".to_string(),
6584                                    repetitions: vec![AssembledGroupInstance {
6585                                        segments: vec![AssembledSegment {
6586                                            tag: "CCI".to_string(),
6587                                            elements: vec![vec![], vec![], vec!["ZB3".to_string()]],
6588                                            mig_number: None,
6589                                            segment_number: None,
6590                                        }],
6591                                        child_groups: vec![],
6592                                        entry_mig_number: None,
6593                                        variant_mig_numbers: vec![],
6594                                        skipped_segments: vec![],
6595                                        skipped_positions: Vec::new(),
6596                                    }],
6597                                }],
6598                                entry_mig_number: None,
6599                                variant_mig_numbers: vec![],
6600                                skipped_segments: vec![],
6601                                skipped_positions: Vec::new(),
6602                            },
6603                            AssembledGroupInstance {
6604                                segments: vec![AssembledSegment {
6605                                    tag: "SEQ".to_string(),
6606                                    elements: vec![vec!["ZD7".to_string()]],
6607                                    mig_number: None,
6608                                    segment_number: None,
6609                                }],
6610                                child_groups: vec![AssembledGroup {
6611                                    group_id: "SG10".to_string(),
6612                                    repetitions: vec![AssembledGroupInstance {
6613                                        segments: vec![AssembledSegment {
6614                                            tag: "CCI".to_string(),
6615                                            elements: vec![vec![], vec![], vec!["ZE6".to_string()]],
6616                                            mig_number: None,
6617                                            segment_number: None,
6618                                        }],
6619                                        child_groups: vec![],
6620                                        entry_mig_number: None,
6621                                        variant_mig_numbers: vec![],
6622                                        skipped_segments: vec![],
6623                                        skipped_positions: Vec::new(),
6624                                    }],
6625                                }],
6626                                entry_mig_number: None,
6627                                variant_mig_numbers: vec![],
6628                                skipped_segments: vec![],
6629                                skipped_positions: Vec::new(),
6630                            },
6631                        ],
6632                    }],
6633                    entry_mig_number: None,
6634                    variant_mig_numbers: vec![],
6635                    skipped_segments: vec![],
6636                    skipped_positions: Vec::new(),
6637                }],
6638            }],
6639            post_group_start: 0,
6640            inter_group_segments: std::collections::BTreeMap::new(),
6641        };
6642
6643        // Resolve SG10 under Z98
6644        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_z98.sg10");
6645        assert!(inst.is_some());
6646        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
6647
6648        // Resolve SG10 under ZD7
6649        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zd7.sg10");
6650        assert!(inst.is_some());
6651        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZE6");
6652
6653        // Unknown qualifier → None
6654        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zzz.sg10");
6655        assert!(inst.is_none());
6656
6657        // Without qualifier → first rep (Z98)
6658        let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8.sg10");
6659        assert!(inst.is_some());
6660        assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
6661    }
6662
6663    #[test]
6664    fn test_parse_source_path_part() {
6665        assert_eq!(parse_source_path_part("sg4"), ("sg4", None));
6666        assert_eq!(parse_source_path_part("sg8_z98"), ("sg8", Some("z98")));
6667        assert_eq!(parse_source_path_part("sg10"), ("sg10", None));
6668        assert_eq!(parse_source_path_part("sg12_z04"), ("sg12", Some("z04")));
6669    }
6670
6671    #[test]
6672    fn test_has_source_path_qualifiers() {
6673        assert!(has_source_path_qualifiers("sg4.sg8_z98.sg10"));
6674        assert!(has_source_path_qualifiers("sg4.sg8_ze1.sg9"));
6675        assert!(!has_source_path_qualifiers("sg4.sg6"));
6676        assert!(!has_source_path_qualifiers("sg4.sg8.sg10"));
6677    }
6678
6679    #[test]
6680    fn test_extract_all_from_instance_collects_all_qualifier_matches() {
6681        use mig_assembly::assembler::*;
6682
6683        // Instance with 3 RFF+Z34 segments
6684        let instance = AssembledGroupInstance {
6685            segments: vec![
6686                AssembledSegment {
6687                    tag: "SEQ".to_string(),
6688                    elements: vec![vec!["ZD6".to_string()]],
6689                    mig_number: None,
6690                    segment_number: None,
6691                },
6692                AssembledSegment {
6693                    tag: "RFF".to_string(),
6694                    elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
6695                    mig_number: None,
6696                    segment_number: None,
6697                },
6698                AssembledSegment {
6699                    tag: "RFF".to_string(),
6700                    elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
6701                    mig_number: None,
6702                    segment_number: None,
6703                },
6704                AssembledSegment {
6705                    tag: "RFF".to_string(),
6706                    elements: vec![vec!["Z34".to_string(), "REF_C".to_string()]],
6707                    mig_number: None,
6708                    segment_number: None,
6709                },
6710                AssembledSegment {
6711                    tag: "RFF".to_string(),
6712                    elements: vec![vec!["Z35".to_string(), "OTHER".to_string()]],
6713                    mig_number: None,
6714                    segment_number: None,
6715                },
6716            ],
6717            child_groups: vec![],
6718            entry_mig_number: None,
6719            variant_mig_numbers: vec![],
6720            skipped_segments: vec![],
6721            skipped_positions: Vec::new(),
6722        };
6723
6724        // Wildcard collect: rff[Z34,*] should collect all 3 RFF+Z34 values
6725        let all = MappingEngine::extract_all_from_instance(&instance, "rff[Z34,*].0.1");
6726        assert_eq!(all, vec!["REF_A", "REF_B", "REF_C"]);
6727
6728        // Non-wildcard still returns single value via extract_from_instance
6729        let single = MappingEngine::extract_from_instance(&instance, "rff[Z34].0.1");
6730        assert_eq!(single, Some("REF_A".to_string()));
6731
6732        let second = MappingEngine::extract_from_instance(&instance, "rff[Z34,1].0.1");
6733        assert_eq!(second, Some("REF_B".to_string()));
6734    }
6735}