import Foundation
import Testing
@testable import CodexBarCore

struct CostUsageScannerClaudeFableTests {
    @Test
    func `claude fable 5 issue row gets priced`() throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }

        let day = try env.makeLocalNoon(year: 2026, month: 6, day: 9)
        let fileURL = try env.writeClaudeProjectFile(
            relativePath: "project-a/fable-5.jsonl",
            contents: env.jsonl([
                [
                    "message": [
                        "model": "claude-fable-5",
                        "id": "msg_fable_5",
                        "type": "message",
                        "role": "assistant",
                        "usage": [
                            "input_tokens": 100,
                            "cache_creation_input_tokens": 10,
                            "cache_read_input_tokens": 20,
                            "output_tokens": 5,
                        ],
                    ],
                    "requestId": "req_fable_5",
                    "type": "assistant",
                    "timestamp": "2026-06-09T12:00:00.000Z",
                    "sessionId": "session_fable_5",
                ],
            ]))

        let parsed = CostUsageScanner.parseClaudeFile(
            fileURL: fileURL,
            range: CostUsageScanner.CostUsageDayRange(since: day, until: day),
            providerFilter: .all)

        #expect(parsed.rows.count == 1)
        #expect(parsed.rows[0].model == "claude-fable-5")
        let expected = 0.001395
        #expect(abs((Double(parsed.rows[0].costNanos) / 1_000_000_000) - expected) < 0.000000001)
    }

    @Test
    func `claude transcript refusal remains priced without billing provenance`() throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }

        let day = try env.makeLocalNoon(year: 2026, month: 6, day: 9)
        let fileURL = try env.writeClaudeProjectFile(
            relativePath: "project-a/fable-5-refusal.jsonl",
            contents: env.jsonl([
                [
                    "message": [
                        "model": "claude-fable-5",
                        "id": "msg_fable_5_refusal",
                        "type": "message",
                        "role": "assistant",
                        "stop_reason": "refusal",
                        "usage": [
                            "input_tokens": 100,
                            "cache_creation_input_tokens": 10,
                            "cache_read_input_tokens": 20,
                            "output_tokens": 0,
                        ],
                    ],
                    "requestId": "req_fable_5_refusal",
                    "type": "assistant",
                    "timestamp": "2026-06-09T12:00:00.000Z",
                    "sessionId": "session_fable_5_refusal",
                ],
            ]))

        let parsed = CostUsageScanner.parseClaudeFile(
            fileURL: fileURL,
            range: CostUsageScanner.CostUsageDayRange(since: day, until: day),
            providerFilter: .all)

        #expect(parsed.rows.count == 1)
        #expect(parsed.rows[0].input == 100)
        #expect(parsed.rows[0].cacheCreate == 10)
        #expect(parsed.rows[0].cacheRead == 20)
        #expect(parsed.rows[0].output == 0)
        let expected = 0.001145
        #expect(abs((Double(parsed.rows[0].costNanos) / 1_000_000_000) - expected) < 0.000000001)
        #expect(parsed.rows[0].costPriced == true)
    }

    @Test
    func `claude fable 5 prices one hour cache creation tokens`() throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }

        let day = try env.makeLocalNoon(year: 2026, month: 6, day: 9)
        let fileURL = try env.writeClaudeProjectFile(
            relativePath: "project-a/fable-5-cache-ttl.jsonl",
            contents: env.jsonl([
                [
                    "message": [
                        "model": "claude-fable-5",
                        "id": "msg_fable_5_cache_ttl",
                        "type": "message",
                        "role": "assistant",
                        "usage": [
                            "input_tokens": 100,
                            "cache_creation_input_tokens": 30,
                            "cache_creation": [
                                "ephemeral_5m_input_tokens": 10,
                                "ephemeral_1h_input_tokens": 20,
                            ],
                            "cache_read_input_tokens": 20,
                            "output_tokens": 5,
                        ],
                    ],
                    "requestId": "req_fable_5_cache_ttl",
                    "type": "assistant",
                    "timestamp": "2026-06-09T12:00:00.000Z",
                    "sessionId": "session_fable_5_cache_ttl",
                ],
            ]))

        let parsed = CostUsageScanner.parseClaudeFile(
            fileURL: fileURL,
            range: CostUsageScanner.CostUsageDayRange(since: day, until: day),
            providerFilter: .all)

        #expect(parsed.rows.count == 1)
        #expect(parsed.rows[0].cacheCreate == 30)
        #expect(parsed.rows[0].cacheCreate1h == 20)
        let expected = 0.001795
        #expect(abs((Double(parsed.rows[0].costNanos) / 1_000_000_000) - expected) < 0.000000001)
    }

    @Test
    func `claude cached rows preserve one hour writes for deferred pricing`() throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }

        let day = try env.makeLocalNoon(year: 2026, month: 6, day: 9)
        _ = try env.writeClaudeProjectFile(
            relativePath: "project-a/custom-cache-ttl.jsonl",
            contents: env.jsonl([
                [
                    "message": [
                        "model": "claude-custom-cache-model",
                        "id": "msg_custom_cache_ttl",
                        "type": "message",
                        "role": "assistant",
                        "usage": [
                            "input_tokens": 100,
                            "cache_creation_input_tokens": 30,
                            "cache_creation": [
                                "ephemeral_5m_input_tokens": 10,
                                "ephemeral_1h_input_tokens": 20,
                            ],
                            "cache_read_input_tokens": 20,
                            "output_tokens": 5,
                        ],
                    ],
                    "requestId": "req_custom_cache_ttl",
                    "type": "assistant",
                    "timestamp": "2026-06-09T12:00:00.000Z",
                    "sessionId": "session_custom_cache_ttl",
                ],
            ]))

        var options = CostUsageScanner.Options(
            claudeProjectsRoots: [env.claudeProjectsRoot],
            cacheRoot: env.cacheRoot)
        options.refreshMinIntervalSeconds = 0

        let unpriced = CostUsageScanner.loadDailyReport(
            provider: .claude,
            since: day,
            until: day,
            now: day,
            options: options)
        #expect(unpriced.summary?.totalCostUSD == nil)

        let cached = CostUsageClaudeCacheIO.load(provider: .claude, cacheRoot: env.cacheRoot)
        #expect(cached.days["2026-06-09"]?["claude-custom-cache-model"]?[safe: 7] == 20)

        try ModelsDevCache.save(
            catalog: Self.anthropicModelsDevCatalog(model: "claude-custom-cache-model"),
            fetchedAt: day,
            cacheRoot: env.cacheRoot)
        options.refreshMinIntervalSeconds = 3600

        let repriced = CostUsageScanner.loadDailyReport(
            provider: .claude,
            since: day,
            until: day,
            now: day.addingTimeInterval(1),
            options: options)

        let expected = 0.001795
        #expect(abs((repriced.summary?.totalCostUSD ?? 0) - expected) < 0.000000001)
    }

    @Test
    func `claude deferred pricing preserves request long context boundaries`() throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }

        let day = try env.makeLocalNoon(year: 2026, month: 6, day: 9)
        _ = try env.writeClaudeProjectFile(
            relativePath: "project-a/custom-threshold.jsonl",
            contents: env.jsonl([
                Self.claudeUsageEvent(
                    model: "claude-custom-threshold-model",
                    messageID: "msg_custom_threshold_1",
                    requestID: "req_custom_threshold_1",
                    inputTokens: 150_000),
                Self.claudeUsageEvent(
                    model: "claude-custom-threshold-model",
                    messageID: "msg_custom_threshold_2",
                    requestID: "req_custom_threshold_2",
                    inputTokens: 150_000),
            ]))

        var options = CostUsageScanner.Options(
            claudeProjectsRoots: [env.claudeProjectsRoot],
            cacheRoot: env.cacheRoot)
        options.refreshMinIntervalSeconds = 0

        let unpriced = CostUsageScanner.loadDailyReport(
            provider: .claude,
            since: day,
            until: day,
            now: day,
            options: options)
        #expect(unpriced.summary?.totalCostUSD == nil)

        try ModelsDevCache.save(
            catalog: Self.anthropicThresholdModelsDevCatalog(model: "claude-custom-threshold-model"),
            fetchedAt: day,
            cacheRoot: env.cacheRoot)
        options.refreshMinIntervalSeconds = 3600

        let repriced = CostUsageScanner.loadDailyReport(
            provider: .claude,
            since: day,
            until: day,
            now: day.addingTimeInterval(1),
            options: options)

        #expect(abs((repriced.summary?.totalCostUSD ?? 0) - 3) < 0.000000001)
    }

    @Test
    func `claude cached rows reprice after models dev catalog changes`() throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }

        let day = try env.makeLocalNoon(year: 2026, month: 6, day: 9)
        let model = "claude-custom-repricing-model"
        _ = try env.writeClaudeProjectFile(
            relativePath: "project-a/custom-repricing.jsonl",
            contents: env.jsonl([
                Self.claudeUsageEvent(
                    model: model,
                    messageID: "msg_custom_repricing",
                    requestID: "req_custom_repricing",
                    inputTokens: 240_000),
            ]))
        try ModelsDevCache.save(
            catalog: Self.anthropicThresholdModelsDevCatalog(model: model),
            fetchedAt: day,
            cacheRoot: env.cacheRoot)

        var options = CostUsageScanner.Options(
            claudeProjectsRoots: [env.claudeProjectsRoot],
            cacheRoot: env.cacheRoot)
        options.refreshMinIntervalSeconds = 0

        let premium = CostUsageScanner.loadDailyReport(
            provider: .claude,
            since: day,
            until: day,
            now: day,
            options: options)
        #expect(abs((premium.summary?.totalCostUSD ?? 0) - 4.8) < 0.000000001)

        try ModelsDevCache.save(
            catalog: Self.anthropicModelsDevCatalog(model: model),
            fetchedAt: day.addingTimeInterval(1),
            cacheRoot: env.cacheRoot)
        options.refreshMinIntervalSeconds = 3600

        let repriced = CostUsageScanner.loadDailyReport(
            provider: .claude,
            since: day,
            until: day,
            now: day.addingTimeInterval(2),
            options: options)

        #expect(abs((repriced.summary?.totalCostUSD ?? 0) - 2.4) < 0.000000001)
    }

    @Test
    func `claude cached historical rows keep original tariff`() throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }

        let day = try env.makeLocalNoon(year: 2026, month: 3, day: 12)
        _ = try env.writeClaudeProjectFile(
            relativePath: "project-a/sonnet-46-historical.jsonl",
            contents: env.jsonl([
                [
                    "message": [
                        "model": "claude-sonnet-4-6",
                        "id": "msg_sonnet_46_historical",
                        "type": "message",
                        "role": "assistant",
                        "usage": [
                            "input_tokens": 240_000,
                            "cache_creation_input_tokens": 0,
                            "cache_read_input_tokens": 0,
                            "output_tokens": 0,
                        ],
                    ],
                    "requestId": "req_sonnet_46_historical",
                    "type": "assistant",
                    "timestamp": "2026-03-12T12:00:00.000Z",
                    "sessionId": "session_sonnet_46_historical",
                ],
            ]))

        var options = CostUsageScanner.Options(
            claudeProjectsRoots: [env.claudeProjectsRoot],
            cacheRoot: env.cacheRoot)
        options.refreshMinIntervalSeconds = 0

        let initial = CostUsageScanner.loadDailyReport(
            provider: .claude,
            since: day,
            until: day,
            now: day,
            options: options)
        #expect(abs((initial.summary?.totalCostUSD ?? 0) - 1.44) < 0.000000001)

        try ModelsDevCache.save(
            catalog: Self.anthropicSonnet46StandardCatalog(),
            fetchedAt: day,
            cacheRoot: env.cacheRoot)
        options.refreshMinIntervalSeconds = 3600

        let cached = CostUsageScanner.loadDailyReport(
            provider: .claude,
            since: day,
            until: day,
            now: day.addingTimeInterval(1),
            options: options)

        #expect(abs((cached.summary?.totalCostUSD ?? 0) - 1.44) < 0.000000001)
    }

    private static func anthropicModelsDevCatalog(model: String) throws -> ModelsDevCatalog {
        let json = """
        {
          "anthropic": {
            "id": "anthropic",
            "models": {
              "\(model)": {
                "id": "\(model)",
                "cost": {
                  "input": 10,
                  "output": 50,
                  "cache_read": 1,
                  "cache_write": 12.5
                }
              }
            }
          }
        }
        """
        return try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(json.utf8))
    }

    private static func anthropicThresholdModelsDevCatalog(model: String) throws -> ModelsDevCatalog {
        let json = """
        {
          "anthropic": {
            "id": "anthropic",
            "models": {
              "\(model)": {
                "id": "\(model)",
                "cost": {
                  "input": 10,
                  "output": 50,
                  "cache_read": 1,
                  "cache_write": 12.5,
                  "context_over_200k": {
                    "input": 20,
                    "output": 75,
                    "cache_read": 2,
                    "cache_write": 25
                  }
                }
              }
            }
          }
        }
        """
        return try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(json.utf8))
    }

    private static func anthropicSonnet46StandardCatalog() throws -> ModelsDevCatalog {
        let json = """
        {
          "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
                }
              }
            }
          }
        }
        """
        return try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(json.utf8))
    }

    private static func claudeUsageEvent(
        model: String,
        messageID: String,
        requestID: String,
        inputTokens: Int) -> [String: Any]
    {
        [
            "message": [
                "model": model,
                "id": messageID,
                "type": "message",
                "role": "assistant",
                "usage": [
                    "input_tokens": inputTokens,
                    "cache_creation_input_tokens": 0,
                    "cache_read_input_tokens": 0,
                    "output_tokens": 0,
                ],
            ],
            "requestId": requestID,
            "type": "assistant",
            "timestamp": "2026-06-09T12:00:00.000Z",
            "sessionId": "session_\(requestID)",
        ]
    }
}
