import XCTest import SwiftData @testable import MealMood final class WeekScheduleTests: XCTestCase { private func makeSettings( mealTypes: [MealType] = [.lunch, .dinner], includeWeekends: Bool = false ) -> AppSettings { let settings = AppSettings() settings.activeMealTypes = mealTypes settings.includeWeekends = includeWeekends return settings } // MARK: - Falling back to the global settings func testScheduleWithoutOverride_followsSettings() { let settings = makeSettings(mealTypes: [.dinner], includeWeekends: false) let plan = WeekPlan(weekStartDate: Date()) let schedule = settings.schedule(for: plan) XCTAssertEqual(schedule.mealTypes, [.dinner]) XCTAssertFalse(schedule.includeWeekends) XCTAssertFalse(schedule.isCustomized) XCTAssertEqual(schedule.dayRange, 0...4) XCTAssertEqual(schedule.lastDay, 4) } func testScheduleForNilPlan_followsSettings() { let settings = makeSettings(mealTypes: [.lunch, .dinner], includeWeekends: true) let schedule = settings.schedule(for: nil) XCTAssertEqual(schedule.mealTypes, [.lunch, .dinner]) XCTAssertTrue(schedule.includeWeekends) XCTAssertEqual(schedule.dayRange, 0...6) } // MARK: - Per-week overrides func testWeekendOverride_onlyAffectsThatWeek() { let settings = makeSettings(includeWeekends: false) let plan = WeekPlan(weekStartDate: Date()) let otherPlan = WeekPlan(weekStartDate: Date().addingDays(7)) plan.includeWeekendsOverride = true XCTAssertTrue(settings.schedule(for: plan).includeWeekends) XCTAssertEqual(settings.schedule(for: plan).dayRange, 0...6) XCTAssertTrue(settings.schedule(for: plan).isCustomized) XCTAssertFalse(settings.schedule(for: otherPlan).includeWeekends) XCTAssertFalse(settings.includeWeekends, "the global setting must stay untouched") } func testMealTypesOverride_replacesTheGlobalMeals() { let settings = makeSettings(mealTypes: [.dinner]) let plan = WeekPlan(weekStartDate: Date()) plan.mealTypesOverride = [.dinner, .breakfast, .snack] XCTAssertEqual(settings.schedule(for: plan).mealTypes, [.breakfast, .snack, .dinner], "meals are stored in chronological order, not selection order") XCTAssertEqual(settings.activeMealTypes, [.dinner]) } func testPartialOverride_keepsSettingsForTheOtherAxis() { let settings = makeSettings(mealTypes: [.lunch, .dinner], includeWeekends: false) let plan = WeekPlan(weekStartDate: Date()) plan.includeWeekendsOverride = true XCTAssertEqual(settings.schedule(for: plan).mealTypes, [.lunch, .dinner]) XCTAssertTrue(settings.schedule(for: plan).includeWeekends) } func testEmptyMealTypesOverride_isTreatedAsNoOverride() { let settings = makeSettings(mealTypes: [.lunch]) let plan = WeekPlan(weekStartDate: Date()) plan.mealTypesOverride = [] XCTAssertNil(plan.mealTypesOverrideRaw) XCTAssertFalse(plan.hasScheduleOverride) XCTAssertEqual(settings.schedule(for: plan).mealTypes, [.lunch]) } func testClearScheduleOverride_returnsToSettings() { let settings = makeSettings(mealTypes: [.dinner], includeWeekends: false) let plan = WeekPlan(weekStartDate: Date()) plan.includeWeekendsOverride = true plan.mealTypesOverride = [.lunch, .dinner] XCTAssertTrue(plan.hasScheduleOverride) plan.clearScheduleOverride() XCTAssertFalse(plan.hasScheduleOverride) XCTAssertEqual(settings.schedule(for: plan).mealTypes, [.dinner]) XCTAssertFalse(settings.schedule(for: plan).includeWeekends) } // MARK: - Premium gating func testOverrideIsHonouredEvenWhenNotPremium() { // A lapsed subscription must not silently drop the slots the user // already planned in a customised week. let settings = makeSettings(includeWeekends: false) settings.isPremium = false let plan = WeekPlan(weekStartDate: Date()) plan.includeWeekendsOverride = true XCTAssertTrue(settings.schedule(for: plan).includeWeekends) XCTAssertFalse(PremiumAccess.canCustomizeWeekSchedule(isPremium: false)) XCTAssertTrue(PremiumAccess.canCustomizeWeekSchedule(isPremium: true)) } } @MainActor final class WeekScheduleSlotSyncTests: XCTestCase { /// Held for the duration of the test: releasing the container tears the /// store off the coordinator and the next SwiftData call traps. private var container: ModelContainer! override func tearDown() { container = nil super.tearDown() } private func makeContext() throws -> ModelContext { container = try ModelContainer( for: AppSettings.self, Dish.self, Tag.self, WeekPlan.self, MealSlot.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true) ) return container.mainContext } func testApplyingAnOverrideAddsTheWeekendAndBreakfastSlots() throws { let context = try makeContext() let settings = AppSettings() settings.isPremium = true settings.activeMealTypes = [.lunch, .dinner] settings.includeWeekends = false context.insert(settings) let monday = Date().startOfWeek() let plan = DefaultDataService.createWeekPlan(for: monday, settings: settings, context: context) XCTAssertEqual(plan.slotList.count, 10, "5 weekdays x 2 meals") let viewModel = HomeViewModel() viewModel.applyWeekSchedule( includeWeekends: true, mealTypes: [.breakfast, .lunch, .dinner], plan: plan, settings: settings, context: context ) XCTAssertEqual(plan.slotList.count, 21, "7 days x 3 meals") XCTAssertTrue(plan.hasScheduleOverride) XCTAssertTrue(plan.slotList.contains { $0.dayOfWeek == 6 && $0.mealTypeEnum == .breakfast }) // The global settings must be untouched — this is a one-week change. XCTAssertFalse(settings.includeWeekends) XCTAssertEqual(settings.activeMealTypes, [.lunch, .dinner]) } func testResettingTheOverrideRestoresTheGlobalSlots() throws { let context = try makeContext() let settings = AppSettings() settings.isPremium = true settings.activeMealTypes = [.dinner] settings.includeWeekends = false context.insert(settings) let plan = DefaultDataService.createWeekPlan(for: Date().startOfWeek(), settings: settings, context: context) let viewModel = HomeViewModel() viewModel.applyWeekSchedule( includeWeekends: true, mealTypes: [.lunch, .dinner], plan: plan, settings: settings, context: context ) XCTAssertEqual(plan.slotList.count, 14) viewModel.resetWeekSchedule(plan: plan, settings: settings, context: context) XCTAssertFalse(plan.hasScheduleOverride) XCTAssertEqual(plan.slotList.count, 5, "back to 5 weekdays x dinner") XCTAssertTrue(plan.slotList.allSatisfy { $0.mealTypeEnum == .dinner && $0.dayOfWeek <= 4 }) } func testApplyingMatchingValuesClearsTheOverride() throws { let context = try makeContext() let settings = AppSettings() settings.isPremium = true settings.activeMealTypes = [.lunch, .dinner] settings.includeWeekends = true context.insert(settings) let plan = DefaultDataService.createWeekPlan(for: Date().startOfWeek(), settings: settings, context: context) let viewModel = HomeViewModel() viewModel.applyWeekSchedule( includeWeekends: true, mealTypes: [.lunch, .dinner], plan: plan, settings: settings, context: context ) XCTAssertFalse(plan.hasScheduleOverride, "matching the global settings means the week keeps following them") } func testRemovingAMealDropsItsPlannedSlots() throws { let context = try makeContext() let settings = AppSettings() settings.isPremium = true settings.activeMealTypes = [.lunch, .dinner] settings.includeWeekends = true context.insert(settings) let dish = Dish(name: "Tortilla") context.insert(dish) let plan = DefaultDataService.createWeekPlan(for: Date().startOfWeek(), settings: settings, context: context) let saturdayLunch = try XCTUnwrap(plan.slotList.first { $0.dayOfWeek == 5 && $0.mealTypeEnum == .lunch }) saturdayLunch.dishId = dish.id let viewModel = HomeViewModel() viewModel.applyWeekSchedule( includeWeekends: false, mealTypes: [.dinner], plan: plan, settings: settings, context: context ) XCTAssertEqual(plan.slotList.count, 5) XCTAssertFalse(plan.slotList.contains { $0.mealTypeEnum == .lunch }) XCTAssertFalse(plan.slotList.contains { $0.dayOfWeek > 4 }) } }