tiny_counter/store/
query.rs

1/// Query builders for fluent event querying.
2use std::ops::Range;
3use std::sync::Arc;
4
5use chrono::Duration;
6
7use crate::{EventStoreInner, TimeUnit};
8
9/// Builder for constructing queries on a single event.
10///
11/// Created by `EventStore::query()`, this builder provides methods to specify
12/// time ranges before executing aggregation operations.
13#[must_use = "query builders do nothing unless consumed"]
14pub struct Query {
15    store: Arc<EventStoreInner>,
16    event_id: String,
17}
18
19impl Query {
20    /// Creates a new Query builder.
21    ///
22    /// This is typically called by EventStore, not directly by users.
23    pub(crate) fn new(store: Arc<EventStoreInner>, event_id: String) -> Self {
24        Self { store, event_id }
25    }
26
27    pub(crate) fn last(self, n: usize, unit: TimeUnit) -> RangeQuery {
28        RangeQuery::new(self.store, self.event_id, unit, 0..n)
29    }
30
31    /// Query the last N seconds.
32    pub fn last_seconds(self, n: usize) -> RangeQuery {
33        self.last(n, TimeUnit::Seconds)
34    }
35
36    /// Query the last N minutes.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use tiny_counter::EventStore;
42    ///
43    /// let store = EventStore::new();
44    /// store.record("event");
45    ///
46    /// let count = store.query("event").last_minutes(60).sum();
47    /// assert_eq!(count, Some(1));
48    /// ```
49    pub fn last_minutes(self, n: usize) -> RangeQuery {
50        self.last(n, TimeUnit::Minutes)
51    }
52
53    /// Query the last N hours.
54    pub fn last_hours(self, n: usize) -> RangeQuery {
55        self.last(n, TimeUnit::Hours)
56    }
57
58    /// Query the last N days.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use tiny_counter::EventStore;
64    ///
65    /// let store = EventStore::new();
66    /// store.record("app_launch");
67    ///
68    /// let count = store.query("app_launch").last_days(7).sum();
69    /// assert_eq!(count, Some(1));
70    /// ```
71    pub fn last_days(self, n: usize) -> RangeQuery {
72        self.last(n, TimeUnit::Days)
73    }
74
75    /// Query the last N weeks.
76    pub fn last_weeks(self, n: usize) -> RangeQuery {
77        self.last(n, TimeUnit::Weeks)
78    }
79
80    /// Query the last N months (28-day approximation).
81    pub fn last_months(self, n: usize) -> RangeQuery {
82        self.last(n, TimeUnit::Months)
83    }
84
85    /// Query the last N years (365-day approximation).
86    pub fn last_years(self, n: usize) -> RangeQuery {
87        self.last(n, TimeUnit::Years)
88    }
89
90    /// Query all available data (all buckets).
91    ///
92    /// Uses the longest time unit configured for this event to capture
93    /// the maximum available history.
94    pub fn ever(self) -> RangeQuery {
95        // Use TimeUnit::Ever to defer resolution until query execution
96        // This avoids loading the counter twice
97        self.last(usize::MAX, TimeUnit::Ever)
98    }
99
100    /// Query a specific range of days.
101    pub fn days(self, range: Range<usize>) -> RangeQuery {
102        RangeQuery::new(self.store, self.event_id, TimeUnit::Days, range)
103    }
104
105    /// Start building a query from an offset of days.
106    ///
107    /// Use with `.take(n)` to specify the range length.
108    pub fn days_from(self, offset: usize) -> RangeQuery {
109        RangeQuery::new(
110            self.store,
111            self.event_id,
112            TimeUnit::Days,
113            offset..usize::MAX,
114        )
115    }
116
117    /// Returns the time since the event was last seen.
118    ///
119    /// Returns None if the event has never been recorded or doesn't exist.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use tiny_counter::EventStore;
125    /// use chrono::Duration;
126    ///
127    /// let store = EventStore::new();
128    /// store.record("settings_visit");
129    ///
130    /// let time_since = store.query("settings_visit").last_seen();
131    /// assert!(time_since.is_some());
132    ///
133    /// // Check for events never recorded
134    /// let never_seen = store.query("never_happened").last_seen();
135    /// assert!(never_seen.is_none());
136    /// ```
137    pub fn last_seen(self) -> Option<Duration> {
138        let clock_now = self.store.clock_now();
139        let counter_arc = self.store.get_counter_for_query(&self.event_id)?;
140        let mut counter = counter_arc.lock().unwrap();
141
142        counter.advance_if_needed(clock_now);
143
144        // Get time units from counter and sort from smallest to largest
145        let configs = self.store.config.configs();
146
147        // Try to find the event in any time unit, starting with the finest granularity
148        for interval_idx in 0..configs.len() {
149            let config = &configs[interval_idx];
150            let time_unit = config.time_unit();
151
152            if let Some(bucket_idx) = counter.last_seen_in(time_unit) {
153                // SAFETY: This cannot panic. We got `config` from the counter's own interval list,
154                // so the time_unit MUST exist in the counter. If this panics, it's a bug in
155                // tiny-counter's internal logic, not user error.
156                let interval_start = counter.interval_start(time_unit).expect(
157                    "BUG: Interval must exist - we just retrieved config from counter's intervals",
158                );
159
160                // If this is the smallest interval, we have the most precise answer
161                if interval_idx == 0 {
162                    let estimate = time_unit.bucket_midway(clock_now, interval_start, bucket_idx);
163                    return Some(clock_now - estimate);
164                }
165
166                // Check for gap: event not in smaller interval but is in this larger one
167                // Since we iterate smallest→largest, if we're here, smaller interval returned None
168                let prev_config = &configs[interval_idx - 1];
169
170                // Calculate where the smaller interval coverage ends
171                // SAFETY: This cannot panic. prev_config came from counter's intervals (index interval_idx-1).
172                // If this panics, it's a bug in tiny-counter, not user error.
173                let prev_interval_start = counter.interval_start(prev_config.time_unit()).expect(
174                    "BUG: Smaller interval must exist - we just retrieved prev_config from counter's intervals",
175                );
176                let prev_coverage_end = prev_config.first_moment_ever(prev_interval_start);
177
178                // Which bucket in THIS interval does that coverage end fall into?
179                let coverage_end_bucket = time_unit
180                    .bucket_idx(interval_start, prev_coverage_end)
181                    .unwrap_or_default();
182
183                // If event is in the same bucket (or earlier) where coverage ends, it's in the gap
184                if bucket_idx <= coverage_end_bucket {
185                    // GAP DETECTED!
186                    // Gap boundaries: between the far end of this bucket and where smaller interval coverage ends
187
188                    // Far end of the bucket (earliest time, furthest from now)
189                    // For bucket 0, this is one full bucket duration ago
190                    let gap_earliest = time_unit.bucket_start(clock_now, bucket_idx);
191                    // End of smaller interval coverage (latest time, closest to now within gap)
192                    let gap_latest = prev_coverage_end;
193
194                    // Calculate gap midpoint as a timestamp
195                    let gap_midpoint = gap_earliest + ((gap_latest - gap_earliest) / 2);
196
197                    // Return duration from now to gap midpoint
198                    return Some(clock_now - gap_midpoint);
199                }
200
201                // Event is beyond the gap, use normal estimation
202                let estimate = time_unit.bucket_midway(clock_now, interval_start, bucket_idx);
203                return Some(clock_now - estimate);
204            }
205        }
206        None
207    }
208
209    /// Returns an estimate for the time since the event was first seen.
210    ///
211    /// Returns None if the event has never been recorded or doesn't exist.
212    /// This searches for the oldest non-zero bucket across all tracked time units.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use tiny_counter::EventStore;
218    /// use chrono::Duration;
219    ///
220    /// let store = EventStore::new();
221    /// store.record("user_signup");
222    ///
223    /// let time_since = store.query("user_signup").first_seen();
224    /// assert!(time_since.is_some());
225    ///
226    /// // Check for events never recorded
227    /// let never_seen = store.query("never_happened").first_seen();
228    /// assert!(never_seen.is_none());
229    /// ```
230    pub fn first_seen(self) -> Option<Duration> {
231        let clock_now = self.store.clock_now();
232        let counter_arc = self.store.get_counter_for_query(&self.event_id)?;
233        let mut counter = counter_arc.lock().unwrap();
234        counter.advance_if_needed(clock_now);
235
236        // Get configs in ascending order, iterate in reverse for largest to smallest
237        let configs = self.store.config.configs();
238
239        // Iterate from largest to smallest (end to start)
240        for interval_idx in (0..configs.len()).rev() {
241            let config = &configs[interval_idx];
242            let time_unit = config.time_unit();
243            if let Some(bucket_idx) = counter.first_seen_in(time_unit) {
244                // SAFETY: This cannot panic. We got `config` from the counter's own interval list,
245                // so the time_unit MUST exist in the counter. If this panics, it's a bug in
246                // tiny-counter's internal logic, not user error.
247                let interval_start = counter.interval_start(time_unit).expect(
248                    "BUG: Interval must exist - we just retrieved config from counter's intervals",
249                );
250                // Check if we're on the smallest interval (e.g. minutes)
251                if interval_idx == 0 {
252                    // Last interval - we have to accept this as our result.
253                    let midway = time_unit.bucket_midway(clock_now, interval_start, bucket_idx);
254                    return Some(clock_now - midway);
255                }
256
257                // Get the next smaller interval
258                let next_config = &configs[interval_idx - 1];
259
260                // Starting bucket is the bucket in larger config which an event that would
261                // have been recorded in the last bucket of the smaller config.
262
263                // SAFETY: This cannot panic. next_config came from counter's intervals (index interval_idx-1).
264                // If this panics, it's a bug in tiny-counter, not user error.
265                let next_interval_start = counter.interval_start(next_config.time_unit()).expect(
266                    "BUG: Smaller interval must exist - we just retrieved next_config from counter's intervals",
267                );
268
269                let starting_moment = next_config.first_moment_ever(next_interval_start);
270                let starting_bucket = time_unit
271                    .bucket_idx(interval_start, starting_moment)
272                    .unwrap_or_default()
273                    + 1;
274
275                // If the bucket where we found the earliest event doesn't correspond to
276                // a bucket of smaller granularity, then we guess the midpoint of the larger bucket.
277                if bucket_idx > starting_bucket {
278                    // Beyond starting bucket - this is our answer
279                    let midway = time_unit.bucket_midway(clock_now, interval_start, bucket_idx);
280                    return Some(clock_now - midway);
281                }
282                // Next, we check if all the events between the starting point and now
283                // are accounted for by the buckets of smaller granularity.
284                let this_count = counter
285                    .sum_range(time_unit, 0..starting_bucket + 1)
286                    .unwrap_or(0);
287                let next_count = counter
288                    .sum_range(next_config.time_unit(), 0..usize::MAX)
289                    .unwrap_or(0);
290
291                // If there was an over count, then we know the first event happened somewhere
292                // after the larger bucket started, but before all of the smaller buckets started.
293                if this_count > next_count {
294                    // So we guess at the midpoint between those two.
295                    let earliest = clock_now - time_unit.bucket_end(interval_start, bucket_idx);
296                    let latest = clock_now - next_config.first_moment_ever(next_interval_start);
297                    return Some((earliest + latest) / 2);
298                }
299                // If not, then: we know that there is a more granular count available,
300                // and we just go to the next time unit.
301            }
302        }
303
304        None
305    }
306
307    /// Returns the time since the event was first seen in a specific time unit.
308    ///
309    /// Returns None if the event has never been recorded, doesn't exist,
310    /// or the time unit is not tracked.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// use tiny_counter::{EventStore, TimeUnit};
316    /// use chrono::Duration;
317    ///
318    /// let store = EventStore::new();
319    /// store.record("page_view");
320    ///
321    /// let time_since = store.query("page_view").first_seen_in(TimeUnit::Days);
322    /// assert!(time_since.is_some());
323    /// ```
324    pub fn first_seen_in(self, time_unit: TimeUnit) -> Option<Duration> {
325        let clock_now = self.store.clock_now();
326        let time_unit = self.store.config.specified_time_unit(time_unit);
327        let counter_arc = self.store.get_counter_for_query(&self.event_id)?;
328        let mut counter = counter_arc.lock().unwrap();
329        counter.advance_if_needed(clock_now);
330
331        if let Some(bucket_idx) = counter.first_seen_in(time_unit) {
332            // Convert bucket index to duration
333            let duration = time_unit.duration() * bucket_idx as i32;
334            Some(duration)
335        } else {
336            None
337        }
338    }
339}
340
341/// Builder for querying a specific time range.
342///
343/// Provides aggregation methods like sum(), average(), and iteration over buckets.
344#[must_use = "query builders do nothing unless consumed"]
345pub struct RangeQuery {
346    store: Arc<EventStoreInner>,
347    event_id: String,
348    time_unit: TimeUnit,
349    range: Range<usize>,
350}
351
352impl RangeQuery {
353    /// Creates a new RangeQuery.
354    fn new(
355        store: Arc<EventStoreInner>,
356        event_id: String,
357        time_unit: TimeUnit,
358        range: Range<usize>,
359    ) -> Self {
360        Self {
361            store,
362            event_id,
363            time_unit,
364            range,
365        }
366    }
367
368    /// Limit the range to at most N buckets.
369    pub fn take(mut self, n: usize) -> Self {
370        self.range.end = self.range.start + n;
371        self
372    }
373
374    /// Sum all events in the range.
375    ///
376    /// Returns None if the event doesn't exist or the time unit isn't tracked.
377    ///
378    /// # Examples
379    ///
380    /// ```
381    /// use tiny_counter::EventStore;
382    ///
383    /// let store = EventStore::new();
384    /// store.record_count("clicks", 10);
385    ///
386    /// let total = store.query("clicks").last_days(7).sum();
387    /// assert_eq!(total, Some(10));
388    /// ```
389    pub fn sum(self) -> Option<u32> {
390        let clock_now = self.store.clock_now();
391        let counter_arc = self.store.get_counter_for_query(&self.event_id)?;
392        let time_unit = self.store.config.specified_time_unit(self.time_unit);
393        let mut counter = counter_arc.lock().unwrap();
394        counter.advance_if_needed(clock_now);
395        counter.sum_range(time_unit, self.range)
396    }
397
398    /// Calculate the average count per bucket.
399    ///
400    /// Returns None if the event doesn't exist, the time unit isn't tracked,
401    /// or there's no data in the range.
402    ///
403    /// # Examples
404    ///
405    /// ```
406    /// use tiny_counter::EventStore;
407    ///
408    /// let store = EventStore::new();
409    /// store.record_count("api_calls", 100);
410    ///
411    /// let avg = store.query("api_calls").last_days(7).average();
412    /// assert!(avg.is_some());
413    /// ```
414    pub fn average(self) -> Option<f64> {
415        let count = (self.range.end - self.range.start) as f64;
416        let sum = self.sum()?;
417        match sum {
418            0 => Some(0.0),
419            s => {
420                if count > 0.0 {
421                    Some(s as f64 / count)
422                } else {
423                    None
424                }
425            }
426        }
427    }
428
429    /// Calculate the average excluding zero buckets.
430    ///
431    /// Returns None if the event doesn't exist, the time unit isn't tracked,
432    /// or there are no non-zero buckets.
433    pub fn average_nonzero(self) -> Option<f64> {
434        let buckets = self.into_buckets();
435        if buckets.is_empty() {
436            return None;
437        }
438
439        let non_zero: Vec<u32> = buckets.iter().copied().filter(|&x| x > 0).collect();
440        if non_zero.is_empty() {
441            return None;
442        }
443
444        let sum: u64 = non_zero
445            .iter()
446            .fold(0u64, |acc, &val| acc.saturating_add(val as u64));
447        let avg = sum as f64 / non_zero.len() as f64;
448        Some(avg)
449    }
450
451    /// Sum the number of non-zero buckets.
452    ///
453    /// Returns None if the event doesn't exist or the time unit isn't tracked.
454    pub fn count_nonzero(self) -> Option<usize> {
455        let buckets = self.into_buckets();
456        if buckets.is_empty() {
457            return None;
458        }
459
460        let count = buckets.iter().filter(|&&x| x > 0).count();
461        Some(count)
462    }
463
464    /// Returns the bucket index of the last seen event.
465    ///
466    /// Returns None if the event doesn't exist, the time unit isn't tracked,
467    /// or the event has never been recorded.
468    pub fn last_seen(self) -> Option<usize> {
469        let clock_now = self.store.clock_now();
470        let time_unit = self.store.config.specified_time_unit(self.time_unit);
471        let counter_arc = self.store.get_counter_for_query(&self.event_id)?;
472        let mut counter = counter_arc.lock().unwrap();
473        counter.advance_if_needed(clock_now);
474        counter.last_seen_in(time_unit)
475    }
476
477    /// Returns the bucket index of the first seen event.
478    ///
479    /// Returns None if the event doesn't exist, the time unit isn't tracked,
480    /// or the event has never been recorded.
481    pub fn first_seen(self) -> Option<usize> {
482        let clock_now = self.store.clock_now();
483        let time_unit = self.store.config.specified_time_unit(self.time_unit);
484        let counter_arc = self.store.get_counter_for_query(&self.event_id)?;
485        let mut counter = counter_arc.lock().unwrap();
486        counter.advance_if_needed(clock_now);
487        counter.first_seen_in(time_unit)
488    }
489
490    /// Returns a vector of bucket counts for the range.
491    pub fn into_buckets(self) -> Vec<u32> {
492        let clock_now = self.store.clock_now();
493        let time_unit = self.store.config.specified_time_unit(self.time_unit);
494
495        let counter_arc = match self.store.get_counter_for_query(&self.event_id) {
496            Some(arc) => arc,
497            None => return Vec::new(),
498        };
499
500        let mut counter = counter_arc.lock().unwrap();
501        counter.advance_if_needed(clock_now);
502
503        let interval = match counter.interval(time_unit) {
504            Some(interval) => interval,
505            None => return Vec::new(),
506        };
507
508        let end = self.range.end.min(interval.bucket_count());
509
510        (self.range.start..end)
511            .filter_map(|i| interval.bucket_value(i))
512            .collect()
513    }
514}
515
516/// Builder for querying multiple events.
517#[must_use = "query builders do nothing unless consumed"]
518pub struct MultiQuery {
519    store: Arc<EventStoreInner>,
520    event_ids: Vec<String>,
521}
522
523impl MultiQuery {
524    /// Creates a new MultiQuery builder.
525    pub(crate) fn new(store: Arc<EventStoreInner>, event_ids: Vec<String>) -> Self {
526        Self { store, event_ids }
527    }
528
529    pub(crate) fn last(self, n: usize, unit: TimeUnit) -> MultiRangeQuery {
530        MultiRangeQuery::new(self.store, self.event_ids, unit, 0..n)
531    }
532
533    /// Query the last N seconds.
534    pub fn last_seconds(self, n: usize) -> MultiRangeQuery {
535        self.last(n, TimeUnit::Seconds)
536    }
537
538    /// Query the last N minutes across all events.
539    pub fn last_minutes(self, n: usize) -> MultiRangeQuery {
540        self.last(n, TimeUnit::Minutes)
541    }
542
543    /// Query the last N hours across all events.
544    pub fn last_hours(self, n: usize) -> MultiRangeQuery {
545        self.last(n, TimeUnit::Hours)
546    }
547
548    /// Query the last N days across all events.
549    pub fn last_days(self, n: usize) -> MultiRangeQuery {
550        self.last(n, TimeUnit::Days)
551    }
552
553    /// Query the last N weeks across all events.
554    pub fn last_weeks(self, n: usize) -> MultiRangeQuery {
555        self.last(n, TimeUnit::Weeks)
556    }
557
558    /// Query the last N months across all events.
559    pub fn last_months(self, n: usize) -> MultiRangeQuery {
560        self.last(n, TimeUnit::Months)
561    }
562
563    /// Query the last N years across all events.
564    pub fn last_years(self, n: usize) -> MultiRangeQuery {
565        self.last(n, TimeUnit::Years)
566    }
567
568    /// Query all available data across all events.
569    pub fn ever(self) -> MultiRangeQuery {
570        // Use TimeUnit::Ever which will be resolved per-event during query execution
571        // For multi-event queries, we take the minimum across events
572        self.last(usize::MAX, TimeUnit::Ever)
573    }
574}
575
576/// Range query over multiple events.
577#[must_use = "query builders do nothing unless consumed"]
578pub struct MultiRangeQuery {
579    store: Arc<EventStoreInner>,
580    event_ids: Vec<String>,
581    time_unit: TimeUnit,
582    range: Range<usize>,
583}
584
585impl MultiRangeQuery {
586    fn new(
587        store: Arc<EventStoreInner>,
588        event_ids: Vec<String>,
589        time_unit: TimeUnit,
590        range: Range<usize>,
591    ) -> Self {
592        Self {
593            store,
594            event_ids,
595            time_unit,
596            range,
597        }
598    }
599
600    /// Sum all events across all tracked counters.
601    ///
602    /// Uses saturating addition - if the sum exceeds u32::MAX, it saturates.
603    pub fn sum(self) -> Option<u32> {
604        let clock_now = self.store.clock_now();
605
606        let mut total = 0u32;
607        let mut found_any = false;
608        let time_unit = self.store.config.specified_time_unit(self.time_unit);
609        for event_id in &self.event_ids {
610            if let Some(counter_arc) = self.store.get_counter_for_query(event_id) {
611                let mut counter = counter_arc.lock().unwrap();
612                counter.advance_if_needed(clock_now);
613                if let Some(sum) = counter.sum_range(time_unit, self.range.clone()) {
614                    total = total.saturating_add(sum);
615                    found_any = true;
616                }
617            }
618        }
619
620        if found_any {
621            Some(total)
622        } else {
623            None
624        }
625    }
626
627    /// Calculate average across all events.
628    pub fn average(self) -> Option<f64> {
629        let count = (self.range.end - self.range.start) as f64;
630        let sum = self.sum();
631        match sum {
632            None => None,
633            Some(0) => Some(0.0),
634            Some(s) => {
635                if count > 0.0 {
636                    Some(s as f64 / count)
637                } else {
638                    None
639                }
640            }
641        }
642    }
643}
644
645/// Builder for calculating ratios between two events.
646#[must_use = "query builders do nothing unless consumed"]
647pub struct RatioQuery {
648    store: Arc<EventStoreInner>,
649    numerator_id: String,
650    denominator_id: String,
651}
652
653impl RatioQuery {
654    /// Creates a new RatioQuery builder.
655    pub(crate) fn new(
656        store: Arc<EventStoreInner>,
657        numerator_id: String,
658        denominator_id: String,
659    ) -> Self {
660        Self {
661            store,
662            numerator_id,
663            denominator_id,
664        }
665    }
666
667    pub(crate) fn last(self, n: usize, unit: TimeUnit) -> Option<f64> {
668        self.calculate_ratio(unit, 0..n)
669    }
670
671    /// Calculate ratio over the last N seconds.
672    pub fn last_seconds(self, n: usize) -> Option<f64> {
673        self.last(n, TimeUnit::Seconds)
674    }
675
676    /// Calculate ratio over the last N minutes.
677    pub fn last_minutes(self, n: usize) -> Option<f64> {
678        self.last(n, TimeUnit::Minutes)
679    }
680
681    /// Calculate ratio over the last N hours.
682    pub fn last_hours(self, n: usize) -> Option<f64> {
683        self.last(n, TimeUnit::Hours)
684    }
685
686    /// Calculate ratio over the last N days.
687    pub fn last_days(self, n: usize) -> Option<f64> {
688        self.last(n, TimeUnit::Days)
689    }
690
691    /// Calculate ratio over the last N weeks.
692    pub fn last_weeks(self, n: usize) -> Option<f64> {
693        self.last(n, TimeUnit::Weeks)
694    }
695
696    /// Calculate ratio over the last N months.
697    pub fn last_months(self, n: usize) -> Option<f64> {
698        self.last(n, TimeUnit::Months)
699    }
700
701    /// Calculate ratio over the last N years.
702    pub fn last_years(self, n: usize) -> Option<f64> {
703        self.last(n, TimeUnit::Years)
704    }
705
706    /// Calculate ratio over all available data.
707    pub fn ever(self) -> Option<f64> {
708        // Use TimeUnit::Ever which will be resolved during query execution
709        self.last(usize::MAX, TimeUnit::Ever)
710    }
711
712    fn calculate_ratio(self, time_unit: TimeUnit, range: Range<usize>) -> Option<f64> {
713        let clock_now = self.store.clock_now();
714
715        // Resolve TimeUnit::Ever from the configuration
716        let time_unit = self.store.config.specified_time_unit(time_unit);
717
718        // Get counters first
719        let numerator = self.store.get_counter_for_query(&self.numerator_id)?;
720        let mut numerator_counter = numerator.lock().unwrap();
721        numerator_counter.advance_if_needed(clock_now);
722
723        let denominator = self.store.get_counter_for_query(&self.denominator_id)?;
724        let mut denominator_counter = denominator.lock().unwrap();
725        denominator_counter.advance_if_needed(clock_now);
726
727        let numerator_sum = numerator_counter
728            .sum_range(time_unit, range.clone())
729            .unwrap_or(0);
730        let denominator_sum = denominator_counter
731            .sum_range(time_unit, range.clone())
732            .unwrap_or(0);
733
734        if denominator_sum == 0 {
735            None // Avoid division by zero
736        } else {
737            Some(numerator_sum as f64 / denominator_sum as f64)
738        }
739    }
740}
741
742/// Builder for calculating deltas (net changes) between two events.
743#[must_use = "query builders do nothing unless consumed"]
744pub struct DeltaQuery {
745    store: Arc<EventStoreInner>,
746    positive_id: String,
747    negative_id: String,
748}
749
750impl DeltaQuery {
751    /// Creates a new DeltaQuery builder.
752    pub(crate) fn new(
753        store: Arc<EventStoreInner>,
754        positive_id: String,
755        negative_id: String,
756    ) -> Self {
757        Self {
758            store,
759            positive_id,
760            negative_id,
761        }
762    }
763
764    pub(crate) fn last(self, n: usize, unit: TimeUnit) -> DeltaRangeQuery {
765        DeltaRangeQuery::new(self.store, self.positive_id, self.negative_id, unit, 0..n)
766    }
767
768    /// Calculate delta over the last N seconds.
769    pub fn last_seconds(self, n: usize) -> DeltaRangeQuery {
770        self.last(n, TimeUnit::Seconds)
771    }
772
773    /// Calculate delta over the last N minutes.
774    pub fn last_minutes(self, n: usize) -> DeltaRangeQuery {
775        self.last(n, TimeUnit::Minutes)
776    }
777
778    /// Calculate delta over the last N hours.
779    pub fn last_hours(self, n: usize) -> DeltaRangeQuery {
780        self.last(n, TimeUnit::Hours)
781    }
782
783    /// Calculate delta over the last N days.
784    pub fn last_days(self, n: usize) -> DeltaRangeQuery {
785        self.last(n, TimeUnit::Days)
786    }
787
788    /// Calculate delta over the last N weeks.
789    pub fn last_weeks(self, n: usize) -> DeltaRangeQuery {
790        self.last(n, TimeUnit::Weeks)
791    }
792
793    /// Calculate delta over the last N months.
794    pub fn last_months(self, n: usize) -> DeltaRangeQuery {
795        self.last(n, TimeUnit::Months)
796    }
797
798    /// Calculate delta over the last N years.
799    pub fn last_years(self, n: usize) -> DeltaRangeQuery {
800        self.last(n, TimeUnit::Years)
801    }
802
803    /// Calculate delta over all available data.
804    pub fn ever(self) -> DeltaRangeQuery {
805        // Use TimeUnit::Ever which will be resolved during query execution
806        self.last(usize::MAX, TimeUnit::Ever)
807    }
808}
809
810/// Range query for delta calculations.
811#[must_use = "query builders do nothing unless consumed"]
812pub struct DeltaRangeQuery {
813    store: Arc<EventStoreInner>,
814    positive_id: String,
815    negative_id: String,
816    time_unit: TimeUnit,
817    range: Range<usize>,
818}
819
820impl DeltaRangeQuery {
821    fn new(
822        store: Arc<EventStoreInner>,
823        positive_id: String,
824        negative_id: String,
825        time_unit: TimeUnit,
826        range: Range<usize>,
827    ) -> Self {
828        Self {
829            store,
830            positive_id,
831            negative_id,
832            time_unit,
833            range,
834        }
835    }
836
837    /// Calculate the net change (positive - negative).
838    ///
839    /// Returns 0 if both events don't exist or the time unit isn't tracked.
840    /// Can return negative values if negative exceeds positive.
841    pub fn sum(self) -> i64 {
842        let clock_now = self.store.clock_now();
843
844        // Get counters (if they exist)
845        let positive_counter = self.store.get_counter_for_query(&self.positive_id);
846        let negative_counter = self.store.get_counter_for_query(&self.negative_id);
847
848        let time_unit = self.store.config.specified_time_unit(self.time_unit);
849
850        let positive_sum = match positive_counter {
851            Some(counter_arc) => {
852                let mut counter = counter_arc.lock().unwrap();
853                counter.advance_if_needed(clock_now);
854                counter
855                    .sum_range(time_unit, self.range.clone())
856                    .unwrap_or(0) as i64
857            }
858            None => 0,
859        };
860
861        let negative_sum = match negative_counter {
862            Some(counter_arc) => {
863                let mut counter = counter_arc.lock().unwrap();
864                counter.advance_if_needed(clock_now);
865                counter
866                    .sum_range(time_unit, self.range.clone())
867                    .unwrap_or(0) as i64
868            }
869            None => 0,
870        };
871
872        positive_sum - negative_sum
873    }
874
875    /// Calculate the average delta per bucket.
876    pub fn average(self) -> f64 {
877        let count = (self.range.end - self.range.start) as f64;
878        let sum = self.sum();
879        if count > 0.0 {
880            sum as f64 / count
881        } else {
882            0.0
883        }
884    }
885}
886
887#[cfg(test)]
888mod tests {
889
890    use std::sync::Arc;
891
892    use chrono::{DateTime, Datelike, Duration, TimeZone, Utc};
893
894    use crate::{Clock, EventStore, TestClock, TimeUnit};
895
896    #[test]
897    fn test_query_last_days_sum() {
898        let store = EventStore::new();
899        store.record_count("event", 5);
900        store.record_count("event", 3);
901
902        let sum = store.query("event").last_days(1).sum();
903        assert_eq!(sum, Some(8));
904    }
905
906    #[test]
907    fn test_query_nonexistent_event() {
908        let store = EventStore::new();
909        let sum = store.query("nonexistent").last_days(1).sum();
910        assert_eq!(sum, None);
911    }
912
913    #[test]
914    fn test_range_query_average() {
915        let store = EventStore::new();
916        store.record_count("event", 10);
917
918        let avg = store.query("event").last_days(2).average();
919        // 10 events across 2 days = 5.0 average
920        assert_eq!(avg, Some(5.0));
921    }
922
923    #[test]
924    fn test_range_query_take() {
925        let store = EventStore::new();
926        store.record_count("event", 5);
927
928        let sum = store.query("event").days_from(0).take(1).sum();
929        assert_eq!(sum, Some(5));
930    }
931
932    #[test]
933    fn test_multi_query_sum() {
934        let store = EventStore::new();
935        store.record_count("event1", 5);
936        store.record_count("event2", 3);
937
938        let sum = store.query_many(&["event1", "event2"]).last_days(1).sum();
939        assert_eq!(sum, Some(8));
940    }
941
942    #[test]
943    fn test_ratio_query() {
944        let store = EventStore::new();
945        store.record_count("num", 6);
946        store.record_count("denom", 3);
947
948        let ratio = store.query_ratio("num", "denom").last_days(1);
949        assert_eq!(ratio, Some(2.0));
950    }
951
952    #[test]
953    fn test_ratio_query_division_by_zero() {
954        let store = EventStore::new();
955        store.record_count("num", 6);
956        // denominator has 0 events
957
958        let ratio = store.query_ratio("num", "denom").last_days(1);
959        assert_eq!(ratio, None); // Should return None for division by zero
960    }
961
962    #[test]
963    fn test_delta_query_positive() {
964        let store = EventStore::new();
965        store.record_count("pos", 10);
966        store.record_count("neg", 3);
967
968        let delta = store.query_delta("pos", "neg").last_days(1).sum();
969        assert_eq!(delta, 7);
970    }
971
972    #[test]
973    fn test_delta_query_negative() {
974        let store = EventStore::new();
975        store.record_count("pos", 3);
976        store.record_count("neg", 10);
977
978        let delta = store.query_delta("pos", "neg").last_days(1).sum();
979        assert_eq!(delta, -7); // Can be negative!
980    }
981
982    #[test]
983    fn test_delta_query_zero() {
984        let store = EventStore::new();
985        let delta = store.query_delta("pos", "neg").last_days(1).sum();
986        assert_eq!(delta, 0);
987    }
988
989    #[test]
990    fn test_count_nonzero() {
991        let store = EventStore::new();
992        store.record_count("event", 5);
993        // Only 1 bucket has data
994
995        let count = store.query("event").last_days(7).count_nonzero();
996        assert_eq!(count, Some(1));
997    }
998
999    #[test]
1000    fn test_average_nonzero() {
1001        let store = EventStore::new();
1002        store.record_count("event", 10);
1003
1004        let avg = store.query("event").last_days(7).average_nonzero();
1005        // Only 1 bucket has data with value 10
1006        assert_eq!(avg, Some(10.0));
1007    }
1008
1009    #[test]
1010    fn test_ever_uses_longest_time_unit() {
1011        // Test that ever() uses the longest configured time unit for the event
1012        let store = EventStore::new();
1013        store.record_count("event", 100);
1014
1015        // The default config includes Years as the longest time unit
1016        let sum = store.query("event").ever().sum();
1017        assert_eq!(sum, Some(100));
1018    }
1019
1020    #[test]
1021    fn test_ever_with_nonexistent_event() {
1022        // Test that ever() handles nonexistent events gracefully
1023        let store = EventStore::new();
1024
1025        let sum = store.query("nonexistent").ever().sum();
1026        assert_eq!(sum, None);
1027    }
1028
1029    #[test]
1030    fn test_ever_uses_time_unit_ever_variant() {
1031        // Test that ever() now uses TimeUnit::Ever instead of loading counter twice
1032        let store = EventStore::new();
1033        store.record_count("event", 100);
1034
1035        // Create a query with ever()
1036        let query = store.query("event").ever();
1037
1038        // The query should have TimeUnit::Ever before execution
1039        // (We can't directly inspect it, but we can verify it still works)
1040        let sum = query.sum();
1041        assert_eq!(sum, Some(100));
1042    }
1043
1044    #[test]
1045    fn test_multi_query_ever() {
1046        let store = EventStore::new();
1047        store.record_count("event1", 50);
1048        store.record_count("event2", 75);
1049
1050        let sum = store.query_many(&["event1", "event2"]).ever().sum();
1051        assert_eq!(sum, Some(125));
1052    }
1053
1054    #[test]
1055    fn test_ratio_query_ever() {
1056        let store = EventStore::new();
1057        store.record_count("num", 100);
1058        store.record_count("denom", 25);
1059
1060        let ratio = store.query_ratio("num", "denom").ever();
1061        assert_eq!(ratio, Some(4.0));
1062    }
1063
1064    #[test]
1065    fn test_delta_query_ever() {
1066        let store = EventStore::new();
1067        store.record_count("pos", 150);
1068        store.record_count("neg", 50);
1069
1070        let delta = store.query_delta("pos", "neg").ever().sum();
1071        assert_eq!(delta, 100);
1072    }
1073
1074    #[test]
1075    fn test_multi_range_query_handles_large_values() {
1076        let store = EventStore::new();
1077
1078        // Record max u32 values
1079        store.record_count("event1", u32::MAX);
1080        store.record_count("event2", u32::MAX);
1081        store.record_count("event3", u32::MAX);
1082
1083        let sum = store
1084            .query_many(&["event1", "event2", "event3"])
1085            .last_days(1)
1086            .sum();
1087
1088        // Sum saturates at u32::MAX
1089        assert_eq!(sum, Some(u32::MAX));
1090    }
1091
1092    #[test]
1093    fn test_average_nonzero_handles_large_values() {
1094        use crate::store::builder::EventStoreBuilder;
1095
1096        let store = EventStoreBuilder::new().track_days(5).build().unwrap();
1097
1098        // Record large values that will saturate at u32::MAX
1099        // These all go to the same bucket
1100        store.record_count("event", u32::MAX / 3);
1101        store.record_count("event", u32::MAX / 3);
1102        store.record_count("event", u32::MAX / 3);
1103
1104        let avg = store.query("event").last_days(5).average_nonzero();
1105
1106        // Should handle large values correctly
1107        assert!(avg.is_some());
1108        let avg_value = avg.unwrap();
1109        // Single bucket with 3×(u32::MAX/3) = u32::MAX, so average equals u32::MAX
1110        assert_eq!(avg_value, u32::MAX as f64);
1111    }
1112
1113    #[test]
1114    fn test_first_seen_returns_oldest_event_touching_buckets() {
1115        use chrono::{TimeZone, Utc};
1116        // Use fixed time at noon to avoid midnight crossing issues
1117        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1118        let clock = TestClock::build_for_testing_at(fixed_time);
1119        let store = EventStore::builder()
1120            .track_days(7)
1121            .track_hours(24)
1122            .track_minutes(60)
1123            .with_clock(Arc::new(clock.clone()))
1124            .build()
1125            .unwrap();
1126
1127        store.record("event");
1128        clock.advance(Duration::hours(5));
1129        store.record("event");
1130
1131        let first = store.query("event").first_seen();
1132        let last = store.query("event").last_seen();
1133
1134        assert_eq!(first, Some(Duration::hours(5) + Duration::minutes(30)));
1135
1136        // last_seen should return 0 (most recent)
1137        assert!(last.is_some());
1138        assert!(last.unwrap() == Duration::minutes(0));
1139    }
1140
1141    #[test]
1142    fn test_first_seen_returns_oldest_event_overlapping_buckets() {
1143        use chrono::{TimeZone, Utc};
1144        // Use fixed time at noon to avoid midnight crossing issues
1145        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1146        let clock = TestClock::build_for_testing_at(fixed_time);
1147        let store = EventStore::builder()
1148            .track_days(7)
1149            .track_hours(24)
1150            .track_minutes(60 * 24)
1151            .with_clock(Arc::new(clock.clone()))
1152            .build()
1153            .unwrap();
1154
1155        store.record("event");
1156        clock.advance(Duration::hours(5));
1157        store.record("event");
1158
1159        let first = store.query("event").first_seen();
1160        let last = store.query("event").last_seen();
1161
1162        assert_eq!(first, Some(Duration::hours(5) + Duration::seconds(30)));
1163
1164        // last_seen should return 0 (most recent)
1165        assert!(last.is_some());
1166        assert!(last.unwrap() == Duration::minutes(0));
1167    }
1168
1169    #[test]
1170    fn test_first_seen_returns_oldest_event_disjoint_buckets() {
1171        use chrono::{TimeZone, Utc};
1172        // Use fixed time at noon to avoid midnight crossing issues
1173        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1174        let clock = TestClock::build_for_testing_at(fixed_time);
1175        let store = EventStore::builder()
1176            .track_days(7)
1177            .track_minutes(60)
1178            .with_clock(Arc::new(clock.clone()))
1179            .build()
1180            .unwrap();
1181
1182        store.record("event");
1183        clock.advance(Duration::hours(5)); // now at 1700.
1184        store.record("event");
1185
1186        // first_seen is somewhere between midnight and 1600.
1187        let first = store.query("event").first_seen();
1188        let last = store.query("event").last_seen();
1189
1190        let expected = ago(clock.now(), 0, 8, 00);
1191        eprintln!("Expected at {}, {expected:?} ago", clock.now() - expected);
1192        eprintln!(
1193            "Actual   at {}, {:?} ago",
1194            clock.now() - first.unwrap(),
1195            first.unwrap()
1196        );
1197        // (Duration::days(1) + Duration::hours(1)) / 2
1198        assert_eq!(first, Some(expected));
1199
1200        // last_seen should return 0 (most recent)
1201        assert!(last.is_some());
1202        assert!(last.unwrap() == Duration::minutes(0));
1203    }
1204
1205    fn ago(now: DateTime<Utc>, days_ago: i64, hours: u32, minutes: u32) -> Duration {
1206        use chrono::{TimeZone, Utc};
1207        let then = Utc
1208            .with_ymd_and_hms(now.year(), now.month(), now.day(), hours, minutes, 0)
1209            .unwrap()
1210            - Duration::days(days_ago);
1211        now - then
1212    }
1213
1214    #[test]
1215    fn test_first_seen_returns_oldest_event_disjoint_muilti_buckets_under() {
1216        use chrono::{TimeZone, Utc};
1217        // Use fixed time at noon to avoid midnight crossing issues
1218        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1219        let clock = TestClock::build_for_testing_at(fixed_time);
1220        let store = EventStore::builder()
1221            .track_days(7)
1222            .track_hours(36)
1223            .with_clock(Arc::new(clock.clone()))
1224            .build()
1225            .unwrap();
1226
1227        store.record("event");
1228        clock.advance(Duration::hours(25));
1229        store.record("event");
1230
1231        let first = store.query("event").first_seen();
1232        let last = store.query("event").last_seen();
1233
1234        assert_eq!(first, Some(Duration::hours(25) + Duration::minutes(30)));
1235
1236        // last_seen should return 0 (most recent)
1237        assert!(last.is_some());
1238        assert!(last.unwrap() == Duration::minutes(0));
1239    }
1240
1241    #[test]
1242    fn test_first_seen_returns_oldest_event_disjoint_muilti_buckets_over() {
1243        use chrono::{TimeZone, Utc};
1244        // Use fixed time at 1400 to avoid midnight crossing issues
1245        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 10, 0, 0).unwrap();
1246        let clock = TestClock::build_for_testing_at(fixed_time);
1247        let store = EventStore::builder()
1248            .track_days(7)
1249            .track_hours(36)
1250            .with_clock(Arc::new(clock.clone()))
1251            .build()
1252            .unwrap();
1253
1254        store.record("event");
1255        clock.advance(Duration::hours(37)); // now is 10 + 37 = 47 = 23h.
1256        store.record("event");
1257
1258        // Query time is 11pm
1259        // first_seen is somewhere between midnight and 11am
1260        let first = store.query("event").first_seen();
1261        let last = store.query("event").last_seen();
1262
1263        // When query time is BEFORE midnight, so first at between end of the 36th hour bucket
1264        // and the end of 2nd day bucket.
1265        let expected = ago(clock.now(), 1, 5, 30);
1266        eprintln!("Expected at {}, {expected:?} ago", clock.now() - expected);
1267        eprintln!(
1268            "Actual   at {}, {:?} ago",
1269            clock.now() - first.unwrap(),
1270            first.unwrap()
1271        );
1272        assert_eq!(first, Some(expected));
1273
1274        // last_seen should return 0 (most recent)
1275        assert_eq!(last, Some(Duration::minutes(0)));
1276
1277        clock.advance(Duration::hours(2)); // 23h + 2h = 1am
1278        let first = store.query("event").first_seen();
1279        let last = store.query("event").last_seen();
1280        // When query time is AFTER midnight, so first at between end of the 36th hour bucket
1281        // and the end of 3rd day bucket.
1282        let expected = ago(clock.now(), 2, 6, 30);
1283        eprintln!("Expected at {}, {expected:?} ago", clock.now() - expected);
1284        eprintln!(
1285            "Actual   at {}, {:?} ago",
1286            clock.now() - first.unwrap(),
1287            first.unwrap()
1288        );
1289        assert_eq!(first, Some(expected));
1290        assert_eq!(last, Some(Duration::hours(2) + Duration::minutes(30)));
1291    }
1292
1293    #[test]
1294    fn test_first_seen_with_no_events() {
1295        let store = EventStore::new();
1296        let first = store.query("nonexistent").first_seen();
1297        assert_eq!(first, None);
1298    }
1299
1300    #[test]
1301    fn test_first_seen_in_specific_time_unit() {
1302        let clock = TestClock::build_for_testing();
1303        let store = EventStore::builder()
1304            .with_clock(Arc::new(clock.clone()))
1305            .build()
1306            .unwrap();
1307
1308        // Record first event
1309        store.record("event");
1310        // Advance clock so first event is now 5 hours old
1311        clock.advance(Duration::hours(5));
1312        // Record second event to force rotation
1313        store.record("event");
1314
1315        let first_hours = store.query("event").first_seen_in(TimeUnit::Hours);
1316
1317        // Should see the oldest event (5 hours ago)
1318        assert_eq!(first_hours, Some(Duration::hours(5)));
1319    }
1320
1321    #[test]
1322    fn test_first_seen_with_single_event_touching_intervals() {
1323        let clock = TestClock::build_for_testing();
1324        let store = EventStore::builder()
1325            .with_clock(Arc::new(clock.clone()))
1326            .track_hours(24)
1327            .track_minutes(60)
1328            .build()
1329            .unwrap();
1330
1331        store.record("event");
1332
1333        let first = store.query("event").first_seen();
1334        let last = store.query("event").last_seen();
1335
1336        // Both should be very close to 0 (current time)
1337        assert!(first.is_some());
1338        assert!(last.is_some());
1339        eprintln!("first = {:?}", first.unwrap());
1340        eprintln!("last = {:?}", last.unwrap());
1341        assert!(
1342            first.unwrap() < Duration::hours(1),
1343            "Expected first < 1 hour, got {:?}",
1344            first.unwrap()
1345        );
1346        assert!(last.unwrap() < Duration::hours(1));
1347    }
1348
1349    #[test]
1350    fn test_first_seen_with_single_event_disjoint_intervals() {
1351        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1352        let clock = TestClock::build_for_testing_at(fixed_time);
1353        let store = EventStore::builder()
1354            .with_clock(Arc::new(clock.clone()))
1355            .track_years(2)
1356            // 1 mo ~ 28 days, but 12 * 28 < 365. Disjoint!
1357            .track_months(12)
1358            .track_days(28)
1359            .track_hours(24)
1360            .build()
1361            .unwrap();
1362
1363        store.record("event");
1364
1365        let first = store.query("event").first_seen();
1366        let last = store.query("event").last_seen();
1367
1368        // Event just happened at exactly 12:00 PM, query at 12:00 PM
1369        // Bucket 0 has elapsed time of 0, so estimate is Duration::zero()
1370        assert!(first.is_some());
1371        assert!(last.is_some());
1372
1373        // Both should be very close to 0 (current time) since event just happened
1374        assert_eq!(first, Some(Duration::zero()));
1375        assert_eq!(last, Some(Duration::zero()));
1376    }
1377
1378    #[test]
1379    #[cfg(not(feature = "calendar"))]
1380    fn test_first_seen_across_multiple_time_units() {
1381        let clock = TestClock::build_for_testing();
1382        let store = EventStore::builder()
1383            .with_clock(Arc::new(clock.clone()))
1384            .build()
1385            .unwrap();
1386
1387        // Record an event
1388        store.record("event");
1389
1390        // Advance time by 10 days
1391        clock.advance(Duration::days(10));
1392        // Record another event to force rotation
1393        store.record("event");
1394
1395        let first = store.query("event").first_seen();
1396        let expected = ago(clock.now(), 11, 12, 00);
1397        eprintln!("Expected at {}, {expected:?} ago", clock.now() - expected);
1398        eprintln!(
1399            "Actual   at {}, {:?} ago",
1400            clock.now() - first.unwrap(),
1401            first.unwrap()
1402        );
1403        // Should find the oldest event (10 days ago)
1404        assert_eq!(first, Some(expected));
1405    }
1406
1407    #[test]
1408    fn test_range_query_first_seen() {
1409        let clock = TestClock::build_for_testing();
1410        let store = EventStore::builder()
1411            .with_clock(Arc::new(clock.clone()))
1412            .build()
1413            .unwrap();
1414
1415        store.record("event");
1416        clock.advance(Duration::hours(5));
1417        store.record("event");
1418
1419        // RangeQuery first_seen returns bucket index, not Duration
1420        let first_bucket = store.query("event").last_hours(24).first_seen();
1421
1422        // Should return the oldest bucket index (5 hours ago)
1423        assert_eq!(first_bucket, Some(5));
1424    }
1425
1426    #[test]
1427    fn test_first_seen_consistency_with_last_seen() {
1428        let clock = TestClock::build_for_testing();
1429        let store = EventStore::builder()
1430            .with_clock(Arc::new(clock.clone()))
1431            .build()
1432            .unwrap();
1433
1434        store.record("event"); // First event
1435        clock.advance(Duration::hours(10));
1436        store.record("event"); // Last event
1437
1438        let first_seen = store.query("event").first_seen();
1439        let last_seen = store.query("event").last_seen();
1440
1441        // first_seen should be larger (further back in time)
1442        assert!(first_seen.is_some());
1443        assert!(last_seen.is_some());
1444        assert!(first_seen.unwrap() > last_seen.unwrap());
1445    }
1446
1447    #[test]
1448    fn test_ever_into_buckets_does_not_hang() {
1449        // This test verifies that .ever().into_buckets() doesn't hang by iterating
1450        // 0..usize::MAX. It should resolve to the actual bucket count.
1451        let store = EventStore::new();
1452        store.record_count("event", 100);
1453
1454        // This should complete quickly, not hang for 60+ seconds
1455        let buckets = store.query("event").ever().into_buckets();
1456
1457        // Should have a reasonable number of buckets (the longest time unit's bucket count)
1458        // Default config has Years as longest with some reasonable bucket count
1459        assert!(!buckets.is_empty());
1460        assert!(buckets.len() < 1000); // Sanity check - should be way less than usize::MAX
1461    }
1462
1463    #[test]
1464    fn test_last_seen_returns_recent_event_touching_intervals() {
1465        use chrono::{TimeZone, Utc};
1466        // Touching intervals: 60 minutes + 24 hours (no gaps)
1467        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1468        let clock = TestClock::build_for_testing_at(fixed_time);
1469        let store = EventStore::builder()
1470            .track_days(7)
1471            .track_hours(24)
1472            .track_minutes(60)
1473            .with_clock(Arc::new(clock.clone()))
1474            .build()
1475            .unwrap();
1476
1477        // Record old event
1478        store.record("event");
1479        // Advance 10 minutes, record recent event
1480        clock.advance(Duration::minutes(10));
1481        store.record("event");
1482
1483        let last = store.query("event").last_seen();
1484
1485        // Should find it in minutes bucket with good precision
1486        // Event is in minute bucket 0, elapsed ~0, so estimate ~Duration::zero()
1487        assert!(last.is_some());
1488        assert!(
1489            last.unwrap() < Duration::minutes(1),
1490            "Expected last < 1 minute, got {:?}",
1491            last.unwrap()
1492        );
1493    }
1494
1495    #[test]
1496    fn test_last_seen_returns_recent_event_overlapping_intervals() {
1497        use chrono::{TimeZone, Utc};
1498        // Overlapping intervals: 1440 minutes (24 hours) + 24 hours
1499        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1500        let clock = TestClock::build_for_testing_at(fixed_time);
1501        let store = EventStore::builder()
1502            .track_days(7)
1503            .track_hours(24)
1504            .track_minutes(60 * 24)
1505            .with_clock(Arc::new(clock.clone()))
1506            .build()
1507            .unwrap();
1508
1509        store.record("event");
1510        clock.advance(Duration::minutes(10));
1511        store.record("event");
1512
1513        let last = store.query("event").last_seen();
1514
1515        // Should find it in minutes bucket 0 with good precision
1516        assert!(last.is_some());
1517        assert!(
1518            last.unwrap() < Duration::minutes(1),
1519            "Expected last < 1 minute, got {:?}",
1520            last.unwrap()
1521        );
1522    }
1523
1524    #[test]
1525    fn test_last_seen_gap_event_disjoint_intervals() {
1526        use chrono::{TimeZone, Utc};
1527        // Disjoint intervals: 45 minutes + 24 hours (gap from 45-60 minutes)
1528        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1529        let clock = TestClock::build_for_testing_at(fixed_time);
1530        let store = EventStore::builder()
1531            .track_days(7)
1532            .track_hours(24)
1533            .track_minutes(45)
1534            .with_clock(Arc::new(clock.clone()))
1535            .build()
1536            .unwrap();
1537
1538        store.record("event");
1539        // Advance 50 minutes - this puts the event in the GAP
1540        // Minutes only cover last 45 minutes, so event not in minutes
1541        // But it IS in hour bucket 0
1542        clock.advance(Duration::minutes(50));
1543        // Don't record another event! We want to query the gap event
1544
1545        let last = store.query("event").last_seen();
1546
1547        // With gap detection: should detect gap and return ~52.5 minutes
1548        // (midpoint between 45 minutes ago and 60 minutes ago)
1549        // Gap is [45, 60), so midpoint is 52.5 minutes
1550        let expected = Duration::minutes(52) + Duration::seconds(30);
1551        assert_eq!(
1552            last,
1553            Some(expected),
1554            "Gap event should be estimated at gap midpoint"
1555        );
1556    }
1557
1558    #[test]
1559    fn test_last_seen_recent_event_in_smallest_bucket() {
1560        use chrono::{TimeZone, Utc};
1561        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1562        let clock = TestClock::build_for_testing_at(fixed_time);
1563        let store = EventStore::builder()
1564            .track_days(7)
1565            .track_hours(24)
1566            .track_minutes(60)
1567            .with_clock(Arc::new(clock.clone()))
1568            .build()
1569            .unwrap();
1570
1571        store.record("event");
1572
1573        let last = store.query("event").last_seen();
1574
1575        // Event just happened, should be Duration::zero()
1576        assert_eq!(last, Some(Duration::zero()));
1577    }
1578
1579    #[test]
1580    fn test_last_seen_event_in_gap_multiple_intervals() {
1581        use chrono::{TimeZone, Utc};
1582        // Setup: 30 minutes + 24 hours (gap from 30-60 minutes)
1583        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1584        let clock = TestClock::build_for_testing_at(fixed_time);
1585        let store = EventStore::builder()
1586            .track_days(7)
1587            .track_hours(24)
1588            .track_minutes(30)
1589            .with_clock(Arc::new(clock.clone()))
1590            .build()
1591            .unwrap();
1592
1593        store.record("event");
1594        // Advance 40 minutes - event is in the gap (30-60 minutes)
1595        clock.advance(Duration::minutes(40));
1596        // Don't record another event! We want to query the gap event
1597
1598        let last = store.query("event").last_seen();
1599
1600        // Gap is [30, 60), so midpoint is 45 minutes
1601        let expected = Duration::minutes(45);
1602        assert_eq!(
1603            last,
1604            Some(expected),
1605            "Gap event should be estimated at gap midpoint"
1606        );
1607    }
1608
1609    #[test]
1610    fn test_last_seen_with_bucket_midway_ago() {
1611        use chrono::{TimeZone, Utc};
1612        let fixed_time = Utc.with_ymd_and_hms(2025, 12, 5, 12, 0, 0).unwrap();
1613        let clock = TestClock::build_for_testing_at(fixed_time);
1614        let store = EventStore::builder()
1615            .track_hours(24)
1616            .with_clock(Arc::new(clock.clone()))
1617            .build()
1618            .unwrap();
1619
1620        store.record("event");
1621        // Advance 30 minutes within the current hour
1622        clock.advance(Duration::minutes(30));
1623
1624        let last = store.query("event").last_seen();
1625
1626        // With bucket_midway_ago, should estimate ~15 minutes ago
1627        // (midpoint of elapsed 30 minutes in current hour bucket)
1628        assert!(last.is_some());
1629        let duration = last.unwrap();
1630
1631        // Should be around 15 minutes (half of 30 minutes elapsed)
1632        assert!(
1633            duration >= Duration::minutes(14) && duration <= Duration::minutes(16),
1634            "Expected ~15 minutes, got {:?}",
1635            duration
1636        );
1637    }
1638}