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