import XCTest
@testable import MacSyncShared

/// Minimal Stats / Error fixtures used to instantiate the generic base.
private struct TestStats: Sendable, Equatable {
    var count: Int = 0
}

private enum TestError: Sendable, Equatable {
    case none
    case authDenied
    case backend(String)
}

@MainActor
private final class TestSyncManager: BaseSyncManager<TestStats, TestError> {
    var performSyncCalls: Int = 0
    var authRequests: Int = 0
    var authorized: Bool = true
    var authorizationGrantedOnRequest: Bool = true

    var onPerform: (@MainActor () async -> Void)?

    init(persistenceKey: String, interval: TimeInterval = 60, authorized: Bool = true) {
        self.authorized = authorized
        super.init(
            initialStats: TestStats(),
            noError: .none,
            persistenceKey: persistenceKey,
            timerInterval: interval
        )
    }

    override func performSync() async {
        performSyncCalls += 1
        stats.count += 1
        if let onPerform { await onPerform() }
    }

    override func requestAuthorization() async -> Bool {
        authRequests += 1
        if authorizationGrantedOnRequest {
            authorized = true
        }
        return authorized
    }

    override func isAuthorized() async -> Bool { authorized }

    override func onAuthorizationDenied() {
        syncError = .authDenied
    }
}

final class BaseSyncManagerTests: XCTestCase {
    override func setUp() async throws {
        // Clear any UserDefaults watermarks from prior runs so tests are
        // hermetic. Each test uses a unique persistenceKey.
        try await super.setUp()
    }

    @MainActor
    func test_syncNow_runsCycleAndAdvancesWatermark() async {
        let key = "test.base.\(UUID().uuidString)"
        defer {
            UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
            UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
        }

        let manager = TestSyncManager(persistenceKey: key)
        XCTAssertNil(manager.lastSync)
        XCTAssertNil(manager.lastSyncCompletedAt)

        manager.syncNow()
        try? await Task.sleep(nanoseconds: 100_000_000)

        XCTAssertEqual(manager.performSyncCalls, 1)
        XCTAssertFalse(manager.isSyncing)
        XCTAssertNotNil(manager.lastSync)
        XCTAssertNotNil(manager.lastSyncCompletedAt)
        XCTAssertEqual(manager.stats.count, 1)
    }

    @MainActor
    func test_syncNow_skipsWhenAlreadyRunning() async {
        let key = "test.base.\(UUID().uuidString)"
        defer {
            UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
            UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
        }

        let manager = TestSyncManager(persistenceKey: key)
        manager.onPerform = {
            try? await Task.sleep(nanoseconds: 50_000_000)
        }

        manager.syncNow()
        manager.syncNow()
        manager.syncNow()
        try? await Task.sleep(nanoseconds: 200_000_000)

        XCTAssertEqual(manager.performSyncCalls, 1, "concurrent syncNow calls must coalesce")
    }

    @MainActor
    func test_authorizationDenied_setsErrorAndSkipsPerform() async {
        let key = "test.base.\(UUID().uuidString)"
        defer {
            UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
            UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
        }

        let manager = TestSyncManager(persistenceKey: key, authorized: false)
        manager.authorizationGrantedOnRequest = false

        manager.syncNow()
        try? await Task.sleep(nanoseconds: 100_000_000)

        XCTAssertEqual(manager.performSyncCalls, 0)
        XCTAssertEqual(manager.syncError, .authDenied)
        XCTAssertNil(manager.lastSync, "denied auth must not advance watermark")
    }

    @MainActor
    func test_setLastSync_persistsAndClears() async {
        let key = "test.base.\(UUID().uuidString)"
        defer {
            UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
            UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
        }

        let manager = TestSyncManager(persistenceKey: key)
        let date = Date(timeIntervalSince1970: 1_700_000_000)
        manager.setLastSync(date)
        XCTAssertEqual(manager.lastSync, date)
        XCTAssertEqual(UserDefaults.standard.object(forKey: "\(key).lastSync") as? Date, date)

        manager.setLastSync(nil)
        XCTAssertNil(manager.lastSync)
        XCTAssertNil(UserDefaults.standard.object(forKey: "\(key).lastSync"))
    }

    @MainActor
    func test_initRehydratesWatermarkFromUserDefaults() async {
        let key = "test.base.\(UUID().uuidString)"
        let date = Date(timeIntervalSince1970: 1_650_000_000)
        UserDefaults.standard.set(date, forKey: "\(key).lastSync")
        UserDefaults.standard.set(date, forKey: "\(key).lastSyncCompletedAt")
        defer {
            UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
            UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
        }

        let manager = TestSyncManager(persistenceKey: key)
        XCTAssertEqual(manager.lastSync, date)
        XCTAssertEqual(manager.lastSyncCompletedAt, date)
    }
}

final class SyncConnectionErrorHeuristicTests: XCTestCase {
    func test_recognisesConnectionishKeywords() {
        struct Boom: LocalizedError { let errorDescription: String? }
        XCTAssertTrue(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "Network is unreachable")))
        XCTAssertTrue(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "The connection timed out")))
        XCTAssertTrue(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "Could not connect to host")))
        XCTAssertFalse(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "Invalid JSON payload")))
        XCTAssertFalse(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "401 Unauthorized")))
    }
}

final class AppleScriptEscapeTests: XCTestCase {
    func test_escapesBackslashesAndQuotes() {
        XCTAssertEqual(AppleScriptEscape.quote("hello"), "hello")
        XCTAssertEqual(AppleScriptEscape.quote(#"say "hi""#), #"say \"hi\""#)
        XCTAssertEqual(AppleScriptEscape.quote(#"C:\path"#), #"C:\\path"#)
        XCTAssertEqual(AppleScriptEscape.quote("line1\nline2"), #"line1\nline2"#)
    }
}
