import Foundation
import Testing
@testable import CodexBarCore

struct CostUsagePricingTests {
    @Test
    func `normalizes codex model variants exactly`() {
        #expect(CostUsagePricing.normalizeCodexModel("openai/gpt-5-codex") == "gpt-5-codex")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.2-codex") == "gpt-5.2-codex")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.1-codex-max") == "gpt-5.1-codex-max")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.4-pro-2026-03-05") == "gpt-5.4-pro")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.4-mini-2026-03-17") == "gpt-5.4-mini")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.4-nano-2026-03-17") == "gpt-5.4-nano")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.5-2026-04-23") == "gpt-5.5")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.5-pro-2026-04-23") == "gpt-5.5-pro")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.3-codex-2026-03-05") == "gpt-5.3-codex")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.3-codex-spark") == "gpt-5.3-codex-spark")
        #expect(CostUsagePricing.normalizeCodexModel("openai/gpt-5.6-sol") == "gpt-5.6-sol")
        #expect(CostUsagePricing.normalizeCodexModel("openai/gpt-5.6-terra") == "gpt-5.6-terra")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.6-luna") == "gpt-5.6-luna")
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.6") == "gpt-5.6-sol")
        // Fictitious dated suffixes only exercise normalize stripping (not released snapshot IDs).
        #expect(CostUsagePricing.normalizeCodexModel("gpt-5.6-sol-2099-01-01") == "gpt-5.6-sol")
        #expect(CostUsagePricing.normalizeCodexModel("openai/gpt-5.6-terra-2099-01-01") == "gpt-5.6-terra")
    }

