// // DataControllerCRUDTests.swift // ReflectTests // // Tests for DataController create, read, update, and delete operations. // import XCTest import SwiftData @testable import Reflect @MainActor final class DataControllerCreateTests: XCTestCase { var sut: DataController! override func setUp() { super.setUp() let schema = Schema([MoodEntryModel.self]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) sut = DataController(container: try! ModelContainer(for: schema, configurations: [config])) } override func tearDown() { sut = nil super.tearDown() } // MARK: - Phase 2: Create Tests func test_add_insertsEntry() { let date = makeDate(2024, 6, 15) sut.add(mood: .great, forDate: date, entryType: .listView) let entry = sut.getEntry(byDate: date) XCTAssertNotNil(entry, "Entry should exist after add") XCTAssertEqual(entry?.mood, .great) XCTAssertEqual(entry?.entryType, EntryType.listView.rawValue) } func test_add_replacesExistingForSameDate() { let date = makeDate(2024, 6, 15) sut.add(mood: .great, forDate: date, entryType: .listView) sut.add(mood: .bad, forDate: date, entryType: .widget) let allEntries = sut.getAllEntries(byDate: date) XCTAssertEqual(allEntries.count, 1, "Should have exactly 1 entry after 2 adds for same date") XCTAssertEqual(allEntries.first?.mood, .bad, "Should keep the latest mood") } func test_add_setsCorrectWeekday() { // June 15, 2024 is a Saturday (weekday = 7 in Gregorian) let date = makeDate(2024, 6, 15) sut.add(mood: .average, forDate: date, entryType: .listView) let entry = sut.getEntry(byDate: date) let expectedWeekday = Calendar.current.component(.weekday, from: date) XCTAssertEqual(entry?.weekDay, expectedWeekday) } func test_add_allMoodValues() { let moods: [Mood] = [.horrible, .bad, .average, .good, .great] for (i, mood) in moods.enumerated() { let date = makeDate(2024, 6, 10 + i) sut.add(mood: mood, forDate: date, entryType: .listView) let entry = sut.getEntry(byDate: date) XCTAssertEqual(entry?.mood, mood, "Mood \(mood) should persist and read back correctly") } } func test_addBatch_insertsMultiple() { let entries: [(mood: Mood, date: Date, entryType: EntryType)] = [ (.great, makeDate(2024, 6, 10), .listView), (.good, makeDate(2024, 6, 11), .widget), (.average, makeDate(2024, 6, 12), .watch), ] sut.addBatch(entries: entries) for (mood, date, _) in entries { let entry = sut.getEntry(byDate: date) XCTAssertNotNil(entry, "Entry for \(date) should exist") XCTAssertEqual(entry?.mood, mood) } } func test_addBatch_replacesExisting() { let date = makeDate(2024, 6, 10) sut.add(mood: .horrible, forDate: date, entryType: .listView) let batchEntries: [(mood: Mood, date: Date, entryType: EntryType)] = [ (.great, date, .widget), ] sut.addBatch(entries: batchEntries) let allEntries = sut.getAllEntries(byDate: date) XCTAssertEqual(allEntries.count, 1) XCTAssertEqual(allEntries.first?.mood, .great) } } // MARK: - Phase 3: Read Tests @MainActor final class DataControllerReadTests: XCTestCase { var sut: DataController! override func setUp() { super.setUp() let schema = Schema([MoodEntryModel.self]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) sut = DataController(container: try! ModelContainer(for: schema, configurations: [config])) } override func tearDown() { sut = nil super.tearDown() } func test_getEntry_emptyDB_returnsNil() { let entry = sut.getEntry(byDate: Date()) XCTAssertNil(entry) } func test_getEntry_boundaryExclusion() { // Insert entry at exactly midnight of Jan 2 (start of Jan 2) // Use hour=0 to place it precisely at the boundary let jan2Midnight = makeDate(2024, 1, 2, hour: 0) let entry = MoodEntryModel(forDate: jan2Midnight, mood: .great, entryType: .listView) sut.modelContext.insert(entry) sut.save() // Query for Jan 1 — should NOT find the Jan 2 entry // getEntry builds range [Jan 1 00:00, Jan 2 00:00] — with <= the midnight entry leaks in let jan1 = makeDate(2024, 1, 1) let result = sut.getEntry(byDate: jan1) XCTAssertNil(result, "Entry at midnight Jan 2 should NOT be returned when querying Jan 1") } func test_getData_filtersWeekdays() { // Add entries for a full week (Mon-Sun) // Jan 1, 2024 is a Monday (weekday=2) for i in 0..<7 { let date = Calendar.current.date(byAdding: .day, value: i, to: makeDate(2024, 1, 1))! sut.add(mood: .average, forDate: date, entryType: .listView) } // Filter to only weekdays 2 (Mon) and 6 (Fri) let startDate = makeDate(2024, 1, 1) let endDate = makeDate(2024, 1, 7) let results = sut.getData(startDate: startDate, endDate: endDate, includedDays: [2, 6]) XCTAssertEqual(results.count, 2, "Should return only Monday and Friday") } func test_getData_emptyIncludedDays_returnsAll() { for i in 0..<7 { let date = Calendar.current.date(byAdding: .day, value: i, to: makeDate(2024, 1, 1))! sut.add(mood: .average, forDate: date, entryType: .listView) } let startDate = makeDate(2024, 1, 1) let endDate = makeDate(2024, 1, 7) let results = sut.getData(startDate: startDate, endDate: endDate, includedDays: []) XCTAssertEqual(results.count, 7, "Empty includedDays should return all 7 entries") } func test_splitIntoYearMonth_groupsCorrectly() { // Add entries in different months sut.add(mood: .great, forDate: makeDate(2024, 1, 15), entryType: .listView) sut.add(mood: .good, forDate: makeDate(2024, 1, 20), entryType: .listView) sut.add(mood: .bad, forDate: makeDate(2024, 3, 5), entryType: .listView) let grouped = sut.splitIntoYearMonth(includedDays: []) XCTAssertEqual(grouped[2024]?[1]?.count, 2, "Jan 2024 should have 2 entries") XCTAssertEqual(grouped[2024]?[3]?.count, 1, "Mar 2024 should have 1 entry") XCTAssertNil(grouped[2024]?[2], "Feb 2024 should have no entries") } func test_earliestEntry() { sut.add(mood: .great, forDate: makeDate(2024, 6, 15), entryType: .listView) sut.add(mood: .bad, forDate: makeDate(2024, 1, 1), entryType: .listView) sut.add(mood: .good, forDate: makeDate(2024, 3, 10), entryType: .listView) let earliest = sut.earliestEntry XCTAssertNotNil(earliest) XCTAssertTrue(Calendar.current.isDate(earliest!.forDate, inSameDayAs: makeDate(2024, 1, 1))) } func test_latestEntry() { sut.add(mood: .great, forDate: makeDate(2024, 6, 15), entryType: .listView) sut.add(mood: .bad, forDate: makeDate(2024, 1, 1), entryType: .listView) sut.add(mood: .good, forDate: makeDate(2024, 3, 10), entryType: .listView) let latest = sut.latestEntry XCTAssertNotNil(latest) XCTAssertTrue(Calendar.current.isDate(latest!.forDate, inSameDayAs: makeDate(2024, 6, 15))) } } // MARK: - Phase 4: Update Tests @MainActor final class DataControllerUpdateTests: XCTestCase { var sut: DataController! override func setUp() { super.setUp() let schema = Schema([MoodEntryModel.self]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) sut = DataController(container: try! ModelContainer(for: schema, configurations: [config])) } override func tearDown() { sut = nil super.tearDown() } func test_update_changesMood() { let date = makeDate(2024, 6, 15) sut.add(mood: .bad, forDate: date, entryType: .listView) let result = sut.update(entryDate: date, withMood: .great) XCTAssertTrue(result) let entry = sut.getEntry(byDate: date) XCTAssertEqual(entry?.mood, .great) } func test_update_nonexistent_returnsFalse() { let result = sut.update(entryDate: makeDate(2024, 6, 15), withMood: .great) XCTAssertFalse(result) } func test_updateNotes_setsNotes() { let date = makeDate(2024, 6, 15) sut.add(mood: .good, forDate: date, entryType: .listView) let result = sut.updateNotes(forDate: date, notes: "Had a great day") XCTAssertTrue(result) let entry = sut.getEntry(byDate: date) XCTAssertEqual(entry?.notes, "Had a great day") } func test_updateNotes_nilClearsNotes() { let date = makeDate(2024, 6, 15) sut.add(mood: .good, forDate: date, entryType: .listView) sut.updateNotes(forDate: date, notes: "Some notes") let result = sut.updateNotes(forDate: date, notes: nil) XCTAssertTrue(result) let entry = sut.getEntry(byDate: date) XCTAssertNil(entry?.notes) } func test_updatePhoto_setsPhotoID() { let date = makeDate(2024, 6, 15) sut.add(mood: .good, forDate: date, entryType: .listView) let photoID = UUID() let result = sut.updatePhoto(forDate: date, photoID: photoID) XCTAssertTrue(result) let entry = sut.getEntry(byDate: date) XCTAssertEqual(entry?.photoID, photoID) } func test_update_refreshesTimestamp() { let date = makeDate(2024, 6, 15) sut.add(mood: .bad, forDate: date, entryType: .listView) let originalTimestamp = sut.getEntry(byDate: date)!.timestamp // Small delay so timestamp would differ let result = sut.update(entryDate: date, withMood: .great) XCTAssertTrue(result) let updatedEntry = sut.getEntry(byDate: date)! XCTAssertGreaterThanOrEqual(updatedEntry.timestamp, originalTimestamp, "Timestamp should be updated on mood change so removeDuplicates picks the right entry") } } // MARK: - Phase 5: Delete Tests @MainActor final class DataControllerDeleteTests: XCTestCase { var sut: DataController! override func setUp() { super.setUp() let schema = Schema([MoodEntryModel.self]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) sut = DataController(container: try! ModelContainer(for: schema, configurations: [config])) } override func tearDown() { sut = nil super.tearDown() } func test_clearDB_removesAll() { sut.add(mood: .great, forDate: makeDate(2024, 6, 10), entryType: .listView) sut.add(mood: .bad, forDate: makeDate(2024, 6, 11), entryType: .listView) sut.add(mood: .good, forDate: makeDate(2024, 6, 12), entryType: .listView) sut.clearDB() let all = sut.getData(startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: []) XCTAssertTrue(all.isEmpty, "clearDB should remove all entries") } func test_deleteAllEntries_removesOnlyTargetDate() { let date1 = makeDate(2024, 6, 10) let date2 = makeDate(2024, 6, 11) sut.add(mood: .great, forDate: date1, entryType: .listView) sut.add(mood: .bad, forDate: date2, entryType: .listView) sut.deleteAllEntries(forDate: date1) XCTAssertNil(sut.getEntry(byDate: date1), "Deleted date should be gone") XCTAssertNotNil(sut.getEntry(byDate: date2), "Other dates should be untouched") } func test_removeDuplicates_keepsBestEntry() { let date = makeDate(2024, 6, 15) // Manually insert 3 entries for the same date to simulate CloudKit conflict let entry1 = MoodEntryModel(forDate: date, mood: .missing, entryType: .filledInMissing) entry1.timestamp = makeDate(2024, 6, 15, hour: 8) sut.modelContext.insert(entry1) let entry2 = MoodEntryModel(forDate: date, mood: .great, entryType: .listView) entry2.timestamp = makeDate(2024, 6, 15, hour: 10) sut.modelContext.insert(entry2) let entry3 = MoodEntryModel(forDate: date, mood: .good, entryType: .listView) entry3.timestamp = makeDate(2024, 6, 15, hour: 9) sut.modelContext.insert(entry3) sut.save() let removed = sut.removeDuplicates() XCTAssertEqual(removed, 2, "Should remove 2 duplicates") let remaining = sut.getAllEntries(byDate: date) XCTAssertEqual(remaining.count, 1, "Should keep exactly 1 entry") // Should keep non-missing with most recent timestamp (entry2, .great at 10am) XCTAssertEqual(remaining.first?.mood, .great, "Should keep the best entry (non-missing, most recent)") } } // MARK: - Test Helpers func makeDate(_ year: Int, _ month: Int, _ day: Int, hour: Int = 12) -> Date { var components = DateComponents() components.year = year components.month = month components.day = day components.hour = hour components.minute = 0 components.second = 0 return Calendar.current.date(from: components)! }