work_tuimer/storage/
mod.rs

1use crate::models::{DayData, WorkRecord};
2use crate::timer::TimerState;
3use anyhow::{Context, Result};
4use std::fs;
5use std::path::PathBuf;
6use std::time::SystemTime;
7use time::Date;
8
9#[derive(Clone)]
10pub struct Storage {
11    data_dir: PathBuf,
12}
13
14/// High-level storage manager that provides transactional operations
15/// and automatic file modification tracking
16pub struct StorageManager {
17    storage: Storage,
18    file_modified_times: std::collections::HashMap<Date, Option<SystemTime>>,
19}
20
21impl StorageManager {
22    /// Create a new StorageManager
23    pub fn new() -> Result<Self> {
24        Ok(StorageManager {
25            storage: Storage::new()?,
26            file_modified_times: std::collections::HashMap::new(),
27        })
28    }
29
30    /// Create a new StorageManager with a custom directory (for testing)
31    #[doc(hidden)]
32    #[allow(dead_code)]
33    pub fn new_with_dir(data_dir: PathBuf) -> Result<Self> {
34        Ok(StorageManager {
35            storage: Storage::new_with_dir(data_dir)?,
36            file_modified_times: std::collections::HashMap::new(),
37        })
38    }
39
40    /// Load day data with automatic file modification tracking
41    /// Returns the loaded data and updates internal tracking
42    pub fn load_with_tracking(&mut self, date: Date) -> Result<DayData> {
43        let data = self.storage.load(&date)?;
44        let modified_time = self.storage.get_file_modified_time(&date);
45        self.file_modified_times.insert(date, modified_time);
46        Ok(data)
47    }
48
49    /// Check if file has been modified externally and reload if needed
50    /// Returns Some(DayData) if file was modified and reloaded, None if no change
51    pub fn check_and_reload(&mut self, date: Date) -> Result<Option<DayData>> {
52        let current_modified = self.storage.get_file_modified_time(&date);
53
54        // Check if we've tracked this date before
55        let is_tracked = self.file_modified_times.contains_key(&date);
56
57        if !is_tracked {
58            // First time checking this date - load it and start tracking
59            let data = self.storage.load(&date)?;
60            self.file_modified_times.insert(date, current_modified);
61            Ok(Some(data))
62        } else {
63            let last_known = self.file_modified_times.get(&date).copied().flatten();
64
65            // If modification times differ, reload the file
66            if current_modified != last_known {
67                let data = self.storage.load(&date)?;
68                self.file_modified_times.insert(date, current_modified);
69                Ok(Some(data))
70            } else {
71                Ok(None)
72            }
73        }
74    }
75
76    /// Add a new work record (transactional: load → add → save → track)
77    #[allow(dead_code)]
78    pub fn add_record(&mut self, date: Date, record: WorkRecord) -> Result<()> {
79        let mut day_data = self.storage.load(&date)?;
80        day_data.add_record(record);
81        self.storage.save(&day_data)?;
82
83        // Update tracking after successful save
84        let modified_time = self.storage.get_file_modified_time(&date);
85        self.file_modified_times.insert(date, modified_time);
86
87        Ok(())
88    }
89
90    /// Update an existing work record (transactional: load → update → save → track)
91    #[allow(dead_code)]
92    pub fn update_record(&mut self, date: Date, record: WorkRecord) -> Result<()> {
93        let mut day_data = self.storage.load(&date)?;
94
95        // Update the record (will replace if ID exists)
96        day_data.add_record(record);
97
98        self.storage.save(&day_data)?;
99
100        // Update tracking after successful save
101        let modified_time = self.storage.get_file_modified_time(&date);
102        self.file_modified_times.insert(date, modified_time);
103
104        Ok(())
105    }
106
107    /// Remove a work record by ID (transactional: load → remove → save → track)
108    /// Returns the removed record if found
109    #[allow(dead_code)]
110    pub fn remove_record(&mut self, date: Date, id: u32) -> Result<WorkRecord> {
111        let mut day_data = self.storage.load(&date)?;
112
113        let record = day_data
114            .work_records
115            .remove(&id)
116            .context(format!("Record with ID {} not found", id))?;
117
118        self.storage.save(&day_data)?;
119
120        // Update tracking after successful save
121        let modified_time = self.storage.get_file_modified_time(&date);
122        self.file_modified_times.insert(date, modified_time);
123
124        Ok(record)
125    }
126
127    /// Save day data and update tracking
128    pub fn save(&mut self, day_data: &DayData) -> Result<()> {
129        self.storage.save(day_data)?;
130
131        // Update tracking after successful save
132        let modified_time = self.storage.get_file_modified_time(&day_data.date);
133        self.file_modified_times
134            .insert(day_data.date, modified_time);
135
136        Ok(())
137    }
138
139    /// Get the last known modification time for a date
140    pub fn get_last_modified(&self, date: &Date) -> Option<SystemTime> {
141        self.file_modified_times.get(date).copied().flatten()
142    }
143
144    /// Pass-through methods for timer operations (these don't need tracking)
145    #[allow(dead_code)]
146    pub fn save_active_timer(&self, timer: &TimerState) -> Result<()> {
147        self.storage.save_active_timer(timer)
148    }
149
150    pub fn load_active_timer(&self) -> Result<Option<TimerState>> {
151        self.storage.load_active_timer()
152    }
153
154    #[allow(dead_code)]
155    pub fn clear_active_timer(&self) -> Result<()> {
156        self.storage.clear_active_timer()
157    }
158
159    /// Create a TimerManager using the internal storage
160    /// This allows timer operations while keeping storage abstraction
161    fn create_timer_manager(&self) -> crate::timer::TimerManager {
162        // Clone the storage for timer operations
163        // This is safe because timer operations are independent
164        crate::timer::TimerManager::new(self.storage.clone())
165    }
166
167    /// Start a new timer with the given task name and optional description
168    pub fn start_timer(
169        &self,
170        task_name: String,
171        description: Option<String>,
172        source_record_id: Option<u32>,
173        source_record_date: Option<time::Date>,
174    ) -> Result<TimerState> {
175        let timer_manager = self.create_timer_manager();
176        timer_manager.start(task_name, description, source_record_id, source_record_date)
177    }
178
179    /// Stop the active timer and return the work record
180    pub fn stop_timer(&self) -> Result<crate::models::WorkRecord> {
181        let timer_manager = self.create_timer_manager();
182        timer_manager.stop()
183    }
184
185    /// Pause the active timer
186    pub fn pause_timer(&self) -> Result<TimerState> {
187        let timer_manager = self.create_timer_manager();
188        timer_manager.pause()
189    }
190
191    /// Resume a paused timer
192    pub fn resume_timer(&self) -> Result<TimerState> {
193        let timer_manager = self.create_timer_manager();
194        timer_manager.resume()
195    }
196
197    /// Get elapsed duration for a timer
198    #[allow(dead_code)]
199    pub fn get_timer_elapsed(&self, timer: &TimerState) -> std::time::Duration {
200        let timer_manager = self.create_timer_manager();
201        timer_manager.get_elapsed_duration(timer)
202    }
203}
204
205impl Storage {
206    pub fn new() -> Result<Self> {
207        let data_dir = Self::get_data_directory()?;
208        fs::create_dir_all(&data_dir).context("Failed to create data directory")?;
209
210        Ok(Storage { data_dir })
211    }
212
213    /// Create a new Storage with a custom directory (for testing)
214    #[doc(hidden)]
215    #[allow(dead_code)]
216    pub fn new_with_dir(data_dir: PathBuf) -> Result<Self> {
217        fs::create_dir_all(&data_dir).context("Failed to create data directory")?;
218        Ok(Storage { data_dir })
219    }
220
221    fn get_data_directory() -> Result<PathBuf> {
222        // Primary: Use system data directory (~/.local/share on Linux, ~/Library/Application Support on macOS)
223        if let Some(data_dir) = dirs::data_local_dir() {
224            let app_dir = data_dir.join("work-tuimer");
225            if fs::create_dir_all(&app_dir).is_ok() {
226                return Ok(app_dir);
227            }
228        }
229
230        // Fallback: Use ./data for development/testing if system location fails
231        let local_data = PathBuf::from("./data");
232        if fs::create_dir_all(&local_data).is_ok() {
233            return Ok(local_data);
234        }
235
236        anyhow::bail!("Failed to create data directory in system location or ./data")
237    }
238
239    fn get_file_path(&self, date: &Date) -> PathBuf {
240        self.data_dir.join(format!(
241            "{}-{:02}-{:02}.json",
242            date.year(),
243            date.month() as u8,
244            date.day()
245        ))
246    }
247
248    pub fn load(&self, date: &Date) -> Result<DayData> {
249        let path = self.get_file_path(date);
250
251        if !path.exists() {
252            return Ok(DayData::new(*date));
253        }
254
255        let contents =
256            fs::read_to_string(&path).context(format!("Failed to read file: {:?}", path))?;
257
258        let day_data: DayData = serde_json::from_str(&contents).context("Failed to parse JSON")?;
259
260        Ok(day_data)
261    }
262
263    pub fn save(&self, day_data: &DayData) -> Result<()> {
264        let path = self.get_file_path(&day_data.date);
265
266        let json = serde_json::to_string_pretty(day_data).context("Failed to serialize data")?;
267
268        fs::write(&path, json).context(format!("Failed to write file: {:?}", path))?;
269
270        Ok(())
271    }
272
273    /// Get the modification time of a day data file
274    /// Returns None if the file doesn't exist
275    pub fn get_file_modified_time(&self, date: &Date) -> Option<std::time::SystemTime> {
276        let path = self.get_file_path(date);
277        if path.exists() {
278            fs::metadata(&path).ok().and_then(|m| m.modified().ok())
279        } else {
280            None
281        }
282    }
283
284    /// Get the path to the running timer file
285    fn get_timer_file_path(&self) -> PathBuf {
286        self.data_dir.join("running_timer.json")
287    }
288
289    /// Save an active timer to running_timer.json
290    pub fn save_active_timer(&self, timer: &TimerState) -> Result<()> {
291        let path = self.get_timer_file_path();
292        let json = serde_json::to_string_pretty(timer).context("Failed to serialize timer")?;
293        fs::write(&path, json).context(format!("Failed to write timer file: {:?}", path))?;
294        Ok(())
295    }
296
297    /// Load the active timer from running_timer.json
298    ///
299    /// Returns None if no timer file exists (no active timer)
300    pub fn load_active_timer(&self) -> Result<Option<TimerState>> {
301        let path = self.get_timer_file_path();
302
303        if !path.exists() {
304            return Ok(None);
305        }
306
307        let contents =
308            fs::read_to_string(&path).context(format!("Failed to read timer file: {:?}", path))?;
309        let timer: TimerState =
310            serde_json::from_str(&contents).context("Failed to parse timer JSON")?;
311
312        Ok(Some(timer))
313    }
314
315    /// Clear the active timer by deleting running_timer.json
316    pub fn clear_active_timer(&self) -> Result<()> {
317        let path = self.get_timer_file_path();
318
319        if path.exists() {
320            fs::remove_file(&path).context(format!("Failed to delete timer file: {:?}", path))?;
321        }
322
323        Ok(())
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use crate::models::{TimePoint, WorkRecord};
331    use tempfile::TempDir;
332    use time::Date;
333
334    fn create_test_date() -> Date {
335        Date::from_calendar_date(2025, time::Month::November, 6).unwrap()
336    }
337
338    fn create_test_record(id: u32, name: &str) -> WorkRecord {
339        let start = TimePoint::new(9, 0).unwrap();
340        let end = TimePoint::new(17, 0).unwrap();
341        WorkRecord::new(id, name.to_string(), start, end)
342    }
343
344    #[test]
345    fn test_new_storage_with_temp_dir() {
346        let temp_dir = TempDir::new().unwrap();
347        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf());
348
349        assert!(storage.is_ok());
350        assert!(temp_dir.path().exists());
351    }
352
353    #[test]
354    fn test_get_file_path_format() {
355        let temp_dir = TempDir::new().unwrap();
356        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
357        let date = create_test_date();
358
359        let file_path = storage.get_file_path(&date);
360
361        assert_eq!(file_path.file_name().unwrap(), "2025-11-06.json");
362    }
363
364    #[test]
365    fn test_load_nonexistent_file_returns_empty_day() {
366        let temp_dir = TempDir::new().unwrap();
367        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
368        let date = create_test_date();
369
370        let result = storage.load(&date);
371
372        assert!(result.is_ok());
373        let day_data = result.unwrap();
374        assert_eq!(day_data.date, date);
375        assert_eq!(day_data.work_records.len(), 0);
376        assert_eq!(day_data.last_id, 0);
377    }
378
379    #[test]
380    fn test_save_and_load_empty_day() {
381        let temp_dir = TempDir::new().unwrap();
382        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
383        let date = create_test_date();
384        let day_data = DayData::new(date);
385
386        // Save
387        let save_result = storage.save(&day_data);
388        assert!(save_result.is_ok());
389
390        // Load
391        let load_result = storage.load(&date);
392        assert!(load_result.is_ok());
393        let loaded_data = load_result.unwrap();
394
395        assert_eq!(loaded_data.date, date);
396        assert_eq!(loaded_data.work_records.len(), 0);
397    }
398
399    #[test]
400    fn test_save_and_load_with_records() {
401        let temp_dir = TempDir::new().unwrap();
402        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
403        let date = create_test_date();
404
405        let mut day_data = DayData::new(date);
406        day_data.add_record(create_test_record(1, "Coding"));
407        day_data.add_record(create_test_record(2, "Meeting"));
408
409        // Save
410        storage.save(&day_data).unwrap();
411
412        // Load
413        let loaded_data = storage.load(&date).unwrap();
414
415        assert_eq!(loaded_data.date, date);
416        assert_eq!(loaded_data.work_records.len(), 2);
417        assert_eq!(loaded_data.last_id, 2);
418        assert!(loaded_data.work_records.contains_key(&1));
419        assert!(loaded_data.work_records.contains_key(&2));
420    }
421
422    #[test]
423    fn test_save_overwrites_existing_file() {
424        let temp_dir = TempDir::new().unwrap();
425        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
426        let date = create_test_date();
427
428        // Save first version
429        let mut day_data1 = DayData::new(date);
430        day_data1.add_record(create_test_record(1, "Task1"));
431        storage.save(&day_data1).unwrap();
432
433        // Save second version (overwrite)
434        let mut day_data2 = DayData::new(date);
435        day_data2.add_record(create_test_record(2, "Task2"));
436        storage.save(&day_data2).unwrap();
437
438        // Load should return second version
439        let loaded_data = storage.load(&date).unwrap();
440        assert_eq!(loaded_data.work_records.len(), 1);
441        assert!(loaded_data.work_records.contains_key(&2));
442        assert!(!loaded_data.work_records.contains_key(&1));
443    }
444
445    #[test]
446    fn test_save_creates_file() {
447        let temp_dir = TempDir::new().unwrap();
448        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
449        let date = create_test_date();
450        let day_data = DayData::new(date);
451
452        let file_path = storage.get_file_path(&date);
453        assert!(!file_path.exists());
454
455        storage.save(&day_data).unwrap();
456
457        assert!(file_path.exists());
458    }
459
460    #[test]
461    fn test_load_preserves_record_details() {
462        let temp_dir = TempDir::new().unwrap();
463        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
464        let date = create_test_date();
465
466        let mut day_data = DayData::new(date);
467        let mut record = create_test_record(1, "Important Task");
468        record.description = "This is a description".to_string();
469        day_data.add_record(record);
470
471        storage.save(&day_data).unwrap();
472        let loaded_data = storage.load(&date).unwrap();
473
474        let loaded_record = loaded_data.work_records.get(&1).unwrap();
475        assert_eq!(loaded_record.name, "Important Task");
476        assert_eq!(loaded_record.description, "This is a description");
477        assert_eq!(loaded_record.total_minutes, 480);
478    }
479
480    #[test]
481    fn test_multiple_dates() {
482        let temp_dir = TempDir::new().unwrap();
483        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
484
485        let date1 = Date::from_calendar_date(2025, time::Month::November, 5).unwrap();
486        let date2 = Date::from_calendar_date(2025, time::Month::November, 6).unwrap();
487
488        let mut day1 = DayData::new(date1);
489        day1.add_record(create_test_record(1, "Day1Task"));
490
491        let mut day2 = DayData::new(date2);
492        day2.add_record(create_test_record(1, "Day2Task"));
493
494        storage.save(&day1).unwrap();
495        storage.save(&day2).unwrap();
496
497        let loaded_day1 = storage.load(&date1).unwrap();
498        let loaded_day2 = storage.load(&date2).unwrap();
499
500        assert_eq!(loaded_day1.work_records.get(&1).unwrap().name, "Day1Task");
501        assert_eq!(loaded_day2.work_records.get(&1).unwrap().name, "Day2Task");
502    }
503
504    #[test]
505    fn test_json_format_is_pretty() {
506        let temp_dir = TempDir::new().unwrap();
507        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
508        let date = create_test_date();
509
510        let mut day_data = DayData::new(date);
511        day_data.add_record(create_test_record(1, "Task"));
512
513        storage.save(&day_data).unwrap();
514
515        let file_path = storage.get_file_path(&date);
516        let contents = fs::read_to_string(file_path).unwrap();
517
518        // Pretty JSON should have newlines
519        assert!(contents.contains('\n'));
520        assert!(contents.contains("  ")); // Indentation
521    }
522
523    #[test]
524    fn test_load_active_timer_returns_none_when_not_exists() {
525        let temp_dir = TempDir::new().unwrap();
526        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
527
528        let result = storage.load_active_timer().unwrap();
529        assert!(result.is_none());
530    }
531
532    #[test]
533    fn test_save_and_load_active_timer() {
534        use crate::timer::{TimerState, TimerStatus};
535        use time::OffsetDateTime;
536
537        let temp_dir = TempDir::new().unwrap();
538        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
539
540        let now = OffsetDateTime::now_utc();
541        let timer = TimerState {
542            id: None,
543            task_name: "Work".to_string(),
544            description: Some("Important".to_string()),
545            start_time: now,
546            end_time: None,
547            date: now.date(),
548            status: TimerStatus::Running,
549            paused_duration_secs: 0,
550            paused_at: None,
551            created_at: now,
552            updated_at: now,
553            source_record_id: None,
554            source_record_date: None,
555        };
556
557        // Save
558        storage.save_active_timer(&timer).unwrap();
559
560        // Load
561        let loaded = storage.load_active_timer().unwrap();
562        assert!(loaded.is_some());
563
564        let loaded_timer = loaded.unwrap();
565        assert_eq!(loaded_timer.task_name, "Work");
566        assert_eq!(loaded_timer.description, Some("Important".to_string()));
567        assert_eq!(loaded_timer.status, TimerStatus::Running);
568    }
569
570    #[test]
571    fn test_clear_active_timer() {
572        use crate::timer::{TimerState, TimerStatus};
573        use time::OffsetDateTime;
574
575        let temp_dir = TempDir::new().unwrap();
576        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
577
578        let now = OffsetDateTime::now_utc();
579        let timer = TimerState {
580            id: None,
581            task_name: "Work".to_string(),
582            description: None,
583            start_time: now,
584            end_time: None,
585            date: now.date(),
586            status: TimerStatus::Running,
587            paused_duration_secs: 0,
588            paused_at: None,
589            created_at: now,
590            updated_at: now,
591            source_record_id: None,
592            source_record_date: None,
593        };
594
595        // Save
596        storage.save_active_timer(&timer).unwrap();
597        assert!(storage.load_active_timer().unwrap().is_some());
598
599        // Clear
600        storage.clear_active_timer().unwrap();
601
602        // Verify it's gone
603        assert!(storage.load_active_timer().unwrap().is_none());
604    }
605
606    #[test]
607    fn test_clear_active_timer_when_none_exists() {
608        let temp_dir = TempDir::new().unwrap();
609        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
610
611        // Should not error even if file doesn't exist
612        let result = storage.clear_active_timer();
613        assert!(result.is_ok());
614    }
615
616    // StorageManager tests
617    #[test]
618    fn test_storage_manager_new() {
619        let temp_dir = TempDir::new().unwrap();
620        let manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf());
621        assert!(manager.is_ok());
622    }
623
624    #[test]
625    fn test_storage_manager_load_with_tracking() {
626        let temp_dir = TempDir::new().unwrap();
627        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
628        let date = create_test_date();
629
630        let result = manager.load_with_tracking(date);
631        assert!(result.is_ok());
632
633        // Should have tracking info now
634        assert!(manager.file_modified_times.contains_key(&date));
635    }
636
637    #[test]
638    fn test_storage_manager_add_record_transactional() {
639        let temp_dir = TempDir::new().unwrap();
640        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
641        let date = create_test_date();
642
643        let record = create_test_record(1, "Test Task");
644
645        // Add record (should load, add, save, track)
646        let result = manager.add_record(date, record);
647        assert!(result.is_ok());
648
649        // Verify it was saved
650        let day_data = manager.load_with_tracking(date).unwrap();
651        assert_eq!(day_data.work_records.len(), 1);
652        assert!(day_data.work_records.contains_key(&1));
653
654        // Verify tracking was updated
655        assert!(manager.get_last_modified(&date).is_some());
656    }
657
658    #[test]
659    fn test_storage_manager_update_record_transactional() {
660        let temp_dir = TempDir::new().unwrap();
661        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
662        let date = create_test_date();
663
664        // Add initial record
665        let record1 = create_test_record(1, "Original Task");
666        manager.add_record(date, record1).unwrap();
667
668        // Update the record
669        let record2 = create_test_record(1, "Updated Task");
670        let result = manager.update_record(date, record2);
671        assert!(result.is_ok());
672
673        // Verify update
674        let day_data = manager.load_with_tracking(date).unwrap();
675        assert_eq!(day_data.work_records.len(), 1);
676        assert_eq!(day_data.work_records.get(&1).unwrap().name, "Updated Task");
677    }
678
679    #[test]
680    fn test_storage_manager_remove_record_transactional() {
681        let temp_dir = TempDir::new().unwrap();
682        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
683        let date = create_test_date();
684
685        // Add record
686        let record = create_test_record(1, "To Be Removed");
687        manager.add_record(date, record).unwrap();
688
689        // Remove it
690        let result = manager.remove_record(date, 1);
691        assert!(result.is_ok());
692        assert_eq!(result.unwrap().name, "To Be Removed");
693
694        // Verify it's gone
695        let day_data = manager.load_with_tracking(date).unwrap();
696        assert_eq!(day_data.work_records.len(), 0);
697    }
698
699    #[test]
700    fn test_storage_manager_remove_nonexistent_record_fails() {
701        let temp_dir = TempDir::new().unwrap();
702        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
703        let date = create_test_date();
704
705        // Try to remove record that doesn't exist
706        let result = manager.remove_record(date, 999);
707        assert!(result.is_err());
708    }
709
710    #[test]
711    fn test_storage_manager_check_and_reload_no_change() {
712        let temp_dir = TempDir::new().unwrap();
713        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
714        let date = create_test_date();
715
716        // Load initially
717        manager.load_with_tracking(date).unwrap();
718
719        // Check for reload (no external changes)
720        let result = manager.check_and_reload(date);
721        assert!(result.is_ok());
722        assert!(result.unwrap().is_none()); // No reload needed
723    }
724
725    #[test]
726    fn test_storage_manager_check_and_reload_with_external_change() {
727        let temp_dir = TempDir::new().unwrap();
728        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
729        let date = create_test_date();
730
731        // Load initially (empty)
732        manager.load_with_tracking(date).unwrap();
733
734        // Simulate external change by using a different storage instance
735        let mut external_manager =
736            StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
737        let record = create_test_record(1, "External Change");
738        external_manager.add_record(date, record).unwrap();
739
740        // Check for reload - should detect change
741        let result = manager.check_and_reload(date);
742        assert!(result.is_ok());
743        let reloaded_data = result.unwrap();
744        assert!(reloaded_data.is_some()); // Reload happened
745        assert_eq!(reloaded_data.unwrap().work_records.len(), 1);
746    }
747
748    #[test]
749    fn test_storage_manager_save_updates_tracking() {
750        let temp_dir = TempDir::new().unwrap();
751        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
752        let date = create_test_date();
753
754        let mut day_data = DayData::new(date);
755        day_data.add_record(create_test_record(1, "Task"));
756
757        // Save should update tracking
758        manager.save(&day_data).unwrap();
759
760        assert!(manager.get_last_modified(&date).is_some());
761    }
762
763    // Additional Storage tests
764    #[test]
765    fn test_get_file_modified_time_returns_none_for_nonexistent_file() {
766        let temp_dir = TempDir::new().unwrap();
767        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
768        let date = create_test_date();
769
770        let modified_time = storage.get_file_modified_time(&date);
771        assert!(modified_time.is_none());
772    }
773
774    #[test]
775    fn test_get_file_modified_time_returns_some_after_save() {
776        let temp_dir = TempDir::new().unwrap();
777        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
778        let date = create_test_date();
779        let day_data = DayData::new(date);
780
781        // Before save - no modification time
782        assert!(storage.get_file_modified_time(&date).is_none());
783
784        // After save - should have modification time
785        storage.save(&day_data).unwrap();
786        assert!(storage.get_file_modified_time(&date).is_some());
787    }
788
789    #[test]
790    fn test_get_timer_file_path_format() {
791        let temp_dir = TempDir::new().unwrap();
792        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
793
794        let timer_path = storage.get_timer_file_path();
795        assert_eq!(timer_path.file_name().unwrap(), "running_timer.json");
796    }
797
798    #[test]
799    fn test_file_path_format_with_single_digit_month_and_day() {
800        let temp_dir = TempDir::new().unwrap();
801        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
802        let date = Date::from_calendar_date(2025, time::Month::January, 5).unwrap();
803
804        let file_path = storage.get_file_path(&date);
805        assert_eq!(file_path.file_name().unwrap(), "2025-01-05.json");
806    }
807
808    #[test]
809    fn test_file_path_format_with_december() {
810        let temp_dir = TempDir::new().unwrap();
811        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
812        let date = Date::from_calendar_date(2025, time::Month::December, 31).unwrap();
813
814        let file_path = storage.get_file_path(&date);
815        assert_eq!(file_path.file_name().unwrap(), "2025-12-31.json");
816    }
817
818    #[test]
819    fn test_save_and_load_timer_with_paused_status() {
820        use crate::timer::{TimerState, TimerStatus};
821        use time::OffsetDateTime;
822
823        let temp_dir = TempDir::new().unwrap();
824        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
825
826        let now = OffsetDateTime::now_utc();
827        let timer = TimerState {
828            id: None,
829            task_name: "Paused Work".to_string(),
830            description: None,
831            start_time: now,
832            end_time: None,
833            date: now.date(),
834            status: TimerStatus::Paused,
835            paused_duration_secs: 120,
836            paused_at: Some(now),
837            created_at: now,
838            updated_at: now,
839            source_record_id: None,
840            source_record_date: None,
841        };
842
843        storage.save_active_timer(&timer).unwrap();
844        let loaded = storage.load_active_timer().unwrap().unwrap();
845
846        assert_eq!(loaded.status, TimerStatus::Paused);
847        assert_eq!(loaded.paused_duration_secs, 120);
848        assert!(loaded.paused_at.is_some());
849    }
850
851    #[test]
852    fn test_save_and_load_timer_with_source_record() {
853        use crate::timer::{TimerState, TimerStatus};
854        use time::OffsetDateTime;
855
856        let temp_dir = TempDir::new().unwrap();
857        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
858
859        let now = OffsetDateTime::now_utc();
860        let source_date = Date::from_calendar_date(2025, time::Month::November, 5).unwrap();
861        let timer = TimerState {
862            id: None,
863            task_name: "Continued Work".to_string(),
864            description: Some("From record 5".to_string()),
865            start_time: now,
866            end_time: None,
867            date: now.date(),
868            status: TimerStatus::Running,
869            paused_duration_secs: 0,
870            paused_at: None,
871            created_at: now,
872            updated_at: now,
873            source_record_id: Some(5),
874            source_record_date: Some(source_date),
875        };
876
877        storage.save_active_timer(&timer).unwrap();
878        let loaded = storage.load_active_timer().unwrap().unwrap();
879
880        assert_eq!(loaded.source_record_id, Some(5));
881        assert_eq!(loaded.source_record_date, Some(source_date));
882    }
883
884    #[test]
885    fn test_save_multiple_records_preserves_order() {
886        let temp_dir = TempDir::new().unwrap();
887        let storage = Storage::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
888        let date = create_test_date();
889
890        let mut day_data = DayData::new(date);
891        for i in 1..=5 {
892            day_data.add_record(create_test_record(i, &format!("Task{}", i)));
893        }
894
895        storage.save(&day_data).unwrap();
896        let loaded = storage.load(&date).unwrap();
897
898        assert_eq!(loaded.work_records.len(), 5);
899        assert_eq!(loaded.last_id, 5);
900        for i in 1..=5 {
901            assert!(loaded.work_records.contains_key(&i));
902        }
903    }
904
905    // Additional StorageManager tests
906    #[test]
907    fn test_storage_manager_timer_passthrough_save_and_load() {
908        use crate::timer::{TimerState, TimerStatus};
909        use time::OffsetDateTime;
910
911        let temp_dir = TempDir::new().unwrap();
912        let manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
913
914        let now = OffsetDateTime::now_utc();
915        let timer = TimerState {
916            id: None,
917            task_name: "Test".to_string(),
918            description: None,
919            start_time: now,
920            end_time: None,
921            date: now.date(),
922            status: TimerStatus::Running,
923            paused_duration_secs: 0,
924            paused_at: None,
925            created_at: now,
926            updated_at: now,
927            source_record_id: None,
928            source_record_date: None,
929        };
930
931        // Test passthrough methods
932        manager.save_active_timer(&timer).unwrap();
933        let loaded = manager.load_active_timer().unwrap();
934        assert!(loaded.is_some());
935
936        manager.clear_active_timer().unwrap();
937        let cleared = manager.load_active_timer().unwrap();
938        assert!(cleared.is_none());
939    }
940
941    #[test]
942    fn test_storage_manager_tracks_multiple_dates() {
943        let temp_dir = TempDir::new().unwrap();
944        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
945
946        let date1 = Date::from_calendar_date(2025, time::Month::November, 1).unwrap();
947        let date2 = Date::from_calendar_date(2025, time::Month::November, 2).unwrap();
948        let date3 = Date::from_calendar_date(2025, time::Month::November, 3).unwrap();
949
950        // Load multiple dates
951        manager.load_with_tracking(date1).unwrap();
952        manager.load_with_tracking(date2).unwrap();
953        manager.load_with_tracking(date3).unwrap();
954
955        // All should be tracked
956        assert!(manager.file_modified_times.contains_key(&date1));
957        assert!(manager.file_modified_times.contains_key(&date2));
958        assert!(manager.file_modified_times.contains_key(&date3));
959    }
960
961    #[test]
962    fn test_storage_manager_get_last_modified_returns_none_for_untracked_date() {
963        let temp_dir = TempDir::new().unwrap();
964        let manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
965        let date = create_test_date();
966
967        // Date not loaded yet - no tracking
968        assert!(manager.get_last_modified(&date).is_none());
969    }
970
971    #[test]
972    fn test_storage_manager_check_and_reload_before_tracking() {
973        let temp_dir = TempDir::new().unwrap();
974        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
975        let date = create_test_date();
976
977        // Check without loading first - should load as new
978        let result = manager.check_and_reload(date).unwrap();
979        assert!(result.is_some());
980    }
981
982    #[test]
983    fn test_storage_manager_add_multiple_records_incrementally() {
984        let temp_dir = TempDir::new().unwrap();
985        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
986        let date = create_test_date();
987
988        // Add records one by one
989        manager
990            .add_record(date, create_test_record(1, "Task1"))
991            .unwrap();
992        manager
993            .add_record(date, create_test_record(2, "Task2"))
994            .unwrap();
995        manager
996            .add_record(date, create_test_record(3, "Task3"))
997            .unwrap();
998
999        // Verify all are saved
1000        let day_data = manager.load_with_tracking(date).unwrap();
1001        assert_eq!(day_data.work_records.len(), 3);
1002    }
1003
1004    #[test]
1005    fn test_storage_manager_update_multiple_times() {
1006        let temp_dir = TempDir::new().unwrap();
1007        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
1008        let date = create_test_date();
1009
1010        // Add initial record
1011        manager
1012            .add_record(date, create_test_record(1, "Version1"))
1013            .unwrap();
1014
1015        // Update multiple times
1016        manager
1017            .update_record(date, create_test_record(1, "Version2"))
1018            .unwrap();
1019        manager
1020            .update_record(date, create_test_record(1, "Version3"))
1021            .unwrap();
1022
1023        // Final version should be loaded
1024        let day_data = manager.load_with_tracking(date).unwrap();
1025        assert_eq!(day_data.work_records.get(&1).unwrap().name, "Version3");
1026    }
1027
1028    #[test]
1029    fn test_storage_manager_remove_from_multiple_records() {
1030        let temp_dir = TempDir::new().unwrap();
1031        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
1032        let date = create_test_date();
1033
1034        // Add multiple records
1035        manager
1036            .add_record(date, create_test_record(1, "Task1"))
1037            .unwrap();
1038        manager
1039            .add_record(date, create_test_record(2, "Task2"))
1040            .unwrap();
1041        manager
1042            .add_record(date, create_test_record(3, "Task3"))
1043            .unwrap();
1044
1045        // Remove middle one
1046        let removed = manager.remove_record(date, 2).unwrap();
1047        assert_eq!(removed.name, "Task2");
1048
1049        // Verify remaining
1050        let day_data = manager.load_with_tracking(date).unwrap();
1051        assert_eq!(day_data.work_records.len(), 2);
1052        assert!(day_data.work_records.contains_key(&1));
1053        assert!(!day_data.work_records.contains_key(&2));
1054        assert!(day_data.work_records.contains_key(&3));
1055    }
1056
1057    #[test]
1058    fn test_storage_manager_save_empty_day_data() {
1059        let temp_dir = TempDir::new().unwrap();
1060        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
1061        let date = create_test_date();
1062
1063        let day_data = DayData::new(date);
1064        manager.save(&day_data).unwrap();
1065
1066        // Should be able to load empty data
1067        let loaded = manager.load_with_tracking(date).unwrap();
1068        assert_eq!(loaded.work_records.len(), 0);
1069        assert_eq!(loaded.last_id, 0);
1070    }
1071
1072    #[test]
1073    fn test_storage_manager_save_overwrites_with_tracking_update() {
1074        let temp_dir = TempDir::new().unwrap();
1075        let mut manager = StorageManager::new_with_dir(temp_dir.path().to_path_buf()).unwrap();
1076        let date = create_test_date();
1077
1078        // First save
1079        let mut day_data1 = DayData::new(date);
1080        day_data1.add_record(create_test_record(1, "Task1"));
1081        manager.save(&day_data1).unwrap();
1082        let first_modified = manager.get_last_modified(&date);
1083
1084        // Give time for modification time to change
1085        std::thread::sleep(std::time::Duration::from_millis(10));
1086
1087        // Second save - overwrite
1088        let mut day_data2 = DayData::new(date);
1089        day_data2.add_record(create_test_record(2, "Task2"));
1090        manager.save(&day_data2).unwrap();
1091        let second_modified = manager.get_last_modified(&date);
1092
1093        // Tracking should be updated
1094        assert_ne!(first_modified, second_modified);
1095
1096        // Data should be overwritten
1097        let loaded = manager.load_with_tracking(date).unwrap();
1098        assert_eq!(loaded.work_records.len(), 1);
1099        assert!(loaded.work_records.contains_key(&2));
1100        assert!(!loaded.work_records.contains_key(&1));
1101    }
1102}