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