Skip to main content

yesser_todo_db/
lib.rs

1use std::{
2    fs::{self, File},
3    path::PathBuf,
4};
5
6use serde::{Deserialize, Serialize};
7use serde_json::{from_reader, to_writer};
8
9pub use yesser_todo_errors::db_error::DatabaseError;
10
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
12pub struct Task {
13    pub name: String,
14    pub done: bool,
15}
16
17#[derive(Serialize, Deserialize, Debug, Clone)]
18pub struct CloudConfig {
19    pub host: String,
20    pub port: String,
21}
22
23impl CloudConfig {
24    /// Construct a `CloudConfig` that owns copies of the provided host and port strings.
25    ///
26    /// The returned `CloudConfig` contains owned `String` values cloned from the provided references.
27    ///
28    /// # Examples
29    ///
30    /// ```
31    /// # use yesser_todo_db::CloudConfig;
32    /// let host = "example.com".to_string();
33    /// let port = "8080".to_string();
34    /// let cfg = CloudConfig::new(&host, &port);
35    /// assert_eq!(cfg.host, "example.com");
36    /// assert_eq!(cfg.port, "8080");
37    /// ```
38    pub fn new(host: &str, port: &str) -> Self {
39        CloudConfig {
40            host: host.to_string(),
41            port: port.to_string(),
42        }
43    }
44}
45
46pub trait SaveData: Send + Sync {
47    fn get_tasks(&mut self) -> &mut Vec<Task>;
48    fn add_task(&mut self, task: Task);
49    fn remove_task(&mut self, task_index: usize);
50    fn mark_task_done(&mut self, task_index: usize) -> bool;
51    fn mark_task_undone(&mut self, task_index: usize) -> bool;
52    fn clear_tasks(&mut self);
53    fn clear_done_tasks(&mut self);
54    fn load_tasks(&mut self) -> Result<(), DatabaseError>;
55    fn save_tasks(&self) -> Result<(), DatabaseError>;
56    fn get_cloud_config(&self) -> Result<Option<(String, String)>, DatabaseError>;
57    fn save_cloud_config(&self, host: &str, port: &str) -> Result<(), DatabaseError>;
58    fn remove_cloud_config(&self) -> Result<(), DatabaseError>;
59}
60
61pub struct JsonSaveData {
62    tasks: Vec<Task>,
63    data_dir: PathBuf,
64    config_dir: PathBuf,
65}
66
67/// Checks whether a task's name exactly equals a query string.
68///
69/// # Examples
70///
71/// ```
72/// use yesser_todo_db::{Task, exactly_matches};
73/// let task = Task { name: "Buy milk".into(), done: false };
74/// assert!(exactly_matches(&task, "Buy milk"));
75/// assert!(!exactly_matches(&task, "buy milk"));
76/// ```
77///
78/// # Returns
79///
80/// `true` if the task's name equals `query_string`, `false` otherwise.
81pub fn exactly_matches(task: &Task, query_string: &str) -> bool {
82    task.name == *query_string
83}
84
85/// Finds the position of the first task whose name exactly matches the given query string.
86///
87/// # Returns
88///
89/// The zero-based index of the matching task, or `None` if no match is found.
90///
91/// # Examples
92///
93/// ```
94/// use yesser_todo_db::{get_index, Task};
95/// let tasks = vec![Task { name: "one".into(), done: false }, Task { name: "two".into(), done: true }];
96/// assert_eq!(get_index(&tasks, "two"), Some(1));
97/// assert_eq!(get_index(&tasks, "three"), None);
98/// ```
99pub fn get_index(tasks: &[Task], query_string: &str) -> Option<usize> {
100    tasks.iter().position(|r| exactly_matches(r, query_string))
101}
102
103impl JsonSaveData {
104    /// Constructs an empty `JsonSaveData` using platform-specific default directories.
105    ///
106    /// # Returns
107    ///
108    /// A `JsonSaveData` whose internal task list is empty.
109    ///
110    /// # Errors
111    ///
112    /// Returns `DatabaseError::UserDirsError` if platform-specific user directories cannot be determined.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use yesser_todo_db::{JsonSaveData, SaveData};
118    /// let mut data = JsonSaveData::new().unwrap();
119    /// assert!(data.get_tasks().is_empty());
120    /// ```
121    pub fn new() -> Result<JsonSaveData, DatabaseError> {
122        let app_dirs = platform_dirs::AppDirs::new(Some("todo"), true).ok_or(DatabaseError::UserDirsError)?;
123        Ok(JsonSaveData {
124            tasks: Vec::new(),
125            data_dir: app_dirs.data_dir,
126            config_dir: app_dirs.config_dir,
127        })
128    }
129
130    /// Constructs a `JsonSaveData` with separate data and config directories.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use std::path::PathBuf;
136    /// use yesser_todo_db::{JsonSaveData, SaveData};
137    ///
138    /// let mut data = JsonSaveData::with_dirs(
139    ///     PathBuf::from("/tmp/my-data"),
140    ///     PathBuf::from("/tmp/my-config"),
141    /// );
142    /// assert!(data.get_tasks().is_empty());
143    /// ```
144    pub fn with_dirs(data_dir: PathBuf, config_dir: PathBuf) -> JsonSaveData {
145        JsonSaveData {
146            tasks: Vec::new(),
147            data_dir,
148            config_dir,
149        }
150    }
151
152    /// Constructs a `JsonSaveData` using a single base directory for both data and config.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use std::path::PathBuf;
158    /// use yesser_todo_db::{JsonSaveData, SaveData};
159    ///
160    /// let mut data = JsonSaveData::with_dir(PathBuf::from("/tmp/my-base"));
161    /// assert!(data.get_tasks().is_empty());
162    /// ```
163    pub fn with_dir(dir: PathBuf) -> JsonSaveData {
164        JsonSaveData {
165            tasks: Vec::new(),
166            data_dir: dir.clone(),
167            config_dir: dir,
168        }
169    }
170
171    #[cfg(test)]
172    pub fn new_temp() -> Result<(JsonSaveData, tempfile::TempDir), DatabaseError> {
173        let dir = tempfile::tempdir().map_err(DatabaseError::IOError)?;
174        let path = dir.path().to_owned();
175        Ok((
176            JsonSaveData {
177                tasks: Vec::new(),
178                data_dir: path.clone(),
179                config_dir: path,
180            },
181            dir,
182        ))
183    }
184
185    fn data_file_path(&self) -> PathBuf {
186        self.data_dir.join("todos.json")
187    }
188
189    fn config_file_path(&self) -> PathBuf {
190        self.config_dir.join("cloud.json")
191    }
192}
193
194impl SaveData for JsonSaveData {
195    /// Retrieves the saved cloud configuration, if present.
196    ///
197    /// If a cloud configuration file exists and contains valid JSON matching `CloudConfig`,
198    /// returns `Some((host, port))`. If the configuration file is missing, returns `None`.
199    /// I/O or deserialization failures are returned as `DatabaseError`.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// use yesser_todo_db::{JsonSaveData, SaveData};
205    ///
206    /// // This example assumes no cloud config is present or a valid one exists.
207    /// let data = JsonSaveData::new().unwrap();
208    /// let res = data.get_cloud_config();
209    ///
210    /// match res {
211    ///     Ok(None) => println!("No cloud config saved"),
212    ///     Ok(Some((host, port))) => println!("Cloud config: {}:{}", host, port),
213    ///     Err(e) => panic!("Failed to read cloud config: {}", e),
214    /// }
215    /// ```
216    fn get_cloud_config(&self) -> Result<Option<(String, String)>, DatabaseError> {
217        let config_file_path = self.config_file_path();
218
219        if !config_file_path.exists() {
220            return Ok(None);
221        }
222
223        let file = File::open(config_file_path)?;
224        let result: CloudConfig = from_reader(file)?;
225
226        Ok(Some((result.host.clone(), result.port.clone())))
227    }
228
229    /// Writes the cloud host and port to the application's `cloud.json` in the config directory.
230    ///
231    /// Creates the config directory if it does not exist and overwrites any existing `cloud.json`.
232    ///
233    /// # Parameters
234    ///
235    /// - `host`: cloud server host name or address.
236    /// - `port`: cloud server port.
237    ///
238    /// # Returns
239    ///
240    /// `Ok(())` on success, `Err(DatabaseError)` if file I/O or serialization fails.
241    ///
242    /// # Examples
243    ///
244    /// ```no_run
245    /// use yesser_todo_db::{JsonSaveData, SaveData};
246    ///
247    /// let data = JsonSaveData::new().unwrap();
248    /// data.save_cloud_config("example.com", "1234").unwrap();
249    /// ```
250    fn save_cloud_config(&self, host: &str, port: &str) -> Result<(), DatabaseError> {
251        fs::create_dir_all(&self.config_dir)?;
252        let file = File::create(self.config_file_path())?;
253        to_writer(file, &CloudConfig::new(host, port))?;
254
255        Ok(())
256    }
257
258    /// Remove the cloud configuration file from the application's config directory.
259    ///
260    /// # Errors
261    ///
262    /// Returns a `DatabaseError` if the file cannot be removed.
263    ///
264    /// # Examples
265    ///
266    /// ```no_run
267    /// use yesser_todo_db::{JsonSaveData, SaveData};
268    ///
269    /// let data = JsonSaveData::new().unwrap();
270    /// // Attempt to remove the cloud configuration file.
271    /// let _ = data.remove_cloud_config();
272    /// ```
273    fn remove_cloud_config(&self) -> Result<(), DatabaseError> {
274        fs::remove_file(self.config_file_path())?;
275        Ok(())
276    }
277
278    /// Loads tasks from the application's data file into this `JsonSaveData` instance.
279    ///
280    /// Ensures the application's data directory exists; if the data file is missing, no changes
281    /// are made to the existing tasks. When the data file is present it is deserialized and
282    /// replaces the current task list.
283    ///
284    /// # Returns
285    ///
286    /// `Ok(())` on success, or a `DatabaseError` if directory creation, file I/O, or deserialization fails.
287    ///
288    /// # Examples
289    ///
290    /// ```no_run
291    /// # use yesser_todo_db::{JsonSaveData, SaveData};
292    /// let mut data = JsonSaveData::new().unwrap();
293    /// // If no data file is present this will succeed and leave tasks empty.
294    /// data.load_tasks().unwrap();
295    /// assert!(data.get_tasks().is_empty());
296    /// ```
297    fn load_tasks(&mut self) -> Result<(), DatabaseError> {
298        let data_file_path = self.data_file_path();
299
300        fs::create_dir_all(&self.data_dir)?;
301
302        if !data_file_path.exists() {
303            return Ok(());
304        }
305
306        let file = File::open(data_file_path)?;
307
308        let result: Vec<Task> = from_reader(file)?;
309        self.tasks = result;
310
311        Ok(())
312    }
313
314    /// Writes the current task list to the platform-specific data file (todos.json), creating the data directory if needed.
315    ///
316    /// # Examples
317    ///
318    /// ```no_run
319    /// use yesser_todo_db::{JsonSaveData, SaveData};
320    ///
321    /// let data = JsonSaveData::new().unwrap();
322    /// data.save_tasks().unwrap();
323    /// ```
324    fn save_tasks(&self) -> Result<(), DatabaseError> {
325        fs::create_dir_all(&self.data_dir)?;
326
327        let file = File::create(self.data_file_path())?;
328
329        to_writer(file, &self.tasks)?;
330
331        Ok(())
332    }
333
334    /// Access the internal list of tasks for in-place modification.
335    ///
336    /// Returns a mutable reference to the internal `Vec<Task>`.
337    ///
338    /// # Examples
339    ///
340    /// ```
341    /// use yesser_todo_db::{JsonSaveData, SaveData, Task};
342    /// let mut data = JsonSaveData::new().unwrap();
343    /// data.get_tasks().push(Task { name: "buy milk".to_string(), done: false });
344    /// assert_eq!(data.get_tasks().len(), 1);
345    /// data.get_tasks()[0].done = true;
346    /// assert!(data.get_tasks()[0].done);
347    /// ```
348    fn get_tasks(&mut self) -> &mut Vec<Task> {
349        &mut self.tasks
350    }
351
352    /// Appends the given task to the internal list of tasks.
353    ///
354    /// # Examples
355    ///
356    /// ```
357    /// use yesser_todo_db::{JsonSaveData, SaveData, Task};
358    /// let mut data = JsonSaveData::new().unwrap();
359    /// data.add_task(Task { name: "Write tests".into(), done: false });
360    /// assert_eq!(data.get_tasks().len(), 1);
361    /// ```
362    fn add_task(&mut self, task: Task) {
363        self.tasks.push(task)
364    }
365
366    fn remove_task(&mut self, task_index: usize) {
367        self.tasks.remove(task_index);
368    }
369
370    /// Mark the task at the given index as done and return its previous completion state.
371    ///
372    /// # Parameters
373    ///
374    /// - `task_index`: Index of the task within the internal tasks list.
375    ///
376    /// # Panics
377    ///
378    /// Panics if `task_index` is out of bounds for the tasks list.
379    ///
380    /// # Returns
381    ///
382    /// `true` if the task was already marked done, `false` otherwise.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use yesser_todo_db::{JsonSaveData, SaveData, Task};
388    /// let mut data = JsonSaveData::new().unwrap();
389    /// data.add_task(Task { name: "a".into(), done: false });
390    /// let prev = data.mark_task_done(0);
391    /// assert_eq!(prev, false);
392    /// assert_eq!(data.get_tasks()[0].done, true);
393    /// ```
394    fn mark_task_done(&mut self, task_index: usize) -> bool {
395        let was_done = self.tasks[task_index].done;
396        self.tasks[task_index].done = true;
397        was_done
398    }
399
400    /// Marks the task at the given index as not done and returns whether it was already not done.
401    ///
402    /// # Examples
403    ///
404    /// ```
405    /// use yesser_todo_db::{JsonSaveData, SaveData, Task};
406    /// let mut data = JsonSaveData::new().unwrap();
407    /// data.add_task(Task { name: "task".into(), done: true });
408    /// // It was done, so the previous "undone" state is false.
409    /// assert_eq!(data.mark_task_undone(0), false);
410    /// // Now it's already not done, so the previous "undone" state is true.
411    /// assert_eq!(data.mark_task_undone(0), true);
412    /// ```
413    fn mark_task_undone(&mut self, task_index: usize) -> bool {
414        let was_undone = !self.tasks[task_index].done;
415        self.tasks[task_index].done = false;
416        was_undone
417    }
418
419    /// Removes all tasks from the saved task list.
420    ///
421    /// # Examples
422    ///
423    /// ```
424    /// use yesser_todo_db::{JsonSaveData, SaveData, Task};
425    /// let mut data = JsonSaveData::new().unwrap();
426    /// data.add_task(Task { name: "a".into(), done: false });
427    /// data.add_task(Task { name: "b".into(), done: true });
428    /// data.clear_tasks();
429    /// assert!(data.get_tasks().is_empty());
430    /// ```
431    fn clear_tasks(&mut self) {
432        self.tasks.clear();
433    }
434
435    /// Removes all tasks marked as completed from the internal task list.
436    ///
437    /// This keeps only tasks whose `done` field is `false`.
438    ///
439    /// # Examples
440    ///
441    /// ```
442    /// use yesser_todo_db::{JsonSaveData, SaveData, Task};
443    /// let mut data = JsonSaveData::new().unwrap();
444    /// data.add_task(Task { name: "a".into(), done: false });
445    /// data.add_task(Task { name: "b".into(), done: true });
446    /// data.clear_done_tasks();
447    /// let tasks = data.get_tasks();
448    /// assert_eq!(tasks.len(), 1);
449    /// assert_eq!(tasks[0].name, "a");
450    /// ```
451    fn clear_done_tasks(&mut self) {
452        self.tasks.retain(|t| !t.done);
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn test_task_equality() {
462        let task1 = Task {
463            name: "test".to_string(),
464            done: false,
465        };
466        let task2 = Task {
467            name: "test".to_string(),
468            done: false,
469        };
470        let task3 = Task {
471            name: "test".to_string(),
472            done: true,
473        };
474        assert_eq!(task1, task2);
475        assert_ne!(task1, task3);
476    }
477
478    #[test]
479    fn test_task_clone() {
480        let task = Task {
481            name: "original".to_string(),
482            done: false,
483        };
484        let cloned = task.clone();
485        assert_eq!(task, cloned);
486    }
487
488    #[test]
489    fn test_cloud_config_new() {
490        let config = CloudConfig::new("localhost", "8080");
491        assert_eq!(config.host, "localhost");
492        assert_eq!(config.port, "8080");
493    }
494
495    #[test]
496    fn test_cloud_config_clone() {
497        let config = CloudConfig::new("example.com", "443");
498        let cloned = config.clone();
499        assert_eq!(config.host, cloned.host);
500        assert_eq!(config.port, cloned.port);
501    }
502
503    #[test]
504    fn test_exactly_matches_true() {
505        let task = Task {
506            name: "Buy milk".to_string(),
507            done: false,
508        };
509        assert!(exactly_matches(&task, "Buy milk"));
510    }
511
512    #[test]
513    fn test_exactly_matches_false_case() {
514        let task = Task {
515            name: "Buy milk".to_string(),
516            done: false,
517        };
518        assert!(!exactly_matches(&task, "buy milk"));
519    }
520
521    #[test]
522    fn test_exactly_matches_false_different() {
523        let task = Task {
524            name: "Buy milk".to_string(),
525            done: false,
526        };
527        assert!(!exactly_matches(&task, "Buy bread"));
528    }
529
530    #[test]
531    fn test_get_index_found() {
532        let tasks = vec![
533            Task {
534                name: "one".to_string(),
535                done: false,
536            },
537            Task {
538                name: "two".to_string(),
539                done: true,
540            },
541            Task {
542                name: "three".to_string(),
543                done: false,
544            },
545        ];
546        assert_eq!(get_index(&tasks, "two"), Some(1));
547    }
548
549    #[test]
550    fn test_get_index_not_found() {
551        let tasks = vec![Task {
552            name: "one".to_string(),
553            done: false,
554        }];
555        assert_eq!(get_index(&tasks, "two"), None);
556    }
557
558    #[test]
559    fn test_get_index_empty_list() {
560        let tasks: Vec<Task> = vec![];
561        assert_eq!(get_index(&tasks, "any"), None);
562    }
563
564    #[test]
565    fn test_get_index_first_match() {
566        let tasks = vec![
567            Task {
568                name: "duplicate".to_string(),
569                done: false,
570            },
571            Task {
572                name: "duplicate".to_string(),
573                done: true,
574            },
575        ];
576        assert_eq!(get_index(&tasks, "duplicate"), Some(0));
577    }
578
579    #[test]
580    fn test_json_save_data_new() {
581        let (save_data, _dir) = JsonSaveData::new_temp().unwrap();
582        assert_eq!(save_data.tasks.len(), 0);
583    }
584
585    #[test]
586    fn test_json_save_data_with_dir() {
587        let dir = tempfile::tempdir().unwrap();
588        let save_data = JsonSaveData::with_dir(dir.path().to_owned());
589        assert_eq!(save_data.tasks.len(), 0);
590    }
591
592    #[test]
593    fn test_json_save_data_with_dirs() {
594        let data_dir = tempfile::tempdir().unwrap();
595        let config_dir = tempfile::tempdir().unwrap();
596        let save_data = JsonSaveData::with_dirs(data_dir.path().to_owned(), config_dir.path().to_owned());
597        assert_eq!(save_data.tasks.len(), 0);
598    }
599
600    #[test]
601    fn test_save_data_add_task() {
602        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
603        let task = Task {
604            name: "test task".to_string(),
605            done: false,
606        };
607        save_data.add_task(task.clone());
608        assert_eq!(save_data.tasks.len(), 1);
609        assert_eq!(save_data.tasks[0], task);
610    }
611
612    #[test]
613    fn test_save_data_add_multiple_tasks() {
614        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
615        save_data.add_task(Task {
616            name: "task1".to_string(),
617            done: false,
618        });
619        save_data.add_task(Task {
620            name: "task2".to_string(),
621            done: true,
622        });
623        save_data.add_task(Task {
624            name: "task3".to_string(),
625            done: false,
626        });
627        assert_eq!(save_data.tasks.len(), 3);
628        assert_eq!(save_data.tasks[1].name, "task2");
629    }
630
631    #[test]
632    fn test_save_data_get_tasks() {
633        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
634        save_data.add_task(Task {
635            name: "test".to_string(),
636            done: false,
637        });
638        let tasks = save_data.get_tasks();
639        assert_eq!(tasks.len(), 1);
640        assert_eq!(tasks[0].name, "test");
641    }
642
643    #[test]
644    fn test_save_data_get_tasks_mutable() {
645        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
646        save_data.add_task(Task {
647            name: "test".to_string(),
648            done: false,
649        });
650        let tasks = save_data.get_tasks();
651        tasks[0].done = true;
652        assert!(save_data.tasks[0].done);
653    }
654
655    #[test]
656    fn test_save_data_remove_task() {
657        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
658        save_data.add_task(Task {
659            name: "task1".to_string(),
660            done: false,
661        });
662        save_data.add_task(Task {
663            name: "task2".to_string(),
664            done: false,
665        });
666        save_data.add_task(Task {
667            name: "task3".to_string(),
668            done: false,
669        });
670        save_data.remove_task(1);
671        assert_eq!(save_data.tasks.len(), 2);
672        assert_eq!(save_data.tasks[0].name, "task1");
673        assert_eq!(save_data.tasks[1].name, "task3");
674    }
675
676    #[test]
677    fn test_save_data_mark_task_done() {
678        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
679        save_data.add_task(Task {
680            name: "task".to_string(),
681            done: false,
682        });
683        let was_done = save_data.mark_task_done(0);
684        assert!(!was_done);
685        assert!(save_data.tasks[0].done);
686    }
687
688    #[test]
689    fn test_save_data_mark_task_done_already_done() {
690        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
691        save_data.add_task(Task {
692            name: "task".to_string(),
693            done: true,
694        });
695        let was_done = save_data.mark_task_done(0);
696        assert!(was_done);
697        assert!(save_data.tasks[0].done);
698    }
699
700    #[test]
701    fn test_save_data_mark_task_undone() {
702        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
703        save_data.add_task(Task {
704            name: "task".to_string(),
705            done: true,
706        });
707        let was_undone = save_data.mark_task_undone(0);
708        assert!(!was_undone);
709        assert!(!save_data.tasks[0].done);
710    }
711
712    #[test]
713    fn test_save_data_mark_task_undone_already_undone() {
714        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
715        save_data.add_task(Task {
716            name: "task".to_string(),
717            done: false,
718        });
719        let was_undone = save_data.mark_task_undone(0);
720        assert!(was_undone);
721        assert!(!save_data.tasks[0].done);
722    }
723
724    #[test]
725    fn test_save_data_clear_tasks() {
726        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
727        save_data.add_task(Task {
728            name: "task1".to_string(),
729            done: false,
730        });
731        save_data.add_task(Task {
732            name: "task2".to_string(),
733            done: true,
734        });
735        save_data.clear_tasks();
736        assert_eq!(save_data.tasks.len(), 0);
737    }
738
739    #[test]
740    fn test_save_data_clear_done_tasks() {
741        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
742        save_data.add_task(Task {
743            name: "undone1".to_string(),
744            done: false,
745        });
746        save_data.add_task(Task {
747            name: "done1".to_string(),
748            done: true,
749        });
750        save_data.add_task(Task {
751            name: "undone2".to_string(),
752            done: false,
753        });
754        save_data.add_task(Task {
755            name: "done2".to_string(),
756            done: true,
757        });
758        save_data.clear_done_tasks();
759        assert_eq!(save_data.tasks.len(), 2);
760        assert_eq!(save_data.tasks[0].name, "undone1");
761        assert_eq!(save_data.tasks[1].name, "undone2");
762    }
763
764    #[test]
765    fn test_save_data_clear_done_tasks_no_done() {
766        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
767        save_data.add_task(Task {
768            name: "task1".to_string(),
769            done: false,
770        });
771        save_data.add_task(Task {
772            name: "task2".to_string(),
773            done: false,
774        });
775        save_data.clear_done_tasks();
776        assert_eq!(save_data.tasks.len(), 2);
777    }
778
779    #[test]
780    fn test_save_data_clear_done_tasks_all_done() {
781        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
782        save_data.add_task(Task {
783            name: "task1".to_string(),
784            done: true,
785        });
786        save_data.add_task(Task {
787            name: "task2".to_string(),
788            done: true,
789        });
790        save_data.clear_done_tasks();
791        assert_eq!(save_data.tasks.len(), 0);
792    }
793
794    #[test]
795    fn test_task_serialization() {
796        let task = Task {
797            name: "test".to_string(),
798            done: true,
799        };
800        let json = serde_json::to_string(&task).unwrap();
801        assert!(json.contains("\"name\""));
802        assert!(json.contains("\"done\""));
803        assert!(json.contains("test"));
804    }
805
806    #[test]
807    fn test_task_deserialization() {
808        let json = r#"{"name":"test task","done":false}"#;
809        let task: Task = serde_json::from_str(json).unwrap();
810        assert_eq!(task.name, "test task");
811        assert!(!task.done);
812    }
813
814    #[test]
815    fn test_cloud_config_serialization() {
816        let config = CloudConfig::new("localhost", "8080");
817        let json = serde_json::to_string(&config).unwrap();
818        assert!(json.contains("localhost"));
819        assert!(json.contains("8080"));
820    }
821
822    #[test]
823    fn test_cloud_config_deserialization() {
824        let json = r#"{"host":"example.com","port":"443"}"#;
825        let config: CloudConfig = serde_json::from_str(json).unwrap();
826        assert_eq!(config.host, "example.com");
827        assert_eq!(config.port, "443");
828    }
829
830    #[test]
831    fn test_multiple_operations() {
832        let (mut save_data, _dir) = JsonSaveData::new_temp().unwrap();
833        save_data.add_task(Task {
834            name: "task1".to_string(),
835            done: false,
836        });
837        save_data.add_task(Task {
838            name: "task2".to_string(),
839            done: false,
840        });
841        save_data.mark_task_done(0);
842        save_data.add_task(Task {
843            name: "task3".to_string(),
844            done: false,
845        });
846        save_data.remove_task(1);
847        assert_eq!(save_data.tasks.len(), 2);
848        assert!(save_data.tasks[0].done);
849        assert_eq!(save_data.tasks[0].name, "task1");
850        assert_eq!(save_data.tasks[1].name, "task3");
851    }
852
853    #[test]
854    fn test_load_save_cloud_config() {
855        let (data, _dir) = JsonSaveData::new_temp().unwrap();
856        data.save_cloud_config("example.com", "6982").unwrap();
857        let result = data.get_cloud_config().unwrap();
858        assert_eq!(result, Some(("example.com".to_string(), "6982".to_string())));
859    }
860
861    #[test]
862    fn test_remove_nonexistent_cloud_config() {
863        let (data, _dir) = JsonSaveData::new_temp().unwrap();
864        assert!(data.get_cloud_config().unwrap().is_none());
865        let result = data.remove_cloud_config();
866        assert!(result.is_err());
867    }
868
869    #[test]
870    fn test_load_save_tasks_roundtrip() {
871        let dir = tempfile::tempdir().unwrap();
872        let path = dir.path().to_owned();
873
874        let mut data = JsonSaveData::with_dir(path.clone());
875        data.add_task(Task {
876            name: "task1".into(),
877            done: false,
878        });
879        data.add_task(Task {
880            name: "task2".into(),
881            done: true,
882        });
883        data.save_tasks().unwrap();
884        drop(data);
885
886        let mut loaded = JsonSaveData::with_dir(path);
887        loaded.load_tasks().unwrap();
888        assert_eq!(loaded.get_tasks().len(), 2);
889        assert_eq!(loaded.get_tasks()[0].name, "task1");
890        assert!(!loaded.get_tasks()[0].done);
891        assert_eq!(loaded.get_tasks()[1].name, "task2");
892        assert!(loaded.get_tasks()[1].done);
893    }
894
895    #[test]
896    fn test_persistence_with_dir() {
897        let dir = tempfile::tempdir().unwrap();
898        let path = dir.path().to_owned();
899
900        let mut data = JsonSaveData::with_dir(path.clone());
901        data.add_task(Task {
902            name: "persist".into(),
903            done: true,
904        });
905        data.save_tasks().unwrap();
906        drop(data);
907
908        let mut loaded = JsonSaveData::with_dir(path);
909        loaded.load_tasks().unwrap();
910        assert_eq!(loaded.get_tasks().len(), 1);
911        assert_eq!(loaded.get_tasks()[0].name, "persist");
912        assert!(loaded.get_tasks()[0].done);
913    }
914}