import Foundation
import Testing
@testable import CodexBarCore

@Suite(.serialized)
struct CodexUsageFetcherFallbackTests {
    @Test
    func `missing CLI binary reports install guidance instead of not running`() async throws {
        let fetcher = UsageFetcher(
            environment: [:],
            initializeTimeoutSeconds: 0.1,
            requestTimeoutSeconds: 0.1,
            codexExecutableResolver: { _, _ in nil })

        do {
            _ = try await fetcher.loadLatestCLIAccountSnapshot()
            Issue.record("Expected missing Codex CLI to throw")
        } catch CodexStatusProbeError.codexNotInstalled {
            let message = CodexStatusProbeError.codexNotInstalled.localizedDescription
            #expect(message.contains("Codex CLI missing"))
            #expect(!message.contains("Codex not running"))
        } catch {
            Issue.record("Expected CodexStatusProbeError.codexNotInstalled, got \(type(of: error)): \(error)")
        }
    }

    @Test
    func `CLI usage recovers from RPC decode mismatch body payload`() {
        let snapshot = UsageFetcher._recoverCodexRPCUsageFromErrorForTesting(
            Self.decodeMismatchBodyMessage)

        #expect(snapshot?.primary?.usedPercent == 4)
        #expect(snapshot?.primary?.windowMinutes == 300)
        #expect(snapshot?.secondary?.usedPercent == 19)
        #expect(snapshot?.secondary?.windowMinutes == 10080)
        #expect(snapshot?.accountEmail(for: UsageProvider.codex) == "prolite-test@example.com")
        #expect(snapshot?.loginMethod(for: UsageProvider.codex) == "prolite")
    }

    @Test
    func `CLI credits recover from RPC decode mismatch body payload`() {
        let credits = UsageFetcher._recoverCodexRPCCreditsFromErrorForTesting(Self.decodeMismatchBodyMessage)

        #expect(credits?.remaining == 0)
    }

