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