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