    @Test
    func `unattributed codex usage stays unpriced despite a catalog collision`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "unknown": {
                "id": "unknown",
                "cost": { "input": 99, "output": 199 }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexCostUSD(
            model: CostUsagePricing.codexUnattributedModel,
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        #expect(cost == nil)
    }

    @Test
    func `codex cost resolves OpenCodex provider qualified models`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "deepseek": {
            "id": "deepseek",
            "models": {
              "deepseek-v4-flash": {
                "id": "deepseek-v4-flash",
                "cost": { "input": 0.14, "output": 0.28 }
              }
            }
          },
          "kimi-for-coding": {
            "id": "kimi-for-coding",
            "models": {
              "k3": {
                "id": "k3",
                "cost": { "input": 0, "output": 0 }
              }
            }
          },
          "opencode": {
            "id": "opencode",
            "models": {
              "deepseek-v4-flash-free": {
                "id": "deepseek-v4-flash-free",
                "cost": { "input": 0, "output": 0 }
              }
            }
          },
          "opencode-go": {
            "id": "opencode-go",
            "models": {
              "deepseek-v4-flash": {
                "id": "deepseek-v4-flash",
                "cost": { "input": 0.07, "output": 0.14 }
              }
            }
          }
        }
        """)

        let opencodeGo = CostUsagePricing.codexCostUSD(
            model: "opencode-go/deepseek-v4-flash",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)
        let opencodeFree = CostUsagePricing.codexCostUSD(
            model: "opencode-free/deepseek-v4-flash-free",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)
        let kimi = CostUsagePricing.codexCostUSD(
            model: "kimi-coding/k3",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)
        let deepseek = CostUsagePricing.codexCostUSD(
            model: "deepseek/deepseek-v4-flash",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        #expect(opencodeGo == (100.0 * 0.07e-6) + (5.0 * 0.14e-6))
        #expect(opencodeFree == 0)
        #expect(kimi == 0)
        #expect(deepseek == (100.0 * 0.14e-6) + (5.0 * 0.28e-6))
    }

    @Test
    func `codex cost does not cross charge an unknown provider prefix`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "deepseek-v4-flash": {
                "id": "deepseek-v4-flash",
                "cost": { "input": 99, "output": 199 }
              }
            }
          },
          "unlisted-route": {
            "id": "unlisted-route",
            "models": {
              "deepseek-v4-flash": {
                "id": "deepseek-v4-flash",
                "cost": { "input": 1, "output": 1 }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexCostUSD(
            model: "unlisted-route/deepseek-v4-flash",
            inputTokens: 100,
            cachedInputTokens: 0,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        #expect(cost == nil)
    }

    @Test
    func `codex cost supports gpt51 codex max`() {
        let cost = CostUsagePricing.codexCostUSD(
            model: "gpt-5.1-codex-max",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5)
        #expect(cost != nil)
    }

    @Test
    func `codex cost supports gpt53 codex`() {
        let cost = CostUsagePricing.codexCostUSD(
            model: "gpt-5.3-codex",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5)
        #expect(cost != nil)
    }

    @Test
    func `codex cost supports gpt54 mini and nano`() {
        let mini = CostUsagePricing.codexCostUSD(
            model: "gpt-5.4-mini-2026-03-17",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5)
        let nano = CostUsagePricing.codexCostUSD(
            model: "gpt-5.4-nano",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5)

        #expect(mini != nil)
        #expect(nano != nil)
    }

    @Test
    func `codex cost supports gpt55 bundled fallback`() throws {
        let root = try Self.cacheRoot()
        let cost = CostUsagePricing.codexCostUSD(
            model: "openai/gpt-5.5-2026-04-23",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        // Codex `input_tokens` includes cached reads, so only the 90 non-cached tokens are
        // billed at the input rate; the 10 cached tokens are billed at the cache rate.
        let expected = (90.0 * 5e-6) + (10.0 * 5e-7) + (5.0 * 3e-5)
        #expect(cost == expected)
    }

    @Test
    func `codex cost supports gpt56 sol terra luna bundled fallback`() throws {
        // Empty models.dev cache root forces the built-in table for GPT-5.6 tiers.
        let root = try Self.cacheRoot()

        let sol = CostUsagePricing.codexCostUSD(
            model: "openai/gpt-5.6-sol",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)
        let terra = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-terra",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)
        let luna = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-luna",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)
        let alias = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        // Rates per token: Sol $5/$30 per 1M, Terra $2/$12, Luna $0.20/$1.20;
        // cache read is 10% of input. Non-cached input is 90 tokens.
        #expect(sol == (90.0 * 5e-6) + (10.0 * 5e-7) + (5.0 * 3e-5))
        #expect(terra == (90.0 * 2e-6) + (10.0 * 2e-7) + (5.0 * 1.2e-5))
        #expect(luna == (90.0 * 2e-7) + (10.0 * 2e-8) + (5.0 * 1.2e-6))
        // Unsuffixed gpt-5.6 alias routes to Sol.
        #expect(alias == sol)
    }

    @Test
    func `codex models dev falls back from gpt56 alias to canonical sol pricing`() throws {
        let canonicalOnlyRoot = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": { "input": 7, "output": 31 }
              }
            }
          }
        }
        """)
        let aliasAndCanonicalRoot = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6": {
                "id": "gpt-5.6",
                "cost": { "input": 3, "output": 13 }
              },
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": { "input": 7, "output": 31 }
              }
            }
          }
        }
        """)

        let canonicalFallback = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6",
            inputTokens: 100,
            cachedInputTokens: 0,
            outputTokens: 0,
            modelsDevCacheRoot: canonicalOnlyRoot)
        let exactAlias = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6",
            inputTokens: 100,
            cachedInputTokens: 0,
            outputTokens: 0,
            modelsDevCacheRoot: aliasAndCanonicalRoot)

        #expect(canonicalFallback == 100.0 * 7e-6)
        #expect(exactAlias == 100.0 * 3e-6)
    }

    @Test
    func `codex pricing key distinguishes an empty long context block from no block`() throws {
        let withoutLongContext = try Self.modelsDevArtifact("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": { "input": 5, "output": 30 }
              }
            }
          }
        }
        """)
        let withEmptyLongContext = try Self.modelsDevArtifact("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": {
                  "input": 5,
                  "output": 30,
                  "context_over_200k": {}
                }
              }
            }
          }
        }
        """)

        let withoutKey = CostUsagePricingKey.codex(
            modelsDevArtifact: withoutLongContext,
            formulaVersion: 1)
        let withEmptyKey = CostUsagePricingKey.codex(
            modelsDevArtifact: withEmptyLongContext,
            formulaVersion: 1)

        #expect(withoutKey != withEmptyKey)
    }

    @Test
    func `codex pricing fingerprint records API fast USD definition`() {
        let fingerprint = CostUsagePricing.codexBuiltInPricingFingerprint()

        #expect(fingerprint.contains("fastPricingDefinition=api-fast-usd-v1"))
    }

    @Test
    func `codex cost applies gpt56 long context rates`() throws {
        let root = try Self.cacheRoot()
        let sol = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 272_001,
            cachedInputTokens: 10,
            outputTokens: 10,
            cacheWriteInputTokens: 20,
            modelsDevCacheRoot: root)
        let terra = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-terra",
            inputTokens: 272_001,
            cachedInputTokens: 10,
            outputTokens: 10,
            cacheWriteInputTokens: 20,
            modelsDevCacheRoot: root)
        let luna = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-luna",
            inputTokens: 272_001,
            cachedInputTokens: 10,
            outputTokens: 10,
            cacheWriteInputTokens: 20,
            modelsDevCacheRoot: root)

        // Long-context (>272K) rates apply to the entire request. Total input contains 10 cached,
        // 20 cache-write, and 271,971 ordinary input tokens.
        #expect(sol == (271_971.0 * 1e-5) + (10.0 * 1e-6) + (20.0 * 1.25e-5) + (10.0 * 4.5e-5))
        #expect(terra == (271_971.0 * 4e-6) + (10.0 * 4e-7) + (20.0 * 5e-6) + (10.0 * 1.8e-5))
        #expect(luna == (271_971.0 * 4e-7) + (10.0 * 4e-8) + (20.0 * 5e-7) + (10.0 * 1.8e-6))
    }

    @Test
    func `codex cost bills gpt56 cache writes at one point two five x input`() throws {
        let root = try Self.cacheRoot()
        // Total prompt 100: 70 uncached + 20 cache-write + 10 cache-read.
        let sol = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            cacheWriteInputTokens: 20,
            modelsDevCacheRoot: root)

        let expected = (70.0 * 5e-6) + (10.0 * 5e-7) + (20.0 * 6.25e-6) + (5.0 * 3e-5)
        #expect(sol == expected)
    }

    @Test
    func `codex API fast cost matches brief gpt56 scenarios`() {
        let sol = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 100_000,
            cachedInputTokens: 20000,
            outputTokens: 20000)
        let terra = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-terra",
            inputTokens: 100_000,
            cachedInputTokens: 20000,
            outputTokens: 20000)
        let luna = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-luna",
            inputTokens: 100_000,
            cachedInputTokens: 20000,
            outputTokens: 20000)

        // Public API Fast rates are 2x Standard for GPT-5.6.
        let expectedSol = 2.02
        let expectedTerra = 0.808
        let expectedLuna = 0.0808
        #expect(abs((sol ?? 0) - expectedSol) < 1e-12)
        #expect(abs((terra ?? 0) - expectedTerra) < 1e-12)
        #expect(abs((luna ?? 0) - expectedLuna) < 1e-12)
    }

    @Test
    func `codex priority cost multiplies standard cache write rates`() {
        let sol = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 100,
            cachedInputTokens: 10,
            cacheWriteInputTokens: 20,
            outputTokens: 5)
        let terra = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-terra",
            inputTokens: 100,
            cachedInputTokens: 10,
            cacheWriteInputTokens: 20,
            outputTokens: 5)
        let luna = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-luna",
            inputTokens: 100,
            cachedInputTokens: 10,
            cacheWriteInputTokens: 20,
            outputTokens: 5)
        let modelWithoutCacheWriteSupport = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.5",
            inputTokens: 100,
            cachedInputTokens: 10,
            cacheWriteInputTokens: 20,
            outputTokens: 5)

        let solInput = 70.0 * 5e-6
        let solCached = 10.0 * 5e-7
        let solWrite = 20.0 * 6.25e-6
        let solOutput = 5.0 * 3e-5
        let expectedSol: Double = (solInput + solCached + solWrite + solOutput) * 2
        let terraInput = 70.0 * 2e-6
        let terraCached = 10.0 * 2e-7
        let terraWrite = 20.0 * 2.5e-6
        let terraOutput = 5.0 * 1.2e-5
        let expectedTerra: Double = (terraInput + terraCached + terraWrite + terraOutput) * 2
        let lunaInput = 70.0 * 2e-7
        let lunaCached = 10.0 * 2e-8
        let lunaWrite = 20.0 * 2.5e-7
        let lunaOutput = 5.0 * 1.2e-6
        let expectedLuna: Double = (lunaInput + lunaCached + lunaWrite + lunaOutput) * 2
        #expect(abs((sol ?? 0) - expectedSol) < 1e-12)
        #expect(abs((terra ?? 0) - expectedTerra) < 1e-12)
        #expect(abs((luna ?? 0) - expectedLuna) < 1e-12)
        // A legacy model without a Standard cache-write price folds writes into uncached input.
        let legacyInput = 90.0 * 1.25e-5
        let legacyCached = 10.0 * 1.25e-6
        let legacyOutput = 5.0 * 7.5e-5
        let expectedLegacy: Double = legacyInput + legacyCached + legacyOutput
        #expect(abs((modelWithoutCacheWriteSupport ?? 0) - expectedLegacy) < 1e-12)
    }

    @Test
    func `codex priority cost multiplies models dev standard pricing`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": { "input": 5, "output": 30, "cache_read": 0.5, "cache_write": 6.25 }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 100_000,
            cachedInputTokens: 20000,
            outputTokens: 20000,
            modelsDevCacheRoot: root)

        // The brief's Standard total is $1.01; API Fast is 2x for GPT-5.6.
        #expect(abs((cost ?? 0) - 2.02) < 1e-12)
    }

    @Test
    func `codex cost applies gpt54 and gpt55 long context rates to full session`() throws {
        let root = try Self.cacheRoot()
        let gpt54 = CostUsagePricing.codexCostUSD(
            model: "gpt-5.4",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10,
            modelsDevCacheRoot: root)
        let gpt55 = CostUsagePricing.codexCostUSD(
            model: "gpt-5.5",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10,
            modelsDevCacheRoot: root)

        #expect(gpt54 == (272_001.0 * 5e-6) + (10.0 * 2.25e-5))
        #expect(gpt55 == (272_001.0 * 1e-5) + (10.0 * 4.5e-5))
    }

    @Test
    func `codex cost keeps normal rates at long context input boundary`() throws {
        let root = try Self.cacheRoot()
        let gpt55 = CostUsagePricing.codexCostUSD(
            model: "gpt-5.5",
            inputTokens: 272_000,
            cachedInputTokens: 0,
            outputTokens: 128_000,
            modelsDevCacheRoot: root)

        #expect(gpt55 == (272_000.0 * 5e-6) + (128_000.0 * 3e-5))
    }

    @Test
    func `codex cost applies long context rates to all cached and non cached input`() throws {
        let root = try Self.cacheRoot()
        let gpt55 = CostUsagePricing.codexCostUSD(
            model: "gpt-5.5",
            inputTokens: 300_000,
            cachedInputTokens: 200_000,
            outputTokens: 10,
            modelsDevCacheRoot: root)

        // 200K cached reads are a subset of the 300K input, leaving 100K non-cached input.
        let cached = 200_000.0 * 1e-6
        let nonCached = 100_000.0 * 1e-5
        let output = 10.0 * 4.5e-5

        #expect(gpt55 == cached + nonCached + output)
    }

    @Test
    func `codex cost clamps cache reads to input tokens`() throws {
        // `cached_input_tokens` can never exceed `input_tokens` in real Codex data; if it does,
        // clamp cached to input so the surplus is not invented and input is never double-billed.
        let root = try Self.cacheRoot()
        let gpt55 = CostUsagePricing.codexCostUSD(
            model: "gpt-5.5",
            inputTokens: 20,
            cachedInputTokens: 500,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        let expected = (20.0 * 5e-7) + (5.0 * 3e-5)

        #expect(gpt55 == expected)
    }

    @Test
    func `codex cost does not double bill cached input tokens`() throws {
        // Regression for the cached double-count: input_tokens includes cached reads, so a turn
        // with 1000 input / 900 cached must bill 100 tokens at the input rate and 900 at the
        // cache rate — not the full 1000 at the input rate plus 900 again at the cache rate.
        let root = try Self.cacheRoot()
        let cost = CostUsagePricing.codexCostUSD(
            model: "gpt-5-codex",
            inputTokens: 1000,
            cachedInputTokens: 900,
            outputTokens: 10,
            modelsDevCacheRoot: root)

        let expected = (100.0 * 1.25e-6) + (900.0 * 1.25e-7) + (10.0 * 1e-5)
        #expect(cost == expected)
    }

    @Test
    func `codex priority cost applies model specific fast rates`() {
        let gpt54 = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.4",
            inputTokens: 100,
            cachedInputTokens: 20,
            outputTokens: 10)
        let gpt55 = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.5",
            inputTokens: 100,
            cachedInputTokens: 20,
            outputTokens: 10)
        let gpt54Mini = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.4-mini",
            inputTokens: 100,
            cachedInputTokens: 20,
            outputTokens: 10)

        #expect(gpt54 == (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5))
        #expect(gpt55 == (80.0 * 1.25e-5) + (20.0 * 1.25e-6) + (10.0 * 7.5e-5))
        #expect(gpt54Mini == (80.0 * 1.5e-6) + (20.0 * 1.5e-7) + (10.0 * 9e-6))
    }

    @Test
    func `codex priority cost is unavailable for long context requests`() {
        let gpt55 = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.5",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10)
        let gpt56Sol = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10)
        let gpt56Terra = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-terra",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10)
        let gpt56Luna = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.6-luna",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10)
        let gpt54Mini = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.4-mini",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10)

        #expect(gpt55 == nil)
        #expect(gpt56Sol == nil)
        #expect(gpt56Terra == nil)
        #expect(gpt56Luna == nil)
        #expect(gpt54Mini == nil)
    }

    @Test
    func `codex priority cost counts only input tokens toward the limit`() {
        let eligible = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.5",
            inputTokens: 200_000,
            cachedInputTokens: 100_000,
            outputTokens: 10)
        let boundary = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.5",
            inputTokens: 272_000,
            cachedInputTokens: 0,
            outputTokens: 10)
        let overLimit = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.5",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10)

        #expect(eligible == (100_000.0 * 1.25e-5) + (100_000.0 * 1.25e-6) + (10.0 * 7.5e-5))
        #expect(boundary != nil)
        #expect(overLimit == nil)
    }

    @Test
    func `codex priority cost remains available at priority input boundary`() {
        let gpt55 = CostUsagePricing.codexPriorityCostUSD(
            model: "gpt-5.5",
            inputTokens: 272_000,
            cachedInputTokens: 0,
            outputTokens: 10)

        #expect(gpt55 == (272_000.0 * 1.25e-5) + (10.0 * 7.5e-5))
    }

    @Test
    func `codex models dev pricing uses codex long context threshold`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.5": {
                "id": "gpt-5.5",
                "cost": {
                  "input": 5,
                  "output": 30,
                  "cache_read": 0.5,
                  "context_over_200k": {
                    "input": 10,
                    "output": 45,
                    "cache_read": 1
                  }
                }
              }
            }
          }
        }
        """)

        let atBoundary = CostUsagePricing.codexCostUSD(
            model: "gpt-5.5",
            inputTokens: 272_000,
            cachedInputTokens: 0,
            outputTokens: 10,
            modelsDevCacheRoot: root)
        let aboveBoundary = CostUsagePricing.codexCostUSD(
            model: "gpt-5.5",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10,
            modelsDevCacheRoot: root)

        #expect(atBoundary == (272_000.0 * 5e-6) + (10.0 * 3e-5))
        #expect(aboveBoundary == (272_001.0 * 1e-5) + (10.0 * 4.5e-5))
    }

    @Test
    func `codex aggregate pricing uses safe base rates and rejects aggregates above thresholds`() throws {
        let emptyRoot = try Self.cacheRoot()
        let bundledBelowThreshold = CostUsagePricing.codexAggregateCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 200_000,
            cachedInputTokens: 0,
            outputTokens: 100,
            modelsDevCacheRoot: emptyRoot)
        let bundledAtThreshold = CostUsagePricing.codexAggregateCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 272_000,
            cachedInputTokens: 0,
            outputTokens: 100,
            modelsDevCacheRoot: emptyRoot)
        let bundledAboveThreshold = CostUsagePricing.codexAggregateCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 400_000,
            cachedInputTokens: 0,
            outputTokens: 100,
            modelsDevCacheRoot: emptyRoot)
        let linear = CostUsagePricing.codexAggregateCostUSD(
            model: "gpt-5.4-mini",
            inputTokens: 400_000,
            cachedInputTokens: 100_000,
            outputTokens: 100,
            modelsDevCacheRoot: emptyRoot)
        let catalogThresholdRoot = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "aggregate-threshold-model": {
                "id": "aggregate-threshold-model",
                "cost": {
                  "input": 5,
                  "output": 30,
                  "context_over_200k": { "input": 10, "output": 45 }
                }
              }
            }
          }
        }
        """)
        let catalogAtThreshold = CostUsagePricing.codexAggregateCostUSD(
            model: "aggregate-threshold-model",
            inputTokens: 200_000,
            cachedInputTokens: 0,
            outputTokens: 100,
            modelsDevCacheRoot: catalogThresholdRoot)
        let catalogAboveThreshold = CostUsagePricing.codexAggregateCostUSD(
            model: "aggregate-threshold-model",
            inputTokens: 200_001,
            cachedInputTokens: 0,
            outputTokens: 100,
            modelsDevCacheRoot: catalogThresholdRoot)

        #expect(bundledBelowThreshold == (200_000.0 * 5e-6) + (100.0 * 30e-6))
        #expect(bundledAtThreshold == (272_000.0 * 5e-6) + (100.0 * 30e-6))
        #expect(bundledAboveThreshold == nil)
        #expect(linear == (300_000.0 * 7.5e-7) + (100_000.0 * 7.5e-8) + (100.0 * 4.5e-6))
        #expect(catalogAtThreshold == (200_000.0 * 5e-6) + (100.0 * 30e-6))
        #expect(catalogAboveThreshold == nil)
    }

    @Test
    func `codex models dev cached fallback uses long context input rate when cache read is absent`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.5": {
                "id": "gpt-5.5",
                "cost": {
                  "input": 5,
                  "output": 30,
                  "context_over_200k": {
                    "input": 10,
                    "output": 45
                  }
                }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexCostUSD(
            model: "gpt-5.5",
            inputTokens: 300_000,
            cachedInputTokens: 200_000,
            outputTokens: 10,
            modelsDevCacheRoot: root)

        // The catalog has a long-context block but omits cache_read, so preserve its omission
        // semantics: cached tokens fall back to the long-context input rate rather than mixing in
        // one field from the bundled table.
        let expected = (100_000.0 * 10e-6) + (200_000.0 * 10e-6) + (10.0 * 45e-6)
        #expect(cost == expected)
    }
}

extension CostUsagePricingTests {
    @Test
    func `codex models dev uses bundled short cache rates only when catalog omits them`() throws {
        let missingRoot = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": { "input": 5, "output": 30 }
              }
            }
          }
        }
        """)
        let explicitZeroRoot = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": { "input": 5, "output": 30, "cache_read": 0, "cache_write": 0 }
              }
            }
          }
        }
        """)

        let missing = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 0,
            cacheWriteInputTokens: 20,
            modelsDevCacheRoot: missingRoot)
        let explicitZero = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 0,
            cacheWriteInputTokens: 20,
            modelsDevCacheRoot: explicitZeroRoot)

        #expect(missing == (70.0 * 5e-6) + (10.0 * 5e-7) + (20.0 * 6.25e-6))
        #expect(explicitZero == 70.0 * 5e-6)
    }

    @Test
    func `codex models dev falls back bundled long context rates when catalog omits them`() throws {
        // Catalog has short-context rates only; bundled table supplies the 272K threshold + rates.
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": {
                  "input": 5,
                  "output": 30,
                  "cache_read": 0.5
                }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 272_001,
            cachedInputTokens: 0,
            outputTokens: 10,
            modelsDevCacheRoot: root)

        // Without bundled above-threshold fallback this would bill short rates ($5/$30) despite
        // entering long-context mode via the bundled threshold.
        #expect(cost == (272_001.0 * 1e-5) + (10.0 * 4.5e-5))
    }

    @Test
    func `codex models dev overrides every gpt56 long context token bucket`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.6-sol": {
                "id": "gpt-5.6-sol",
                "cost": {
                  "input": 1,
                  "output": 2,
                  "cache_read": 0.1,
                  "cache_write": 1.25,
                  "context_over_200k": {
                    "input": 11,
                    "output": 22,
                    "cache_read": 1.1,
                    "cache_write": 13.75
                  }
                }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexCostUSD(
            model: "gpt-5.6-sol",
            inputTokens: 272_001,
            cachedInputTokens: 100_000,
            outputTokens: 10,
            cacheWriteInputTokens: 50000,
            modelsDevCacheRoot: root)

        let expected = (122_001.0 * 11e-6)
            + (100_000.0 * 1.1e-6)
            + (50000.0 * 13.75e-6)
            + (10.0 * 22e-6)
        #expect(cost == expected)
    }

    @Test
    func `codex cost supports gpt55 pro bundled fallback`() throws {
        let root = try Self.cacheRoot()
        let cost = CostUsagePricing.codexCostUSD(
            model: "openai/gpt-5.5-pro-2026-04-23",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        // gpt-5.5-pro has no cache-read rate, so cached falls back to the input rate; with 90
        // non-cached + 10 cached priced at the same rate this is 100 tokens at 3e-5.
        let expected = (100.0 * 3e-5) + (5.0 * 1.8e-4)
        #expect(cost == expected)
    }

    @Test
    func `codex cost returns zero for research preview fallback model`() throws {
        let root = try Self.cacheRoot()
        let cost = CostUsagePricing.codexCostUSD(
            model: "gpt-5.3-codex-spark",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)
        #expect(cost == 0)
        #expect(CostUsagePricing.codexDisplayLabel(model: "gpt-5.3-codex-spark") == "Research Preview")
        #expect(CostUsagePricing.codexDisplayLabel(model: "gpt-5.2-codex") == nil)
    }

    @Test
    func `codex cost prefers models dev cache over bundled fallback`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.5": {
                "id": "gpt-5.5",
                "cost": { "input": 10, "output": 20, "cache_read": 1 }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexCostUSD(
            model: "openai/gpt-5.5",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        let expected = (90.0 * 10e-6) + (10.0 * 1e-6) + (5.0 * 20e-6)
        #expect(cost == expected)
    }

    @Test
    func `codex cost lets models dev override research preview fallback`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "openai": {
            "id": "openai",
            "models": {
              "gpt-5.3-codex-spark": {
                "id": "gpt-5.3-codex-spark",
                "cost": { "input": 2, "output": 8, "cache_read": 0.2 }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexCostUSD(
            model: "gpt-5.3-codex-spark",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        let expected = (90.0 * 2e-6) + (10.0 * 0.2e-6) + (5.0 * 8e-6)
        #expect(cost == expected)
        #expect(CostUsagePricing.codexDisplayLabel(model: "gpt-5.3-codex-spark") == "Research Preview")
    }

    @Test
    func `codex cost falls back to bundled pricing when models dev misses provider model`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "anthropic": {
            "id": "anthropic",
            "models": {
              "gpt-5.5": {
                "id": "gpt-5.5",
                "cost": { "input": 10, "output": 20, "cache_read": 1 }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.codexCostUSD(
            model: "openai/gpt-5.5",
            inputTokens: 100,
            cachedInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        let expected = (90.0 * 5e-6) + (10.0 * 5e-7) + (5.0 * 3e-5)
        #expect(cost == expected)
    }

    @Test
    func `normalizes claude opus41 dated variants`() {
        #expect(CostUsagePricing.normalizeClaudeModel("claude-opus-4-1-20250805") == "claude-opus-4-1")
    }

    @Test
    func `claude cost supports opus41 dated variant`() {
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-opus-4-1-20250805",
            inputTokens: 10,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 0,
            outputTokens: 5)
        #expect(cost != nil)
    }

    @Test
    func `claude cost supports opus46 dated variant`() {
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-opus-4-6-20260205",
            inputTokens: 10,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 0,
            outputTokens: 5)
        #expect(cost != nil)
    }

    @Test
    func `claude cost supports opus47`() {
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-opus-4-7",
            inputTokens: 10,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 0,
            outputTokens: 5)
        let expected = (10.0 * 5e-6) + (5.0 * 2.5e-5)
        #expect(cost == expected)
    }

    @Test
    func `claude cost supports opus48`() throws {
        // Point at a fresh, empty cache root so the models.dev lookup misses and this
        // exercises the built-in fallback table specifically — not a local cache hit.
        let emptyCacheRoot = try Self.cacheRoot()
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-opus-4-8",
            inputTokens: 10,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 0,
            outputTokens: 5,
            modelsDevCacheRoot: emptyCacheRoot)
        let expected = (10.0 * 5e-6) + (5.0 * 2.5e-5)
        #expect(cost == expected)
    }

    @Test
    func `claude cost supports fable5 bundled fallback`() throws {
        let emptyCacheRoot = try Self.cacheRoot()
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-fable-5",
            inputTokens: 100,
            cacheReadInputTokens: 20,
            cacheCreationInputTokens: 10,
            outputTokens: 5,
            modelsDevCacheRoot: emptyCacheRoot)
        let expected = (100.0 * 1e-5) + (20.0 * 1e-6) + (10.0 * 1.25e-5) + (5.0 * 5e-5)
        #expect(cost == expected)
    }

    @Test
    func `claude cost preserves historical sonnet46 long context pricing`() throws {
        let emptyCacheRoot = try Self.cacheRoot()
        let historical = CostUsagePricing.claudeCostUSD(
            model: "claude-sonnet-4-6",
            inputTokens: 240_000,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 0,
            outputTokens: 0,
            pricingDate: Date(timeIntervalSince1970: 1_773_359_999),
            modelsDevCacheRoot: emptyCacheRoot)
        let current = CostUsagePricing.claudeCostUSD(
            model: "claude-sonnet-4-6",
            inputTokens: 240_000,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 0,
            outputTokens: 0,
            pricingDate: Date(timeIntervalSince1970: 1_773_360_000),
            modelsDevCacheRoot: emptyCacheRoot)

        #expect(historical == 1.44)
        #expect(current == 0.72)
    }

    @Test
    func `claude cost ignores stale sonnet46 threshold catalog after cutover`() throws {
        let cacheRoot = try Self.seedModelsDevCache("""
        {
          "anthropic": {
            "id": "anthropic",
            "models": {
              "claude-sonnet-4-6": {
                "id": "claude-sonnet-4-6",
                "cost": {
                  "input": 3,
                  "output": 15,
                  "cache_read": 0.3,
                  "cache_write": 3.75,
                  "context_over_200k": {
                    "input": 6,
                    "output": 22.5,
                    "cache_read": 0.6,
                    "cache_write": 7.5
                  }
                }
              }
            }
          }
        }
        """)
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-sonnet-4-6",
            inputTokens: 240_000,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 0,
            outputTokens: 0,
            pricingDate: Date(timeIntervalSince1970: 1_773_360_000),
            modelsDevCacheRoot: cacheRoot)

        #expect(cost == 0.72)
    }

    @Test
    func `claude cost prices one hour cache writes separately`() throws {
        let emptyCacheRoot = try Self.cacheRoot()
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-fable-5",
            inputTokens: 100,
            cacheReadInputTokens: 20,
            cacheCreationInputTokens: 30,
            cacheCreationInputTokens1h: 20,
            outputTokens: 5,
            modelsDevCacheRoot: emptyCacheRoot)
        let expected = (100.0 * 1e-5)
            + (20.0 * 1e-6)
            + (10.0 * 1.25e-5)
            + (20.0 * 2e-5)
            + (5.0 * 5e-5)
        #expect(cost == expected)
    }

    @Test
    func `claude cost applies long context rates across cache write durations`() throws {
        let cacheRoot = try Self.seedModelsDevCache("""
        {
          "anthropic": {
            "id": "anthropic",
            "models": {
              "claude-threshold-model": {
                "id": "claude-threshold-model",
                "cost": {
                  "input": 3,
                  "output": 15,
                  "cache_read": 0.3,
                  "cache_write": 3.75,
                  "context_over_200k": {
                    "input": 6,
                    "output": 22.5,
                    "cache_read": 0.6,
                    "cache_write": 7.5
                  }
                }
              }
            }
          }
        }
        """)
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-threshold-model",
            inputTokens: 0,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 240_000,
            cacheCreationInputTokens1h: 120_000,
            outputTokens: 0,
            modelsDevCacheRoot: cacheRoot)
        let expected = (120_000.0 * 12e-6)
            + (120_000.0 * 7.5e-6)
        #expect(cost == expected)
    }

    @Test
    func `claude sonnet46 uses standard pricing across full context`() throws {
        let emptyCacheRoot = try Self.cacheRoot()
        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-sonnet-4-6",
            inputTokens: 0,
            cacheReadInputTokens: 0,
            cacheCreationInputTokens: 240_000,
            outputTokens: 0,
            modelsDevCacheRoot: emptyCacheRoot)
        #expect(cost == 240_000.0 * 3.75e-6)
    }

    @Test
    func `claude cost returns nil for unknown models`() {
        let cost = CostUsagePricing.claudeCostUSD(
            model: "glm-4.6",
            inputTokens: 100,
            cacheReadInputTokens: 500,
            cacheCreationInputTokens: 0,
            outputTokens: 40)
        #expect(cost == nil)
    }

    @Test
    func `claude cost prefers models dev cache with threshold pricing`() throws {
        let root = try Self.seedModelsDevCache("""
        {
          "anthropic": {
            "id": "anthropic",
            "models": {
              "claude-sonnet-4-6": {
                "id": "claude-sonnet-4-6",
                "cost": {
                  "input": 3,
                  "output": 15,
                  "cache_read": 0.3,
                  "cache_write": 3.75,
                  "context_over_200k": {
                    "input": 6,
                    "output": 22.5,
                    "cache_read": 0.6,
                    "cache_write": 7.5
                  }
                }
              }
            }
          }
        }
        """)

        let cost = CostUsagePricing.claudeCostUSD(
            model: "claude-sonnet-4-6",
            inputTokens: 200_010,
            cacheReadInputTokens: 5,
            cacheCreationInputTokens: 5,
            outputTokens: 5,
            modelsDevCacheRoot: root)

        let expected = (200_010.0 * 6e-6)
            + (5.0 * 0.6e-6)
            + (5.0 * 7.5e-6)
            + (5.0 * 22.5e-6)
        #expect(cost == expected)
    }

    private static func seedModelsDevCache(_ json: String) throws -> URL {
        let root = try Self.cacheRoot()
        let catalog = try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(json.utf8))
        ModelsDevCache.save(catalog: catalog, fetchedAt: Date(), cacheRoot: root)
        return root
    }

    private static func modelsDevArtifact(_ json: String) throws -> ModelsDevCacheArtifact {
        let catalog = try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(json.utf8))
        return ModelsDevCacheArtifact(
            version: ModelsDevCache.artifactVersion,
            fetchedAt: Date(timeIntervalSince1970: 0),
            catalog: catalog)
    }

    private static func cacheRoot() throws -> URL {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("codexbar-pricing-tests-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
        return root
    }
}
