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    fn collect_unknown_ids(&self, expr: &ConditionExpr, ctx: &EvaluationContext) -> Vec<u32> {
385        let mut ids = Vec::new();
386        self.collect_unknown_ids_inner(expr, ctx, &mut ids);
387        ids
388    }
389
390    fn collect_unknown_ids_inner(
391        &self,
392        expr: &ConditionExpr,
393        ctx: &EvaluationContext,
394        ids: &mut Vec<u32>,
395    ) {
396        match expr {
397            ConditionExpr::Ref(id) => {
398                if self.evaluator.evaluate(*id, ctx).is_unknown() {
399                    ids.push(*id);
400                }
401            }
402            ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
403                for e in exprs {
404                    self.collect_unknown_ids_inner(e, ctx, ids);
405                }
406            }
407            ConditionExpr::Xor(left, right) => {
408                self.collect_unknown_ids_inner(left, ctx, ids);
409                self.collect_unknown_ids_inner(right, ctx, ids);
410            }
411            ConditionExpr::Not(inner) => {
412                self.collect_unknown_ids_inner(inner, ctx, ids);
413            }
414            ConditionExpr::Package { .. } => {
415                // Package constraints have no condition IDs to collect
416            }
417        }
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::super::evaluator::{ConditionResult as CR, NoOpExternalProvider};
424
425    #[test]
426    fn requiredness_drops_notes_formats_and_unexpanded_ub() {
427        use crate::expr::ConditionParser;
428        let reduce = |status: &str| {
429            ConditionParser::parse(status)
430                .unwrap()
431                .and_then(|e| requiredness(&e))
432        };
433        assert_eq!(reduce("X ([10]) ∧ ([UB1])"), Some(ConditionExpr::Ref(10)));
434        assert_eq!(reduce("X [914] ∧ [937]"), None);
435        assert_eq!(reduce("X [UB1]"), None);
436        assert_eq!(
437            reduce("X ([914] ∧ [930] [140]) ⊻ ([926] ∧ [937] [172])"),
438            Some(ConditionExpr::Xor(
439                Box::new(ConditionExpr::Ref(140)),
440                Box::new(ConditionExpr::Ref(172))
441            ))
442        );
443        assert_eq!(
444            reduce("Muss [356] ∧ [2061]"),
445            Some(ConditionExpr::And(vec![
446                ConditionExpr::Ref(356),
447                ConditionExpr::Ref(2061)
448            ]))
449        );
450    }
451
452    #[test]
453    fn a_package_without_a_minimum_requires_nothing() {
454        use crate::expr::ConditionParser;
455        let parse = |s: &str| ConditionParser::parse(s).unwrap().unwrap();
456        assert!(packages_allow_none(&parse("S [9P0..1]")));
457        assert!(!packages_allow_none(&parse("X [4P1..1]")));
458        assert!(!packages_allow_none(&parse("X [4P0..1] ∧ [12]")));
459    }
460    use super::*;
461    use mig_types::segment::OwnedSegment;
462    use std::collections::HashMap;
463
464    /// A mock condition evaluator for testing.
465    struct MockEvaluator {
466        results: HashMap<u32, ConditionResult>,
467        external_ids: Vec<u32>,
468    }
469
470    impl MockEvaluator {
471        fn new() -> Self {
472            Self {
473                results: HashMap::new(),
474                external_ids: Vec::new(),
475            }
476        }
477
478        fn with_condition(mut self, id: u32, result: ConditionResult) -> Self {
479            self.results.insert(id, result);
480            self
481        }
482    }
483
484    impl ConditionEvaluator for MockEvaluator {
485        fn evaluate(&self, condition: u32, _ctx: &EvaluationContext) -> ConditionResult {
486            self.results
487                .get(&condition)
488                .copied()
489                .unwrap_or(ConditionResult::Unknown)
490        }
491
492        fn is_external(&self, condition: u32) -> bool {
493            self.external_ids.contains(&condition)
494        }
495
496        fn message_type(&self) -> &str {
497            "TEST"
498        }
499
500        fn format_version(&self) -> &str {
501            "FV_TEST"
502        }
503    }
504
505    fn empty_context() -> (NoOpExternalProvider, Vec<OwnedSegment>) {
506        (NoOpExternalProvider, Vec::new())
507    }
508
509    fn make_ctx<'a>(
510        external: &'a NoOpExternalProvider,
511        segments: &'a [OwnedSegment],
512    ) -> EvaluationContext<'a> {
513        EvaluationContext::new("11001", external, segments)
514    }
515
516    // === Single condition ===
517
518    #[test]
519    fn test_eval_single_true() {
520        let eval = MockEvaluator::new().with_condition(1, CR::True);
521        let (ext, segs) = empty_context();
522        let ctx = make_ctx(&ext, &segs);
523        let expr_eval = ConditionExprEvaluator::new(&eval);
524
525        assert_eq!(expr_eval.evaluate(&ConditionExpr::Ref(1), &ctx), CR::True);
526    }
527
528    #[test]
529    fn test_eval_single_false() {
530        let eval = MockEvaluator::new().with_condition(1, CR::False);
531        let (ext, segs) = empty_context();
532        let ctx = make_ctx(&ext, &segs);
533        let expr_eval = ConditionExprEvaluator::new(&eval);
534
535        assert_eq!(expr_eval.evaluate(&ConditionExpr::Ref(1), &ctx), CR::False);
536    }
537
538    #[test]
539    fn test_eval_single_unknown() {
540        let eval = MockEvaluator::new(); // No condition registered -> Unknown
541        let (ext, segs) = empty_context();
542        let ctx = make_ctx(&ext, &segs);
543        let expr_eval = ConditionExprEvaluator::new(&eval);
544
545        assert_eq!(
546            expr_eval.evaluate(&ConditionExpr::Ref(999), &ctx),
547            CR::Unknown
548        );
549    }
550
551    // === AND ===
552
553    #[test]
554    fn test_eval_and_both_true() {
555        let eval = MockEvaluator::new()
556            .with_condition(1, CR::True)
557            .with_condition(2, CR::True);
558        let (ext, segs) = empty_context();
559        let ctx = make_ctx(&ext, &segs);
560        let expr_eval = ConditionExprEvaluator::new(&eval);
561
562        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
563        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
564    }
565
566    #[test]
567    fn test_eval_and_one_false_short_circuits() {
568        let eval = MockEvaluator::new()
569            .with_condition(1, CR::False)
570            .with_condition(2, CR::True);
571        let (ext, segs) = empty_context();
572        let ctx = make_ctx(&ext, &segs);
573        let expr_eval = ConditionExprEvaluator::new(&eval);
574
575        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
576        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
577    }
578
579    #[test]
580    fn test_eval_and_one_unknown_true_gives_unknown() {
581        let eval = MockEvaluator::new()
582            .with_condition(1, CR::True)
583            .with_condition(2, CR::Unknown);
584        let (ext, segs) = empty_context();
585        let ctx = make_ctx(&ext, &segs);
586        let expr_eval = ConditionExprEvaluator::new(&eval);
587
588        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
589        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
590    }
591
592    #[test]
593    fn test_eval_and_false_beats_unknown() {
594        // AND with False and Unknown should be False (short-circuit)
595        let eval = MockEvaluator::new()
596            .with_condition(1, CR::False)
597            .with_condition(2, CR::Unknown);
598        let (ext, segs) = empty_context();
599        let ctx = make_ctx(&ext, &segs);
600        let expr_eval = ConditionExprEvaluator::new(&eval);
601
602        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
603        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
604    }
605
606    #[test]
607    fn test_eval_and_three_way() {
608        let eval = MockEvaluator::new()
609            .with_condition(182, CR::True)
610            .with_condition(6, CR::True)
611            .with_condition(570, CR::True);
612        let (ext, segs) = empty_context();
613        let ctx = make_ctx(&ext, &segs);
614        let expr_eval = ConditionExprEvaluator::new(&eval);
615
616        let expr = ConditionExpr::And(vec![
617            ConditionExpr::Ref(182),
618            ConditionExpr::Ref(6),
619            ConditionExpr::Ref(570),
620        ]);
621        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
622    }
623
624    #[test]
625    fn test_eval_and_three_way_one_false() {
626        let eval = MockEvaluator::new()
627            .with_condition(182, CR::True)
628            .with_condition(6, CR::True)
629            .with_condition(570, CR::False);
630        let (ext, segs) = empty_context();
631        let ctx = make_ctx(&ext, &segs);
632        let expr_eval = ConditionExprEvaluator::new(&eval);
633
634        let expr = ConditionExpr::And(vec![
635            ConditionExpr::Ref(182),
636            ConditionExpr::Ref(6),
637            ConditionExpr::Ref(570),
638        ]);
639        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
640    }
641
642    // === OR ===
643
644    #[test]
645    fn test_eval_or_both_false() {
646        let eval = MockEvaluator::new()
647            .with_condition(1, CR::False)
648            .with_condition(2, CR::False);
649        let (ext, segs) = empty_context();
650        let ctx = make_ctx(&ext, &segs);
651        let expr_eval = ConditionExprEvaluator::new(&eval);
652
653        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
654        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
655    }
656
657    #[test]
658    fn test_eval_or_one_true_short_circuits() {
659        let eval = MockEvaluator::new()
660            .with_condition(1, CR::False)
661            .with_condition(2, CR::True);
662        let (ext, segs) = empty_context();
663        let ctx = make_ctx(&ext, &segs);
664        let expr_eval = ConditionExprEvaluator::new(&eval);
665
666        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
667        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
668    }
669
670    #[test]
671    fn test_eval_or_true_beats_unknown() {
672        let eval = MockEvaluator::new()
673            .with_condition(1, CR::Unknown)
674            .with_condition(2, CR::True);
675        let (ext, segs) = empty_context();
676        let ctx = make_ctx(&ext, &segs);
677        let expr_eval = ConditionExprEvaluator::new(&eval);
678
679        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
680        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
681    }
682
683    #[test]
684    fn test_eval_or_false_and_unknown_gives_unknown() {
685        let eval = MockEvaluator::new()
686            .with_condition(1, CR::False)
687            .with_condition(2, CR::Unknown);
688        let (ext, segs) = empty_context();
689        let ctx = make_ctx(&ext, &segs);
690        let expr_eval = ConditionExprEvaluator::new(&eval);
691
692        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
693        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
694    }
695
696    // === XOR ===
697
698    #[test]
699    fn test_eval_xor_true_false() {
700        let eval = MockEvaluator::new()
701            .with_condition(1, CR::True)
702            .with_condition(2, CR::False);
703        let (ext, segs) = empty_context();
704        let ctx = make_ctx(&ext, &segs);
705        let expr_eval = ConditionExprEvaluator::new(&eval);
706
707        let expr = ConditionExpr::Xor(
708            Box::new(ConditionExpr::Ref(1)),
709            Box::new(ConditionExpr::Ref(2)),
710        );
711        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
712    }
713
714    #[test]
715    fn test_eval_xor_both_true() {
716        let eval = MockEvaluator::new()
717            .with_condition(1, CR::True)
718            .with_condition(2, CR::True);
719        let (ext, segs) = empty_context();
720        let ctx = make_ctx(&ext, &segs);
721        let expr_eval = ConditionExprEvaluator::new(&eval);
722
723        let expr = ConditionExpr::Xor(
724            Box::new(ConditionExpr::Ref(1)),
725            Box::new(ConditionExpr::Ref(2)),
726        );
727        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
728    }
729
730    #[test]
731    fn test_eval_xor_both_false() {
732        let eval = MockEvaluator::new()
733            .with_condition(1, CR::False)
734            .with_condition(2, CR::False);
735        let (ext, segs) = empty_context();
736        let ctx = make_ctx(&ext, &segs);
737        let expr_eval = ConditionExprEvaluator::new(&eval);
738
739        let expr = ConditionExpr::Xor(
740            Box::new(ConditionExpr::Ref(1)),
741            Box::new(ConditionExpr::Ref(2)),
742        );
743        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
744    }
745
746    #[test]
747    fn test_eval_xor_unknown_propagates() {
748        let eval = MockEvaluator::new()
749            .with_condition(1, CR::True)
750            .with_condition(2, CR::Unknown);
751        let (ext, segs) = empty_context();
752        let ctx = make_ctx(&ext, &segs);
753        let expr_eval = ConditionExprEvaluator::new(&eval);
754
755        let expr = ConditionExpr::Xor(
756            Box::new(ConditionExpr::Ref(1)),
757            Box::new(ConditionExpr::Ref(2)),
758        );
759        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
760    }
761
762    // === NOT ===
763
764    #[test]
765    fn test_eval_not_true() {
766        let eval = MockEvaluator::new().with_condition(1, CR::True);
767        let (ext, segs) = empty_context();
768        let ctx = make_ctx(&ext, &segs);
769        let expr_eval = ConditionExprEvaluator::new(&eval);
770
771        let expr = ConditionExpr::Not(Box::new(ConditionExpr::Ref(1)));
772        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
773    }
774
775    #[test]
776    fn test_eval_not_false() {
777        let eval = MockEvaluator::new().with_condition(1, CR::False);
778        let (ext, segs) = empty_context();
779        let ctx = make_ctx(&ext, &segs);
780        let expr_eval = ConditionExprEvaluator::new(&eval);
781
782        let expr = ConditionExpr::Not(Box::new(ConditionExpr::Ref(1)));
783        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
784    }
785
786    #[test]
787    fn test_eval_not_unknown() {
788        let eval = MockEvaluator::new(); // 1 -> Unknown
789        let (ext, segs) = empty_context();
790        let ctx = make_ctx(&ext, &segs);
791        let expr_eval = ConditionExprEvaluator::new(&eval);
792
793        let expr = ConditionExpr::Not(Box::new(ConditionExpr::Ref(1)));
794        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
795    }
796
797    // === Complex expressions ===
798
799    #[test]
800    fn test_eval_complex_nested() {
801        // (([1] ∧ [2]) ∨ ([3] ∧ [4])) ∧ [5]
802        // [1]=T, [2]=F, [3]=T, [4]=T, [5]=T
803        // ([1]∧[2])=F, ([3]∧[4])=T, F∨T=T, T∧[5]=T
804        let eval = MockEvaluator::new()
805            .with_condition(1, CR::True)
806            .with_condition(2, CR::False)
807            .with_condition(3, CR::True)
808            .with_condition(4, CR::True)
809            .with_condition(5, CR::True);
810        let (ext, segs) = empty_context();
811        let ctx = make_ctx(&ext, &segs);
812        let expr_eval = ConditionExprEvaluator::new(&eval);
813
814        let expr = ConditionExpr::And(vec![
815            ConditionExpr::Or(vec![
816                ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]),
817                ConditionExpr::And(vec![ConditionExpr::Ref(3), ConditionExpr::Ref(4)]),
818            ]),
819            ConditionExpr::Ref(5),
820        ]);
821        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
822    }
823
824    #[test]
825    fn test_eval_xor_with_nested_and() {
826        // ([102] ∧ [2006]) ⊻ ([103] ∧ [2005])
827        // [102]=T, [2006]=T, [103]=F, [2005]=F
828        // T∧T=T, F∧F=F, T⊻F=T
829        let eval = MockEvaluator::new()
830            .with_condition(102, CR::True)
831            .with_condition(2006, CR::True)
832            .with_condition(103, CR::False)
833            .with_condition(2005, 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::Xor(
839            Box::new(ConditionExpr::And(vec![
840                ConditionExpr::Ref(102),
841                ConditionExpr::Ref(2006),
842            ])),
843            Box::new(ConditionExpr::And(vec![
844                ConditionExpr::Ref(103),
845                ConditionExpr::Ref(2005),
846            ])),
847        );
848        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
849    }
850
851    // === evaluate_status ===
852
853    #[test]
854    fn test_evaluate_status_with_conditions() {
855        let eval = MockEvaluator::new()
856            .with_condition(182, CR::True)
857            .with_condition(152, CR::True);
858        let (ext, segs) = empty_context();
859        let ctx = make_ctx(&ext, &segs);
860        let expr_eval = ConditionExprEvaluator::new(&eval);
861
862        assert_eq!(
863            expr_eval.evaluate_status("Muss [182] ∧ [152]", &ctx),
864            CR::True
865        );
866    }
867
868    #[test]
869    fn test_evaluate_status_no_conditions() {
870        let eval = MockEvaluator::new();
871        let (ext, segs) = empty_context();
872        let ctx = make_ctx(&ext, &segs);
873        let expr_eval = ConditionExprEvaluator::new(&eval);
874
875        assert_eq!(expr_eval.evaluate_status("Muss", &ctx), CR::True);
876    }
877
878    #[test]
879    fn test_evaluate_status_empty() {
880        let eval = MockEvaluator::new();
881        let (ext, segs) = empty_context();
882        let ctx = make_ctx(&ext, &segs);
883        let expr_eval = ConditionExprEvaluator::new(&eval);
884
885        assert_eq!(expr_eval.evaluate_status("", &ctx), CR::True);
886    }
887
888    // === Unknown propagation comprehensive ===
889
890    #[test]
891    fn test_unknown_propagation_and_or_mix() {
892        // [1](Unknown) ∨ ([2](True) ∧ [3](Unknown))
893        // [2]∧[3] = Unknown (True ∧ Unknown)
894        // Unknown ∨ Unknown = Unknown
895        let eval = MockEvaluator::new().with_condition(2, CR::True);
896        // 1 and 3 default to Unknown
897        let (ext, segs) = empty_context();
898        let ctx = make_ctx(&ext, &segs);
899        let expr_eval = ConditionExprEvaluator::new(&eval);
900
901        let expr = ConditionExpr::Or(vec![
902            ConditionExpr::Ref(1),
903            ConditionExpr::And(vec![ConditionExpr::Ref(2), ConditionExpr::Ref(3)]),
904        ]);
905        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::Unknown);
906    }
907
908    #[test]
909    fn test_or_short_circuits_past_unknown() {
910        // [1](Unknown) ∨ [2](True) -> True (True short-circuits)
911        let eval = MockEvaluator::new().with_condition(2, CR::True);
912        let (ext, segs) = empty_context();
913        let ctx = make_ctx(&ext, &segs);
914        let expr_eval = ConditionExprEvaluator::new(&eval);
915
916        let expr = ConditionExpr::Or(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
917        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::True);
918    }
919
920    #[test]
921    fn test_and_short_circuits_past_unknown() {
922        // [1](Unknown) ∧ [2](False) -> False (False short-circuits)
923        let eval = MockEvaluator::new().with_condition(2, CR::False);
924        let (ext, segs) = empty_context();
925        let ctx = make_ctx(&ext, &segs);
926        let expr_eval = ConditionExprEvaluator::new(&eval);
927
928        let expr = ConditionExpr::And(vec![ConditionExpr::Ref(1), ConditionExpr::Ref(2)]);
929        assert_eq!(expr_eval.evaluate(&expr, &ctx), CR::False);
930    }
931
932    // === evaluate_status_detailed ===
933
934    #[test]
935    fn test_detailed_returns_unknown_ids() {
936        // [182]=True, [8]=Unknown → AND = Unknown, unknown_ids = [8]
937        let eval = MockEvaluator::new().with_condition(182, CR::True);
938        let (ext, segs) = empty_context();
939        let ctx = make_ctx(&ext, &segs);
940        let expr_eval = ConditionExprEvaluator::new(&eval);
941
942        let (result, unknown_ids) = expr_eval.evaluate_status_detailed("Muss [182] ∧ [8]", &ctx);
943        assert_eq!(result, CR::Unknown);
944        assert_eq!(unknown_ids, vec![8]);
945    }
946
947    #[test]
948    fn test_detailed_multiple_unknown_ids() {
949        // [1]=Unknown, [2]=True, [3]=Unknown → AND = Unknown, unknown_ids = [1, 3]
950        let eval = MockEvaluator::new().with_condition(2, CR::True);
951        let (ext, segs) = empty_context();
952        let ctx = make_ctx(&ext, &segs);
953        let expr_eval = ConditionExprEvaluator::new(&eval);
954
955        let (result, unknown_ids) =
956            expr_eval.evaluate_status_detailed("Muss [1] ∧ [2] ∧ [3]", &ctx);
957        assert_eq!(result, CR::Unknown);
958        assert_eq!(unknown_ids, vec![1, 3]);
959    }
960
961    #[test]
962    fn test_detailed_no_unknown_when_true() {
963        let eval = MockEvaluator::new()
964            .with_condition(182, CR::True)
965            .with_condition(152, CR::True);
966        let (ext, segs) = empty_context();
967        let ctx = make_ctx(&ext, &segs);
968        let expr_eval = ConditionExprEvaluator::new(&eval);
969
970        let (result, unknown_ids) = expr_eval.evaluate_status_detailed("Muss [182] ∧ [152]", &ctx);
971        assert_eq!(result, CR::True);
972        assert!(unknown_ids.is_empty());
973    }
974
975    #[test]
976    fn test_detailed_no_unknown_when_false() {
977        let eval = MockEvaluator::new().with_condition(182, CR::False);
978        let (ext, segs) = empty_context();
979        let ctx = make_ctx(&ext, &segs);
980        let expr_eval = ConditionExprEvaluator::new(&eval);
981
982        let (result, unknown_ids) = expr_eval.evaluate_status_detailed("Muss [182] ∧ [8]", &ctx);
983        assert_eq!(result, CR::False);
984        assert!(unknown_ids.is_empty());
985    }
986
987    // === resolve_status: several status lines ===
988
989    fn resolve(eval: &MockEvaluator, status: &str) -> StatusResolution {
990        let (ext, segs) = empty_context();
991        let ctx = make_ctx(&ext, &segs);
992        ConditionExprEvaluator::new(eval).resolve_status(status, &ctx, &Default::default())
993    }
994
995    #[test]
996    fn test_resolve_single_line_keeps_its_status() {
997        let eval = MockEvaluator::new().with_condition(1, CR::True);
998        let r = resolve(&eval, "Soll [1]");
999        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Soll)));
1000    }
1001
1002    #[test]
1003    fn test_resolve_only_later_line_holds_gives_its_status() {
1004        // Muss [2119] / Soll [130]: 130 alone makes the part recommended, not required.
1005        let eval = MockEvaluator::new()
1006            .with_condition(2119, CR::False)
1007            .with_condition(130, CR::True);
1008        let r = resolve(&eval, "Muss [2119]\r\nSoll [130]");
1009        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Soll)));
1010    }
1011
1012    #[test]
1013    fn test_resolve_several_lines_hold_strictest_wins() {
1014        let eval = MockEvaluator::new()
1015            .with_condition(47, CR::True)
1016            .with_condition(46, CR::True);
1017        let r = resolve(&eval, "Soll [47]\nMuss [46]");
1018        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Muss)));
1019    }
1020
1021    #[test]
1022    fn test_resolve_unconditional_fallback_line() {
1023        let eval = MockEvaluator::new().with_condition(48, CR::False);
1024        let r = resolve(&eval, "Muss [48]\nKann");
1025        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Kann)));
1026    }
1027
1028    #[test]
1029    fn test_resolve_package_needing_no_code_requires_nothing() {
1030        // 55052 STS+7 Ergänzung `X [17P0..1]`: at most one code of package
1031        // 17, possibly none. The field may be sent; it need not be.
1032        let eval = MockEvaluator::new();
1033        let r = resolve(&eval, "X [17P0..1]");
1034        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Kann)));
1035        let r = resolve(&eval, "X [1P1..1]");
1036        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Muss)));
1037    }
1038
1039    #[test]
1040    fn test_resolve_weaker_unknown_line_does_not_block() {
1041        // [130] unknown can at most add Soll; Muss already holds.
1042        let eval = MockEvaluator::new().with_condition(2119, CR::True);
1043        let r = resolve(&eval, "Muss [2119]\nSoll [130]");
1044        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Muss)));
1045        assert!(r.unknown_ids.is_empty());
1046    }
1047
1048    #[test]
1049    fn test_resolve_stronger_unknown_line_is_unknown() {
1050        // [2119] unknown could make it Muss; Soll alone does not decide it.
1051        let eval = MockEvaluator::new().with_condition(130, CR::True);
1052        let r = resolve(&eval, "Muss [2119]\nSoll [130]");
1053        assert_eq!(r.result, CR::Unknown);
1054        assert_eq!(r.unknown_ids, vec![2119]);
1055    }
1056
1057    #[test]
1058    fn test_resolve_equally_strong_unknown_line_does_not_block() {
1059        let eval = MockEvaluator::new().with_condition(2, CR::True);
1060        let r = resolve(&eval, "Muss [1]\nMuss [2]");
1061        assert_eq!((r.result, r.kind), (CR::True, Some(StatusKind::Muss)));
1062    }
1063
1064    #[test]
1065    fn test_resolve_nothing_holds_but_something_unknown() {
1066        let eval = MockEvaluator::new().with_condition(1, CR::False);
1067        let r = resolve(&eval, "Muss [1]\nSoll [2]");
1068        assert_eq!(r.result, CR::Unknown);
1069        assert_eq!(r.unknown_ids, vec![2]);
1070    }
1071
1072    #[test]
1073    fn test_resolve_no_line_holds_reports_strictest_status() {
1074        let eval = MockEvaluator::new()
1075            .with_condition(1, CR::False)
1076            .with_condition(2, CR::False);
1077        let r = resolve(&eval, "Soll [1]\nMuss [2]");
1078        assert_eq!((r.result, r.kind), (CR::False, Some(StatusKind::Muss)));
1079    }
1080}