import Foundation
import Testing
@testable import CodexBarCore

struct CodexOAuthCreditLimitTests {
    private struct StubFetchStrategy: ProviderFetchStrategy {
        let id = "stub.cli"
        let kind: ProviderFetchKind = .cli
        let available: Bool
        let result: ProviderFetchResult?

        func isAvailable(_: ProviderFetchContext) async -> Bool {
            self.available
        }

        func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult {
            guard let result else { throw UsageError.noRateLimitsFound }
            return result
        }

        func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
            false
        }
    }

    private func makeContext(
        sourceMode: ProviderSourceMode = .auto,
        includeCredits: Bool = true,
        managedWorkspaceAccountID: String? = nil) -> ProviderFetchContext
    {
        let browserDetection = BrowserDetection(cacheTTL: 0)
        let settings = managedWorkspaceAccountID.map { accountID in
            ProviderSettingsSnapshot.make(codex: CodexProviderSettings(
                usageDataSource: .auto,
                cookieSource: .off,
                manualCookieHeader: nil,
                managedWorkspaceAccountID: accountID))
        }
        return ProviderFetchContext(
            runtime: .app,
            sourceMode: sourceMode,
            includeCredits: includeCredits,
            webTimeout: 60,
            webDebugDumpHTML: false,
            verbose: false,
            env: [:],
            settings: settings,
            fetcher: UsageFetcher(),
            claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
            browserDetection: browserDetection)
    }

    private func makeCredentials() -> CodexOAuthCredentials {
        CodexOAuthCredentials(
            accessToken: "access",
            refreshToken: "refresh",
            idToken: nil,
            accountId: nil,
            lastRefresh: Date())
    }

    private func makeCLIResult(
        credits: CreditsSnapshot?,
        email: String? = nil) -> ProviderFetchResult
    {
        ProviderFetchResult(
            usage: UsageSnapshot(
                primary: nil,
                secondary: nil,
                updatedAt: Date(),
                identity: email.map {
                    ProviderIdentitySnapshot(
                        providerID: .codex,
                        accountEmail: $0,
                        accountOrganization: nil,
                        loginMethod: "enterprise")
                }),
            credits: credits,
            dashboard: nil,
            sourceLabel: "codex-cli",
            strategyID: "stub.cli",
            strategyKind: .cli)
    }

    private func replacingIdentity(
        _ result: ProviderFetchResult,
        email: String) -> ProviderFetchResult
    {
        ProviderFetchResult(
            usage: result.usage.withIdentity(ProviderIdentitySnapshot(
                providerID: .codex,
                accountEmail: email,
                accountOrganization: nil,
                loginMethod: "enterprise")),
            credits: result.credits,
            dashboard: result.dashboard,
            sourceLabel: result.sourceLabel,
            strategyID: result.strategyID,
            strategyKind: result.strategyKind)
    }

    private func makeMonthlyLimitCredits() -> CreditsSnapshot {
        let now = Date()
        let limit = CodexCreditLimitSnapshot(
            used: 250,
            limit: 1000,
            remainingPercent: 75,
            resetsAt: nil,
            updatedAt: now)
        return CreditsSnapshot(
            remaining: limit.remaining,
            events: [],
            updatedAt: now,
            codexCreditLimit: limit)
    }

    private func oauthZeroCreditRateWindowJSON() -> String {
        """
        {
          "rate_limit": {
            "primary_window": {
              "used_percent": 12,
              "reset_at": 1766948068,
              "limit_window_seconds": 18000
            },
            "secondary_window": null
          },
          "credits": {
            "has_credits": true,
            "unlimited": false,
            "balance": "0"
          }
        }
        """
    }

    @Test
    func `decodes monthly credit limit from rate limit payload`() throws {
        let json = """
        {
          "plan_type": "enterprise",
          "rate_limit": {
            "primary_window": null,
            "secondary_window": null,
            "individual_limit": {
              "limit": 100000,
              "used": "7761",
              "remaining_percent": 92.239,
              "resets_at": 1782864000
            }
          },
          "credits": {
            "has_credits": true,
            "unlimited": false,
            "balance": "0"
          }
        }
        """
        let response = try CodexOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
        #expect(response.rateLimit?.individualLimit?.limit == 100_000)
        #expect(response.rateLimit?.individualLimit?.used == 7761)
        #expect(response.rateLimit?.individualLimit?.remainingPercent == 92.239)
        #expect(response.rateLimit?.individualLimit?.resetsAt == 1_782_864_000)
    }

    @Test
    func `monthly credit limit O auth payload displays limit when balance is zero`() throws {
        let json = """
        {
          "rate_limit": {
            "primary_window": null,
            "secondary_window": null,
            "individual_limit": {
              "limit": 100000,
              "used": 7761,
              "remaining_percent": 92.239,
              "resets_at": 1782864000
            }
          },
          "credits": {
            "has_credits": true,
            "unlimited": false,
            "balance": "0"
          }
        }
        """
        let creds = self.makeCredentials()

        let result = try CodexOAuthFetchStrategy._mapResultForTesting(Data(json.utf8), credentials: creds)

        #expect(result.credits?.remaining == 0)
        #expect(result.credits?.codexCreditLimit?.remaining == 92239)
        #expect(result.credits?.codexCreditLimit?.remainingPercent == 92.239)
        #expect(result.sourceLabel == "oauth")
    }

    @Test
    func `explicit O auth zero credits without monthly limit keeps partial result`() throws {
        let json = """
        {
          "rate_limit": {
            "primary_window": null,
            "secondary_window": null
          },
          "credits": {
            "has_credits": true,
            "unlimited": false,
            "balance": "0"
          }
        }
        """
        let result = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(json.utf8),
            credentials: self.makeCredentials(),
            sourceMode: .oauth)

        #expect(result.credits?.remaining == 0)
        #expect(result.credits?.codexCreditLimit == nil)
        #expect(result.sourceLabel == "oauth")
    }

    @Test
    func `auto O auth zero credits preserves O auth usage while adding CLI monthly limit`() async throws {
        let mappedOAuth = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.oauthZeroCreditRateWindowJSON().utf8),
            credentials: self.makeCredentials(),
            sourceMode: .auto)
        let oauthResult = self.replacingIdentity(mappedOAuth, email: "owner@example.com")
        let cliResult = self.makeCLIResult(
            credits: self.makeMonthlyLimitCredits(),
            email: "owner@example.com")

        let result = try await CodexOAuthFetchStrategy._replaceWithCLIMonthlyLimitForTesting(
            oauthResult: oauthResult,
            context: self.makeContext(sourceMode: .auto),
            cliStrategy: StubFetchStrategy(available: true, result: cliResult))

        #expect(oauthResult.usage.primary != nil)
        #expect(CodexOAuthFetchStrategy._shouldTryCLIForMonthlyLimitForTesting(oauthResult))
        #expect(result.sourceLabel == "oauth")
        #expect(result.strategyKind == .oauth)
        #expect(result.usage.primary == oauthResult.usage.primary)
        #expect(result.credits?.remaining == oauthResult.credits?.remaining)
        #expect(result.credits?.codexCreditLimit?.remaining == 750)
    }

    @Test
    func `usage-only O auth refresh does not launch CLI monthly limit enrichment`() async throws {
        let mappedOAuth = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.oauthZeroCreditRateWindowJSON().utf8),
            credentials: self.makeCredentials(),
            sourceMode: .auto)
        let oauthResult = self.replacingIdentity(mappedOAuth, email: "owner@example.com")
        let cliResult = self.makeCLIResult(
            credits: self.makeMonthlyLimitCredits(),
            email: "owner@example.com")

        let result = try await CodexOAuthFetchStrategy._replaceWithCLIMonthlyLimitForTesting(
            oauthResult: oauthResult,
            context: self.makeContext(sourceMode: .auto, includeCredits: false),
            cliStrategy: StubFetchStrategy(available: true, result: cliResult))

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.primary == oauthResult.usage.primary)
        #expect(result.credits?.codexCreditLimit == nil)
    }

    @Test
    func `managed workspace O auth does not mix in unscoped CLI monthly limit`() async throws {
        let mappedOAuth = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.oauthZeroCreditRateWindowJSON().utf8),
            credentials: self.makeCredentials(),
            sourceMode: .auto)
        let oauthResult = self.replacingIdentity(mappedOAuth, email: "owner@example.com")
        let cliResult = self.makeCLIResult(
            credits: self.makeMonthlyLimitCredits(),
            email: "owner@example.com")

        let result = try await CodexOAuthFetchStrategy._replaceWithCLIMonthlyLimitForTesting(
            oauthResult: oauthResult,
            context: self.makeContext(
                sourceMode: .auto,
                managedWorkspaceAccountID: "workspace-team"),
            cliStrategy: StubFetchStrategy(available: true, result: cliResult))

        #expect(result.credits?.codexCreditLimit == nil)
    }

    @Test
    func `auto O auth zero credits rejects CLI monthly limit without verified identity`() async throws {
        let mappedOAuth = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.oauthZeroCreditRateWindowJSON().utf8),
            credentials: self.makeCredentials(),
            sourceMode: .auto)
        let oauthResult = self.replacingIdentity(mappedOAuth, email: "owner@example.com")
        let cliResult = self.makeCLIResult(credits: self.makeMonthlyLimitCredits())

        let result = try await CodexOAuthFetchStrategy._replaceWithCLIMonthlyLimitForTesting(
            oauthResult: oauthResult,
            context: self.makeContext(sourceMode: .auto),
            cliStrategy: StubFetchStrategy(available: true, result: cliResult))

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.identity?.accountEmail == "owner@example.com")
        #expect(result.credits?.codexCreditLimit == nil)
    }

    @Test
    func `auto O auth zero credits rejects CLI monthly limit from another account`() async throws {
        let mappedOAuth = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.oauthZeroCreditRateWindowJSON().utf8),
            credentials: self.makeCredentials(),
            sourceMode: .auto)
        let oauthResult = self.replacingIdentity(mappedOAuth, email: "owner@example.com")
        let cliResult = self.makeCLIResult(
            credits: self.makeMonthlyLimitCredits(),
            email: "other@example.com")

        let result = try await CodexOAuthFetchStrategy._replaceWithCLIMonthlyLimitForTesting(
            oauthResult: oauthResult,
            context: self.makeContext(sourceMode: .auto),
            cliStrategy: StubFetchStrategy(available: true, result: cliResult))

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.identity?.accountEmail == "owner@example.com")
        #expect(result.credits?.codexCreditLimit == nil)
    }

    @Test
    func `auto O auth zero credits accepts matching CLI account case insensitively`() async throws {
        let mappedOAuth = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.oauthZeroCreditRateWindowJSON().utf8),
            credentials: self.makeCredentials(),
            sourceMode: .auto)
        let oauthResult = self.replacingIdentity(mappedOAuth, email: "Owner@Example.com")
        let cliResult = self.makeCLIResult(
            credits: self.makeMonthlyLimitCredits(),
            email: " owner@example.COM ")

        let result = try await CodexOAuthFetchStrategy._replaceWithCLIMonthlyLimitForTesting(
            oauthResult: oauthResult,
            context: self.makeContext(sourceMode: .auto),
            cliStrategy: StubFetchStrategy(available: true, result: cliResult))

        #expect(result.sourceLabel == "oauth")
        #expect(result.usage.identity?.accountEmail == "Owner@Example.com")
        #expect(result.credits?.codexCreditLimit?.remaining == 750)
    }

    @Test
    func `auto O auth zero credits keeps partial result when CLI is unavailable`() async throws {
        let oauthResult = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.oauthZeroCreditRateWindowJSON().utf8),
            credentials: self.makeCredentials(),
            sourceMode: .auto)

        let result = try await CodexOAuthFetchStrategy._replaceWithCLIMonthlyLimitForTesting(
            oauthResult: oauthResult,
            context: self.makeContext(sourceMode: .auto),
            cliStrategy: StubFetchStrategy(available: false, result: nil))

        #expect(result.sourceLabel == "oauth")
        #expect(result.credits?.remaining == 0)
        #expect(result.usage.primary != nil)
    }

    @Test
    func `auto O auth zero credits keeps partial result when CLI lacks monthly limit`() async throws {
        let oauthResult = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.oauthZeroCreditRateWindowJSON().utf8),
            credentials: self.makeCredentials(),
            sourceMode: .auto)
        let cliResult = self.makeCLIResult(credits: CreditsSnapshot(
            remaining: 0,
            events: [],
            updatedAt: Date()))

        let result = try await CodexOAuthFetchStrategy._replaceWithCLIMonthlyLimitForTesting(
            oauthResult: oauthResult,
            context: self.makeContext(sourceMode: .auto),
            cliStrategy: StubFetchStrategy(available: true, result: cliResult))

        #expect(result.sourceLabel == "oauth")
        #expect(result.credits?.codexCreditLimit == nil)
    }

    /// Team workspaces nest the monthly pool under `spend_control.individual_limit` and spell the
    /// reset timestamp `reset_at`; the root and `rate_limit` spellings are both absent.
    private func teamSpendControlJSON() -> String {
        """
        {
          "plan_type": "team",
          "rate_limit": {
            "allowed": false,
            "limit_reached": true,
            "primary_window": {
              "used_percent": 100,
              "limit_window_seconds": 604800,
              "reset_after_seconds": 45962,
              "reset_at": 1786161204
            },
            "secondary_window": null
          },
          "credits": {
            "has_credits": true,
            "unlimited": false,
            "balance": null
          },
          "spend_control": {
            "reached": false,
            "individual_limit": {
              "source": "workspace_spend_controls",
              "limit": "1000",
              "used": "36.79748725891113",
              "remaining": "963.2025127410889",
              "used_percent": 4,
              "remaining_percent": 96,
              "reset_after_seconds": 2105558,
              "reset_at": 1788220800
            }
          }
        }
        """
    }

    @Test
    func `decodes monthly credit limit from spend control payload`() throws {
        let response = try CodexOAuthUsageFetcher._decodeUsageResponseForTesting(
            Data(self.teamSpendControlJSON().utf8))

        #expect(response.individualLimit == nil)
        #expect(response.rateLimit?.individualLimit == nil)
        #expect(response.spendControlIndividualLimit?.limit == 1000)
        #expect(response.spendControlIndividualLimit?.used == 36.79748725891113)
        #expect(response.spendControlIndividualLimit?.remainingPercent == 96)
        #expect(response.spendControlIndividualLimit?.resetsAt == 1_788_220_800)
    }

    @Test
    func `spend control payload surfaces monthly credit limit when balance is null`() throws {
        let result = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(self.teamSpendControlJSON().utf8),
            credentials: self.makeCredentials())

        #expect(result.credits?.codexCreditLimit?.limit == 1000)
        #expect(result.credits?.codexCreditLimit?.used == 36.79748725891113)
        #expect(result.credits?.codexCreditLimit?.remainingPercent == 96)
        #expect(result.credits?.codexCreditLimit?.resetsAt == Date(timeIntervalSince1970: 1_788_220_800))
        #expect(result.sourceLabel == "oauth")
    }

    private func spendControlBlockJSON() -> String {
        """
        "spend_control": {
          "individual_limit": {
            "limit": "1000",
            "used": "36.79748725891113",
            "remaining_percent": 96,
            "reset_at": 1788220800
          }
        }
        """
    }

    @Test
    func `root individual limit still wins over spend control`() throws {
        let json = """
        {
          "plan_type": "team",
          "individual_limit": {
            "limit": 500,
            "used": 100,
            "remaining_percent": 80,
            "resets_at": 1782864000
          },
          \(self.spendControlBlockJSON())
        }
        """
        let result = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(json.utf8),
            credentials: self.makeCredentials())

        #expect(result.credits?.codexCreditLimit?.limit == 500)
        #expect(result.credits?.codexCreditLimit?.resetsAt == Date(timeIntervalSince1970: 1_782_864_000))
    }

    @Test
    func `rate limit individual limit still wins over spend control`() throws {
        let json = """
        {
          "plan_type": "team",
          "rate_limit": {
            "primary_window": null,
            "secondary_window": null,
            "individual_limit": {
              "limit": 100000,
              "used": 7761,
              "remaining_percent": 92.239,
              "resets_at": 1782864000
            }
          },
          \(self.spendControlBlockJSON())
        }
        """
        let response = try CodexOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
        #expect(response.rateLimit?.individualLimit?.limit == 100_000)
        #expect(response.spendControlIndividualLimit?.limit == 1000)

        let result = try CodexOAuthFetchStrategy._mapResultForTesting(
            Data(json.utf8),
            credentials: self.makeCredentials())

        #expect(result.credits?.codexCreditLimit?.limit == 100_000)
        #expect(result.credits?.codexCreditLimit?.remaining == 92239)
        #expect(result.credits?.codexCreditLimit?.resetsAt == Date(timeIntervalSince1970: 1_782_864_000))
    }
}
