import XCTest @testable import KisaniCal /// Verifies recurring-task occurrence expansion maps to the correct real calendar /// days (the calendar's event-date mapping for tasks). Uses Calendar.current — the /// same calendar the production code uses — so assertions hold in any timezone. @MainActor final class RecurrenceTests: XCTestCase { private let cal = Calendar.current private func day(_ y: Int, _ m: Int, _ d: Int, h: Int = 0, min: Int = 0) -> Date { cal.date(from: DateComponents(year: y, month: m, day: d, hour: h, minute: min))! } private func vm(with tasks: [TaskItem]) -> TaskViewModel { let v = TaskViewModel() v.tasks = tasks return v } private func task(_ label: String, start: Date, hasTime: Bool = false, end: Date? = nil) -> TaskItem { var t = TaskItem(title: "T-\(label)", dueDate: start) t.isRecurring = true t.recurrenceLabel = label t.hasTime = hasTime t.recurrenceEnd = end return t } private func hasOccurrence(_ v: TaskViewModel, _ id: UUID, on date: Date) -> Bool { v.occurrences(on: date).contains { $0.id == id } } func testDailyExpandsEveryDayFromStart() { let t = task("Daily", start: day(2026, 6, 1)) let v = vm(with: [t]) XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 5, 31)), "no occurrence before start") XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 1))) XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 24))) } func testWeeklyHitsEverySevenDaysOnly() { let t = task("Weekly", start: day(2026, 6, 1)) // Monday let v = vm(with: [t]) XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 8))) XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 22))) XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 9)), "off-cadence day must not match") XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 24))) } func testEveryWeekdaySkipsWeekend() { let t = task("Every Weekday", start: day(2026, 6, 1)) let v = vm(with: [t]) XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 24)), "Wed is a weekday") XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 27)), "Sat excluded") XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 28)), "Sun excluded") } func testMonthlyMatchesDayOfMonth() { let t = task("Monthly", start: day(2026, 1, 15)) let v = vm(with: [t]) XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 15))) XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 16))) } func testYearlyMatchesMonthAndDay() { let t = task("Yearly", start: day(2025, 6, 24)) let v = vm(with: [t]) XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 24))) XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 23))) } func testRecurrenceEndStopsExpansion() { let t = task("Daily", start: day(2026, 6, 1), end: day(2026, 6, 10)) let v = vm(with: [t]) XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 10)), "inclusive of end day") XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 11)), "past repeat-until") XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 24))) } func testOccurrenceCopyPreservesTimeOnTargetLocalDay() { let t = task("Daily", start: day(2026, 6, 1, h: 9, min: 30), hasTime: true) let v = vm(with: [t]) let occ = v.occurrences(on: day(2026, 6, 24)).first { $0.id == t.id } XCTAssertNotNil(occ) XCTAssertTrue(cal.isDate(occ!.dueDate!, inSameDayAs: day(2026, 6, 24)), "mapped to June 24") XCTAssertEqual(cal.component(.hour, from: occ!.dueDate!), 9) XCTAssertEqual(cal.component(.minute, from: occ!.dueDate!), 30) } func testNonRecurringTaskAppearsOnlyOnItsDay() { var t = TaskItem(title: "Dinner with Jay", dueDate: day(2026, 6, 24)) t.isRecurring = false let v = vm(with: [t]) XCTAssertTrue(hasOccurrence(v, t.id, on: day(2026, 6, 24))) XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 23))) XCTAssertFalse(hasOccurrence(v, t.id, on: day(2026, 6, 25))) } }