// // CoreDataCareScheduleStorageTests.swift // PlantGuideTests // // Unit tests for CoreDataCareScheduleStorage - the Core Data implementation // of care schedule persistence. // import XCTest import CoreData @testable import PlantGuide // MARK: - Mock Core Data Stack /// Mock implementation of CoreDataStackProtocol for testing final class MockCoreDataStack: CoreDataStackProtocol, @unchecked Sendable { private let persistentContainer: NSPersistentContainer init() { // Create an in-memory persistent container for testing let modelURL = Bundle.main.url(forResource: "PlantGuideModel", withExtension: "momd") let managedObjectModel: NSManagedObjectModel if let modelURL = modelURL { managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)! } else { // Create a minimal model programmatically for testing if model file not found managedObjectModel = MockCoreDataStack.createTestModel() } persistentContainer = NSPersistentContainer(name: "PlantGuideModel", managedObjectModel: managedObjectModel) let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType description.shouldAddStoreAsynchronously = false persistentContainer.persistentStoreDescriptions = [description] persistentContainer.loadPersistentStores { _, error in if let error = error { fatalError("Failed to load in-memory store: \(error)") } } persistentContainer.viewContext.automaticallyMergesChangesFromParent = true } func viewContext() -> NSManagedObjectContext { return persistentContainer.viewContext } func newBackgroundContext() -> NSManagedObjectContext { let context = persistentContainer.newBackgroundContext() context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy context.automaticallyMergesChangesFromParent = true return context } func performBackgroundTask(_ block: @escaping @Sendable (NSManagedObjectContext) throws -> T) async throws -> T { let context = newBackgroundContext() return try await withCheckedThrowingContinuation { continuation in context.perform { do { let result = try block(context) if context.hasChanges { try context.save() } continuation.resume(returning: result) } catch { continuation.resume(throwing: error) } } } } func save(context: NSManagedObjectContext) throws { guard context.hasChanges else { return } try context.save() } func reset() throws { let context = viewContext() let entityNames = persistentContainer.managedObjectModel.entities.compactMap { $0.name } for entityName in entityNames { let fetchRequest = NSFetchRequest(entityName: entityName) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) try context.execute(deleteRequest) } try save(context: context) } /// Creates a minimal test model programmatically private static func createTestModel() -> NSManagedObjectModel { let model = NSManagedObjectModel() // CareScheduleMO Entity let scheduleEntity = NSEntityDescription() scheduleEntity.name = "CareScheduleMO" scheduleEntity.managedObjectClassName = NSStringFromClass(CareScheduleMO.self) let scheduleIdAttr = NSAttributeDescription() scheduleIdAttr.name = "id" scheduleIdAttr.attributeType = .UUIDAttributeType let plantIdAttr = NSAttributeDescription() plantIdAttr.name = "plantID" plantIdAttr.attributeType = .UUIDAttributeType let lightReqAttr = NSAttributeDescription() lightReqAttr.name = "lightRequirement" lightReqAttr.attributeType = .stringAttributeType let wateringAttr = NSAttributeDescription() wateringAttr.name = "wateringSchedule" wateringAttr.attributeType = .stringAttributeType let tempMinAttr = NSAttributeDescription() tempMinAttr.name = "temperatureMin" tempMinAttr.attributeType = .integer32AttributeType let tempMaxAttr = NSAttributeDescription() tempMaxAttr.name = "temperatureMax" tempMaxAttr.attributeType = .integer32AttributeType let fertilizerAttr = NSAttributeDescription() fertilizerAttr.name = "fertilizerSchedule" fertilizerAttr.attributeType = .stringAttributeType scheduleEntity.properties = [ scheduleIdAttr, plantIdAttr, lightReqAttr, wateringAttr, tempMinAttr, tempMaxAttr, fertilizerAttr ] // CareTaskMO Entity let taskEntity = NSEntityDescription() taskEntity.name = "CareTaskMO" taskEntity.managedObjectClassName = NSStringFromClass(CareTaskMO.self) let taskIdAttr = NSAttributeDescription() taskIdAttr.name = "id" taskIdAttr.attributeType = .UUIDAttributeType let taskPlantIdAttr = NSAttributeDescription() taskPlantIdAttr.name = "plantID" taskPlantIdAttr.attributeType = .UUIDAttributeType let typeAttr = NSAttributeDescription() typeAttr.name = "type" typeAttr.attributeType = .stringAttributeType let scheduledDateAttr = NSAttributeDescription() scheduledDateAttr.name = "scheduledDate" scheduledDateAttr.attributeType = .dateAttributeType let completedDateAttr = NSAttributeDescription() completedDateAttr.name = "completedDate" completedDateAttr.attributeType = .dateAttributeType completedDateAttr.isOptional = true let notesAttr = NSAttributeDescription() notesAttr.name = "notes" notesAttr.attributeType = .stringAttributeType taskEntity.properties = [ taskIdAttr, taskPlantIdAttr, typeAttr, scheduledDateAttr, completedDateAttr, notesAttr ] // Relationships let tasksRelation = NSRelationshipDescription() tasksRelation.name = "tasks" tasksRelation.destinationEntity = taskEntity tasksRelation.isOptional = true tasksRelation.deleteRule = .cascadeDeleteRule let scheduleRelation = NSRelationshipDescription() scheduleRelation.name = "careSchedule" scheduleRelation.destinationEntity = scheduleEntity scheduleRelation.maxCount = 1 scheduleRelation.isOptional = true tasksRelation.inverseRelationship = scheduleRelation scheduleRelation.inverseRelationship = tasksRelation scheduleEntity.properties.append(tasksRelation) taskEntity.properties.append(scheduleRelation) model.entities = [scheduleEntity, taskEntity] return model } } // MARK: - CoreDataCareScheduleStorageTests final class CoreDataCareScheduleStorageTests: XCTestCase { // MARK: - Properties private var sut: CoreDataCareScheduleStorage! private var mockCoreDataStack: MockCoreDataStack! // MARK: - Test Lifecycle override func setUp() { super.setUp() mockCoreDataStack = MockCoreDataStack() sut = CoreDataCareScheduleStorage(coreDataStack: mockCoreDataStack) } override func tearDown() { try? mockCoreDataStack.reset() sut = nil mockCoreDataStack = nil super.tearDown() } // MARK: - Test Helpers private func createTestSchedule( id: UUID = UUID(), plantID: UUID = UUID(), lightRequirement: LightRequirement = .partialShade, wateringSchedule: String = "Every 3 days", temperatureRange: ClosedRange = 60...80, fertilizerSchedule: String = "Monthly during growing season", tasks: [CareTask] = [] ) -> PlantCareSchedule { PlantCareSchedule( id: id, plantID: plantID, lightRequirement: lightRequirement, wateringSchedule: wateringSchedule, temperatureRange: temperatureRange, fertilizerSchedule: fertilizerSchedule, tasks: tasks ) } private func createTestTask( id: UUID = UUID(), plantID: UUID = UUID(), type: CareTaskType = .watering, scheduledDate: Date = Date(), completedDate: Date? = nil, notes: String = "" ) -> CareTask { CareTask( id: id, plantID: plantID, type: type, scheduledDate: scheduledDate, completedDate: completedDate, notes: notes ) } // MARK: - save() and fetch() Round-Trip Tests func testSaveAndFetch_WithValidSchedule_RoundTripsSuccessfully() async throws { // Given let plantID = UUID() let schedule = createTestSchedule( plantID: plantID, lightRequirement: .fullSun, wateringSchedule: "Every 2 days", temperatureRange: 65...75, fertilizerSchedule: "Weekly" ) // When try await sut.save(schedule) let fetchedSchedule = try await sut.fetch(for: plantID) // Then XCTAssertNotNil(fetchedSchedule) XCTAssertEqual(fetchedSchedule?.plantID, plantID) XCTAssertEqual(fetchedSchedule?.lightRequirement, .fullSun) XCTAssertEqual(fetchedSchedule?.wateringSchedule, "Every 2 days") XCTAssertEqual(fetchedSchedule?.temperatureRange, 65...75) XCTAssertEqual(fetchedSchedule?.fertilizerSchedule, "Weekly") } func testSaveAndFetch_WithTasks_RoundTripsSuccessfully() async throws { // Given let plantID = UUID() let task1 = createTestTask(plantID: plantID, type: .watering, notes: "Water thoroughly") let task2 = createTestTask(plantID: plantID, type: .fertilizing, notes: "Use organic fertilizer") let schedule = createTestSchedule(plantID: plantID, tasks: [task1, task2]) // When try await sut.save(schedule) let fetchedSchedule = try await sut.fetch(for: plantID) // Then XCTAssertNotNil(fetchedSchedule) XCTAssertEqual(fetchedSchedule?.tasks.count, 2) let taskTypes = fetchedSchedule?.tasks.map { $0.type } ?? [] XCTAssertTrue(taskTypes.contains(.watering)) XCTAssertTrue(taskTypes.contains(.fertilizing)) } func testSave_WhenScheduleAlreadyExists_UpdatesExistingSchedule() async throws { // Given let plantID = UUID() let originalSchedule = createTestSchedule( plantID: plantID, wateringSchedule: "Weekly" ) try await sut.save(originalSchedule) let updatedSchedule = createTestSchedule( plantID: plantID, wateringSchedule: "Every 2 days" ) // When try await sut.save(updatedSchedule) let fetchedSchedule = try await sut.fetch(for: plantID) // Then XCTAssertEqual(fetchedSchedule?.wateringSchedule, "Every 2 days") // Verify only one schedule exists for the plant let allSchedules = try await sut.fetchAll() let schedulesForPlant = allSchedules.filter { $0.plantID == plantID } XCTAssertEqual(schedulesForPlant.count, 1) } func testFetch_WhenScheduleDoesNotExist_ReturnsNil() async throws { // Given let nonExistentPlantID = UUID() // When let fetchedSchedule = try await sut.fetch(for: nonExistentPlantID) // Then XCTAssertNil(fetchedSchedule) } func testSaveAndFetch_WithAllLightRequirements_RoundTripsCorrectly() async throws { // Test each light requirement for lightReq in LightRequirement.allCases { // Given let plantID = UUID() let schedule = createTestSchedule(plantID: plantID, lightRequirement: lightReq) // When try await sut.save(schedule) let fetchedSchedule = try await sut.fetch(for: plantID) // Then XCTAssertEqual(fetchedSchedule?.lightRequirement, lightReq, "Failed for light requirement: \(lightReq)") } } // MARK: - fetchAll() Tests func testFetchAll_WhenEmpty_ReturnsEmptyArray() async throws { // When let schedules = try await sut.fetchAll() // Then XCTAssertTrue(schedules.isEmpty) } func testFetchAll_WithMultipleSchedules_ReturnsAllSchedules() async throws { // Given let schedule1 = createTestSchedule(plantID: UUID()) let schedule2 = createTestSchedule(plantID: UUID()) let schedule3 = createTestSchedule(plantID: UUID()) try await sut.save(schedule1) try await sut.save(schedule2) try await sut.save(schedule3) // When let schedules = try await sut.fetchAll() // Then XCTAssertEqual(schedules.count, 3) } func testFetchAll_SortsByPlantID() async throws { // Given let plantID1 = UUID() let plantID2 = UUID() let plantID3 = UUID() try await sut.save(createTestSchedule(plantID: plantID2)) try await sut.save(createTestSchedule(plantID: plantID1)) try await sut.save(createTestSchedule(plantID: plantID3)) // When let schedules = try await sut.fetchAll() // Then XCTAssertEqual(schedules.count, 3) // Results should be sorted by plantID (UUID string comparison) let plantIDs = schedules.map { $0.plantID } XCTAssertEqual(plantIDs, plantIDs.sorted { $0.uuidString < $1.uuidString }) } // MARK: - delete() Tests func testDelete_WhenScheduleExists_RemovesSchedule() async throws { // Given let plantID = UUID() let schedule = createTestSchedule(plantID: plantID) try await sut.save(schedule) // Verify schedule exists let fetchedBefore = try await sut.fetch(for: plantID) XCTAssertNotNil(fetchedBefore) // When try await sut.delete(for: plantID) // Then let fetchedAfter = try await sut.fetch(for: plantID) XCTAssertNil(fetchedAfter) } func testDelete_WhenScheduleDoesNotExist_ThrowsScheduleNotFound() async { // Given let nonExistentPlantID = UUID() // When/Then do { try await sut.delete(for: nonExistentPlantID) XCTFail("Expected scheduleNotFound error to be thrown") } catch let error as CareScheduleStorageError { switch error { case .scheduleNotFound(let id): XCTAssertEqual(id, nonExistentPlantID) default: XCTFail("Expected scheduleNotFound error, got \(error)") } } catch { XCTFail("Expected CareScheduleStorageError, got \(error)") } } func testDelete_AlsoDeletesAssociatedTasks() async throws { // Given let plantID = UUID() let task1 = createTestTask(plantID: plantID, type: .watering) let task2 = createTestTask(plantID: plantID, type: .fertilizing) let schedule = createTestSchedule(plantID: plantID, tasks: [task1, task2]) try await sut.save(schedule) // Verify tasks exist let tasksBefore = try await sut.fetchAllTasks() XCTAssertEqual(tasksBefore.count, 2) // When try await sut.delete(for: plantID) // Then - Tasks should also be deleted let tasksAfter = try await sut.fetchAllTasks() XCTAssertTrue(tasksAfter.isEmpty) } // MARK: - updateTask() Tests func testUpdateTask_WhenTaskExists_UpdatesTask() async throws { // Given let plantID = UUID() let taskID = UUID() let originalTask = CareTask( id: taskID, plantID: plantID, type: .watering, scheduledDate: Date(), completedDate: nil, notes: "Original notes" ) let schedule = createTestSchedule(plantID: plantID, tasks: [originalTask]) try await sut.save(schedule) // When let updatedTask = CareTask( id: taskID, plantID: plantID, type: .watering, scheduledDate: Date().addingTimeInterval(86400), // Tomorrow completedDate: Date(), notes: "Updated notes" ) try await sut.updateTask(updatedTask) // Then let allTasks = try await sut.fetchAllTasks() let fetchedTask = allTasks.first { $0.id == taskID } XCTAssertNotNil(fetchedTask) XCTAssertEqual(fetchedTask?.notes, "Updated notes") XCTAssertNotNil(fetchedTask?.completedDate) } func testUpdateTask_WhenTaskDoesNotExist_ThrowsTaskNotFound() async { // Given let nonExistentTaskID = UUID() let task = createTestTask(id: nonExistentTaskID) // When/Then do { try await sut.updateTask(task) XCTFail("Expected taskNotFound error to be thrown") } catch let error as CareScheduleStorageError { switch error { case .taskNotFound(let id): XCTAssertEqual(id, nonExistentTaskID) default: XCTFail("Expected taskNotFound error, got \(error)") } } catch { XCTFail("Expected CareScheduleStorageError, got \(error)") } } func testUpdateTask_UpdatesTaskType() async throws { // Given let plantID = UUID() let taskID = UUID() let task = CareTask( id: taskID, plantID: plantID, type: .watering, scheduledDate: Date(), notes: "" ) let schedule = createTestSchedule(plantID: plantID, tasks: [task]) try await sut.save(schedule) // When let updatedTask = CareTask( id: taskID, plantID: plantID, type: .fertilizing, scheduledDate: Date(), notes: "" ) try await sut.updateTask(updatedTask) // Then let allTasks = try await sut.fetchAllTasks() let fetchedTask = allTasks.first { $0.id == taskID } XCTAssertEqual(fetchedTask?.type, .fertilizing) } // MARK: - fetchUpcomingTasks() Tests func testFetchUpcomingTasks_ReturnsTasksWithinSpecifiedDays() async throws { // Given let plantID = UUID() let now = Date() // Task scheduled tomorrow (should be included) let tomorrowTask = CareTask( id: UUID(), plantID: plantID, type: .watering, scheduledDate: Calendar.current.date(byAdding: .day, value: 1, to: now)!, notes: "Tomorrow task" ) // Task scheduled in 3 days (should be included for 7 days) let threeDaysTask = CareTask( id: UUID(), plantID: plantID, type: .fertilizing, scheduledDate: Calendar.current.date(byAdding: .day, value: 3, to: now)!, notes: "Three days task" ) // Task scheduled in 10 days (should NOT be included for 7 days) let tenDaysTask = CareTask( id: UUID(), plantID: plantID, type: .pruning, scheduledDate: Calendar.current.date(byAdding: .day, value: 10, to: now)!, notes: "Ten days task" ) // Task in the past (should NOT be included) let pastTask = CareTask( id: UUID(), plantID: plantID, type: .repotting, scheduledDate: Calendar.current.date(byAdding: .day, value: -1, to: now)!, notes: "Past task" ) // Completed task (should NOT be included) let completedTask = CareTask( id: UUID(), plantID: plantID, type: .watering, scheduledDate: Calendar.current.date(byAdding: .day, value: 2, to: now)!, completedDate: now, notes: "Completed task" ) let schedule = createTestSchedule( plantID: plantID, tasks: [tomorrowTask, threeDaysTask, tenDaysTask, pastTask, completedTask] ) try await sut.save(schedule) // When let upcomingTasks = try await sut.fetchUpcomingTasks(days: 7) // Then XCTAssertEqual(upcomingTasks.count, 2) let notes = upcomingTasks.map { $0.notes } XCTAssertTrue(notes.contains("Tomorrow task")) XCTAssertTrue(notes.contains("Three days task")) XCTAssertFalse(notes.contains("Ten days task")) XCTAssertFalse(notes.contains("Past task")) XCTAssertFalse(notes.contains("Completed task")) } func testFetchUpcomingTasks_ReturnsTasksSortedByScheduledDate() async throws { // Given let plantID = UUID() let now = Date() let task3Days = createTestTask( plantID: plantID, scheduledDate: Calendar.current.date(byAdding: .day, value: 3, to: now)!, notes: "3 days" ) let task1Day = createTestTask( plantID: plantID, scheduledDate: Calendar.current.date(byAdding: .day, value: 1, to: now)!, notes: "1 day" ) let task2Days = createTestTask( plantID: plantID, scheduledDate: Calendar.current.date(byAdding: .day, value: 2, to: now)!, notes: "2 days" ) let schedule = createTestSchedule(plantID: plantID, tasks: [task3Days, task1Day, task2Days]) try await sut.save(schedule) // When let upcomingTasks = try await sut.fetchUpcomingTasks(days: 7) // Then XCTAssertEqual(upcomingTasks.count, 3) XCTAssertEqual(upcomingTasks[0].notes, "1 day") XCTAssertEqual(upcomingTasks[1].notes, "2 days") XCTAssertEqual(upcomingTasks[2].notes, "3 days") } func testFetchUpcomingTasks_WhenNoTasks_ReturnsEmptyArray() async throws { // When let upcomingTasks = try await sut.fetchUpcomingTasks(days: 7) // Then XCTAssertTrue(upcomingTasks.isEmpty) } func testFetchUpcomingTasks_WithZeroDays_ReturnsNoTasks() async throws { // Given let plantID = UUID() let task = createTestTask( plantID: plantID, scheduledDate: Date().addingTimeInterval(3600) // 1 hour from now ) let schedule = createTestSchedule(plantID: plantID, tasks: [task]) try await sut.save(schedule) // When let upcomingTasks = try await sut.fetchUpcomingTasks(days: 0) // Then XCTAssertTrue(upcomingTasks.isEmpty) } // MARK: - fetchAllTasks() Tests func testFetchAllTasks_ReturnsAllTasks() async throws { // Given let plantID1 = UUID() let plantID2 = UUID() let schedule1 = createTestSchedule( plantID: plantID1, tasks: [createTestTask(plantID: plantID1), createTestTask(plantID: plantID1)] ) let schedule2 = createTestSchedule( plantID: plantID2, tasks: [createTestTask(plantID: plantID2)] ) try await sut.save(schedule1) try await sut.save(schedule2) // When let allTasks = try await sut.fetchAllTasks() // Then XCTAssertEqual(allTasks.count, 3) } // MARK: - exists() Tests func testExists_WhenScheduleExists_ReturnsTrue() async throws { // Given let plantID = UUID() let schedule = createTestSchedule(plantID: plantID) try await sut.save(schedule) // When let exists = try await sut.exists(for: plantID) // Then XCTAssertTrue(exists) } func testExists_WhenScheduleDoesNotExist_ReturnsFalse() async throws { // Given let nonExistentPlantID = UUID() // When let exists = try await sut.exists(for: nonExistentPlantID) // Then XCTAssertFalse(exists) } // MARK: - markTaskCompleted() Tests func testMarkTaskCompleted_SetsCompletedDate() async throws { // Given let plantID = UUID() let taskID = UUID() let task = CareTask( id: taskID, plantID: plantID, type: .watering, scheduledDate: Date(), completedDate: nil, notes: "" ) let schedule = createTestSchedule(plantID: plantID, tasks: [task]) try await sut.save(schedule) // When try await sut.markTaskCompleted(taskID: taskID) // Then let allTasks = try await sut.fetchAllTasks() let completedTask = allTasks.first { $0.id == taskID } XCTAssertNotNil(completedTask?.completedDate) } func testMarkTaskCompleted_WhenTaskDoesNotExist_ThrowsTaskNotFound() async { // Given let nonExistentTaskID = UUID() // When/Then do { try await sut.markTaskCompleted(taskID: nonExistentTaskID) XCTFail("Expected taskNotFound error to be thrown") } catch let error as CareScheduleStorageError { switch error { case .taskNotFound(let id): XCTAssertEqual(id, nonExistentTaskID) default: XCTFail("Expected taskNotFound error, got \(error)") } } catch { XCTFail("Expected CareScheduleStorageError, got \(error)") } } // MARK: - fetchOverdueTasks() Tests func testFetchOverdueTasks_ReturnsOverdueIncompleteTasks() async throws { // Given let plantID = UUID() let now = Date() // Overdue task (past date, not completed) let overdueTask = createTestTask( plantID: plantID, type: .watering, scheduledDate: Calendar.current.date(byAdding: .day, value: -2, to: now)!, completedDate: nil, notes: "Overdue" ) // Past but completed (should NOT be included) let completedPastTask = createTestTask( plantID: plantID, type: .fertilizing, scheduledDate: Calendar.current.date(byAdding: .day, value: -1, to: now)!, completedDate: now, notes: "Completed past" ) // Future task (should NOT be included) let futureTask = createTestTask( plantID: plantID, type: .pruning, scheduledDate: Calendar.current.date(byAdding: .day, value: 1, to: now)!, notes: "Future" ) let schedule = createTestSchedule( plantID: plantID, tasks: [overdueTask, completedPastTask, futureTask] ) try await sut.save(schedule) // When let overdueTasks = try await sut.fetchOverdueTasks() // Then XCTAssertEqual(overdueTasks.count, 1) XCTAssertEqual(overdueTasks.first?.notes, "Overdue") } // MARK: - fetchTasks(for:) Tests func testFetchTasksForPlant_ReturnsOnlyTasksForSpecifiedPlant() async throws { // Given let plantID1 = UUID() let plantID2 = UUID() let schedule1 = createTestSchedule( plantID: plantID1, tasks: [ createTestTask(plantID: plantID1, notes: "Plant 1 Task 1"), createTestTask(plantID: plantID1, notes: "Plant 1 Task 2") ] ) let schedule2 = createTestSchedule( plantID: plantID2, tasks: [createTestTask(plantID: plantID2, notes: "Plant 2 Task")] ) try await sut.save(schedule1) try await sut.save(schedule2) // When let tasksForPlant1 = try await sut.fetchTasks(for: plantID1) // Then XCTAssertEqual(tasksForPlant1.count, 2) XCTAssertTrue(tasksForPlant1.allSatisfy { $0.plantID == plantID1 }) } // MARK: - getTaskStatistics() Tests func testGetTaskStatistics_ReturnsCorrectCounts() async throws { // Given let plantID = UUID() let now = Date() let tasks = [ // Upcoming (within 7 days) createTestTask( plantID: plantID, scheduledDate: Calendar.current.date(byAdding: .day, value: 2, to: now)! ), createTestTask( plantID: plantID, scheduledDate: Calendar.current.date(byAdding: .day, value: 5, to: now)! ), // Overdue createTestTask( plantID: plantID, scheduledDate: Calendar.current.date(byAdding: .day, value: -1, to: now)! ), createTestTask( plantID: plantID, scheduledDate: Calendar.current.date(byAdding: .day, value: -3, to: now)! ), createTestTask( plantID: plantID, scheduledDate: Calendar.current.date(byAdding: .day, value: -5, to: now)! ) ] let schedule = createTestSchedule(plantID: plantID, tasks: tasks) try await sut.save(schedule) // When let stats = try await sut.getTaskStatistics() // Then XCTAssertEqual(stats.upcoming, 2) XCTAssertEqual(stats.overdue, 3) } // MARK: - Error Description Tests func testCareScheduleStorageError_HasCorrectDescriptions() { // Test scheduleNotFound let scheduleNotFoundError = CareScheduleStorageError.scheduleNotFound(UUID()) XCTAssertNotNil(scheduleNotFoundError.errorDescription) XCTAssertTrue(scheduleNotFoundError.errorDescription?.contains("not found") ?? false) // Test taskNotFound let taskNotFoundError = CareScheduleStorageError.taskNotFound(UUID()) XCTAssertNotNil(taskNotFoundError.errorDescription) XCTAssertTrue(taskNotFoundError.errorDescription?.contains("not found") ?? false) // Test saveFailed let saveFailed = CareScheduleStorageError.saveFailed(NSError(domain: "test", code: 1)) XCTAssertNotNil(saveFailed.errorDescription) // Test fetchFailed let fetchFailed = CareScheduleStorageError.fetchFailed(NSError(domain: "test", code: 1)) XCTAssertNotNil(fetchFailed.errorDescription) // Test deleteFailed let deleteFailed = CareScheduleStorageError.deleteFailed(NSError(domain: "test", code: 1)) XCTAssertNotNil(deleteFailed.errorDescription) // Test updateFailed let updateFailed = CareScheduleStorageError.updateFailed(NSError(domain: "test", code: 1)) XCTAssertNotNil(updateFailed.errorDescription) // Test invalidData let invalidData = CareScheduleStorageError.invalidData("Test message") XCTAssertNotNil(invalidData.errorDescription) XCTAssertTrue(invalidData.errorDescription?.contains("Test message") ?? false) // Test entityNotFound let entityNotFound = CareScheduleStorageError.entityNotFound("TestEntity") XCTAssertNotNil(entityNotFound.errorDescription) XCTAssertTrue(entityNotFound.errorDescription?.contains("TestEntity") ?? false) } // MARK: - Protocol Conformance Tests func testCoreDataCareScheduleStorage_ConformsToProtocol() { // Then XCTAssertTrue(sut is CareScheduleRepositoryProtocol) } // MARK: - Edge Cases func testSaveAndFetch_WithEmptyTasks_WorksCorrectly() async throws { // Given let plantID = UUID() let schedule = createTestSchedule(plantID: plantID, tasks: []) // When try await sut.save(schedule) let fetchedSchedule = try await sut.fetch(for: plantID) // Then XCTAssertNotNil(fetchedSchedule) XCTAssertTrue(fetchedSchedule?.tasks.isEmpty ?? false) } func testSaveAndFetch_WithAllTaskTypes_WorksCorrectly() async throws { // Given let plantID = UUID() let tasks = CareTaskType.allCases.map { type in createTestTask(plantID: plantID, type: type) } let schedule = createTestSchedule(plantID: plantID, tasks: tasks) // When try await sut.save(schedule) let fetchedSchedule = try await sut.fetch(for: plantID) // Then XCTAssertEqual(fetchedSchedule?.tasks.count, CareTaskType.allCases.count) for type in CareTaskType.allCases { XCTAssertTrue(fetchedSchedule?.tasks.contains { $0.type == type } ?? false) } } func testConcurrentSaves_DoNotCorruptData() async throws { // Given let iterations = 10 // When await withTaskGroup(of: Void.self) { group in for i in 0..