use actionqueue_core::ids::TaskId;
use actionqueue_core::run::run_instance::RunInstance;
use actionqueue_core::task::run_policy::RunPolicyError;
use crate::derive::{map_run_construction_error, DerivationError, DerivationSuccess};
use crate::time::clock::Clock;
pub struct RepeatDerivationParams {
count: u32,
interval_secs: u64,
already_derived: u32,
schedule_origin: u64,
}
impl RepeatDerivationParams {
pub fn new(count: u32, interval_secs: u64, already_derived: u32, schedule_origin: u64) -> Self {
Self { count, interval_secs, already_derived, schedule_origin }
}
pub fn count(&self) -> u32 {
self.count
}
pub fn interval_secs(&self) -> u64 {
self.interval_secs
}
pub fn already_derived(&self) -> u32 {
self.already_derived
}
pub fn schedule_origin(&self) -> u64 {
self.schedule_origin
}
}
pub fn derive_repeat(
clock: &impl Clock,
task_id: TaskId,
params: RepeatDerivationParams,
) -> Result<DerivationSuccess, DerivationError> {
let RepeatDerivationParams { count, interval_secs, already_derived, schedule_origin } = params;
if count == 0 {
return Err(DerivationError::InvalidPolicy {
source: RunPolicyError::InvalidRepeatCount { count },
});
}
if interval_secs == 0 {
return Err(DerivationError::InvalidPolicy {
source: RunPolicyError::InvalidRepeatIntervalSecs { interval_secs },
});
}
let now = clock.now();
let mut derived = Vec::new();
for i in already_derived..count {
let idx: u64 = i as u64;
let product = match idx.checked_mul(interval_secs) {
Some(p) => p,
None => {
return Err(DerivationError::ArithmeticOverflow {
policy: "Repeat".to_string(),
operation: format!("multiplication: index {idx} * interval {interval_secs}"),
});
}
};
let scheduled_at = match schedule_origin.checked_add(product) {
Some(sa) => sa,
None => {
return Err(DerivationError::ArithmeticOverflow {
policy: "Repeat".to_string(),
operation: format!(
"addition: schedule_origin {schedule_origin} + product {product}"
),
});
}
};
let run = RunInstance::new_scheduled(task_id, scheduled_at, now)
.map_err(|source| map_run_construction_error(task_id, source))?;
derived.push(run);
}
Ok(DerivationSuccess::new(derived, count))
}
#[cfg(test)]
mod tests {
use super::*;
fn params(
count: u32,
interval_secs: u64,
already_derived: u32,
schedule_origin: u64,
) -> RepeatDerivationParams {
RepeatDerivationParams::new(count, interval_secs, already_derived, schedule_origin)
}
#[test]
fn derive_repeat_creates_correct_count() {
let clock = crate::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let result = derive_repeat(&clock, task_id, params(3, 60, 0, 1000)).unwrap();
assert_eq!(result.derived().len(), 3);
assert_eq!(result.already_derived(), 3);
assert_eq!(result.derived()[0].scheduled_at(), 1000); assert_eq!(result.derived()[1].scheduled_at(), 1060); assert_eq!(result.derived()[2].scheduled_at(), 1120); }
#[test]
fn derive_repeat_does_not_duplicate() {
let clock = crate::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let result1 = derive_repeat(&clock, task_id, params(5, 60, 0, 1000)).unwrap();
assert_eq!(result1.derived().len(), 5);
let result2 = derive_repeat(&clock, task_id, params(5, 60, 2, 1000)).unwrap();
assert_eq!(result2.derived().len(), 3);
assert_eq!(result2.already_derived(), 5);
}
#[test]
fn derive_repeat_rejects_zero_count() {
let clock = crate::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let result = derive_repeat(&clock, task_id, params(0, 60, 0, 1000));
assert_eq!(
result,
Err(DerivationError::InvalidPolicy {
source: RunPolicyError::InvalidRepeatCount { count: 0 },
})
);
}
#[test]
fn derive_repeat_rejects_zero_interval() {
let clock = crate::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let result = derive_repeat(&clock, task_id, params(3, 0, 0, 1000));
assert_eq!(
result,
Err(DerivationError::InvalidPolicy {
source: RunPolicyError::InvalidRepeatIntervalSecs { interval_secs: 0 },
})
);
}
#[test]
fn derive_repeat_remains_stable_when_clock_advances() {
let mut clock = crate::time::clock::MockClock::new(1000);
let task_id = TaskId::new();
let first = derive_repeat(&clock, task_id, params(4, 60, 0, 900)).unwrap();
assert_eq!(first.derived().len(), 4);
assert_eq!(first.derived()[0].scheduled_at(), 900);
assert_eq!(first.derived()[1].scheduled_at(), 960);
assert_eq!(first.derived()[2].scheduled_at(), 1020);
assert_eq!(first.derived()[3].scheduled_at(), 1080);
clock.advance_by(600);
let second = derive_repeat(&clock, task_id, params(4, 60, 2, 900)).unwrap();
assert_eq!(second.derived().len(), 2);
assert_eq!(second.derived()[0].scheduled_at(), 1020);
assert_eq!(second.derived()[1].scheduled_at(), 1080);
}
#[test]
fn derive_repeat_returns_typed_error_for_nil_task_id_without_partial_derivation() {
let clock = crate::time::clock::MockClock::new(1000);
let task_id = "00000000-0000-0000-0000-000000000000"
.parse::<TaskId>()
.expect("nil task id literal must parse");
let result = derive_repeat(&clock, task_id, params(4, 60, 0, 1000));
assert_eq!(result, Err(DerivationError::InvalidTaskIdForRunConstruction { task_id }));
}
}