import XCTest @testable import MealMood /// Regression suite for "the watch showed Wednesday's meal on Saturday". /// /// The snapshot the iPhone sends carries an `isToday` flag, which is true only /// at the moment it was built. The watch outlives that moment — it keeps the /// last snapshot in its app group — so "today" has to be resolved when the view /// renders, never read from the payload. final class WatchWeekPayloadTests: XCTestCase { private let calendar = Calendar(identifier: .gregorian) private func date(_ year: Int, _ month: Int, _ day: Int) -> Date { calendar.date(from: DateComponents(year: year, month: month, day: day, hour: 12))! } private func makePayload(weekStart: Date, todayFlagOn flaggedDay: Int?) -> WatchWeekPayload { let days = (0...6).map { offset in WatchWeekPayload.Day( dayOfWeek: offset, title: "Day \(offset)", isToday: offset == flaggedDay, date: calendar.date(byAdding: .day, value: offset, to: weekStart)!, meals: [ WatchWeekPayload.Meal(type: "dinner", label: "Dinner", name: "Dish \(offset)") ] ) } return WatchWeekPayload( weekTitle: "Week", days: days, updatedAt: weekStart, emptyMealText: "Not planned", emptyDayText: "Nothing planned for today", staleText: "Open MealMood on your iPhone" ) } // MARK: - The reported bug func testSaturdayShowsSaturdayEvenIfTheSnapshotWasBuiltOnWednesday() { let monday = date(2026, 9, 7) // Snapshot built on Wednesday: that's the day it flagged as "today". let payload = makePayload(weekStart: monday, todayFlagOn: 2) let saturday = date(2026, 9, 12) let today = payload.currentDay(now: saturday, calendar: calendar) XCTAssertEqual(today?.dayOfWeek, 5, "Saturday must win over the frozen Wednesday flag") XCTAssertEqual(today?.meals.first?.name, "Dish 5") XCTAssertFalse(payload.isStale(now: saturday, calendar: calendar)) } func testSnapshotFromAnotherWeekHasNoToday() { let lastMonday = date(2026, 9, 7) let payload = makePayload(weekStart: lastMonday, todayFlagOn: 2) let nextSaturday = date(2026, 9, 19) XCTAssertNil(payload.currentDay(now: nextSaturday, calendar: calendar)) XCTAssertTrue(payload.isStale(now: nextSaturday, calendar: calendar), "showing last week's meals as today's is exactly what must not happen") } func testEachDayKnowsWhetherItIsToday() { let monday = date(2026, 9, 7) let payload = makePayload(weekStart: monday, todayFlagOn: 2) let thursday = date(2026, 9, 10) XCTAssertTrue(payload.days[3].isCurrentDay(now: thursday, calendar: calendar)) XCTAssertFalse(payload.days[2].isCurrentDay(now: thursday, calendar: calendar), "the day flagged when the snapshot was built is not today any more") } // MARK: - Empty states func testDayWithNoDishesIsEmpty() { let empty = WatchWeekPayload.Day( dayOfWeek: 1, title: "Tue", isToday: true, date: date(2026, 9, 8), meals: [ WatchWeekPayload.Meal(type: "lunch", label: "Lunch", name: nil), WatchWeekPayload.Meal(type: "dinner", label: "Dinner", name: nil) ] ) XCTAssertTrue(empty.isEmpty) let planned = WatchWeekPayload.Day( dayOfWeek: 1, title: "Tue", isToday: true, date: date(2026, 9, 8), meals: [ WatchWeekPayload.Meal(type: "lunch", label: "Lunch", name: nil), WatchWeekPayload.Meal(type: "dinner", label: "Dinner", name: "Tortilla") ] ) XCTAssertFalse(planned.isEmpty, "one planned meal is enough for the day not to be empty") } func testPayloadCarriesTheLocalizedEmptyTexts() { // The watch bundle has no strings of its own, so these have to travel. let payload = makePayload(weekStart: date(2026, 9, 7), todayFlagOn: 0) XCTAssertEqual(payload.emptyMealText, "Not planned") XCTAssertEqual(payload.emptyDayText, "Nothing planned for today") XCTAssertEqual(payload.staleText, "Open MealMood on your iPhone") } // MARK: - Compatibility with snapshots already on people's watches func testSnapshotWrittenBeforeTheFixStillDecodes() throws { // Shape written by 2.1.0: no `date`, no texts. Stamped inside the // current week, which is when its weekday offsets still mean something. let wednesday = date(2026, 9, 9) let json = """ { "weekTitle": "7 – 13 sep", "updatedAt": \(wednesday.timeIntervalSinceReferenceDate), "days": [ {"dayOfWeek": 0, "title": "Mon 7", "isToday": false, "meals": [{"type": "dinner", "label": "Dinner", "name": "Pizza"}]}, {"dayOfWeek": 2, "title": "Wed 9", "isToday": true, "meals": [{"type": "dinner", "label": "Dinner", "name": "Salmon"}]} ] } """ let payload = try XCTUnwrap(WatchWeekPayload.decode(Data(json.utf8))) XCTAssertEqual(payload.days.count, 2) XCTAssertNil(payload.days[0].date) XCTAssertNil(payload.emptyMealText, "old snapshots have no texts; the UI falls back") // Without dates it can only fall back to the weekday offset. XCTAssertEqual(payload.currentDay(now: wednesday, calendar: calendar)?.dayOfWeek, 2) } func testDatelessSnapshotFromAnotherWeekIsStale() throws { // The watch that reported the bug had one of these: no dates, built in a // previous week. Trusting its weekday offsets is what showed the wrong // day, so it now counts as stale instead. let lastWednesday = date(2026, 9, 2) let json = """ { "weekTitle": "31 ago – 6 sep", "updatedAt": \(lastWednesday.timeIntervalSinceReferenceDate), "days": [ {"dayOfWeek": 5, "title": "Sat 5", "isToday": false, "meals": [{"type": "dinner", "label": "Dinner", "name": "Old dinner"}]} ] } """ let payload = try XCTUnwrap(WatchWeekPayload.decode(Data(json.utf8))) let saturday = date(2026, 9, 12) XCTAssertNil(payload.currentDay(now: saturday, calendar: calendar)) XCTAssertTrue(payload.isStale(now: saturday, calendar: calendar)) } func testWeekdayOffsetMapsMondayToZeroAndSundayToSix() { XCTAssertEqual(WatchWeekPayload.weekdayOffset(for: date(2026, 9, 7), calendar: calendar), 0) XCTAssertEqual(WatchWeekPayload.weekdayOffset(for: date(2026, 9, 12), calendar: calendar), 5) XCTAssertEqual(WatchWeekPayload.weekdayOffset(for: date(2026, 9, 13), calendar: calendar), 6) } }