Skip to main content

automapper_validation/eval/
context.rs

1//! Evaluation context for condition evaluation.
2
3use super::evaluator::{ConditionResult, ExternalConditionProvider};
4use mig_types::navigator::GroupNavigator;
5use mig_types::segment::OwnedSegment;
6
7/// Context passed to condition evaluators during evaluation.
8///
9/// Carries references to the transaction data and external condition
10/// provider needed to evaluate AHB conditions.
11pub struct EvaluationContext<'a> {
12    /// The Pruefidentifikator (e.g., "11001", "55001") that identifies
13    /// the specific AHB workflow being validated against.
14    pub pruefidentifikator: &'a str,
15
16    /// Provider for external conditions that depend on business context
17    /// outside the EDIFACT message.
18    pub external: &'a dyn ExternalConditionProvider,
19
20    /// Parsed EDIFACT segments for direct segment inspection by condition
21    /// evaluators. Conditions often need to check specific segment values.
22    pub segments: &'a [OwnedSegment],
23
24    /// Optional group navigator for group-scoped condition queries.
25    /// When None, group-scoped methods return empty / false / 0.
26    pub navigator: Option<&'a dyn GroupNavigator>,
27
28    /// The resolved value of the data element currently being validated.
29    ///
30    /// Set per-field during tree-based validation from `AhbNode.value`.
31    /// Format/value conditions (e.g., [931] "ZZZ=+00", [932] "HHMM=2200")
32    /// check this first instead of searching message-wide.
33    /// When None, conditions fall back to message-wide segment searches.
34    pub resolved_value: Option<&'a str>,
35
36    /// The full segment elements for the segment being validated.
37    ///
38    /// Allows cross-element access within the same segment (e.g., check
39    /// qualifier in element 0 while validating value in element 1).
40    /// When None, conditions fall back to message-wide segment searches.
41    pub resolved_segment: Option<&'a [Vec<String>]>,
42}
43
44/// A no-op group navigator that returns empty results for all queries.
45pub struct NoOpGroupNavigator;
46
47impl GroupNavigator for NoOpGroupNavigator {
48    fn find_segments_in_group(&self, _: &str, _: &[&str], _: usize) -> Vec<OwnedSegment> {
49        Vec::new()
50    }
51    fn find_segments_with_qualifier_in_group(
52        &self,
53        _: &str,
54        _: usize,
55        _: &str,
56        _: &[&str],
57        _: usize,
58    ) -> Vec<OwnedSegment> {
59        Vec::new()
60    }
61    fn group_instance_count(&self, _: &[&str]) -> usize {
62        0
63    }
64}
65
66impl<'a> EvaluationContext<'a> {
67    /// Create a new evaluation context (without group navigator).
68    pub fn new(
69        pruefidentifikator: &'a str,
70        external: &'a dyn ExternalConditionProvider,
71        segments: &'a [OwnedSegment],
72    ) -> Self {
73        Self {
74            pruefidentifikator,
75            external,
76            segments,
77            navigator: None,
78            resolved_value: None,
79            resolved_segment: None,
80        }
81    }
82
83    /// Create a new evaluation context with a group navigator.
84    pub fn with_navigator(
85        pruefidentifikator: &'a str,
86        external: &'a dyn ExternalConditionProvider,
87        segments: &'a [OwnedSegment],
88        navigator: &'a dyn GroupNavigator,
89    ) -> Self {
90        Self {
91            pruefidentifikator,
92            external,
93            segments,
94            navigator: Some(navigator),
95            resolved_value: None,
96            resolved_segment: None,
97        }
98    }
99
100    /// Create a context with a resolved field value for tree-based validation.
101    ///
102    /// The resolved value comes from the `ValidatedTree` — the specific data
103    /// element value at the current tree position. Conditions check this first.
104    pub fn with_resolved(
105        &self,
106        value: Option<&'a str>,
107        segment: Option<&'a [Vec<String>]>,
108    ) -> Self {
109        Self {
110            resolved_value: value,
111            resolved_segment: segment,
112            ..*self
113        }
114    }
115
116    /// Get the group navigator, if one is set.
117    pub fn navigator(&self) -> Option<&'a dyn GroupNavigator> {
118        self.navigator
119    }
120
121    /// Find the first segment with the given ID.
122    pub fn find_segment(&self, segment_id: &str) -> Option<&'a OwnedSegment> {
123        self.segments.iter().find(|s| s.id == segment_id)
124    }
125
126    /// Find all segments with the given ID.
127    pub fn find_segments(&self, segment_id: &str) -> Vec<&'a OwnedSegment> {
128        self.segments
129            .iter()
130            .filter(|s| s.id == segment_id)
131            .collect()
132    }
133
134    /// Find segments with a specific qualifier value on a given element.
135    pub fn find_segments_with_qualifier(
136        &self,
137        segment_id: &str,
138        element_index: usize,
139        qualifier: &str,
140    ) -> Vec<&'a OwnedSegment> {
141        self.segments
142            .iter()
143            .filter(|s| {
144                s.id == segment_id
145                    && s.elements
146                        .get(element_index)
147                        .and_then(|e| e.first())
148                        .is_some_and(|v| v == qualifier)
149            })
150            .collect()
151    }
152
153    /// Validate a format condition on the current field value.
154    ///
155    /// Prefers `resolved_value` (exact field from tree-based validation).
156    /// Falls back to searching segments by tag and extracting `elements[elem][comp]`,
157    /// but only when exactly one segment of that tag exists — otherwise the
158    /// fallback would pick an arbitrary occurrence and validate the wrong field.
159    /// Returns `ConditionResult::Unknown` when no unambiguous value is available.
160    ///
161    /// Usage: `ctx.format_check("DTM", 0, 1, validate_timezone_utc)`
162    pub fn format_check(
163        &self,
164        tag: &str,
165        elem: usize,
166        comp: usize,
167        validate: impl FnOnce(&str) -> ConditionResult,
168    ) -> ConditionResult {
169        // Tree-based validation: the exact field value is authoritative.
170        if let Some(val) = self.resolved_value {
171            return validate(val);
172        }
173        // Non-tree fallback: use the segment only when unambiguous. Picking
174        // `.first()` from multiple same-tag segments (e.g. several DTMs in the
175        // message) would validate an arbitrary one, producing both false
176        // positives and false negatives. Unknown is the honest answer.
177        let segs = self.find_segments(tag);
178        if segs.len() != 1 {
179            return ConditionResult::Unknown;
180        }
181        match segs[0]
182            .elements
183            .get(elem)
184            .and_then(|e| e.get(comp))
185            .map(|s| s.as_str())
186        {
187            Some(val) => validate(val),
188            None => ConditionResult::Unknown,
189        }
190    }
191
192    /// Validate a format condition with qualifier-filtered segment fallback.
193    ///
194    /// Like `format_check`, but the fallback searches for segments matching
195    /// `tag` where `elements[qual_elem][0] == qualifier`, then extracts
196    /// `elements[val_elem][val_comp]`. Returns `Unknown` when the qualifier
197    /// filter doesn't narrow down to exactly one segment.
198    ///
199    /// Usage: `ctx.format_check_qualified("DTM", 0, "163", 0, 1, |v| validate_hhmm_equals(v, "2200"))`
200    pub fn format_check_qualified(
201        &self,
202        tag: &str,
203        qual_elem: usize,
204        qualifier: &str,
205        val_elem: usize,
206        val_comp: usize,
207        validate: impl FnOnce(&str) -> ConditionResult,
208    ) -> ConditionResult {
209        if let Some(val) = self.resolved_value {
210            return validate(val);
211        }
212        let segs = self.find_segments_with_qualifier(tag, qual_elem, qualifier);
213        if segs.len() != 1 {
214            return ConditionResult::Unknown;
215        }
216        match segs[0]
217            .elements
218            .get(val_elem)
219            .and_then(|e| e.get(val_comp))
220            .map(|s| s.as_str())
221        {
222            Some(val) => validate(val),
223            None => ConditionResult::Unknown,
224        }
225    }
226
227    /// Read a value at `elements[elem][comp]` of the segment currently being
228    /// validated ("im selben Segment" conditions).
229    ///
230    /// Prefers `resolved_segment` (the exact segment the field lives in,
231    /// bound per-field during tree-based validation). Falls back to the
232    /// first message-wide segment matching `tag` when no tree context is
233    /// available — matches the prior `find_segments(tag).first()` behavior.
234    ///
235    /// Conditions phrased "Wenn im selben Segment im DE2379 der Code 303
236    /// vorhanden ist" should use this instead of iterating every `tag`
237    /// segment in the message, which finds spurious matches elsewhere.
238    pub fn self_segment_value(
239        &self,
240        tag: &str,
241        elem: usize,
242        comp: usize,
243    ) -> Option<&str> {
244        if let Some(elements) = self.resolved_segment {
245            return elements
246                .get(elem)
247                .and_then(|e| e.get(comp))
248                .map(|s| s.as_str());
249        }
250        self.segments
251            .iter()
252            .find(|s| s.id == tag)
253            .and_then(|s| s.elements.get(elem))
254            .and_then(|e| e.get(comp))
255            .map(|s| s.as_str())
256    }
257
258    /// Convenience wrapper — returns `true` when `self_segment_value` equals
259    /// `expected`. The common shape for "im selben Segment" code-equality
260    /// conditions.
261    pub fn self_segment_value_equals(
262        &self,
263        tag: &str,
264        elem: usize,
265        comp: usize,
266        expected: &str,
267    ) -> bool {
268        self.self_segment_value(tag, elem, comp)
269            .is_some_and(|v| v == expected)
270    }
271
272    /// Check if a segment with the given ID exists.
273    pub fn has_segment(&self, segment_id: &str) -> bool {
274        self.segments.iter().any(|s| s.id == segment_id)
275    }
276
277    /// Find all segments with the given tag within a specific group instance.
278    /// Returns empty if no navigator is set.
279    pub fn find_segments_in_group(
280        &self,
281        segment_id: &str,
282        group_path: &[&str],
283        instance_index: usize,
284    ) -> Vec<OwnedSegment> {
285        match self.navigator {
286            Some(nav) => nav.find_segments_in_group(segment_id, group_path, instance_index),
287            None => Vec::new(),
288        }
289    }
290
291    /// Find segments matching a tag + qualifier within a group instance.
292    /// Returns empty if no navigator is set.
293    pub fn find_segments_with_qualifier_in_group(
294        &self,
295        segment_id: &str,
296        element_index: usize,
297        qualifier: &str,
298        group_path: &[&str],
299        instance_index: usize,
300    ) -> Vec<OwnedSegment> {
301        match self.navigator {
302            Some(nav) => nav.find_segments_with_qualifier_in_group(
303                segment_id,
304                element_index,
305                qualifier,
306                group_path,
307                instance_index,
308            ),
309            None => Vec::new(),
310        }
311    }
312
313    /// Check if a segment exists in a specific group instance.
314    /// Returns false if no navigator is set.
315    pub fn has_segment_in_group(
316        &self,
317        segment_id: &str,
318        group_path: &[&str],
319        instance_index: usize,
320    ) -> bool {
321        !self
322            .find_segments_in_group(segment_id, group_path, instance_index)
323            .is_empty()
324    }
325
326    /// Count repetitions of a group at the given path.
327    /// Returns 0 if no navigator is set.
328    pub fn group_instance_count(&self, group_path: &[&str]) -> usize {
329        match self.navigator {
330            Some(nav) => nav.group_instance_count(group_path),
331            None => 0,
332        }
333    }
334
335    /// Count child group repetitions within a specific parent group instance.
336    /// Returns 0 if no navigator is set.
337    pub fn child_group_instance_count(
338        &self,
339        parent_path: &[&str],
340        parent_instance: usize,
341        child_group_id: &str,
342    ) -> usize {
343        match self.navigator {
344            Some(nav) => {
345                nav.child_group_instance_count(parent_path, parent_instance, child_group_id)
346            }
347            None => 0,
348        }
349    }
350
351    /// Find segments in a child group within a specific parent group instance.
352    /// Returns empty if no navigator is set.
353    pub fn find_segments_in_child_group(
354        &self,
355        segment_id: &str,
356        parent_path: &[&str],
357        parent_instance: usize,
358        child_group_id: &str,
359        child_instance: usize,
360    ) -> Vec<OwnedSegment> {
361        match self.navigator {
362            Some(nav) => nav.find_segments_in_child_group(
363                segment_id,
364                parent_path,
365                parent_instance,
366                child_group_id,
367                child_instance,
368            ),
369            None => Vec::new(),
370        }
371    }
372
373    /// Extract a single value from the first matching segment in a group instance.
374    /// Returns None if no navigator is set or value not found.
375    pub fn extract_value_in_group(
376        &self,
377        segment_id: &str,
378        element_index: usize,
379        component_index: usize,
380        group_path: &[&str],
381        instance_index: usize,
382    ) -> Option<String> {
383        self.navigator?.extract_value_in_group(
384            segment_id,
385            element_index,
386            component_index,
387            group_path,
388            instance_index,
389        )
390    }
391
392    // --- High-level condition helpers ---
393    // These reduce generated condition evaluator boilerplate by ~50%.
394
395    /// Check if any segment with the given tag + qualifier exists (message-wide).
396    /// Returns `True` if found, `False` if not.
397    pub fn has_qualifier(
398        &self,
399        tag: &str,
400        element_index: usize,
401        qualifier: &str,
402    ) -> ConditionResult {
403        ConditionResult::from(
404            !self
405                .find_segments_with_qualifier(tag, element_index, qualifier)
406                .is_empty(),
407        )
408    }
409
410    /// Check if a segment with given tag + qualifier does NOT exist (message-wide).
411    /// Returns `True` if absent, `False` if present.
412    pub fn lacks_qualifier(
413        &self,
414        tag: &str,
415        element_index: usize,
416        qualifier: &str,
417    ) -> ConditionResult {
418        ConditionResult::from(
419            self.find_segments_with_qualifier(tag, element_index, qualifier)
420                .is_empty(),
421        )
422    }
423
424    /// Check if any segment with the given tag + qualifier has a specific sub-element value.
425    ///
426    /// Finds segments matching `tag` with `elements[qual_elem][0] == qualifier`,
427    /// then checks if `elements[value_elem][value_comp]` matches any of `values`.
428    pub fn has_qualified_value(
429        &self,
430        tag: &str,
431        qual_elem: usize,
432        qualifier: &str,
433        value_elem: usize,
434        value_comp: usize,
435        values: &[&str],
436    ) -> ConditionResult {
437        let segments = self.find_segments_with_qualifier(tag, qual_elem, qualifier);
438        if segments.is_empty() {
439            return ConditionResult::Unknown;
440        }
441        for seg in &segments {
442            if let Some(v) = seg
443                .elements
444                .get(value_elem)
445                .and_then(|e| e.get(value_comp))
446                .map(|s| s.as_str())
447            {
448                if values.contains(&v) {
449                    return ConditionResult::True;
450                }
451            }
452        }
453        ConditionResult::False
454    }
455
456    /// Group-scoped qualifier existence check with message-wide fallback.
457    ///
458    /// Checks if any group instance at `group_path` contains a segment matching
459    /// `tag` with `elements[element_index][0] == qualifier`. Falls back to
460    /// message-wide search if no group navigator is available.
461    pub fn any_group_has_qualifier(
462        &self,
463        tag: &str,
464        element_index: usize,
465        qualifier: &str,
466        group_path: &[&str],
467    ) -> ConditionResult {
468        let instance_count = self.group_instance_count(group_path);
469        if instance_count > 0 {
470            for i in 0..instance_count {
471                if !self
472                    .find_segments_with_qualifier_in_group(
473                        tag,
474                        element_index,
475                        qualifier,
476                        group_path,
477                        i,
478                    )
479                    .is_empty()
480                {
481                    return ConditionResult::True;
482                }
483            }
484            return ConditionResult::False;
485        }
486        // Fallback: message-wide search
487        self.has_qualifier(tag, element_index, qualifier)
488    }
489
490    /// Group-scoped segment existence check (any tag match, no qualifier).
491    ///
492    /// Checks if any group instance at `group_path` contains a segment with
493    /// `elements[element_index][0]` matching any of `qualifiers`. Falls back
494    /// to message-wide search if no group navigator is available.
495    pub fn any_group_has_any_qualifier(
496        &self,
497        tag: &str,
498        element_index: usize,
499        qualifiers: &[&str],
500        group_path: &[&str],
501    ) -> ConditionResult {
502        let instance_count = self.group_instance_count(group_path);
503        if instance_count > 0 {
504            for i in 0..instance_count {
505                let segs = self.find_segments_in_group(tag, group_path, i);
506                if segs.iter().any(|seg| {
507                    seg.elements
508                        .get(element_index)
509                        .and_then(|e| e.first())
510                        .map(|s| s.as_str())
511                        .is_some_and(|v| qualifiers.contains(&v))
512                }) {
513                    return ConditionResult::True;
514                }
515            }
516            return ConditionResult::False;
517        }
518        // Fallback: message-wide search
519        let found = self.find_segments(tag).iter().any(|seg| {
520            seg.elements
521                .get(element_index)
522                .and_then(|e| e.first())
523                .map(|s| s.as_str())
524                .is_some_and(|v| qualifiers.contains(&v))
525        });
526        ConditionResult::from(found)
527    }
528
529    /// Group-scoped check for sub-element value within qualified segments.
530    ///
531    /// For each group instance at `group_path`, checks if a segment matching
532    /// `tag` with `elements[qual_elem][0] == qualifier` has
533    /// `elements[value_elem][value_comp]` in `values`. Falls back to message-wide.
534    pub fn any_group_has_qualified_value(
535        &self,
536        tag: &str,
537        qual_elem: usize,
538        qualifier: &str,
539        value_elem: usize,
540        value_comp: usize,
541        values: &[&str],
542        group_path: &[&str],
543    ) -> ConditionResult {
544        let instance_count = self.group_instance_count(group_path);
545        if instance_count > 0 {
546            for i in 0..instance_count {
547                let segs = self.find_segments_with_qualifier_in_group(
548                    tag, qual_elem, qualifier, group_path, i,
549                );
550                for seg in &segs {
551                    if seg
552                        .elements
553                        .get(value_elem)
554                        .and_then(|e| e.get(value_comp))
555                        .map(|s| s.as_str())
556                        .is_some_and(|v| values.contains(&v))
557                    {
558                        return ConditionResult::True;
559                    }
560                }
561            }
562            return ConditionResult::False;
563        }
564        // Fallback: message-wide search
565        self.has_qualified_value(tag, qual_elem, qualifier, value_elem, value_comp, values)
566    }
567
568    // --- Parent-child group navigation helpers ---
569
570    /// Pattern A: Check if parent group instances matching a qualifier have a child
571    /// group containing a specific qualifier.
572    ///
573    /// Example: "In the SG8 with SEQ+Z98, does its SG10 child have CCI+Z23?"
574    ///
575    /// Falls back to message-wide search if no navigator is available.
576    #[allow(clippy::too_many_arguments)]
577    pub fn filtered_parent_child_has_qualifier(
578        &self,
579        parent_path: &[&str],
580        parent_tag: &str,
581        parent_elem: usize,
582        parent_qual: &str,
583        child_group_id: &str,
584        child_tag: &str,
585        child_elem: usize,
586        child_qual: &str,
587    ) -> ConditionResult {
588        let parent_count = self.group_instance_count(parent_path);
589        if parent_count > 0 {
590            for pi in 0..parent_count {
591                // Check if this parent instance has the required qualifier
592                let parent_segs = self.find_segments_with_qualifier_in_group(
593                    parent_tag,
594                    parent_elem,
595                    parent_qual,
596                    parent_path,
597                    pi,
598                );
599                if parent_segs.is_empty() {
600                    continue;
601                }
602                // Check child group instances for the child qualifier
603                let child_count = self.child_group_instance_count(parent_path, pi, child_group_id);
604                for ci in 0..child_count {
605                    let child_segs = self.find_segments_in_child_group(
606                        child_tag,
607                        parent_path,
608                        pi,
609                        child_group_id,
610                        ci,
611                    );
612                    if child_segs.iter().any(|s| {
613                        s.elements
614                            .get(child_elem)
615                            .and_then(|e| e.first())
616                            .is_some_and(|v| v == child_qual)
617                    }) {
618                        return ConditionResult::True;
619                    }
620                }
621            }
622            return ConditionResult::False;
623        }
624        // Fallback: message-wide — check both qualifiers exist independently
625        let has_parent = !self
626            .find_segments_with_qualifier(parent_tag, parent_elem, parent_qual)
627            .is_empty();
628        let has_child = !self
629            .find_segments_with_qualifier(child_tag, child_elem, child_qual)
630            .is_empty();
631        ConditionResult::from(has_parent && has_child)
632    }
633
634    /// Pattern B: Check if any group instance has one qualifier present but another absent.
635    ///
636    /// Example: "In any SG8, SEQ+Z59 is present but CCI+11 is absent"
637    ///
638    /// Falls back to message-wide search if no navigator is available.
639    #[allow(clippy::too_many_arguments)]
640    pub fn any_group_has_qualifier_without(
641        &self,
642        present_tag: &str,
643        present_elem: usize,
644        present_qual: &str,
645        absent_tag: &str,
646        absent_elem: usize,
647        absent_qual: &str,
648        group_path: &[&str],
649    ) -> ConditionResult {
650        let instance_count = self.group_instance_count(group_path);
651        if instance_count > 0 {
652            for i in 0..instance_count {
653                let has_present = !self
654                    .find_segments_with_qualifier_in_group(
655                        present_tag,
656                        present_elem,
657                        present_qual,
658                        group_path,
659                        i,
660                    )
661                    .is_empty();
662                let has_absent = !self
663                    .find_segments_with_qualifier_in_group(
664                        absent_tag,
665                        absent_elem,
666                        absent_qual,
667                        group_path,
668                        i,
669                    )
670                    .is_empty();
671                if has_present && !has_absent {
672                    return ConditionResult::True;
673                }
674            }
675            return ConditionResult::False;
676        }
677        // Fallback: message-wide
678        let has_present = !self
679            .find_segments_with_qualifier(present_tag, present_elem, present_qual)
680            .is_empty();
681        let has_absent = !self
682            .find_segments_with_qualifier(absent_tag, absent_elem, absent_qual)
683            .is_empty();
684        ConditionResult::from(has_present && !has_absent)
685    }
686
687    /// Pattern C helper: Collect all values at a specific element+component across group instances.
688    ///
689    /// Returns `(instance_index, value)` pairs for non-empty values.
690    pub fn collect_group_values(
691        &self,
692        tag: &str,
693        elem: usize,
694        comp: usize,
695        group_path: &[&str],
696    ) -> Vec<(usize, String)> {
697        let instance_count = self.group_instance_count(group_path);
698        let mut results = Vec::new();
699        for i in 0..instance_count {
700            if let Some(val) = self
701                .navigator
702                .and_then(|nav| nav.extract_value_in_group(tag, elem, comp, group_path, i))
703            {
704                if !val.is_empty() {
705                    results.push((i, val));
706                }
707            }
708        }
709        results
710    }
711
712    /// Pattern C: Check if a value from one group path matches a value in another group path.
713    ///
714    /// Example: "Zeitraum-ID in SG6 RFF+Z49 matches reference in SG8 SEQ.c286"
715    ///
716    /// Finds qualified segments in source_path, extracts their value, then checks if
717    /// any instance in target_path has the same value at the target position.
718    ///
719    /// Falls back to message-wide search if no navigator is available.
720    #[allow(clippy::too_many_arguments)]
721    pub fn groups_share_qualified_value(
722        &self,
723        source_tag: &str,
724        source_qual_elem: usize,
725        source_qual: &str,
726        source_value_elem: usize,
727        source_value_comp: usize,
728        source_path: &[&str],
729        target_tag: &str,
730        target_elem: usize,
731        target_comp: usize,
732        target_path: &[&str],
733    ) -> ConditionResult {
734        let source_count = self.group_instance_count(source_path);
735        let target_count = self.group_instance_count(target_path);
736        if source_count > 0 && target_count > 0 {
737            // Collect source values from qualified segments
738            let mut source_values = Vec::new();
739            for si in 0..source_count {
740                let segs = self.find_segments_with_qualifier_in_group(
741                    source_tag,
742                    source_qual_elem,
743                    source_qual,
744                    source_path,
745                    si,
746                );
747                for seg in &segs {
748                    if let Some(val) = seg
749                        .elements
750                        .get(source_value_elem)
751                        .and_then(|e| e.get(source_value_comp))
752                    {
753                        if !val.is_empty() {
754                            source_values.push(val.clone());
755                        }
756                    }
757                }
758            }
759            if source_values.is_empty() {
760                return ConditionResult::Unknown;
761            }
762            // Check if any target instance has a matching value
763            let target_values =
764                self.collect_group_values(target_tag, target_elem, target_comp, target_path);
765            for (_, tv) in &target_values {
766                if source_values.iter().any(|sv| sv == tv) {
767                    return ConditionResult::True;
768                }
769            }
770            return ConditionResult::False;
771        }
772        // Fallback: message-wide search
773        let source_segs =
774            self.find_segments_with_qualifier(source_tag, source_qual_elem, source_qual);
775        let source_vals: Vec<&str> = source_segs
776            .iter()
777            .filter_map(|s| {
778                s.elements
779                    .get(source_value_elem)
780                    .and_then(|e| e.get(source_value_comp))
781                    .map(|v| v.as_str())
782            })
783            .filter(|v| !v.is_empty())
784            .collect();
785        if source_vals.is_empty() {
786            return ConditionResult::Unknown;
787        }
788        let target_segs = self.find_segments(target_tag);
789        let has_match = target_segs.iter().any(|s| {
790            s.elements
791                .get(target_elem)
792                .and_then(|e| e.get(target_comp))
793                .map(|v| v.as_str())
794                .is_some_and(|v| source_vals.contains(&v))
795        });
796        ConditionResult::from(has_match)
797    }
798
799    /// Group-scoped co-occurrence check: two segment conditions must both be true
800    /// in the same group instance.
801    ///
802    /// For each group instance, checks that:
803    /// 1. A segment with `tag_a` has `elements[elem_a][0]` in `quals_a`
804    /// 2. A segment with `tag_b` has `elements[elem_b][comp_b]` in `vals_b`
805    ///
806    /// Falls back to message-wide search.
807    #[allow(clippy::too_many_arguments)]
808    pub fn any_group_has_co_occurrence(
809        &self,
810        tag_a: &str,
811        elem_a: usize,
812        quals_a: &[&str],
813        tag_b: &str,
814        elem_b: usize,
815        comp_b: usize,
816        vals_b: &[&str],
817        group_path: &[&str],
818    ) -> ConditionResult {
819        let instance_count = self.group_instance_count(group_path);
820        if instance_count > 0 {
821            for i in 0..instance_count {
822                let a_present = self
823                    .find_segments_in_group(tag_a, group_path, i)
824                    .iter()
825                    .any(|seg| {
826                        seg.elements
827                            .get(elem_a)
828                            .and_then(|e| e.first())
829                            .map(|s| s.as_str())
830                            .is_some_and(|v| quals_a.contains(&v))
831                    });
832                let b_present = self
833                    .find_segments_in_group(tag_b, group_path, i)
834                    .iter()
835                    .any(|seg| {
836                        seg.elements
837                            .get(elem_b)
838                            .and_then(|e| e.get(comp_b))
839                            .map(|s| s.as_str())
840                            .is_some_and(|v| vals_b.contains(&v))
841                    });
842                if a_present && b_present {
843                    return ConditionResult::True;
844                }
845            }
846            return ConditionResult::False;
847        }
848        // Fallback: message-wide
849        let a_found = self.find_segments(tag_a).iter().any(|seg| {
850            seg.elements
851                .get(elem_a)
852                .and_then(|e| e.first())
853                .map(|s| s.as_str())
854                .is_some_and(|v| quals_a.contains(&v))
855        });
856        if !a_found {
857            return ConditionResult::False;
858        }
859        let b_found = self.find_segments(tag_b).iter().any(|seg| {
860            seg.elements
861                .get(elem_b)
862                .and_then(|e| e.get(comp_b))
863                .map(|s| s.as_str())
864                .is_some_and(|v| vals_b.contains(&v))
865        });
866        ConditionResult::from(b_found)
867    }
868
869    // --- Multi-element matching helpers ---
870
871    /// Check if any segment with the given tag has ALL specified element/component values.
872    ///
873    /// Each check is `(element_index, component_index, expected_value)`.
874    /// Returns `True` if a matching segment exists, `False` if segments exist but none match,
875    /// `Unknown` if no segments with the tag exist.
876    ///
877    /// Example: `ctx.has_segment_matching("STS", &[(0, 0, "Z20"), (1, 0, "Z32"), (2, 0, "A99")])`
878    pub fn has_segment_matching(
879        &self,
880        tag: &str,
881        checks: &[(usize, usize, &str)],
882    ) -> ConditionResult {
883        let segments = self.find_segments(tag);
884        if segments.is_empty() {
885            return ConditionResult::Unknown;
886        }
887        let found = segments.iter().any(|seg| {
888            checks.iter().all(|(elem, comp, val)| {
889                seg.elements
890                    .get(*elem)
891                    .and_then(|e| e.get(*comp))
892                    .is_some_and(|v| v == val)
893            })
894        });
895        ConditionResult::from(found)
896    }
897
898    /// Group-scoped multi-element match with message-wide fallback.
899    ///
900    /// Checks if any group instance at `group_path` contains a segment with `tag`
901    /// where ALL element/component checks match.
902    pub fn has_segment_matching_in_group(
903        &self,
904        tag: &str,
905        checks: &[(usize, usize, &str)],
906        group_path: &[&str],
907    ) -> ConditionResult {
908        let instance_count = self.group_instance_count(group_path);
909        if instance_count > 0 {
910            for i in 0..instance_count {
911                let segs = self.find_segments_in_group(tag, group_path, i);
912                if segs.iter().any(|seg| {
913                    checks.iter().all(|(elem, comp, val)| {
914                        seg.elements
915                            .get(*elem)
916                            .and_then(|e| e.get(*comp))
917                            .is_some_and(|v| v == val)
918                    })
919                }) {
920                    return ConditionResult::True;
921                }
922            }
923            return ConditionResult::False;
924        }
925        // Fallback: message-wide
926        self.has_segment_matching(tag, checks)
927    }
928
929    // --- DTM date comparison helpers ---
930
931    /// Check if a DTM segment with the given qualifier has a value >= threshold.
932    ///
933    /// Both the DTM value and threshold should be in EDIFACT format 303 (CCYYMMDDHHMM).
934    /// Returns `Unknown` if no DTM with the qualifier exists.
935    pub fn dtm_ge(&self, qualifier: &str, threshold: &str) -> ConditionResult {
936        let segs = self.find_segments_with_qualifier("DTM", 0, qualifier);
937        match segs.first() {
938            Some(dtm) => match dtm.elements.first().and_then(|e| e.get(1)) {
939                Some(val) => ConditionResult::from(val.as_str() >= threshold),
940                None => ConditionResult::Unknown,
941            },
942            None => ConditionResult::Unknown,
943        }
944    }
945
946    /// Check if a DTM segment with the given qualifier has a value < threshold.
947    pub fn dtm_lt(&self, qualifier: &str, threshold: &str) -> ConditionResult {
948        let segs = self.find_segments_with_qualifier("DTM", 0, qualifier);
949        match segs.first() {
950            Some(dtm) => match dtm.elements.first().and_then(|e| e.get(1)) {
951                Some(val) => ConditionResult::from((val.as_str()) < threshold),
952                None => ConditionResult::Unknown,
953            },
954            None => ConditionResult::Unknown,
955        }
956    }
957
958    /// Check if a DTM segment with the given qualifier has a value <= threshold.
959    pub fn dtm_le(&self, qualifier: &str, threshold: &str) -> ConditionResult {
960        let segs = self.find_segments_with_qualifier("DTM", 0, qualifier);
961        match segs.first() {
962            Some(dtm) => match dtm.elements.first().and_then(|e| e.get(1)) {
963                Some(val) => ConditionResult::from(val.as_str() <= threshold),
964                None => ConditionResult::Unknown,
965            },
966            None => ConditionResult::Unknown,
967        }
968    }
969
970    // --- Group-scoped cardinality helpers ---
971
972    /// Count segments matching tag + qualifier within a group path (across all instances).
973    ///
974    /// Returns the total count across all group instances.
975    /// Falls back to message-wide count if no navigator.
976    pub fn count_qualified_in_group(
977        &self,
978        tag: &str,
979        element_index: usize,
980        qualifier: &str,
981        group_path: &[&str],
982    ) -> usize {
983        let instance_count = self.group_instance_count(group_path);
984        if instance_count > 0 {
985            let mut total = 0;
986            for i in 0..instance_count {
987                total += self
988                    .find_segments_with_qualifier_in_group(
989                        tag,
990                        element_index,
991                        qualifier,
992                        group_path,
993                        i,
994                    )
995                    .len();
996            }
997            return total;
998        }
999        // Fallback: message-wide
1000        self.find_segments_with_qualifier(tag, element_index, qualifier)
1001            .len()
1002    }
1003
1004    /// Count segments matching tag (any qualifier) within a group path.
1005    pub fn count_in_group(&self, tag: &str, group_path: &[&str]) -> usize {
1006        let instance_count = self.group_instance_count(group_path);
1007        if instance_count > 0 {
1008            let mut total = 0;
1009            for i in 0..instance_count {
1010                total += self.find_segments_in_group(tag, group_path, i).len();
1011            }
1012            return total;
1013        }
1014        // Fallback: message-wide
1015        self.find_segments(tag).len()
1016    }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::super::evaluator::NoOpExternalProvider;
1022    use super::*;
1023    use mig_types::navigator::GroupNavigator;
1024
1025    fn make_segment(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
1026        OwnedSegment {
1027            id: id.to_string(),
1028            elements: elements
1029                .into_iter()
1030                .map(|e| e.into_iter().map(|c| c.to_string()).collect())
1031                .collect(),
1032            segment_number: 0,
1033        }
1034    }
1035
1036    // --- Mock navigator for testing ---
1037    struct MockGroupNavigator {
1038        groups: Vec<(Vec<String>, usize, Vec<OwnedSegment>)>,
1039        /// Children: (parent_path, parent_instance, child_group_id, child_instance, segments)
1040        children: Vec<(Vec<String>, usize, String, usize, Vec<OwnedSegment>)>,
1041    }
1042
1043    impl MockGroupNavigator {
1044        fn new() -> Self {
1045            Self {
1046                groups: vec![],
1047                children: vec![],
1048            }
1049        }
1050        fn with_group(mut self, path: &[&str], instance: usize, segs: Vec<OwnedSegment>) -> Self {
1051            self.groups
1052                .push((path.iter().map(|s| s.to_string()).collect(), instance, segs));
1053            self
1054        }
1055        fn with_child_group(
1056            mut self,
1057            parent_path: &[&str],
1058            parent_instance: usize,
1059            child_id: &str,
1060            child_instance: usize,
1061            segs: Vec<OwnedSegment>,
1062        ) -> Self {
1063            self.children.push((
1064                parent_path.iter().map(|s| s.to_string()).collect(),
1065                parent_instance,
1066                child_id.to_string(),
1067                child_instance,
1068                segs,
1069            ));
1070            self
1071        }
1072        fn find_instance(&self, group_path: &[&str], idx: usize) -> Option<&[OwnedSegment]> {
1073            self.groups
1074                .iter()
1075                .find(|(p, i, _)| {
1076                    let ps: Vec<&str> = p.iter().map(|s| s.as_str()).collect();
1077                    ps.as_slice() == group_path && *i == idx
1078                })
1079                .map(|(_, _, segs)| segs.as_slice())
1080        }
1081    }
1082
1083    impl GroupNavigator for MockGroupNavigator {
1084        fn find_segments_in_group(
1085            &self,
1086            segment_id: &str,
1087            group_path: &[&str],
1088            instance_index: usize,
1089        ) -> Vec<OwnedSegment> {
1090            self.find_instance(group_path, instance_index)
1091                .map(|segs| {
1092                    segs.iter()
1093                        .filter(|s| s.id == segment_id)
1094                        .cloned()
1095                        .collect()
1096                })
1097                .unwrap_or_default()
1098        }
1099        fn find_segments_with_qualifier_in_group(
1100            &self,
1101            segment_id: &str,
1102            element_index: usize,
1103            qualifier: &str,
1104            group_path: &[&str],
1105            instance_index: usize,
1106        ) -> Vec<OwnedSegment> {
1107            self.find_segments_in_group(segment_id, group_path, instance_index)
1108                .into_iter()
1109                .filter(|s| {
1110                    s.elements
1111                        .get(element_index)
1112                        .and_then(|e| e.first())
1113                        .is_some_and(|v| v == qualifier)
1114                })
1115                .collect()
1116        }
1117        fn group_instance_count(&self, group_path: &[&str]) -> usize {
1118            self.groups
1119                .iter()
1120                .filter(|(p, _, _)| {
1121                    let ps: Vec<&str> = p.iter().map(|s| s.as_str()).collect();
1122                    ps.as_slice() == group_path
1123                })
1124                .count()
1125        }
1126        fn child_group_instance_count(
1127            &self,
1128            parent_path: &[&str],
1129            parent_instance: usize,
1130            child_group_id: &str,
1131        ) -> usize {
1132            self.children
1133                .iter()
1134                .filter(|(pp, pi, cid, _, _)| {
1135                    let ps: Vec<&str> = pp.iter().map(|s| s.as_str()).collect();
1136                    ps.as_slice() == parent_path && *pi == parent_instance && cid == child_group_id
1137                })
1138                .count()
1139        }
1140        fn find_segments_in_child_group(
1141            &self,
1142            segment_id: &str,
1143            parent_path: &[&str],
1144            parent_instance: usize,
1145            child_group_id: &str,
1146            child_instance: usize,
1147        ) -> Vec<OwnedSegment> {
1148            self.children
1149                .iter()
1150                .find(|(pp, pi, cid, ci, _)| {
1151                    let ps: Vec<&str> = pp.iter().map(|s| s.as_str()).collect();
1152                    ps.as_slice() == parent_path
1153                        && *pi == parent_instance
1154                        && cid == child_group_id
1155                        && *ci == child_instance
1156                })
1157                .map(|(_, _, _, _, segs)| {
1158                    segs.iter()
1159                        .filter(|s| s.id == segment_id)
1160                        .cloned()
1161                        .collect()
1162                })
1163                .unwrap_or_default()
1164        }
1165        fn extract_value_in_group(
1166            &self,
1167            segment_id: &str,
1168            element_index: usize,
1169            component_index: usize,
1170            group_path: &[&str],
1171            instance_index: usize,
1172        ) -> Option<String> {
1173            let segs = self.find_instance(group_path, instance_index)?;
1174            let seg = segs.iter().find(|s| s.id == segment_id)?;
1175            seg.elements
1176                .get(element_index)?
1177                .get(component_index)
1178                .cloned()
1179        }
1180    }
1181
1182    #[test]
1183    fn test_find_segment() {
1184        let segments = vec![
1185            make_segment("UNH", vec![vec!["test"]]),
1186            make_segment("NAD", vec![vec!["MS"], vec!["123456789", "", "293"]]),
1187        ];
1188        let external = NoOpExternalProvider;
1189        let ctx = EvaluationContext::new("11001", &external, &segments);
1190
1191        assert!(ctx.find_segment("NAD").is_some());
1192        assert!(ctx.find_segment("DTM").is_none());
1193    }
1194
1195    #[test]
1196    fn test_find_segments_with_qualifier() {
1197        let segments = vec![
1198            make_segment("NAD", vec![vec!["MS"], vec!["111"]]),
1199            make_segment("NAD", vec![vec!["MR"], vec!["222"]]),
1200            make_segment("NAD", vec![vec!["MS"], vec!["333"]]),
1201        ];
1202        let external = NoOpExternalProvider;
1203        let ctx = EvaluationContext::new("11001", &external, &segments);
1204
1205        let ms_nads = ctx.find_segments_with_qualifier("NAD", 0, "MS");
1206        assert_eq!(ms_nads.len(), 2);
1207    }
1208
1209    #[test]
1210    fn test_has_segment() {
1211        let segments = vec![make_segment("UNH", vec![vec!["test"]])];
1212        let external = NoOpExternalProvider;
1213        let ctx = EvaluationContext::new("11001", &external, &segments);
1214
1215        assert!(ctx.has_segment("UNH"));
1216        assert!(!ctx.has_segment("NAD"));
1217    }
1218
1219    // --- Group navigator tests ---
1220
1221    #[test]
1222    fn test_no_navigator_group_find_returns_empty() {
1223        let segments = vec![make_segment("SEQ", vec![vec!["Z98"]])];
1224        let external = NoOpExternalProvider;
1225        let ctx = EvaluationContext::new("55001", &external, &segments);
1226        assert!(ctx
1227            .find_segments_in_group("SEQ", &["SG4", "SG8"], 0)
1228            .is_empty());
1229    }
1230
1231    #[test]
1232    fn test_no_navigator_group_instance_count_zero() {
1233        let external = NoOpExternalProvider;
1234        let ctx = EvaluationContext::new("55001", &external, &[]);
1235        assert_eq!(ctx.group_instance_count(&["SG4"]), 0);
1236    }
1237
1238    #[test]
1239    fn test_with_navigator_finds_segments_in_group() {
1240        let external = NoOpExternalProvider;
1241        let nav = MockGroupNavigator::new().with_group(
1242            &["SG4", "SG8"],
1243            0,
1244            vec![
1245                make_segment("SEQ", vec![vec!["Z98"]]),
1246                make_segment("CCI", vec![vec!["Z30"], vec![], vec!["Z07"]]),
1247            ],
1248        );
1249        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1250        let result = ctx.find_segments_in_group("SEQ", &["SG4", "SG8"], 0);
1251        assert_eq!(result.len(), 1);
1252        assert_eq!(result[0].id, "SEQ");
1253    }
1254
1255    #[test]
1256    fn test_with_navigator_qualifier_in_group() {
1257        let external = NoOpExternalProvider;
1258        let nav = MockGroupNavigator::new().with_group(
1259            &["SG4", "SG8"],
1260            0,
1261            vec![
1262                make_segment("SEQ", vec![vec!["Z98"]]),
1263                make_segment("SEQ", vec![vec!["Z01"]]),
1264            ],
1265        );
1266        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1267        let result = ctx.find_segments_with_qualifier_in_group("SEQ", 0, "Z98", &["SG4", "SG8"], 0);
1268        assert_eq!(result.len(), 1);
1269    }
1270
1271    #[test]
1272    fn test_group_instance_count_with_navigator() {
1273        let external = NoOpExternalProvider;
1274        let nav = MockGroupNavigator::new()
1275            .with_group(
1276                &["SG4", "SG8"],
1277                0,
1278                vec![make_segment("SEQ", vec![vec!["Z98"]])],
1279            )
1280            .with_group(
1281                &["SG4", "SG8"],
1282                1,
1283                vec![make_segment("SEQ", vec![vec!["Z01"]])],
1284            );
1285        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1286        assert_eq!(ctx.group_instance_count(&["SG4", "SG8"]), 2);
1287    }
1288
1289    #[test]
1290    fn test_has_segment_in_group() {
1291        let external = NoOpExternalProvider;
1292        let nav = MockGroupNavigator::new().with_group(
1293            &["SG4", "SG8"],
1294            0,
1295            vec![make_segment("SEQ", vec![vec!["Z98"]])],
1296        );
1297        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1298        assert!(ctx.has_segment_in_group("SEQ", &["SG4", "SG8"], 0));
1299        assert!(!ctx.has_segment_in_group("CCI", &["SG4", "SG8"], 0));
1300        assert!(!ctx.has_segment_in_group("SEQ", &["SG4", "SG5"], 0));
1301    }
1302
1303    // --- High-level helper tests ---
1304
1305    #[test]
1306    fn test_has_qualifier() {
1307        let segments = vec![
1308            make_segment("NAD", vec![vec!["MS"], vec!["111"]]),
1309            make_segment("NAD", vec![vec!["MR"], vec!["222"]]),
1310        ];
1311        let external = NoOpExternalProvider;
1312        let ctx = EvaluationContext::new("11001", &external, &segments);
1313
1314        assert_eq!(ctx.has_qualifier("NAD", 0, "MS"), ConditionResult::True);
1315        assert_eq!(ctx.has_qualifier("NAD", 0, "DP"), ConditionResult::False);
1316    }
1317
1318    #[test]
1319    fn test_lacks_qualifier() {
1320        let segments = vec![make_segment("DTM", vec![vec!["92", "2025"]])];
1321        let external = NoOpExternalProvider;
1322        let ctx = EvaluationContext::new("11001", &external, &segments);
1323
1324        assert_eq!(ctx.lacks_qualifier("DTM", 0, "93"), ConditionResult::True);
1325        assert_eq!(ctx.lacks_qualifier("DTM", 0, "92"), ConditionResult::False);
1326    }
1327
1328    #[test]
1329    fn test_has_qualified_value() {
1330        let segments = vec![make_segment("STS", vec![vec!["7"], vec![], vec!["ZG9"]])];
1331        let external = NoOpExternalProvider;
1332        let ctx = EvaluationContext::new("55001", &external, &segments);
1333
1334        assert_eq!(
1335            ctx.has_qualified_value("STS", 0, "7", 2, 0, &["ZG9", "ZH1", "ZH2"]),
1336            ConditionResult::True,
1337        );
1338        assert_eq!(
1339            ctx.has_qualified_value("STS", 0, "7", 2, 0, &["E01"]),
1340            ConditionResult::False,
1341        );
1342        // No STS+E01 → Unknown
1343        assert_eq!(
1344            ctx.has_qualified_value("STS", 0, "E01", 2, 0, &["Z01"]),
1345            ConditionResult::Unknown,
1346        );
1347    }
1348
1349    #[test]
1350    fn test_any_group_has_qualifier() {
1351        let external = NoOpExternalProvider;
1352        let nav = MockGroupNavigator::new()
1353            .with_group(
1354                &["SG4", "SG8"],
1355                0,
1356                vec![make_segment("SEQ", vec![vec!["Z01"]])],
1357            )
1358            .with_group(
1359                &["SG4", "SG8"],
1360                1,
1361                vec![make_segment("SEQ", vec![vec!["Z98"]])],
1362            );
1363        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1364
1365        assert_eq!(
1366            ctx.any_group_has_qualifier("SEQ", 0, "Z98", &["SG4", "SG8"]),
1367            ConditionResult::True,
1368        );
1369        assert_eq!(
1370            ctx.any_group_has_qualifier("SEQ", 0, "Z99", &["SG4", "SG8"]),
1371            ConditionResult::False,
1372        );
1373    }
1374
1375    #[test]
1376    fn test_any_group_has_qualifier_fallback() {
1377        // No navigator — falls back to message-wide search
1378        let segments = vec![make_segment("SEQ", vec![vec!["Z98"]])];
1379        let external = NoOpExternalProvider;
1380        let ctx = EvaluationContext::new("55001", &external, &segments);
1381
1382        assert_eq!(
1383            ctx.any_group_has_qualifier("SEQ", 0, "Z98", &["SG4", "SG8"]),
1384            ConditionResult::True,
1385        );
1386    }
1387
1388    #[test]
1389    fn test_any_group_has_any_qualifier() {
1390        let external = NoOpExternalProvider;
1391        let nav = MockGroupNavigator::new().with_group(
1392            &["SG4", "SG8"],
1393            0,
1394            vec![make_segment("SEQ", vec![vec!["Z80"]])],
1395        );
1396        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1397
1398        assert_eq!(
1399            ctx.any_group_has_any_qualifier("SEQ", 0, &["Z01", "Z80", "Z81"], &["SG4", "SG8"]),
1400            ConditionResult::True,
1401        );
1402        assert_eq!(
1403            ctx.any_group_has_any_qualifier("SEQ", 0, &["Z98"], &["SG4", "SG8"]),
1404            ConditionResult::False,
1405        );
1406    }
1407
1408    #[test]
1409    fn test_any_group_has_co_occurrence() {
1410        let external = NoOpExternalProvider;
1411        let nav = MockGroupNavigator::new().with_group(
1412            &["SG4", "SG8"],
1413            0,
1414            vec![
1415                make_segment("SEQ", vec![vec!["Z01"]]),
1416                make_segment("CCI", vec![vec!["Z30"], vec![], vec!["Z07"]]),
1417            ],
1418        );
1419        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1420
1421        assert_eq!(
1422            ctx.any_group_has_co_occurrence(
1423                "SEQ",
1424                0,
1425                &["Z01"],
1426                "CCI",
1427                2,
1428                0,
1429                &["Z07"],
1430                &["SG4", "SG8"],
1431            ),
1432            ConditionResult::True,
1433        );
1434        // Wrong CCI value
1435        assert_eq!(
1436            ctx.any_group_has_co_occurrence(
1437                "SEQ",
1438                0,
1439                &["Z01"],
1440                "CCI",
1441                2,
1442                0,
1443                &["ZC0"],
1444                &["SG4", "SG8"],
1445            ),
1446            ConditionResult::False,
1447        );
1448    }
1449
1450    // --- Parent-child navigation tests ---
1451
1452    #[test]
1453    fn test_filtered_parent_child_has_qualifier() {
1454        let external = NoOpExternalProvider;
1455        // SG8[0] has SEQ+Z98, with SG10 child having CCI+Z23
1456        // SG8[1] has SEQ+Z01, no SG10 children
1457        let nav = MockGroupNavigator::new()
1458            .with_group(
1459                &["SG4", "SG8"],
1460                0,
1461                vec![make_segment("SEQ", vec![vec!["Z98"]])],
1462            )
1463            .with_group(
1464                &["SG4", "SG8"],
1465                1,
1466                vec![make_segment("SEQ", vec![vec!["Z01"]])],
1467            )
1468            .with_child_group(
1469                &["SG4", "SG8"],
1470                0,
1471                "SG10",
1472                0,
1473                vec![make_segment("CCI", vec![vec!["Z23"]])],
1474            );
1475        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1476
1477        // SG8 with SEQ+Z98 has SG10 child with CCI+Z23 → True
1478        assert_eq!(
1479            ctx.filtered_parent_child_has_qualifier(
1480                &["SG4", "SG8"],
1481                "SEQ",
1482                0,
1483                "Z98",
1484                "SG10",
1485                "CCI",
1486                0,
1487                "Z23",
1488            ),
1489            ConditionResult::True,
1490        );
1491        // SG8 with SEQ+Z01 has no SG10 children → False
1492        assert_eq!(
1493            ctx.filtered_parent_child_has_qualifier(
1494                &["SG4", "SG8"],
1495                "SEQ",
1496                0,
1497                "Z01",
1498                "SG10",
1499                "CCI",
1500                0,
1501                "Z23",
1502            ),
1503            ConditionResult::False,
1504        );
1505        // Wrong child qualifier → False
1506        assert_eq!(
1507            ctx.filtered_parent_child_has_qualifier(
1508                &["SG4", "SG8"],
1509                "SEQ",
1510                0,
1511                "Z98",
1512                "SG10",
1513                "CCI",
1514                0,
1515                "Z99",
1516            ),
1517            ConditionResult::False,
1518        );
1519    }
1520
1521    #[test]
1522    fn test_filtered_parent_child_fallback() {
1523        // No navigator — falls back to message-wide
1524        let segments = vec![
1525            make_segment("SEQ", vec![vec!["Z98"]]),
1526            make_segment("CCI", vec![vec!["Z23"]]),
1527        ];
1528        let external = NoOpExternalProvider;
1529        let ctx = EvaluationContext::new("55001", &external, &segments);
1530
1531        assert_eq!(
1532            ctx.filtered_parent_child_has_qualifier(
1533                &["SG4", "SG8"],
1534                "SEQ",
1535                0,
1536                "Z98",
1537                "SG10",
1538                "CCI",
1539                0,
1540                "Z23",
1541            ),
1542            ConditionResult::True,
1543        );
1544        // Missing child qualifier in message-wide → False
1545        assert_eq!(
1546            ctx.filtered_parent_child_has_qualifier(
1547                &["SG4", "SG8"],
1548                "SEQ",
1549                0,
1550                "Z98",
1551                "SG10",
1552                "CCI",
1553                0,
1554                "Z99",
1555            ),
1556            ConditionResult::False,
1557        );
1558    }
1559
1560    #[test]
1561    fn test_any_group_has_qualifier_without() {
1562        let external = NoOpExternalProvider;
1563        // SG8[0]: SEQ+Z59 present, CCI+11 absent
1564        // SG8[1]: SEQ+Z01 present, CCI+11 present
1565        let nav = MockGroupNavigator::new()
1566            .with_group(
1567                &["SG4", "SG8"],
1568                0,
1569                vec![make_segment("SEQ", vec![vec!["Z59"]])],
1570            )
1571            .with_group(
1572                &["SG4", "SG8"],
1573                1,
1574                vec![
1575                    make_segment("SEQ", vec![vec!["Z01"]]),
1576                    make_segment("CCI", vec![vec!["11"]]),
1577                ],
1578            );
1579        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1580
1581        // SG8[0] has SEQ+Z59 without CCI+11 → True
1582        assert_eq!(
1583            ctx.any_group_has_qualifier_without("SEQ", 0, "Z59", "CCI", 0, "11", &["SG4", "SG8"]),
1584            ConditionResult::True,
1585        );
1586        // Looking for SEQ+Z01 without CCI+11 → False (SG8[1] has both)
1587        assert_eq!(
1588            ctx.any_group_has_qualifier_without("SEQ", 0, "Z01", "CCI", 0, "11", &["SG4", "SG8"]),
1589            ConditionResult::False,
1590        );
1591        // Looking for SEQ+Z99 (doesn't exist) → False
1592        assert_eq!(
1593            ctx.any_group_has_qualifier_without("SEQ", 0, "Z99", "CCI", 0, "11", &["SG4", "SG8"]),
1594            ConditionResult::False,
1595        );
1596    }
1597
1598    #[test]
1599    fn test_any_group_has_qualifier_without_fallback() {
1600        let segments = vec![make_segment("SEQ", vec![vec!["Z59"]])];
1601        let external = NoOpExternalProvider;
1602        let ctx = EvaluationContext::new("55001", &external, &segments);
1603
1604        // Message-wide: SEQ+Z59 present, CCI+11 absent → True
1605        assert_eq!(
1606            ctx.any_group_has_qualifier_without("SEQ", 0, "Z59", "CCI", 0, "11", &["SG4", "SG8"]),
1607            ConditionResult::True,
1608        );
1609    }
1610
1611    #[test]
1612    fn test_collect_group_values() {
1613        let external = NoOpExternalProvider;
1614        let nav = MockGroupNavigator::new()
1615            .with_group(
1616                &["SG4", "SG6"],
1617                0,
1618                vec![make_segment("RFF", vec![vec!["Z49", "REF001"]])],
1619            )
1620            .with_group(
1621                &["SG4", "SG6"],
1622                1,
1623                vec![make_segment("RFF", vec![vec!["Z49", "REF002"]])],
1624            );
1625        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1626
1627        let values = ctx.collect_group_values("RFF", 0, 1, &["SG4", "SG6"]);
1628        assert_eq!(values.len(), 2);
1629        assert_eq!(values[0], (0, "REF001".to_string()));
1630        assert_eq!(values[1], (1, "REF002".to_string()));
1631    }
1632
1633    #[test]
1634    fn test_groups_share_qualified_value() {
1635        let external = NoOpExternalProvider;
1636        // SG6[0]: RFF+Z49 with value "TS001"
1637        // SG8[0]: SEQ with c286 value "TS001" (matches)
1638        // SG8[1]: SEQ with c286 value "TS999" (no match)
1639        let nav = MockGroupNavigator::new()
1640            .with_group(
1641                &["SG4", "SG6"],
1642                0,
1643                vec![make_segment("RFF", vec![vec!["Z49", "TS001"]])],
1644            )
1645            .with_group(
1646                &["SG4", "SG8"],
1647                0,
1648                vec![make_segment("SEQ", vec![vec!["Z98"], vec!["TS001"]])],
1649            )
1650            .with_group(
1651                &["SG4", "SG8"],
1652                1,
1653                vec![make_segment("SEQ", vec![vec!["Z01"], vec!["TS999"]])],
1654            );
1655        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1656
1657        // RFF+Z49 value "TS001" matches SEQ value at [1][0] → True
1658        assert_eq!(
1659            ctx.groups_share_qualified_value(
1660                "RFF",
1661                0,
1662                "Z49",
1663                0,
1664                1,
1665                &["SG4", "SG6"],
1666                "SEQ",
1667                1,
1668                0,
1669                &["SG4", "SG8"],
1670            ),
1671            ConditionResult::True,
1672        );
1673    }
1674
1675    #[test]
1676    fn test_groups_share_qualified_value_no_match() {
1677        let external = NoOpExternalProvider;
1678        let nav = MockGroupNavigator::new()
1679            .with_group(
1680                &["SG4", "SG6"],
1681                0,
1682                vec![make_segment("RFF", vec![vec!["Z49", "TS001"]])],
1683            )
1684            .with_group(
1685                &["SG4", "SG8"],
1686                0,
1687                vec![make_segment("SEQ", vec![vec!["Z98"], vec!["TS999"]])],
1688            );
1689        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1690
1691        // No matching value → False
1692        assert_eq!(
1693            ctx.groups_share_qualified_value(
1694                "RFF",
1695                0,
1696                "Z49",
1697                0,
1698                1,
1699                &["SG4", "SG6"],
1700                "SEQ",
1701                1,
1702                0,
1703                &["SG4", "SG8"],
1704            ),
1705            ConditionResult::False,
1706        );
1707    }
1708
1709    #[test]
1710    fn test_groups_share_qualified_value_no_source() {
1711        let external = NoOpExternalProvider;
1712        // No RFF+Z49 at all
1713        let nav = MockGroupNavigator::new()
1714            .with_group(
1715                &["SG4", "SG6"],
1716                0,
1717                vec![make_segment("RFF", vec![vec!["Z13", "55001"]])],
1718            )
1719            .with_group(
1720                &["SG4", "SG8"],
1721                0,
1722                vec![make_segment("SEQ", vec![vec!["Z98"], vec!["TS001"]])],
1723            );
1724        let ctx = EvaluationContext::with_navigator("55001", &external, &[], &nav);
1725
1726        // No source qualifier match → Unknown
1727        assert_eq!(
1728            ctx.groups_share_qualified_value(
1729                "RFF",
1730                0,
1731                "Z49",
1732                0,
1733                1,
1734                &["SG4", "SG6"],
1735                "SEQ",
1736                1,
1737                0,
1738                &["SG4", "SG8"],
1739            ),
1740            ConditionResult::Unknown,
1741        );
1742    }
1743
1744    #[test]
1745    fn test_groups_share_qualified_value_fallback() {
1746        // No navigator — falls back to message-wide
1747        let segments = vec![
1748            make_segment("RFF", vec![vec!["Z49", "TS001"]]),
1749            make_segment("SEQ", vec![vec!["Z98"], vec!["TS001"]]),
1750        ];
1751        let external = NoOpExternalProvider;
1752        let ctx = EvaluationContext::new("55001", &external, &segments);
1753
1754        assert_eq!(
1755            ctx.groups_share_qualified_value(
1756                "RFF",
1757                0,
1758                "Z49",
1759                0,
1760                1,
1761                &["SG4", "SG6"],
1762                "SEQ",
1763                1,
1764                0,
1765                &["SG4", "SG8"],
1766            ),
1767            ConditionResult::True,
1768        );
1769    }
1770
1771    #[test]
1772    fn test_child_group_pass_throughs_no_navigator() {
1773        let external = NoOpExternalProvider;
1774        let ctx = EvaluationContext::new("55001", &external, &[]);
1775
1776        assert_eq!(
1777            ctx.child_group_instance_count(&["SG4", "SG8"], 0, "SG10"),
1778            0
1779        );
1780        assert!(ctx
1781            .find_segments_in_child_group("CCI", &["SG4", "SG8"], 0, "SG10", 0)
1782            .is_empty());
1783        assert_eq!(
1784            ctx.extract_value_in_group("SEQ", 0, 0, &["SG4", "SG8"], 0),
1785            None
1786        );
1787    }
1788
1789    // --- has_segment_matching tests ---
1790
1791    #[test]
1792    fn test_has_segment_matching_found() {
1793        let segments = vec![
1794            make_segment("STS", vec![vec!["7"], vec!["E01"], vec!["ZW4"]]),
1795            make_segment("STS", vec![vec!["Z20"], vec!["Z32"], vec!["A99"]]),
1796        ];
1797        let external = NoOpExternalProvider;
1798        let ctx = EvaluationContext::new("55001", &external, &segments);
1799
1800        assert_eq!(
1801            ctx.has_segment_matching("STS", &[(0, 0, "Z20"), (1, 0, "Z32"), (2, 0, "A99")]),
1802            ConditionResult::True,
1803        );
1804    }
1805
1806    #[test]
1807    fn test_has_segment_matching_not_found() {
1808        let segments = vec![make_segment(
1809            "STS",
1810            vec![vec!["7"], vec!["E01"], vec!["ZW4"]],
1811        )];
1812        let external = NoOpExternalProvider;
1813        let ctx = EvaluationContext::new("55001", &external, &segments);
1814
1815        assert_eq!(
1816            ctx.has_segment_matching("STS", &[(0, 0, "Z20"), (1, 0, "Z32")]),
1817            ConditionResult::False,
1818        );
1819    }
1820
1821    #[test]
1822    fn test_has_segment_matching_no_segments() {
1823        let external = NoOpExternalProvider;
1824        let ctx = EvaluationContext::new("55001", &external, &[]);
1825
1826        assert_eq!(
1827            ctx.has_segment_matching("STS", &[(0, 0, "Z20")]),
1828            ConditionResult::Unknown,
1829        );
1830    }
1831
1832    #[test]
1833    fn test_has_segment_matching_in_group() {
1834        let nav = MockGroupNavigator::new()
1835            .with_group(
1836                &["SG4"],
1837                0,
1838                vec![make_segment("STS", vec![vec!["7"], vec!["E01"]])],
1839            )
1840            .with_group(
1841                &["SG4"],
1842                1,
1843                vec![make_segment("STS", vec![vec!["Z20"], vec!["Z32"]])],
1844            );
1845        let segments = vec![
1846            make_segment("STS", vec![vec!["7"], vec!["E01"]]),
1847            make_segment("STS", vec![vec!["Z20"], vec!["Z32"]]),
1848        ];
1849        let external = NoOpExternalProvider;
1850        let ctx = EvaluationContext::with_navigator("55001", &external, &segments, &nav);
1851
1852        assert_eq!(
1853            ctx.has_segment_matching_in_group("STS", &[(0, 0, "Z20"), (1, 0, "Z32")], &["SG4"]),
1854            ConditionResult::True,
1855        );
1856    }
1857
1858    // --- DTM comparison tests ---
1859
1860    #[test]
1861    fn test_dtm_ge() {
1862        let segments = vec![make_segment(
1863            "DTM",
1864            vec![vec!["137", "202601010000", "303"]],
1865        )];
1866        let external = NoOpExternalProvider;
1867        let ctx = EvaluationContext::new("55001", &external, &segments);
1868
1869        assert_eq!(ctx.dtm_ge("137", "202601010000"), ConditionResult::True);
1870        assert_eq!(ctx.dtm_ge("137", "202501010000"), ConditionResult::True);
1871        assert_eq!(ctx.dtm_ge("137", "202701010000"), ConditionResult::False);
1872        assert_eq!(ctx.dtm_ge("999", "202601010000"), ConditionResult::Unknown);
1873    }
1874
1875    #[test]
1876    fn test_dtm_lt() {
1877        let segments = vec![make_segment(
1878            "DTM",
1879            vec![vec!["137", "202601010000", "303"]],
1880        )];
1881        let external = NoOpExternalProvider;
1882        let ctx = EvaluationContext::new("55001", &external, &segments);
1883
1884        assert_eq!(ctx.dtm_lt("137", "202701010000"), ConditionResult::True);
1885        assert_eq!(ctx.dtm_lt("137", "202601010000"), ConditionResult::False);
1886        assert_eq!(ctx.dtm_lt("137", "202501010000"), ConditionResult::False);
1887    }
1888
1889    #[test]
1890    fn test_dtm_le() {
1891        let segments = vec![make_segment(
1892            "DTM",
1893            vec![vec!["137", "202601010000", "303"]],
1894        )];
1895        let external = NoOpExternalProvider;
1896        let ctx = EvaluationContext::new("55001", &external, &segments);
1897
1898        assert_eq!(ctx.dtm_le("137", "202601010000"), ConditionResult::True);
1899        assert_eq!(ctx.dtm_le("137", "202701010000"), ConditionResult::True);
1900        assert_eq!(ctx.dtm_le("137", "202501010000"), ConditionResult::False);
1901    }
1902
1903    // --- Count helpers tests ---
1904
1905    #[test]
1906    fn test_count_qualified_in_group() {
1907        let nav = MockGroupNavigator::new()
1908            .with_group(
1909                &["SG4", "SG8"],
1910                0,
1911                vec![
1912                    make_segment("CCI", vec![vec!["Z23"]]),
1913                    make_segment("CCI", vec![vec!["Z30"]]),
1914                ],
1915            )
1916            .with_group(
1917                &["SG4", "SG8"],
1918                1,
1919                vec![make_segment("CCI", vec![vec!["Z23"]])],
1920            );
1921        let segments = vec![
1922            make_segment("CCI", vec![vec!["Z23"]]),
1923            make_segment("CCI", vec![vec!["Z30"]]),
1924            make_segment("CCI", vec![vec!["Z23"]]),
1925        ];
1926        let external = NoOpExternalProvider;
1927        let ctx = EvaluationContext::with_navigator("55001", &external, &segments, &nav);
1928
1929        assert_eq!(
1930            ctx.count_qualified_in_group("CCI", 0, "Z23", &["SG4", "SG8"]),
1931            2
1932        );
1933        assert_eq!(
1934            ctx.count_qualified_in_group("CCI", 0, "Z30", &["SG4", "SG8"]),
1935            1
1936        );
1937        assert_eq!(
1938            ctx.count_qualified_in_group("CCI", 0, "Z99", &["SG4", "SG8"]),
1939            0
1940        );
1941    }
1942
1943    #[test]
1944    fn test_count_in_group() {
1945        let nav = MockGroupNavigator::new()
1946            .with_group(
1947                &["SG4", "SG8"],
1948                0,
1949                vec![
1950                    make_segment("SEQ", vec![vec!["Z98"]]),
1951                    make_segment("CCI", vec![vec!["Z23"]]),
1952                ],
1953            )
1954            .with_group(
1955                &["SG4", "SG8"],
1956                1,
1957                vec![make_segment("SEQ", vec![vec!["Z01"]])],
1958            );
1959        let segments = vec![
1960            make_segment("SEQ", vec![vec!["Z98"]]),
1961            make_segment("CCI", vec![vec!["Z23"]]),
1962            make_segment("SEQ", vec![vec!["Z01"]]),
1963        ];
1964        let external = NoOpExternalProvider;
1965        let ctx = EvaluationContext::with_navigator("55001", &external, &segments, &nav);
1966
1967        assert_eq!(ctx.count_in_group("SEQ", &["SG4", "SG8"]), 2);
1968        assert_eq!(ctx.count_in_group("CCI", &["SG4", "SG8"]), 1);
1969        assert_eq!(ctx.count_in_group("DTM", &["SG4", "SG8"]), 0);
1970    }
1971
1972    #[test]
1973    fn test_count_fallback_no_navigator() {
1974        let segments = vec![
1975            make_segment("CCI", vec![vec!["Z23"]]),
1976            make_segment("CCI", vec![vec!["Z30"]]),
1977            make_segment("CCI", vec![vec!["Z23"]]),
1978        ];
1979        let external = NoOpExternalProvider;
1980        let ctx = EvaluationContext::new("55001", &external, &segments);
1981
1982        // Falls back to message-wide count
1983        assert_eq!(
1984            ctx.count_qualified_in_group("CCI", 0, "Z23", &["SG4", "SG8"]),
1985            2
1986        );
1987        assert_eq!(ctx.count_in_group("CCI", &["SG4", "SG8"]), 3);
1988    }
1989
1990    // --- format_check / format_check_qualified tests ---
1991
1992    #[test]
1993    fn format_check_prefers_resolved_value() {
1994        // Segments have "WRONG" but resolved_value is "CORRECT"
1995        let segments = vec![make_segment("DTM", vec![vec!["92", "WRONG"]])];
1996        let external = NoOpExternalProvider;
1997        let ctx = EvaluationContext::new("55001", &external, &segments)
1998            .with_resolved(Some("CORRECT"), None);
1999
2000        let result = ctx.format_check("DTM", 0, 1, |v| ConditionResult::from(v == "CORRECT"));
2001        assert_eq!(result, ConditionResult::True);
2002    }
2003
2004    #[test]
2005    fn format_check_falls_back_to_segment_search() {
2006        // No resolved_value, segments have the value
2007        let segments = vec![make_segment("DTM", vec![vec!["92", "202501011200"]])];
2008        let external = NoOpExternalProvider;
2009        let ctx = EvaluationContext::new("55001", &external, &segments);
2010
2011        let result = ctx.format_check("DTM", 0, 1, |v| ConditionResult::from(v == "202501011200"));
2012        assert_eq!(result, ConditionResult::True);
2013    }
2014
2015    #[test]
2016    fn format_check_returns_unknown_when_segment_absent() {
2017        // No resolved_value, no matching segments — we can't decide.
2018        let segments: Vec<OwnedSegment> = vec![];
2019        let external = NoOpExternalProvider;
2020        let ctx = EvaluationContext::new("55001", &external, &segments);
2021
2022        let result = ctx.format_check("DTM", 0, 1, |_| ConditionResult::True);
2023        assert_eq!(result, ConditionResult::Unknown);
2024    }
2025
2026    #[test]
2027    fn format_check_returns_unknown_when_multiple_segments() {
2028        // Two DTMs, no resolved_value. Picking .first() would silently pick
2029        // one; Unknown is the honest answer when we can't tell which is being
2030        // validated.
2031        let segments = vec![
2032            make_segment("DTM", vec![vec!["92", "OK"]]),
2033            make_segment("DTM", vec![vec!["163", "BAD"]]),
2034        ];
2035        let external = NoOpExternalProvider;
2036        let ctx = EvaluationContext::new("55001", &external, &segments);
2037
2038        let result = ctx.format_check("DTM", 0, 1, |v| ConditionResult::from(v == "OK"));
2039        assert_eq!(result, ConditionResult::Unknown);
2040    }
2041
2042    #[test]
2043    fn format_check_qualified_prefers_resolved_value() {
2044        // Two DTM segments but resolved_value overrides
2045        let segments = vec![
2046            make_segment("DTM", vec![vec!["92", "WRONG"]]),
2047            make_segment("DTM", vec![vec!["163", "ALSO_WRONG"]]),
2048        ];
2049        let external = NoOpExternalProvider;
2050        let ctx = EvaluationContext::new("55001", &external, &segments)
2051            .with_resolved(Some("CORRECT"), None);
2052
2053        let result = ctx.format_check_qualified("DTM", 0, "163", 0, 1, |v| {
2054            ConditionResult::from(v == "CORRECT")
2055        });
2056        assert_eq!(result, ConditionResult::True);
2057    }
2058
2059    #[test]
2060    fn format_check_qualified_falls_back_to_qualified_search() {
2061        // Two DTM segments with different qualifiers, no resolved_value
2062        let segments = vec![
2063            make_segment("DTM", vec![vec!["92", "2200"]]),
2064            make_segment("DTM", vec![vec!["163", "0800"]]),
2065        ];
2066        let external = NoOpExternalProvider;
2067        let ctx = EvaluationContext::new("55001", &external, &segments);
2068
2069        // Should find DTM+163 and extract "0800", not DTM+92's "2200"
2070        let result = ctx.format_check_qualified("DTM", 0, "163", 0, 1, |v| {
2071            ConditionResult::from(v == "0800")
2072        });
2073        assert_eq!(result, ConditionResult::True);
2074
2075        // Verify it does NOT pick DTM+92's value
2076        let result2 = ctx.format_check_qualified("DTM", 0, "163", 0, 1, |v| {
2077            ConditionResult::from(v == "2200")
2078        });
2079        assert_eq!(result2, ConditionResult::False);
2080    }
2081
2082    // --- self_segment_value / self_segment_value_equals tests ---
2083
2084    #[test]
2085    fn self_segment_value_prefers_resolved_segment() {
2086        // Two DTM segments in the message: the first has "OTHER", the
2087        // resolved_segment (the one being validated) has "303". Per-field
2088        // condition must read "303", not "OTHER".
2089        let segments = vec![
2090            make_segment("DTM", vec![vec!["472", "", "OTHER"]]),
2091            make_segment("DTM", vec![vec!["Z25", "", "303"]]),
2092        ];
2093        let external = NoOpExternalProvider;
2094        let resolved_elements = vec![vec![
2095            "Z25".to_string(),
2096            String::new(),
2097            "303".to_string(),
2098        ]];
2099        let ctx = EvaluationContext::new("55001", &external, &segments)
2100            .with_resolved(None, Some(&resolved_elements));
2101
2102        assert_eq!(ctx.self_segment_value("DTM", 0, 2), Some("303"));
2103        assert!(ctx.self_segment_value_equals("DTM", 0, 2, "303"));
2104        assert!(!ctx.self_segment_value_equals("DTM", 0, 2, "OTHER"));
2105    }
2106
2107    #[test]
2108    fn self_segment_value_falls_back_when_no_resolved_segment() {
2109        // Outside tree context — fall back to the first tag match.
2110        let segments = vec![
2111            make_segment("DTM", vec![vec!["Z25", "", "303"]]),
2112            make_segment("DTM", vec![vec!["472", "", "OTHER"]]),
2113        ];
2114        let external = NoOpExternalProvider;
2115        let ctx = EvaluationContext::new("55001", &external, &segments);
2116
2117        assert_eq!(ctx.self_segment_value("DTM", 0, 2), Some("303"));
2118    }
2119
2120    #[test]
2121    fn self_segment_value_missing_returns_none() {
2122        let segments: Vec<OwnedSegment> = vec![];
2123        let external = NoOpExternalProvider;
2124        let ctx = EvaluationContext::new("55001", &external, &segments);
2125
2126        assert_eq!(ctx.self_segment_value("DTM", 0, 2), None);
2127        assert!(!ctx.self_segment_value_equals("DTM", 0, 2, "303"));
2128    }
2129}