import Testing
import Foundation
import SQLite3
@testable import TokenLensCore

@Test func formatTokensUsesChineseMagnitude() {
    #expect(TokenFormatter.format(9_999) == "9999")
    #expect(TokenFormatter.format(10_000) == "1.000万")
    #expect(TokenFormatter.format(71_400) == "7.140万")
    #expect(TokenFormatter.format(6_600_000) == "660.000万")
    #expect(TokenFormatter.format(12_345_678) == "1234.6万")
    #expect(TokenFormatter.format(196_700_000) == "1.967亿")
    #expect(TokenFormatter.format(1_020_000_000) == "10.200亿")
    #expect(TokenFormatter.format(100_000_000_000) == "1000.0亿")
}

@Test func parseClaudeUsageLineSkipsConversationContent() throws {
    let line = """
    {"timestamp":"2026-06-11T02:20:00.000Z","sessionId":"s1","cwd":"/tmp","message":{"model":"claude-3-5-sonnet","content":"private text","usage":{"input_tokens":120,"output_tokens":30,"cache_read_input_tokens":50,"cache_creation_input_tokens":10}}}
    """

    let record = try #require(TokenUsageParser.parseClaudeLine(line, sourcePath: "/tmp/a.jsonl"))

    #expect(record.provider == .claude)
    #expect(record.model == "claude-3-5-sonnet")
    #expect(record.inputTokens == 120)
    #expect(record.outputTokens == 30)
    #expect(record.cachedTokens == 60)
    #expect(record.totalTokens == 210)
}

@Test func parseCodexUsesLastTokenUsageOnly() throws {
    let line = """
    {"timestamp":"2026-06-11T03:10:00.000Z","type":"event_msg","payload":{"info":{"total_token_usage":{"input_tokens":1000,"output_tokens":300,"cached_input_tokens":200,"reasoning_output_tokens":40,"total_tokens":1300},"last_token_usage":{"input_tokens":100,"output_tokens":30,"cached_input_tokens":20,"reasoning_output_tokens":4,"total_tokens":130}}}}
    """

    let record = try #require(TokenUsageParser.parseCodexLine(line, sourcePath: "/tmp/rollout.jsonl"))

    #expect(record.provider == .codex)
    #expect(record.inputTokens == 100)
    #expect(record.outputTokens == 30)
    #expect(record.cachedTokens == 20)
    #expect(record.reasoningTokens == 4)
    #expect(record.totalTokens == 154)
}

@Test func parseGeminiTokensLine() throws {
    let line = """
    {"timestamp":"2026-06-11T04:00:00.000Z","type":"response","model":"gemini-2.5-pro","tokens":{"input":70,"output":20,"cached":5,"thoughts":3,"tool":2,"total":100}}
    """

    let record = try #require(TokenUsageParser.parseGeminiLine(line, sourcePath: "/tmp/session.jsonl"))

    #expect(record.provider == .gemini)
    #expect(record.model == "gemini-2.5-pro")
    #expect(record.inputTokens == 70)
    #expect(record.outputTokens == 20)
    #expect(record.cachedTokens == 5)
    #expect(record.reasoningTokens == 3)
    #expect(record.toolTokens == 2)
    #expect(record.totalTokens == 100)
}

@Test func summaryAggregatesTodayMonthProvidersAndSources() {
    let calendar = Calendar(identifier: .gregorian)
    let now = ISO8601DateFormatter().date(from: "2026-06-11T04:00:00Z")!
    let records = [
        usage(.claude, "2026-06-11T01:00:00Z", 100, 20, 10),
        usage(.codex, "2026-06-10T01:00:00Z", 80, 20, 0),
        usage(.gemini, "2026-05-31T01:00:00Z", 50, 10, 0)
    ]

    let summary = UsageSummary.make(records: records, now: now, calendar: calendar)

    #expect(summary.cards.todayTokens == 130)
    #expect(summary.cards.monthTokens == 230)
    #expect(summary.cards.totalTokens == 290)
    #expect(summary.providerTotals[.claude]?.totalTokens == 130)
    #expect(summary.providerTotals[.codex]?.totalTokens == 100)
    #expect(summary.providerTotals[.gemini]?.totalTokens == 60)
    #expect(summary.todayProviderTotals[.claude]?.totalTokens == 130)
    #expect(summary.todayProviderTotals[.codex]?.totalTokens == 0)
    #expect(summary.todayProviderTotals[.gemini]?.totalTokens == 0)
    #expect(summary.toolRows.count == 3)
}

@Test func toolRowsKeepProviderSpecificTrends() {
    let calendar = Calendar(identifier: .gregorian)
    let now = ISO8601DateFormatter().date(from: "2026-06-11T04:00:00Z")!
    let records = [
        usage(.claude, "2026-06-09T01:00:00Z", 10, 0, 0),
        usage(.claude, "2026-06-11T01:00:00Z", 30, 0, 0),
        usage(.codex, "2026-06-10T01:00:00Z", 20, 0, 0),
        usage(.gemini, "2026-06-11T02:00:00Z", 5, 0, 0)
    ]

    let summary = UsageSummary.make(records: records, now: now, calendar: calendar)
    let trends = Dictionary(uniqueKeysWithValues: summary.toolRows.map { ($0.provider, $0.trendTokens) })

    #expect(trends[.claude] == [10, 0, 30])
    #expect(trends[.codex] == [0, 20, 0])
    #expect(trends[.gemini] == [0, 0, 5])
}

