use actionqueue_core::ids::TaskId;
use actionqueue_core::run::state::RunState;
use actionqueue_core::task::run_policy::{RunPolicy, RunPolicyError};
use actionqueue_engine::derive::{derive_runs, DerivationError};
#[test]
fn repeat_policy_creates_correct_count() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(5, 60).expect("repeat policy should be valid");
let result = derive_runs(&clock, task_id, &run_policy, 0, 0).unwrap();
assert_eq!(result.derived().len(), 5, "Repeat policy must create exactly 5 runs");
assert_eq!(result.already_derived(), 5, "Repeat policy must report 5 as already_derived");
assert_eq!(result.derived()[0].scheduled_at(), 0);
assert_eq!(result.derived()[1].scheduled_at(), 60);
assert_eq!(result.derived()[2].scheduled_at(), 120);
assert_eq!(result.derived()[3].scheduled_at(), 180);
assert_eq!(result.derived()[4].scheduled_at(), 240);
for run in result.derived() {
assert_eq!(run.state(), RunState::Scheduled, "Each run must be in Scheduled state");
assert_eq!(run.task_id(), task_id, "Each run must have correct task_id");
}
}
#[test]
fn repeat_policy_does_not_duplicate() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(10, 30).expect("repeat policy should be valid");
let result1 = derive_runs(&clock, task_id, &run_policy, 0, 0).unwrap();
assert_eq!(result1.derived().len(), 10, "First derivation must create 10 runs");
let result2 = derive_runs(&clock, task_id, &run_policy, 5, 0).unwrap();
assert_eq!(result2.derived().len(), 5, "Second derivation must create only missing runs");
assert_eq!(result2.already_derived(), 10, "already_derived must be updated to total count");
assert_eq!(result2.derived()[0].scheduled_at(), 150); assert_eq!(result2.derived()[1].scheduled_at(), 180); assert_eq!(result2.derived()[2].scheduled_at(), 210); assert_eq!(result2.derived()[3].scheduled_at(), 240); assert_eq!(result2.derived()[4].scheduled_at(), 270);
let result3 = derive_runs(&clock, task_id, &run_policy, 10, 0).unwrap();
assert_eq!(result3.derived().len(), 0, "Third derivation must create no new runs");
assert_eq!(result3.already_derived(), 10, "already_derived must remain 10");
}
#[test]
fn repeat_policy_accounting_deterministic_with_multiple_tasks() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id1 = TaskId::new();
let task_id2 = TaskId::new();
let task_id3 = TaskId::new();
let run_policy = RunPolicy::repeat(3, 120).expect("repeat policy should be valid");
let result1 = derive_runs(&clock, task_id1, &run_policy, 0, 0).unwrap();
assert_eq!(result1.derived().len(), 3);
let run1_id = result1.derived()[0].id();
let result2 = derive_runs(&clock, task_id2, &run_policy, 0, 0).unwrap();
assert_eq!(result2.derived().len(), 3);
assert_ne!(result2.derived()[0].id(), run1_id, "Different tasks must have different run IDs");
let result3 = derive_runs(&clock, task_id1, &run_policy, 3, 0).unwrap();
assert_eq!(result3.derived().len(), 0, "Task 1 already has 3 runs, must not create more");
let result4 = derive_runs(&clock, task_id3, &run_policy, 0, 0).unwrap();
assert_eq!(result4.derived().len(), 3);
assert_ne!(result4.derived()[0].id(), run1_id);
assert_ne!(result4.derived()[0].id(), result2.derived()[0].id());
}
#[test]
fn repeat_policy_accounting_stable_with_clock_advancement() {
let mut clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(5, 60).expect("repeat policy should be valid");
let result1 = derive_runs(&clock, task_id, &run_policy, 0, 0).unwrap();
assert_eq!(result1.derived().len(), 5);
assert_eq!(result1.derived()[0].scheduled_at(), 0);
assert_eq!(result1.derived()[1].scheduled_at(), 60);
assert_eq!(result1.derived()[2].scheduled_at(), 120);
assert_eq!(result1.derived()[3].scheduled_at(), 180);
assert_eq!(result1.derived()[4].scheduled_at(), 240);
clock.advance_by(600);
let result2 = derive_runs(&clock, task_id, &run_policy, 2, 0).unwrap();
assert_eq!(result2.derived().len(), 3);
assert_eq!(result2.derived()[0].scheduled_at(), 120);
assert_eq!(result2.derived()[1].scheduled_at(), 180);
assert_eq!(result2.derived()[2].scheduled_at(), 240);
assert_eq!(result1.derived()[0].scheduled_at(), 0, "Scheduled time must not change");
}
#[test]
fn repeat_policy_zero_count_is_rejected_at_construction() {
use actionqueue_core::task::run_policy::RepeatPolicy;
let result = RepeatPolicy::new(0, 60);
assert_eq!(result, Err(RunPolicyError::InvalidRepeatCount { count: 0 }));
}
#[test]
fn repeat_policy_zero_interval_is_rejected_at_construction() {
use actionqueue_core::task::run_policy::RepeatPolicy;
let result = RepeatPolicy::new(3, 0);
assert_eq!(result, Err(RunPolicyError::InvalidRepeatIntervalSecs { interval_secs: 0 }));
}
#[test]
fn repeat_policy_accounting_consistent_with_policy() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(4, 90).expect("repeat policy should be valid");
let r1 = derive_runs(&clock, task_id, &run_policy, 0, 0).unwrap();
assert_eq!(r1.derived().len(), 4);
assert_eq!(r1.already_derived(), 4);
for _ in 1..=10 {
let r = derive_runs(&clock, task_id, &run_policy, 4, 0).unwrap();
assert_eq!(r.derived().len(), 0, "No runs must be derived on repeat calls");
assert_eq!(r.already_derived(), 4, "already_derived must remain 4");
}
let _run_ids: Vec<_> = r1.derived().iter().map(|r| r.id()).collect();
let r_final = derive_runs(&clock, task_id, &run_policy, 4, 0).unwrap();
assert_eq!(r_final.derived().len(), 0);
assert_eq!(r_final.already_derived(), 4);
}
#[test]
fn repeat_policy_respects_schedule_origin() {
let clock = actionqueue_engine::time::clock::MockClock::new(2000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(3, 100).expect("repeat policy should be valid");
let result1 = derive_runs(&clock, task_id, &run_policy, 0, 500).unwrap();
assert_eq!(result1.derived().len(), 3);
assert_eq!(result1.derived()[0].scheduled_at(), 500);
assert_eq!(result1.derived()[1].scheduled_at(), 600);
assert_eq!(result1.derived()[2].scheduled_at(), 700);
let result2 = derive_runs(&clock, task_id, &run_policy, 0, 1000).unwrap();
assert_eq!(result2.derived().len(), 3);
assert_eq!(result2.derived()[0].scheduled_at(), 1000);
assert_eq!(result2.derived()[1].scheduled_at(), 1100);
assert_eq!(result2.derived()[2].scheduled_at(), 1200);
}
#[test]
fn repeat_policy_large_count_returns_error_on_overflow() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy =
RunPolicy::repeat(u32::MAX, u64::MAX / 2).expect("repeat policy should be valid");
let result = derive_runs(&clock, task_id, &run_policy, 0, 0);
assert!(result.is_err(), "Large count with interval overflow should return error");
match result {
Err(DerivationError::ArithmeticOverflow { policy, operation }) => {
assert_eq!(policy, "Repeat");
assert!(operation.contains("multiplication") || operation.contains("addition"));
}
_ => panic!("Expected arithmetic overflow error"),
}
}
#[test]
fn repeat_policy_large_interval_does_not_overflow() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(3, u64::MAX / 2).expect("repeat policy should be valid");
let result = derive_runs(&clock, task_id, &run_policy, 0, 0).unwrap();
assert_eq!(result.derived().len(), 3);
assert_eq!(result.already_derived(), 3);
assert_eq!(result.derived()[0].scheduled_at(), 0);
assert_eq!(result.derived()[1].scheduled_at(), u64::MAX / 2);
assert_eq!(result.derived()[2].scheduled_at(), u64::MAX - 1);
}
#[test]
fn repeat_policy_large_origin_returns_error_on_overflow() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(3, 60).expect("repeat policy should be valid");
let large_origin = u64::MAX - 100;
let result = derive_runs(&clock, task_id, &run_policy, 0, large_origin);
assert!(result.is_err(), "Large origin with addition overflow should return error");
match result {
Err(DerivationError::ArithmeticOverflow { policy, operation }) => {
assert_eq!(policy, "Repeat");
assert!(operation.contains("addition"), "Operation should mention addition");
}
_ => panic!("Expected arithmetic overflow error"),
}
}
#[test]
fn repeat_policy_large_values_boundary_consistency() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let policy1 = RunPolicy::repeat(u32::MAX, u64::MAX / 2).expect("repeat policy should be valid");
let result1 = derive_runs(&clock, task_id, &policy1, 0, 0);
assert!(result1.is_err(), "Large count with large interval should return error");
match result1 {
Err(DerivationError::ArithmeticOverflow { policy, operation }) => {
assert_eq!(policy, "Repeat");
assert!(operation.contains("multiplication"));
}
_ => panic!("Expected arithmetic overflow error"),
}
let policy2 =
RunPolicy::repeat(10_000, u64::MAX / 10_000).expect("repeat policy should be valid");
let result2 = derive_runs(&clock, task_id, &policy2, 0, 0).unwrap();
assert_eq!(result2.derived().len(), 10_000);
assert_eq!(result2.derived()[0].scheduled_at(), 0);
assert_eq!(result2.derived()[1].scheduled_at(), u64::MAX / 10_000);
assert_eq!(result2.derived()[9999].scheduled_at(), 9999 * (u64::MAX / 10_000));
assert_eq!(result2.already_derived(), 10_000);
let policy3 = RunPolicy::repeat(1000, 60).expect("repeat policy should be valid");
let result3 = derive_runs(&clock, task_id, &policy3, 0, 0).unwrap();
assert_eq!(result3.derived().len(), 1000);
assert_eq!(result3.already_derived(), 1000);
assert_eq!(result3.derived()[999].scheduled_at(), 999 * 60);
}
#[test]
fn repeat_policy_rejects_already_derived_exceeds_count() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(5, 60).expect("repeat policy should be valid");
let result = derive_runs(&clock, task_id, &run_policy, 10, 0);
assert!(result.is_err(), "already_derived (10) > count (5) should return an error");
match result {
Err(DerivationError::ArithmeticOverflow { policy, operation }) => {
assert_eq!(policy, "Repeat", "Policy should be Repeat");
assert!(
operation.contains("already_derived"),
"Operation should mention already_derived"
);
assert!(operation.contains("count"), "Operation should mention count");
}
_ => panic!("Expected arithmetic overflow error for invalid already_derived"),
}
}
#[test]
fn repeat_policy_boundary_already_derived_equals_count() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let run_policy = RunPolicy::repeat(5, 60).expect("repeat policy should be valid");
let result1 = derive_runs(&clock, task_id, &run_policy, 0, 0).unwrap();
assert_eq!(result1.derived().len(), 5);
assert_eq!(result1.already_derived(), 5);
let result2 = derive_runs(&clock, task_id, &run_policy, 5, 0).unwrap();
assert_eq!(result2.derived().len(), 0, "No new runs should be created");
assert_eq!(result2.already_derived(), 5, "already_derived should remain at count");
}
#[test]
fn repeat_policy_nil_task_id_returns_typed_error_without_partial_results() {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = "00000000-0000-0000-0000-000000000000"
.parse::<TaskId>()
.expect("nil task id literal must parse");
let run_policy = RunPolicy::repeat(4, 60).expect("repeat policy should be valid");
let result = derive_runs(&clock, task_id, &run_policy, 0, 1000);
assert_eq!(result, Err(DerivationError::InvalidTaskIdForRunConstruction { task_id }));
}
#[test]
fn repeat_policy_nil_task_id_does_not_panic() {
let outcome = std::panic::catch_unwind(|| {
let clock = actionqueue_engine::time::clock::MockClock::new(1000);
let task_id = "00000000-0000-0000-0000-000000000000"
.parse::<TaskId>()
.expect("nil task id literal must parse");
let run_policy = RunPolicy::repeat(4, 60).expect("repeat policy should be valid");
derive_runs(&clock, task_id, &run_policy, 0, 1000)
});
match outcome {
Ok(result) => {
let task_id = "00000000-0000-0000-0000-000000000000"
.parse::<TaskId>()
.expect("nil task id literal must parse");
assert_eq!(result, Err(DerivationError::InvalidTaskIdForRunConstruction { task_id }));
}
Err(_) => panic!("repeat derivation must return typed errors, never unwind"),
}
}