package predicates_test import ( "testing" "time" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/task/predicates" ) // Helper to create a time pointer func timePtr(t time.Time) *time.Time { return &t } func TestIsCompleted(t *testing.T) { tests := []struct { name string task *models.Task expected bool }{ { name: "completed: NextDueDate nil with completions", task: &models.Task{ NextDueDate: nil, Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}, }, expected: true, }, { name: "not completed: NextDueDate set with completions", task: &models.Task{ NextDueDate: timePtr(time.Now().AddDate(0, 0, 7)), Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}, }, expected: false, }, { name: "not completed: NextDueDate nil without completions", task: &models.Task{ NextDueDate: nil, Completions: []models.TaskCompletion{}, }, expected: false, }, { name: "not completed: NextDueDate set without completions", task: &models.Task{ NextDueDate: timePtr(time.Now().AddDate(0, 0, 7)), Completions: []models.TaskCompletion{}, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.IsCompleted(tt.task) if result != tt.expected { t.Errorf("IsCompleted() = %v, expected %v", result, tt.expected) } }) } } func TestIsActive(t *testing.T) { tests := []struct { name string task *models.Task expected bool }{ { name: "active: not cancelled, not archived", task: &models.Task{IsCancelled: false, IsArchived: false}, expected: true, }, { name: "not active: cancelled", task: &models.Task{IsCancelled: true, IsArchived: false}, expected: false, }, { name: "not active: archived", task: &models.Task{IsCancelled: false, IsArchived: true}, expected: false, }, { name: "not active: both cancelled and archived", task: &models.Task{IsCancelled: true, IsArchived: true}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.IsActive(tt.task) if result != tt.expected { t.Errorf("IsActive() = %v, expected %v", result, tt.expected) } }) } } func TestIsInProgress(t *testing.T) { inProgressStatus := &models.TaskStatus{Name: "In Progress"} pendingStatus := &models.TaskStatus{Name: "Pending"} tests := []struct { name string task *models.Task expected bool }{ { name: "in progress: status is In Progress", task: &models.Task{Status: inProgressStatus}, expected: true, }, { name: "not in progress: status is Pending", task: &models.Task{Status: pendingStatus}, expected: false, }, { name: "not in progress: no status", task: &models.Task{Status: nil}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.IsInProgress(tt.task) if result != tt.expected { t.Errorf("IsInProgress() = %v, expected %v", result, tt.expected) } }) } } func TestEffectiveDate(t *testing.T) { now := time.Now() nextWeek := now.AddDate(0, 0, 7) nextMonth := now.AddDate(0, 1, 0) tests := []struct { name string task *models.Task expected *time.Time }{ { name: "prefers NextDueDate when both set", task: &models.Task{ NextDueDate: timePtr(nextWeek), DueDate: timePtr(nextMonth), }, expected: timePtr(nextWeek), }, { name: "falls back to DueDate when NextDueDate nil", task: &models.Task{ NextDueDate: nil, DueDate: timePtr(nextMonth), }, expected: timePtr(nextMonth), }, { name: "returns nil when both nil", task: &models.Task{ NextDueDate: nil, DueDate: nil, }, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.EffectiveDate(tt.task) if tt.expected == nil { if result != nil { t.Errorf("EffectiveDate() = %v, expected nil", result) } } else { if result == nil { t.Errorf("EffectiveDate() = nil, expected %v", tt.expected) } else if !result.Equal(*tt.expected) { t.Errorf("EffectiveDate() = %v, expected %v", result, tt.expected) } } }) } } func TestIsOverdue(t *testing.T) { now := time.Now().UTC() yesterday := now.AddDate(0, 0, -1) tomorrow := now.AddDate(0, 0, 1) tests := []struct { name string task *models.Task now time.Time expected bool }{ { name: "overdue: effective date in past", task: &models.Task{ NextDueDate: timePtr(yesterday), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, expected: true, }, { name: "not overdue: effective date in future", task: &models.Task{ NextDueDate: timePtr(tomorrow), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, expected: false, }, { name: "not overdue: cancelled task", task: &models.Task{ NextDueDate: timePtr(yesterday), IsCancelled: true, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, expected: false, }, { name: "not overdue: archived task", task: &models.Task{ NextDueDate: timePtr(yesterday), IsCancelled: false, IsArchived: true, Completions: []models.TaskCompletion{}, }, now: now, expected: false, }, { name: "not overdue: completed task (NextDueDate nil with completions)", task: &models.Task{ NextDueDate: nil, DueDate: timePtr(yesterday), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}, }, now: now, expected: false, }, { name: "not overdue: no due date", task: &models.Task{ NextDueDate: nil, DueDate: nil, IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, expected: false, }, { name: "overdue: uses DueDate when NextDueDate nil (no completions)", task: &models.Task{ NextDueDate: nil, DueDate: timePtr(yesterday), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.IsOverdue(tt.task, tt.now) if result != tt.expected { t.Errorf("IsOverdue() = %v, expected %v", result, tt.expected) } }) } } func TestIsDueSoon(t *testing.T) { now := time.Now().UTC() yesterday := now.AddDate(0, 0, -1) in5Days := now.AddDate(0, 0, 5) in60Days := now.AddDate(0, 0, 60) tests := []struct { name string task *models.Task now time.Time daysThreshold int expected bool }{ { name: "due soon: within threshold", task: &models.Task{ NextDueDate: timePtr(in5Days), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, daysThreshold: 30, expected: true, }, { name: "not due soon: beyond threshold", task: &models.Task{ NextDueDate: timePtr(in60Days), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, daysThreshold: 30, expected: false, }, { name: "not due soon: overdue (in past)", task: &models.Task{ NextDueDate: timePtr(yesterday), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, daysThreshold: 30, expected: false, }, { name: "not due soon: cancelled", task: &models.Task{ NextDueDate: timePtr(in5Days), IsCancelled: true, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, daysThreshold: 30, expected: false, }, { name: "not due soon: completed", task: &models.Task{ NextDueDate: nil, DueDate: timePtr(in5Days), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}, }, now: now, daysThreshold: 30, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.IsDueSoon(tt.task, tt.now, tt.daysThreshold) if result != tt.expected { t.Errorf("IsDueSoon() = %v, expected %v", result, tt.expected) } }) } } func TestIsUpcoming(t *testing.T) { now := time.Now().UTC() in5Days := now.AddDate(0, 0, 5) in60Days := now.AddDate(0, 0, 60) tests := []struct { name string task *models.Task now time.Time daysThreshold int expected bool }{ { name: "upcoming: beyond threshold", task: &models.Task{ NextDueDate: timePtr(in60Days), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, daysThreshold: 30, expected: true, }, { name: "upcoming: no due date", task: &models.Task{ NextDueDate: nil, DueDate: nil, IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, daysThreshold: 30, expected: true, }, { name: "not upcoming: within due soon threshold", task: &models.Task{ NextDueDate: timePtr(in5Days), IsCancelled: false, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, daysThreshold: 30, expected: false, }, { name: "not upcoming: cancelled", task: &models.Task{ NextDueDate: timePtr(in60Days), IsCancelled: true, IsArchived: false, Completions: []models.TaskCompletion{}, }, now: now, daysThreshold: 30, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.IsUpcoming(tt.task, tt.now, tt.daysThreshold) if result != tt.expected { t.Errorf("IsUpcoming() = %v, expected %v", result, tt.expected) } }) } } func TestHasCompletions(t *testing.T) { tests := []struct { name string task *models.Task expected bool }{ { name: "has completions", task: &models.Task{Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}}, expected: true, }, { name: "no completions", task: &models.Task{Completions: []models.TaskCompletion{}}, expected: false, }, { name: "nil completions", task: &models.Task{Completions: nil}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.HasCompletions(tt.task) if result != tt.expected { t.Errorf("HasCompletions() = %v, expected %v", result, tt.expected) } }) } } func TestIsRecurring(t *testing.T) { days := 7 tests := []struct { name string task *models.Task expected bool }{ { name: "recurring: frequency with days", task: &models.Task{Frequency: &models.TaskFrequency{Days: &days}}, expected: true, }, { name: "not recurring: frequency without days (one-time)", task: &models.Task{Frequency: &models.TaskFrequency{Days: nil}}, expected: false, }, { name: "not recurring: no frequency", task: &models.Task{Frequency: nil}, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := predicates.IsRecurring(tt.task) if result != tt.expected { t.Errorf("IsRecurring() = %v, expected %v", result, tt.expected) } }) } }