import Foundation
import Testing
@testable import CodexBarCore

struct KiloUsageFetcherTests {
    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(
        env: [String: String] = [:],
        sourceMode: ProviderSourceMode = .api) -> ProviderFetchContext
    {
        let browserDetection = BrowserDetection(cacheTTL: 0)
        return ProviderFetchContext(
            runtime: .cli,
            sourceMode: sourceMode,
            includeCredits: false,
            webTimeout: 1,
            webDebugDumpHTML: false,
            verbose: false,
            env: env,
            settings: nil,
            fetcher: UsageFetcher(environment: env),
            claudeFetcher: StubClaudeFetcher(),
            browserDetection: browserDetection)
    }

    @Test
    func `batch URL uses authenticated TRPC batch format`() throws {
        let baseURL = try #require(URL(string: "https://kilo.example/trpc"))
        let url = try KiloUsageFetcher._buildBatchURLForTesting(baseURL: baseURL)

        #expect(url.path.contains("user.getCreditBlocks,kiloPass.getState,user.getAutoTopUpPaymentMethod"))

        let components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false))
        let batch = components.queryItems?.first(where: { $0.name == "batch" })?.value
        let inputValue = components.queryItems?.first(where: { $0.name == "input" })?.value

        #expect(batch == "1")
        let requiredInput = try #require(inputValue)
        let inputData = Data(requiredInput.utf8)
        let inputObject = try #require(try JSONSerialization.jsonObject(with: inputData) as? [String: Any])
        let first = try #require(inputObject["0"] as? [String: Any])
        let second = try #require(inputObject["1"] as? [String: Any])
        let third = try #require(inputObject["2"] as? [String: Any])

        #expect(inputObject.keys.contains("0"))
        #expect(inputObject.keys.contains("1"))
        #expect(inputObject.keys.contains("2"))
        #expect(first["json"] is NSNull)
        #expect(second["json"] is NSNull)
        #expect(third["json"] is NSNull)
    }

    @Test
    func `parse snapshot maps business fields and identity`() throws {
        let json = """
        [
          {
            "result": {
              "data": {
                "json": {
                  "blocks": [
                    {
                      "usedCredits": 25,
                      "totalCredits": 100,
                      "remainingCredits": 75
                    }
                  ]
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "plan": {
                    "name": "Kilo Pass Pro"
                  }
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "enabled": true,
                  "paymentMethod": "visa"
                }
              }
            }
          }
        ]
        """

        let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        let snapshot = parsed.toUsageSnapshot()

        #expect(snapshot.primary?.usedPercent == 25)
        #expect(snapshot.identity?.providerID == .kilo)
        #expect(snapshot.loginMethod(for: .kilo)?.contains("Kilo Pass Pro") == true)
        #expect(snapshot.loginMethod(for: .kilo)?.contains("Auto top-up") == true)
    }

    @Test
    func `parse snapshot maps kilo pass window from subscription state`() throws {
        let json = """
        [
          {
            "result": {
              "data": {
                "creditBlocks": [
                  {
                    "id": "cb-1",
                    "effective_date": "2026-02-01T00:00:00Z",
                    "expiry_date": null,
                    "balance_mUsd": 19000000,
                    "amount_mUsd": 19000000,
                    "is_free": false
                  }
                ],
                "totalBalance_mUsd": 19000000,
                "autoTopUpEnabled": false
              }
            }
          },
          {
            "result": {
              "data": {
                "subscription": {
                  "tier": "tier_19",
                  "currentPeriodUsageUsd": 0,
                  "currentPeriodBaseCreditsUsd": 19.0,
                  "currentPeriodBonusCreditsUsd": 9.5,
                  "nextBillingAt": "2026-03-28T04:00:00.000Z"
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "enabled": false,
                "amountCents": 5000,
                "paymentMethod": null
              }
            }
          }
        ]
        """

        let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        let snapshot = parsed.toUsageSnapshot()

        #expect(snapshot.primary?.usedPercent == 0)
        #expect(snapshot.secondary?.usedPercent == 0)
        #expect(snapshot.secondary?.resetsAt != nil)
        #expect(snapshot.secondary?.resetDescription == "$0.00 / $19.00 (+ $9.50 bonus)")
        #expect(snapshot.loginMethod(for: .kilo) == "Starter · Auto top-up: off")
    }

    @Test
    func `parse snapshot maps known tier names and defaults to kilo pass`() throws {
        let proTierJSON = """
        [
          { "result": { "data": { "creditBlocks": [], "totalBalance_mUsd": 0, "autoTopUpEnabled": false } } },
          { "result": { "data": { "subscription": { "tier": "tier_49" } } } },
          { "result": { "data": { "enabled": false, "paymentMethod": null } } }
        ]
        """
        let proTierSnapshot = try KiloUsageFetcher._parseSnapshotForTesting(Data(proTierJSON.utf8)).toUsageSnapshot()
        #expect(proTierSnapshot.loginMethod(for: .kilo) == "Pro · Auto top-up: off")

        let noTierJSON = """
        [
          { "result": { "data": { "creditBlocks": [], "totalBalance_mUsd": 0, "autoTopUpEnabled": false } } },
          { "result": { "data": { "subscription": {
            "currentPeriodUsageUsd": 1.0,
            "currentPeriodBaseCreditsUsd": 19.0
          } } } },
          { "result": { "data": { "enabled": false, "paymentMethod": null } } }
        ]
        """
        let noTierSnapshot = try KiloUsageFetcher._parseSnapshotForTesting(Data(noTierJSON.utf8)).toUsageSnapshot()
        #expect(noTierSnapshot.loginMethod(for: .kilo) == "Kilo Pass · Auto top-up: off")
    }

    @Test
    func `parse snapshot uses auto top up amount when enabled without payment method`() throws {
        let json = """
        [
          { "result": { "data": { "creditBlocks": [], "totalBalance_mUsd": 0, "autoTopUpEnabled": true } } },
          { "result": { "data": { "subscription": null } } },
          { "result": { "data": { "enabled": true, "amountCents": 5000, "paymentMethod": null } } }
        ]
        """

        let snapshot = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8)).toUsageSnapshot()
        #expect(snapshot.loginMethod(for: .kilo) == "Auto top-up: $50")
    }

    @Test
    func `parse snapshot fallback pass fields use micro dollar scale`() throws {
        let json = """
        [
          {
            "result": {
              "data": {
                "json": {
                  "blocks": [
                    {
                      "usedCredits": 0,
                      "totalCredits": 19,
                      "remainingCredits": 19
                    }
                  ]
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "planName": "Starter",
                  "amount_mUsd": 28500000,
                  "used_mUsd": 3500000,
                  "bonus_mUsd": 9500000,
                  "nextRenewalAt": "2026-03-28T04:00:00.000Z"
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "enabled": false,
                  "paymentMethod": null
                }
              }
            }
          }
        ]
        """

        let snapshot = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8)).toUsageSnapshot()
        #expect(snapshot.secondary?.resetDescription == "$3.50 / $19.00 (+ $9.50 bonus)")
        #expect(snapshot.loginMethod(for: .kilo) == "Starter · Auto top-up: off")
    }

    @Test
    func `parse snapshot treats empty and null business fields as no data success`() throws {
        let json = """
        [
          {
            "result": {
              "data": {
                "json": {
                  "blocks": []
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "plan": {
                    "name": null
                  }
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "enabled": null,
                  "paymentMethod": null
                }
              }
            }
          }
        ]
        """

        let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        let snapshot = parsed.toUsageSnapshot()

        #expect(snapshot.primary == nil)
        #expect(snapshot.identity?.providerID == .kilo)
        #expect(snapshot.loginMethod(for: .kilo) == nil)
    }

    @Test
    func `parse snapshot keeps sparse indexed object routing by procedure index`() throws {
        let json = """
        {
          "0": {
            "result": {
              "data": {
                "json": {
                  "creditsUsed": 10,
                  "creditsRemaining": 90
                }
              }
            }
          },
          "2": {
            "result": {
              "data": {
                "json": {
                  "planName": "wrong-route",
                  "enabled": true,
                  "method": "visa"
                }
              }
            }
          }
        }
        """

        let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        let snapshot = parsed.toUsageSnapshot()

        #expect(snapshot.primary?.usedPercent == 10)
        #expect(snapshot.loginMethod(for: .kilo) == "Auto top-up: visa")
    }

    @Test
    func `parse snapshot uses top level credits used fallback`() throws {
        let json = """
        [
          {
            "result": {
              "data": {
                "json": {
                  "creditsUsed": 40,
                  "creditsRemaining": 60
                }
              }
            }
          }
        ]
        """

        let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        let snapshot = parsed.toUsageSnapshot()

        #expect(snapshot.primary?.usedPercent == 40)
        #expect(snapshot.primary?.resetDescription == "40/100 credits")
    }

    @Test
    func `parse snapshot keeps zero total visible when activity exists`() throws {
        let json = """
        [
          {
            "result": {
              "data": {
                "json": {
                  "creditsUsed": 0,
                  "creditsRemaining": 0
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "planName": "Kilo Pass Pro"
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "enabled": true,
                  "paymentMethod": "visa"
                }
              }
            }
          }
        ]
        """

        let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        let snapshot = parsed.toUsageSnapshot()

        #expect(snapshot.primary?.remainingPercent == 0)
        #expect(snapshot.primary?.usedPercent == 100)
        #expect(snapshot.primary?.resetDescription == "0/0 credits")
        #expect(snapshot.loginMethod(for: .kilo)?.contains("Auto top-up: visa") == true)
    }

    @Test
    func `parse snapshot treats zero balance without credit blocks as visible zero total`() throws {
        let json = """
        [
          {
            "result": {
              "data": {
                "creditBlocks": [],
                "totalBalance_mUsd": 0,
                "isFirstPurchase": true,
                "autoTopUpEnabled": false
              }
            }
          },
          {
            "result": {
              "data": {
                "subscription": null
              }
            }
          },
          {
            "result": {
              "data": {
                "enabled": false,
                "amountCents": 5000,
                "paymentMethod": null
              }
            }
          }
        ]
        """

        let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        let snapshot = parsed.toUsageSnapshot()

        #expect(snapshot.primary?.usedPercent == 100)
        #expect(snapshot.primary?.remainingPercent == 0)
        #expect(snapshot.primary?.resetDescription == "0/0 credits")
        #expect(snapshot.loginMethod(for: .kilo) == "Auto top-up: off")
    }

    @Test
    func `parse snapshot degrades optional auto top up TRPC error`() throws {
        let json = """
        [
          {
            "result": {
              "data": {
                "json": {
                  "creditsUsed": 10,
                  "creditsRemaining": 90
                }
              }
            }
          },
          {
            "result": {
              "data": {
                "json": {
                  "planName": "Starter"
                }
              }
            }
          },
          {
            "error": {
              "json": {
                "message": "Internal server error",
                "data": {
                  "code": "INTERNAL_SERVER_ERROR"
                }
              }
            }
          }
        ]
        """

        let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        let snapshot = parsed.toUsageSnapshot()

        #expect(snapshot.primary?.usedPercent == 10)
        #expect(snapshot.loginMethod(for: .kilo) == "Starter")
    }

    @Test
    func `parse snapshot keeps required procedure TRPC error fatal`() {
        let json = """
        [
          {
            "result": {
              "data": {
                "json": {
                  "creditsUsed": 10,
                  "creditsRemaining": 90
                }
              }
            }
          },
          {
            "error": {
              "json": {
                "message": "Unauthorized",
                "data": {
                  "code": "UNAUTHORIZED"
                }
              }
            }
          }
        ]
        """

        #expect {
            _ = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        } throws: { error in
            guard let kiloError = error as? KiloUsageError else { return false }
            guard case .unauthorized = kiloError else { return false }
            return true
        }
    }

    @Test
    func `parse snapshot maps unauthorized TRPC error`() {
        let json = """
        [
          {
            "error": {
              "json": {
                "message": "Unauthorized",
                "data": {
                  "code": "UNAUTHORIZED"
                }
              }
            }
          }
        ]
        """

        #expect {
            _ = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
        } throws: { error in
            guard let kiloError = error as? KiloUsageError else { return false }
            guard case .unauthorized = kiloError else { return false }
            return true
        }
    }

    @Test
    func `parse snapshot maps invalid JSON to parse error`() {
        #expect {
            _ = try KiloUsageFetcher._parseSnapshotForTesting(Data("not-json".utf8))
        } throws: { error in
            guard let kiloError = error as? KiloUsageError else { return false }
            guard case .parseFailed = kiloError else { return false }
            return true
        }
    }

    @Test
    func `status error mapping covers auth and server failures`() {
        #expect(KiloUsageFetcher._statusErrorForTesting(401) == .unauthorized)
        #expect(KiloUsageFetcher._statusErrorForTesting(403) == .unauthorized)
        #expect(KiloUsageFetcher._statusErrorForTesting(404) == .endpointNotFound)

        guard let serviceError = KiloUsageFetcher._statusErrorForTesting(503) else {
            Issue.record("Expected service unavailable mapping")
            return
        }
        guard case let .serviceUnavailable(statusCode) = serviceError else {
            Issue.record("Expected service unavailable mapping")
            return
        }
        #expect(statusCode == 503)
    }

    @Test
    func `fetch usage without credentials fails fast`() async {
        await #expect(throws: KiloUsageError.missingCredentials) {
            _ = try await KiloUsageFetcher.fetchUsage(apiKey: "  ", environment: [:])
        }
    }

    @Test
    func `descriptor fetch outcome without credentials returns actionable error`() async {
        let descriptor = ProviderDescriptorRegistry.descriptor(for: .kilo)
        let outcome = await descriptor.fetchOutcome(context: self.makeContext())

        switch outcome.result {
        case .success:
            Issue.record("Expected missing credentials failure")
        case let .failure(error):
            #expect((error as? KiloUsageError) == .missingCredentials)
        }

        #expect(outcome.attempts.count == 1)
        #expect(outcome.attempts.first?.strategyID == "kilo.api")
        #expect(outcome.attempts.first?.wasAvailable == true)
    }

    @Test
    func `descriptor API mode ignores CLI session fallback`() async throws {
        let homeDirectory = try self.makeTemporaryHomeDirectory()
        defer { try? FileManager.default.removeItem(at: homeDirectory) }
        try self.writeKiloAuthFile(
            homeDirectory: homeDirectory,
            contents: #"{"kilo":{"access":"file-token"}}"#)

        let descriptor = ProviderDescriptorRegistry.descriptor(for: .kilo)
        let outcome = await descriptor.fetchOutcome(context: self.makeContext(
            env: ["HOME": homeDirectory.path],
            sourceMode: .api))

        switch outcome.result {
        case .success:
            Issue.record("Expected missing API credentials failure")
        case let .failure(error):
            #expect((error as? KiloUsageError) == .missingCredentials)
        }

        #expect(outcome.attempts.count == 1)
        #expect(outcome.attempts.first?.strategyID == "kilo.api")
    }

    @Test
    func `descriptor CLI mode missing session returns actionable error`() async throws {
        let homeDirectory = try self.makeTemporaryHomeDirectory()
        defer { try? FileManager.default.removeItem(at: homeDirectory) }
        let expectedPath = KiloSettingsReader.defaultAuthFileURL(homeDirectory: homeDirectory).path

        let descriptor = ProviderDescriptorRegistry.descriptor(for: .kilo)
        let outcome = await descriptor.fetchOutcome(context: self.makeContext(
            env: ["HOME": homeDirectory.path],
            sourceMode: .cli))

        switch outcome.result {
        case .success:
            Issue.record("Expected missing CLI session failure")
        case let .failure(error):
            #expect((error as? KiloUsageError) == .cliSessionMissing(expectedPath))
        }

        #expect(outcome.attempts.count == 1)
        #expect(outcome.attempts.first?.strategyID == "kilo.cli")
    }

    @Test
    func `descriptor auto mode falls back from API to CLI`() async throws {
        let homeDirectory = try self.makeTemporaryHomeDirectory()
        defer { try? FileManager.default.removeItem(at: homeDirectory) }
        let expectedPath = KiloSettingsReader.defaultAuthFileURL(homeDirectory: homeDirectory).path

        let descriptor = ProviderDescriptorRegistry.descriptor(for: .kilo)
        let outcome = await descriptor.fetchOutcome(context: self.makeContext(
            env: ["HOME": homeDirectory.path],
            sourceMode: .auto))

        switch outcome.result {
        case .success:
            Issue.record("Expected missing CLI session failure after API fallback")
        case let .failure(error):
            #expect((error as? KiloUsageError) == .cliSessionMissing(expectedPath))
        }

        #expect(outcome.attempts.count == 2)
        #expect(outcome.attempts.map(\.strategyID) == ["kilo.api", "kilo.cli"])
    }

    @Test
    func `api strategy falls back on unauthorized only in auto mode`() {
        let strategy = KiloAPIFetchStrategy()
        #expect(strategy.shouldFallback(
            on: KiloUsageError.unauthorized,
            context: self.makeContext(sourceMode: .auto)))
        #expect(!strategy.shouldFallback(
            on: KiloUsageError.unauthorized,
            context: self.makeContext(sourceMode: .api)))
    }

    @Test
    func `api strategy falls back on missing credentials only in auto mode`() {
        let strategy = KiloAPIFetchStrategy()
        #expect(strategy.shouldFallback(
            on: KiloUsageError.missingCredentials,
            context: self.makeContext(sourceMode: .auto)))
        #expect(!strategy.shouldFallback(
            on: KiloUsageError.missingCredentials,
            context: self.makeContext(sourceMode: .api)))
    }

    @Test
    func `request builder adds org header for organization scope`() throws {
        let baseURL = try #require(URL(string: "https://kilo.example/trpc"))
        let request = try KiloUsageFetcher._buildRequestForTesting(
            baseURL: baseURL,
            apiKey: "test-token",
            scope: .organization(id: "org_42", name: "Acme"))
        #expect(request.value(forHTTPHeaderField: "X-KILOCODE-ORGANIZATIONID") == "org_42")
        #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-token")
    }

    @Test
    func `request builder omits org header for personal scope`() throws {
        let baseURL = try #require(URL(string: "https://kilo.example/trpc"))
        let request = try KiloUsageFetcher._buildRequestForTesting(
            baseURL: baseURL,
            apiKey: "test-token",
            scope: .personal)
        #expect(request.value(forHTTPHeaderField: "X-KILOCODE-ORGANIZATIONID") == nil)
        #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-token")
    }

    @Test
    func `parseOrganizations decodes tRPC array shape`() throws {
        let json = #"""
        [
          {
            "result": {
              "data": {
                "json": [
                  { "id": "org_1", "name": "Alpha", "role": "owner" },
                  { "id": "org_2", "name": "Beta", "role": "member" }
                ]
              }
            }
          }
        ]
        """#
        let orgs = try KiloUsageFetcher._parseOrganizationsForTesting(Data(json.utf8))
        #expect(orgs.count == 2)
        #expect(orgs[0].id == "org_1")
        #expect(orgs[0].name == "Alpha")
        #expect(orgs[0].role == "owner")
        #expect(orgs[1].id == "org_2")
        #expect(orgs[1].role == "member")
    }

    @Test
    func `parseOrganizations decodes profile REST shape`() throws {
        let json = #"""
        {
          "user": { "email": "test@example.com" },
          "organizations": [
            { "id": "org_42", "name": "Gamma" }
          ]
        }
        """#
        let orgs = try KiloUsageFetcher._parseOrganizationsForTesting(Data(json.utf8))
        #expect(orgs.count == 1)
        #expect(orgs[0].id == "org_42")
        #expect(orgs[0].role == nil)
    }

    @Test
    func `parseOrganizations returns empty for no orgs`() throws {
        let json = #"""
        { "user": { "email": "x@y" }, "organizations": [] }
        """#
        let orgs = try KiloUsageFetcher._parseOrganizationsForTesting(Data(json.utf8))
        #expect(orgs.isEmpty)
    }

    private func makeTemporaryHomeDirectory() throws -> URL {
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString, isDirectory: true)
        try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
        return directory
    }

    private func writeKiloAuthFile(homeDirectory: URL, contents: String) throws {
        let fileURL = KiloSettingsReader.defaultAuthFileURL(homeDirectory: homeDirectory)
        try FileManager.default.createDirectory(
            at: fileURL.deletingLastPathComponent(),
            withIntermediateDirectories: true)
        try contents.write(to: fileURL, atomically: true, encoding: .utf8)
    }
}
