tiny_counter/store/
limiter.rs

1/// Rate limiting with fluent constraint API.
2use std::fmt;
3use std::sync::Arc;
4
5use chrono::{DateTime, Datelike, Duration, Local, Timelike, Utc, Weekday};
6
7use crate::{Error, LimitExceeded, Result, TimeUnit, TimeWindow};
8
9use super::inner::EventStoreInner;
10use super::query::Query;
11use super::EventId;
12
13/// Fluent builder for rate limiting with multiple constraints.
14///
15/// Created by `EventStore::limit()`, this allows chaining multiple constraints
16/// that must all pass before an event can be recorded.
17pub struct Limiter {
18    store: Arc<EventStoreInner>,
19    constraints: Vec<Constraint>,
20}
21
22/// A rate limiting constraint.
23#[derive(Debug, Clone)]
24pub enum Constraint {
25    /// Event must not exceed count within time window.
26    AtMost {
27        event_id: String,
28        limit: u32,
29        window_count: usize,
30        time_unit: TimeUnit,
31    },
32    /// Event must have been recorded at least count times within time window.
33    AtLeast {
34        event_id: String,
35        count: u32,
36        window_count: usize,
37        time_unit: TimeUnit,
38    },
39    /// Event must wait duration since last occurrence.
40    Cooldown {
41        event_id: String,
42        duration: Duration,
43    },
44    /// Prerequisite event must have occurred within duration.
45    Within {
46        prerequisite_event: String,
47        duration: Duration,
48    },
49    /// Current time must be within schedule.
50    During { schedule: Schedule },
51    /// Current time must be outside schedule.
52    OutsideOf { schedule: Schedule },
53}
54
55/// A time-based schedule for constraint checking.
56#[derive(Clone)]
57pub enum Schedule {
58    /// Business hours (e.g., 9am-5pm).
59    /// When `use_local_tz` is true, hours are interpreted in the system's local timezone.
60    /// When false, hours are interpreted in UTC.
61    Hours {
62        start_hour: u8,
63        end_hour: u8,
64        use_local_tz: bool,
65    },
66
67    /// Monday through Friday.
68    /// When `use_local_tz` is true, weekday is determined in the system's local timezone.
69    /// When false, weekday is determined in UTC.
70    Weekdays { use_local_tz: bool },
71
72    /// Saturday and Sunday.
73    /// When `use_local_tz` is true, weekday is determined in the system's local timezone.
74    /// When false, weekday is determined in UTC.
75    Weekends { use_local_tz: bool },
76
77    /// Custom schedule using a closure.
78    Custom(Arc<dyn Fn(DateTime<Utc>) -> bool + Send + Sync>),
79}
80
81impl fmt::Debug for Schedule {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Schedule::Hours {
85                start_hour,
86                end_hour,
87                use_local_tz,
88            } => {
89                let tz_str = if *use_local_tz { "Local" } else { "UTC" };
90                write!(
91                    f,
92                    "Hours {{ start_hour: {}, end_hour: {}, timezone: {} }}",
93                    start_hour, end_hour, tz_str
94                )
95            }
96            Schedule::Weekdays { use_local_tz } => {
97                let tz_str = if *use_local_tz { "Local" } else { "UTC" };
98                write!(f, "Weekdays({})", tz_str)
99            }
100            Schedule::Weekends { use_local_tz } => {
101                let tz_str = if *use_local_tz { "Local" } else { "UTC" };
102                write!(f, "Weekends({})", tz_str)
103            }
104            Schedule::Custom(_) => write!(f, "Custom(<closure>)"),
105        }
106    }
107}
108
109impl Schedule {
110    /// Create a business hours schedule (UTC).
111    ///
112    /// For local timezone-aware schedules, use `hours_local_tz()`.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if:
117    /// - `start_hour` is greater than 23
118    /// - `end_hour` is greater than 23
119    /// - `start_hour` is greater than or equal to `end_hour`
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use tiny_counter::Schedule;
125    ///
126    /// let schedule = Schedule::hours(9, 17).unwrap();  // 9am-5pm UTC
127    /// let schedule = Schedule::hours(6, 22).unwrap();  // 6am-10pm UTC
128    /// let schedule = Schedule::hours_local_tz(9, 17).unwrap();  // 9am-5pm local time
129    /// ```
130    pub fn hours(start_hour: u8, end_hour: u8) -> Result<Self> {
131        Self::validate_hours(start_hour, end_hour)?;
132        Ok(Schedule::Hours {
133            start_hour,
134            end_hour,
135            use_local_tz: false,
136        })
137    }
138
139    /// Create a business hours schedule using the system's local timezone.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if:
144    /// - `start_hour` is greater than 23
145    /// - `end_hour` is greater than 23
146    /// - `start_hour` is greater than or equal to `end_hour`
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use tiny_counter::Schedule;
152    ///
153    /// let schedule = Schedule::hours_local_tz(9, 17).unwrap();  // 9am-5pm in system's local timezone
154    /// ```
155    pub fn hours_local_tz(start_hour: u8, end_hour: u8) -> Result<Self> {
156        Self::validate_hours(start_hour, end_hour)?;
157        Ok(Schedule::Hours {
158            start_hour,
159            end_hour,
160            use_local_tz: true,
161        })
162    }
163
164    /// Create a weekdays schedule (Monday-Friday, UTC).
165    pub fn weekdays() -> Self {
166        Schedule::Weekdays {
167            use_local_tz: false,
168        }
169    }
170
171    /// Create a weekdays schedule using the system's local timezone.
172    pub fn weekdays_local_tz() -> Self {
173        Schedule::Weekdays { use_local_tz: true }
174    }
175
176    /// Create a weekends schedule (Saturday-Sunday, UTC).
177    pub fn weekends() -> Self {
178        Schedule::Weekends {
179            use_local_tz: false,
180        }
181    }
182
183    /// Create a weekends schedule using the system's local timezone.
184    pub fn weekends_local_tz() -> Self {
185        Schedule::Weekends { use_local_tz: true }
186    }
187
188    fn validate_hours(start_hour: u8, end_hour: u8) -> Result<()> {
189        if start_hour > 23 {
190            Err(Error::InvalidHour(start_hour))
191        } else if end_hour > 23 {
192            Err(Error::InvalidHour(end_hour))
193        } else if start_hour >= end_hour {
194            Err(Error::InvalidRange(start_hour, end_hour))
195        } else {
196            Ok(())
197        }
198    }
199}
200
201/// Usage information for a rate limit constraint.
202pub struct LimitUsage {
203    pub count: u32,
204    pub limit: u32,
205    pub remaining: u32,
206}
207
208/// A reservation for a rate-limited event.
209///
210/// Created by `Limiter::reserve()`, this represents a reserved slot that must be
211/// explicitly committed or will automatically cancel on drop.
212///
213/// # Examples
214///
215/// ```
216/// use tiny_counter::{EventStore, TimeUnit};
217///
218/// let store = EventStore::new();
219///
220/// // Reserve a slot
221/// let reservation = store.limit()
222///     .at_most("api_call", 10, TimeUnit::Hours)
223///     .reserve("api_call")
224///     .unwrap();
225///
226/// // Do some work...
227/// // If successful, commit
228/// reservation.commit();
229///
230/// // If reservation is dropped without commit, it auto-cancels
231/// ```
232pub struct Reservation {
233    store: Arc<EventStoreInner>,
234    event_id: String,
235    committed: bool,
236}
237
238impl Reservation {
239    /// Commit the reservation, recording the event.
240    ///
241    /// This decrements the pending counter and records the event atomically.
242    pub fn commit(mut self) {
243        self.release_and_record();
244        self.committed = true;
245    }
246
247    /// Cancel the reservation without recording the event.
248    ///
249    /// This is equivalent to dropping the reservation without committing.
250    pub fn cancel(mut self) {
251        self.release_pending();
252        self.committed = true;
253    }
254
255    /// Release the pending reservation and record the event.
256    fn release_and_record(&self) {
257        if let Some(counter_arc) = self.store.events.get(&self.event_id) {
258            let mut counter = counter_arc.lock().unwrap();
259            counter.decrement_pending();
260            let now = self.store.clock_now();
261            counter.advance_if_needed(now);
262            counter.record(1);
263            counter.mark_dirty();
264        }
265    }
266
267    /// Release the pending reservation without recording.
268    fn release_pending(&self) {
269        if let Some(counter_arc) = self.store.events.get(&self.event_id) {
270            let mut counter = counter_arc.lock().unwrap();
271            counter.decrement_pending();
272            counter.mark_dirty();
273        }
274    }
275}
276
277impl Drop for Reservation {
278    fn drop(&mut self) {
279        if !self.committed {
280            // Auto-cancel if not committed
281            self.release_pending();
282        }
283    }
284}
285
286impl Limiter {
287    /// Creates a new Limiter builder.
288    pub(crate) fn new(store: Arc<EventStoreInner>) -> Self {
289        Self {
290            store,
291            constraints: Vec::new(),
292        }
293    }
294
295    /// Add an "at most" constraint: event cannot exceed limit within time window.
296    ///
297    /// Accepts flexible time window specifications:
298    /// - `TimeUnit` - defaults to 1 unit (e.g., `TimeUnit::Days` means 1 day)
299    /// - `(usize, TimeUnit)` - explicit count (e.g., `(7, TimeUnit::Days)` means 7 days)
300    /// - `Duration` - converted to best matching TimeUnit
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// use tiny_counter::{EventStore, TimeUnit};
306    /// use chrono::Duration;
307    ///
308    /// let mut store = EventStore::new();
309    ///
310    /// // Single time unit (backward compatible)
311    /// let result = store
312    ///     .limit()
313    ///     .at_most("api_call", 10, TimeUnit::Minutes)
314    ///     .check_and_record("api_call");
315    /// assert!(result.is_ok());
316    ///
317    /// // Multiple time units with tuple
318    /// let result = store
319    ///     .limit()
320    ///     .at_most("api_call", 100, (7, TimeUnit::Days))
321    ///     .check("api_call");
322    /// assert!(result.is_ok());
323    ///
324    /// // Duration syntax
325    /// let result = store
326    ///     .limit()
327    ///     .at_most("api_call", 100, Duration::days(7))
328    ///     .check("api_call");
329    /// assert!(result.is_ok());
330    /// ```
331    pub fn at_most(
332        mut self,
333        event_id: impl EventId,
334        limit: u32,
335        window: impl Into<TimeWindow>,
336    ) -> Self {
337        let window = window.into();
338        self.constraints.push(Constraint::AtMost {
339            event_id: event_id.as_ref().to_string(),
340            limit,
341            window_count: window.count,
342            time_unit: window.time_unit,
343        });
344        self
345    }
346
347    /// Add an "at least" constraint: prerequisite must occur at least count times within time window.
348    ///
349    /// Accepts flexible time window specifications:
350    /// - `TimeUnit` - defaults to 1 unit (e.g., `TimeUnit::Days` means 1 day)
351    /// - `(usize, TimeUnit)` - explicit count (e.g., `(7, TimeUnit::Days)` means 7 days)
352    /// - `Duration` - converted to best matching TimeUnit
353    pub fn at_least(
354        mut self,
355        event_id: impl EventId,
356        count: u32,
357        window: impl Into<TimeWindow>,
358    ) -> Self {
359        let window = window.into();
360        self.constraints.push(Constraint::AtLeast {
361            event_id: event_id.as_ref().to_string(),
362            count,
363            window_count: window.count,
364            time_unit: window.time_unit,
365        });
366        self
367    }
368
369    /// Add a cooldown constraint: event must wait duration since last occurrence.
370    pub fn cooldown(mut self, event_id: impl EventId, duration: Duration) -> Self {
371        self.constraints.push(Constraint::Cooldown {
372            event_id: event_id.as_ref().to_string(),
373            duration,
374        });
375        self
376    }
377
378    /// Add a "within" constraint: prerequisite must have occurred within duration.
379    pub fn within(mut self, prerequisite_event: impl EventId, duration: Duration) -> Self {
380        self.constraints.push(Constraint::Within {
381            prerequisite_event: prerequisite_event.as_ref().to_string(),
382            duration,
383        });
384        self
385    }
386
387    /// Add a "during" constraint: event must occur within schedule.
388    pub fn during(mut self, schedule: Schedule) -> Self {
389        self.constraints.push(Constraint::During { schedule });
390        self
391    }
392
393    /// Add an "outside of" constraint: event must occur outside schedule.
394    pub fn outside_of(mut self, schedule: Schedule) -> Self {
395        self.constraints.push(Constraint::OutsideOf { schedule });
396        self
397    }
398
399    /// Check if all constraints pass without recording the event.
400    ///
401    /// # Examples
402    ///
403    /// ```
404    /// use tiny_counter::{EventStore, TimeUnit};
405    ///
406    /// let store = EventStore::new();
407    ///
408    /// let result = store
409    ///     .limit()
410    ///     .at_most("action", 5, TimeUnit::Minutes)
411    ///     .check("action");
412    ///
413    /// assert!(result.is_ok());
414    /// ```
415    pub fn check(self, event_id: impl EventId) -> Result<()> {
416        let event_id_str = event_id.as_ref();
417        self.evaluate_constraints(event_id_str)
418    }
419
420    /// Check if all constraints pass, and if so, record the event.
421    ///
422    /// This operation is atomic: the event is recorded only if all constraints pass.
423    ///
424    /// # Examples
425    ///
426    /// ```
427    /// use tiny_counter::{EventStore, TimeUnit};
428    ///
429    /// let store = EventStore::new();
430    ///
431    /// // First call succeeds and records the event
432    /// let result = store
433    ///     .limit()
434    ///     .at_most("limited_action", 1, TimeUnit::Minutes)
435    ///     .check_and_record("limited_action");
436    /// assert!(result.is_ok());
437    ///
438    /// // Second call fails because limit is reached
439    /// let result = store
440    ///     .limit()
441    ///     .at_most("limited_action", 1, TimeUnit::Minutes)
442    ///     .check_and_record("limited_action");
443    /// assert!(result.is_err());
444    /// ```
445    pub fn check_and_record(self, event_id: impl EventId) -> Result<()> {
446        let event_id_str = event_id.as_ref();
447        self.evaluate_constraints(event_id_str)?;
448
449        // Inline record logic: get or create counter and record
450        let now = self.store.clock.now();
451        let counter = self.store.get_counter_for_record(event_id_str);
452
453        let mut counter = counter.lock().unwrap();
454        counter.advance_if_needed(now);
455        counter.record(1);
456        counter.mark_dirty();
457
458        Ok(())
459    }
460
461    /// Reserve a slot if all constraints pass, returning a Reservation.
462    ///
463    /// This atomically checks rate limits including pending reservations,
464    /// preventing race conditions. The reservation must be explicitly committed
465    /// to record the event, or it will auto-cancel on drop.
466    ///
467    /// # Examples
468    ///
469    /// ```
470    /// use tiny_counter::{EventStore, TimeUnit};
471    ///
472    /// let store = EventStore::new();
473    ///
474    /// // Reserve a slot (atomically checks and reserves)
475    /// let reservation = store.limit()
476    ///     .at_most("api_call", 10, TimeUnit::Hours)
477    ///     .reserve("api_call").unwrap();
478    ///
479    /// // Do work (e.g., make API call)
480    /// // ...
481    ///
482    /// // Commit if successful
483    /// reservation.commit();
484    /// ```
485    ///
486    /// # Transactional Pattern
487    ///
488    /// ```
489    /// use tiny_counter::{EventStore, TimeUnit};
490    ///
491    /// fn make_api_call() -> Result<(), Box<dyn std::error::Error>> {
492    ///     // ... API call logic
493    ///     Ok(())
494    /// }
495    ///
496    /// let store = EventStore::new();
497    ///
498    /// let reservation = store.limit()
499    ///     .at_most("api_call", 10, TimeUnit::Hours)
500    ///     .reserve("api_call").unwrap();
501    ///
502    /// match make_api_call() {
503    ///     Ok(_) => reservation.commit(),  // Record on success
504    ///     Err(_) => {
505    ///         // Reservation auto-cancels on drop
506    ///         // Or explicitly: reservation.cancel();
507    ///     }
508    /// }
509    /// # Ok::<(), Box<dyn std::error::Error>>(())
510    /// ```
511    pub fn reserve(self, event_id: impl EventId) -> Result<Reservation> {
512        let clock_now = self.store.clock_now();
513        let event_id_str = event_id.as_ref();
514
515        // Get or create counter with write lock
516        let counter_arc = self.store.get_counter_for_record(event_id_str);
517
518        let mut counter = counter_arc.lock().unwrap();
519        counter.advance_if_needed(clock_now);
520
521        // Check all constraints including pending
522        for constraint in &self.constraints {
523            match constraint {
524                Constraint::AtMost {
525                    event_id: _constraint_event,
526                    limit,
527                    window_count,
528                    time_unit,
529                } => {
530                    // Use total_with_pending to include reservations
531                    let total = counter.total_with_pending(*time_unit, 0..*window_count);
532                    if total >= *limit {
533                        return Err(Error::LimitExceeded(LimitExceeded {
534                            event_id: event_id_str.to_string(),
535                            constraint: constraint.clone(),
536                            retry_after: Some(time_unit.duration()),
537                        }));
538                    }
539                }
540                // Other constraint types checked without pending
541                _ => {
542                    // Release lock temporarily to evaluate
543                    drop(counter);
544                    self.evaluate_constraint(event_id_str, constraint)?;
545                    counter = counter_arc.lock().unwrap();
546                }
547            }
548        }
549
550        // All constraints pass - reserve the slot
551        counter.increment_pending();
552        counter.mark_dirty();
553
554        Ok(Reservation {
555            store: self.store.clone(),
556            event_id: event_id_str.to_string(),
557            committed: false,
558        })
559    }
560
561    /// Check if all constraints would pass (convenience method).
562    pub fn allowed(self, event_id: impl EventId) -> bool {
563        self.check(event_id).is_ok()
564    }
565
566    /// Get usage information for an "at most" constraint.
567    ///
568    /// Returns usage data for the first AtMost constraint that matches the event_id.
569    pub fn usage(self, event_id: impl EventId) -> Result<LimitUsage> {
570        let event_id_str = event_id.as_ref();
571
572        // Find the first AtMost constraint matching this event
573        for constraint in &self.constraints {
574            if let Constraint::AtMost {
575                event_id: constraint_event,
576                limit,
577                window_count,
578                time_unit,
579            } = constraint
580            {
581                if constraint_event == event_id_str {
582                    let current = self
583                        .query_sum_window(constraint_event, *window_count, *time_unit)
584                        .unwrap_or(0);
585                    let remaining = limit.saturating_sub(current);
586                    return Ok(LimitUsage {
587                        count: current,
588                        limit: *limit,
589                        remaining,
590                    });
591                }
592            }
593        }
594
595        // No AtMost constraint found, return zero usage
596        Ok(LimitUsage {
597            count: 0,
598            limit: 0,
599            remaining: 0,
600        })
601    }
602
603    /// Evaluates all constraints, short-circuiting on first failure.
604    fn evaluate_constraints(&self, event_id: &str) -> Result<()> {
605        for constraint in &self.constraints {
606            self.evaluate_constraint(event_id, constraint)?;
607        }
608        Ok(())
609    }
610
611    /// Evaluates a single constraint.
612    fn evaluate_constraint(&self, event_id: &str, constraint: &Constraint) -> Result<()> {
613        match constraint {
614            Constraint::AtMost {
615                event_id: constraint_event,
616                limit,
617                window_count,
618                time_unit,
619            } => {
620                let current = self
621                    .query_sum_window(constraint_event, *window_count, *time_unit)
622                    .unwrap_or(0);
623                if current >= *limit {
624                    return Err(Error::LimitExceeded(LimitExceeded {
625                        event_id: event_id.to_string(),
626                        constraint: constraint.clone(),
627                        retry_after: Some(time_unit.duration()),
628                    }));
629                }
630            }
631            Constraint::AtLeast {
632                event_id: constraint_event,
633                count,
634                window_count,
635                time_unit,
636            } => {
637                let current = self
638                    .query_sum_window(constraint_event, *window_count, *time_unit)
639                    .unwrap_or(0);
640                if current < *count {
641                    return Err(Error::LimitExceeded(LimitExceeded {
642                        event_id: event_id.to_string(),
643                        constraint: constraint.clone(),
644                        retry_after: None,
645                    }));
646                }
647            }
648            Constraint::Cooldown {
649                event_id: constraint_event,
650                duration,
651            } => {
652                if let Some(elapsed) = self.query_last_seen(constraint_event) {
653                    if elapsed < *duration {
654                        let remaining = *duration - elapsed;
655                        return Err(Error::LimitExceeded(LimitExceeded {
656                            event_id: event_id.to_string(),
657                            constraint: constraint.clone(),
658                            retry_after: Some(remaining),
659                        }));
660                    }
661                }
662                // Never seen before = cooldown is satisfied
663            }
664            Constraint::Within {
665                prerequisite_event,
666                duration,
667            } => {
668                match self.query_last_seen(prerequisite_event) {
669                    None => {
670                        // Never seen
671                        return Err(Error::LimitExceeded(LimitExceeded {
672                            event_id: event_id.to_string(),
673                            constraint: constraint.clone(),
674                            retry_after: None,
675                        }));
676                    }
677                    Some(elapsed) => {
678                        if elapsed > *duration {
679                            // Too long ago
680                            return Err(Error::LimitExceeded(LimitExceeded {
681                                event_id: event_id.to_string(),
682                                constraint: constraint.clone(),
683                                retry_after: None,
684                            }));
685                        }
686                    }
687                }
688            }
689            Constraint::During { schedule } => {
690                if !schedule.allows(&self.store) {
691                    return Err(Error::LimitExceeded(LimitExceeded {
692                        event_id: event_id.to_string(),
693                        constraint: constraint.clone(),
694                        retry_after: schedule.next_allowed(&self.store),
695                    }));
696                }
697            }
698            Constraint::OutsideOf { schedule } => {
699                if schedule.allows(&self.store) {
700                    return Err(Error::LimitExceeded(LimitExceeded {
701                        event_id: event_id.to_string(),
702                        constraint: constraint.clone(),
703                        retry_after: schedule.block_end(&self.store),
704                    }));
705                }
706            }
707        }
708        Ok(())
709    }
710
711    /// Query the count for an event within a time window.
712    fn query_sum_window(
713        &self,
714        event_id: &str,
715        window_count: usize,
716        time_unit: TimeUnit,
717    ) -> Option<u32> {
718        let query = Query::new(self.store.clone(), event_id.to_string());
719        query.last(window_count, time_unit).sum()
720    }
721
722    /// Query the time since last seen.
723    fn query_last_seen(&self, event_id: &str) -> Option<Duration> {
724        Query::new(self.store.clone(), event_id.to_string()).last_seen()
725    }
726}
727
728impl Schedule {
729    /// Check if the schedule allows the current time.
730    fn allows(&self, store: &EventStoreInner) -> bool {
731        let now = store.clock_now();
732        match self {
733            Schedule::Hours {
734                start_hour,
735                end_hour,
736                use_local_tz,
737            } => {
738                let hour = if *use_local_tz {
739                    now.with_timezone(&Local).hour() as u8
740                } else {
741                    now.hour() as u8
742                };
743                hour >= *start_hour && hour < *end_hour
744            }
745            Schedule::Weekdays { use_local_tz } => {
746                let weekday = if *use_local_tz {
747                    now.with_timezone(&Local).weekday()
748                } else {
749                    now.weekday()
750                };
751                !matches!(weekday, Weekday::Sat | Weekday::Sun)
752            }
753            Schedule::Weekends { use_local_tz } => {
754                let weekday = if *use_local_tz {
755                    now.with_timezone(&Local).weekday()
756                } else {
757                    now.weekday()
758                };
759                matches!(weekday, Weekday::Sat | Weekday::Sun)
760            }
761            Schedule::Custom(f) => f(now),
762        }
763    }
764
765    /// Calculate when the schedule next allows events.
766    fn next_allowed(&self, store: &EventStoreInner) -> Option<Duration> {
767        let now = store.clock_now();
768        match self {
769            Schedule::Hours {
770                start_hour,
771                end_hour: _,
772                use_local_tz,
773            } => {
774                let current_hour = if *use_local_tz {
775                    now.with_timezone(&Local).hour() as u8
776                } else {
777                    now.hour() as u8
778                };
779
780                if current_hour < *start_hour {
781                    // Before business hours today
782                    let hours_until = (*start_hour - current_hour) as i64;
783                    Some(Duration::hours(hours_until))
784                } else {
785                    // After business hours, wait until tomorrow
786                    let hours_until = 24 - current_hour + *start_hour;
787                    Some(Duration::hours(hours_until as i64))
788                }
789            }
790            Schedule::Weekdays { use_local_tz } => {
791                let weekday = if *use_local_tz {
792                    now.with_timezone(&Local).weekday()
793                } else {
794                    now.weekday()
795                };
796                // If weekend, wait until Monday
797                match weekday {
798                    Weekday::Sat => Some(Duration::days(2)),
799                    Weekday::Sun => Some(Duration::days(1)),
800                    _ => None,
801                }
802            }
803            Schedule::Weekends { use_local_tz } => {
804                let weekday = if *use_local_tz {
805                    now.with_timezone(&Local).weekday()
806                } else {
807                    now.weekday()
808                };
809                // If weekday, wait until Saturday
810                let days_until_sat = match weekday {
811                    Weekday::Mon => 5,
812                    Weekday::Tue => 4,
813                    Weekday::Wed => 3,
814                    Weekday::Thu => 2,
815                    Weekday::Fri => 1,
816                    _ => 0,
817                };
818                if days_until_sat > 0 {
819                    Some(Duration::days(days_until_sat))
820                } else {
821                    None
822                }
823            }
824            Schedule::Custom(_) => None, // Can't calculate for custom schedules
825        }
826    }
827
828    /// Calculate when the schedule block ends.
829    fn block_end(&self, store: &EventStoreInner) -> Option<Duration> {
830        let now = store.clock_now();
831        match self {
832            Schedule::Hours {
833                start_hour: _,
834                end_hour,
835                use_local_tz,
836            } => {
837                let current_hour = if *use_local_tz {
838                    now.with_timezone(&Local).hour() as u8
839                } else {
840                    now.hour() as u8
841                };
842
843                if current_hour < *end_hour {
844                    let hours_until = (*end_hour - current_hour) as i64;
845                    Some(Duration::hours(hours_until))
846                } else {
847                    None
848                }
849            }
850            Schedule::Weekdays { use_local_tz } => {
851                let weekday = if *use_local_tz {
852                    now.with_timezone(&Local).weekday()
853                } else {
854                    now.weekday()
855                };
856                // Blocked during weekdays, ends on Saturday
857                match weekday {
858                    Weekday::Mon => Some(Duration::days(5)),
859                    Weekday::Tue => Some(Duration::days(4)),
860                    Weekday::Wed => Some(Duration::days(3)),
861                    Weekday::Thu => Some(Duration::days(2)),
862                    Weekday::Fri => Some(Duration::days(1)),
863                    _ => None,
864                }
865            }
866            Schedule::Weekends { use_local_tz } => {
867                let weekday = if *use_local_tz {
868                    now.with_timezone(&Local).weekday()
869                } else {
870                    now.weekday()
871                };
872                // Blocked during weekends, ends on Monday
873                match weekday {
874                    Weekday::Sat => Some(Duration::days(2)),
875                    Weekday::Sun => Some(Duration::days(1)),
876                    _ => None,
877                }
878            }
879            Schedule::Custom(_) => None, // Can't calculate for custom schedules
880        }
881    }
882}
883
884#[cfg(test)]
885mod tests {
886    use std::thread;
887
888    use chrono::{Duration, TimeZone, Weekday};
889
890    use crate::{EventStore, TestClock, TimeUnit};
891
892    use super::*;
893
894    #[test]
895    fn test_at_most_constraint_passes() {
896        let store = EventStore::new();
897        store.record_count("test_event", 5);
898
899        let result = store
900            .limit()
901            .at_most("test_event", 10, TimeUnit::Days)
902            .check("action");
903
904        assert!(result.is_ok());
905    }
906
907    #[test]
908    fn test_at_most_constraint_fails() {
909        let store = EventStore::new();
910        store.record_count("test_event", 10);
911
912        let result = store
913            .limit()
914            .at_most("test_event", 5, TimeUnit::Days)
915            .check("action");
916
917        assert!(result.is_err());
918        let Error::LimitExceeded(err) = result.unwrap_err() else {
919            panic!("Expected LimitExceeded error");
920        };
921        assert_eq!(err.event_id, "action");
922        assert!(err.retry_after.is_some());
923    }
924
925    #[test]
926    fn test_at_least_constraint_passes() {
927        let store = EventStore::new();
928        store.record_count("prerequisite", 5);
929
930        let result = store
931            .limit()
932            .at_least("prerequisite", 3, TimeUnit::Days)
933            .check("action");
934
935        assert!(result.is_ok());
936    }
937
938    #[test]
939    fn test_at_least_constraint_fails() {
940        let store = EventStore::new();
941        store.record_count("prerequisite", 2);
942
943        let result = store
944            .limit()
945            .at_least("prerequisite", 5, TimeUnit::Days)
946            .check("action");
947
948        assert!(result.is_err());
949        let Error::LimitExceeded(err) = result.unwrap_err() else {
950            panic!("Expected LimitExceeded error");
951        };
952        assert!(err.retry_after.is_none());
953    }
954
955    #[test]
956    fn test_cooldown_constraint_passes() {
957        let store = EventStore::new();
958        store.record_ago("test_event", Duration::hours(2));
959
960        let result = store
961            .limit()
962            .cooldown("test_event", Duration::hours(1))
963            .check("action");
964
965        assert!(result.is_ok());
966    }
967
968    #[test]
969    fn test_cooldown_constraint_fails() {
970        let store = EventStore::new();
971        store.record("test_event");
972
973        let result = store
974            .limit()
975            .cooldown("test_event", Duration::hours(1))
976            .check("action");
977
978        assert!(result.is_err());
979        let Error::LimitExceeded(err) = result.unwrap_err() else {
980            panic!("Expected LimitExceeded error");
981        };
982        assert!(err.retry_after.is_some());
983    }
984
985    #[test]
986    fn test_within_constraint_passes() {
987        let store = EventStore::new();
988        store.record("prerequisite");
989
990        let result = store
991            .limit()
992            .within("prerequisite", Duration::hours(1))
993            .check("action");
994
995        assert!(result.is_ok());
996    }
997
998    #[test]
999    fn test_within_constraint_fails_never_seen() {
1000        let store = EventStore::new();
1001
1002        let result = store
1003            .limit()
1004            .within("prerequisite", Duration::hours(1))
1005            .check("action");
1006
1007        assert!(result.is_err());
1008    }
1009
1010    #[test]
1011    fn test_within_constraint_fails_too_long_ago() {
1012        let store = EventStore::new();
1013        store.record_ago("prerequisite", Duration::hours(2));
1014
1015        let result = store
1016            .limit()
1017            .within("prerequisite", Duration::hours(1))
1018            .check("action");
1019
1020        assert!(result.is_err());
1021    }
1022
1023    #[test]
1024    fn test_multiple_constraints_all_pass() {
1025        let store = EventStore::new();
1026        store.record_count("prerequisite", 5);
1027        store.record_count("test_event", 2);
1028
1029        let result = store
1030            .limit()
1031            .at_least("prerequisite", 3, TimeUnit::Days)
1032            .at_most("test_event", 10, TimeUnit::Days)
1033            .check("action");
1034
1035        assert!(result.is_ok());
1036    }
1037
1038    #[test]
1039    fn test_multiple_constraints_first_fails() {
1040        let store = EventStore::new();
1041        store.record_count("prerequisite", 1);
1042        store.record_count("test_event", 2);
1043
1044        let result = store
1045            .limit()
1046            .at_least("prerequisite", 3, TimeUnit::Days)
1047            .at_most("test_event", 10, TimeUnit::Days)
1048            .check("action");
1049
1050        assert!(result.is_err());
1051    }
1052
1053    #[test]
1054    fn test_check_and_record_success() {
1055        let store = EventStore::new();
1056
1057        let result = store
1058            .limit()
1059            .at_most("test_event", 5, TimeUnit::Days)
1060            .check_and_record("test_event");
1061
1062        assert!(result.is_ok());
1063
1064        // Verify event was recorded
1065        let count = store.query("test_event").last_days(1).sum();
1066        assert_eq!(count, Some(1));
1067    }
1068
1069    #[test]
1070    fn test_check_and_record_failure_does_not_record() {
1071        let store = EventStore::new();
1072        store.record_count("test_event", 10);
1073
1074        let result = store
1075            .limit()
1076            .at_most("test_event", 5, TimeUnit::Days)
1077            .check_and_record("test_event");
1078
1079        assert!(result.is_err());
1080
1081        // Verify count is still 10 (not 11)
1082        let count = store.query("test_event").last_days(1).sum();
1083        assert_eq!(count, Some(10));
1084    }
1085
1086    #[test]
1087    fn test_allowed_convenience_method() {
1088        let store = EventStore::new();
1089        store.record_count("test_event", 3);
1090
1091        let allowed = store
1092            .limit()
1093            .at_most("test_event", 5, TimeUnit::Days)
1094            .allowed("action");
1095        assert!(allowed);
1096
1097        // After adding 2 more (total 5), should hit limit of 5
1098        store.record_count("test_event", 2);
1099        let not_allowed = store
1100            .limit()
1101            .at_most("test_event", 5, TimeUnit::Days)
1102            .allowed("action");
1103        assert!(!not_allowed);
1104    }
1105
1106    #[test]
1107    fn test_usage_returns_correct_counts() {
1108        let store = EventStore::new();
1109        store.record_count("test_event", 3);
1110
1111        let usage = store
1112            .limit()
1113            .at_most("test_event", 10, TimeUnit::Days)
1114            .usage("test_event")
1115            .unwrap();
1116
1117        assert_eq!(usage.count, 3);
1118        assert_eq!(usage.limit, 10);
1119        assert_eq!(usage.remaining, 7);
1120    }
1121
1122    #[test]
1123    fn test_business_hours_schedule_during_hours() {
1124        // Set to Tuesday at 10 AM (during business hours)
1125        let tuesday_10am = Utc.with_ymd_and_hms(2025, 1, 7, 10, 0, 0).unwrap();
1126        let clock = TestClock::build_for_testing_at(tuesday_10am);
1127
1128        let store = EventStore::builder()
1129            .with_clock(Arc::new(clock.clone()))
1130            .build()
1131            .unwrap();
1132
1133        // Should succeed during business hours (9am-5pm)
1134        let result = store
1135            .limit()
1136            .during(Schedule::hours(9, 17).unwrap())
1137            .check_and_record("action");
1138        assert!(result.is_ok());
1139
1140        // Set to Tuesday at 7 PM (outside business hours)
1141        let tuesday_7pm = Utc.with_ymd_and_hms(2025, 1, 7, 19, 0, 0).unwrap();
1142        clock.set(tuesday_7pm);
1143
1144        // Should fail outside business hours
1145        let result = store
1146            .limit()
1147            .during(Schedule::hours(9, 17).unwrap())
1148            .check("action");
1149        assert!(result.is_err());
1150    }
1151
1152    #[test]
1153    fn test_weekdays_schedule() {
1154        // Set to Tuesday (weekday)
1155        let tuesday = Utc.with_ymd_and_hms(2025, 1, 7, 12, 0, 0).unwrap();
1156        assert_eq!(tuesday.weekday(), Weekday::Tue);
1157        let clock = TestClock::build_for_testing_at(tuesday);
1158
1159        let store = EventStore::builder()
1160            .with_clock(Arc::new(clock.clone()))
1161            .build()
1162            .unwrap();
1163
1164        // Should succeed on weekday
1165        let result = store
1166            .limit()
1167            .during(Schedule::weekdays())
1168            .check_and_record("action");
1169        assert!(result.is_ok());
1170
1171        // Set to Saturday (weekend)
1172        let saturday = Utc.with_ymd_and_hms(2025, 1, 11, 12, 0, 0).unwrap();
1173        assert_eq!(saturday.weekday(), Weekday::Sat);
1174        clock.set(saturday);
1175
1176        // Should fail on weekend
1177        let result = store.limit().during(Schedule::weekdays()).check("action");
1178        assert!(result.is_err());
1179    }
1180
1181    #[test]
1182    fn test_custom_schedule() {
1183        let store = EventStore::new();
1184        let schedule = Schedule::Custom(Arc::new(|_time| true)); // Always allowed
1185
1186        let result = store.limit().during(schedule).check("action");
1187        assert!(result.is_ok());
1188    }
1189
1190    #[test]
1191    fn test_schedule_hours_constructor() {
1192        let schedule = Schedule::hours(9, 17).unwrap();
1193        match schedule {
1194            Schedule::Hours {
1195                start_hour,
1196                end_hour,
1197                ..
1198            } => {
1199                assert_eq!(start_hour, 9);
1200                assert_eq!(end_hour, 17);
1201            }
1202            _ => panic!("Expected Hours variant"),
1203        }
1204    }
1205
1206    #[test]
1207    fn test_schedule_hours_validates_start_hour() {
1208        let result = Schedule::hours(24, 17);
1209        assert!(result.is_err());
1210        match result.unwrap_err() {
1211            Error::InvalidHour(hour) => assert_eq!(hour, 24),
1212            _ => panic!("Expected InvalidHour error"),
1213        }
1214    }
1215
1216    #[test]
1217    fn test_schedule_hours_validates_end_hour() {
1218        let result = Schedule::hours(9, 25);
1219        assert!(result.is_err());
1220        match result.unwrap_err() {
1221            Error::InvalidHour(hour) => assert_eq!(hour, 25),
1222            _ => panic!("Expected InvalidHour error"),
1223        }
1224    }
1225
1226    #[test]
1227    fn test_schedule_hours_validates_range() {
1228        let result = Schedule::hours(17, 9);
1229        assert!(result.is_err());
1230        match result.unwrap_err() {
1231            Error::InvalidRange(start, end) => {
1232                assert_eq!(start, 17);
1233                assert_eq!(end, 9);
1234            }
1235            _ => panic!("Expected InvalidRange error"),
1236        }
1237    }
1238
1239    #[test]
1240    fn test_schedule_hours_rejects_equal_hours() {
1241        let result = Schedule::hours(9, 9);
1242        assert!(result.is_err());
1243        match result.unwrap_err() {
1244            Error::InvalidRange(start, end) => {
1245                assert_eq!(start, 9);
1246                assert_eq!(end, 9);
1247            }
1248            _ => panic!("Expected InvalidRange error"),
1249        }
1250    }
1251
1252    #[test]
1253    fn test_schedule_hours_accepts_valid_boundary_cases() {
1254        // 0-23 should work
1255        let schedule = Schedule::hours(0, 23).unwrap();
1256        match schedule {
1257            Schedule::Hours {
1258                start_hour,
1259                end_hour,
1260                ..
1261            } => {
1262                assert_eq!(start_hour, 0);
1263                assert_eq!(end_hour, 23);
1264            }
1265            _ => panic!("Expected Hours variant"),
1266        }
1267
1268        // Single hour difference should work
1269        let schedule = Schedule::hours(0, 1).unwrap();
1270        match schedule {
1271            Schedule::Hours {
1272                start_hour,
1273                end_hour,
1274                ..
1275            } => {
1276                assert_eq!(start_hour, 0);
1277                assert_eq!(end_hour, 1);
1278            }
1279            _ => panic!("Expected Hours variant"),
1280        }
1281    }
1282
1283    #[test]
1284    fn test_retry_after_calculation_for_at_most() {
1285        let store = EventStore::new();
1286        store.record_count("test_event", 10);
1287
1288        let result = store
1289            .limit()
1290            .at_most("test_event", 5, TimeUnit::Hours)
1291            .check("action");
1292
1293        assert!(result.is_err());
1294        let Error::LimitExceeded(err) = result.unwrap_err() else {
1295            panic!("Expected LimitExceeded error");
1296        };
1297        assert_eq!(err.retry_after, Some(Duration::hours(1)));
1298    }
1299
1300    #[test]
1301    fn test_cooldown_never_seen_passes() {
1302        let store = EventStore::new();
1303
1304        let result = store
1305            .limit()
1306            .cooldown("never_seen", Duration::hours(1))
1307            .check("action");
1308
1309        assert!(result.is_ok());
1310    }
1311
1312    #[test]
1313    fn test_flexible_time_windows_with_tuple() {
1314        let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1315        let clock = TestClock::build_for_testing_at(start_time);
1316
1317        let store = EventStore::builder()
1318            .with_clock(Arc::new(clock.clone()))
1319            .build()
1320            .unwrap();
1321
1322        // Record 10 events over 3 days
1323        for _i in 0..10 {
1324            store.record("api");
1325            clock.advance(Duration::hours(7));
1326        }
1327
1328        // Should pass - 10 recorded, limit 11 means we can still record more
1329        let result = store
1330            .limit()
1331            .at_most("api", 11, (7, TimeUnit::Days))
1332            .check("api");
1333        assert!(result.is_ok());
1334
1335        // Should fail - 10 >= 10 in last 7 days
1336        let result = store
1337            .limit()
1338            .at_most("api", 10, (7, TimeUnit::Days))
1339            .check("api");
1340        assert!(result.is_err());
1341    }
1342
1343    #[test]
1344    fn test_flexible_time_windows_with_duration() {
1345        let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1346        let clock = TestClock::build_for_testing_at(start_time);
1347
1348        let store = EventStore::builder()
1349            .with_clock(Arc::new(clock.clone()))
1350            .build()
1351            .unwrap();
1352
1353        // Record 10 events over 3 days
1354        for _i in 0..10 {
1355            store.record("api");
1356            clock.advance(Duration::hours(7));
1357        }
1358
1359        // Duration syntax - should pass with limit 11
1360        let result = store
1361            .limit()
1362            .at_most("api", 11, Duration::days(7))
1363            .check("api");
1364        assert!(result.is_ok());
1365
1366        // Duration syntax - should fail with limit 10
1367        let result = store
1368            .limit()
1369            .at_most("api", 10, Duration::days(7))
1370            .check("api");
1371        assert!(result.is_err());
1372    }
1373
1374    #[test]
1375    fn test_flexible_time_windows_backward_compatible() {
1376        let store = EventStore::new();
1377        store.record_count("api", 5);
1378
1379        // Old syntax should still work - TimeUnit means 1 unit
1380        let result = store
1381            .limit()
1382            .at_most("api", 10, TimeUnit::Days)
1383            .check("api");
1384        assert!(result.is_ok());
1385    }
1386
1387    #[test]
1388    fn test_flexible_time_windows_30_minutes() {
1389        let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1390        let clock = TestClock::build_for_testing_at(start_time);
1391
1392        let store = EventStore::builder()
1393            .with_clock(Arc::new(clock.clone()))
1394            .build()
1395            .unwrap();
1396
1397        // Record 10 events over 20 minutes
1398        for _i in 0..10 {
1399            store.record("api");
1400            clock.advance(Duration::minutes(2));
1401        }
1402
1403        // Should pass - 10 recorded, limit 11
1404        let result = store
1405            .limit()
1406            .at_most("api", 11, (30, TimeUnit::Minutes))
1407            .check("api");
1408        assert!(result.is_ok());
1409
1410        // Should fail - 10 >= 10 in last 30 minutes
1411        let result = store
1412            .limit()
1413            .at_most("api", 10, (30, TimeUnit::Minutes))
1414            .check("api");
1415        assert!(result.is_err());
1416    }
1417
1418    #[test]
1419    fn test_at_least_with_flexible_windows() {
1420        let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1421        let clock = TestClock::build_for_testing_at(start_time);
1422
1423        let store = EventStore::builder()
1424            .with_clock(Arc::new(clock.clone()))
1425            .build()
1426            .unwrap();
1427
1428        // Record 10 events over 3 days
1429        for _i in 0..10 {
1430            store.record("prerequisite");
1431            clock.advance(Duration::hours(7));
1432        }
1433
1434        // Should pass - at least 5 in last 7 days
1435        let result = store
1436            .limit()
1437            .at_least("prerequisite", 5, (7, TimeUnit::Days))
1438            .check("action");
1439        assert!(result.is_ok());
1440
1441        // Should fail - need at least 15 in last 7 days
1442        let result = store
1443            .limit()
1444            .at_least("prerequisite", 15, (7, TimeUnit::Days))
1445            .check("action");
1446        assert!(result.is_err());
1447    }
1448
1449    #[test]
1450    fn test_duration_conversion_to_time_window() {
1451        let start_time = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1452        let clock = TestClock::build_for_testing_at(start_time);
1453
1454        let store = EventStore::builder()
1455            .with_clock(Arc::new(clock.clone()))
1456            .build()
1457            .unwrap();
1458
1459        // Record events
1460        for _i in 0..5 {
1461            store.record("api");
1462            clock.advance(Duration::hours(1));
1463        }
1464
1465        // Test Duration::hours conversion
1466        let result = store
1467            .limit()
1468            .at_most("api", 10, Duration::hours(12))
1469            .check("api");
1470        assert!(result.is_ok());
1471
1472        // Test Duration::minutes conversion
1473        store.record_count("fast", 20);
1474        let result = store
1475            .limit()
1476            .at_most("fast", 25, Duration::minutes(30))
1477            .check("fast");
1478        assert!(result.is_ok());
1479    }
1480
1481    #[test]
1482    fn test_reservation_prevents_race_condition() {
1483        let store = EventStore::new();
1484
1485        // Reserve first slot
1486        let res1 = store
1487            .limit()
1488            .at_most("api", 1, TimeUnit::Hours)
1489            .reserve("api")
1490            .unwrap();
1491
1492        // Second reservation should fail (pending is counted)
1493        let res2 = store
1494            .limit()
1495            .at_most("api", 1, TimeUnit::Hours)
1496            .reserve("api");
1497        assert!(res2.is_err());
1498
1499        // Commit first reservation
1500        res1.commit();
1501
1502        // Now recorded
1503        assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1504    }
1505
1506    #[test]
1507    fn test_reservation_auto_cancel_on_drop() {
1508        let store = EventStore::new();
1509
1510        {
1511            let _res = store
1512                .limit()
1513                .at_most("api", 10, TimeUnit::Hours)
1514                .reserve("api")
1515                .unwrap();
1516            // Drop without commit
1517        }
1518
1519        // Nothing recorded (but counter was created, so it returns Some(0))
1520        let sum = store.query("api").last_hours(1).sum();
1521        assert!(sum.is_none() || sum == Some(0));
1522
1523        // Can reserve again (pending was released)
1524        let res = store
1525            .limit()
1526            .at_most("api", 1, TimeUnit::Hours)
1527            .reserve("api");
1528        assert!(res.is_ok());
1529    }
1530
1531    #[test]
1532    fn test_reservation_explicit_cancel() {
1533        let store = EventStore::new();
1534
1535        let res = store
1536            .limit()
1537            .at_most("api", 10, TimeUnit::Hours)
1538            .reserve("api")
1539            .unwrap();
1540
1541        // Explicitly cancel
1542        res.cancel();
1543
1544        // Nothing recorded (but counter was created, so it returns Some(0))
1545        let sum = store.query("api").last_hours(1).sum();
1546        assert!(sum.is_none() || sum == Some(0));
1547
1548        // Can reserve again
1549        let res = store
1550            .limit()
1551            .at_most("api", 1, TimeUnit::Hours)
1552            .reserve("api");
1553        assert!(res.is_ok());
1554    }
1555
1556    #[test]
1557    fn test_reservation_commit_records_event() {
1558        let store = EventStore::new();
1559
1560        let res = store
1561            .limit()
1562            .at_most("api", 10, TimeUnit::Hours)
1563            .reserve("api")
1564            .unwrap();
1565
1566        // Commit the reservation
1567        res.commit();
1568
1569        // Event is recorded
1570        assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1571    }
1572
1573    #[test]
1574    fn test_reservation_concurrent_limits() {
1575        let store = Arc::new(EventStore::new());
1576
1577        // Try to reserve 20 times with limit of 10
1578        let handles: Vec<_> = (0..20)
1579            .map(|_| {
1580                let store = store.clone();
1581                thread::spawn(move || {
1582                    store
1583                        .limit()
1584                        .at_most("api", 10, TimeUnit::Hours)
1585                        .reserve("api")
1586                })
1587            })
1588            .collect();
1589
1590        let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1591
1592        // Exactly 10 should succeed
1593        let successes: Vec<_> = results.into_iter().filter(|r| r.is_ok()).collect();
1594        assert_eq!(successes.len(), 10);
1595
1596        // Commit all successful reservations
1597        for result in successes {
1598            result.unwrap().commit();
1599        }
1600
1601        // Exactly 10 recorded
1602        assert_eq!(store.query("api").last_hours(1).sum(), Some(10));
1603    }
1604
1605    #[test]
1606    fn test_reservation_transactional_pattern() {
1607        fn simulated_work(should_succeed: bool) -> std::result::Result<(), &'static str> {
1608            if should_succeed {
1609                Ok(())
1610            } else {
1611                Err("Work failed")
1612            }
1613        }
1614
1615        let store = EventStore::new();
1616
1617        // Success case
1618        let res = store
1619            .limit()
1620            .at_most("api", 10, TimeUnit::Hours)
1621            .reserve("api")
1622            .unwrap();
1623
1624        match simulated_work(true) {
1625            Ok(_) => res.commit(),
1626            Err(_) => res.cancel(),
1627        }
1628
1629        // Event recorded
1630        assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1631
1632        // Failure case
1633        let res = store
1634            .limit()
1635            .at_most("api", 10, TimeUnit::Hours)
1636            .reserve("api")
1637            .unwrap();
1638
1639        match simulated_work(false) {
1640            Ok(_) => res.commit(),
1641            Err(_) => res.cancel(),
1642        }
1643
1644        // Still only 1 recorded (second was cancelled)
1645        assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1646    }
1647
1648    #[test]
1649    fn test_reservation_with_multiple_constraints() {
1650        let store = EventStore::new();
1651
1652        // Set up prerequisite
1653        store.record_count("prerequisite", 5);
1654
1655        // Reserve with multiple constraints
1656        let res = store
1657            .limit()
1658            .at_most("api", 10, TimeUnit::Hours)
1659            .at_least("prerequisite", 3, TimeUnit::Days)
1660            .reserve("api")
1661            .unwrap();
1662
1663        res.commit();
1664
1665        // Verify event recorded
1666        assert_eq!(store.query("api").last_hours(1).sum(), Some(1));
1667    }
1668}