// // DataManagerExtendedTests.swift // CaseraTests // // Extended unit tests covering TASK-005, TASK-012, THEME-001, TCOMP-003, and QA-002. // // IMPORTANT: DataManager-mutating suites are nested inside the DataLayerTests // serialized parent (defined in DataLayerTests.swift) via extension, so ALL // DataManager tests serialize together and avoid concurrent state interference. // import Testing import Foundation @testable import Casera import ComposeApp // MARK: - Extension of DataLayerTests (serialized parent in DataLayerTests.swift) extension DataLayerTests { // MARK: - TASK-005: Template Search Tests @Suite struct TemplateSearchTests { // Helper to build a TaskTemplate with the fields that searchTaskTemplates inspects. // categoryId and frequencyId are nullable Int in Kotlin; pass nil directly. private func makeTemplate( id: Int32, title: String, description: String = "", tags: [String] = [] ) -> TaskTemplate { TaskTemplate( id: id, title: title, description: description, categoryId: nil, category: nil, frequencyId: nil, frequency: nil, iconIos: "", iconAndroid: "", tags: tags, displayOrder: 0, isActive: true ) } @Test func searchWithSingleCharReturnsEmpty() { DataManager.shared.clear() DataManager.shared.setTaskTemplates(templates: [ makeTemplate(id: 1, title: "Appliance check") ]) let results = DataManager.shared.searchTaskTemplates(query: "A") #expect(results.isEmpty) DataManager.shared.clear() } @Test func searchWithTwoCharsMatchesTitles() { DataManager.shared.clear() DataManager.shared.setTaskTemplates(templates: [ makeTemplate(id: 1, title: "Plumbing repair"), makeTemplate(id: 2, title: "HVAC inspection"), makeTemplate(id: 3, title: "Lawn care") ]) let results = DataManager.shared.searchTaskTemplates(query: "Pl") #expect(results.count == 1) #expect(results.first?.title == "Plumbing repair") DataManager.shared.clear() } @Test func searchIsCaseInsensitive() { DataManager.shared.clear() DataManager.shared.setTaskTemplates(templates: [ makeTemplate(id: 1, title: "Plumbing repair"), makeTemplate(id: 2, title: "Electrical panel check") ]) let results = DataManager.shared.searchTaskTemplates(query: "plumbing") #expect(results.count == 1) #expect(results.first?.title == "Plumbing repair") DataManager.shared.clear() } @Test func searchMatchesDescription() { DataManager.shared.clear() DataManager.shared.setTaskTemplates(templates: [ makeTemplate(id: 1, title: "Pipe maintenance", description: "Fix water leaks and pipe corrosion"), makeTemplate(id: 2, title: "Roof inspection", description: "Check shingles and gutters") ]) let results = DataManager.shared.searchTaskTemplates(query: "water") #expect(results.count == 1) #expect(results.first?.title == "Pipe maintenance") DataManager.shared.clear() } @Test func searchMatchesTags() { DataManager.shared.clear() DataManager.shared.setTaskTemplates(templates: [ makeTemplate(id: 1, title: "Air filter replacement", tags: ["hvac", "filter", "air quality"]), makeTemplate(id: 2, title: "Lawn mowing", tags: ["landscaping", "outdoor"]) ]) let results = DataManager.shared.searchTaskTemplates(query: "hvac") #expect(results.count == 1) #expect(results.first?.title == "Air filter replacement") DataManager.shared.clear() } @Test func searchReturnsMaxTenResults() { DataManager.shared.clear() // Create 15 templates all matching "maintenance" var templates: [TaskTemplate] = [] for i in 1...15 { templates.append(makeTemplate(id: Int32(i), title: "maintenance task \(i)")) } DataManager.shared.setTaskTemplates(templates: templates) let results = DataManager.shared.searchTaskTemplates(query: "maintenance") #expect(results.count == 10) DataManager.shared.clear() } @Test func searchNoMatchReturnsEmpty() { DataManager.shared.clear() DataManager.shared.setTaskTemplates(templates: [ makeTemplate(id: 1, title: "Plumbing repair"), makeTemplate(id: 2, title: "Roof inspection") ]) let results = DataManager.shared.searchTaskTemplates(query: "xyz") #expect(results.isEmpty) DataManager.shared.clear() } @Test func emptyQueryReturnsEmpty() { DataManager.shared.clear() DataManager.shared.setTaskTemplates(templates: [ makeTemplate(id: 1, title: "Plumbing repair") ]) let results = DataManager.shared.searchTaskTemplates(query: "") #expect(results.isEmpty) DataManager.shared.clear() } } // MARK: - TASK-012: Remove Task Cache Updates @Suite struct RemoveTaskTests { @Test func allTasksIsNilAfterClear() { DataManager.shared.clear() let value = DataManager.shared.allTasks.value #expect(value == nil) } @Test func removeTaskOnNilAllTasksIsNoOp() { // When allTasks is nil, removeTask should not crash and allTasks remains nil. DataManager.shared.clear() DataManager.shared.removeTask(taskId: 42) let value = DataManager.shared.allTasks.value #expect(value == nil) DataManager.shared.clear() } @Test func tasksByResidenceIsEmptyAfterClear() { // After clear, tasksByResidence is empty — removeTask has no residence caches to update. // Calling removeTask with no tasks cached should be a no-op (no crash, allTasks stays nil). DataManager.shared.clear() // removeTask with empty caches must not throw or crash DataManager.shared.removeTask(taskId: 999) // allTasks should remain nil after a no-op removeTask on empty state #expect(DataManager.shared.allTasks.value == nil) } // NOTE: Full integration coverage for removeTask (removing from kanban columns and // residence caches) is exercised in the UI test suite (TaskIntegrationTests) where // real TaskColumnsResponse objects are constructed through the API layer. // Constructing TaskColumnsResponse directly from Swift requires bridging complex // Kotlin generics (Map for icons) that are impractical in unit tests. } // MARK: - THEME-001: Theme Persistence Tests @Suite struct ThemePersistenceTests { @Test func defaultThemeIdIsDefault() { // Explicitly set to "default" first since clear() does NOT reset themeId. DataManager.shared.setThemeId(id: "default") DataManager.shared.clear() let themeId = DataManager.shared.themeId.value as! String #expect(themeId == "default") } @Test func setThemeIdUpdatesValue() { DataManager.shared.clear() DataManager.shared.setThemeId(id: "ocean") let themeId = DataManager.shared.themeId.value as! String #expect(themeId == "ocean") // Restore to default so other tests are unaffected DataManager.shared.setThemeId(id: "default") } @Test func setThemeIdToMultipleThemes() { DataManager.shared.clear() DataManager.shared.setThemeId(id: "forest") let intermediate = DataManager.shared.themeId.value as! String #expect(intermediate == "forest") DataManager.shared.setThemeId(id: "midnight") let final_ = DataManager.shared.themeId.value as! String #expect(final_ == "midnight") // Restore DataManager.shared.setThemeId(id: "default") } @Test func clearDoesNotResetTheme() { // Per CLAUDE.md architecture, clear() does NOT reset themeId. // The Kotlin source confirms _themeId is absent from the clear() method. DataManager.shared.setThemeId(id: "ocean") DataManager.shared.clear() // Theme should be preserved after clear — verify it is still a non-empty string. let themeId = DataManager.shared.themeId.value as! String #expect(!themeId.isEmpty) // Restore DataManager.shared.setThemeId(id: "default") } @Test func themeIdIsPreservedAsOceanAfterClear() { // Stronger assertion: the exact value set before clear() survives clear(). DataManager.shared.setThemeId(id: "ocean") DataManager.shared.clear() let themeId = DataManager.shared.themeId.value as! String #expect(themeId == "ocean") // Restore DataManager.shared.setThemeId(id: "default") } } } // MARK: - TCOMP-003: Task Completion Form Validation // These tests use only ValidationHelpers (pure Swift, no DataManager mutation). struct TaskCompletionValidationTests { @Test func completedByFieldRequired() { let result = ValidationHelpers.validateRequired("", fieldName: "Completed by") #expect(!result.isValid) #expect(result.errorMessage == "Completed by is required") } @Test func completedByFieldWithValuePasses() { let result = ValidationHelpers.validateRequired("Trey Tartt", fieldName: "Completed by") #expect(result.isValid) #expect(result.errorMessage == nil) } @Test func completedByFieldWhitespaceOnlyFails() { let result = ValidationHelpers.validateRequired(" ", fieldName: "Completed by") #expect(!result.isValid) #expect(result.errorMessage == "Completed by is required") } } // MARK: - QA-002: JSON Unknown Fields Resilience // Swift's JSONDecoder ignores unknown keys by default — these tests confirm that behaviour // holds for WidgetDataManager.WidgetTask, which uses a custom CodingKeys enum. struct JSONUnknownFieldsResilienceTests { @Test func widgetTaskDecodesWithExtraFields() throws { let json = """ { "id": 1, "title": "Test", "description": null, "priority": "high", "in_progress": false, "due_date": null, "category": "test", "residence_name": "Home", "is_overdue": false, "is_due_within_7_days": false, "is_due_8_to_30_days": false, "unknown_field": "should be ignored", "another_unknown": 42 } """.data(using: .utf8)! let task = try JSONDecoder().decode(WidgetDataManager.WidgetTask.self, from: json) #expect(task.id == 1) #expect(task.title == "Test") #expect(task.priority == "high") #expect(task.isOverdue == false) #expect(task.isDueWithin7Days == false) #expect(task.isDue8To30Days == false) #expect(task.residenceName == "Home") } @Test func widgetTaskIgnoresUnknownNestedObjects() throws { let json = """ { "id": 99, "title": "Nested unknown test", "description": "Some description", "priority": "low", "in_progress": true, "due_date": "2026-03-01", "category": "plumbing", "residence_name": null, "is_overdue": true, "is_due_within_7_days": false, "is_due_8_to_30_days": false, "unknown_nested": { "key1": "value1", "key2": 123, "key3": true }, "unknown_array": [1, 2, 3] } """.data(using: .utf8)! let task = try JSONDecoder().decode(WidgetDataManager.WidgetTask.self, from: json) #expect(task.id == 99) #expect(task.title == "Nested unknown test") #expect(task.description == "Some description") #expect(task.inProgress == true) #expect(task.dueDate == "2026-03-01") #expect(task.category == "plumbing") #expect(task.residenceName == nil) #expect(task.isOverdue == true) } }