import Foundation import SwiftData import Testing @testable import Rituals struct RitualStoreTests { @MainActor @Test func quickRitualStartsIncomplete() throws { let store = makeStore() store.createQuickRitual() #expect(store.activeRitual != nil) #expect(abs(store.activeRitualProgress) < 0.0001) } @MainActor @Test func toggleHabitCompletionMarksComplete() throws { let store = makeStore() store.createQuickRitual() guard let habit = store.activeRitual?.habits.first else { throw TestError.missingHabit } store.toggleHabitCompletion(habit) #expect(store.isHabitCompletedToday(habit) == true) } @MainActor @Test func toggleHabitCompletionForSpecificDate() throws { let store = makeStore() store.createQuickRitual() guard let habit = store.activeRitual?.habits.first else { throw TestError.missingHabit } let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())! store.toggleHabitCompletion(habit, date: yesterday) let completions = store.habitCompletions(for: yesterday) let completion = completions.first { $0.habit.id == habit.id } #expect(completion?.isCompleted == true) #expect(store.isHabitCompletedToday(habit) == false) } @MainActor @Test func arcRenewalCreatesNewArc() throws { let store = makeStore() store.createQuickRitual() guard let ritual = store.activeRitual else { throw TestError.missingHabit } #expect((ritual.arcs?.count ?? 0) == 1) #expect(ritual.currentArc?.arcNumber == 1) // End the current arc store.endArc(for: ritual) #expect(ritual.currentArc == nil) // Renew the arc store.renewArc(for: ritual, durationDays: 30, copyHabits: true) #expect((ritual.arcs?.count ?? 0) == 2) #expect(ritual.currentArc?.arcNumber == 2) #expect((ritual.currentArc?.habits?.count ?? 0) == 3) } @MainActor @Test func currentStreakCountsOnlyPerfectDays() throws { let store = makeStore() store.createQuickRitual() guard let ritual = store.activeRitual else { throw TestError.missingRitual } let habits = ritual.habits guard habits.count >= 2 else { throw TestError.missingHabit } let calendar = Calendar.current let today = Date() let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today // Yesterday is perfect: complete all habits. for habit in habits { store.toggleHabitCompletion(habit, date: yesterday) } // Today is partial: complete only one habit. store.toggleHabitCompletion(habits[0], date: today) #expect(store.currentStreak() == 1) } @MainActor @Test func allRitualCompletionRateIncludesEndedArcsForHistoricalDates() throws { let store = makeStore() store.createQuickRitual() guard let ritual = store.activeRitual, let habit = ritual.habits.first, let arcStart = ritual.currentArc?.startDate else { throw TestError.missingHabit } store.toggleHabitCompletion(habit, date: arcStart) store.endArc(for: ritual) let rate = store.completionRate(for: arcStart, ritual: nil) #expect(rate > 0.0) } @MainActor @Test func completedActiveArcStillTriggersRenewalPrompt() throws { let store = makeStore() store.createQuickRitual() guard let ritual = store.activeRitual, let arc = ritual.currentArc else { throw TestError.missingRitual } let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date() arc.endDate = yesterday #expect(ritual.hasActiveArc == false) store.checkForCompletedArcs() #expect(store.ritualNeedingRenewal?.id == ritual.id) } @MainActor @Test func currentArcUsesMostRecentActiveArc() throws { let calendar = Calendar.current let today = calendar.startOfDay(for: Date()) let oldStart = calendar.date(byAdding: .day, value: -20, to: today) ?? today let oldEnd = calendar.date(byAdding: .day, value: -5, to: today) ?? today let currentStart = calendar.date(byAdding: .day, value: -2, to: today) ?? today let currentEnd = calendar.date(byAdding: .day, value: 10, to: today) ?? today let oldArc = RitualArc(startDate: oldStart, endDate: oldEnd, arcNumber: 1, isActive: true) let currentArc = RitualArc(startDate: currentStart, endDate: currentEnd, arcNumber: 2, isActive: true) let ritual = Ritual(title: "Test", theme: "Theme", arcs: [oldArc, currentArc]) #expect(ritual.currentArc?.arcNumber == 2) #expect(ritual.hasActiveArc == true) } @MainActor @Test func completedArcCountIncludesExpiredActiveArcs() throws { let calendar = Calendar.current let today = calendar.startOfDay(for: Date()) let start = calendar.date(byAdding: .day, value: -10, to: today) ?? today let end = calendar.date(byAdding: .day, value: -1, to: today) ?? today let expiredButActiveArc = RitualArc(startDate: start, endDate: end, arcNumber: 1, isActive: true) let ritual = Ritual(title: "Expired", theme: "Theme", arcs: [expiredButActiveArc]) #expect(ritual.completedArcCount == 1) #expect(Calendar.current.isDate(ritual.lastCompletedDate ?? .distantPast, inSameDayAs: end)) } @MainActor @Test func nextUpcomingRitualContextIdentifiesTomorrowForFutureArc() throws { let calendar = Calendar.current let today = calendar.startOfDay(for: Date()) let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? today let nextWeek = calendar.date(byAdding: .day, value: 7, to: tomorrow) ?? tomorrow let middayToday = calendar.date(byAdding: .hour, value: 12, to: today) ?? today let tomorrowArc = RitualArc(startDate: tomorrow, endDate: nextWeek, arcNumber: 1, isActive: true) let eveningRitual = Ritual( title: "Evening Wind Down", theme: "Test", timeOfDay: .evening, arcs: [tomorrowArc] ) let context = RitualAnalytics.nextUpcomingRitualContext(from: [eveningRitual], currentDate: middayToday) #expect(context?.ritual.id == eveningRitual.id) #expect(context?.isTomorrow == true) } } @MainActor private func makeStore() -> RitualStore { let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self]) let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) let container: ModelContainer do { container = try ModelContainer(for: schema, configurations: [configuration]) } catch { fatalError("Test container failed: \(error)") } return RitualStore(modelContext: container.mainContext, seedService: EmptySeedService(), settingsStore: SettingsStore()) } private struct EmptySeedService: RitualSeedProviding { func makeSeedRituals(startDate: Date) -> [Ritual] { [] } } private enum TestError: Error { case missingHabit case missingRitual }