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