import XCTest @testable import KisaniCal @MainActor final class StreakLogicTests: XCTestCase { private let cal = Calendar.current private let fmt: DateFormatter = { let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; return f }() private func day(_ offset: Int, from ref: Date) -> String { fmt.string(from: cal.date(byAdding: .day, value: offset, to: ref)!) } // MARK: - Workout tally (lifetime; only ever grows, never resets) /// The field scenario: 6-day week, Thursday skipped, 5 workouts done → /// the count is 5, not 1. A missed day never reduces it. func testSkippedDayStillCountsAchieved() { let today = cal.startOfDay(for: Date()) var dates: [String] = [] guard let week = cal.dateInterval(of: .weekOfYear, for: today) else { return XCTFail() } var d = week.start, added = 0 while d <= today { let key = fmt.string(from: d) if key != day(-2, from: today) { dates.append(key); added += 1 } // skip one day d = cal.date(byAdding: .day, value: 1, to: d)! } XCTAssertEqual(WorkoutViewModel.totalAchievedWorkoutDays(dates), added, "every logged day counts; the skipped day is simply absent") } /// A below-goal week (4/5) and a full week (5/5) both contribute all their /// achieved days — 9 total, nothing lost to the 4/5 week "failing." func testBelowGoalAndFullWeekBothCountFully() { let today = cal.startOfDay(for: Date()) guard let prevRef = cal.date(byAdding: .weekOfYear, value: -1, to: today), let prevWeek = cal.dateInterval(of: .weekOfYear, for: prevRef), let thisWeek = cal.dateInterval(of: .weekOfYear, for: today) else { return XCTFail() } var dates: [String] = [] var d = prevWeek.start for i in 0..<7 { if i < 4 { dates.append(fmt.string(from: d)) }; d = cal.date(byAdding: .day, value: 1, to: d)! } // 4/5 d = thisWeek.start for i in 0..<7 { if i < 5 { dates.append(fmt.string(from: d)) }; d = cal.date(byAdding: .day, value: 1, to: d)! } // 5/5 XCTAssertEqual(WorkoutViewModel.totalAchievedWorkoutDays(dates), 9, "4/5 + 5/5 = 9; a below-goal week is never discarded") } /// A completely blank week between two active stretches does not reset the /// tally — earlier achievements are preserved. func testBlankWeekDoesNotReset() { let today = cal.startOfDay(for: Date()) guard let twoBack = cal.date(byAdding: .weekOfYear, value: -2, to: today), let oldWeek = cal.dateInterval(of: .weekOfYear, for: twoBack), let thisWeek = cal.dateInterval(of: .weekOfYear, for: today) else { return XCTFail() } var dates: [String] = [] var d = oldWeek.start for i in 0..<7 { if i < 3 { dates.append(fmt.string(from: d)) }; d = cal.date(byAdding: .day, value: 1, to: d)! } // (previous week: nothing — a blank week) dates.append(fmt.string(from: thisWeek.start)) XCTAssertEqual(WorkoutViewModel.totalAchievedWorkoutDays(dates), 4, "3 old + 1 now; the empty week in between changes nothing") } /// Duplicate day entries (e.g. HealthKit merge) never double-count. func testDuplicatesCountOnce() { let key = fmt.string(from: cal.startOfDay(for: Date())) XCTAssertEqual(WorkoutViewModel.totalAchievedWorkoutDays([key, key, key]), 1) } func testEmptyDataIsZero() { XCTAssertEqual(WorkoutViewModel.totalAchievedWorkoutDays([]), 0) } // MARK: - Task completion counting (recurring occurrences) private func makeRecurring(title: String, completedOn keys: [String]) -> TaskItem { var t = TaskItem(title: title, dueDate: cal.date(byAdding: .day, value: -30, to: Date())) t.isRecurring = true t.recurrenceLabel = "Daily" t.completedOccurrences = keys return t } /// Recurring completions must count toward per-day stats even though the /// stored task's isComplete/completedAt stay untouched. func testRecurringOccurrencesCountedPerDay() { let today = cal.startOfDay(for: Date()) let todayKey = fmt.string(from: today) let recurring = makeRecurring(title: "Morning routine", completedOn: [todayKey]) var oneOff = TaskItem(title: "Buy milk") oneOff.isComplete = true oneOff.completedAt = today.addingTimeInterval(3600) let n = TaskViewModel.completionCount(in: [recurring, oneOff], on: today, calendar: cal) XCTAssertEqual(n, 2, "one recurring occurrence + one one-off completion") } func testRecurringNotCountedOnOtherDays() { let today = cal.startOfDay(for: Date()) let yesterdayKey = day(-1, from: today) let recurring = makeRecurring(title: "Morning routine", completedOn: [yesterdayKey]) XCTAssertEqual(TaskViewModel.completionCount(in: [recurring], on: today, calendar: cal), 0) let yesterday = cal.date(byAdding: .day, value: -1, to: today)! XCTAssertEqual(TaskViewModel.completionCount(in: [recurring], on: yesterday, calendar: cal), 1) } func testIncompleteOneOffNotCounted() { let today = cal.startOfDay(for: Date()) let t = TaskItem(title: "Open task") XCTAssertEqual(TaskViewModel.completionCount(in: [t], on: today, calendar: cal), 0) } }