import Foundation
import Testing
@testable import CodexBarCore

@Suite(.serialized)
struct PerplexityProviderTests {
    private static let now = Date(timeIntervalSince1970: 1_740_000_000)

    private final class LockedArray<Element>: @unchecked Sendable {
        private let lock = NSLock()
        private var values: [Element] = []

        func append(_ value: Element) {
            self.lock.lock()
            defer { self.lock.unlock() }
            self.values.append(value)
        }

        func snapshot() -> [Element] {
            self.lock.lock()
            defer { self.lock.unlock() }
            return self.values
        }
    }

    private final class LockedCounter: @unchecked Sendable {
        private let lock = NSLock()
        private var value: Int = 0

        func increment() {
            self.lock.lock()
            defer { self.lock.unlock() }
            self.value += 1
        }

        func snapshot() -> Int {
            self.lock.lock()
            defer { self.lock.unlock() }
            return self.value
        }
    }

    private struct StubClaudeFetcher: ClaudeUsageFetching {
        func loadLatestUsage(model _: String) async throws -> ClaudeUsageSnapshot {
            throw ClaudeUsageError.parseFailed("stub")
        }

        func debugRawProbe(model _: String) async -> String {
            "stub"
        }

        func detectVersion() -> String? {
            nil
        }
    }

    private func makeContext(
        settings: ProviderSettingsSnapshot?,
        env: [String: String] = [:]) -> ProviderFetchContext
    {
        ProviderFetchContext(
            runtime: .app,
            sourceMode: .auto,
            includeCredits: false,
            webTimeout: 1,
            webDebugDumpHTML: false,
            verbose: false,
            env: env,
            settings: settings,
            fetcher: UsageFetcher(environment: env),
            claudeFetcher: StubClaudeFetcher(),
            browserDetection: BrowserDetection(cacheTTL: 0))
    }

    private func stubSnapshot(now: Date = Self.now) -> PerplexityUsageSnapshot {
        PerplexityUsageSnapshot(
            response: PerplexityCreditsResponse(
                balanceCents: 500,
                renewalDateTs: now.addingTimeInterval(3600).timeIntervalSince1970,
                currentPeriodPurchasedCents: 0,
                creditGrants: [
                    PerplexityCreditGrant(type: "recurring", amountCents: 1000, expiresAtTs: nil),
                ],
                totalUsageCents: 500),
            now: now)
    }

    private func withIsolatedCacheStore<T>(operation: () async throws -> T) async rethrows -> T {
        let service = "perplexity-provider-tests-\(UUID().uuidString)"
        return try await KeychainCacheStore.withServiceOverrideForTesting(service) {
            KeychainCacheStore.setTestStoreForTesting(true)
            defer { KeychainCacheStore.setTestStoreForTesting(false) }
            return try await operation()
        }
    }

    @Test
    func `off mode ignores environment session cookie`() async {
        let strategy = PerplexityWebFetchStrategy()
        let settings = ProviderSettingsSnapshot.make(
            perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
                cookieSource: .off,
                manualCookieHeader: nil))
        let context = self.makeContext(
            settings: settings,
            env: ["PERPLEXITY_COOKIE": "authjs.session-token=env-token"])