@Test func trendPointsRegroupBySelectedMode() {
    var calendar = Calendar(identifier: .gregorian)
    calendar.timeZone = TimeZone(secondsFromGMT: 0)!
    calendar.firstWeekday = 2
    let now = ISO8601DateFormatter().date(from: "2026-06-11T12:30:00Z")!
    let records = [
        usage(.claude, "2026-06-11T01:00:00Z", 10, 0, 0),
        usage(.codex, "2026-06-11T10:00:00Z", 20, 0, 0),
        usage(.claude, "2026-06-11T01:00:00Z", 30, 0, 0),
        usage(.gemini, "2026-05-11T01:00:00Z", 5, 0, 0),
        usage(.codex, "2026-04-01T01:00:00Z", 7, 0, 0)
    ]
    let summary = UsageSummary.make(records: records, now: now, calendar: calendar)

    let daily = summary.trendPoints(mode: .daily, calendar: calendar)
    #expect(daily.count == 13)
    #expect(daily[1].values[.claude] == 40)
    #expect(daily[10].values[.codex] == 20)

    let weekly = summary.trendPoints(mode: .weekly, calendar: calendar)
    #expect(weekly.count == 4)
    #expect(weekly.last?.values[.claude] == 40)
    #expect(weekly.last?.values[.codex] == 20)

    let monthly = summary.trendPoints(mode: .monthly, calendar: calendar)
    #expect(monthly.count == 11)
    #expect(monthly.last?.values[.claude] == 40)
    #expect(monthly.last?.values[.codex] == 20)

    let yearly = summary.trendPoints(mode: .yearly, calendar: calendar)
    #expect(yearly.count == 6)
    #expect(yearly[3].values[.codex] == 7)
    #expect(yearly[4].values[.gemini] == 5)
    #expect(yearly[5].values[.claude] == 40)
}

@Test func dailyUsageDetailsAggregateProviderTotals() {
    var calendar = Calendar(identifier: .gregorian)
    calendar.timeZone = TimeZone(secondsFromGMT: 0)!
    let now = ISO8601DateFormatter().date(from: "2026-06-11T12:30:00Z")!
    let records = [
        usage(.claude, "2026-06-11T01:00:00Z", 10, 2, 1),
        usage(.codex, "2026-06-11T10:00:00Z", 20, 3, 0),
        usage(.gemini, "2026-06-10T01:00:00Z", 5, 1, 0)
    ]
    let summary = UsageSummary.make(records: records, now: now, calendar: calendar)
    let details = summary.dailyUsageDetails(calendar: calendar)

    #expect(details.count == 2)
    #expect(details[0].totalTokens == 36)
    #expect(details[0].inputTokens == 30)
    #expect(details[0].outputTokens == 5)
    #expect(details[0].cachedTokens == 1)
    #expect(details[0].providerTotals[.claude] == 13)
    #expect(details[0].providerTotals[.codex] == 23)
    #expect(details[1].totalTokens == 6)
    #expect(details[1].providerTotals[.gemini] == 6)
}

@Test func scannerIndexesLogsThroughSQLiteCache() async throws {
    let root = FileManager.default.temporaryDirectory
        .appending(path: "TokenLensTests-\(UUID().uuidString)")
    defer { try? FileManager.default.removeItem(at: root) }

    let sessions = root.appending(path: ".codex/sessions")
    try FileManager.default.createDirectory(at: sessions, withIntermediateDirectories: true)
    let file = sessions.appending(path: "rollout.jsonl")
    let line = """
    {"timestamp":"2026-06-11T03:10:00.000Z","type":"event_msg","payload":{"info":{"last_token_usage":{"input_tokens":100,"output_tokens":30,"cached_input_tokens":20,"reasoning_output_tokens":4}}}}
    """
    try line.write(to: file, atomically: true, encoding: .utf8)

    let scanner = TokenUsageScanner(
        homeDirectory: root,
        cacheURL: root.appending(path: "Library/Application Support/TokenLens/test-cache.sqlite")
    )
    let now = ISO8601DateFormatter().date(from: "2026-06-11T12:30:00Z")!

    let first = await scanner.scan(now: now)
    let second = await scanner.scan(now: now)
    let appendedLine = """
    {"timestamp":"2026-06-11T03:12:00.000Z","type":"event_msg","payload":{"info":{"last_token_usage":{"input_tokens":200,"output_tokens":60,"cached_input_tokens":40,"reasoning_output_tokens":8}}}}
    """
    let handle = try FileHandle(forWritingTo: file)
    try handle.seekToEnd()
    try handle.write(contentsOf: Data(("\n" + appendedLine).utf8))
    try handle.close()
    let third = await scanner.scan(now: now)

    #expect(first.cards.totalTokens == 154)
    #expect(second.cards.totalTokens == 154)
    #expect(second.providerTotals[.codex]?.totalTokens == 154)
    #expect(third.cards.totalTokens == 462)
    #expect(third.providerTotals[.codex]?.totalTokens == 462)
}

