use fabula::prelude::*;
use fabula_memory::{MemGraph, MemValue};
#[test]
fn metric_before_gap_in_range() {
use fabula::pattern::MetricGap;
let mut g = MemGraph::new();
g.add_edge_bounded("ev1", "eventType", MemValue::Str("crisis".into()), 1, 4);
g.add_edge_bounded("ev2", "eventType", MemValue::Str("betrayal".into()), 8, 12);
g.set_time(3);
let pattern = PatternBuilder::new("test")
.stage("e1", |s| {
s.edge("e1", "eventType".into(), MemValue::Str("crisis".into()))
})
.stage("e2", |s| {
s.edge("e2", "eventType".into(), MemValue::Str("betrayal".into()))
})
.temporal_with_gap(
"e1",
AllenRelation::Before,
"e2",
MetricGap {
min: Some(3.0),
max: Some(10.0),
},
)
.build();
let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
engine.register(pattern);
g.set_time(3);
engine.on_edge_added(
&g,
&"ev1".into(),
&"eventType".into(),
&MemValue::Str("crisis".into()),
&Interval::new(1, 4),
);
g.set_time(10);
let events = engine.on_edge_added(
&g,
&"ev2".into(),
&"eventType".into(),
&MemValue::Str("betrayal".into()),
&Interval::new(8, 12),
);
let completed = events
.iter()
.filter(|e| matches!(e, SiftEvent::Completed { .. }))
.count();
assert_eq!(completed, 1, "gap=4 within [3,10] → match");
}
#[test]
fn metric_before_gap_too_far() {
use fabula::pattern::MetricGap;
let mut g = MemGraph::new();
g.add_edge_bounded("ev1", "eventType", MemValue::Str("crisis".into()), 1, 4);
g.add_edge_bounded("ev2", "eventType", MemValue::Str("betrayal".into()), 20, 25);
let pattern = PatternBuilder::new("test")
.stage("e1", |s| {
s.edge("e1", "eventType".into(), MemValue::Str("crisis".into()))
})
.stage("e2", |s| {
s.edge("e2", "eventType".into(), MemValue::Str("betrayal".into()))
})
.temporal_with_gap(
"e1",
AllenRelation::Before,
"e2",
MetricGap {
min: Some(3.0),
max: Some(10.0),
},
)
.build();
let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
engine.register(pattern);
g.set_time(3);
engine.on_edge_added(
&g,
&"ev1".into(),
&"eventType".into(),
&MemValue::Str("crisis".into()),
&Interval::new(1, 4),
);
g.set_time(22);
let events = engine.on_edge_added(
&g,
&"ev2".into(),
&"eventType".into(),
&MemValue::Str("betrayal".into()),
&Interval::new(20, 25),
);
let completed = events
.iter()
.filter(|e| matches!(e, SiftEvent::Completed { .. }))
.count();
assert_eq!(completed, 0, "gap=16 exceeds max=10 → no match");
}
#[test]
fn metric_before_gap_too_close() {
use fabula::pattern::MetricGap;
let mut g = MemGraph::new();
g.add_edge_bounded("ev1", "eventType", MemValue::Str("crisis".into()), 1, 4);
g.add_edge_bounded("ev2", "eventType", MemValue::Str("betrayal".into()), 5, 8);
let pattern = PatternBuilder::new("test")
.stage("e1", |s| {
s.edge("e1", "eventType".into(), MemValue::Str("crisis".into()))
})
.stage("e2", |s| {
s.edge("e2", "eventType".into(), MemValue::Str("betrayal".into()))
})
.temporal_with_gap(
"e1",
AllenRelation::Before,
"e2",
MetricGap {
min: Some(3.0),
max: Some(10.0),
},
)
.build();
let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
engine.register(pattern);
g.set_time(3);
engine.on_edge_added(
&g,
&"ev1".into(),
&"eventType".into(),
&MemValue::Str("crisis".into()),
&Interval::new(1, 4),
);
g.set_time(6);
let events = engine.on_edge_added(
&g,
&"ev2".into(),
&"eventType".into(),
&MemValue::Str("betrayal".into()),
&Interval::new(5, 8),
);
let completed = events
.iter()
.filter(|e| matches!(e, SiftEvent::Completed { .. }))
.count();
assert_eq!(completed, 0, "gap=1 below min=3 → no match");
}
#[test]
fn metric_open_ended_skips_gap_check() {
use fabula::pattern::MetricGap;
let mut g = MemGraph::new();
g.add_str("ev1", "eventType", "crisis", 1);
g.add_str("ev2", "eventType", "betrayal", 100);
g.set_time(100);
let pattern = PatternBuilder::new("test")
.stage("e1", |s| {
s.edge("e1", "eventType".into(), MemValue::Str("crisis".into()))
})
.stage("e2", |s| {
s.edge("e2", "eventType".into(), MemValue::Str("betrayal".into()))
})
.temporal_with_gap(
"e1",
AllenRelation::Before,
"e2",
MetricGap {
min: Some(3.0),
max: Some(10.0),
},
)
.build();
let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
engine.register(pattern);
assert_eq!(
engine.evaluate(&g).len(),
1,
"open-ended intervals: metric check skipped, Allen fallback passes"
);
}
#[test]
fn memvalue_cross_variant_ordering() {
assert!(MemValue::Node("z".into()) < MemValue::Str("a".into()));
assert!(MemValue::Str("z".into()) < MemValue::Num(0.0));
assert!(MemValue::Num(f64::MAX) < MemValue::Bool(false));
}
#[test]
fn memvalue_nan_comparisons() {
let nan = MemValue::Num(f64::NAN);
let lt = ValueConstraint::Lt(MemValue::Num(5.0));
assert!(!lt.matches(&nan), "NaN is not less than 5.0");
let gt = ValueConstraint::Gt(MemValue::Num(5.0));
assert!(!gt.matches(&nan), "NaN is not greater than 5.0");
let eq = ValueConstraint::Eq(MemValue::Num(f64::NAN));
assert!(!eq.matches(&nan), "NaN != NaN");
let between = ValueConstraint::Between(MemValue::Num(0.0), MemValue::Num(10.0));
assert!(!between.matches(&nan), "NaN is not between 0 and 10");
}
#[test]
fn between_reversed_bounds_never_matches() {
let c = ValueConstraint::Between(20, 10); assert!(!c.matches(&15), "nothing can be >= 20 AND <= 10");
assert!(!c.matches(&10));
assert!(!c.matches(&20));
assert!(!c.matches(&0));
assert!(!c.matches(&100));
}
#[test]
fn between_equal_bounds_matches_only_exact() {
let c = ValueConstraint::Between(5, 5);
assert!(c.matches(&5), "value == lo == hi should match");
assert!(!c.matches(&4));
assert!(!c.matches(&6));
}
#[test]
fn cross_variant_constraint_between() {
let c = ValueConstraint::Between(MemValue::Num(0.0), MemValue::Num(10.0));
assert!(
!c.matches(&MemValue::Str("5".into())),
"cross-variant Between comparison: Str is not between two Nums"
);
}
#[test]
fn unless_global_single_stage_works() {
let mut g = MemGraph::new();
g.add_str("ev1", "eventType", "crime", 1);
g.add_ref("ev1", "actor", "alice", 1);
g.add_str("ev2", "eventType", "pardon", 2);
g.add_ref("ev2", "actor", "alice", 2);
g.set_time(10);
let pattern = PatternBuilder::new("unpardoned_crime")
.stage("e1", |s| {
s.edge("e1", "eventType".into(), MemValue::Str("crime".into()))
.edge_bind("e1", "actor".into(), "criminal")
})
.unless_global(|neg| {
neg.edge("p", "eventType".into(), MemValue::Str("pardon".into()))
.edge_bind("p", "actor".into(), "criminal")
})
.build();
let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
engine.register(pattern);
let matches = engine.evaluate(&g);
assert_eq!(
matches.len(),
0,
"pardon should block the crime via unless_global"
);
}
#[test]
fn negation_before_window_start_does_not_block() {
let mut g = MemGraph::new();
g.add_str("ev0", "eventType", "pardon", 0); g.add_ref("ev0", "actor", "alice", 0);
g.add_str("ev1", "eventType", "crime", 1);
g.add_ref("ev1", "actor", "alice", 1);
g.add_str("ev2", "eventType", "crime", 3);
g.add_ref("ev2", "actor", "alice", 3);
g.set_time(10);
let pattern = PatternBuilder::new("double_crime")
.stage("e1", |s| {
s.edge("e1", "eventType".into(), MemValue::Str("crime".into()))
.edge_bind("e1", "actor".into(), "criminal")
})
.stage("e2", |s| {
s.edge("e2", "eventType".into(), MemValue::Str("crime".into()))
.edge_bind("e2", "actor".into(), "criminal")
})
.unless_between("e1", "e2", |neg| {
neg.edge("p", "eventType".into(), MemValue::Str("pardon".into()))
.edge_bind("p", "actor".into(), "criminal")
})
.build();
let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
engine.register(pattern);
assert_eq!(
engine.evaluate(&g).len(),
1,
"negation event before window start should not block"
);
}
#[test]
fn negation_at_exact_window_boundary() {
let mut g = MemGraph::new();
g.add_str("ev1", "eventType", "crime", 1);
g.add_ref("ev1", "actor", "alice", 1);
g.add_str("ev_pardon", "eventType", "pardon", 1); g.add_ref("ev_pardon", "actor", "alice", 1);
g.add_str("ev2", "eventType", "crime", 3);
g.add_ref("ev2", "actor", "alice", 3);
g.set_time(10);
let pattern = PatternBuilder::new("double_crime")
.stage("e1", |s| {
s.edge("e1", "eventType".into(), MemValue::Str("crime".into()))
.edge_bind("e1", "actor".into(), "criminal")
})
.stage("e2", |s| {
s.edge("e2", "eventType".into(), MemValue::Str("crime".into()))
.edge_bind("e2", "actor".into(), "criminal")
})
.unless_between("e1", "e2", |neg| {
neg.edge("p", "eventType".into(), MemValue::Str("pardon".into()))
.edge_bind("p", "actor".into(), "criminal")
})
.build();
let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
engine.register(pattern);
let matches = engine.evaluate(&g);
assert_eq!(
matches.len(),
1,
"event at exact window start is outside exclusive window — match succeeds"
);
g.add_str("ev_pardon2", "eventType", "pardon", 2);
g.add_ref("ev_pardon2", "actor", "alice", 2);
let matches2 = engine.evaluate(&g);
assert_eq!(
matches2.len(),
0,
"pardon at t=2 is strictly between 1 and 3 — blocks match"
);
}