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