import Foundation
import Testing
@testable import CodexBarCore

@Suite(.serialized)
struct ClaudeOAuthDelegatedRefreshRecoveryTests {
    private actor AsyncCounter {
        private var value = 0

        func increment() -> Int {
            self.value += 1
            return self.value
        }

        func current() -> Int {
            self.value
        }
    }

    private actor TokenCapture {
        private var token: String?

        func set(_ token: String) {
            self.token = token
        }

        func get() -> String? {
            self.token
        }
    }

    private static func makeOAuthUsageResponse() throws -> OAuthUsageResponse {
        let json = """
        {
          "five_hour": { "utilization": 7, "resets_at": "2025-12-23T16:00:00.000Z" },
          "seven_day": { "utilization": 21, "resets_at": "2025-12-29T23:00:00.000Z" }
        }
        """
        return try ClaudeOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
    }

    private func makeCredentialsData(accessToken: String, expiresAt: Date, refreshToken: String? = nil) -> Data {
        let millis = Int(expiresAt.timeIntervalSince1970 * 1000)
        let refreshField: String = {
            guard let refreshToken else { return "" }
            return ",\n            \"refreshToken\": \"\(refreshToken)\""
        }()
        let json = """
        {
          "claudeAiOauth": {
            "accessToken": "\(accessToken)",
            "expiresAt": \(millis),
            "scopes": ["user:profile"]\(refreshField)
          }
        }
        """
        return Data(json.utf8)
    }

