1use tauri::{AppHandle, Emitter};
2
3use crate::config::Profile;
4
5use super::super::settings::Settings;
6use super::super::timers::reset_timers_keep_sleep;
7use super::super::Scheduler;
8
9async fn emit_profile_changed(app: &AppHandle, scheduler: &Scheduler) -> tauri::Result<()> {
10 let name = scheduler.active_profile_name.lock().await.clone();
11 app.emit("profile:changed", &name)
12}
13
14#[tauri::command]
17pub async fn list_profiles(scheduler: tauri::State<'_, Scheduler>) -> Result<Vec<String>, String> {
18 Ok(scheduler
19 .profiles
20 .lock()
21 .await
22 .iter()
23 .map(|p| p.name.clone())
24 .collect())
25}
26
27#[tauri::command]
29pub async fn get_active_profile(scheduler: tauri::State<'_, Scheduler>) -> Result<String, String> {
30 Ok(scheduler.active_profile_name.lock().await.clone())
31}
32
33pub async fn set_active_profile_impl(
37 app: &AppHandle,
38 scheduler: &Scheduler,
39 name: String,
40) -> Result<(), String> {
41 let next_settings = {
42 let profiles = scheduler.profiles.lock().await;
43 profiles
44 .iter()
45 .find(|p| p.name == name)
46 .map(|p| p.settings.clone())
47 .ok_or_else(|| format!("profile not found: {name}"))?
48 };
49 {
50 let current = scheduler.active_profile_name.lock().await.clone();
51 if current == name {
52 return Ok(());
53 }
54 }
55 *scheduler.settings.lock().await = next_settings;
56 *scheduler.active_profile_name.lock().await = name.clone();
57 {
58 let mut t = scheduler.timers.lock().await;
59 reset_timers_keep_sleep(&mut t);
60 }
61 super::super::persist_profiles(scheduler).await;
62 let _ = app.emit("profile:changed", &name);
63 Ok(())
64}
65
66#[tauri::command]
69pub async fn set_active_profile(
70 app: AppHandle,
71 scheduler: tauri::State<'_, Scheduler>,
72 name: String,
73) -> Result<(), String> {
74 set_active_profile_impl(&app, scheduler.inner(), name).await
75}
76
77fn validate_profile_name(name: &str) -> Result<String, String> {
78 let trimmed = name.trim();
79 if trimmed.is_empty() {
80 return Err("profile name cannot be empty".to_string());
81 }
82 Ok(trimmed.to_string())
83}
84
85fn validate_delete(profiles: &[Profile], active: &str, target: &str) -> Result<(), String> {
86 if profiles.len() <= 1 {
87 return Err("cannot delete the only profile".to_string());
88 }
89 if active == target {
90 return Err("cannot delete the active profile".to_string());
91 }
92 if !profiles.iter().any(|p| p.name == target) {
93 return Err(format!("profile not found: {target}"));
94 }
95 Ok(())
96}
97
98fn validate_rename(profiles: &[Profile], from: &str, to: &str) -> Result<(), String> {
99 if from == to {
100 return Ok(());
101 }
102 if !profiles.iter().any(|p| p.name == from) {
103 return Err(format!("profile not found: {from}"));
104 }
105 if profiles.iter().any(|p| p.name == to) {
106 return Err(format!("profile already exists: {to}"));
107 }
108 Ok(())
109}
110
111fn validate_reorder(profiles: &[Profile], desired: &[String]) -> Result<(), String> {
112 if desired.len() != profiles.len() {
113 return Err(format!(
114 "reorder list length {} does not match profile count {}",
115 desired.len(),
116 profiles.len()
117 ));
118 }
119 for (i, name) in desired.iter().enumerate() {
120 if desired[..i].iter().any(|other| other == name) {
121 return Err(format!("duplicate profile in reorder list: {name}"));
122 }
123 if !profiles.iter().any(|p| &p.name == name) {
124 return Err(format!("profile not found: {name}"));
125 }
126 }
127 Ok(())
128}
129
130pub async fn create_profile_impl(scheduler: &Scheduler, name: String) -> Result<(), String> {
135 let name = validate_profile_name(&name)?;
136 {
137 let profiles = scheduler.profiles.lock().await;
138 if profiles.iter().any(|p| p.name == name) {
139 return Err(format!("profile already exists: {name}"));
140 }
141 }
142 let source = {
143 let active = scheduler.active_profile_name.lock().await.clone();
144 let profiles = scheduler.profiles.lock().await;
145 profiles
146 .iter()
147 .find(|p| p.name == active)
148 .map(|p| p.settings.clone())
149 .unwrap_or_default()
150 };
151 scheduler.profiles.lock().await.push(Profile {
152 name: name.clone(),
153 settings: source,
154 });
155 super::super::persist_profiles(scheduler).await;
156 Ok(())
157}
158
159#[tauri::command]
162pub async fn create_profile(
163 app: AppHandle,
164 scheduler: tauri::State<'_, Scheduler>,
165 name: String,
166) -> Result<(), String> {
167 create_profile_impl(scheduler.inner(), name).await?;
168 let _ = emit_profile_changed(&app, scheduler.inner()).await;
169 Ok(())
170}
171
172pub async fn duplicate_profile_impl(
174 scheduler: &Scheduler,
175 source: String,
176 name: String,
177) -> Result<(), String> {
178 let name = validate_profile_name(&name)?;
179 let source_settings = {
180 let profiles = scheduler.profiles.lock().await;
181 if profiles.iter().any(|p| p.name == name) {
182 return Err(format!("profile already exists: {name}"));
183 }
184 profiles
185 .iter()
186 .find(|p| p.name == source)
187 .map(|p| p.settings.clone())
188 .ok_or_else(|| format!("profile not found: {source}"))?
189 };
190 scheduler.profiles.lock().await.push(Profile {
191 name: name.clone(),
192 settings: source_settings,
193 });
194 super::super::persist_profiles(scheduler).await;
195 Ok(())
196}
197
198#[tauri::command]
203pub async fn duplicate_profile(
204 app: AppHandle,
205 scheduler: tauri::State<'_, Scheduler>,
206 source: String,
207 name: String,
208) -> Result<(), String> {
209 duplicate_profile_impl(scheduler.inner(), source, name).await?;
210 let _ = emit_profile_changed(&app, scheduler.inner()).await;
211 Ok(())
212}
213
214pub async fn rename_profile_impl(
217 scheduler: &Scheduler,
218 from: String,
219 to: String,
220) -> Result<(), String> {
221 let to = validate_profile_name(&to)?;
222 {
223 let profiles = scheduler.profiles.lock().await;
224 validate_rename(&profiles, &from, &to)?;
225 }
226 if from == to {
227 return Ok(());
228 }
229 {
230 let mut profiles = scheduler.profiles.lock().await;
231 if let Some(p) = profiles.iter_mut().find(|p| p.name == from) {
232 p.name = to.clone();
233 }
234 }
235 {
236 let mut active = scheduler.active_profile_name.lock().await;
237 if *active == from {
238 *active = to.clone();
239 }
240 }
241 super::super::persist_profiles(scheduler).await;
242 Ok(())
243}
244
245#[tauri::command]
249pub async fn rename_profile(
250 app: AppHandle,
251 scheduler: tauri::State<'_, Scheduler>,
252 from: String,
253 to: String,
254) -> Result<(), String> {
255 rename_profile_impl(scheduler.inner(), from, to).await?;
256 let _ = emit_profile_changed(&app, scheduler.inner()).await;
257 Ok(())
258}
259
260pub async fn delete_profile_impl(scheduler: &Scheduler, name: String) -> Result<(), String> {
262 {
263 let profiles = scheduler.profiles.lock().await;
264 let active = scheduler.active_profile_name.lock().await.clone();
265 validate_delete(&profiles, &active, &name)?;
266 }
267 {
268 let mut profiles = scheduler.profiles.lock().await;
269 profiles.retain(|p| p.name != name);
270 }
271 super::super::persist_profiles(scheduler).await;
272 Ok(())
273}
274
275#[tauri::command]
278pub async fn delete_profile(
279 app: AppHandle,
280 scheduler: tauri::State<'_, Scheduler>,
281 name: String,
282) -> Result<(), String> {
283 delete_profile_impl(scheduler.inner(), name).await?;
284 let _ = emit_profile_changed(&app, scheduler.inner()).await;
285 Ok(())
286}
287
288pub async fn reorder_profiles_impl(
290 scheduler: &Scheduler,
291 names: Vec<String>,
292) -> Result<(), String> {
293 {
294 let profiles = scheduler.profiles.lock().await;
295 validate_reorder(&profiles, &names)?;
296 }
297 {
298 let mut profiles = scheduler.profiles.lock().await;
299 let mut next: Vec<Profile> = Vec::with_capacity(profiles.len());
300 for name in &names {
301 if let Some(pos) = profiles.iter().position(|p| &p.name == name) {
302 next.push(profiles.swap_remove(pos));
303 }
304 }
305 *profiles = next;
306 }
307 super::super::persist_profiles(scheduler).await;
308 Ok(())
309}
310
311#[tauri::command]
315pub async fn reorder_profiles(
316 app: AppHandle,
317 scheduler: tauri::State<'_, Scheduler>,
318 names: Vec<String>,
319) -> Result<(), String> {
320 reorder_profiles_impl(scheduler.inner(), names).await?;
321 let _ = emit_profile_changed(&app, scheduler.inner()).await;
322 Ok(())
323}
324
325pub async fn reset_profile_to_defaults_impl(
329 scheduler: &Scheduler,
330 name: String,
331) -> Result<(), String> {
332 let active = scheduler.active_profile_name.lock().await.clone();
333 {
334 let profiles = scheduler.profiles.lock().await;
335 if !profiles.iter().any(|p| p.name == name) {
336 return Err(format!("profile not found: {name}"));
337 }
338 }
339 let defaults = Settings::default();
340 {
341 let mut profiles = scheduler.profiles.lock().await;
342 if let Some(p) = profiles.iter_mut().find(|p| p.name == name) {
343 p.settings = defaults.clone();
344 }
345 }
346 if active == name {
347 *scheduler.settings.lock().await = defaults;
348 }
349 super::super::persist_profiles(scheduler).await;
350 Ok(())
351}
352
353#[tauri::command]
357pub async fn reset_profile_to_defaults(
358 app: AppHandle,
359 scheduler: tauri::State<'_, Scheduler>,
360 name: String,
361) -> Result<(), String> {
362 reset_profile_to_defaults_impl(scheduler.inner(), name).await?;
363 let _ = emit_profile_changed(&app, scheduler.inner()).await;
364 Ok(())
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn named_profile(name: &str) -> Profile {
372 Profile {
373 name: name.to_string(),
374 settings: Settings::default(),
375 }
376 }
377
378 #[test]
379 fn validate_delete_rejects_only_profile() {
380 let profiles = vec![named_profile("Default")];
381 let err = validate_delete(&profiles, "Default", "Default").unwrap_err();
382 assert!(err.contains("only profile"));
383 }
384
385 #[test]
386 fn validate_delete_rejects_active() {
387 let profiles = vec![named_profile("Default"), named_profile("Work")];
388 let err = validate_delete(&profiles, "Work", "Work").unwrap_err();
389 assert!(err.contains("active"));
390 }
391
392 #[test]
393 fn validate_delete_rejects_missing() {
394 let profiles = vec![named_profile("Default"), named_profile("Work")];
395 let err = validate_delete(&profiles, "Default", "Missing").unwrap_err();
396 assert!(err.contains("not found"));
397 }
398
399 #[test]
400 fn validate_delete_accepts_inactive() {
401 let profiles = vec![named_profile("Default"), named_profile("Work")];
402 assert!(validate_delete(&profiles, "Default", "Work").is_ok());
403 }
404
405 #[test]
406 fn validate_rename_rejects_collision() {
407 let profiles = vec![named_profile("Default"), named_profile("Work")];
408 let err = validate_rename(&profiles, "Default", "Work").unwrap_err();
409 assert!(err.contains("already exists"));
410 }
411
412 #[test]
413 fn validate_rename_rejects_missing_source() {
414 let profiles = vec![named_profile("Default")];
415 let err = validate_rename(&profiles, "Missing", "Other").unwrap_err();
416 assert!(err.contains("not found"));
417 }
418
419 #[test]
420 fn validate_rename_allows_same_name_noop() {
421 let profiles = vec![named_profile("Default")];
422 assert!(validate_rename(&profiles, "Default", "Default").is_ok());
423 }
424
425 #[test]
426 fn validate_rename_accepts_unique_target() {
427 let profiles = vec![named_profile("Default")];
428 assert!(validate_rename(&profiles, "Default", "Work").is_ok());
429 }
430
431 #[test]
432 fn validate_profile_name_rejects_empty() {
433 assert!(validate_profile_name("").is_err());
434 assert!(validate_profile_name(" ").is_err());
435 }
436
437 #[test]
438 fn validate_reorder_rejects_length_mismatch() {
439 let profiles = vec![named_profile("a"), named_profile("b")];
440 let err = validate_reorder(&profiles, &["a".to_string()]).unwrap_err();
441 assert!(err.contains("does not match"));
442 }
443
444 #[test]
445 fn validate_reorder_rejects_duplicate() {
446 let profiles = vec![named_profile("a"), named_profile("b")];
447 let err = validate_reorder(&profiles, &["a".to_string(), "a".to_string()]).unwrap_err();
448 assert!(err.contains("duplicate"));
449 }
450
451 #[test]
452 fn validate_reorder_rejects_unknown() {
453 let profiles = vec![named_profile("a"), named_profile("b")];
454 let err = validate_reorder(&profiles, &["a".to_string(), "c".to_string()]).unwrap_err();
455 assert!(err.contains("not found"));
456 }
457
458 #[test]
459 fn validate_reorder_accepts_permutation() {
460 let profiles = vec![named_profile("a"), named_profile("b"), named_profile("c")];
461 let desired = vec!["c".to_string(), "a".to_string(), "b".to_string()];
462 assert!(validate_reorder(&profiles, &desired).is_ok());
463 }
464
465 #[test]
466 fn validate_profile_name_trims_whitespace() {
467 assert_eq!(validate_profile_name(" Work ").unwrap(), "Work");
468 }
469
470 #[test]
471 fn postpone_exhaustion_threshold_matches_max() {
472 let s = Settings::default();
473 assert_eq!(s.postpone_max_count, 3);
474 }
475
476 use crate::config::DEFAULT_PROFILE_NAME;
479 use crate::scheduler::break_stats::BreakStats;
480 use crate::scheduler::pause::PauseState;
481 use crate::scheduler::screen_time::ScreenTimeState;
482 use crate::scheduler::timers::BreakTimers;
483 use crate::screen_time_store::ScreenTimeSnapshot;
484 use crate::stats::Logger;
485 use crate::test_support::{temp_dir, TempDir};
486 use std::sync::atomic::{AtomicBool, AtomicU8};
487 use std::sync::Arc;
488 use tokio::sync::Mutex;
489
490 fn build_test_scheduler(profiles: Vec<Profile>, active: &str) -> (TempDir, Scheduler) {
491 let dir = temp_dir();
492 let config_path = dir.path().join("settings.json");
493 let pause_path = dir.path().join("pause.json");
494 let events_path = dir.path().join("events.jsonl");
495 let screen_time_path = dir.path().join("screen_time.json");
496 let logger = Logger::spawn(events_path.clone());
497 let active_settings = profiles
498 .iter()
499 .find(|p| p.name == active)
500 .map(|p| p.settings.clone())
501 .unwrap_or_default();
502 let sched = Scheduler {
503 settings: Arc::new(Mutex::new(active_settings)),
504 pause_state: Arc::new(Mutex::new(PauseState::Running)),
505 camera_active: Arc::new(AtomicBool::new(false)),
506 video_active: Arc::new(AtomicBool::new(false)),
507 auto_suppress_reason: Arc::new(AtomicU8::new(0)),
508 config_path,
509 pause_path,
510 events_path,
511 screen_time_path,
512 timers: Arc::new(Mutex::new(BreakTimers::new())),
513 stats: Arc::new(Mutex::new(BreakStats::default())),
514 screen_time: Arc::new(Mutex::new(ScreenTimeState::from_snapshot(
515 ScreenTimeSnapshot::default(),
516 "1970-01-01",
517 ))),
518 current_break: Arc::new(std::sync::Mutex::new(None)),
519 logger,
520 profiles: Arc::new(Mutex::new(profiles)),
521 active_profile_name: Arc::new(Mutex::new(active.to_string())),
522 hook_dialog_busy: Arc::new(AtomicBool::new(false)),
523 };
524 (dir, sched)
525 }
526
527 fn one_profile() -> Vec<Profile> {
528 vec![Profile {
529 name: DEFAULT_PROFILE_NAME.to_string(),
530 settings: Settings {
531 micro_interval_secs: 1234,
532 ..Settings::default()
533 },
534 }]
535 }
536
537 fn two_profiles() -> Vec<Profile> {
538 vec![
539 Profile {
540 name: DEFAULT_PROFILE_NAME.to_string(),
541 settings: Settings {
542 micro_interval_secs: 1234,
543 ..Settings::default()
544 },
545 },
546 Profile {
547 name: "Work".to_string(),
548 settings: Settings {
549 micro_interval_secs: 600,
550 ..Settings::default()
551 },
552 },
553 ]
554 }
555
556 #[tokio::test]
557 async fn create_profile_appends_copy_of_active_settings() {
558 let (_dir, sched) = build_test_scheduler(one_profile(), DEFAULT_PROFILE_NAME);
559 create_profile_impl(&sched, "Focus".to_string())
560 .await
561 .unwrap();
562 let profiles = sched.profiles.lock().await;
563 assert_eq!(profiles.len(), 2);
564 assert_eq!(profiles[1].name, "Focus");
565 assert_eq!(profiles[1].settings.micro_interval_secs, 1234);
567 }
568
569 #[tokio::test]
570 async fn create_profile_rejects_empty_name() {
571 let (_dir, sched) = build_test_scheduler(one_profile(), DEFAULT_PROFILE_NAME);
572 let err = create_profile_impl(&sched, " ".to_string())
573 .await
574 .unwrap_err();
575 assert!(err.contains("cannot be empty"));
576 assert_eq!(sched.profiles.lock().await.len(), 1);
577 }
578
579 #[tokio::test]
580 async fn create_profile_rejects_duplicate_name() {
581 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
582 let err = create_profile_impl(&sched, "Work".to_string())
583 .await
584 .unwrap_err();
585 assert!(err.contains("already exists"));
586 }
587
588 #[tokio::test]
589 async fn duplicate_profile_clones_named_source_without_switching_active() {
590 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
591 duplicate_profile_impl(&sched, "Work".to_string(), "Focus".to_string())
592 .await
593 .unwrap();
594 let profiles = sched.profiles.lock().await;
595 assert_eq!(profiles.len(), 3);
596 let focus = profiles.iter().find(|p| p.name == "Focus").unwrap();
597 assert_eq!(focus.settings.micro_interval_secs, 600);
599 let active = sched.active_profile_name.lock().await;
601 assert_eq!(*active, DEFAULT_PROFILE_NAME);
602 }
603
604 #[tokio::test]
605 async fn duplicate_profile_errors_when_source_missing() {
606 let (_dir, sched) = build_test_scheduler(one_profile(), DEFAULT_PROFILE_NAME);
607 let err = duplicate_profile_impl(&sched, "Missing".to_string(), "Focus".to_string())
608 .await
609 .unwrap_err();
610 assert!(err.contains("not found"));
611 assert_eq!(sched.profiles.lock().await.len(), 1);
612 }
613
614 #[tokio::test]
615 async fn rename_profile_renames_in_place_and_follows_active_pointer() {
616 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
617 rename_profile_impl(
618 &sched,
619 DEFAULT_PROFILE_NAME.to_string(),
620 "Personal".to_string(),
621 )
622 .await
623 .unwrap();
624 let profiles = sched.profiles.lock().await;
625 assert_eq!(profiles[0].name, "Personal");
626 assert_eq!(profiles[1].name, "Work");
627 let active = sched.active_profile_name.lock().await;
628 assert_eq!(*active, "Personal", "active pointer follows the rename");
629 }
630
631 #[tokio::test]
632 async fn rename_profile_leaves_active_alone_when_renaming_inactive() {
633 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
634 rename_profile_impl(&sched, "Work".to_string(), "Office".to_string())
635 .await
636 .unwrap();
637 assert_eq!(
638 *sched.active_profile_name.lock().await,
639 DEFAULT_PROFILE_NAME,
640 );
641 }
642
643 #[tokio::test]
644 async fn rename_profile_noop_on_same_name() {
645 let (_dir, sched) = build_test_scheduler(one_profile(), DEFAULT_PROFILE_NAME);
646 rename_profile_impl(
647 &sched,
648 DEFAULT_PROFILE_NAME.to_string(),
649 DEFAULT_PROFILE_NAME.to_string(),
650 )
651 .await
652 .unwrap();
653 assert_eq!(sched.profiles.lock().await.len(), 1);
654 }
655
656 #[tokio::test]
657 async fn delete_profile_removes_inactive_entry() {
658 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
659 delete_profile_impl(&sched, "Work".to_string())
660 .await
661 .unwrap();
662 let profiles = sched.profiles.lock().await;
663 assert_eq!(profiles.len(), 1);
664 assert_eq!(profiles[0].name, DEFAULT_PROFILE_NAME);
665 }
666
667 #[tokio::test]
668 async fn delete_profile_rejects_active() {
669 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
670 let err = delete_profile_impl(&sched, DEFAULT_PROFILE_NAME.to_string())
671 .await
672 .unwrap_err();
673 assert!(err.contains("active"));
674 assert_eq!(sched.profiles.lock().await.len(), 2);
675 }
676
677 #[tokio::test]
678 async fn delete_profile_rejects_only_profile() {
679 let (_dir, sched) = build_test_scheduler(one_profile(), DEFAULT_PROFILE_NAME);
680 let err = delete_profile_impl(&sched, DEFAULT_PROFILE_NAME.to_string())
681 .await
682 .unwrap_err();
683 assert!(err.contains("only profile"));
684 }
685
686 #[tokio::test]
687 async fn reorder_profiles_reorders_in_place() {
688 let three = vec![
689 Profile {
690 name: "a".into(),
691 settings: Settings::default(),
692 },
693 Profile {
694 name: "b".into(),
695 settings: Settings::default(),
696 },
697 Profile {
698 name: "c".into(),
699 settings: Settings::default(),
700 },
701 ];
702 let (_dir, sched) = build_test_scheduler(three, "a");
703 reorder_profiles_impl(&sched, vec!["c".into(), "a".into(), "b".into()])
704 .await
705 .unwrap();
706 let names: Vec<String> = sched
707 .profiles
708 .lock()
709 .await
710 .iter()
711 .map(|p| p.name.clone())
712 .collect();
713 assert_eq!(names, vec!["c", "a", "b"]);
714 }
715
716 #[tokio::test]
717 async fn reorder_profiles_rejects_unknown_name() {
718 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
719 let err = reorder_profiles_impl(&sched, vec!["Work".into(), "Missing".into()])
720 .await
721 .unwrap_err();
722 assert!(err.contains("not found"));
723 }
724
725 #[tokio::test]
726 async fn reset_profile_to_defaults_resets_inactive_only_on_disk() {
727 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
728 reset_profile_to_defaults_impl(&sched, "Work".to_string())
729 .await
730 .unwrap();
731 let profiles = sched.profiles.lock().await;
732 let work = profiles.iter().find(|p| p.name == "Work").unwrap();
733 assert_eq!(
734 work.settings.micro_interval_secs,
735 Settings::default().micro_interval_secs,
736 );
737 assert_eq!(
740 sched.settings.lock().await.micro_interval_secs,
741 1234,
742 "active profile's live settings stay put when the inactive one resets",
743 );
744 }
745
746 #[tokio::test]
747 async fn reset_profile_to_defaults_also_resets_live_settings_when_active() {
748 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
749 reset_profile_to_defaults_impl(&sched, DEFAULT_PROFILE_NAME.to_string())
750 .await
751 .unwrap();
752 assert_eq!(
753 sched.settings.lock().await.micro_interval_secs,
754 Settings::default().micro_interval_secs,
755 );
756 }
757
758 #[tokio::test]
759 async fn reset_profile_to_defaults_errors_when_missing() {
760 let (_dir, sched) = build_test_scheduler(one_profile(), DEFAULT_PROFILE_NAME);
761 let err = reset_profile_to_defaults_impl(&sched, "Missing".to_string())
762 .await
763 .unwrap_err();
764 assert!(err.contains("not found"));
765 }
766
767 #[tokio::test]
768 async fn set_active_profile_switches_settings_and_resets_timers() {
769 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
770 {
772 let mut t = sched.timers.lock().await;
773 t.micro_warned = true;
774 t.long_warned = true;
775 t.last_sleep = Some(std::time::Instant::now());
776 }
777 let next = sched
787 .profiles
788 .lock()
789 .await
790 .iter()
791 .find(|p| p.name == "Work")
792 .map(|p| p.settings.clone())
793 .unwrap();
794 *sched.settings.lock().await = next.clone();
795 *sched.active_profile_name.lock().await = "Work".to_string();
796 crate::scheduler::timers::reset_timers_keep_sleep(&mut *sched.timers.lock().await);
797 assert_eq!(sched.settings.lock().await.micro_interval_secs, 600);
800 assert_eq!(*sched.active_profile_name.lock().await, "Work");
801 let t = sched.timers.lock().await;
802 assert!(!t.micro_warned);
803 assert!(!t.long_warned);
804 assert!(
805 t.last_sleep.is_some(),
806 "sleep marker preserved across switch"
807 );
808 }
809
810 #[tokio::test]
811 async fn list_profiles_returns_names_in_storage_order() {
812 let (_dir, sched) = build_test_scheduler(two_profiles(), DEFAULT_PROFILE_NAME);
813 let names: Vec<String> = sched
814 .profiles
815 .lock()
816 .await
817 .iter()
818 .map(|p| p.name.clone())
819 .collect();
820 assert_eq!(names, vec![DEFAULT_PROFILE_NAME.to_string(), "Work".into()]);
821 }
822}