import Testing
import Foundation
import GRDB
@testable import SyncEngine
@testable import IMAPClient
@testable import MailStore
@testable import Models

@Suite("ActionQueue")
struct ActionQueueTests {
	func makeStore() throws -> MailStore {
		try MailStore(dbWriter: DatabaseSetup.openInMemoryDatabase())
	}

	func makeAccountAndStore() throws -> (MailStore, String) {
		let store = try makeStore()
		let accountId = "acc1"
		try store.insertAccount(AccountRecord(
			id: accountId,
			name: "Test",
			email: "test@example.com",
			imapHost: "imap.example.com",
			imapPort: 993
		))
		return (store, accountId)
	}

	@Test("enqueue persists action to database")
	func enqueuePersists() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = MockIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		let action = PendingAction(
			accountId: accountId,
			actionType: .setFlags,
			payload: .setFlags(uid: 1, mailbox: "INBOX", add: ["\\Seen"], remove: [])
		)

		try await queue.enqueue(action)

		let pending = try store.pendingActions(accountId: accountId)
		#expect(pending.count >= 1)
		let found = pending.first { $0.id == action.id }
		#expect(found != nil)
		#expect(found?.actionType == "setFlags")
	}

	@Test("flush dispatches actions and removes them from queue")
	func flushDispatches() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = MockIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		// Persist action directly (bypass fire-and-forget dispatch)
		let action = PendingAction(
			accountId: accountId,
			actionType: .setFlags,
			payload: .setFlags(uid: 1, mailbox: "INBOX", add: ["\\Seen"], remove: [])
		)
		let record = await queue.persistAction(action)
		try store.insertPendingAction(record)

		#expect(try store.pendingActionCount(accountId: accountId) == 1)

		await queue.flush()

		#expect(try store.pendingActionCount(accountId: accountId) == 0)
		#expect(mock.storedFlags.count == 1)
		#expect(mock.storedFlags[0].uid == 1)
		#expect(mock.storedFlags[0].add == ["\\Seen"])
	}

	@Test("flush dispatches actions in creation order (FIFO)")
	func flushFIFO() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = MockIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		let now = Date()
		let action1 = PendingAction(
			id: "first",
			accountId: accountId,
			actionType: .setFlags,
			payload: .setFlags(uid: 1, mailbox: "INBOX", add: ["\\Seen"], remove: []),
			createdAt: now
		)
		let action2 = PendingAction(
			id: "second",
			accountId: accountId,
			actionType: .setFlags,
			payload: .setFlags(uid: 2, mailbox: "INBOX", add: ["\\Flagged"], remove: []),
			createdAt: now.addingTimeInterval(1)
		)

		try store.insertPendingAction(await queue.persistAction(action1))
		try store.insertPendingAction(await queue.persistAction(action2))

		await queue.flush()

		#expect(mock.storedFlags.count == 2)
		#expect(mock.storedFlags[0].uid == 1)
		#expect(mock.storedFlags[1].uid == 2)
	}

	@Test("pendingCount reflects queue state")
	func pendingCountReflectsState() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = MockIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		#expect(await queue.pendingCount == 0)

		let action = PendingAction(
			accountId: accountId,
			actionType: .move,
			payload: .move(uid: 1, from: "INBOX", to: "Archive")
		)
		let record = await queue.persistAction(action)
		try store.insertPendingAction(record)

		#expect(await queue.pendingCount == 1)

		await queue.flush()

		#expect(await queue.pendingCount == 0)
	}

	@Test("failed dispatch increments retryCount")
	func failedDispatchRetry() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = FailingIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		let action = PendingAction(
			accountId: accountId,
			actionType: .setFlags,
			payload: .setFlags(uid: 1, mailbox: "INBOX", add: ["\\Seen"], remove: [])
		)
		let record = await queue.persistAction(action)
		try store.insertPendingAction(record)

		await queue.flush()

		let pending = try store.pendingActions(accountId: accountId)
		#expect(pending.count == 1)
		#expect(pending[0].retryCount == 1)
		#expect(pending[0].lastError != nil)
	}

	@Test("action removed after 5 failures")
	func removedAfterMaxRetries() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = FailingIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		let action = PendingAction(
			accountId: accountId,
			actionType: .setFlags,
			payload: .setFlags(uid: 1, mailbox: "INBOX", add: ["\\Seen"], remove: [])
		)
		var record = await queue.persistAction(action)
		record.retryCount = 4
		try store.insertPendingAction(record)

		await queue.flush()

		// retryCount was 4, incremented to 5 → removed
		#expect(try store.pendingActionCount(accountId: accountId) == 0)
	}

	@Test("delete action in trash expunges instead of moving")
	func deleteInTrashExpunges() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = MockIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		let action = PendingAction(
			accountId: accountId,
			actionType: .delete,
			payload: .delete(uid: 5, mailbox: "Trash", trashMailbox: "Trash")
		)
		let record = await queue.persistAction(action)
		try store.insertPendingAction(record)

		await queue.flush()

		// Should have stored \Deleted flag and expunged
		#expect(mock.storedFlags.count == 1)
		#expect(mock.storedFlags[0].add == ["\\Deleted"])
		#expect(mock.expungedMailboxes == ["Trash"])
		#expect(mock.movedMessages.isEmpty)
	}

	@Test("delete action not in trash moves to trash")
	func deleteNotInTrashMoves() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = MockIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		let action = PendingAction(
			accountId: accountId,
			actionType: .delete,
			payload: .delete(uid: 3, mailbox: "INBOX", trashMailbox: "Trash")
		)
		let record = await queue.persistAction(action)
		try store.insertPendingAction(record)

		await queue.flush()

		#expect(mock.movedMessages.count == 1)
		#expect(mock.movedMessages[0].from == "INBOX")
		#expect(mock.movedMessages[0].to == "Trash")
		#expect(mock.storedFlags.isEmpty)
	}

	@Test("move action dispatches correctly")
	func moveDispatches() async throws {
		let (store, accountId) = try makeAccountAndStore()
		let mock = MockIMAPClient()
		let queue = ActionQueue(
			store: store,
			accountId: accountId,
			imapClientProvider: { mock }
		)

		let action = PendingAction(
			accountId: accountId,
			actionType: .move,
			payload: .move(uid: 7, from: "INBOX", to: "Archive")
		)
		let record = await queue.persistAction(action)
		try store.insertPendingAction(record)

		await queue.flush()

		#expect(mock.movedMessages.count == 1)
		#expect(mock.movedMessages[0].uid == 7)
		#expect(mock.movedMessages[0].from == "INBOX")
		#expect(mock.movedMessages[0].to == "Archive")
	}
}

