import Foundation import Testing @testable import CodexBarCore struct WidgetSnapshotTests { @Test func `widget snapshot round trip`() throws { let entry = WidgetSnapshot.ProviderEntry( provider: .codex, updatedAt: Date(), primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil), secondary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil), tertiary: nil, usageRows: [ WidgetSnapshot.WidgetUsageRowSnapshot(id: "session", title: "Session", percentLeft: 90), WidgetSnapshot.WidgetUsageRowSnapshot(id: "weekly", title: "Weekly", percentLeft: 80), ], creditsRemaining: 123.4, codeReviewRemainingPercent: 80, tokenUsage: WidgetSnapshot.TokenUsageSummary( sessionCostUSD: 12.3, sessionTokens: 1200, last30DaysCostUSD: 456.7, last30DaysTokens: 9800, currencyCode: "eur", sessionLabel: "Latest billing day", last30DaysLabel: "This month"), dailyUsage: [ WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-20", totalTokens: 1200, costUSD: 12.3), ]) let snapshot = WidgetSnapshot( entries: [entry], enabledProviders: [.codex, .claude], usageBarsShowUsed: true, generatedAt: Date()) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let data = try encoder.encode(snapshot) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let decoded = try decoder.decode(WidgetSnapshot.self, from: data) #expect(decoded.entries.count == 1) #expect(decoded.entries.first?.provider == .codex) #expect(decoded.entries.first?.tokenUsage?.sessionTokens == 1200) #expect(decoded.entries.first?.tokenUsage?.currencyCode == "EUR") #expect(decoded.entries.first?.tokenUsage?.sessionLabel == "Latest billing day") #expect(decoded.entries.first?.tokenUsage?.last30DaysLabel == "This month") #expect(decoded.entries.first?.usageRows?.map(\.id) == ["session", "weekly"]) #expect(decoded.enabledProviders == [.codex, .claude]) #expect(decoded.usageBarsShowUsed) } @Test func `widget snapshot round trip preserves kilo provider`() throws { let entry = WidgetSnapshot.ProviderEntry( provider: .kilo, updatedAt: Date(), primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "40/100 credits"), secondary: nil, tertiary: nil, creditsRemaining: nil, codeReviewRemainingPercent: nil, tokenUsage: WidgetSnapshot.TokenUsageSummary( sessionCostUSD: 1.25, sessionTokens: 4200, last30DaysCostUSD: 19.75, last30DaysTokens: 58000), dailyUsage: [ WidgetSnapshot.DailyUsagePoint(dayKey: "2026-02-27", totalTokens: 4200, costUSD: 1.25), ]) let snapshot = WidgetSnapshot( entries: [entry], enabledProviders: [.kilo, .codex], generatedAt: Date()) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let data = try encoder.encode(snapshot) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let decoded = try decoder.decode(WidgetSnapshot.self, from: data) #expect(decoded.entries.first?.provider == .kilo) #expect(decoded.entries.first?.primary?.resetDescription == "40/100 credits") #expect(decoded.enabledProviders == [.kilo, .codex]) } @Test func `widget snapshot round trip preserves kilo zero total edge state`() throws { let now = Date() let kiloSnapshot = KiloUsageSnapshot( creditsUsed: 0, creditsTotal: 0, creditsRemaining: 0, planName: "Kilo Pass Pro", autoTopUpEnabled: true, autoTopUpMethod: "visa", updatedAt: now).toUsageSnapshot() let entry = WidgetSnapshot.ProviderEntry( provider: .kilo, updatedAt: now, primary: kiloSnapshot.primary, secondary: kiloSnapshot.secondary, tertiary: kiloSnapshot.tertiary, creditsRemaining: nil, codeReviewRemainingPercent: nil, tokenUsage: nil, dailyUsage: []) let snapshot = WidgetSnapshot( entries: [entry], enabledProviders: [.kilo], generatedAt: now) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let data = try encoder.encode(snapshot) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let decoded = try decoder.decode(WidgetSnapshot.self, from: data) #expect(decoded.entries.first?.provider == .kilo) #expect(decoded.entries.first?.primary?.usedPercent == 100) #expect(decoded.entries.first?.primary?.remainingPercent == 0) #expect(decoded.entries.first?.primary?.resetDescription == "0/0 credits") #expect(decoded.enabledProviders == [.kilo]) } @Test func `widget snapshot decodes legacy payload without usage rows`() throws { let json = """ { "entries": [ { "provider": "codex", "updatedAt": "2026-04-04T06:30:00Z", "primary": null, "secondary": { "usedPercent": 25, "windowMinutes": 10080, "resetsAt": null, "resetDescription": null }, "tertiary": null, "creditsRemaining": null, "codeReviewRemainingPercent": null, "tokenUsage": null, "dailyUsage": [] } ], "enabledProviders": ["codex"], "generatedAt": "2026-04-04T06:30:00Z" } """ let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let decoded = try decoder.decode(WidgetSnapshot.self, from: Data(json.utf8)) #expect(decoded.entries.count == 1) #expect(decoded.entries.first?.usageRows == nil) #expect(decoded.entries.first?.secondary?.usedPercent == 25) #expect(!decoded.usageBarsShowUsed) } @Test func `widget snapshot decodes legacy token usage as usd`() throws { let json = """ { "entries": [ { "provider": "codex", "updatedAt": "2026-04-04T06:30:00Z", "primary": null, "secondary": null, "tertiary": null, "creditsRemaining": null, "codeReviewRemainingPercent": null, "tokenUsage": { "sessionCostUSD": 1.25, "sessionTokens": 1200, "last30DaysCostUSD": 9.50, "last30DaysTokens": 4200 }, "dailyUsage": [] } ], "generatedAt": "2026-04-04T06:30:00Z" } """ let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let decoded = try decoder.decode(WidgetSnapshot.self, from: Data(json.utf8)) #expect(decoded.entries.first?.tokenUsage?.currencyCode == "USD") #expect(decoded.entries.first?.tokenUsage?.sessionLabel == "Today") #expect(decoded.entries.first?.tokenUsage?.last30DaysLabel == "30d") #expect(decoded.enabledProviders == [.codex]) } @Test func `token usage summary round trips updatedAt and tolerates legacy payloads`() throws { let updatedAt = Date(timeIntervalSince1970: 1_760_000_000) let summary = WidgetSnapshot.TokenUsageSummary( sessionCostUSD: 1.5, sessionTokens: 100, last30DaysCostUSD: 30, last30DaysTokens: 2000, updatedAt: updatedAt) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let decoded = try decoder.decode( WidgetSnapshot.TokenUsageSummary.self, from: encoder.encode(summary)) #expect(decoded.updatedAt == updatedAt) let legacy = try decoder.decode( WidgetSnapshot.TokenUsageSummary.self, from: Data(#"{"sessionCostUSD": 1.5, "sessionTokens": 100}"#.utf8)) #expect(legacy.updatedAt == nil) } @Test func `token usage staleness discloses only meaningful lag`() { let entryUpdatedAt = Date() func summary(updatedAt: Date?) -> WidgetSnapshot.TokenUsageSummary { WidgetSnapshot.TokenUsageSummary( sessionCostUSD: nil, sessionTokens: nil, last30DaysCostUSD: nil, last30DaysTokens: nil, updatedAt: updatedAt) } #expect(!summary(updatedAt: entryUpdatedAt.addingTimeInterval(-5 * 60)) .isStale(comparedTo: entryUpdatedAt)) #expect(summary(updatedAt: entryUpdatedAt.addingTimeInterval(-61 * 60)) .isStale(comparedTo: entryUpdatedAt)) #expect(!summary(updatedAt: nil).isStale(comparedTo: entryUpdatedAt)) } }