import Foundation
import Testing
@testable import CodexBarCore

@Suite(.serialized)
struct ClaudeOAuthHistoryCredentialRoutingTests {
    @Test
    func `history keychain reference only matches the credential that won routing`() throws {
        let keychainData = self.makeCredentialsData(accessToken: "keychain-token")
        let keychainCredentials = try ClaudeOAuthCredentials.parse(data: keychainData)
        let differentCredentials = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "different-token"))
        let fingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
            modifiedAt: 1,
            createdAt: 1,
            persistentRefHash: "opaque-ref")

        let matchingCLIRecord = ClaudeOAuthCredentialRecord(
            credentials: keychainCredentials,
            owner: .claudeCLI,
            source: .memoryCache)
        let differentCLIRecord = ClaudeOAuthCredentialRecord(
            credentials: differentCredentials,
            owner: .claudeCLI,
            source: .credentialsFile)
        let matchingEnvironmentRecord = ClaudeOAuthCredentialRecord(
            credentials: keychainCredentials,
            owner: .environment,
            source: .environment)
        let matchingCodexBarRecord = ClaudeOAuthCredentialRecord(
            credentials: keychainCredentials,
            owner: .codexbar,
            source: .cacheKeychain)

        ProviderInteractionContext.$current.withValue(.userInitiated) {
            ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
                ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
                    data: keychainData,
                    fingerprint: fingerprint)
                {
                    #expect(ClaudeOAuthCredentialsStore
                        .matchingClaudeKeychainPersistentRefHashWithoutPrompt(for: matchingCLIRecord) == "opaque-ref")
                    #expect(ClaudeOAuthCredentialsStore
                        .matchingClaudeKeychainPersistentRefHashWithoutPrompt(for: differentCLIRecord) == nil)
                    #expect(ClaudeOAuthCredentialsStore
                        .matchingClaudeKeychainPersistentRefHashWithoutPrompt(for: matchingEnvironmentRecord) == nil)
                    #expect(ClaudeOAuthCredentialsStore
                        .matchingClaudeKeychainPersistentRefHashWithoutPrompt(for: matchingCodexBarRecord) == nil)
                    #expect(ClaudeOAuthCredentialsStore
                        .claudeKeychainCredentialMatchWithoutPrompt(for: matchingCLIRecord) ==
                        .matched(persistentRefHash: "opaque-ref"))
                    #expect(ClaudeOAuthCredentialsStore
                        .claudeKeychainCredentialMatchWithoutPrompt(for: differentCLIRecord) == .mismatch)
                    #expect(ClaudeOAuthCredentialsStore
                        .claudeKeychainCredentialMatchWithoutPrompt(for: matchingEnvironmentRecord) == .notApplicable)
                }
            }
        }

        ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
            data: nil,
            fingerprint: fingerprint)
        {
            let unavailable = ClaudeOAuthCredentialsStore
                .claudeKeychainCredentialMatchWithoutPrompt(for: matchingCLIRecord)
            #expect(unavailable == .unavailable)
            #expect(unavailable.isUnavailable)
            #expect(!unavailable.isMismatch)
        }

        let absentStore = ClaudeOAuthCredentialsStore.ClaudeKeychainOverrideStore()
        ClaudeOAuthCredentialsStore.withMutableClaudeKeychainOverrideStoreForTesting(absentStore) {
            #expect(ClaudeOAuthCredentialsStore
                .claudeKeychainCredentialMatchWithoutPrompt(for: matchingCLIRecord) == .absent)
        }
    }

    @Test
    func `newest duplicate reference cannot label a different winning credential`() throws {
        let winningCredentials = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "winning-token"))
        let newestCandidateCredentials = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "newest-candidate-token"))
        let winningRecord = ClaudeOAuthCredentialRecord(
            credentials: winningCredentials,
            owner: .claudeCLI,
            source: .memoryCache)
        let newestCandidateRecord = ClaudeOAuthCredentialRecord(
            credentials: newestCandidateCredentials,
            owner: .claudeCLI,
            source: .claudeKeychain)

        #expect(ClaudeOAuthCredentialsStore._matchingClaudeKeychainPersistentRefHashForTesting(
            record: winningRecord,
            candidateCredentials: newestCandidateCredentials,
            persistentRefHash: "newest-candidate-ref") == nil)
        #expect(ClaudeOAuthCredentialsStore._matchingClaudeKeychainPersistentRefHashForTesting(
            record: newestCandidateRecord,
            candidateCredentials: newestCandidateCredentials,
            persistentRefHash: "newest-candidate-ref") == "newest-candidate-ref")
    }

    @Test
    func `history owner follows refresh credential across access token rotation`() throws {
        let beforeRefresh = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "access-before", refreshToken: "stable-refresh"))
        let afterRefresh = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "access-after", refreshToken: "stable-refresh"))

        let beforeIdentifier = try #require(beforeRefresh.historyOwnerIdentifier)
        let afterIdentifier = try #require(afterRefresh.historyOwnerIdentifier)
        #expect(beforeIdentifier == afterIdentifier)
        #expect(beforeIdentifier.count == 64)
        #expect(!beforeIdentifier.contains("stable-refresh"))
    }

    @Test
    func `access-only credential replacement rotates history owner`() throws {
        let original = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "original-access"))
        let replacement = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "replacement-access"))

        let originalIdentifier = try #require(original.historyOwnerIdentifier)
        let replacementIdentifier = try #require(replacement.historyOwnerIdentifier)
        #expect(originalIdentifier != replacementIdentifier)
        #expect(!originalIdentifier.contains("original-access"))
        #expect(!replacementIdentifier.contains("replacement-access"))
    }

    @Test
    func `only an explicit refresh lineage can preserve a rotated credential owner`() throws {
        let original = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "access-before", refreshToken: "refresh-before"))
        let rotated = try ClaudeOAuthCredentials.parse(
            data: self.makeCredentialsData(accessToken: "access-after", refreshToken: "refresh-after"))
        let originalIdentifier = try #require(original.historyOwnerIdentifier)
        let rotatedIdentifier = try #require(rotated.historyOwnerIdentifier)
        #expect(originalIdentifier != rotatedIdentifier)

        let refreshProvenRecord = ClaudeOAuthCredentialRecord(
            credentials: rotated,
            owner: .codexbar,
            source: .memoryCache,
            historyOwnerIdentifier: originalIdentifier)
        let unrelatedReplacementRecord = ClaudeOAuthCredentialRecord(
            credentials: rotated,
            owner: .codexbar,
            source: .cacheKeychain)

        #expect(refreshProvenRecord.historyOwnerIdentifier == originalIdentifier)
        #expect(unrelatedReplacementRecord.historyOwnerIdentifier == rotatedIdentifier)
    }

    private func makeCredentialsData(accessToken: String, refreshToken: String? = nil) -> Data {
        let expiresAt = Int(Date(timeIntervalSinceNow: 3600).timeIntervalSince1970 * 1000)
        let refreshTokenJSON = refreshToken.map { "\n            \"refreshToken\": \"\($0)\"," } ?? ""
        return Data("""
        {
          "claudeAiOauth": {
            "accessToken": "\(accessToken)",
            \(refreshTokenJSON)
            "expiresAt": \(expiresAt),
            "scopes": ["user:profile"]
          }
        }
        """.utf8)
    }
}
