package models import ( "testing" "time" ) func TestTaskToAtom(t *testing.T) { now := time.Now() task := Task{ ID: "task-123", Content: "Test task", Description: "Task description", DueDate: &now, Priority: 3, URL: "https://todoist.com/task/123", CreatedAt: now, IsRecurring: true, } atom := TaskToAtom(task) if atom.ID != "task-123" { t.Errorf("Expected ID 'task-123', got '%s'", atom.ID) } if atom.Title != "Test task" { t.Errorf("Expected title 'Test task', got '%s'", atom.Title) } if atom.Source != SourceTodoist { t.Errorf("Expected source Todoist, got '%s'", atom.Source) } if atom.Type != TypeTask { t.Errorf("Expected type Task, got '%s'", atom.Type) } if atom.Priority != 3 { t.Errorf("Expected priority 3, got %d", atom.Priority) } if atom.SourceIcon != "🔴" { t.Error("Expected red circle icon") } if !atom.IsRecurring { t.Error("Expected IsRecurring to be true") } } func TestTaskToAtom_PriorityClamping(t *testing.T) { // Test priority below 1 lowTask := Task{Priority: 0} lowAtom := TaskToAtom(lowTask) if lowAtom.Priority != 1 { t.Errorf("Priority should be clamped to 1, got %d", lowAtom.Priority) } // Test priority above 4 highTask := Task{Priority: 10} highAtom := TaskToAtom(highTask) if highAtom.Priority != 4 { t.Errorf("Priority should be clamped to 4, got %d", highAtom.Priority) } } func TestCardToAtom(t *testing.T) { now := time.Now() card := Card{ ID: "card-456", Name: "Test card", ListName: "To Do", DueDate: &now, URL: "https://trello.com/c/456", } atom := CardToAtom(card) if atom.ID != "card-456" { t.Errorf("Expected ID 'card-456', got '%s'", atom.ID) } if atom.Title != "Test card" { t.Errorf("Expected title 'Test card', got '%s'", atom.Title) } if atom.Description != "To Do" { t.Errorf("Expected description 'To Do', got '%s'", atom.Description) } if atom.Source != SourceTrello { t.Errorf("Expected source Trello, got '%s'", atom.Source) } if atom.Priority != 2 { t.Errorf("Expected default priority 2, got %d", atom.Priority) } if atom.SourceIcon != "📋" { t.Error("Expected clipboard icon") } } func TestMealToAtom(t *testing.T) { date := time.Now() meal := Meal{ ID: "meal-789", RecipeName: "Pasta", MealType: "dinner", Date: date, RecipeURL: "https://plantoeat.com/recipe/789", } atom := MealToAtom(meal) if atom.ID != "meal-789" { t.Errorf("Expected ID 'meal-789', got '%s'", atom.ID) } if atom.Title != "Pasta" { t.Errorf("Expected title 'Pasta', got '%s'", atom.Title) } if atom.Description != "dinner" { t.Errorf("Expected description 'dinner', got '%s'", atom.Description) } if atom.Source != SourceMeal { t.Errorf("Expected source Meal, got '%s'", atom.Source) } if atom.Type != TypeMeal { t.Errorf("Expected type Meal, got '%s'", atom.Type) } if atom.Priority != 1 { t.Errorf("Expected priority 1, got %d", atom.Priority) } } func TestAtom_ComputeUIFields(t *testing.T) { // Test nil due date t.Run("nil due date", func(t *testing.T) { atom := Atom{} atom.ComputeUIFields() // Should not panic and fields should remain default if atom.IsOverdue || atom.IsFuture || atom.HasSetTime { t.Error("Fields should be false for nil due date") } }) // Test with due date at midnight (no specific time) t.Run("midnight due date", func(t *testing.T) { now := time.Now() midnightTomorrow := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) atom := Atom{DueDate: &midnightTomorrow} atom.ComputeUIFields() if atom.HasSetTime { t.Error("HasSetTime should be false for midnight") } }) // Test with specific time t.Run("specific time", func(t *testing.T) { now := time.Now() withTime := time.Date(now.Year(), now.Month(), now.Day()+1, 14, 30, 0, 0, time.UTC) atom := Atom{DueDate: &withTime} atom.ComputeUIFields() if !atom.HasSetTime { t.Error("HasSetTime should be true for 14:30") } }) } func TestTimelineItem_ComputeDaySection(t *testing.T) { // Use UTC since that's the default display timezone when not configured tz := time.UTC now := time.Now().In(tz) today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, tz) tomorrow := today.AddDate(0, 0, 1) nextWeek := today.AddDate(0, 0, 7) yesterday := today.AddDate(0, 0, -1) tests := []struct { name string itemTime time.Time wantSection DaySection wantOverdue bool wantAllDay bool }{ { name: "today with specific time", itemTime: time.Date(now.Year(), now.Month(), now.Day(), 14, 30, 0, 0, tz), wantSection: DaySectionToday, wantOverdue: false, wantAllDay: false, }, { name: "today all day (midnight)", itemTime: today, wantSection: DaySectionToday, wantOverdue: false, wantAllDay: true, }, { name: "tomorrow", itemTime: tomorrow.Add(10 * time.Hour), wantSection: DaySectionTomorrow, wantOverdue: false, wantAllDay: false, }, { name: "later (next week)", itemTime: nextWeek, wantSection: DaySectionLater, wantOverdue: false, wantAllDay: true, }, { name: "overdue (yesterday)", itemTime: yesterday, wantSection: DaySectionToday, // Overdue items show in today section wantOverdue: true, wantAllDay: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { item := &TimelineItem{Time: tc.itemTime} item.ComputeDaySection(now) if item.DaySection != tc.wantSection { t.Errorf("Expected section %s, got %s", tc.wantSection, item.DaySection) } if item.IsOverdue != tc.wantOverdue { t.Errorf("Expected overdue=%v, got %v", tc.wantOverdue, item.IsOverdue) } if item.IsAllDay != tc.wantAllDay { t.Errorf("Expected allDay=%v, got %v", tc.wantAllDay, item.IsAllDay) } }) } } func TestCacheMetadata_IsCacheValid(t *testing.T) { t.Run("valid cache", func(t *testing.T) { cm := CacheMetadata{ LastFetch: time.Now(), TTLMinutes: 5, } if !cm.IsCacheValid() { t.Error("Cache should be valid") } }) t.Run("expired cache", func(t *testing.T) { cm := CacheMetadata{ LastFetch: time.Now().Add(-10 * time.Minute), TTLMinutes: 5, } if cm.IsCacheValid() { t.Error("Cache should be expired") } }) t.Run("zero TTL", func(t *testing.T) { cm := CacheMetadata{ LastFetch: time.Now().Add(-1 * time.Second), TTLMinutes: 0, } if cm.IsCacheValid() { t.Error("Cache with zero TTL should be expired") } }) }