import { describe, expect, it } from "vitest"; import { canonicalRepair, findConflicts, localTimeCandidates, mergeCalendars, parseCalendar, previewOccurrences, timezoneDiagnostics, } from "../../src/calendar/model"; const recurring = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\nBEGIN:VEVENT\r\nUID:a\r\nDTSTAMP:20260101T000000Z\r\nDTSTART:20260901T100000Z\r\nDTEND:20260901T110000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nSUMMARY:A\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:b\r\nDTSTART:20260901T103000Z\r\nDTEND:20260901T120000Z\r\nSUMMARY:B\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n`; describe("calendar model", () => { it("parses and expands recurrence under a bound", () => { const document = parseCalendar(recurring); expect(document.events).toHaveLength(2); const preview = previewOccurrences(document, 10, 30); expect(preview.occurrences.filter((item) => item.uid === "a")).toHaveLength( 3, ); expect(findConflicts(preview.occurrences)).toEqual( expect.arrayContaining([expect.objectContaining({ kind: "overlap" })]), ); }); it("canonicalizes line endings and supplies required metadata", () => { const input = `BEGIN:VCALENDAR\nBEGIN:VEVENT\nUID:x\nDTSTART:20260901T100000Z\nDTEND:20260901T110000Z\nEND:VEVENT\nEND:VCALENDAR\n`; const repaired = canonicalRepair(input); expect(repaired.source).toContain("VERSION:2.0\r\n"); expect(repaired.source).toContain("PRODID:"); expect(repaired.source).not.toMatch(/(? { const newer = recurring.replace( "SUMMARY:A", "SEQUENCE:2\r\nSUMMARY:Updated A", ); const merged = mergeCalendars([ parseCalendar(recurring), parseCalendar(newer), ]); expect(merged.deduplicated).toBe(2); expect(merged.source).toContain("SUMMARY:Updated A"); expect(parseCalendar(merged.source).events).toHaveLength(2); }); it("never deduplicates separate UID-less events", () => { const uidless = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\nBEGIN:VEVENT\r\nDTSTART:20260901T100000Z\r\nDTEND:20260901T110000Z\r\nSUMMARY:No identity\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n`; const merged = mergeCalendars([ parseCalendar(uidless), parseCalendar(uidless), ]); expect(merged.deduplicated).toBe(0); expect(parseCalendar(merged.source).events).toHaveLength(2); expect(merged.diagnostics.map((item) => item.message).join(" ")).toMatch( /UID-less/u, ); }); it("uses embedded VTIMEZONE observances for custom TZIDs", () => { const custom = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Custom/Test\r\nBEGIN:STANDARD\r\nDTSTART:19700101T000000\r\nTZOFFSETFROM:+0230\r\nTZOFFSETTO:+0230\r\nTZNAME:CST\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:custom\r\nDTSTART;TZID=Custom/Test:20260901T100000\r\nDTEND;TZID=Custom/Test:20260901T110000\r\nSUMMARY:Custom zone\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n`; const document = parseCalendar(custom); expect(document.timezones[0]?.tzid).toBe("Custom/Test"); expect(document.events[0]?.startMs).toBe( Date.parse("2026-09-01T07:30:00.000Z"), ); }); it("classifies DST overlaps and reports yearly transitions", () => { const candidates = localTimeCandidates("2026-10-25T02:30", "Europe/Berlin"); expect(candidates).toHaveLength(2); expect(timezoneDiagnostics("Europe/Berlin", 2026).transitions).toHaveLength( 2, ); }); it("rejects unbounded and malformed inputs", () => { expect(() => previewOccurrences(parseCalendar(recurring), 501, 30)).toThrow( /1–500/u, ); expect(() => parseCalendar("not a calendar")).toThrow( /parse failed|root component/u, ); }); });