mig-bo4e 0.1.30

Declarative TOML-based MIG-tree to BO4E mapping engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
//! Maps EDIFACT validation paths to BO4E field paths.
//!
//! When validation runs on EDIFACT produced by reverse-mapping BO4E JSON,
//! the resulting `ValidationIssue`s contain EDIFACT segment paths like
//! `SG4/SG5/LOC/C517/3225`. This module resolves those back to BO4E paths
//! like `stammdaten.Marktlokation.marktlokationsId` so users can find
//! the source of the problem in their BO4E input.

use mig_types::schema::mig::{MigSchema, MigSegment, MigSegmentGroup};

use crate::definition::{FieldMapping, MappingDefinition};
use crate::path_resolver::ReversePathResolver;

/// Maps EDIFACT segment paths from validation errors to BO4E field paths.
pub struct Bo4eFieldIndex {
    entries: Vec<IndexEntry>,
}

struct IndexEntry {
    /// EDIFACT group+segment prefix: "SG4/SG5/LOC", "SG2/NAD", "SG4/IDE", etc.
    edifact_prefix: String,
    /// BO4E entity name from TOML meta: "Marktlokation", "Prozessdaten"
    entity: String,
    /// Whether this entity is in stammdaten or transaktionsdaten.
    location: FieldLocation,
    /// Optional companion type (for companion_fields entries).
    companion_type: Option<String>,
    /// Individual field mappings within this segment.
    fields: Vec<FieldEntry>,
}

#[derive(Clone, Copy)]
enum FieldLocation {
    Stammdaten,
}

struct FieldEntry {
    /// Full EDIFACT field_path this matches (e.g., "SG4/SG5/LOC/C517/3225").
    edifact_path: String,
    /// BO4E target field name (e.g., "marktlokationsId").
    bo4e_field: String,
    /// Whether this is a companion field.
    is_companion: bool,
    /// Optional qualifier from the TOML path (e.g., "93" from "dtm[93].0.1").
    /// Used to disambiguate fields that share the same EDIFACT path.
    qualifier: Option<String>,
}

impl Bo4eFieldIndex {
    /// Build the index from TOML mapping definitions and a MIG schema.
    ///
    /// For each field in each definition, resolves the TOML numeric path
    /// (e.g., `loc.1.0`) to an AHB-style EDIFACT path (e.g., `SG4/SG5/LOC/C517/3225`)
    /// using the MIG schema for element ID lookup.
    pub fn build(definitions: &[MappingDefinition], mig: &MigSchema) -> Self {
        Self::build_inner(definitions, mig, None)
    }

    /// Build the index using a `ReversePathResolver` for element ID lookup.
    ///
    /// This is more accurate than `build` because the resolver is built from
    /// unmerged PID schema JSONs, avoiding data loss from MIG group merging
    /// (e.g., CCI composites lost when merging SG10 variants).
    pub fn build_with_resolver(
        definitions: &[MappingDefinition],
        mig: &MigSchema,
        resolver: &ReversePathResolver,
    ) -> Self {
        Self::build_inner(definitions, mig, Some(resolver))
    }

    fn build_inner(
        definitions: &[MappingDefinition],
        mig: &MigSchema,
        resolver: Option<&ReversePathResolver>,
    ) -> Self {
        let mut entries = Vec::new();

        for def in definitions {
            let group_path = source_group_to_slash(&def.meta.source_group);
            let location = classify_entity(&def.meta.entity);
            let companion_type = def.meta.companion_type.clone();

            let mut fields = Vec::new();

            // Process [fields]
            Self::collect_fields_inner(
                &def.fields,
                &group_path,
                mig,
                resolver,
                false,
                &mut fields,
            );

            // Process [companion_fields]
            if let Some(ref companion) = def.companion_fields {
                Self::collect_fields_inner(
                    companion,
                    &group_path,
                    mig,
                    resolver,
                    true,
                    &mut fields,
                );
            }

            if !fields.is_empty() {
                entries.push(IndexEntry {
                    edifact_prefix: group_path.clone(),
                    entity: def.meta.entity.clone(),
                    location,
                    companion_type,
                    fields,
                });
            }
        }

        Self { entries }
    }

