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