Skip to main content

automapper_validation/eval/
expr_eval.rs

1//! Evaluates `ConditionExpr` trees using a `ConditionEvaluator`.
2
3use super::context::EvaluationContext;
4use super::evaluator::{ConditionEvaluator, ConditionResult};
5use crate::expr::{ConditionExpr, StatusKind};
6
7/// Which status an AHB status applies, see
8/// [`ConditionExprEvaluator::resolve_status`].
9#[derive(Debug, Clone, PartialEq)]
10pub struct StatusResolution {
11    /// `True`: `kind` applies. `False`: no line holds; `kind` is the strictest
12    /// line's. `Unknown`: an undecided line could change the answer.
13    pub result: ConditionResult,
14    pub kind: Option<StatusKind>,
15    /// Conditions that were unknown and could change the answer.
16    pub unknown_ids: Vec<u32>,
17}
18
19/// Evaluates a `ConditionExpr` AST against an evaluation context.
20///
21/// Uses three-valued short-circuit logic:
22/// - AND: False short-circuits to False; all True -> True; else Unknown
23/// - OR: True short-circuits to True; all False -> False; else Unknown
24/// - XOR: requires both operands known; Unknown if either is Unknown
25/// - NOT: inverts True/False; preserves Unknown
26pub struct ConditionExprEvaluator<'a, E: ConditionEvaluator> {
27    evaluator: &'a E,
28}
29
30/// Whether `expr` consists of package terms only (`[4P0..1]`, or several
31/// joined).
32fn is_package_only(expr: &ConditionExpr) -> bool {
33    match expr {
34        ConditionExpr::Package { .. } => true,
35        ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
36            !exprs.is_empty() && exprs.iter().all(is_package_only)
37        }
38        ConditionExpr::Xor(left, right) => is_package_only(left) && is_package_only(right),
39        ConditionExpr::Not(inner) => is_package_only(inner),
40        ConditionExpr::Ref(_) => false,
41    }
42}
43
44/// What of `expr` decides whether something missing must be sent.
45///
46/// Notes (`[500]`–`[899]`) and formats (`[900]`–`[999]`) describe a value that
47/// is there, so a missing value cannot fail them, and they say nothing about
48/// whether one is needed. A `[UB…]` the status could not expand (read as
49/// `[0]`) is one of the AHB's date-format unions — `UB1`: `([931] ∧ [932]
50/// [490]) ⊻ ([931] ∧ [933] [491])`, whose `[490]`/`[491]` ask about the date
51/// itself. All of them are dropped: `X ([10]) ∧ ([UB1])` requires its value
52/// where `[10]` holds, `X [914] ∧ [937]` always. Read as `True` instead, the
53/// two sides of `UB1`'s `⊻` would cancel out.
54///
55/// `None` when nothing is left: the status's keyword alone decides.
56pub fn requiredness(expr: &ConditionExpr) -> Option<ConditionExpr> {
57    match expr {
58        ConditionExpr::Ref(id) if *id == 0 || (500..1000).contains(id) => None,
59        ConditionExpr::Ref(_) | ConditionExpr::Package { .. } => Some(expr.clone()),
60        ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
61            let mut kept: Vec<ConditionExpr> = exprs.iter().filter_map(requiredness).collect();
62            match kept.len() {
63                0 => None,
64                1 => kept.pop(),
65                _ if matches!(expr, ConditionExpr::And(_)) => Some(ConditionExpr::And(kept)),
66                _ => Some(ConditionExpr::Or(kept)),
67            }
68        }
69        ConditionExpr::Xor(left, right) => match (requiredness(left), requiredness(right)) {
70            (None, None) => None,
71            (Some(one), None) | (None, Some(one)) => Some(one),
72            (Some(l), Some(r)) => Some(ConditionExpr::Xor(Box::new(l), Box::new(r))),
73        },
74        ConditionExpr::Not(inner) => {
75            requiredness(inner).map(|inner| ConditionExpr::Not(Box::new(inner)))
76        }
77    }
78}
79
80/// Whether `expr` is package terms only, none of which needs a code
81/// (`[9P0..1]`): a status of that kind requires nothing.
82pub fn packages_allow_none(expr: &ConditionExpr) -> bool {
83    fn mins(expr: &ConditionExpr, out: &mut Vec<u32>) -> bool {
84        match expr {
85            ConditionExpr::Package { min, .. } => {
86                out.push(*min);
87                true
88            }
89            ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
90                exprs.iter().all(|e| mins(e, out))
91            }
92            ConditionExpr::Xor(l, r) => mins(l, out) && mins(r, out),
93            ConditionExpr::Not(_) | ConditionExpr::Ref(_) => false,
94        }
95    }
96    let mut out = Vec::new();
97    mins(expr, &mut out) && !out.is_empty() && out.iter().all(|m| *m == 0)
98}
99
100impl<'a, E: ConditionEvaluator> ConditionExprEvaluator<'a, E> {
101    /// Create a new expression evaluator wrapping a condition evaluator.
102    pub fn new(evaluator: &'a E) -> Self {
103        Self { evaluator }
104    }
105
106    /// The wrapped condition evaluator.
107    pub fn evaluator(&self) -> &'a E {
108        self.evaluator
109    }
110
111    /// Evaluate a condition expression tree.
112    pub fn evaluate(&self, expr: &ConditionExpr, ctx: &EvaluationContext) -> ConditionResult {
113        match expr {
114            ConditionExpr::Ref(id) => self.evaluator.evaluate(*id, ctx),
115
116            ConditionExpr::And(exprs) => self.evaluate_and(exprs, ctx),
117
118            ConditionExpr::Or(exprs) => self.evaluate_or(exprs, ctx),
119
120            // A side made of package terms only states which package a code
121            // counts toward (`EHZ`: `X [4P0..1] ⊻ [5P0..1]`), not a condition:
122            // the package cardinality is checked separately. It is neutral
123            // here — read as `True`, two of them would make the XOR false.
124            ConditionExpr::Xor(left, right) => {
125                match (is_package_only(left), is_package_only(right)) {
126                    (true, true) => ConditionResult::True,
127                    (true, false) => self.evaluate(right, ctx),
128                    (false, true) => self.evaluate(left, ctx),
129                    (false, false) => {
130                        let l = self.evaluate(left, ctx);
131                        let r = self.evaluate(right, ctx);
132                        self.evaluate_xor(l, r)
133                    }
134                }
135            }
136
137            ConditionExpr::Not(inner) => {
138                let result = self.evaluate(inner, ctx);
139                self.evaluate_not(result)
140            }
141
142            ConditionExpr::Package { .. } => {
143                // Package cardinality constraints are evaluated separately;
144                // in a boolean context they are trivially true.
145                ConditionResult::True
146            }
147        }
148    }
149
150    /// AND with short-circuit: any False -> False, all True -> True, else Unknown.
151    fn evaluate_and(&self, exprs: &[ConditionExpr], ctx: &EvaluationContext) -> ConditionResult {
152        let mut has_unknown = false;
153
154        for expr in exprs {
155            match self.evaluate(expr, ctx) {
156                ConditionResult::False => return ConditionResult::False,
157                ConditionResult::Unknown => has_unknown = true,
158                ConditionResult::True => {}
159            }
160        }
161
162        if has_unknown {
163            ConditionResult::Unknown
164        } else {
165            ConditionResult::True
166        }
167    }
168
169    /// OR with short-circuit: any True -> True, all False -> False, else Unknown.
170    fn evaluate_or(&self, exprs: &[ConditionExpr], ctx: &EvaluationContext) -> ConditionResult {
171        let mut has_unknown = false;
172
173        for expr in exprs {
174            match self.evaluate(expr, ctx) {
175                ConditionResult::True => return ConditionResult::True,
176                ConditionResult::Unknown => has_unknown = true,
177                ConditionResult::False => {}
178            }
179        }
180
181        if has_unknown {
182            ConditionResult::Unknown
183        } else {
184            ConditionResult::False
185        }
186    }
187
188    /// XOR: both must be known. True XOR False = True, same values = False, Unknown if either Unknown.
189    fn evaluate_xor(&self, left: ConditionResult, right: ConditionResult) -> ConditionResult {
190        match (left, right) {
191            (ConditionResult::True, ConditionResult::False)
192            | (ConditionResult::False, ConditionResult::True) => ConditionResult::True,
193            (ConditionResult::True, ConditionResult::True)
194            | (ConditionResult::False, ConditionResult::False) => ConditionResult::False,
195            _ => ConditionResult::Unknown,
196        }
197    }
198
199    /// NOT: inverts True/False, preserves Unknown.
200    fn evaluate_not(&self, result: ConditionResult) -> ConditionResult {
201        match result {
202            ConditionResult::True => ConditionResult::False,
203            ConditionResult::False => ConditionResult::True,
204            ConditionResult::Unknown => ConditionResult::Unknown,
205        }
206    }
207
208    /// Parse an AHB status string, evaluate it, and return the result.
209    ///
210    /// Returns `ConditionResult::True` if there are no conditions (unconditionally required).
211    pub fn evaluate_status(&self, ahb_status: &str, ctx: &EvaluationContext) -> ConditionResult {
212        use crate::expr::ConditionParser;
213
214        match ConditionParser::parse(ahb_status) {
215            Ok(Some(expr)) => self.evaluate(&expr, ctx),
216            Ok(None) => ConditionResult::True, // No conditions = unconditionally true
217            Err(_) => ConditionResult::Unknown, // Parse error = treat as unknown
218        }
219    }
220
221    /// Like [`evaluate_status`](Self::evaluate_status), but expands UB condition
222    /// references inline during parsing.
223    pub fn evaluate_status_with_ub(
224        &self,
225        ahb_status: &str,
226        ctx: &EvaluationContext,
227        ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
228    ) -> ConditionResult {
229        use crate::expr::ConditionParser;
230
231        match ConditionParser::parse_with_ub(ahb_status, ub_definitions) {
232            Ok(Some(expr)) => self.evaluate(&expr, ctx),
233            Ok(None) => ConditionResult::True,
234            Err(_) => ConditionResult::Unknown,
235        }
236    }
237
238    /// Like [`evaluate_status`](Self::evaluate_status), but also returns the
239    /// IDs of conditions that evaluated to `Unknown`.
240    pub fn evaluate_status_detailed(
241        &self,
242        ahb_status: &str,
243        ctx: &EvaluationContext,
244    ) -> (ConditionResult, Vec<u32>) {
245        use crate::expr::ConditionParser;
246
247        match ConditionParser::parse(ahb_status) {
248            Ok(Some(expr)) => {
249                let result = self.evaluate(&expr, ctx);
250                if result.is_unknown() {
251                    let unknown_ids = self.collect_unknown_ids(&expr, ctx);
252                    (result, unknown_ids)
253                } else {
254                    (result, Vec::new())
255                }
256            }
257            Ok(None) => (ConditionResult::True, Vec::new()),
258            Err(_) => (ConditionResult::Unknown, Vec::new()),
259        }
260    }
261
262    /// Like [`evaluate_status_detailed`](Self::evaluate_status_detailed), but
263    /// expands UB condition references inline during parsing.
264    pub fn evaluate_status_detailed_with_ub(
265        &self,
266        ahb_status: &str,
267        ctx: &EvaluationContext,
268        ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
269    ) -> (ConditionResult, Vec<u32>) {
270        use crate::expr::ConditionParser;
271
272        match ConditionParser::parse_with_ub(ahb_status, ub_definitions) {
273            Ok(Some(expr)) => {
274                let result = self.evaluate(&expr, ctx);
275                if result.is_unknown() {
276                    let unknown_ids = self.collect_unknown_ids(&expr, ctx);
277                    (result, unknown_ids)
278                } else {
279                    (result, Vec::new())
280                }
281            }
282            Ok(None) => (ConditionResult::True, Vec::new()),
283            Err(_) => (ConditionResult::Unknown, Vec::new()),
284        }
285    }
286
287    /// Decide which status of an AHB status applies.
288    ///
289    /// A status can carry several lines (`Muss [2119]` over `Soll [130]`). The
290    /// strictest line whose condition holds applies — `Muss` over `Soll` over
291    /// `Kann`, whatever the order. A line whose condition is unknown blocks the
292    /// decision only if it is stricter than that: then the answer could change,
293    /// and the result is `Unknown`. A line without a condition always holds.
294    ///
295    /// A single-line status resolves to its own status word and the result of
296    /// its condition, as [`evaluate_status_detailed_with_ub`] gives it.
297    ///
298    /// [`evaluate_status_detailed_with_ub`]: Self::evaluate_status_detailed_with_ub
299    pub fn resolve_status(
300        &self,
301        ahb_status: &str,
302        ctx: &EvaluationContext,
303        ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
304    ) -> StatusResolution {
305        use crate::expr::ConditionParser;
306
307        let branches = match ConditionParser::parse_status_branches(ahb_status, ub_definitions) {
308            Ok(branches) => branches,
309            Err(_) => {
310                return StatusResolution {
311                    result: ConditionResult::Unknown,
312                    kind: StatusKind::of(ahb_status),
313                    unknown_ids: Vec::new(),
314                }
315            }
316        };
317
318        let mut holding: Option<Option<StatusKind>> = None;
319        let mut unknown: Vec<(Option<StatusKind>, &ConditionExpr)> = Vec::new();
320        for branch in &branches {
321            // A line of packages none of which needs a code (`X [17P0..1]`)
322            // allows the part and requires nothing: it reads as `Kann`.
323            let kind = match &branch.condition {
324                Some(c) if packages_allow_none(c) => branch.kind.map(|_| StatusKind::Kann),
325                _ => branch.kind,
326            };
327            let result = branch
328                .condition
329                .as_ref()
330                .map_or(ConditionResult::True, |c| self.evaluate(c, ctx));
331            match result {
332                ConditionResult::True => {
333                    holding = Some(holding.map_or(kind, |k| k.max(kind)));
334                }
335                ConditionResult::Unknown => {
336                    // Unknown needs a condition: a line without one holds.
337                    if let Some(condition) = &branch.condition {
338                        unknown.push((kind, condition));
339                    }
340                }
341                ConditionResult::False => {}
342            }
343        }
344
345        // Unknown lines that could still change the answer.
346        let blocking: Vec<&ConditionExpr> = unknown
347            .iter()
348            .filter(|(kind, _)| match holding {
349                None => true,
350                Some(h) => *kind > h,
351            })
352            .map(|(_, condition)| *condition)
353            .collect();
354
355        if !blocking.is_empty() {
356            return StatusResolution {
357                result: ConditionResult::Unknown,
358                kind: None,
359                unknown_ids: blocking
360                    .into_iter()
361                    .flat_map(|c| self.collect_unknown_ids(c, ctx))
362                    .collect(),
363            };
364        }
365        match holding {
366            Some(kind) => StatusResolution {
367                result: ConditionResult::True,
368                kind,
369                unknown_ids: Vec::new(),
370            },
371            None => StatusResolution {
372                result: if branches.is_empty() {
373                    ConditionResult::True
374                } else {
375                    ConditionResult::False
376                },
377                kind: branches.iter().map(|b| b.kind).max().flatten(),
378                unknown_ids: Vec::new(),
379            },
380        }
381    }
382
383    /// Collect condition IDs that evaluate to `Unknown` within an expression.
384    /// The conditions that make `ahb_status` false: each condition that is
385    /// false where it counts for itself, and each that is true under a
386    /// negation. An APERAK names the error by them (a format condition
387    /// [901]–[999] is a format error, a repetition condition [2000]–[2499] a
388    /// repetition error).
389    pub fn false_ids_with_ub(
390        &self,
391        ahb_status: &str,
392        ctx: &EvaluationContext,
393        ub_definitions: &std::collections::BTreeMap<String, crate::expr::ConditionExpr>,
394    ) -> Vec<u32> {
395        let Ok(Some(expr)) =
396            crate::expr::ConditionParser::parse_with_ub(ahb_status, ub_definitions)
397        else {
398            return Vec::new();
399        };
400        let mut ids = Vec::new();
401        self.collect_false_ids(&expr, ctx, false, &mut ids);
402        ids.sort_unstable();
403        ids.dedup();
404        ids
405    }
406
407    fn collect_false_ids(
408        &self,
409        expr: &ConditionExpr,
410        ctx: &EvaluationContext,
411        negated: bool,
412        ids: &mut Vec<u32>,
413    ) {
414        match expr {
415            ConditionExpr::Ref(id) => {
416                let result = self.evaluator.evaluate(*id, ctx);
417                let failing = if negated {
418                    result == ConditionResult::True
419                } else {
420                    result == ConditionResult::False
421                };
422                if failing {
423                    ids.push(*id);
424                }
425            }
426            ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
427                for e in exprs {
428                    self.collect_false_ids(e, ctx, negated, ids);
429                }
430            }
431            ConditionExpr::Xor(left, right) => {
432                self.collect_false_ids(left, ctx, negated, ids);
433                self.collect_false_ids(right, ctx, negated, ids);
434            }
435            ConditionExpr::Not(inner) => self.collect_false_ids(inner, ctx, !negated, ids),
436            ConditionExpr::Package { .. } => {}
437        }
438    }
439
440    fn collect_unknown_ids(&self, expr: &ConditionExpr, ctx: &EvaluationContext) -> Vec<u32> {
441        let mut ids = Vec::new();
442        self.collect_unknown_ids_inner(expr, ctx, &mut ids);
443        ids
444    }
445
446    fn collect_unknown_ids_inner(
447        &self,
448        expr: &ConditionExpr,
449        ctx: &EvaluationContext,
450        ids: &mut Vec<u32>,
451    ) {
452        match expr {
453            ConditionExpr::Ref(id) => {
454                if self.evaluator.evaluate(*id, ctx).is_unknown() {
455                    ids.push(*id);
456                }
457            }
458            ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
459                for e in exprs {
460                    self.collect_unknown_ids_inner(e, ctx, ids);
461                }
462            }
463            ConditionExpr::Xor(left, right) => {
464                self.collect_unknown_ids_inner(left, ctx, ids);
465                self.collect_unknown_ids_inner(right, ctx, ids);
466            }
467            ConditionExpr::Not(inner) => {
468                self.collect_unknown_ids_inner(inner, ctx, ids);
469            }
470            ConditionExpr::Package { .. } => {
471                // Package constraints have no condition IDs to collect
472            }
473        }
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::super::evaluator::{ConditionResult as CR, NoOpExternalProvider};
480
481    #[test]
482    fn requiredness_drops_notes_formats_and_unexpanded_ub() {
483        use crate::expr::ConditionParser;
484        let reduce = |status: &str| {
485            ConditionParser::parse(status)
486                .unwrap()
487                .and_then(|e| requiredness(&e))
488        };
489        assert_eq!(reduce("X ([10]) ∧ ([UB1])"), Some(ConditionExpr::Ref(10)));
490        assert_eq!(reduce("X [914] ∧ [937]"), None);
491        assert_eq!(reduce("X [UB1]"), None);
492        assert_eq!(
493            reduce("X ([914] ∧ [930] [140]) ⊻ ([926] ∧ [937] [172])"),
494            Some(ConditionExpr::Xor(
495                Box::new(ConditionExpr::Ref(140)),
496                Box::new(ConditionExpr::Ref(172))
497            ))
498        );
499        assert_eq!(
500            reduce("Muss [356] ∧ [2061]"),
501            Some(ConditionExpr::And(vec![
502                ConditionExpr::Ref(356),
503                ConditionExpr::Ref(2061)
504            ]))
505        );
506    }
507
508    #[test]
509    fn a_package_without_a_minimum_requires_nothing() {
510        use crate::expr::ConditionParser;
511        let parse = |s: &str| ConditionParser::parse(s).unwrap().unwrap();
512        assert!(packages_allow_none(&parse("S [9P0..1]")));
513        assert!(!packages_allow_none(&parse("X [4P1..1]")));
514        assert!(!packages_allow_none(&parse("X [4P0..1] ∧ [12]")));
515    }
516    use super::*;
517    use mig_types::segment::OwnedSegment;
518    use std::collections::HashMap;
519
520    /// A mock condition evaluator for testing.
521    struct MockEvaluator {
522        results: HashMap<u32, ConditionResult>,
523        external_ids: Vec<u32>,
524    }
525
526    impl MockEvaluator {
527        fn new() -> Self {
528            Self {
529                results: HashMap::new(),
530                external_ids: Vec::new(),
531            }
532        }
533
534        fn with_condition(mut self, id: u32, result: ConditionResult) -> Self {
535            self.results.insert(id, result);
536            self
537        }
538    }
539
540    impl ConditionEvaluator for MockEvaluator {
541        fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> ConditionResult {
542            self.results
543                .get(&condition)
544                .copied()
545                .unwrap_or(ConditionResult::Unknown)
546        }
547
548        fn is_external(&self, condition: u32) -> bool {
549            self.external_ids.contains(&condition)
550        }
551
552        fn message_type(&self) -> &str {
553            "TEST"
554        }
555
556        fn format_version(&self) -> &str {
557            "FV_TEST"
558        }
559    }
560
561    fn empty_context() -> (NoOpExternalProvider, Vec<OwnedSegment>) {
562        (NoOpExternalProvider, Vec::new())
563    }
564
565    fn make_ctx<'a>(
566        external: &'a NoOpExternalProvider,
567        segments: &'a [OwnedSegment],
568    ) -> EvaluationContext<'a> {
569        EvaluationContext::new("11001", external, segments)
570    }
571
572    // === Single condition ===
573
574    #[test]
575    fn test_eval_single_true() {
576        let eval = MockEvaluator::new().with_condition(1, CR::True);
577        let (ext, segs) = empty_context();
578        let ctx = make_ctx(&ext, &segs);
579        let expr_eval = ConditionExprEvaluator::new(&eval);
580
581        assert_eq!(expr_eval.evaluate(&ConditionExpr::Ref(1), &ctx), CR::True);
582    }
583
584    #[test]
585    fn test_eval_single_false() {
586        let eval = MockEvaluator::new().with_condition(1, CR::False);
587        let (ext, segs) = empty_context();
588        let ctx = make_ctx(&ext, &segs);
589        let expr_eval = ConditionExprEvaluator::new(&eval);
590
591        assert_eq!(expr_eval.evaluate(&ConditionExpr::Ref(1), &ctx), CR::False);
592    }
593
594    #[test]
595    fn test_eval_single_unknown() {
596        let eval = MockEvaluator::new(); // No condition registered -> Unknown
597        let (ext, segs) = empty_context();
598        let ctx = make_ctx(&ext, &segs);
599        let expr_eval = ConditionExprEvaluator::new(&eval);
600
601        assert_eq!(
602            expr_eval.evaluate(&ConditionExpr::Ref(999), &ctx),
603            CR::Unknown
604        );
605    }
606
607    // === AND ===
608
609    #[test]
610    fn test_eval_and_both_true() {
611        let eval = MockEvaluator::new()
612            .with_condition(1, CR::True)
613            .with_condition(2, CR::True);
614        let (ext, segs) = empty_context();
615        let ctx = make_ctx(&ext, &segs);
616        let expr_eval = ConditionExprEvaluator::new(&eval);
617
618        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
619        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
620    }
621
622    #[test]
623    fn test_eval_and_one_false_short_circuits() {
624        let eval = MockEvaluator::new()
625            .with_condition(1, CR::False)
626            .with_condition(2, CR::True);
627        let (ext, segs) = empty_context();
628        let ctx = make_ctx(&ext, &segs);
629        let expr_eval = ConditionExprEvaluator::new(&eval);
630
631        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
632        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
633    }
634
635    #[test]
636    fn test_eval_and_one_unknown_true_gives_unknown() {
637        let eval = MockEvaluator::new()
638            .with_condition(1, CR::True)
639            .with_condition(2, CR::Unknown);
640        let (ext, segs) = empty_context();
641        let ctx = make_ctx(&ext, &segs);
642        let expr_eval = ConditionExprEvaluator::new(&eval);
643
644        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
645        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
646    }
647
648    #[test]
649    fn test_eval_and_false_beats_unknown() {
650        // AND with False and Unknown should be False (short-circuit)
651        let eval = MockEvaluator::new()
652            .with_condition(1, CR::False)
653            .with_condition(2, CR::Unknown);
654        let (ext, segs) = empty_context();
655        let ctx = make_ctx(&ext, &segs);
656        let expr_eval = ConditionExprEvaluator::new(&eval);
657
658        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
659        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
660    }
661
662    #[test]
663    fn test_eval_and_three_way() {
664        let eval = MockEvaluator::new()
665            .with_condition(182, CR::True)
666            .with_condition(6, CR::True)
667            .with_condition(570, CR::True);
668        let (ext, segs) = empty_context();
669        let ctx = make_ctx(&ext, &segs);
670        let expr_eval = ConditionExprEvaluator::new(&eval);
671
672        let expr = ConditionExpr::And(vec![
673            ConditionExpr::Ref(182),
674            ConditionExpr::Ref(6),
675            ConditionExpr::Ref(570),
676        ]);
677        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
678    }
679
680    #[test]
681    fn test_eval_and_three_way_one_false() {
682        let eval = MockEvaluator::new()
683            .with_condition(182, CR::True)
684            .with_condition(6, CR::True)
685            .with_condition(570, CR::False);
686        let (ext, segs) = empty_context();
687        let ctx = make_ctx(&ext, &segs);
688        let expr_eval = ConditionExprEvaluator::new(&eval);
689
690        let expr = ConditionExpr::And(vec![
691            ConditionExpr::Ref(182),
692            ConditionExpr::Ref(6),
693            ConditionExpr::Ref(570),
694        ]);
695        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
696    }
697
698    // === OR ===
699
700    #[test]
701    fn test_eval_or_both_false() {
702        let eval = MockEvaluator::new()
703            .with_condition(1, CR::False)
704            .with_condition(2, CR::False);
705        let (ext, segs) = empty_context();
706        let ctx = make_ctx(&ext, &segs);
707        let expr_eval = ConditionExprEvaluator::new(&eval);
708
709        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
710        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
711    }
712
713    #[test]
714    fn test_eval_or_one_true_short_circuits() {
715        let eval = MockEvaluator::new()
716            .with_condition(1, CR::False)
717            .with_condition(2, CR::True);
718        let (ext, segs) = empty_context();
719        let ctx = make_ctx(&ext, &segs);
720        let expr_eval = ConditionExprEvaluator::new(&eval);
721
722        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
723        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
724    }
725
726    #[test]
727    fn test_eval_or_true_beats_unknown() {
728        let eval = MockEvaluator::new()
729            .with_condition(1, CR::Unknown)
730            .with_condition(2, CR::True);
731        let (ext, segs) = empty_context();
732        let ctx = make_ctx(&ext, &segs);
733        let expr_eval = ConditionExprEvaluator::new(&eval);
734
735        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
736        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
737    }
738
739    #[test]
740    fn test_eval_or_false_and_unknown_gives_unknown() {
741        let eval = MockEvaluator::new()
742            .with_condition(1, CR::False)
743            .with_condition(2, CR::Unknown);
744        let (ext, segs) = empty_context();
745        let ctx = make_ctx(&ext, &segs);
746        let expr_eval = ConditionExprEvaluator::new(&eval);
747
748        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
749        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
750    }
751
752    // === XOR ===
753
754    #[test]
755    fn test_eval_xor_true_false() {
756        let eval = MockEvaluator::new()
757            .with_condition(1, CR::True)
758            .with_condition(2, CR::False);
759        let (ext, segs) = empty_context();
760        let ctx = make_ctx(&ext, &segs);
761        let expr_eval = ConditionExprEvaluator::new(&eval);
762
763        let expr = ConditionExpr::Xor(
764            Box::new(ConditionExpr::Ref(1)),
765            Box::new(ConditionExpr::Ref(2)),
766        );
767        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
768    }
769
770    #[test]
771    fn test_eval_xor_both_true() {
772        let eval = MockEvaluator::new()
773            .with_condition(1, CR::True)
774            .with_condition(2, CR::True);
775        let (ext, segs) = empty_context();
776        let ctx = make_ctx(&ext, &segs);
777        let expr_eval = ConditionExprEvaluator::new(&eval);
778
779        let expr = ConditionExpr::Xor(
780            Box::new(ConditionExpr::Ref(1)),
781            Box::new(ConditionExpr::Ref(2)),
782        );
783        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
784    }
785
786    #[test]
787    fn test_eval_xor_both_false() {
788        let eval = MockEvaluator::new()
789            .with_condition(1, CR::False)
790            .with_condition(2, CR::False);
791        let (ext, segs) = empty_context();
792        let ctx = make_ctx(&ext, &segs);
793        let expr_eval = ConditionExprEvaluator::new(&eval);
794
795        let expr = ConditionExpr::Xor(
796            Box::new(ConditionExpr::Ref(1)),
797            Box::new(ConditionExpr::Ref(2)),
798        );
799        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
800    }
801
802    #[test]
803    fn test_eval_xor_unknown_propagates() {
804        let eval = MockEvaluator::new()
805            .with_condition(1, CR::True)
806            .with_condition(2, CR::Unknown);
807        let (ext, segs) = empty_context();
808        let ctx = make_ctx(&ext, &segs);
809        let expr_eval = ConditionExprEvaluator::new(&eval);
810
811        let expr = ConditionExpr::Xor(
812            Box::new(ConditionExpr::Ref(1)),
813            Box::new(ConditionExpr::Ref(2)),
814        );
815        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
816    }
817
818    // === NOT ===
819
820    #[test]
821    fn test_eval_not_true() {
822        let eval = MockEvaluator::new().with_condition(1, CR::True);
823        let (ext, segs) = empty_context();
824        let ctx = make_ctx(&ext, &segs);
825        let expr_eval = ConditionExprEvaluator::new(&eval);
826
827        let expr = ConditionExpr::Not(Box::new(ConditionExpr::Ref(1)));
828        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
829    }
830
831    #[test]
832    fn test_eval_not_false() {
833        let eval = MockEvaluator::new().with_condition(1, CR::False);
834        let (ext, segs) = empty_context();
835        let ctx = make_ctx(&ext, &segs);
836        let expr_eval = ConditionExprEvaluator::new(&eval);
837
838        let expr = ConditionExpr::Not(Box::new(ConditionExpr::Ref(1)));
839        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
840    }
841
842    #[test]
843    fn test_eval_not_unknown() {
844        let eval = MockEvaluator::new(); // 1 -> Unknown
845        let (ext, segs) = empty_context();
846        let ctx = make_ctx(&ext, &segs);
847        let expr_eval = ConditionExprEvaluator::new(&eval);
848
849        let expr = ConditionExpr::Not(Box::new(ConditionExpr::Ref(1)));
850        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
851    }
852
853    // === Complex expressions ===
854
855    #[test]
856    fn test_eval_complex_nested() {
857        // (([1] ∧ [2]) ∨ ([3] ∧ [4])) ∧ [5]
858        // [1]=T, [2]=F, [3]=T, [4]=T, [5]=T
859        // ([1]∧[2])=F, ([3]∧[4])=T, F∨T=T, T∧[5]=T
860        let eval = MockEvaluator::new()
861            .with_condition(1, CR::True)
862            .with_condition(2, CR::False)
863            .with_condition(3, CR::True)
864            .with_condition(4, CR::True)
865            .with_condition(5, CR::True);
866        let (ext, segs) = empty_context();
867        let ctx = make_ctx(&ext, &segs);
868        let expr_eval = ConditionExprEvaluator::new(&eval);
869
870        let expr = ConditionExpr::And(vec![
871            ConditionExpr::Or(vec![
872                ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]),
873                ConditionExpr::And(vec![ConditionExpr::Ref(3), ConditionExpr::Ref(4)]),
874            ]),
875            ConditionExpr::Ref(5),
876        ]);
877        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
878    }
879
880    #[test]
881    fn test_eval_xor_with_nested_and() {
882        // ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])
883        // [102]=T, [2006]=T, [103]=F, [2005]=F
884        // T∧T=T, F∧F=F, T⊻F=T
885        let eval = MockEvaluator::new()
886            .with_condition(102, CR::True)
887            .with_condition(2006, CR::True)
888            .with_condition(103, CR::False)
889            .with_condition(2005, CR::False);
890        let (ext, segs) = empty_context();
891        let ctx = make_ctx(&ext, &segs);
892        let expr_eval = ConditionExprEvaluator::new(&eval);
893
894        let expr = ConditionExpr::Xor(
895            Box::new(ConditionExpr::And(vec![
896                ConditionExpr::Ref(102),
897                ConditionExpr::Ref(2006),
898            ])),
899            Box::new(ConditionExpr::And(vec![
900                ConditionExpr::Ref(103),
901                ConditionExpr::Ref(2005),
902            ])),
903        );
904        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
905    }
906
907    // === evaluate_status ===
908
909    #[test]
910    fn test_evaluate_status_with_conditions() {
911        let eval = MockEvaluator::new()
912            .with_condition(182, CR::True)
913            .with_condition(152, CR::True);
914        let (ext, segs) = empty_context();
915        let ctx = make_ctx(&ext, &segs);
916        let expr_eval = ConditionExprEvaluator::new(&eval);
917
918        assert_eq!(
919            expr_eval.evaluate_status("Muss [182] ∧ [152]", &ctx),
920            CR::True
921        );
922    }
923
924    #[test]
925    fn test_evaluate_status_no_conditions() {
926        let eval = MockEvaluator::new();
927        let (ext, segs) = empty_context();
928        let ctx = make_ctx(&ext, &segs);
929        let expr_eval = ConditionExprEvaluator::new(&eval);
930
931        assert_eq!(expr_eval.evaluate_status("Muss", &ctx), CR::True);
932    }
933
934    #[test]
935    fn test_evaluate_status_empty() {
936        let eval = MockEvaluator::new();
937        let (ext, segs) = empty_context();
938        let ctx = make_ctx(&ext, &segs);
939        let expr_eval = ConditionExprEvaluator::new(&eval);
940
941        assert_eq!(expr_eval.evaluate_status("", &ctx), CR::True);
942    }
943
944    // === Unknown propagation comprehensive ===
945
946    #[test]
947    fn test_unknown_propagation_and_or_mix() {
948        // [1](Unknown) ∨ ([2](True) ∧ [3](Unknown))
949        // [2]∧[3] = Unknown (True ∧ Unknown)
950        // Unknown ∨ Unknown = Unknown
951        let eval = MockEvaluator::new().with_condition(2, CR::True);
952        // 1 and 3 default to Unknown
953        let (ext, segs) = empty_context();
954        let ctx = make_ctx(&ext, &segs);
955        let expr_eval = ConditionExprEvaluator::new(&eval);
956
957        let expr = ConditionExpr::Or(vec![
958            ConditionExpr::Ref(1),
959            ConditionExpr::And(vec![ConditionExpr::Ref(2), ConditionExpr::Ref(3)]),
960        ]);
961        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
962    }
963
964    #[test]
965    fn test_or_short_circuits_past_unknown() {
966        // [1](Unknown) ∨ [2](True) -> True (True short-circuits)
967        let eval = MockEvaluator::new().with_condition(2, CR::True);
968        let (ext, segs) = empty_context();
969        let ctx = make_ctx(&ext, &segs);
970        let expr_eval = ConditionExprEvaluator::new(&eval);
971
972        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
973        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
974    }
975
976    #[test]
977    fn test_and_short_circuits_past_unknown() {
978        // [1](Unknown) ∧ [2](False) -> False (False short-circuits)
979        let eval = MockEvaluator::new().with_condition(2, CR::False);
980        let (ext, segs) = empty_context();
981        let ctx = make_ctx(&ext, &segs);
982        let expr_eval = ConditionExprEvaluator::new(&eval);
983
984        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
985        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
986    }
987
988    // === evaluate_status_detailed ===
989
990    #[test]
991    fn test_detailed_returns_unknown_ids() {
992        // [182]=True, [8]=Unknown → AND = Unknown, unknown_ids = [8]
993        let eval = MockEvaluator::new().with_condition(182, CR::True);
994        let (ext, segs) = empty_context();
995        let ctx = make_ctx(&ext, &segs);
996        let expr_eval = ConditionExprEvaluator::new(&eval);
997
998        let (result, unknown_ids) = expr_eval.evaluate_status_detailed("Muss [182] ∧ [8]", &ctx);
999        assert_eq!(result, CR::Unknown);
1000        assert_eq!(unknown_ids, vec![8]);
1001    }
1002
1003    #[test]
1004    fn test_detailed_multiple_unknown_ids() {
1005        // [1]=Unknown, [2]=True, [3]=Unknown → AND = Unknown, unknown_ids = [1, 3]
1006        let eval = MockEvaluator::new().with_condition(2, CR::True);
1007        let (ext, segs) = empty_context();
1008        let ctx = make_ctx(&ext, &segs);
1009        let expr_eval = ConditionExprEvaluator::new(&eval);
1010
1011        let (result, unknown_ids) =
1012            expr_eval.evaluate_status_detailed("Muss [1] ∧ [2] ∧ [3]", &ctx);
1013        assert_eq!(result, CR::Unknown);
1014        assert_eq!(unknown_ids, vec![1, 3]);
1015    }
1016
1017    #[test]
1018    fn test_detailed_no_unknown_when_true() {
1019        let eval = MockEvaluator::new()
1020            .with_condition(182, CR::True)
1021            .with_condition(152, CR::True);
1022        let (ext, segs) = empty_context();
1023        let ctx = make_ctx(&ext, &segs);
1024        let expr_eval = ConditionExprEvaluator::new(&eval);
1025
1026        let (result, unknown_ids) = expr_eval.evaluate_status_detailed("Muss [182] ∧ [152]", &ctx);
1027        assert_eq!(result, CR::True);
1028        assert!(unknown_ids.is_empty());
1029    }
1030
1031    #[test]
1032    fn test_detailed_no_unknown_when_false() {
1033        let eval = MockEvaluator::new().with_condition(182, CR::False);
1034        let (ext, segs) = empty_context();
1035        let ctx = make_ctx(&ext, &segs);
1036        let expr_eval = ConditionExprEvaluator::new(&eval);
1037
1038        let (result, unknown_ids) = expr_eval.evaluate_status_detailed("Muss [182] ∧ [8]", &ctx);
1039        assert_eq!(result, CR::False);
1040        assert!(unknown_ids.is_empty());
1041    }
1042
1043    // === resolve_status: several status lines ===
1044
1045    fn resolve(eval: &MockEvaluator, status: &str) -> StatusResolution {
1046        let (ext, segs) = empty_context();
1047        let ctx = make_ctx(&ext, &segs);
1048        ConditionExprEvaluator::new(eval).resolve_status(status, &ctx, &Default::default())
1049    }
1050
1051    #[test]
1052    fn test_resolve_single_line_keeps_its_status() {
1053        let eval = MockEvaluator::new().with_condition(1, CR::True);
1054        let r = resolve(&eval, "Soll [1]");
1055        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Soll)));
1056    }
1057
1058    #[test]
1059    fn test_resolve_only_later_line_holds_gives_its_status() {
1060        // Muss [2119] / Soll [130]: 130 alone makes the part recommended, not required.
1061        let eval = MockEvaluator::new()
1062            .with_condition(2119, CR::False)
1063            .with_condition(130, CR::True);
1064        let r = resolve(&eval, "Muss [2119]\r\nSoll [130]");
1065        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Soll)));
1066    }
1067
1068    #[test]
1069    fn test_resolve_several_lines_hold_strictest_wins() {
1070        let eval = MockEvaluator::new()
1071            .with_condition(47, CR::True)
1072            .with_condition(46, CR::True);
1073        let r = resolve(&eval, "Soll [47]\nMuss [46]");
1074        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Muss)));
1075    }
1076
1077    #[test]
1078    fn test_resolve_unconditional_fallback_line() {
1079        let eval = MockEvaluator::new().with_condition(48, CR::False);
1080        let r = resolve(&eval, "Muss [48]\nKann");
1081        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Kann)));
1082    }
1083
1084    #[test]
1085    fn test_resolve_package_needing_no_code_requires_nothing() {
1086        // 55052 STS+7 Ergänzung `X [17P0..1]`: at most one code of package
1087        // 17, possibly none. The field may be sent; it need not be.
1088        let eval = MockEvaluator::new();
1089        let r = resolve(&eval, "X [17P0..1]");
1090        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Kann)));
1091        let r = resolve(&eval, "X [1P1..1]");
1092        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Muss)));
1093    }
1094
1095    #[test]
1096    fn test_resolve_weaker_unknown_line_does_not_block() {
1097        // [130] unknown can at most add Soll; Muss already holds.
1098        let eval = MockEvaluator::new().with_condition(2119, CR::True);
1099        let r = resolve(&eval, "Muss [2119]\nSoll [130]");
1100        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Muss)));
1101        assert!(r.unknown_ids.is_empty());
1102    }
1103
1104    #[test]
1105    fn test_resolve_stronger_unknown_line_is_unknown() {
1106        // [2119] unknown could make it Muss; Soll alone does not decide it.
1107        let eval = MockEvaluator::new().with_condition(130, CR::True);
1108        let r = resolve(&eval, "Muss [2119]\nSoll [130]");
1109        assert_eq!(r.result, CR::Unknown);
1110        assert_eq!(r.unknown_ids, vec![2119]);
1111    }
1112
1113    #[test]
1114    fn test_resolve_equally_strong_unknown_line_does_not_block() {
1115        let eval = MockEvaluator::new().with_condition(2, CR::True);
1116        let r = resolve(&eval, "Muss [1]\nMuss [2]");
1117        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Muss)));
1118    }
1119
1120    #[test]
1121    fn test_resolve_nothing_holds_but_something_unknown() {
1122        let eval = MockEvaluator::new().with_condition(1, CR::False);
1123        let r = resolve(&eval, "Muss [1]\nSoll [2]");
1124        assert_eq!(r.result, CR::Unknown);
1125        assert_eq!(r.unknown_ids, vec![2]);
1126    }
1127
1128    #[test]
1129    fn test_resolve_no_line_holds_reports_strictest_status() {
1130        let eval = MockEvaluator::new()
1131            .with_condition(1, CR::False)
1132            .with_condition(2, CR::False);
1133        let r = resolve(&eval, "Soll [1]\nMuss [2]");
1134        assert_eq!((r.result, r.kind), (CR::False, Some(StatusKind::Muss)));
1135    }
1136}