import Foundation
import Testing

@testable import NucleicProtocol

@Suite struct ActivityShadingTests {
    private let cal: Calendar = {
        var c = Calendar(identifier: .gregorian)
        c.timeZone = TimeZone(identifier: "UTC")!
        return c
    }()
    private let epoch = Date(timeIntervalSince1970: 0)
    private func days(_ counts: [Int]) -> [Date: Int] {
        Dictionary(uniqueKeysWithValues: counts.enumerated().map { i, n in
            (cal.date(byAdding: .day, value: i, to: epoch)!, n)
        })
    }

    @Test func tokensDriveIntensityWhenPresent() {
        // A day with token usage is shaded by the token distribution, not the message one.
        let shading = ActivityShading(
            tokensByDay: days([1_000, 2_000, 3_000, 4_000]),   // token upperBound 4_000
            messagesByDay: days([1, 2, 3, 4]))
        // 4_000 tokens is a peak token day ⇒ full brightness, regardless of message count.
        #expect(shading.intensity(tokens: 4_000, messages: 1) == 1.0)
        #expect(abs(shading.intensity(tokens: 2_000, messages: 1)
            - ActivityScale(activityByDay: days([1_000, 2_000, 3_000, 4_000])).intensity(for: 2_000)) < 1e-9)
    }

    @Test func fallsBackToMessagesWhenNoTokens() {
        // A day that recorded no tokens shades by its message count against the message scale.
        let shading = ActivityShading(
            tokensByDay: days([1_000, 2_000, 3_000, 4_000]),
            messagesByDay: days([1, 2, 3, 4]))
        let messageScale = ActivityScale(activityByDay: days([1, 2, 3, 4]))
        #expect(shading.intensity(tokens: 0, messages: 4) == messageScale.intensity(for: 4))
        #expect(shading.intensity(tokens: 0, messages: 4) == 1.0)  // peak message day = full
    }

    @Test func messageOnlyDaysDoNotCollapseUnderTokenScale() {
        // The whole point of dual scales: a busy message-only day stays bright even though token
        // counts are ~1000× larger. A single shared scale would crush it toward the floor.
        let shading = ActivityShading(
            tokensByDay: days([500_000, 800_000]),   // huge token days
            messagesByDay: days([1, 2, 3, 4, 5]))    // ordinary message days
        let bright = shading.intensity(tokens: 0, messages: 5)   // peak message-only day
        #expect(bright == 1.0)
        #expect(bright >= ActivityScale.defaultFloor)
    }

    @Test func emptyDayIsZero() {
        let shading = ActivityShading(tokensByDay: days([1_000]), messagesByDay: days([3]))
        #expect(shading.intensity(tokens: 0, messages: 0) == 0)
    }

    @Test func noDataAtAllIsInert() {
        let shading = ActivityShading(tokensByDay: [:], messagesByDay: [:])
        #expect(shading.intensity(tokens: 0, messages: 0) == 0)
    }
}

@Suite struct UsageTotalTokensTests {
    @Test func sumsAllDisjointTokenCategories() {
        let usage = Usage(
            inputTokens: 10, cachedInputTokens: 100, cacheCreationInputTokens: 5,
            outputTokens: 20, reasoningTokens: 7)
        #expect(usage.totalTokens == 142)
    }

    @Test func absentCategoriesCountAsZero() {
        #expect(Usage(outputTokens: 42).totalTokens == 42)
        #expect(Usage().totalTokens == 0)
    }
}

@Suite struct TokenCountTests {
    @Test func exactBelowOneThousand() {
        #expect(TokenCount.abbreviated(0) == "0")
        #expect(TokenCount.abbreviated(942) == "942")
        #expect(TokenCount.abbreviated(-5) == "0")   // clamps negatives
    }

    @Test func abbreviatesThousandsMillionsBillions() {
        #expect(TokenCount.abbreviated(1_000) == "1K")
        #expect(TokenCount.abbreviated(12_345) == "12.3K")
        #expect(TokenCount.abbreviated(4_500_000) == "4.5M")
        #expect(TokenCount.abbreviated(1_200_000_000) == "1.2B")
    }

    @Test func trimsTrailingPointZero() {
        #expect(TokenCount.abbreviated(5_000) == "5K")
        #expect(TokenCount.abbreviated(2_000_000) == "2M")
    }

    @Test func adaptiveCandidatesRunWidestToNarrowest() {
        // Three significant numerals wide, two narrow — never the full number.
        #expect(TokenCount.adaptiveCandidates(1_632_451_000) == ["1.63B", "1.6B"])
        #expect(TokenCount.adaptiveCandidates(4_532_100) == ["4.53M", "4.5M"])
        #expect(TokenCount.adaptiveCandidates(12_345) == ["12.3K", "12K"])
    }

    @Test func adaptiveCandidatesNeverExceedThreeNumerals() {
        // The cap is the whole point: no rendering of any value shows more than three numerals.
        for n in [0, 7, 942, 1_000, 12_345, 123_456, 4_532_100, 999_999_999, 1_632_451_000] {
            for candidate in TokenCount.adaptiveCandidates(n) {
                let numerals = candidate.filter(\.isNumber).count
                #expect(numerals <= 3, "\(candidate) (from \(n)) shows \(numerals) numerals")
            }
        }
    }

    @Test func adaptiveCandidatesCollapseDuplicates() {
        // Round numbers and sub-unit values render identically at both rungs — one candidate.
        #expect(TokenCount.adaptiveCandidates(942) == ["942"])
        #expect(TokenCount.adaptiveCandidates(0) == ["0"])
        #expect(TokenCount.adaptiveCandidates(2_000_000_000) == ["2B"])
        #expect(TokenCount.adaptiveCandidates(123_456) == ["123K"])
    }
}