    /// Given an EDIFACT field_path from a ValidationIssue, return the BO4E path.
    ///
    /// `hint` is an optional disambiguation string (e.g., expected value or AHB rule)
    /// that helps pick the right entry when multiple TOML definitions map to the same
    /// EDIFACT path (e.g., DTM+92 and DTM+93 both map to SG4/DTM/C507/2005).
    pub fn resolve(&self, edifact_field_path: &str, hint: Option<&str>) -> Option<String> {
        // Exact match on field entries — prefer qualifier-matching entries when hint is available
        let mut exact_matches: Vec<(&IndexEntry, &FieldEntry)> = Vec::new();
        for entry in &self.entries {
            for field in &entry.fields {
                if field.edifact_path == edifact_field_path {
                    exact_matches.push((entry, field));
                }
            }
        }

        if !exact_matches.is_empty() {
            // If we have a hint, try to match it against field qualifiers
            if let Some(hint) = hint {
                if let Some((entry, field)) = exact_matches
                    .iter()
                    .find(|(_, f)| f.qualifier.as_deref() == Some(hint))
                {
                    return Some(self.build_bo4e_path(entry, field));
                }
                // Also try matching hint as a substring of qualifier or vice versa
                if let Some((entry, field)) = exact_matches
                    .iter()
                    .find(|(_, f)| {
                        f.qualifier
                            .as_deref()
                            .is_some_and(|q| hint.contains(q) || q.contains(hint))
                    })
                {
                    return Some(self.build_bo4e_path(entry, field));
                }
            }
            // No hint match — return first exact match
            let (entry, field) = exact_matches[0];
            return Some(self.build_bo4e_path(entry, field));
        }

        // Sibling match: for qualifier fields like SG4/DTM/C507/2005, find entries
        // for the same segment composite (SG4/DTM/C507/*) that match the hint qualifier.
        // This handles cases where the qualifier element (2005) doesn't have its own
        // index entry but its sibling data field (2380) does.
        if let Some(hint) = hint {
            let composite_prefix = edifact_field_path.rsplit_once('/').map(|(p, _)| p);
            if let Some(prefix) = composite_prefix {
                for entry in &self.entries {
                    for field in &entry.fields {
                        if field.edifact_path.starts_with(prefix)
                            && field.qualifier.as_deref() == Some(hint)
                        {
                            return Some(self.build_bo4e_path(entry, field));
                        }
                    }
                }
            }
        }

        // Prefix match for code/qualifier paths — longest prefix wins
        let mut best: Option<&IndexEntry> = None;
        for entry in &self.entries {
            if !entry.edifact_prefix.is_empty()
                && edifact_field_path.starts_with(&entry.edifact_prefix)
                && best
                    .map(|b| entry.edifact_prefix.len() > b.edifact_prefix.len())
                    .unwrap_or(true)
            {
                best = Some(entry);
            }
        }
        best.map(|entry| self.build_entity_path(entry))
    }

    /// Debug: return all entries as (edifact_path, entity, bo4e_field) tuples.
    pub fn debug_entries(&self) -> Vec<(String, String, String)> {
        let mut out = Vec::new();
        for entry in &self.entries {
            for field in &entry.fields {
                out.push((
                    field.edifact_path.clone(),
                    entry.entity.clone(),
                    field.bo4e_field.clone(),
                ));
            }
        }
        out
    }

