import XCTest @testable import ReynaCLIHostCore import Foundation // TDD for calendar.events.list and calendar.event.create – fake providers only, no live EventKit final class CalendarEventsTests: XCTestCase { // MARK: - Helper types struct FakeCalendarListForSelection: CalendarListProviding { var calendars: [CalendarListItem] func listCalendars() throws -> [CalendarListItem] { calendars } } struct FakeEventsProvider: CalendarEventsListProviding { var events: [CalendarEventItem] var shouldThrow: CalendarProviderError? = nil func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] { if let err = shouldThrow { throw err } var filtered = events if let cid = calendarId { filtered = filtered.filter { $0.calendar_id == cid } } else if let ctitle = calendarTitle { filtered = filtered.filter { $0.calendar_title == ctitle } } return filtered } } struct SelectingEventsProvider: CalendarEventsListProviding { var availableCalendars: [CalendarListItem] var events: [CalendarEventItem] func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] { if let cid = calendarId { guard availableCalendars.contains(where: { $0.id == cid }) else { throw CalendarProviderError.invalidRequest("Calendar not found: \(cid)") } return events.filter { $0.calendar_id == cid }.prefix(limit).map { $0 } } if let title = calendarTitle { let matched = availableCalendars.filter { $0.title == title } if matched.isEmpty { throw CalendarProviderError.invalidRequest("Calendar not found: \(title)") } if matched.count > 1 { throw CalendarProviderError.invalidRequest("Ambiguous calendar title: \(title) matches \(matched.count) calendars") } return events.filter { $0.calendar_title == title }.prefix(limit).map { $0 } } let sorted = events.sorted { $0.start < $1.start } return Array(sorted.prefix(limit)) } } struct FakeCreateProvider: CalendarEventCreateProviding { var shouldThrow: CalendarProviderError? = nil var willReturn: CalendarEventItem func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem { if let err = shouldThrow { throw err } return willReturn } } struct SelectingCreateProvider: CalendarEventCreateProviding { var availableCalendars: [CalendarListItem] var writableIds: Set var willReturn: CalendarEventItem func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem { if let cid = calendarId { guard let cal = availableCalendars.first(where: { $0.id == cid }) else { throw CalendarProviderError.invalidRequest("Calendar not found: \(cid)") } guard writableIds.contains(cal.id) else { throw CalendarProviderError.invalidRequest("Calendar is read-only: \(cal.title)") } return willReturn } if let t = calendarTitle { let matched = availableCalendars.filter { $0.title == t } if matched.isEmpty { throw CalendarProviderError.invalidRequest("Calendar not found: \(t)") } if matched.count > 1 { throw CalendarProviderError.invalidRequest("Ambiguous calendar title: \(t) matches \(matched.count) calendars") } guard writableIds.contains(matched[0].id) else { throw CalendarProviderError.invalidRequest("Calendar is read-only: \(matched[0].title)") } return willReturn } throw CalendarProviderError.invalidRequest("Calendar must be specified by id or exact unique title") } } struct DeniedEventsProvider: CalendarEventsListProviding { func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] { throw CalendarProviderError.permissionRequired } } struct FailEventsProvider: CalendarEventsListProviding { func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] { throw CalendarProviderError.unavailable("disk fail") } } struct DeniedCreateProvider: CalendarEventCreateProviding { func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem { throw CalendarProviderError.permissionRequired } } struct MockAuthProvider: CalendarAuthorizationProviding { var status: CalendarAuthorizationStatus func authorizationStatus() -> CalendarAuthorizationStatus { status } func requestFullAccess() throws -> Bool { false } } final class CountingCreate: CalendarEventCreateProviding, @unchecked Sendable { var count = 0 func createEvent(title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String?) throws -> CalendarEventItem { count += 1 return CalendarEventItem(id: "sample", title: "Sample", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: nil, location: nil) } } func sampleEvent() -> CalendarEventItem { CalendarEventItem(id: "sample", title: "Sample", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: nil, location: nil) } // MARK: - List tests func testEventsListMissingStartReturnsInvalidRequest() { let req = Request(id: "e1", operation: "calendar.events.list", arguments: .object(["end": .string("2026-01-02T00:00:00Z")])) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testEventsListInvalidISODate() { let args: JSONValue = .object(["start": .string("not-a-date"), "end": .string("2026-01-02T00:00:00Z")]) let req = Request(id: "e2", operation: "calendar.events.list", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testEventsListStartAfterEnd() { let args: JSONValue = .object(["start": .string("2026-01-03T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z")]) let req = Request(id: "e3", operation: "calendar.events.list", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testEventsListLimitBounded() { let argsLow: JSONValue = .object(["start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "limit": .number(0)]) let reqLow = Request(id: "e4", operation: "calendar.events.list", arguments: argsLow) let respLow = dispatch(request: reqLow, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(respLow.ok) XCTAssertEqual(respLow.error?.code, "invalid_request") let argsHigh: JSONValue = .object(["start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "limit": .number(251)]) let reqHigh = Request(id: "e5", operation: "calendar.events.list", arguments: argsHigh) let respHigh = dispatch(request: reqHigh, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(respHigh.ok) XCTAssertEqual(respHigh.error?.code, "invalid_request") } func testEventsListSuccessSortedAndMinimalFields() { let ev1 = CalendarEventItem(id: "2", title: "B", start: "2026-01-01T11:00:00Z", end: "2026-01-01T12:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: "n2", location: "loc2") let ev2 = CalendarEventItem(id: "1", title: "A", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "cal1", calendar_title: "Home", notes: nil, location: nil) let provider = FakeEventsProvider(events: [ev1, ev2]) let args: JSONValue = .object(["start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "limit": .number(10)]) let req = Request(id: "e6", operation: "calendar.events.list", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: provider, createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertTrue(resp.ok) XCTAssertEqual(resp.result?.events?.count, 2) XCTAssertEqual(resp.result?.events?.first?.id, "1") let encoded = try! JSONEncoder().encode(resp) let obj = try! JSONSerialization.jsonObject(with: encoded) as! [String: Any] let result = obj["result"] as! [String: Any] let events = result["events"] as! [[String: Any]] for ev in events { XCTAssertNotNil(ev["id"]) XCTAssertNotNil(ev["title"]) XCTAssertNotNil(ev["start"]) XCTAssertNotNil(ev["end"]) XCTAssertNotNil(ev["calendar_id"]) XCTAssertNotNil(ev["calendar_title"]) let allowed = Set(["id","title","start","end","all_day","calendar_id","calendar_title","notes","location"]) XCTAssertTrue(Set(ev.keys).isSubset(of: allowed), "Unexpected keys: \(ev.keys)") } } func testEventsListCalendarIdWinsOverTitle() { let calendars = [ CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav"), CalendarListItem(id: "id2", title: "Home", source: "Local", type: "local") ] let ev1 = CalendarEventItem(id: "e1", title: "T", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "id1", calendar_title: "Home", notes: nil, location: nil) let ev2 = CalendarEventItem(id: "e2", title: "T", start: "2026-01-01T11:00:00Z", end: "2026-01-01T12:00:00Z", all_day: false, calendar_id: "id2", calendar_title: "Home", notes: nil, location: nil) let provider = SelectingEventsProvider(availableCalendars: calendars, events: [ev1, ev2]) let args: JSONValue = .object([ "start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "calendar_id": .string("id1"), "calendar": .string("Home"), "limit": .number(10) ]) let req = Request(id: "e7", operation: "calendar.events.list", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: provider, createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertTrue(resp.ok) XCTAssertEqual(resp.result?.events?.count, 1) XCTAssertEqual(resp.result?.events?.first?.calendar_id, "id1") } func testEventsListUnknownCalendarFailsDeterministic() { let calendars = [CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav")] let provider = SelectingEventsProvider(availableCalendars: calendars, events: []) let args: JSONValue = .object([ "start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "calendar": .string("Work") ]) let req = Request(id: "e8", operation: "calendar.events.list", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: provider, createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testEventsListAmbiguousTitleFails() { let calendars = [ CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav"), CalendarListItem(id: "id2", title: "Home", source: "Local", type: "local") ] let provider = SelectingEventsProvider(availableCalendars: calendars, events: []) let args: JSONValue = .object([ "start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z"), "calendar": .string("Home") ]) let req = Request(id: "e9", operation: "calendar.events.list", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: provider, createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") XCTAssertTrue(resp.error?.message.contains("Ambiguous") ?? false) } func testEventsListPermissionRequired() { let args: JSONValue = .object([ "start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z") ]) let req = Request(id: "e10", operation: "calendar.events.list", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: DeniedEventsProvider(), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "permission_required") } func testEventsListProviderFailureMapsToUnavailable() { let args: JSONValue = .object([ "start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z") ]) let req = Request(id: "e11", operation: "calendar.events.list", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FailEventsProvider(), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "calendar_unavailable") } // MARK: - Create tests func testCreateMissingTitleInvalid() { let args: JSONValue = .object([ "start": .string("2026-01-01T10:00:00Z"), "end": .string("2026-01-01T11:00:00Z"), "calendar_id": .string("id1") ]) let req = Request(id: "c1", operation: "calendar.event.create", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testCreateStartAfterEndInvalid() { let args: JSONValue = .object([ "title": .string("Meeting"), "start": .string("2026-01-01T12:00:00Z"), "end": .string("2026-01-01T11:00:00Z"), "calendar_id": .string("id1") ]) let req = Request(id: "c2", operation: "calendar.event.create", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testCreateInvalidISO() { let args: JSONValue = .object([ "title": .string("Meeting"), "start": .string("bad-date"), "end": .string("2026-01-01T11:00:00Z"), "calendar_id": .string("id1") ]) let req = Request(id: "c3", operation: "calendar.event.create", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: FakeCreateProvider(willReturn: sampleEvent()), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testCreateNoCalendarSpecifiedFailsNoDefault() { let args: JSONValue = .object([ "title": .string("Meeting"), "start": .string("2026-01-01T10:00:00Z"), "end": .string("2026-01-01T11:00:00Z") ]) let req = Request(id: "c4", operation: "calendar.event.create", arguments: args) let calendars = [CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav")] let provider = SelectingCreateProvider(availableCalendars: calendars, writableIds: ["id1"], willReturn: sampleEvent()) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") XCTAssertTrue(resp.error?.message.contains("must be specified") ?? false) } func testCreateUnknownCalendarFails() { let args: JSONValue = .object([ "title": .string("Meeting"), "start": .string("2026-01-01T10:00:00Z"), "end": .string("2026-01-01T11:00:00Z"), "calendar": .string("Nonexistent") ]) let req = Request(id: "c5", operation: "calendar.event.create", arguments: args) let calendars = [CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav")] let provider = SelectingCreateProvider(availableCalendars: calendars, writableIds: ["id1"], willReturn: sampleEvent()) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testCreateAmbiguousTitleFails() { let args: JSONValue = .object([ "title": .string("Meeting"), "start": .string("2026-01-01T10:00:00Z"), "end": .string("2026-01-01T11:00:00Z"), "calendar": .string("Home") ]) let req = Request(id: "c6", operation: "calendar.event.create", arguments: args) let calendars = [ CalendarListItem(id: "id1", title: "Home", source: "iCloud", type: "caldav"), CalendarListItem(id: "id2", title: "Home", source: "Local", type: "local") ] let provider = SelectingCreateProvider(availableCalendars: calendars, writableIds: ["id1","id2"], willReturn: sampleEvent()) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testCreateReadOnlyCalendarFails() { let args: JSONValue = .object([ "title": .string("Meeting"), "start": .string("2026-01-01T10:00:00Z"), "end": .string("2026-01-01T11:00:00Z"), "calendar_id": .string("id1") ]) let req = Request(id: "c7", operation: "calendar.event.create", arguments: args) let calendars = [CalendarListItem(id: "id1", title: "Birthdays", source: "iCloud", type: "birthday")] let provider = SelectingCreateProvider(availableCalendars: calendars, writableIds: [], willReturn: sampleEvent()) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: calendars), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "invalid_request") } func testCreateSuccessReturnsMetadata() { let args: JSONValue = .object([ "title": .string("Meeting"), "start": .string("2026-01-01T10:00:00Z"), "end": .string("2026-01-01T11:00:00Z"), "calendar_id": .string("id1"), "notes": .string("bring docs"), "location": .string("Room 1") ]) let req = Request(id: "c8", operation: "calendar.event.create", arguments: args) let created = CalendarEventItem(id: "new-id", title: "Meeting", start: "2026-01-01T10:00:00Z", end: "2026-01-01T11:00:00Z", all_day: false, calendar_id: "id1", calendar_title: "Home", notes: "bring docs", location: "Room 1") let provider = FakeCreateProvider(willReturn: created) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: provider, authProvider: MockAuthProvider(status: .authorized)) XCTAssertTrue(resp.ok) XCTAssertEqual(resp.result?.event?.id, "new-id") XCTAssertEqual(resp.result?.event?.calendar_id, "id1") XCTAssertEqual(resp.result?.operation, "calendar.event.create") } func testCreatePermissionRequired() { let args: JSONValue = .object([ "title": .string("Meeting"), "start": .string("2026-01-01T10:00:00Z"), "end": .string("2026-01-01T11:00:00Z"), "calendar_id": .string("id1") ]) let req = Request(id: "c9", operation: "calendar.event.create", arguments: args) let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: DeniedCreateProvider(), authProvider: MockAuthProvider(status: .authorized)) XCTAssertFalse(resp.ok) XCTAssertEqual(resp.error?.code, "permission_required") } func testListDoesNotTriggerCreate() { let args: JSONValue = .object([ "start": .string("2026-01-01T00:00:00Z"), "end": .string("2026-01-02T00:00:00Z") ]) let req = Request(id: "iso", operation: "calendar.events.list", arguments: args) let counter = CountingCreate() let resp = dispatch(request: req, calendarProvider: FakeCalendarListForSelection(calendars: []), eventsProvider: FakeEventsProvider(events: []), createProvider: counter, authProvider: MockAuthProvider(status: .authorized)) XCTAssertTrue(resp.ok) XCTAssertEqual(counter.count, 0, "list must not trigger create") } }