    @Test
    func `CLI credits recover from RPC error body when usage windows are unusable`() async throws {
        let stubCLIPath = try self.makeDecodeMismatchStubCodexCLI(message: Self.creditsOnlyDecodeMismatchBodyMessage)
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = self.makeStubUsageFetcher(stubCLIPath)
        let credits = try await fetcher.loadLatestCredits()

        #expect(credits.remaining == 14.5)
        await #expect(throws: UsageError.noRateLimitsFound) {
            _ = try await fetcher.loadLatestUsage()
        }
    }

    @Test
    func `CLI usage does not partially recover malformed RPC body without session lane`() {
        let snapshot = UsageFetcher._recoverCodexRPCUsageFromErrorForTesting(
            Self.partialDecodeBodyMessage)

        #expect(snapshot == nil)
    }

    @Test
    func `CLI usage recovers from RPC body without TTY fallback`() async throws {
        let stubCLIPath = try self.makeDecodeMismatchStubCodexCLI(message: Self.decodeMismatchBodyMessage)
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = self.makeStubUsageFetcher(stubCLIPath)
        let snapshot = try await fetcher.loadLatestUsage()

        #expect(snapshot.primary?.usedPercent == 4)
        #expect(snapshot.primary?.windowMinutes == 300)
        #expect(snapshot.secondary?.usedPercent == 19)
        #expect(snapshot.secondary?.windowMinutes == 10080)
    }

    @Test
    func `CLI credits recover from RPC body without TTY fallback`() async throws {
        let stubCLIPath = try self.makeDecodeMismatchStubCodexCLI(message: Self.decodeMismatchBodyMessage)
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = self.makeStubUsageFetcher(stubCLIPath)
        let credits = try await fetcher.loadLatestCredits()

        #expect(credits.remaining == 0)
    }

    @Test
    func `CLI credits load from RPC response without usage windows`() async throws {
        let stubCLIPath = try self.makeCreditsOnlyStubCodexCLI()
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = self.makeStubUsageFetcher(stubCLIPath)
        let credits = try await fetcher.loadLatestCredits()

        #expect(credits.remaining == 21)
        await #expect(throws: UsageError.noRateLimitsFound) {
            _ = try await fetcher.loadLatestUsage()
        }
    }

    @Test
    func `CLI usage loads plan only RPC response as unavailable limits`() async throws {
        let stubCLIPath = try self.makePlanOnlyStubCodexCLI()
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = self.makeStubUsageFetcher(stubCLIPath)
        let snapshot = try await fetcher.loadLatestUsage()

        #expect(snapshot.primary == nil)
        #expect(snapshot.secondary == nil)
        #expect(snapshot.accountEmail(for: .codex) == "stub@example.com")
        #expect(snapshot.loginMethod(for: .codex) == "pro")
        #expect(snapshot.rateLimitsUnavailable(for: .codex))
    }

    @Test
    func `CLI plan and credits response without usage windows keeps unavailable limits`() async throws {
        let stubCLIPath = try self.makePlanOnlyStubCodexCLI(includeCredits: true)
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = self.makeStubUsageFetcher(stubCLIPath)
        let snapshot = try await fetcher.loadLatestCLIAccountSnapshot()

        #expect(snapshot.usage?.primary == nil)
        #expect(snapshot.usage?.secondary == nil)
        #expect(snapshot.usage?.rateLimitsUnavailable(for: .codex) == true)
        #expect(snapshot.credits?.remaining == 21)
    }

    @Test
    func `CLI usage fails when RPC body recovery misses session lane`() async throws {
        let stubCLIPath = try self.makeDecodeMismatchStubCodexCLI(message: Self.partialDecodeBodyMessage)
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = self.makeStubUsageFetcher(stubCLIPath)

        do {
            _ = try await fetcher.loadLatestUsage()
            Issue.record("Expected RPC failure without PTY fallback")
        } catch {
            #expect(error.localizedDescription.contains("Codex connection failed"))
        }
    }

    @Test
    func `hung CLI RPC rate limits request times out within budget`() async throws {
        let stubCLIPath = try self.makeHungRateLimitsStubCodexCLI()
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = UsageFetcher(
            environment: ["CODEX_CLI_PATH": stubCLIPath],
            initializeTimeoutSeconds: 20.0,
            requestTimeoutSeconds: 0.2)

        let started = Date()
        do {
            _ = try await fetcher.loadLatestUsage()
            Issue.record("Expected hung Codex RPC usage request to time out")
        } catch let error as RPCWireError {
            guard case let .timeout(method) = error else {
                Issue.record("Expected RPC timeout, got \(error)")
                return
            }
            #expect(method == "account/rateLimits/read")
        } catch {
            Issue.record("Expected RPCWireError.timeout, got \(type(of: error)): \(error)")
        }

        let elapsed = Date().timeIntervalSince(started)
        #expect(elapsed < 5.0, "Hung RPC request must fail fast, took \(elapsed)s")
    }

    @Test
    func `repeated hung CLI RPC requests stay bounded`() async throws {
        let stubCLIPath = try self.makeHungRateLimitsStubCodexCLI()
        defer { try? FileManager.default.removeItem(atPath: stubCLIPath) }

        let fetcher = UsageFetcher(
            environment: ["CODEX_CLI_PATH": stubCLIPath],
            initializeTimeoutSeconds: 20.0,
            requestTimeoutSeconds: 0.2)

        for attempt in 1...2 {
            let started = Date()
            do {
                _ = try await fetcher.loadLatestCredits()
                Issue.record("Expected hung Codex RPC credits request \(attempt) to time out")
            } catch let error as RPCWireError {
                guard case .timeout = error else {
                    Issue.record("Expected RPC timeout on attempt \(attempt), got \(error)")
                    return
                }
            } catch {
                Issue.record("Expected RPCWireError.timeout on attempt \(attempt), got \(type(of: error)): \(error)")
            }

            let elapsed = Date().timeIntervalSince(started)
            #expect(elapsed < 5.0, "Hung RPC request \(attempt) must fail fast, took \(elapsed)s")
        }
    }

    private static let decodeMismatchBodyMessage = """
    failed to fetch codex rate limits: Decode error for https://chatgpt.com/backend-api/wham/usage:
    unknown variant `prolite`, expected one of `guest`, `free`, `go`, `plus`, `pro`;
    content-type=application/json; body={
      "user_id": "user-TEST",
      "account_id": "account-TEST",
      "email": "prolite-test@example.com",
      "plan_type": "prolite",
      "rate_limit": {
        "allowed": true,
        "limit_reached": false,
        "primary_window": {
          "used_percent": 4,
          "limit_window_seconds": 18000,
          "reset_after_seconds": 8657,
          "reset_at": 1776216359
        },
        "secondary_window": {
          "used_percent": 19,
          "limit_window_seconds": 604800,
          "reset_after_seconds": 187681,
          "reset_at": 1776395384
        }
      },
      "credits": {
        "has_credits": false,
        "unlimited": false,
        "overage_limit_reached": false,
        "balance": "0E-10"
      }
    }
    """

    private static let partialDecodeBodyMessage = """
    failed to fetch codex rate limits: Decode error for https://chatgpt.com/backend-api/wham/usage:
    unknown variant `prolite`, expected one of `guest`, `free`, `go`, `plus`, `pro`;
    content-type=application/json; body={
      "email": "prolite-test@example.com",
      "plan_type": "prolite",
      "rate_limit": {
        "allowed": true,
        "limit_reached": false,
        "primary_window": {
          "used_percent": "oops",
          "limit_window_seconds": 18000,
          "reset_at": 1776216359
        },
        "secondary_window": {
          "used_percent": 19,
          "limit_window_seconds": 604800,
          "reset_after_seconds": 187681,
          "reset_at": 1776395384
        }
      }
    }
    """

    private static let creditsOnlyDecodeMismatchBodyMessage = """
    failed to fetch codex rate limits: Decode error for https://chatgpt.com/backend-api/wham/usage:
    unknown variant `prolite`, expected one of `guest`, `free`, `go`, `plus`, `pro`;
    content-type=application/json; body={
      "email": "prolite-test@example.com",
      "plan_type": "prolite",
      "rate_limit": {
        "allowed": true,
        "limit_reached": false,
        "primary_window": {
          "used_percent": "oops",
          "limit_window_seconds": 18000,
          "reset_at": 1776216359
        }
      },
      "credits": {
        "has_credits": true,
        "unlimited": false,
        "overage_limit_reached": false,
        "balance": "14.5"
      }
    }
    """

    private func makeStubUsageFetcher(_ stubCLIPath: String) -> UsageFetcher {
        UsageFetcher(
            environment: ["CODEX_CLI_PATH": stubCLIPath],
            initializeTimeoutSeconds: 20.0,
            requestTimeoutSeconds: 3.0)
    }

    private func makeDecodeMismatchStubCodexCLI(
        message: String = Self.decodeMismatchBodyMessage)
        throws -> String
    {
        let script = """
        #!/usr/bin/python3 -S
        import json
        import sys

        args = sys.argv[1:]
        if "app-server" in args:
            for line in sys.stdin:
                if not line.strip():
                    continue
                message = json.loads(line)
                method = message.get("method")
                if method == "initialized":
                    continue

                identifier = message.get("id")
                if method == "initialize":
                    payload = {"id": identifier, "result": {}}
                elif method == "account/rateLimits/read":
                    payload = {
                        "id": identifier,
                        "error": {
                            "message": '''\(message)'''
                        }
                    }
                elif method == "account/read":
                    payload = {
                        "id": identifier,
                        "result": {
                            "account": {
                                "type": "chatgpt",
                                "email": "stub@example.com",
                                "planType": "prolite"
                            },
                            "requiresOpenaiAuth": False
                        }
                    }
                else:
                    payload = {"id": identifier, "result": {}}

                print(json.dumps(payload), flush=True)
        else:
            sys.stderr.write("unexpected non app-server Codex invocation\\n")
            sys.exit(92)
        """
        let url = FileManager.default.temporaryDirectory
            .appendingPathComponent("codex-fallback-stub-\(UUID().uuidString)", isDirectory: false)
        try Data(script.utf8).write(to: url)
        try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
        return url.path
    }

    private func makePlanOnlyStubCodexCLI(includeCredits: Bool = false) throws -> String {
        let creditsPayload = includeCredits
            ? [
                ",",
                "                                \"credits\": {",
                "                                    \"hasCredits\": True,",
                "                                    \"unlimited\": False,",
                "                                    \"balance\": \"21\"",
                "                                }",
            ].joined(separator: "\n")
            : ""
        let script = """
        #!/usr/bin/python3 -S
        import json
        import sys

        args = sys.argv[1:]
        if "app-server" in args:
            for line in sys.stdin:
                if not line.strip():
                    continue
                message = json.loads(line)
                method = message.get("method")
                if method == "initialized":
                    continue

                identifier = message.get("id")
                if method == "initialize":
                    payload = {"id": identifier, "result": {}}
                elif method == "account/rateLimits/read":
                    payload = {
                        "id": identifier,
                        "result": {
                            "rateLimits": {
                                "planType": "pro"
                                \(creditsPayload)
                            }
                        }
                    }
                elif method == "account/read":
                    payload = {
                        "id": identifier,
                        "result": {
                            "account": {
                                "type": "chatgpt",
                                "email": "stub@example.com",
                                "planType": "pro"
                            },
                            "requiresOpenaiAuth": False
                        }
                    }
                else:
                    payload = {"id": identifier, "result": {}}

                print(json.dumps(payload), flush=True)
        else:
            sys.stderr.write("unexpected non app-server Codex invocation\\n")
            sys.exit(92)
        """
        let url = FileManager.default.temporaryDirectory
            .appendingPathComponent("codex-plan-only-stub-\(UUID().uuidString)", isDirectory: false)
        try Data(script.utf8).write(to: url)
        try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
        return url.path
    }

    private func makeCreditsOnlyStubCodexCLI() throws -> String {
        let script = """
        #!/usr/bin/python3 -S
        import json
        import sys

        args = sys.argv[1:]
        if "app-server" in args:
            for line in sys.stdin:
                if not line.strip():
                    continue
                message = json.loads(line)
                method = message.get("method")
                if method == "initialized":
                    continue

                identifier = message.get("id")
                if method == "initialize":
                    payload = {"id": identifier, "result": {}}
                elif method == "account/rateLimits/read":
                    payload = {
                        "id": identifier,
                        "result": {
                            "rateLimits": {
                                "credits": {
                                    "hasCredits": True,
                                    "unlimited": False,
                                    "balance": "21"
                                }
                            }
                        }
                    }
                elif method == "account/read":
                    payload = {
                        "id": identifier,
                        "result": {
                            "account": {
                                "type": "chatgpt",
                                "email": "stub@example.com",
                                "planType": "pro"
                            },
                            "requiresOpenaiAuth": False
                        }
                    }
                else:
                    payload = {"id": identifier, "result": {}}

                print(json.dumps(payload), flush=True)
        else:
            sys.stderr.write("unexpected non app-server Codex invocation\\n")
            sys.exit(92)
        """
        let url = FileManager.default.temporaryDirectory
            .appendingPathComponent("codex-credits-only-stub-\(UUID().uuidString)", isDirectory: false)
        try Data(script.utf8).write(to: url)
        try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
        return url.path
    }

    private func makeHungRateLimitsStubCodexCLI() throws -> String {
        let script = """
        #!/bin/sh
        case " $* " in
          *" app-server "*) ;;
          *) printf '%s\\n' "unexpected non app-server Codex invocation" >&2; exit 92 ;;
        esac

        while IFS= read -r line; do
          case "$line" in
            *'"method":"initialized"'*|*'"method": "initialized"'*)
              ;;
            *'"method":"initialize"'*|*'"method": "initialize"'*)
              printf '%s\\n' '{"id":1,"result":{}}'
              ;;
            *'"method":"account/rateLimits/read"'*|*'"method": "account/rateLimits/read"'*)
              sleep 30
              ;;
            *)
              printf '%s\\n' '{"id":1,"result":{}}'
              ;;
          esac
        done
        """
        let url = FileManager.default.temporaryDirectory
            .appendingPathComponent("codex-hung-stub-\(UUID().uuidString)", isDirectory: false)
        try Data(script.utf8).write(to: url)
        try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
        return url.path
    }
}
