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