    @Test
    func `silent keychain repair recovers without delegation`() async throws {
        let delegatedCounter = AsyncCounter()
        let usageResponse = try Self.makeOAuthUsageResponse()
        let tokenCapture = TokenCapture()
        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"

        try await KeychainCacheStore.withServiceOverrideForTesting(service) {
            try await KeychainAccessGate.withTaskOverrideForTesting(false) {
                KeychainCacheStore.setTestStoreForTesting(true)
                defer { KeychainCacheStore.setTestStoreForTesting(false) }

                ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
                defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() }
                ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting()
                defer { ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting() }

                try await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
                    try await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
                        let tempDir = FileManager.default.temporaryDirectory
                            .appendingPathComponent(UUID().uuidString, isDirectory: true)
                        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
                        let fileURL = tempDir.appendingPathComponent("credentials.json")
                        let snapshot = try await ClaudeOAuthCredentialsStore
                            .withCredentialsURLOverrideForTesting(fileURL) {
                                // Seed an expired cache entry owned by Claude CLI, so the initial load delegates
                                // refresh.
                                ClaudeOAuthCredentialsStore.invalidateCache()
                                let expiredData = self.makeCredentialsData(
                                    accessToken: "expired-token",
                                    expiresAt: Date(timeIntervalSinceNow: -3600))
                                let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude)
                                let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry(
                                    data: expiredData,
                                    storedAt: Date(),
                                    owner: .claudeCLI)
                                KeychainCacheStore.store(key: cacheKey, entry: cacheEntry)
                                defer { KeychainCacheStore.clear(key: cacheKey) }

                                // Sanity: setup should be visible to the code under test.
                                // Otherwise it may attempt interactive reads.
                                #expect(ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: [:]) == true)

                                // Simulate Claude CLI writing fresh credentials into the Claude Code keychain entry.
                                let freshData = self.makeCredentialsData(
                                    accessToken: "fresh-token",
                                    expiresAt: Date(timeIntervalSinceNow: 3600))
                                let fingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
                                    modifiedAt: 1,
                                    createdAt: 1,
                                    persistentRefHash: "test")

                                let fetcher = ClaudeUsageFetcher(
                                    browserDetection: BrowserDetection(cacheTTL: 0),
                                    environment: [:],
                                    dataSource: .oauth,
                                    oauthKeychainPromptCooldownEnabled: true)

                                let fetchOverride: @Sendable (
                                    String,
                                    Bool) async throws -> OAuthUsageResponse = { token, _ in
                                    await tokenCapture.set(token)
                                    return usageResponse
                                }
                                let delegatedOverride: (@Sendable (
                                    Date,
                                    TimeInterval,
                                    [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? =
                                    { _, _, _ in
                                        _ = await delegatedCounter.increment()
                                        return .attemptedSucceeded
                                    }

                                let snapshot = try await ClaudeOAuthKeychainPromptPreference
                                    .withTaskOverrideForTesting(.onlyOnUserAction) {
                                        try await ProviderInteractionContext.$current.withValue(.userInitiated) {
                                            try await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
                                                data: freshData,
                                                fingerprint: fingerprint)
                                            {
                                                try await ClaudeUsageFetcher.$fetchOAuthUsageOverride
                                                    .withValue(fetchOverride) {
                                                        try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride
                                                            .withValue(delegatedOverride) {
                                                                try await fetcher.loadLatestUsage(model: "sonnet")
                                                            }
                                                    }
                                            }
                                        }
                                    }

                                // If Claude keychain already contains fresh credentials, we should recover without
                                // needing a
                                // CLI
                                // touch.
                                #expect(await delegatedCounter.current() == 0)
                                #expect(await tokenCapture.get() == "fresh-token")
                                #expect(snapshot.primary.usedPercent == 7)
                                #expect(snapshot.secondary?.usedPercent == 21)
                                return snapshot
                            }
                        _ = snapshot
                    }
                }
            }
        }
    }

    @Test
    func `delegated refresh attempted succeeded recovers after keychain sync`() async throws {
        let delegatedCounter = AsyncCounter()
        let usageResponse = try Self.makeOAuthUsageResponse()
        let tokenCapture = TokenCapture()
        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"

        try await KeychainCacheStore.withServiceOverrideForTesting(service) {
            try await KeychainAccessGate.withTaskOverrideForTesting(false) {
                KeychainCacheStore.setTestStoreForTesting(true)
                defer { KeychainCacheStore.setTestStoreForTesting(false) }

                ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
                defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() }
                ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting()
                defer { ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting() }

                try await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
                    try await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
                        let tempDir = FileManager.default.temporaryDirectory
                            .appendingPathComponent(UUID().uuidString, isDirectory: true)
                        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
                        let fileURL = tempDir.appendingPathComponent("credentials.json")
                        let snapshot = try await ClaudeOAuthCredentialsStore
                            .withCredentialsURLOverrideForTesting(fileURL) {
                                // Seed an expired cache entry owned by Claude CLI, so the initial load delegates
                                // refresh.
                                ClaudeOAuthCredentialsStore.invalidateCache()
                                let expiredData = self.makeCredentialsData(
                                    accessToken: "expired-token",
                                    expiresAt: Date(timeIntervalSinceNow: -3600))
                                let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude)
                                let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry(
                                    data: expiredData,
                                    storedAt: Date(),
                                    owner: .claudeCLI)
                                KeychainCacheStore.store(key: cacheKey, entry: cacheEntry)
                                defer { KeychainCacheStore.clear(key: cacheKey) }

                                // Ensure we don't silently repair from the Claude keychain before delegation.
                                // Use an explicit empty-data override so we never consult the real system Keychain
                                // during
                                // tests.
                                let stubFingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
                                    modifiedAt: 1,
                                    createdAt: 1,
                                    persistentRefHash: "test")
                                let keychainOverrideStore = ClaudeOAuthCredentialsStore.ClaudeKeychainOverrideStore(
                                    data: Data(),
                                    fingerprint: stubFingerprint)

                                let freshData = self.makeCredentialsData(
                                    accessToken: "fresh-token",
                                    expiresAt: Date(timeIntervalSinceNow: 3600))

                                let fetcher = ClaudeUsageFetcher(
                                    browserDetection: BrowserDetection(cacheTTL: 0),
                                    environment: [:],
                                    dataSource: .oauth,
                                    oauthKeychainPromptCooldownEnabled: true)

                                let fetchOverride: @Sendable (
                                    String,
                                    Bool) async throws -> OAuthUsageResponse = { token, _ in
                                    await tokenCapture.set(token)
                                    return usageResponse
                                }

                                let delegatedOverride: (@Sendable (
                                    Date,
                                    TimeInterval,
                                    [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? =
                                    { _, _, _ in
                                        // Simulate Claude CLI writing fresh credentials after the delegated refresh
                                        // touch.
                                        keychainOverrideStore.data = freshData
                                        keychainOverrideStore.fingerprint = stubFingerprint
                                        _ = await delegatedCounter.increment()
                                        return .attemptedSucceeded
                                    }

                                let snapshot = try await ClaudeOAuthKeychainPromptPreference
                                    .withTaskOverrideForTesting(.always) {
                                        try await ProviderInteractionContext.$current.withValue(.userInitiated) {
                                            try await ClaudeOAuthCredentialsStore
                                                .withMutableClaudeKeychainOverrideStoreForTesting(
                                                    keychainOverrideStore)
                                                {
                                                    try await ClaudeUsageFetcher.$fetchOAuthUsageOverride
                                                        .withValue(fetchOverride) {
                                                            try await ClaudeUsageFetcher
                                                                .$delegatedRefreshAttemptOverride
                                                                .withValue(delegatedOverride) {
                                                                    try await fetcher.loadLatestUsage(model: "sonnet")
                                                                }
                                                        }
                                                }
                                        }
                                    }

                                #expect(await delegatedCounter.current() == 1)
                                let capturedToken = await tokenCapture.get()
                                if capturedToken != "fresh-token" {
                                    Issue.record("Expected fresh-token, got \(capturedToken ?? "nil")")
                                }
                                #expect(capturedToken == "fresh-token")
                                #expect(snapshot.primary.usedPercent == 7)
                                #expect(snapshot.secondary?.usedPercent == 21)
                                return snapshot
                            }
                        _ = snapshot
                    }
                }
            }
        }
    }

    @Test
    func `delegated refresh attempted succeeded background only on user action does not recover from keychain`()
        async throws
    {
        let delegatedCounter = AsyncCounter()
        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"

        try await KeychainCacheStore.withServiceOverrideForTesting(service) {
            try await KeychainAccessGate.withTaskOverrideForTesting(false) {
                KeychainCacheStore.setTestStoreForTesting(true)
                defer { KeychainCacheStore.setTestStoreForTesting(false) }

                ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
                defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() }
                ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting()
                defer { ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting() }

                try await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
                    try await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
                        let tempDir = FileManager.default.temporaryDirectory
                            .appendingPathComponent(UUID().uuidString, isDirectory: true)
                        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
                        let fileURL = tempDir.appendingPathComponent("credentials.json")

                        await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) {
                            ClaudeOAuthCredentialsStore.invalidateCache()
                            let expiredData = self.makeCredentialsData(
                                accessToken: "expired-token",
                                expiresAt: Date(timeIntervalSinceNow: -3600))
                            let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude)
                            let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry(
                                data: expiredData,
                                storedAt: Date(),
                                owner: .claudeCLI)
                            KeychainCacheStore.store(key: cacheKey, entry: cacheEntry)
                            defer { KeychainCacheStore.clear(key: cacheKey) }

                            // Expired Claude-CLI-owned credentials are still considered cache-present (delegatable).
                            #expect(ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: [:]) == true)

                            let stubFingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
                                modifiedAt: 1,
                                createdAt: 1,
                                persistentRefHash: "test")
                            let keychainOverrideStore = ClaudeOAuthCredentialsStore.ClaudeKeychainOverrideStore(
                                data: Data(),
                                fingerprint: stubFingerprint)
                            let freshData = self.makeCredentialsData(
                                accessToken: "fresh-token",
                                expiresAt: Date(timeIntervalSinceNow: 3600))

                            let fetcher = ClaudeUsageFetcher(
                                browserDetection: BrowserDetection(cacheTTL: 0),
                                environment: [:],
                                dataSource: .oauth,
                                oauthKeychainPromptCooldownEnabled: false,
                                allowBackgroundDelegatedRefresh: true)

                            let delegatedOverride: (@Sendable (
                                Date,
                                TimeInterval,
                                [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? =
                                { _, _, _ in
                                    keychainOverrideStore.data = freshData
                                    keychainOverrideStore.fingerprint = stubFingerprint
                                    _ = await delegatedCounter.increment()
                                    return .attemptedSucceeded
                                }

                            do {
                                _ = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
                                    .onlyOnUserAction)
                                {
                                    try await ProviderInteractionContext.$current.withValue(.background) {
                                        try await ClaudeOAuthCredentialsStore
                                            .withMutableClaudeKeychainOverrideStoreForTesting(keychainOverrideStore) {
                                                try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride
                                                    .withValue(delegatedOverride) {
                                                        try await fetcher.loadLatestUsage(model: "sonnet")
                                                    }
                                            }
                                    }
                                }
                                Issue.record(
                                    "Expected OAuth fetch failure: background keychain recovery should stay blocked")
                            } catch let error as ClaudeUsageError {
                                guard case let .oauthFailed(message) = error else {
                                    Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)")
                                    return
                                }
                                #expect(message.contains("still unavailable after delegated Claude CLI refresh"))
                            } catch {
                                Issue.record("Expected ClaudeUsageError, got \(error)")
                            }

                            #expect(await delegatedCounter.current() == 1)
                        }
                    }
                }
            }
        }
    }
}