// MARK: - FailingIMAPClient

private final class FailingIMAPClient: IMAPClientProtocol, @unchecked Sendable {
	func connect() async throws {}
	func disconnect() async throws {}
	func listMailboxes() async throws -> [IMAPMailboxInfo] { [] }
	func selectMailbox(_ name: String) async throws -> IMAPMailboxStatus {
		throw FailingIMAPError.alwaysFails
	}
	func fetchEnvelopes(uidsGreaterThan uid: Int) async throws -> [FetchedEnvelope] { [] }
	func fetchFlags(uids: ClosedRange<Int>) async throws -> [UIDFlagsPair] { [] }
	func fetchBody(uid: Int) async throws -> (text: String?, html: String?) { (nil, nil) }
	func storeFlags(uid: Int, mailbox: String, add: [String], remove: [String]) async throws {
		throw FailingIMAPError.alwaysFails
	}
	func moveMessage(uid: Int, from: String, to: String) async throws {
		throw FailingIMAPError.alwaysFails
	}
	func copyMessage(uid: Int, from: String, to: String) async throws {
		throw FailingIMAPError.alwaysFails
	}
	func expunge(mailbox: String) async throws {
		throw FailingIMAPError.alwaysFails
	}
	func appendMessage(to mailbox: String, message: Data, flags: [String]) async throws {
		throw FailingIMAPError.alwaysFails
	}
	func capabilities() async throws -> Set<String> { [] }
}

private enum FailingIMAPError: Error {
	case alwaysFails
}