@Test func scannerIndexesHermesAndOpenCodeSQLiteSources() async throws {
    let root = FileManager.default.temporaryDirectory
        .appending(path: "TokenLensSQLiteSources-\(UUID().uuidString)")
    defer { try? FileManager.default.removeItem(at: root) }

    let hermes = root.appending(path: ".hermes")
    try FileManager.default.createDirectory(at: hermes, withIntermediateDirectories: true)
    try makeHermesStateDB(at: hermes.appending(path: "state.db"))

    let opencode = root.appending(path: ".local/share/opencode")
    try FileManager.default.createDirectory(at: opencode, withIntermediateDirectories: true)
    try makeOpenCodeDB(at: opencode.appending(path: "opencode.db"))

    let scanner = TokenUsageScanner(
        homeDirectory: root,
        cacheURL: root.appending(path: "Library/Application Support/TokenLens/test-cache.sqlite")
    )
    let now = Date(timeIntervalSince1970: 1_780_938_000)
    let summary = await scanner.scan(now: now)

    #expect(summary.providerTotals[.hermes]?.totalTokens == 23)
    #expect(summary.providerTotals[.opencode]?.totalTokens == 41)
    #expect(summary.cards.totalTokens == 64)
    #expect(summary.toolRows.map(\.provider).contains(.hermes))
    #expect(summary.toolRows.map(\.provider).contains(.opencode))
}

private func usage(_ provider: Provider, _ timestamp: String, _ input: Int, _ output: Int, _ cached: Int) -> UsageRecord {
    UsageRecord(
        provider: provider,
        source: "\(provider.rawValue) source",
        sourcePath: "/tmp/\(provider.rawValue).jsonl",
        timestamp: ISO8601DateFormatter().date(from: timestamp)!,
        sessionID: "",
        model: provider.rawValue,
        inputTokens: input,
        outputTokens: output,
        cachedTokens: cached,
        reasoningTokens: 0,
        toolTokens: 0
    )
}

private func makeHermesStateDB(at url: URL) throws {
    let statements = [
        """
        CREATE TABLE sessions (
            id TEXT PRIMARY KEY,
            source TEXT NOT NULL,
            model TEXT,
            started_at REAL NOT NULL,
            input_tokens INTEGER DEFAULT 0,
            output_tokens INTEGER DEFAULT 0,
            cache_read_tokens INTEGER DEFAULT 0,
            cache_write_tokens INTEGER DEFAULT 0,
            reasoning_tokens INTEGER DEFAULT 0
        )
        """,
        """
        INSERT INTO sessions (
            id, source, model, started_at, input_tokens, output_tokens,
            cache_read_tokens, cache_write_tokens, reasoning_tokens
        ) VALUES ('h1', 'cli', 'gpt-5.5', 1780937894.0, 10, 5, 3, 2, 3)
        """
    ]
    try writeSQLiteDatabase(at: url, statements: statements)
}

private func makeOpenCodeDB(at url: URL) throws {
    let statements = [
        """
        CREATE TABLE session (
            id TEXT PRIMARY KEY,
            time_created INTEGER NOT NULL,
            time_updated INTEGER NOT NULL,
            model TEXT,
            tokens_input INTEGER DEFAULT 0 NOT NULL,
            tokens_output INTEGER DEFAULT 0 NOT NULL,
            tokens_reasoning INTEGER DEFAULT 0 NOT NULL,
            tokens_cache_read INTEGER DEFAULT 0 NOT NULL,
            tokens_cache_write INTEGER DEFAULT 0 NOT NULL
        )
        """,
        """
        INSERT INTO session (
            id, time_created, time_updated, model, tokens_input, tokens_output,
            tokens_reasoning, tokens_cache_read, tokens_cache_write
        ) VALUES (
            'o1', 1780937894000, 1780937895000,
            '{"id":"MiniMax-M3","providerID":"minimax-cn"}',
            11, 7, 5, 13, 5
        )
        """
    ]
    try writeSQLiteDatabase(at: url, statements: statements)
}

private func writeSQLiteDatabase(at url: URL, statements: [String]) throws {
    var database: OpaquePointer?
    guard sqlite3_open_v2(url.path, &database, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nil) == SQLITE_OK else {
        sqlite3_close(database)
        struct OpenError: Error {}
        throw OpenError()
    }
    defer { sqlite3_close(database) }
    for statement in statements {
        guard sqlite3_exec(database, statement, nil, nil, nil) == SQLITE_OK else {
            struct ExecuteError: Error {}
            throw ExecuteError()
        }
    }
}