        #expect(await strategy.isAvailable(context) == false)
    }

    @Test
    func `manual mode invalid cookie does not fall back to cache or environment`() async {
        await self.withIsolatedCacheStore {
            CookieHeaderCache.store(
                provider: .perplexity,
                cookieHeader: "\(PerplexityCookieHeader.defaultSessionCookieName)=cached-token",
                sourceLabel: "web")

            let strategy = PerplexityWebFetchStrategy()
            let settings = ProviderSettingsSnapshot.make(
                perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
                    cookieSource: .manual,
                    manualCookieHeader: "foo=bar"))
            let context = self.makeContext(
                settings: settings,
                env: ["PERPLEXITY_COOKIE": "authjs.session-token=env-token"])
            let fetchOverride: @Sendable (String, String, Date) async throws -> PerplexityUsageSnapshot = { _, _, _ in
                self.stubSnapshot()
            }

            do {
                _ = try await PerplexityUsageFetcher.$fetchCreditsOverride.withValue(fetchOverride, operation: {
                    try await strategy.fetch(context)
                })
                Issue.record("Expected invalid manual-cookie error instead of falling back to cache/environment")
            } catch let error as PerplexityAPIError {
                #expect(error == .invalidCookie)
            } catch {
                Issue.record("Expected PerplexityAPIError.invalidCookie, got \(error)")
            }
        }
    }

    @Test
    func `environment token does not populate browser cookie cache`() async throws {
        try await self.withIsolatedCacheStore {
            PerplexityCookieImporter.invalidateImportSessionCache()
            defer {
                PerplexityCookieImporter.invalidateImportSessionCache()
            }

            try await PerplexityCookieImporter.withImportSessionOverrideForTesting { _, _ in
                throw PerplexityCookieImportError.noCookies
            } operation: {
                let strategy = PerplexityWebFetchStrategy()
                let settings = ProviderSettingsSnapshot.make(
                    perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
                        cookieSource: .auto,
                        manualCookieHeader: nil))
                let context = self.makeContext(
                    settings: settings,
                    env: ["PERPLEXITY_COOKIE": "authjs.session-token=env-token"])
                let fetchOverride: @Sendable (String, String, Date) async throws
                    -> PerplexityUsageSnapshot = { _, _, _ in
                        self.stubSnapshot()
                    }

                _ = try await PerplexityUsageFetcher.$fetchCreditsOverride.withValue(fetchOverride, operation: {
                    try await strategy.fetch(context)
                })

                #expect(CookieHeaderCache.load(provider: .perplexity) == nil)
            }
        }
    }

    @Test
    func `manual token does not populate browser cookie cache`() async throws {
        try await self.withIsolatedCacheStore {
            let strategy = PerplexityWebFetchStrategy()
            let settings = ProviderSettingsSnapshot.make(
                perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
                    cookieSource: .manual,
                    manualCookieHeader: "authjs.session-token=manual-token"))
            let context = self.makeContext(settings: settings)
            let fetchOverride: @Sendable (String, String, Date) async throws -> PerplexityUsageSnapshot = { _, _, _ in
                self.stubSnapshot()
            }

            _ = try await PerplexityUsageFetcher.$fetchCreditsOverride.withValue(fetchOverride, operation: {
                try await strategy.fetch(context)
            })

            #expect(CookieHeaderCache.load(provider: .perplexity) == nil)
        }
    }

    @Test
    func `bare environment token falls back to auth JS cookie name`() async throws {
        try await self.withIsolatedCacheStore {
            PerplexityCookieImporter.invalidateImportSessionCache()
            defer {
                PerplexityCookieImporter.invalidateImportSessionCache()
            }

            try await PerplexityCookieImporter.withImportSessionOverrideForTesting { _, _ in
                throw PerplexityCookieImportError.noCookies
            } operation: {
                let attemptedCookieNames = LockedArray<String>()
                let strategy = PerplexityWebFetchStrategy()
                let settings = ProviderSettingsSnapshot.make(
                    perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
                        cookieSource: .auto,
                        manualCookieHeader: nil))
                let context = self.makeContext(
                    settings: settings,
                    env: ["PERPLEXITY_SESSION_TOKEN": "env-token"])
                let fetchOverride: @Sendable (String, String, Date) async throws
                    -> PerplexityUsageSnapshot = { token, cookieName, _ in
                        #expect(token == "env-token")
                        attemptedCookieNames.append(cookieName)
                        if cookieName == "authjs.session-token" {
                            return self.stubSnapshot()
                        }
                        throw PerplexityAPIError.invalidToken
                    }

                _ = try await PerplexityUsageFetcher.$fetchCreditsOverride.withValue(fetchOverride, operation: {
                    try await strategy.fetch(context)
                })

                #expect(attemptedCookieNames.snapshot() == [
                    "__Secure-authjs.session-token",
                    "authjs.session-token",
                ])
            }
        }
    }

    @Test
    func `valid environment cookie wins after invalid browser session`() async throws {
        try await self.withIsolatedCacheStore {
            PerplexityCookieImporter.invalidateImportSessionCache()
            defer {
                PerplexityCookieImporter.invalidateImportSessionCache()
            }

            try await PerplexityCookieImporter.withImportSessionOverrideForTesting { _, _ in
                let cookie = try #require(HTTPCookie(properties: [
                    .domain: "www.perplexity.ai",
                    .path: "/",
                    .name: PerplexityCookieHeader.defaultSessionCookieName,
                    .value: "browser-token",
                    .secure: "TRUE",
                ]))
                return PerplexityCookieImporter.SessionInfo(cookies: [cookie], sourceLabel: "Chrome")
            } operation: {
                let attemptedTokens = LockedArray<String>()
                let strategy = PerplexityWebFetchStrategy()
                let settings = ProviderSettingsSnapshot.make(
                    perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
                        cookieSource: .auto,
                        manualCookieHeader: nil))
                let context = self.makeContext(
                    settings: settings,
                    env: ["PERPLEXITY_COOKIE": "authjs.session-token=env-token"])
                let fetchOverride: @Sendable (String, String, Date) async throws
                    -> PerplexityUsageSnapshot = { token, _, _ in
                        attemptedTokens.append(token)
                        if token == "browser-token" {
                            throw PerplexityAPIError.invalidToken
                        }
                        if token == "env-token" {
                            return self.stubSnapshot()
                        }
                        Issue.record("Unexpected token \(token)")
                        throw PerplexityAPIError.invalidToken
                    }

                _ = try await PerplexityUsageFetcher.$fetchCreditsOverride.withValue(fetchOverride, operation: {
                    try await strategy.fetch(context)
                })

                #expect(attemptedTokens.snapshot() == ["browser-token", "env-token"])
            }
        }
    }

    @Test
    func `later browser session wins after earlier imported session fails auth`() async throws {
        try await self.withIsolatedCacheStore {
            PerplexityCookieImporter.invalidateImportSessionCache()
            defer {
                PerplexityCookieImporter.invalidateImportSessionCache()
            }

            try await PerplexityCookieImporter.withImportSessionsOverrideForTesting { _, _ in
                let staleCookie = try #require(HTTPCookie(properties: [
                    .domain: "www.perplexity.ai",
                    .path: "/",
                    .name: "__Secure-authjs.session-token",
                    .value: "stale-browser-token",
                    .secure: "TRUE",
                ]))
                let liveCookie = try #require(HTTPCookie(properties: [
                    .domain: "www.perplexity.ai",
                    .path: "/",
                    .name: "__Secure-authjs.session-token",
                    .value: "live-browser-token",
                    .secure: "TRUE",
                ]))
                return [
                    PerplexityCookieImporter.SessionInfo(cookies: [staleCookie], sourceLabel: "Chrome"),
                    PerplexityCookieImporter.SessionInfo(cookies: [liveCookie], sourceLabel: "Safari"),
                ]
            } operation: {
                let attemptedTokens = LockedArray<String>()
                let strategy = PerplexityWebFetchStrategy()
                let settings = ProviderSettingsSnapshot.make(
                    perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
                        cookieSource: .auto,
                        manualCookieHeader: nil))
                let context = self.makeContext(settings: settings)
                let fetchOverride: @Sendable (String, String, Date) async throws
                    -> PerplexityUsageSnapshot = { token, _, _ in
                        attemptedTokens.append(token)
                        if token == "stale-browser-token" {
                            throw PerplexityAPIError.invalidToken
                        }
                        if token == "live-browser-token" {
                            return self.stubSnapshot()
                        }
                        Issue.record("Unexpected token \(token)")
                        throw PerplexityAPIError.invalidToken
                    }

                _ = try await PerplexityUsageFetcher.$fetchCreditsOverride.withValue(fetchOverride, operation: {
                    try await strategy.fetch(context)
                })

                #expect(attemptedTokens.snapshot() == ["stale-browser-token", "live-browser-token"])
            }
        }
    }

    @Test
    func `auto mode reuses browser import between availability and fetch`() async throws {
        try await self.withIsolatedCacheStore {
            let importCount = LockedCounter()
            PerplexityCookieImporter.invalidateImportSessionCache()
            defer {
                PerplexityCookieImporter.invalidateImportSessionCache()
            }

            try await PerplexityCookieImporter.withImportSessionOverrideForTesting { _, _ in
                importCount.increment()
                let cookie = try #require(HTTPCookie(properties: [
                    .domain: "www.perplexity.ai",
                    .path: "/",
                    .name: PerplexityCookieHeader.defaultSessionCookieName,
                    .value: "browser-token",
                    .secure: "TRUE",
                ]))
                return PerplexityCookieImporter.SessionInfo(cookies: [cookie], sourceLabel: "Chrome")
            } operation: {
                let strategy = PerplexityWebFetchStrategy()
                let settings = ProviderSettingsSnapshot.make(
                    perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
                        cookieSource: .auto,
                        manualCookieHeader: nil))
                let context = self.makeContext(settings: settings)
                let fetchOverride: @Sendable (String, String, Date) async throws
                    -> PerplexityUsageSnapshot = { token, _, _ in
                        #expect(token == "browser-token")
                        return self.stubSnapshot()
                    }

                #expect(await strategy.isAvailable(context))

                _ = try await PerplexityUsageFetcher.$fetchCreditsOverride.withValue(fetchOverride, operation: {
                    try await strategy.fetch(context)
                })

                #expect(importCount.snapshot() == 1)
            }
        }
    }
}