    fn collect_fields_inner(
        field_map: &indexmap::IndexMap<String, FieldMapping>,
        group_path: &str,
        mig: &MigSchema,
        resolver: Option<&ReversePathResolver>,
        is_companion: bool,
        out: &mut Vec<FieldEntry>,
    ) {
        // First pass: collect qualifier paths (empty target) and data field paths.
        // Qualifier paths like dtm[92].c507.d2005 have empty target but carry a
        // default value (the qualifier code). We'll create entries for them in
        // a second pass, pointing to their sibling data field.
        struct QualifierPath {
            parsed: ParsedTomlPath,
        }
        let mut qualifier_paths: Vec<QualifierPath> = Vec::new();
        // Map from (tag, qualifier) → first data field BO4E name for sibling lookup
        let mut tag_qualifier_to_field: std::collections::HashMap<(String, String), String> =
            std::collections::HashMap::new();

        for (toml_path, mapping) in field_map {
            let target = match mapping {
                FieldMapping::Simple(s) => s.as_str(),
                FieldMapping::Structured(s) => s.target.as_str(),
                FieldMapping::Nested(_) => continue,
            };

            let parsed = match parse_toml_path(toml_path) {
                Some(p) => p,
                None => continue,
            };

            if target.is_empty() {
                // Qualifier/default field — collect for second pass
                qualifier_paths.push(QualifierPath { parsed });
                continue;
            }

            // Track first data field per (tag, qualifier) for sibling lookup
            if let Some(ref q) = parsed.qualifier {
                tag_qualifier_to_field
                    .entry((parsed.segment_tag.clone(), q.clone()))
                    .or_insert_with(|| target.to_string());
            }

            // Resolve and add the data field entry
            let edifact_path = resolver
                .and_then(|r| resolve_edifact_path_via_resolver(group_path, &parsed, r))
                .or_else(|| resolve_edifact_path(group_path, &parsed, mig));

            if let Some(edifact_path) = edifact_path {
                out.push(FieldEntry {
                    edifact_path,
                    bo4e_field: target.to_string(),
                    is_companion,
                    qualifier: parsed.qualifier.clone(),
                });
            }
        }

        // Second pass: create entries for qualifier paths that reference their
        // sibling data field. E.g., dtm[93].c507.d2005 (qualifier for gueltigBis)
        // gets an entry pointing to "gueltigBis" so the missing-qualifier error
        // resolves to stammdaten.Prozessdaten.gueltigBis instead of just Prozessdaten.
        for qp in &qualifier_paths {
            if let Some(ref q) = qp.parsed.qualifier {
                let key = (qp.parsed.segment_tag.clone(), q.clone());
                if let Some(sibling_field) = tag_qualifier_to_field.get(&key)
                {
                    let edifact_path = resolver
                        .and_then(|r| {
                            resolve_edifact_path_via_resolver(group_path, &qp.parsed, r)
                        })
                        .or_else(|| resolve_edifact_path(group_path, &qp.parsed, mig));

                    if let Some(edifact_path) = edifact_path {
                        out.push(FieldEntry {
                            edifact_path,
                            bo4e_field: sibling_field.clone(),
                            is_companion,
                            qualifier: qp.parsed.qualifier.clone(),
                        });
                    }
                }
            }
        }
    }

    fn build_bo4e_path(&self, entry: &IndexEntry, field: &FieldEntry) -> String {
        let location = match entry.location {
            FieldLocation::Stammdaten => "stammdaten",
        };
        if field.is_companion {
            if let Some(ref ct) = entry.companion_type {
                format!(
                    "{}.{}.{}.{}",
                    location,
                    entry.entity,
                    to_camel_first_lower(ct),
                    field.bo4e_field
                )
            } else {
                format!("{}.{}.{}", location, entry.entity, field.bo4e_field)
            }
        } else {
            format!("{}.{}.{}", location, entry.entity, field.bo4e_field)
        }
    }

    fn build_entity_path(&self, entry: &IndexEntry) -> String {
        let location = match entry.location {
            FieldLocation::Stammdaten => "stammdaten",
        };
        format!("{}.{}", location, entry.entity)
    }
}

/// Parsed TOML field path components.
struct ParsedTomlPath {
    /// Segment tag in uppercase (e.g., "LOC", "DTM").
    segment_tag: String,
    /// Element index (e.g., 1 in "loc.1.0").
    element_idx: usize,
    /// Optional component sub-index (e.g., 0 in "loc.1.0").
    component_idx: Option<usize>,
    /// Optional qualifier from the tag (e.g., "93" from "dtm[93]").
    qualifier: Option<String>,
}

/// Parse a TOML field path like "loc.1.0" or "dtm[92].0.1".
fn parse_toml_path(path: &str) -> Option<ParsedTomlPath> {
    let parts: Vec<&str> = path.split('.').collect();
    if parts.len() < 2 {
        return None;
    }

    // Extract qualifier from tag: "dtm[92]" → tag="DTM", qualifier=Some("92")
    let raw_tag = parts[0];
    let (tag, qualifier) = if let Some(bracket) = raw_tag.find('[') {
        let end = raw_tag.find(']').unwrap_or(raw_tag.len());
        let qual = &raw_tag[bracket + 1..end];
        // Strip occurrence suffix: "Z34,1" → "Z34"
        let qual = qual.split(',').next().unwrap_or(qual);
        (&raw_tag[..bracket], Some(qual.to_string()))
    } else {
        (raw_tag, None)
    };

    let element_idx: usize = parts[1].parse().ok()?;
    let component_idx = if parts.len() > 2 {
        Some(parts[2].parse::<usize>().ok()?)
    } else {
        None
    };

    Some(ParsedTomlPath {
        segment_tag: tag.to_uppercase(),
        element_idx,
        component_idx,
        qualifier,
    })
}

