// // TaskMetricsTests.swift // CaseraTests // // Unit tests for WidgetDataManager.TaskMetrics and task categorization logic. // import Testing @testable import iosApp // MARK: - Column Name Constants Tests struct ColumnNameConstantsTests { @Test func overdueColumnName() { #expect(WidgetDataManager.overdueColumn == "overdue_tasks") } @Test func dueWithin7DaysColumnName() { #expect(WidgetDataManager.dueWithin7DaysColumn == "due_soon_tasks") } @Test func due8To30DaysColumnName() { #expect(WidgetDataManager.due8To30DaysColumn == "upcoming_tasks") } @Test func completedColumnName() { #expect(WidgetDataManager.completedColumn == "completed_tasks") } @Test func cancelledColumnName() { #expect(WidgetDataManager.cancelledColumn == "cancelled_tasks") } @Test func allColumnNamesUnique() { let columns = [ WidgetDataManager.overdueColumn, WidgetDataManager.dueWithin7DaysColumn, WidgetDataManager.due8To30DaysColumn, WidgetDataManager.completedColumn, WidgetDataManager.cancelledColumn ] #expect(Set(columns).count == columns.count) } } // MARK: - TaskMetrics Calculation Tests struct TaskMetricsCalculationTests { private func makeTask( id: Int, isOverdue: Bool = false, isDueWithin7Days: Bool = false, isDue8To30Days: Bool = false ) -> WidgetDataManager.WidgetTask { WidgetDataManager.WidgetTask( id: id, title: "Task \(id)", description: nil, priority: "medium", inProgress: false, dueDate: nil, category: "maintenance", residenceName: "Home", isOverdue: isOverdue, isDueWithin7Days: isDueWithin7Days, isDue8To30Days: isDue8To30Days ) } @Test func emptyArrayReturnsZeroCounts() { let metrics = WidgetDataManager.calculateMetrics(from: []) #expect(metrics.totalCount == 0) #expect(metrics.overdueCount == 0) #expect(metrics.upcoming7Days == 0) #expect(metrics.upcoming30Days == 0) } @Test func allOverdueTasks() { let tasks = [ makeTask(id: 1, isOverdue: true), makeTask(id: 2, isOverdue: true), makeTask(id: 3, isOverdue: true) ] let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 3) #expect(metrics.overdueCount == 3) #expect(metrics.upcoming7Days == 0) #expect(metrics.upcoming30Days == 0) } @Test func allDueWithin7DaysTasks() { let tasks = [ makeTask(id: 1, isDueWithin7Days: true), makeTask(id: 2, isDueWithin7Days: true), makeTask(id: 3, isDueWithin7Days: true), makeTask(id: 4, isDueWithin7Days: true) ] let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 4) #expect(metrics.overdueCount == 0) #expect(metrics.upcoming7Days == 4) #expect(metrics.upcoming30Days == 0) } @Test func allDue8To30DaysTasks() { let tasks = [ makeTask(id: 1, isDue8To30Days: true), makeTask(id: 2, isDue8To30Days: true) ] let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 2) #expect(metrics.overdueCount == 0) #expect(metrics.upcoming7Days == 0) #expect(metrics.upcoming30Days == 2) } @Test func mixedTaskCategories() { let tasks = [ makeTask(id: 1, isOverdue: true), makeTask(id: 2, isOverdue: true), makeTask(id: 3, isDueWithin7Days: true), makeTask(id: 4, isDueWithin7Days: true), makeTask(id: 5, isDueWithin7Days: true), makeTask(id: 6, isDue8To30Days: true) ] let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 6) #expect(metrics.overdueCount == 2) #expect(metrics.upcoming7Days == 3) #expect(metrics.upcoming30Days == 1) } @Test func tasksWithNoFlagsCountedInTotalOnly() { let tasks = [ makeTask(id: 1), makeTask(id: 2), makeTask(id: 3, isOverdue: true) ] let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 3) #expect(metrics.overdueCount == 1) #expect(metrics.upcoming7Days == 0) #expect(metrics.upcoming30Days == 0) } @Test func singleTask() { let tasks = [makeTask(id: 1, isOverdue: true)] let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 1) #expect(metrics.overdueCount == 1) } @Test func largeDataset() { var tasks: [WidgetDataManager.WidgetTask] = [] for i in 1...50 { tasks.append(makeTask(id: i, isOverdue: true)) } for i in 51...150 { tasks.append(makeTask(id: i, isDueWithin7Days: true)) } for i in 151...225 { tasks.append(makeTask(id: i, isDue8To30Days: true)) } for i in 226...250 { tasks.append(makeTask(id: i)) } let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 250) #expect(metrics.overdueCount == 50) #expect(metrics.upcoming7Days == 100) #expect(metrics.upcoming30Days == 75) } @Test func orderDoesNotAffectMetrics() { let tasks1 = [ makeTask(id: 1, isOverdue: true), makeTask(id: 2, isDueWithin7Days: true), makeTask(id: 3, isDue8To30Days: true) ] let tasks2 = [ makeTask(id: 3, isDue8To30Days: true), makeTask(id: 1, isOverdue: true), makeTask(id: 2, isDueWithin7Days: true) ] let m1 = WidgetDataManager.calculateMetrics(from: tasks1) let m2 = WidgetDataManager.calculateMetrics(from: tasks2) #expect(m1.totalCount == m2.totalCount) #expect(m1.overdueCount == m2.overdueCount) #expect(m1.upcoming7Days == m2.upcoming7Days) #expect(m1.upcoming30Days == m2.upcoming30Days) } } // MARK: - TaskMetrics Struct Tests struct TaskMetricsStructTests { @Test func initializesWithCorrectValues() { let metrics = WidgetDataManager.TaskMetrics( totalCount: 10, overdueCount: 2, upcoming7Days: 3, upcoming30Days: 5 ) #expect(metrics.totalCount == 10) #expect(metrics.overdueCount == 2) #expect(metrics.upcoming7Days == 3) #expect(metrics.upcoming30Days == 5) } @Test func allowsZeroValues() { let metrics = WidgetDataManager.TaskMetrics( totalCount: 0, overdueCount: 0, upcoming7Days: 0, upcoming30Days: 0 ) #expect(metrics.totalCount == 0) #expect(metrics.overdueCount == 0) #expect(metrics.upcoming7Days == 0) #expect(metrics.upcoming30Days == 0) } } // MARK: - WidgetTask Codable Tests struct WidgetTaskCodableTests { @Test func decodesFromJSON() throws { let json = """ { "id": 123, "title": "Test Task", "description": "Desc", "priority": "high", "in_progress": true, "due_date": "2024-12-25", "category": "plumbing", "residence_name": "Home", "is_overdue": false, "is_due_within_7_days": true, "is_due_8_to_30_days": false } """.data(using: .utf8)! let task = try JSONDecoder().decode(WidgetDataManager.WidgetTask.self, from: json) #expect(task.id == 123) #expect(task.title == "Test Task") #expect(task.description == "Desc") #expect(task.priority == "high") #expect(task.inProgress == true) #expect(task.dueDate == "2024-12-25") #expect(task.category == "plumbing") #expect(task.residenceName == "Home") #expect(task.isOverdue == false) #expect(task.isDueWithin7Days == true) #expect(task.isDue8To30Days == false) } @Test func encodesToJSON() throws { let task = WidgetDataManager.WidgetTask( id: 456, title: "Encode Test", description: nil, priority: "medium", inProgress: false, dueDate: "2024-06-01", category: "hvac", residenceName: nil, isOverdue: true, isDueWithin7Days: false, isDue8To30Days: false ) let data = try JSONEncoder().encode(task) let json = String(data: data, encoding: .utf8)! #expect(json.contains("\"id\":456")) #expect(json.contains("\"is_overdue\":true")) #expect(json.contains("\"in_progress\":false")) } @Test func roundTripsJSON() throws { let original = WidgetDataManager.WidgetTask( id: 789, title: "Round Trip", description: "Testing", priority: "low", inProgress: true, dueDate: "2024-03-15", category: "landscaping", residenceName: "Cabin", isOverdue: false, isDueWithin7Days: true, isDue8To30Days: false ) let data = try JSONEncoder().encode(original) let decoded = try JSONDecoder().decode(WidgetDataManager.WidgetTask.self, from: data) #expect(decoded.id == original.id) #expect(decoded.title == original.title) #expect(decoded.description == original.description) #expect(decoded.inProgress == original.inProgress) #expect(decoded.isOverdue == original.isOverdue) #expect(decoded.isDueWithin7Days == original.isDueWithin7Days) #expect(decoded.isDue8To30Days == original.isDue8To30Days) } @Test func handlesNilOptionalFields() throws { let json = """ { "id": 1, "title": "Minimal", "description": null, "priority": null, "in_progress": false, "due_date": null, "category": null, "residence_name": null, "is_overdue": false, "is_due_within_7_days": false, "is_due_8_to_30_days": false } """.data(using: .utf8)! let task = try JSONDecoder().decode(WidgetDataManager.WidgetTask.self, from: json) #expect(task.id == 1) #expect(task.description == nil) #expect(task.priority == nil) #expect(task.dueDate == nil) #expect(task.category == nil) #expect(task.residenceName == nil) } } // MARK: - Realistic Scenario Tests struct RealisticScenarioTests { private func makeTask( id: Int, title: String, isOverdue: Bool = false, isDueWithin7Days: Bool = false, isDue8To30Days: Bool = false ) -> WidgetDataManager.WidgetTask { WidgetDataManager.WidgetTask( id: id, title: title, description: nil, priority: "medium", inProgress: false, dueDate: nil, category: "maintenance", residenceName: "Home", isOverdue: isOverdue, isDueWithin7Days: isDueWithin7Days, isDue8To30Days: isDue8To30Days ) } @Test func newUserWithNoTasks() { let metrics = WidgetDataManager.calculateMetrics(from: []) #expect(metrics.totalCount == 0) #expect(metrics.overdueCount == 0) } @Test func userWithOverdueTasks() { let tasks = [ makeTask(id: 1, title: "HVAC maintenance", isOverdue: true), makeTask(id: 2, title: "Roof inspection", isOverdue: true), makeTask(id: 3, title: "Weekly lawn care", isDueWithin7Days: true), makeTask(id: 4, title: "Monthly pest control", isDue8To30Days: true) ] let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 4) #expect(metrics.overdueCount == 2) #expect(metrics.upcoming7Days == 1) #expect(metrics.upcoming30Days == 1) } @Test func typicalHomeowner() { let tasks = [ makeTask(id: 1, title: "Replace water heater anode", isOverdue: true), makeTask(id: 2, title: "Mow lawn", isDueWithin7Days: true), makeTask(id: 3, title: "Take out trash", isDueWithin7Days: true), makeTask(id: 4, title: "Water plants", isDueWithin7Days: true), makeTask(id: 5, title: "Change air filter", isDue8To30Days: true), makeTask(id: 6, title: "Clean dryer vent", isDue8To30Days: true), makeTask(id: 7, title: "Organize garage"), makeTask(id: 8, title: "Paint fence") ] let metrics = WidgetDataManager.calculateMetrics(from: tasks) #expect(metrics.totalCount == 8) #expect(metrics.overdueCount == 1) #expect(metrics.upcoming7Days == 3) #expect(metrics.upcoming30Days == 2) } }