import EventKit
import Foundation
import MacSyncShared
import Testing
@testable import IReminderSync

// MARK: - Test fake

/// Hermetic stand-in for `ReminderApplying` that records calls and returns
/// caller-controlled lookups, so tests never touch the real EventKit store.
@MainActor
final class FakeReminderApplier: ReminderApplying {
    var calendarsByID: [String: EKCalendar] = [:]
    var remindersByID: [String: EKReminder] = [:]
    var saved: [EKReminder] = []
    var removed: [EKReminder] = []
    var throwOnSave: Bool = false
    var throwOnRemove: Bool = false

    let backingStore = EKEventStore()

    func calendar(withIdentifier id: String) -> EKCalendar? { calendarsByID[id] }

    func reminder(withIdentifier id: String) -> EKReminder? { remindersByID[id] }

    func makeReminder() -> EKReminder { EKReminder(eventStore: backingStore) }

    func save(_ reminder: EKReminder) throws {
        if throwOnSave { throw NSError(domain: "test", code: 1, userInfo: [NSLocalizedDescriptionKey: "boom"]) }
        saved.append(reminder)
    }

    func remove(_ reminder: EKReminder) throws {
        if throwOnRemove { throw NSError(domain: "test", code: 2, userInfo: [NSLocalizedDescriptionKey: "boom"]) }
        removed.append(reminder)
    }
}

private func isSent(_ r: SendQueueApplyResult) -> Bool {
    if case .sent = r { return true }
    return false
}

private func failureReason(_ r: SendQueueApplyResult) -> String? {
    if case .failed(let reason) = r { return reason }
    return nil
}

@Suite("IReminderSync.Sender")
@MainActor
struct SenderTests {
    @Test("unknown action fails fast")
    func unknownAction() async {
        let applier = FakeReminderApplier()
        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "frobnicate",
            payload: ReminderSendPayload(),
            createdAt: "2026-05-13T10:00:00Z"
        )
        let result = await sender.apply(item)
        #expect(failureReason(result)?.contains("unknown action") == true)
    }

    @Test("create_reminder requires calendarIdentifier")
    func createMissingCalendar() async {
        let applier = FakeReminderApplier()
        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "create_reminder",
            payload: ReminderSendPayload(title: "x"),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(failureReason(result) == "missing calendarIdentifier")
        #expect(applier.saved.isEmpty)
    }

    @Test("create_reminder requires title")
    func createMissingTitle() async {
        let applier = FakeReminderApplier()
        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "create_reminder",
            payload: ReminderSendPayload(calendarIdentifier: "list-1"),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(failureReason(result) == "missing title")
    }

    @Test("create_reminder fails when calendar not found")
    func createCalendarNotFound() async {
        let applier = FakeReminderApplier()
        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "create_reminder",
            payload: ReminderSendPayload(calendarIdentifier: "missing", title: "Buy milk"),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(failureReason(result)?.contains("calendar not found") == true)
    }

    @Test("update_reminder requires reminderIdentifier")
    func updateMissingId() async {
        let applier = FakeReminderApplier()
        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "update_reminder",
            payload: ReminderSendPayload(title: "x"),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(failureReason(result) == "missing reminderIdentifier")
    }

    @Test("update_reminder fails when reminder not found")
    func updateNotFound() async {
        let applier = FakeReminderApplier()
        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "update_reminder",
            payload: ReminderSendPayload(reminderIdentifier: "rem-1", title: "x"),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(failureReason(result)?.contains("reminder not found") == true)
    }

    @Test("update_reminder applies fields when reminder exists")
    func updateApplies() async {
        let applier = FakeReminderApplier()
        let reminder = EKReminder(eventStore: applier.backingStore)
        reminder.title = "old"
        applier.remindersByID["rem-1"] = reminder

        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "update_reminder",
            payload: ReminderSendPayload(
                reminderIdentifier: "rem-1",
                title: "new title",
                notes: "note body",
                priority: 7,
                isCompleted: true
            ),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(isSent(result))
        #expect(reminder.title == "new title")
        #expect(reminder.notes == "note body")
        #expect(reminder.priority == 7)
        #expect(reminder.isCompleted == true)
        #expect(applier.saved.count == 1)
    }

    @Test("update_reminder rejects invalid dueDate")
    func updateInvalidDueDate() async {
        let applier = FakeReminderApplier()
        let reminder = EKReminder(eventStore: applier.backingStore)
        applier.remindersByID["rem-1"] = reminder

        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "update_reminder",
            payload: ReminderSendPayload(reminderIdentifier: "rem-1", dueDate: "not-a-date"),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(failureReason(result) == "invalid dueDate")
        #expect(applier.saved.isEmpty)
    }

    @Test("delete_reminder requires reminderIdentifier")
    func deleteMissingId() async {
        let applier = FakeReminderApplier()
        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "delete_reminder",
            payload: ReminderSendPayload(),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(failureReason(result) == "missing reminderIdentifier")
    }

    @Test("delete_reminder removes when reminder exists")
    func deleteApplies() async {
        let applier = FakeReminderApplier()
        let reminder = EKReminder(eventStore: applier.backingStore)
        applier.remindersByID["rem-1"] = reminder

        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "delete_reminder",
            payload: ReminderSendPayload(reminderIdentifier: "rem-1"),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(isSent(result))
        #expect(applier.removed.count == 1)
    }

    @Test("delete_reminder surfaces remove errors")
    func deleteSurfacesError() async {
        let applier = FakeReminderApplier()
        let reminder = EKReminder(eventStore: applier.backingStore)
        applier.remindersByID["rem-1"] = reminder
        applier.throwOnRemove = true

        let sender = ReminderSender(applier: applier)
        let item = PendingReminderSend(
            id: "1",
            action: "delete_reminder",
            payload: ReminderSendPayload(reminderIdentifier: "rem-1"),
            createdAt: ""
        )
        let result = await sender.apply(item)
        #expect(failureReason(result)?.contains("remove failed") == true)
    }
}