/// Convert source_group dot notation to slash notation, stripping `:N` suffixes.
/// "SG4.SG5" → "SG4/SG5", "SG8:1.SG10" → "SG8/SG10"
fn source_group_to_slash(source_group: &str) -> String {
    source_group
        .split('.')
        .map(|part| {
            if let Some(colon) = part.find(':') {
                &part[..colon]
            } else {
                part
            }
        })
        .collect::<Vec<_>>()
        .join("/")
}

/// Classify entity location. All entities are now in stammdaten
/// (the transaktionsdaten split has been removed).
fn classify_entity(_entity: &str) -> FieldLocation {
    FieldLocation::Stammdaten
}

/// Resolve a parsed TOML path to an AHB-style EDIFACT path using the ReversePathResolver.
///
/// This is more accurate than MIG-based resolution because the resolver is built from
/// unmerged PID schema JSONs, preserving composites that get lost during MIG merging.
fn resolve_edifact_path_via_resolver(
    group_path: &str,
    parsed: &ParsedTomlPath,
    resolver: &ReversePathResolver,
) -> Option<String> {
    // Build the numeric path: "cci.2.0" from tag=CCI, elem=2, comp=0
    let numeric_path = if let Some(ci) = parsed.component_idx {
        format!(
            "{}.{}.{}",
            parsed.segment_tag.to_lowercase(),
            parsed.element_idx,
            ci
        )
    } else {
        format!(
            "{}.{}",
            parsed.segment_tag.to_lowercase(),
            parsed.element_idx
        )
    };

    // Use ReversePathResolver to get named path: "cci.c240.d7037"
    let named = resolver.reverse_path(&numeric_path);
    if named == numeric_path {
        // Resolver couldn't resolve — not in schema
        return None;
    }

    // Convert named path to AHB-style: "cci.c240.d7037" → "CCI/C240/7037"
    let parts: Vec<&str> = named.split('.').collect();
    let ahb_parts: Vec<String> = parts
        .iter()
        .map(|p| {
            // Strip qualifier suffix: "dtm[92]" → "DTM"
            let clean = if let Some(bracket) = p.find('[') {
                &p[..bracket]
            } else {
                p
            };
            // Strip edifact id prefix (c/d/s): "c240" → "C240", "d7037" → "7037"
            if clean.len() > 1
                && (clean.starts_with('c') || clean.starts_with('C'))
                && clean[1..].chars().next().is_some_and(|c| c.is_ascii_digit())
            {
                clean.to_uppercase()
            } else if clean.len() > 1
                && (clean.starts_with('d') || clean.starts_with('D'))
                && clean[1..].chars().next().is_some_and(|c| c.is_ascii_digit())
            {
                // Data element: strip 'd' prefix → "7037"
                clean[1..].to_string()
            } else {
                clean.to_uppercase()
            }
        })
        .collect();

    let edifact_suffix = ahb_parts.join("/");

    if group_path.is_empty() {
        Some(edifact_suffix)
    } else {
        Some(format!("{}/{}", group_path, edifact_suffix))
    }
}

/// Resolve a parsed TOML path to an AHB-style EDIFACT path using the MIG.
fn resolve_edifact_path(
    group_path: &str,
    parsed: &ParsedTomlPath,
    mig: &MigSchema,
) -> Option<String> {
    // Find the segment in the MIG
    let segment = find_segment_in_mig(mig, group_path, &parsed.segment_tag)?;

    // Build a unified list of (position, element_kind) sorted by position
    let resolved = resolve_element_at_position(segment, parsed.element_idx, parsed.component_idx)?;

    let prefix = if group_path.is_empty() {
        parsed.segment_tag.clone()
    } else {
        format!("{}/{}", group_path, parsed.segment_tag)
    };

    match resolved {
        ResolvedElement::DataElement(id) => Some(format!("{}/{}", prefix, id)),
        ResolvedElement::CompositeElement(composite_id, element_id) => {
            Some(format!("{}/{}/{}", prefix, composite_id, element_id))
        }
    }
}

enum ResolvedElement {
    /// A standalone data element: just the element ID.
    DataElement(String),
    /// A component within a composite: (composite_id, data_element_id).
    CompositeElement(String, String),
}

