1use std::path::Path;
2
3use log::error;
4use serde::Serialize;
5
6use crate::screen_time_store::{self, ScreenTimeSnapshot};
7
8#[derive(Debug, Clone, Default, Serialize)]
12pub struct ScreenTimeState {
13 pub date: String,
14 pub seconds: u64,
15 pub last_reminder_epoch_secs: Option<u64>,
16}
17
18impl ScreenTimeState {
19 pub fn from_snapshot(snap: ScreenTimeSnapshot, today: &str) -> Self {
23 if snap.date == today {
24 Self {
25 date: snap.date,
26 seconds: snap.seconds,
27 last_reminder_epoch_secs: snap.last_reminder_epoch_secs,
28 }
29 } else {
30 Self {
31 date: today.to_string(),
32 seconds: 0,
33 last_reminder_epoch_secs: None,
34 }
35 }
36 }
37
38 pub fn to_snapshot(&self) -> ScreenTimeSnapshot {
41 ScreenTimeSnapshot {
42 date: self.date.clone(),
43 seconds: self.seconds,
44 last_reminder_epoch_secs: self.last_reminder_epoch_secs,
45 }
46 }
47}
48
49pub fn rollover_if_new_day(state: &mut ScreenTimeState, today: &str) -> bool {
53 if state.date != today {
54 state.date = today.to_string();
55 state.seconds = 0;
56 state.last_reminder_epoch_secs = None;
57 true
58 } else {
59 false
60 }
61}
62
63pub fn should_remind_screen_time(
69 enabled: bool,
70 counter_secs: u64,
71 budget_secs: u64,
72 last_reminder_epoch_secs: Option<u64>,
73 remind_again_secs: u64,
74 now_epoch_secs: u64,
75) -> bool {
76 if !enabled || budget_secs == 0 {
77 return false;
78 }
79 if counter_secs < budget_secs {
80 return false;
81 }
82 match last_reminder_epoch_secs {
83 None => true,
84 Some(_) if remind_again_secs == 0 => false,
85 Some(prev) => now_epoch_secs.saturating_sub(prev) >= remind_again_secs,
86 }
87}
88
89pub fn persist_screen_time(path: &Path, state: &ScreenTimeState) {
92 let snap = state.to_snapshot();
93 if let Err(e) = screen_time_store::save(path, &snap) {
94 error!("screen_time_store: failed to save {}: {e}", path.display());
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn should_remind_screen_time_disabled_never_fires() {
104 assert!(!should_remind_screen_time(
105 false, 28_800, 28_800, None, 3600, 1_000_000
106 ));
107 }
108
109 #[test]
110 fn should_remind_screen_time_under_budget_does_not_fire() {
111 assert!(!should_remind_screen_time(
112 true, 28_739, 28_800, None, 3600, 1_000_000
113 ));
114 }
115
116 #[test]
117 fn should_remind_screen_time_just_crossed_fires_first_time() {
118 assert!(should_remind_screen_time(
119 true, 28_800, 28_800, None, 3600, 1_000_000
120 ));
121 }
122
123 #[test]
124 fn should_remind_screen_time_increment_by_one_minute_crosses_budget() {
125 let budget_secs: u64 = 480 * 60;
126 let counter_before: u64 = budget_secs - 60;
127 assert!(!should_remind_screen_time(
128 true,
129 counter_before,
130 budget_secs,
131 None,
132 3600,
133 1_000_000
134 ));
135 let counter_after = counter_before + 60;
136 assert!(should_remind_screen_time(
137 true,
138 counter_after,
139 budget_secs,
140 None,
141 3600,
142 1_000_000
143 ));
144 }
145
146 #[test]
147 fn should_remind_screen_time_snooze_blocks_repeat() {
148 let budget_secs: u64 = 480 * 60;
149 let now: u64 = 1_000_000;
150 let ten_min_ago = now - 600;
151 assert!(!should_remind_screen_time(
152 true,
153 budget_secs + 120,
154 budget_secs,
155 Some(ten_min_ago),
156 3600,
157 now
158 ));
159 }
160
161 #[test]
162 fn should_remind_screen_time_after_snooze_fires_again() {
163 let budget_secs: u64 = 480 * 60;
164 let now: u64 = 1_000_000;
165 let an_hour_ago = now - 3600;
166 assert!(should_remind_screen_time(
167 true,
168 budget_secs + 120,
169 budget_secs,
170 Some(an_hour_ago),
171 3600,
172 now
173 ));
174 }
175
176 #[test]
177 fn should_remind_screen_time_zero_remind_again_fires_only_once() {
178 let budget_secs: u64 = 480 * 60;
179 let now: u64 = 1_000_000;
180 assert!(should_remind_screen_time(
181 true,
182 budget_secs,
183 budget_secs,
184 None,
185 0,
186 now
187 ));
188 assert!(!should_remind_screen_time(
189 true,
190 budget_secs + 7200,
191 budget_secs,
192 Some(now - 7200),
193 0,
194 now
195 ));
196 }
197
198 #[test]
199 fn should_remind_screen_time_zero_budget_disabled_path() {
200 assert!(!should_remind_screen_time(
201 true, 99_999, 0, None, 3600, 1_000_000
202 ));
203 }
204
205 #[test]
206 fn rollover_resets_counter_at_midnight() {
207 let mut st = ScreenTimeState {
208 date: "2026-05-15".into(),
209 seconds: 28_800,
210 last_reminder_epoch_secs: Some(1_000_000),
211 };
212 let rolled = rollover_if_new_day(&mut st, "2026-05-16");
213 assert!(rolled);
214 assert_eq!(st.date, "2026-05-16");
215 assert_eq!(st.seconds, 0);
216 assert!(st.last_reminder_epoch_secs.is_none());
217 }
218
219 #[test]
220 fn rollover_noop_when_same_day() {
221 let mut st = ScreenTimeState {
222 date: "2026-05-15".into(),
223 seconds: 1234,
224 last_reminder_epoch_secs: Some(99),
225 };
226 let rolled = rollover_if_new_day(&mut st, "2026-05-15");
227 assert!(!rolled);
228 assert_eq!(st.seconds, 1234);
229 assert_eq!(st.last_reminder_epoch_secs, Some(99));
230 }
231
232 #[test]
233 fn screen_time_state_from_snapshot_keeps_today_data() {
234 let snap = ScreenTimeSnapshot {
235 date: "2026-05-15".into(),
236 seconds: 500,
237 last_reminder_epoch_secs: Some(42),
238 };
239 let st = ScreenTimeState::from_snapshot(snap, "2026-05-15");
240 assert_eq!(st.seconds, 500);
241 assert_eq!(st.last_reminder_epoch_secs, Some(42));
242 }
243
244 #[test]
245 fn screen_time_state_from_snapshot_resets_on_stale_date() {
246 let snap = ScreenTimeSnapshot {
247 date: "2026-05-14".into(),
248 seconds: 28_800,
249 last_reminder_epoch_secs: Some(42),
250 };
251 let st = ScreenTimeState::from_snapshot(snap, "2026-05-15");
252 assert_eq!(st.date, "2026-05-15");
253 assert_eq!(st.seconds, 0);
254 assert!(st.last_reminder_epoch_secs.is_none());
255 }
256}