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