things3_core/
models.rs

1//! Data models for Things 3 entities
2
3use chrono::{DateTime, NaiveDate, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Task status enumeration
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum TaskStatus {
10    #[serde(rename = "incomplete")]
11    Incomplete,
12    #[serde(rename = "completed")]
13    Completed,
14    #[serde(rename = "canceled")]
15    Canceled,
16    #[serde(rename = "trashed")]
17    Trashed,
18}
19
20/// Task type enumeration
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum TaskType {
23    #[serde(rename = "to-do")]
24    Todo,
25    #[serde(rename = "project")]
26    Project,
27    #[serde(rename = "heading")]
28    Heading,
29    #[serde(rename = "area")]
30    Area,
31}
32
33/// How to handle child tasks when deleting a parent
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum DeleteChildHandling {
36    /// Return error if task has children (default)
37    #[serde(rename = "error")]
38    Error,
39    /// Delete parent and all children
40    #[serde(rename = "cascade")]
41    Cascade,
42    /// Delete parent only, orphan children
43    #[serde(rename = "orphan")]
44    Orphan,
45}
46
47/// Main task entity
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Task {
50    /// Unique identifier
51    pub uuid: Uuid,
52    /// Task title
53    pub title: String,
54    /// Task type
55    pub task_type: TaskType,
56    /// Task status
57    pub status: TaskStatus,
58    /// Optional notes
59    pub notes: Option<String>,
60    /// Start date
61    pub start_date: Option<NaiveDate>,
62    /// Deadline
63    pub deadline: Option<NaiveDate>,
64    /// Creation timestamp
65    pub created: DateTime<Utc>,
66    /// Last modification timestamp
67    pub modified: DateTime<Utc>,
68    /// Completion timestamp (when status changed to completed)
69    pub stop_date: Option<DateTime<Utc>>,
70    /// Parent project UUID
71    pub project_uuid: Option<Uuid>,
72    /// Parent area UUID
73    pub area_uuid: Option<Uuid>,
74    /// Parent task UUID
75    pub parent_uuid: Option<Uuid>,
76    /// Associated tags
77    pub tags: Vec<String>,
78    /// Child tasks
79    pub children: Vec<Task>,
80}
81
82/// Project entity
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Project {
85    /// Unique identifier
86    pub uuid: Uuid,
87    /// Project title
88    pub title: String,
89    /// Optional notes
90    pub notes: Option<String>,
91    /// Start date
92    pub start_date: Option<NaiveDate>,
93    /// Deadline
94    pub deadline: Option<NaiveDate>,
95    /// Creation timestamp
96    pub created: DateTime<Utc>,
97    /// Last modification timestamp
98    pub modified: DateTime<Utc>,
99    /// Parent area UUID
100    pub area_uuid: Option<Uuid>,
101    /// Associated tags
102    pub tags: Vec<String>,
103    /// Project status
104    pub status: TaskStatus,
105    /// Child tasks
106    pub tasks: Vec<Task>,
107}
108
109/// Area entity
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Area {
112    /// Unique identifier
113    pub uuid: Uuid,
114    /// Area title
115    pub title: String,
116    /// Optional notes
117    pub notes: Option<String>,
118    /// Creation timestamp
119    pub created: DateTime<Utc>,
120    /// Last modification timestamp
121    pub modified: DateTime<Utc>,
122    /// Associated tags
123    pub tags: Vec<String>,
124    /// Child projects
125    pub projects: Vec<Project>,
126}
127
128/// Tag entity (enhanced with duplicate prevention support)
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct Tag {
131    /// Unique identifier
132    pub uuid: Uuid,
133    /// Tag title (display form, preserves case)
134    pub title: String,
135    /// Keyboard shortcut
136    pub shortcut: Option<String>,
137    /// Parent tag UUID (for nested tags)
138    pub parent_uuid: Option<Uuid>,
139    /// Creation timestamp
140    pub created: DateTime<Utc>,
141    /// Last modification timestamp
142    pub modified: DateTime<Utc>,
143    /// How many tasks use this tag
144    pub usage_count: u32,
145    /// Last time this tag was used
146    pub last_used: Option<DateTime<Utc>>,
147}
148
149/// Tag creation request
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct CreateTagRequest {
152    /// Tag title (required)
153    pub title: String,
154    /// Keyboard shortcut
155    pub shortcut: Option<String>,
156    /// Parent tag UUID (for nested tags)
157    pub parent_uuid: Option<Uuid>,
158}
159
160/// Tag update request
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct UpdateTagRequest {
163    /// Tag UUID
164    pub uuid: Uuid,
165    /// New title
166    pub title: Option<String>,
167    /// New shortcut
168    pub shortcut: Option<String>,
169    /// New parent UUID
170    pub parent_uuid: Option<Uuid>,
171}
172
173/// Tag match type classification
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175pub enum TagMatchType {
176    /// Exact match (case-insensitive)
177    #[serde(rename = "exact")]
178    Exact,
179    /// Same text, different case
180    #[serde(rename = "case_mismatch")]
181    CaseMismatch,
182    /// Fuzzy match (high similarity via Levenshtein distance)
183    #[serde(rename = "similar")]
184    Similar,
185    /// Substring/contains match
186    #[serde(rename = "partial")]
187    PartialMatch,
188}
189
190/// Tag search result with similarity scoring
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct TagMatch {
193    /// The matched tag
194    pub tag: Tag,
195    /// Similarity score (0.0 to 1.0, higher is better)
196    pub similarity_score: f32,
197    /// Type of match
198    pub match_type: TagMatchType,
199}
200
201/// Result of tag creation with duplicate checking
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub enum TagCreationResult {
204    /// New tag was created
205    #[serde(rename = "created")]
206    Created {
207        /// UUID of created tag
208        uuid: Uuid,
209        /// Always true for this variant
210        is_new: bool,
211    },
212    /// Existing exact match found
213    #[serde(rename = "existing")]
214    Existing {
215        /// The existing tag
216        tag: Tag,
217        /// Always false for this variant
218        is_new: bool,
219    },
220    /// Similar tags found (user decision needed)
221    #[serde(rename = "similar_found")]
222    SimilarFound {
223        /// Tags similar to requested title
224        similar_tags: Vec<TagMatch>,
225        /// The title user requested
226        requested_title: String,
227    },
228}
229
230/// Result of tag assignment to task
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub enum TagAssignmentResult {
233    /// Tag assigned successfully
234    #[serde(rename = "assigned")]
235    Assigned {
236        /// UUID of the tag that was assigned
237        tag_uuid: Uuid,
238    },
239    /// Similar tags found (user decision needed)
240    #[serde(rename = "suggestions")]
241    Suggestions {
242        /// Suggested alternative tags
243        similar_tags: Vec<TagMatch>,
244    },
245}
246
247/// Tag auto-completion suggestion
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct TagCompletion {
250    /// The tag
251    pub tag: Tag,
252    /// Priority score (based on usage, recency, match quality)
253    pub score: f32,
254}
255
256/// Tag statistics for analytics
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct TagStatistics {
259    /// Tag UUID
260    pub uuid: Uuid,
261    /// Tag title
262    pub title: String,
263    /// Total usage count
264    pub usage_count: u32,
265    /// Task UUIDs using this tag
266    pub task_uuids: Vec<Uuid>,
267    /// Related tags (frequently used together)
268    pub related_tags: Vec<(String, u32)>, // (tag_title, co_occurrence_count)
269}
270
271/// Pair of similar tags (for duplicate detection)
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct TagPair {
274    /// First tag
275    pub tag1: Tag,
276    /// Second tag
277    pub tag2: Tag,
278    /// Similarity score between them
279    pub similarity: f32,
280}
281
282/// Task creation request
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct CreateTaskRequest {
285    /// Task title (required)
286    pub title: String,
287    /// Task type (defaults to Todo)
288    pub task_type: Option<TaskType>,
289    /// Optional notes
290    pub notes: Option<String>,
291    /// Start date
292    pub start_date: Option<NaiveDate>,
293    /// Deadline
294    pub deadline: Option<NaiveDate>,
295    /// Project UUID (validated if provided)
296    pub project_uuid: Option<Uuid>,
297    /// Area UUID (validated if provided)
298    pub area_uuid: Option<Uuid>,
299    /// Parent task UUID (for subtasks)
300    pub parent_uuid: Option<Uuid>,
301    /// Tags (as string names)
302    pub tags: Option<Vec<String>>,
303    /// Initial status (defaults to Incomplete)
304    pub status: Option<TaskStatus>,
305}
306
307/// Task update request
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct UpdateTaskRequest {
310    /// Task UUID
311    pub uuid: Uuid,
312    /// New title
313    pub title: Option<String>,
314    /// New notes
315    pub notes: Option<String>,
316    /// New start date
317    pub start_date: Option<NaiveDate>,
318    /// New deadline
319    pub deadline: Option<NaiveDate>,
320    /// New status
321    pub status: Option<TaskStatus>,
322    /// New project UUID
323    pub project_uuid: Option<Uuid>,
324    /// New area UUID
325    pub area_uuid: Option<Uuid>,
326    /// New tags
327    pub tags: Option<Vec<String>>,
328}
329
330/// Task filters for queries
331#[derive(Debug, Clone, Serialize, Deserialize, Default)]
332pub struct TaskFilters {
333    /// Filter by status
334    pub status: Option<TaskStatus>,
335    /// Filter by task type
336    pub task_type: Option<TaskType>,
337    /// Filter by project UUID
338    pub project_uuid: Option<Uuid>,
339    /// Filter by area UUID
340    pub area_uuid: Option<Uuid>,
341    /// Filter by tags
342    pub tags: Option<Vec<String>>,
343    /// Filter by start date range
344    pub start_date_from: Option<NaiveDate>,
345    pub start_date_to: Option<NaiveDate>,
346    /// Filter by deadline range
347    pub deadline_from: Option<NaiveDate>,
348    pub deadline_to: Option<NaiveDate>,
349    /// Search query
350    pub search_query: Option<String>,
351    /// Limit results
352    pub limit: Option<usize>,
353    /// Offset for pagination
354    pub offset: Option<usize>,
355}
356
357/// Project creation request
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct CreateProjectRequest {
360    /// Project title (required)
361    pub title: String,
362    /// Optional notes
363    pub notes: Option<String>,
364    /// Area UUID (validated if provided)
365    pub area_uuid: Option<Uuid>,
366    /// Start date
367    pub start_date: Option<NaiveDate>,
368    /// Deadline
369    pub deadline: Option<NaiveDate>,
370    /// Tags (as string names)
371    pub tags: Option<Vec<String>>,
372}
373
374/// Project update request
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct UpdateProjectRequest {
377    /// Project UUID
378    pub uuid: Uuid,
379    /// New title
380    pub title: Option<String>,
381    /// New notes
382    pub notes: Option<String>,
383    /// New area UUID
384    pub area_uuid: Option<Uuid>,
385    /// New start date
386    pub start_date: Option<NaiveDate>,
387    /// New deadline
388    pub deadline: Option<NaiveDate>,
389    /// New tags
390    pub tags: Option<Vec<String>>,
391}
392
393/// Area creation request
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct CreateAreaRequest {
396    /// Area title (required)
397    pub title: String,
398}
399
400/// Area update request
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct UpdateAreaRequest {
403    /// Area UUID
404    pub uuid: Uuid,
405    /// New title
406    pub title: String,
407}
408
409/// How to handle child tasks when completing/deleting a project
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
411pub enum ProjectChildHandling {
412    /// Return error if project has child tasks (default, safest)
413    #[serde(rename = "error")]
414    #[default]
415    Error,
416    /// Complete/delete all child tasks
417    #[serde(rename = "cascade")]
418    Cascade,
419    /// Move child tasks to inbox (orphan them)
420    #[serde(rename = "orphan")]
421    Orphan,
422}
423
424// ============================================================================
425// Bulk Operation Models
426// ============================================================================
427
428/// Request to move multiple tasks to a project or area
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct BulkMoveRequest {
431    /// Task UUIDs to move
432    pub task_uuids: Vec<Uuid>,
433    /// Target project UUID (optional)
434    pub project_uuid: Option<Uuid>,
435    /// Target area UUID (optional)
436    pub area_uuid: Option<Uuid>,
437}
438
439/// Request to update dates for multiple tasks
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct BulkUpdateDatesRequest {
442    /// Task UUIDs to update
443    pub task_uuids: Vec<Uuid>,
444    /// New start date (None means don't update)
445    pub start_date: Option<NaiveDate>,
446    /// New deadline (None means don't update)
447    pub deadline: Option<NaiveDate>,
448    /// Clear start date (set to NULL)
449    pub clear_start_date: bool,
450    /// Clear deadline (set to NULL)
451    pub clear_deadline: bool,
452}
453
454/// Request to complete multiple tasks
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct BulkCompleteRequest {
457    /// Task UUIDs to complete
458    pub task_uuids: Vec<Uuid>,
459}
460
461/// Request to delete multiple tasks
462#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct BulkDeleteRequest {
464    /// Task UUIDs to delete
465    pub task_uuids: Vec<Uuid>,
466}
467
468/// Result of a bulk operation
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct BulkOperationResult {
471    /// Whether the operation succeeded
472    pub success: bool,
473    /// Number of tasks processed
474    pub processed_count: usize,
475    /// Result message
476    pub message: String,
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use chrono::NaiveDate;
483
484    #[test]
485    fn test_task_status_serialization() {
486        let status = TaskStatus::Incomplete;
487        let serialized = serde_json::to_string(&status).unwrap();
488        assert_eq!(serialized, "\"incomplete\"");
489
490        let status = TaskStatus::Completed;
491        let serialized = serde_json::to_string(&status).unwrap();
492        assert_eq!(serialized, "\"completed\"");
493
494        let status = TaskStatus::Canceled;
495        let serialized = serde_json::to_string(&status).unwrap();
496        assert_eq!(serialized, "\"canceled\"");
497
498        let status = TaskStatus::Trashed;
499        let serialized = serde_json::to_string(&status).unwrap();
500        assert_eq!(serialized, "\"trashed\"");
501    }
502
503    #[test]
504    fn test_task_status_deserialization() {
505        let deserialized: TaskStatus = serde_json::from_str("\"incomplete\"").unwrap();
506        assert_eq!(deserialized, TaskStatus::Incomplete);
507
508        let deserialized: TaskStatus = serde_json::from_str("\"completed\"").unwrap();
509        assert_eq!(deserialized, TaskStatus::Completed);
510
511        let deserialized: TaskStatus = serde_json::from_str("\"canceled\"").unwrap();
512        assert_eq!(deserialized, TaskStatus::Canceled);
513
514        let deserialized: TaskStatus = serde_json::from_str("\"trashed\"").unwrap();
515        assert_eq!(deserialized, TaskStatus::Trashed);
516    }
517
518    #[test]
519    fn test_task_type_serialization() {
520        let task_type = TaskType::Todo;
521        let serialized = serde_json::to_string(&task_type).unwrap();
522        assert_eq!(serialized, "\"to-do\"");
523
524        let task_type = TaskType::Project;
525        let serialized = serde_json::to_string(&task_type).unwrap();
526        assert_eq!(serialized, "\"project\"");
527
528        let task_type = TaskType::Heading;
529        let serialized = serde_json::to_string(&task_type).unwrap();
530        assert_eq!(serialized, "\"heading\"");
531
532        let task_type = TaskType::Area;
533        let serialized = serde_json::to_string(&task_type).unwrap();
534        assert_eq!(serialized, "\"area\"");
535    }
536
537    #[test]
538    fn test_task_type_deserialization() {
539        let deserialized: TaskType = serde_json::from_str("\"to-do\"").unwrap();
540        assert_eq!(deserialized, TaskType::Todo);
541
542        let deserialized: TaskType = serde_json::from_str("\"project\"").unwrap();
543        assert_eq!(deserialized, TaskType::Project);
544
545        let deserialized: TaskType = serde_json::from_str("\"heading\"").unwrap();
546        assert_eq!(deserialized, TaskType::Heading);
547
548        let deserialized: TaskType = serde_json::from_str("\"area\"").unwrap();
549        assert_eq!(deserialized, TaskType::Area);
550    }
551
552    #[test]
553    fn test_task_creation() {
554        let uuid = Uuid::new_v4();
555        let now = Utc::now();
556        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
557        let deadline = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
558
559        let task = Task {
560            uuid,
561            title: "Test Task".to_string(),
562            task_type: TaskType::Todo,
563            status: TaskStatus::Incomplete,
564            notes: Some("Test notes".to_string()),
565            start_date: Some(start_date),
566            deadline: Some(deadline),
567            created: now,
568            modified: now,
569            stop_date: None,
570            project_uuid: None,
571            area_uuid: None,
572            parent_uuid: None,
573            tags: vec!["work".to_string(), "urgent".to_string()],
574            children: vec![],
575        };
576
577        assert_eq!(task.uuid, uuid);
578        assert_eq!(task.title, "Test Task");
579        assert_eq!(task.task_type, TaskType::Todo);
580        assert_eq!(task.status, TaskStatus::Incomplete);
581        assert_eq!(task.notes, Some("Test notes".to_string()));
582        assert_eq!(task.start_date, Some(start_date));
583        assert_eq!(task.deadline, Some(deadline));
584        assert_eq!(task.tags.len(), 2);
585        assert!(task.tags.contains(&"work".to_string()));
586        assert!(task.tags.contains(&"urgent".to_string()));
587    }
588
589    #[test]
590    fn test_task_serialization() {
591        let uuid = Uuid::new_v4();
592        let now = Utc::now();
593
594        let task = Task {
595            uuid,
596            title: "Test Task".to_string(),
597            task_type: TaskType::Todo,
598            status: TaskStatus::Incomplete,
599            notes: None,
600            start_date: None,
601            deadline: None,
602            created: now,
603            modified: now,
604            stop_date: None,
605            project_uuid: None,
606            area_uuid: None,
607            parent_uuid: None,
608            tags: vec![],
609            children: vec![],
610        };
611
612        let serialized = serde_json::to_string(&task).unwrap();
613        let deserialized: Task = serde_json::from_str(&serialized).unwrap();
614
615        assert_eq!(deserialized.uuid, task.uuid);
616        assert_eq!(deserialized.title, task.title);
617        assert_eq!(deserialized.task_type, task.task_type);
618        assert_eq!(deserialized.status, task.status);
619    }
620
621    #[test]
622    fn test_project_creation() {
623        let uuid = Uuid::new_v4();
624        let area_uuid = Uuid::new_v4();
625        let now = Utc::now();
626
627        let project = Project {
628            uuid,
629            title: "Test Project".to_string(),
630            notes: Some("Project notes".to_string()),
631            start_date: None,
632            deadline: None,
633            created: now,
634            modified: now,
635            area_uuid: Some(area_uuid),
636            tags: vec!["project".to_string()],
637            status: TaskStatus::Incomplete,
638            tasks: vec![],
639        };
640
641        assert_eq!(project.uuid, uuid);
642        assert_eq!(project.title, "Test Project");
643        assert_eq!(project.area_uuid, Some(area_uuid));
644        assert_eq!(project.status, TaskStatus::Incomplete);
645        assert_eq!(project.tags.len(), 1);
646    }
647
648    #[test]
649    fn test_project_serialization() {
650        let uuid = Uuid::new_v4();
651        let now = Utc::now();
652
653        let project = Project {
654            uuid,
655            title: "Test Project".to_string(),
656            notes: None,
657            start_date: None,
658            deadline: None,
659            created: now,
660            modified: now,
661            area_uuid: None,
662            tags: vec![],
663            status: TaskStatus::Incomplete,
664            tasks: vec![],
665        };
666
667        let serialized = serde_json::to_string(&project).unwrap();
668        let deserialized: Project = serde_json::from_str(&serialized).unwrap();
669
670        assert_eq!(deserialized.uuid, project.uuid);
671        assert_eq!(deserialized.title, project.title);
672        assert_eq!(deserialized.status, project.status);
673    }
674
675    #[test]
676    fn test_area_creation() {
677        let uuid = Uuid::new_v4();
678        let now = Utc::now();
679
680        let area = Area {
681            uuid,
682            title: "Test Area".to_string(),
683            notes: Some("Area notes".to_string()),
684            created: now,
685            modified: now,
686            tags: vec!["area".to_string()],
687            projects: vec![],
688        };
689
690        assert_eq!(area.uuid, uuid);
691        assert_eq!(area.title, "Test Area");
692        assert_eq!(area.notes, Some("Area notes".to_string()));
693        assert_eq!(area.tags.len(), 1);
694    }
695
696    #[test]
697    fn test_area_serialization() {
698        let uuid = Uuid::new_v4();
699        let now = Utc::now();
700
701        let area = Area {
702            uuid,
703            title: "Test Area".to_string(),
704            notes: None,
705            created: now,
706            modified: now,
707            tags: vec![],
708            projects: vec![],
709        };
710
711        let serialized = serde_json::to_string(&area).unwrap();
712        let deserialized: Area = serde_json::from_str(&serialized).unwrap();
713
714        assert_eq!(deserialized.uuid, area.uuid);
715        assert_eq!(deserialized.title, area.title);
716    }
717
718    #[test]
719    fn test_tag_creation() {
720        let uuid = Uuid::new_v4();
721        let parent_uuid = Uuid::new_v4();
722        let now = Utc::now();
723
724        let tag = Tag {
725            uuid,
726            title: "work".to_string(),
727            shortcut: Some("w".to_string()),
728            parent_uuid: Some(parent_uuid),
729            created: now,
730            modified: now,
731            usage_count: 5,
732            last_used: Some(now),
733        };
734
735        assert_eq!(tag.uuid, uuid);
736        assert_eq!(tag.title, "work");
737        assert_eq!(tag.shortcut, Some("w".to_string()));
738        assert_eq!(tag.parent_uuid, Some(parent_uuid));
739        assert_eq!(tag.usage_count, 5);
740        assert_eq!(tag.last_used, Some(now));
741    }
742
743    #[test]
744    fn test_tag_serialization() {
745        let uuid = Uuid::new_v4();
746        let now = Utc::now();
747
748        let tag = Tag {
749            uuid,
750            title: "test".to_string(),
751            shortcut: None,
752            parent_uuid: None,
753            created: now,
754            modified: now,
755            usage_count: 0,
756            last_used: None,
757        };
758
759        let serialized = serde_json::to_string(&tag).unwrap();
760        let deserialized: Tag = serde_json::from_str(&serialized).unwrap();
761
762        assert_eq!(deserialized.uuid, tag.uuid);
763        assert_eq!(deserialized.title, tag.title);
764        assert_eq!(deserialized.usage_count, tag.usage_count);
765    }
766
767    #[test]
768    fn test_create_task_request() {
769        let project_uuid = Uuid::new_v4();
770        let area_uuid = Uuid::new_v4();
771        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
772
773        let request = CreateTaskRequest {
774            title: "New Task".to_string(),
775            task_type: None,
776            notes: Some("Task notes".to_string()),
777            start_date: Some(start_date),
778            deadline: None,
779            project_uuid: Some(project_uuid),
780            area_uuid: Some(area_uuid),
781            parent_uuid: None,
782            tags: Some(vec!["new".to_string()]),
783            status: None,
784        };
785
786        assert_eq!(request.title, "New Task");
787        assert_eq!(request.project_uuid, Some(project_uuid));
788        assert_eq!(request.area_uuid, Some(area_uuid));
789        assert_eq!(request.start_date, Some(start_date));
790        assert_eq!(request.tags.as_ref().unwrap().len(), 1);
791    }
792
793    #[test]
794    fn test_create_task_request_serialization() {
795        let request = CreateTaskRequest {
796            title: "Test".to_string(),
797            task_type: None,
798            notes: None,
799            start_date: None,
800            deadline: None,
801            project_uuid: None,
802            area_uuid: None,
803            parent_uuid: None,
804            tags: None,
805            status: None,
806        };
807
808        let serialized = serde_json::to_string(&request).unwrap();
809        let deserialized: CreateTaskRequest = serde_json::from_str(&serialized).unwrap();
810
811        assert_eq!(deserialized.title, request.title);
812    }
813
814    #[test]
815    fn test_update_task_request() {
816        let uuid = Uuid::new_v4();
817
818        let request = UpdateTaskRequest {
819            uuid,
820            title: Some("Updated Title".to_string()),
821            notes: Some("Updated notes".to_string()),
822            start_date: None,
823            deadline: None,
824            status: Some(TaskStatus::Completed),
825            project_uuid: None,
826            area_uuid: None,
827            tags: Some(vec!["updated".to_string()]),
828        };
829
830        assert_eq!(request.uuid, uuid);
831        assert_eq!(request.title, Some("Updated Title".to_string()));
832        assert_eq!(request.status, Some(TaskStatus::Completed));
833        assert_eq!(request.tags, Some(vec!["updated".to_string()]));
834    }
835
836    #[test]
837    fn test_update_task_request_serialization() {
838        let uuid = Uuid::new_v4();
839
840        let request = UpdateTaskRequest {
841            uuid,
842            title: None,
843            notes: None,
844            start_date: None,
845            deadline: None,
846            status: None,
847            project_uuid: None,
848            area_uuid: None,
849            tags: None,
850        };
851
852        let serialized = serde_json::to_string(&request).unwrap();
853        let deserialized: UpdateTaskRequest = serde_json::from_str(&serialized).unwrap();
854
855        assert_eq!(deserialized.uuid, request.uuid);
856    }
857
858    #[test]
859    fn test_task_filters_default() {
860        let filters = TaskFilters::default();
861
862        assert!(filters.status.is_none());
863        assert!(filters.task_type.is_none());
864        assert!(filters.project_uuid.is_none());
865        assert!(filters.area_uuid.is_none());
866        assert!(filters.tags.is_none());
867        assert!(filters.start_date_from.is_none());
868        assert!(filters.start_date_to.is_none());
869        assert!(filters.deadline_from.is_none());
870        assert!(filters.deadline_to.is_none());
871        assert!(filters.search_query.is_none());
872        assert!(filters.limit.is_none());
873        assert!(filters.offset.is_none());
874    }
875
876    #[test]
877    fn test_task_filters_creation() {
878        let project_uuid = Uuid::new_v4();
879        let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
880
881        let filters = TaskFilters {
882            status: Some(TaskStatus::Incomplete),
883            task_type: Some(TaskType::Todo),
884            project_uuid: Some(project_uuid),
885            area_uuid: None,
886            tags: Some(vec!["work".to_string()]),
887            start_date_from: Some(start_date),
888            start_date_to: None,
889            deadline_from: None,
890            deadline_to: None,
891            search_query: Some("test".to_string()),
892            limit: Some(10),
893            offset: Some(0),
894        };
895
896        assert_eq!(filters.status, Some(TaskStatus::Incomplete));
897        assert_eq!(filters.task_type, Some(TaskType::Todo));
898        assert_eq!(filters.project_uuid, Some(project_uuid));
899        assert_eq!(filters.search_query, Some("test".to_string()));
900        assert_eq!(filters.limit, Some(10));
901        assert_eq!(filters.offset, Some(0));
902    }
903
904    #[test]
905    fn test_task_filters_serialization() {
906        let filters = TaskFilters {
907            status: Some(TaskStatus::Completed),
908            task_type: Some(TaskType::Project),
909            project_uuid: None,
910            area_uuid: None,
911            tags: None,
912            start_date_from: None,
913            start_date_to: None,
914            deadline_from: None,
915            deadline_to: None,
916            search_query: None,
917            limit: None,
918            offset: None,
919        };
920
921        let serialized = serde_json::to_string(&filters).unwrap();
922        let deserialized: TaskFilters = serde_json::from_str(&serialized).unwrap();
923
924        assert_eq!(deserialized.status, filters.status);
925        assert_eq!(deserialized.task_type, filters.task_type);
926    }
927
928    #[test]
929    fn test_task_status_equality() {
930        assert_eq!(TaskStatus::Incomplete, TaskStatus::Incomplete);
931        assert_ne!(TaskStatus::Incomplete, TaskStatus::Completed);
932        assert_ne!(TaskStatus::Completed, TaskStatus::Canceled);
933        assert_ne!(TaskStatus::Canceled, TaskStatus::Trashed);
934    }
935
936    #[test]
937    fn test_task_type_equality() {
938        assert_eq!(TaskType::Todo, TaskType::Todo);
939        assert_ne!(TaskType::Todo, TaskType::Project);
940        assert_ne!(TaskType::Project, TaskType::Heading);
941        assert_ne!(TaskType::Heading, TaskType::Area);
942    }
943
944    #[test]
945    fn test_task_with_children() {
946        let parent_uuid = Uuid::new_v4();
947        let child_uuid = Uuid::new_v4();
948        let now = Utc::now();
949
950        let child_task = Task {
951            uuid: child_uuid,
952            title: "Child Task".to_string(),
953            task_type: TaskType::Todo,
954            status: TaskStatus::Incomplete,
955            notes: None,
956            start_date: None,
957            deadline: None,
958            created: now,
959            modified: now,
960            stop_date: None,
961            project_uuid: None,
962            area_uuid: None,
963            parent_uuid: Some(parent_uuid),
964            tags: vec![],
965            children: vec![],
966        };
967
968        let parent_task = Task {
969            uuid: parent_uuid,
970            title: "Parent Task".to_string(),
971            task_type: TaskType::Heading,
972            status: TaskStatus::Incomplete,
973            notes: None,
974            start_date: None,
975            deadline: None,
976            created: now,
977            modified: now,
978            stop_date: None,
979            project_uuid: None,
980            area_uuid: None,
981            parent_uuid: None,
982            tags: vec![],
983            children: vec![child_task],
984        };
985
986        assert_eq!(parent_task.children.len(), 1);
987        assert_eq!(parent_task.children[0].parent_uuid, Some(parent_uuid));
988        assert_eq!(parent_task.children[0].title, "Child Task");
989    }
990
991    #[test]
992    fn test_project_with_tasks() {
993        let project_uuid = Uuid::new_v4();
994        let task_uuid = Uuid::new_v4();
995        let now = Utc::now();
996
997        let task = Task {
998            uuid: task_uuid,
999            title: "Project Task".to_string(),
1000            task_type: TaskType::Todo,
1001            status: TaskStatus::Incomplete,
1002            notes: None,
1003            start_date: None,
1004            deadline: None,
1005            created: now,
1006            modified: now,
1007            stop_date: None,
1008            project_uuid: Some(project_uuid),
1009            area_uuid: None,
1010            parent_uuid: None,
1011            tags: vec![],
1012            children: vec![],
1013        };
1014
1015        let project = Project {
1016            uuid: project_uuid,
1017            title: "Test Project".to_string(),
1018            notes: None,
1019            start_date: None,
1020            deadline: None,
1021            created: now,
1022            modified: now,
1023            area_uuid: None,
1024            tags: vec![],
1025            status: TaskStatus::Incomplete,
1026            tasks: vec![task],
1027        };
1028
1029        assert_eq!(project.tasks.len(), 1);
1030        assert_eq!(project.tasks[0].project_uuid, Some(project_uuid));
1031        assert_eq!(project.tasks[0].title, "Project Task");
1032    }
1033
1034    #[test]
1035    fn test_area_with_projects() {
1036        let area_uuid = Uuid::new_v4();
1037        let project_uuid = Uuid::new_v4();
1038        let now = Utc::now();
1039
1040        let project = Project {
1041            uuid: project_uuid,
1042            title: "Area Project".to_string(),
1043            notes: None,
1044            start_date: None,
1045            deadline: None,
1046            created: now,
1047            modified: now,
1048            area_uuid: Some(area_uuid),
1049            tags: vec![],
1050            status: TaskStatus::Incomplete,
1051            tasks: vec![],
1052        };
1053
1054        let area = Area {
1055            uuid: area_uuid,
1056            title: "Test Area".to_string(),
1057            notes: None,
1058            created: now,
1059            modified: now,
1060            tags: vec![],
1061            projects: vec![project],
1062        };
1063
1064        assert_eq!(area.projects.len(), 1);
1065        assert_eq!(area.projects[0].area_uuid, Some(area_uuid));
1066        assert_eq!(area.projects[0].title, "Area Project");
1067    }
1068}