/// Find a segment by tag within a group path in the MIG.
fn find_segment_in_mig<'a>(
    mig: &'a MigSchema,
    group_path: &str,
    segment_tag: &str,
) -> Option<&'a MigSegment> {
    if group_path.is_empty() {
        // Root-level segment
        return mig
            .segments
            .iter()
            .find(|s| s.id.eq_ignore_ascii_case(segment_tag));
    }

    let parts: Vec<&str> = group_path.split('/').collect();

    // Find the first group
    let mut current_group = mig
        .segment_groups
        .iter()
        .find(|g| g.id.eq_ignore_ascii_case(parts[0]))?;

    // Navigate nested groups
    for &part in &parts[1..] {
        current_group = current_group
            .nested_groups
            .iter()
            .find(|g| g.id.eq_ignore_ascii_case(part))?;
    }

    find_segment_in_group(current_group, segment_tag)
}

/// Find a segment by tag within a group (checking the group and its nested groups).
fn find_segment_in_group<'a>(
    group: &'a MigSegmentGroup,
    segment_tag: &str,
) -> Option<&'a MigSegment> {
    group
        .segments
        .iter()
        .find(|s| s.id.eq_ignore_ascii_case(segment_tag))
}

/// Resolve an element at a given position within a MIG segment.
///
/// Builds a unified position list from data_elements and composites,
/// then finds what's at element_idx. If it's a composite and component_idx
/// is provided, returns the sub-element.
fn resolve_element_at_position(
    segment: &MigSegment,
    element_idx: usize,
    component_idx: Option<usize>,
) -> Option<ResolvedElement> {
    // Check composites first — they have a position field
    if let Some(composite) = segment
        .composites
        .iter()
        .find(|c| c.position == element_idx)
    {
        let comp_idx = component_idx.unwrap_or(0);
        // Find the data element at the component sub-index by sorting by position
        let mut sub_elements: Vec<_> = composite.data_elements.iter().collect();
        sub_elements.sort_by_key(|de| de.position);
        let de = sub_elements.get(comp_idx)?;
        return Some(ResolvedElement::CompositeElement(
            composite.id.clone(),
            de.id.clone(),
        ));
    }

    // Check standalone data elements
    if let Some(de) = segment
        .data_elements
        .iter()
        .find(|d| d.position == element_idx)
    {
        return Some(ResolvedElement::DataElement(de.id.clone()));
    }

    None
}

/// Convert PascalCase to camelCase (first char lowercase).
fn to_camel_first_lower(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_source_group_to_slash() {
        assert_eq!(source_group_to_slash("SG4.SG5"), "SG4/SG5");
        assert_eq!(source_group_to_slash("SG4"), "SG4");
        assert_eq!(source_group_to_slash("SG8:1.SG10"), "SG8/SG10");
        assert_eq!(source_group_to_slash(""), "");
    }

    #[test]
    fn test_parse_toml_path() {
        let p = parse_toml_path("loc.1.0").unwrap();
        assert_eq!(p.segment_tag, "LOC");
        assert_eq!(p.element_idx, 1);
        assert_eq!(p.component_idx, Some(0));

        let p = parse_toml_path("ide.1").unwrap();
        assert_eq!(p.segment_tag, "IDE");
        assert_eq!(p.element_idx, 1);
        assert_eq!(p.component_idx, None);

        let p = parse_toml_path("dtm[92].0.1").unwrap();
        assert_eq!(p.segment_tag, "DTM");
        assert_eq!(p.element_idx, 0);
        assert_eq!(p.component_idx, Some(1));

        assert!(parse_toml_path("loc").is_none());
    }

    #[test]
    fn test_classify_entity() {
        // All entities are now classified as Stammdaten (no more transaktionsdaten split)
        assert!(matches!(
            classify_entity("Prozessdaten"),
            FieldLocation::Stammdaten
        ));
        assert!(matches!(
            classify_entity("Nachricht"),
            FieldLocation::Stammdaten
        ));
        assert!(matches!(
            classify_entity("Marktlokation"),
            FieldLocation::Stammdaten
        ));
        assert!(matches!(
            classify_entity("Marktteilnehmer"),
            FieldLocation::Stammdaten
        ));
    }

    #[test]
    fn test_to_camel_first_lower() {
        assert_eq!(
            to_camel_first_lower("MarktlokationEdifact"),
            "marktlokationEdifact"
        );
        assert_eq!(to_camel_first_lower("Foo"), "foo");
        assert_eq!(to_camel_first_lower(""), "");
    }

    #[test]
    fn test_resolve_returns_none_for_unknown_path() {
        let index = Bo4eFieldIndex { entries: vec![] };
        assert!(index.resolve("SG99/UNKNOWN/9999", None).is_none());
    }
}