import XCTest @testable import KisaniCal /// Calendar correctness tests. These treat the month grid as a data-integrity concern: /// every cell must be a real date, columns must respect firstWeekday, and the weekday /// header must match the grid for any firstWeekday and timezone. final class CalendarGridTests: XCTestCase { private func makeCalendar(firstWeekday: Int, tz: String) -> Calendar { var c = Calendar(identifier: .gregorian) c.timeZone = TimeZone(identifier: tz)! c.firstWeekday = firstWeekday return c } private func iso(_ date: Date, _ cal: Calendar) -> String { let c = cal.dateComponents([.year, .month, .day], from: date) return String(format: "%04d-%02d-%02d", c.year!, c.month!, c.day!) } private func date(_ y: Int, _ m: Int, _ d: Int, _ cal: Calendar) -> Date { cal.date(from: DateComponents(year: y, month: m, day: d))! } private let timezones = ["America/New_York", "Europe/London", "Pacific/Kiritimati", "UTC"] // MARK: - Ground-truth weekday facts func testJune2026WeekdayFacts() { for tz in timezones { let cal = makeCalendar(firstWeekday: 1, tz: tz) let f = DateFormatter(); f.calendar = cal; f.timeZone = cal.timeZone; f.dateFormat = "EEEE" XCTAssertEqual(f.string(from: date(2026, 6, 1, cal)), "Monday", tz) XCTAssertEqual(f.string(from: date(2026, 6, 7, cal)), "Sunday", tz) XCTAssertEqual(f.string(from: date(2026, 6, 24, cal)), "Wednesday", tz) XCTAssertEqual(f.string(from: date(2026, 6, 27, cal)), "Saturday", tz) XCTAssertEqual(f.string(from: date(2026, 6, 30, cal)), "Tuesday", tz) XCTAssertEqual(f.string(from: date(2026, 7, 1, cal)), "Wednesday", tz) } } // MARK: - Sunday-first June 2026 grid matches the exact spec func testSundayFirstJune2026GridMatchesSpec() { let expected = [ "2026-05-31","2026-06-01","2026-06-02","2026-06-03","2026-06-04","2026-06-05","2026-06-06", "2026-06-07","2026-06-08","2026-06-09","2026-06-10","2026-06-11","2026-06-12","2026-06-13", "2026-06-14","2026-06-15","2026-06-16","2026-06-17","2026-06-18","2026-06-19","2026-06-20", "2026-06-21","2026-06-22","2026-06-23","2026-06-24","2026-06-25","2026-06-26","2026-06-27", "2026-06-28","2026-06-29","2026-06-30","2026-07-01","2026-07-02","2026-07-03","2026-07-04", ] for tz in timezones { let cal = makeCalendar(firstWeekday: 1, tz: tz) let grid = CalendarGrid.monthGrid(for: date(2026, 6, 15, cal), calendar: cal) XCTAssertEqual(grid.map { iso($0, cal) }, expected, "Sunday-first grid wrong in \(tz)") XCTAssertEqual(grid.count % 7, 0, "grid must be whole weeks") } } // MARK: - Header always matches grid (the actual bug) func testHeaderMatchesGridFirstColumnForAnyFirstWeekday() { for tz in timezones { for fw in 1...7 { let cal = makeCalendar(firstWeekday: fw, tz: tz) let grid = CalendarGrid.monthGrid(for: date(2026, 6, 15, cal), calendar: cal) let header = CalendarGrid.weekdaySymbols(calendar: cal) let letters = ["S","M","T","W","T","F","S"] // weekday 1...7 for col in 0..<7 { let weekday = cal.component(.weekday, from: grid[col]) // 1=Sun..7=Sat XCTAssertEqual(header[col], letters[weekday - 1], "col \(col) header \(header[col]) != grid weekday \(weekday) (fw=\(fw), tz=\(tz))") } } } } func testHeaderRotations() { let sun = makeCalendar(firstWeekday: 1, tz: "UTC") let mon = makeCalendar(firstWeekday: 2, tz: "UTC") XCTAssertEqual(CalendarGrid.weekdaySymbols(calendar: sun), ["S","M","T","W","T","F","S"]) XCTAssertEqual(CalendarGrid.weekdaySymbols(calendar: mon), ["M","T","W","T","F","S","S"]) } // MARK: - Cell date identity func testCellIdentityIsRealDate() { let cal = makeCalendar(firstWeekday: 1, tz: "America/New_York") let grid = CalendarGrid.monthGrid(for: date(2026, 6, 1, cal), calendar: cal) // June 24 must be a Wednesday and the 25th element (index 24) in the spec grid. let june24 = grid.first { cal.isDate($0, inSameDayAs: date(2026, 6, 24, cal)) } XCTAssertNotNil(june24) XCTAssertEqual(cal.component(.weekday, from: june24!), 4) // Wednesday // No duplicate dates, strictly increasing by one day. for i in 1.. trails into Jan 2027 let dec = CalendarGrid.monthGrid(for: date(2026, 12, 10, cal), calendar: cal).map { iso($0, cal) } XCTAssertTrue(dec.contains("2027-01-01")) XCTAssertEqual(dec.first, "2026-11-29") // Feb 2026 (non-leap): contains Feb 28, not Feb 29 let feb26 = CalendarGrid.monthGrid(for: date(2026, 2, 10, cal), calendar: cal).map { iso($0, cal) } XCTAssertTrue(feb26.contains("2026-02-28")) XCTAssertFalse(feb26.contains("2026-02-29")) // Feb 2028 (leap): contains Feb 29 let feb28 = CalendarGrid.monthGrid(for: date(2028, 2, 10, cal), calendar: cal).map { iso($0, cal) } XCTAssertTrue(feb28.contains("2028-02-29")) // May & July 2026 endpoints let may = CalendarGrid.monthGrid(for: date(2026, 5, 10, cal), calendar: cal).map { iso($0, cal) } XCTAssertTrue(may.contains("2026-05-01") && may.contains("2026-05-31")) let jul = CalendarGrid.monthGrid(for: date(2026, 7, 10, cal), calendar: cal).map { iso($0, cal) } XCTAssertTrue(jul.contains("2026-07-01") && jul.contains("2026-07-31")) } // MARK: - Event-day bucketing (same logic CalendarStore.events(for:) uses) /// Mirrors CalendarStore.events(for:) overlap test: s < endOfDay && e > startOfDay. private func overlaps(start: Date, end: Date, day: Date, cal: Calendar) -> Bool { let dayStart = cal.startOfDay(for: day) let dayEnd = cal.date(byAdding: .day, value: 1, to: dayStart)! return start < dayEnd && end > dayStart } func testAllDayEventMapsToSingleLocalDay() { let cal = makeCalendar(firstWeekday: 1, tz: "America/New_York") // All-day June 24: [Jun24 00:00, Jun25 00:00) let s = cal.startOfDay(for: date(2026, 6, 24, cal)) let e = cal.date(byAdding: .day, value: 1, to: s)! XCTAssertTrue(overlaps(start: s, end: e, day: date(2026, 6, 24, cal), cal: cal)) XCTAssertFalse(overlaps(start: s, end: e, day: date(2026, 6, 23, cal), cal: cal)) XCTAssertFalse(overlaps(start: s, end: e, day: date(2026, 6, 25, cal), cal: cal)) } func testLateNightTimedEventStaysOnLocalDay() { let cal = makeCalendar(firstWeekday: 1, tz: "America/New_York") // 11:30pm–11:45pm local on June 24 — must not bleed into the 25th. let s = cal.date(from: DateComponents(year: 2026, month: 6, day: 24, hour: 23, minute: 30))! let e = cal.date(from: DateComponents(year: 2026, month: 6, day: 24, hour: 23, minute: 45))! XCTAssertTrue(overlaps(start: s, end: e, day: date(2026, 6, 24, cal), cal: cal)) XCTAssertFalse(overlaps(start: s, end: e, day: date(2026, 6, 25, cal), cal: cal)) } func testEventCrossingMidnightHitsBothDays() { let cal = makeCalendar(firstWeekday: 1, tz: "America/New_York") let s = cal.date(from: DateComponents(year: 2026, month: 6, day: 24, hour: 23, minute: 30))! let e = cal.date(from: DateComponents(year: 2026, month: 6, day: 25, hour: 0, minute: 30))! XCTAssertTrue(overlaps(start: s, end: e, day: date(2026, 6, 24, cal), cal: cal)) XCTAssertTrue(overlaps(start: s, end: e, day: date(2026, 6, 25, cal), cal: cal)) XCTAssertFalse(overlaps(start: s, end: e, day: date(2026, 6, 23, cal), cal: cal)) } }