import Foundation import Testing @testable import CodexBarCore @Suite struct CCUsageDecodingTests { @Test func decodesDailyReportTypeFormat() throws { let json = """ { "type": "daily", "data": [ { "date": "2025-12-20", "inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "costUSD": 0.12 } ], "summary": { "totalInputTokens": 10, "totalOutputTokens": 20, "totalTokens": 30, "totalCostUSD": 0.12 } } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) #expect(report.data.count == 1) #expect(report.data[0].date == "2025-12-20") #expect(report.data[0].totalTokens == 30) #expect(report.data[0].costUSD == 0.12) #expect(report.summary?.totalCostUSD == 0.12) } @Test func decodesDailyReportLegacyFormat() throws { let json = """ { "daily": [ { "date": "2025-12-20", "inputTokens": 1, "outputTokens": 2, "totalTokens": 3, "totalCost": 0.01 } ], "totals": { "totalInputTokens": 1, "totalOutputTokens": 2, "totalTokens": 3, "totalCost": 0.01 } } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) #expect(report.data.count == 1) #expect(report.summary?.totalTokens == 3) #expect(report.summary?.totalCostUSD == 0.01) } @Test func decodesDailyReportLegacyFormatWithModelMap() throws { let json = """ { "daily": [ { "date": "Dec 20, 2025", "inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "costUSD": 0.12, "models": { "gpt-5.2": { "inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "isFallback": false } } } ], "totals": { "totalTokens": 30, "costUSD": 0.12 } } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) #expect(report.data.count == 1) #expect(report.data[0].costUSD == 0.12) #expect(report.data[0].modelsUsed == ["gpt-5.2"]) } @Test func decodesDailyReportLegacyFormatWithModelMapSorted() throws { let json = """ { "daily": [ { "date": "Dec 20, 2025", "totalTokens": 30, "costUSD": 0.12, "models": { "z-model": { "totalTokens": 10 }, "a-model": { "totalTokens": 20 }, "m-model": { "totalTokens": 0 } } } ] } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) #expect(report.data[0].modelsUsed == ["a-model", "m-model", "z-model"]) } @Test func decodesDailyReportLegacyFormatWithEmptyModelMapAsNil() throws { let json = """ { "daily": [ { "date": "Dec 20, 2025", "totalTokens": 30, "costUSD": 0.12, "models": {} } ] } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) #expect(report.data[0].modelsUsed == nil) } @Test func decodesDailyReportLegacyFormatPrefersModelsUsedListOverModelsMap() throws { let json = """ { "daily": [ { "date": "Dec 20, 2025", "totalTokens": 30, "costUSD": 0.12, "modelsUsed": ["gpt-5.2"], "models": { "ignored-model": { "totalTokens": 30 } } } ] } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) #expect(report.data[0].modelsUsed == ["gpt-5.2"]) } @Test func decodesDailyReportLegacyFormatWithModelsList() throws { let json = """ { "daily": [ { "date": "Dec 20, 2025", "totalTokens": 30, "costUSD": 0.12, "models": ["gpt-5.2", "gpt-5.2-mini"] } ] } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) #expect(report.data[0].modelsUsed == ["gpt-5.2", "gpt-5.2-mini"]) } @Test func decodesDailyReportLegacyFormatWithInvalidModelsField() throws { let json = """ { "daily": [ { "date": "Dec 20, 2025", "totalTokens": 30, "costUSD": 0.12, "models": "gpt-5.2" } ] } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) #expect(report.data[0].modelsUsed == nil) } @Test func decodesMonthlyReportLegacyFormat() throws { let json = """ { "monthly": [ { "month": "Dec 2025", "totalTokens": 123, "costUSD": 4.56 } ], "totals": { "totalTokens": 123, "costUSD": 4.56 } } """ let report = try JSONDecoder().decode(CCUsageMonthlyReport.self, from: Data(json.utf8)) #expect(report.data.count == 1) #expect(report.data[0].month == "Dec 2025") #expect(report.data[0].costUSD == 4.56) #expect(report.summary?.totalCostUSD == 4.56) } @Test func selectsMostRecentSession() throws { let json = """ { "type": "session", "data": [ { "session": "A", "totalTokens": 100, "costUSD": 0.50, "lastActivity": "2025-12-19" }, { "session": "B", "totalTokens": 50, "costUSD": 0.20, "lastActivity": "2025-12-20T12:00:00Z" }, { "session": "C", "totalTokens": 200, "costUSD": 0.10, "lastActivity": "2025-12-20T11:00:00Z" } ], "summary": { "totalCostUSD": 0.80 } } """ let report = try JSONDecoder().decode(CCUsageSessionReport.self, from: Data(json.utf8)) let selected = CCUsageFetcher.selectCurrentSession(from: report.data) #expect(selected?.session == "B") } @Test func tokenSnapshotSelectsMostRecentDay() throws { let json = """ { "type": "daily", "data": [ { "date": "Dec 20, 2025", "totalTokens": 30, "costUSD": 1.23 }, { "date": "2025-12-21", "totalTokens": 10, "costUSD": 4.56 } ], "summary": { "totalCostUSD": 5.79 } } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) let now = Date(timeIntervalSince1970: 1_766_275_200) // 2025-12-21 let snapshot = CCUsageFetcher.tokenSnapshot(from: report, now: now) #expect(snapshot.sessionTokens == 10) #expect(snapshot.sessionCostUSD == 4.56) #expect(snapshot.last30DaysCostUSD == 5.79) #expect(snapshot.daily.count == 2) #expect(snapshot.updatedAt == now) } @Test func tokenSnapshotUsesSummaryTotalCostWhenAvailable() throws { let json = """ { "type": "daily", "data": [ { "date": "2025-12-20", "costUSD": 1.00 }, { "date": "2025-12-21", "costUSD": 2.00 } ], "summary": { "totalCostUSD": 99.00 } } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) let snapshot = CCUsageFetcher.tokenSnapshot(from: report, now: Date()) #expect(snapshot.last30DaysCostUSD == 99.00) } @Test func tokenSnapshotFallsBackToSummedEntriesWhenSummaryMissing() throws { let json = """ { "type": "daily", "data": [ { "date": "2025-12-20", "costUSD": 1.00 }, { "date": "2025-12-21", "costUSD": 2.00 } ] } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) let snapshot = CCUsageFetcher.tokenSnapshot(from: report, now: Date()) #expect(snapshot.last30DaysCostUSD == 3.00) } @Test func tokenSnapshotReturnsNilTotalWhenNoCostsPresent() throws { let json = """ { "type": "daily", "data": [ { "date": "2025-12-20", "totalTokens": 10 }, { "date": "2025-12-21", "totalTokens": 20 } ] } """ let report = try JSONDecoder().decode(CCUsageDailyReport.self, from: Data(json.utf8)) let snapshot = CCUsageFetcher.tokenSnapshot(from: report, now: Date()) #expect(snapshot.last30DaysCostUSD == nil) } }