import CodexBarCore
import Foundation
import Testing
@testable import CodexBar

@Suite(.serialized)
struct UsageFormatterTests {
    private static let usageFormatterLocalizationKeys: [String] = [
        "%@ left",
        "Resets %@",
        "Resets in %@",
        "Resets now",
        "reset_tomorrow_format",
        "Updated %@",
        "Updated relative %@",
        "Updated absolute %@",
        "Updated %@h ago",
        "Updated %@m ago",
        "Updated just now",
        "usage_percent_suffix_left",
        "usage_percent_suffix_used",
        "byte_unit_byte",
        "byte_unit_bytes",
        "byte_unit_kilobyte",
        "byte_unit_kilobytes",
        "byte_unit_megabyte",
        "byte_unit_megabytes",
        "byte_unit_gigabyte",
        "byte_unit_gigabytes",
    ]

    @Test
    func `formats usage line`() {
        UsageFormatter.clearLocalizationProvider()
        UsageFormatter.clearLocaleProvider()
        let line = UsageFormatter.usageLine(remaining: 25, used: 75, showUsed: false)
        #expect(line == "25% left")
    }

    @Test
    func `formats usage line show used`() {
        UsageFormatter.clearLocalizationProvider()
        UsageFormatter.clearLocaleProvider()
        let line = UsageFormatter.usageLine(remaining: 25, used: 75, showUsed: true)
        #expect(line == "75% used")
    }

    @Test
    func `positive sub percent usage stays visible`() {
        #expect(UsageFormatter.percentString(-1) == "0%")
        #expect(UsageFormatter.percentString(0) == "0%")
        #expect(UsageFormatter.percentString(0.1) == "<1%")
        #expect(UsageFormatter.percentString(0.96) == "<1%")
        #expect(UsageFormatter.percentString(1) == "1%")
        #expect(UsageFormatter.percentString(101) == "100%")
        #expect(UsageFormatter.usageLine(remaining: 99.9, used: 0.1, showUsed: true) == "<1% used")
        // Values in (0.5, 1) round up to "1%" under %.0f, so the old post-format
        // "0%" -> "<1%" replacement missed them. percentText must show "<1%"
        // across the whole sub-1% range, matching percentString above.
        #expect(UsageFormatter.usageLine(remaining: 99.4, used: 0.6, showUsed: true) == "<1% used")
        #expect(UsageFormatter.usageLine(remaining: 99.25, used: 0.75, showUsed: true) == "<1% used")
        #expect(UsageFormatter.usageLine(remaining: 0.75, used: 99.25, showUsed: false) == "<1% left")

        let usedWindow = RateWindow(usedPercent: 0.1, windowMinutes: nil, resetsAt: nil, resetDescription: nil)
        let leftWindow = RateWindow(usedPercent: 99.9, windowMinutes: nil, resetsAt: nil, resetDescription: nil)
        #expect(MenuBarDisplayText.percentText(window: usedWindow, showUsed: true) == "<1%")
        #expect(MenuBarDisplayText.percentText(window: leftWindow, showUsed: false) == "<1%")
    }

    @Test
    func `usage line respects injected localization provider`() {
        UsageFormatter.setLocalizationProvider { key in
            switch key {
            case "%.0f%% %@": "%2$@ %1$.0f%%"
            case "<1%% %@": "%1$@ <1%%"
            case "usage_percent_suffix_left": "剩余"
            case "usage_percent_suffix_used": "已使用"
            default: key
            }
        }
        defer { UsageFormatter.clearLocalizationProvider() }

        #expect(UsageFormatter.usageLine(remaining: 22, used: 78, showUsed: false) == "剩余 22%")
        #expect(UsageFormatter.usageLine(remaining: 22, used: 78, showUsed: true) == "已使用 78%")
        #expect(UsageFormatter.usageLine(remaining: 0.75, used: 99.25, showUsed: false) == "剩余 <1%")
        #expect(UsageFormatter.usageLine(remaining: 99.4, used: 0.6, showUsed: true) == "已使用 <1%")
    }

    @Test
    func `default locale fallback matches stable en US POSIX behavior`() {
        UsageFormatter.clearLocalizationProvider()
        UsageFormatter.clearLocaleProvider()

        let now = Date(timeIntervalSince1970: 1_710_048_000)
        let old = now.addingTimeInterval(-(26 * 3600))

        let defaultOutput = UsageFormatter.updatedString(from: old, now: now)
        UsageFormatter.setLocaleProvider { Locale(identifier: "en_US_POSIX") }
        let injectedStableOutput = UsageFormatter.updatedString(from: old, now: now)
        UsageFormatter.clearLocaleProvider()

        #expect(defaultOutput == injectedStableOutput)
    }

    @Test
    func `injected zh Hans locale applies app language formatting`() {
        UsageFormatter.setLocalizationProvider { key in
            switch key {
            case "Updated absolute %@":
                "更新于 %@"
            default:
                key
            }
        }
        UsageFormatter.setLocaleProvider { Locale(identifier: "zh-Hans") }
        defer {
            UsageFormatter.clearLocalizationProvider()
            UsageFormatter.clearLocaleProvider()
        }

        let now = Date(timeIntervalSince1970: 1_710_048_000)
        let old = now.addingTimeInterval(-(26 * 3600))
        let output = UsageFormatter.updatedString(from: old, now: now)

        #expect(output.hasPrefix("更新于 "))
    }

    @Test
    func `injected zh Hant relative updated string can place updated after relative time`() {
        UsageFormatter.setLocalizationProvider { key in
            switch key {
            case "Updated relative %@":
                "%@已更新"
            default:
                key
            }
        }
        UsageFormatter.setLocaleProvider { Locale(identifier: "zh-Hant") }
        defer {
            UsageFormatter.clearLocalizationProvider()
            UsageFormatter.clearLocaleProvider()
        }

        let now = Date(timeIntervalSince1970: 1_710_048_000)
        let old = now.addingTimeInterval(-(5 * 3600))
        let output = UsageFormatter.updatedString(from: old, now: now)

        #expect(output.hasSuffix("已更新"))
        #expect(!output.hasPrefix("已更新"))
    }

    @Test
    func `clearing locale provider returns to stable default behavior`() {
        UsageFormatter.clearLocalizationProvider()
        UsageFormatter.clearLocaleProvider()

        let now = Date(timeIntervalSince1970: 1_710_048_000)
        let old = now.addingTimeInterval(-(26 * 3600))
        let baseline = UsageFormatter.updatedString(from: old, now: now)

        UsageFormatter.setLocaleProvider { Locale(identifier: "fr_FR") }
        _ = UsageFormatter.updatedString(from: old, now: now)
        UsageFormatter.clearLocaleProvider()

        let restored = UsageFormatter.updatedString(from: old, now: now)
        #expect(restored == baseline)
    }

    @Test
    func `tomorrow reset description uses localized format`() throws {
        UsageFormatter.setLocalizationProvider { key in
            key == "reset_tomorrow_format" ? "明日 %@" : key
        }
        UsageFormatter.setLocaleProvider { Locale(identifier: "ja_JP") }
        defer {
            UsageFormatter.clearLocalizationProvider()
            UsageFormatter.clearLocaleProvider()
        }

        let calendar = Calendar.current
        let today = calendar.startOfDay(for: Date(timeIntervalSince1970: 1_750_000_000))
        let now = try #require(calendar.date(byAdding: .hour, value: 12, to: today))
        let tomorrow = try #require(calendar.date(byAdding: .day, value: 1, to: today))
        let reset = try #require(calendar.date(byAdding: .minute, value: 10 * 60 + 50, to: tomorrow))

        let output = UsageFormatter.resetDescription(from: reset, now: now)
        #expect(output.hasPrefix("明日 "))
        #expect(!output.contains("tomorrow"))
        #expect(!output.contains("%@"))
    }

    @Test
    func `relative updated recent`() {
        let now = Date()
        let fiveHoursAgo = now.addingTimeInterval(-5 * 3600)
        let text = UsageFormatter.updatedString(from: fiveHoursAgo, now: now)
        #expect(text.hasPrefix("Updated ") || text.hasPrefix("更新"))
        #expect(text.contains("5"))
        #expect(text.lowercased().contains("ago") || text.contains("前"))
    }

    @Test
    func `absolute updated old`() {
        let now = Date()
        let dayAgo = now.addingTimeInterval(-26 * 3600)
        let text = UsageFormatter.updatedString(from: dayAgo, now: now)
        #expect(text.contains("Updated"))
        #expect(!text.contains("ago"))
    }

    @Test
    func `reset countdown minutes`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval(10 * 60 + 1)
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 11m")
    }

    @Test
    func `reset countdown hours and minutes`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval(3 * 3600 + 31 * 60)
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 3h 31m")
    }

    @Test
    func `reset countdown caps days with hours at two units`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval((26 * 3600) + (1 * 60))
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 1d 2h")
    }

    @Test
    func `reset countdown days and exact hours`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval(26 * 3600)
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 1d 2h")
    }

    @Test
    func `reset countdown days and minutes without whole hours`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval((24 * 3600) + (5 * 60))
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 1d 5m")
    }

    @Test
    func `reset countdown exact days`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval(2 * 24 * 3600)
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 2d")
    }

    @Test
    func `reset countdown rounds the last minute into a day`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval((24 * 3600) - 59)
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 1d")
    }

    @Test
    func `reset countdown exact hour`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval(60 * 60)
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "in 1h")
    }

    @Test
    func `reset countdown past date`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval(-10)
        #expect(UsageFormatter.resetCountdownDescription(from: reset, now: now) == "now")
    }

    @Test
    func `reset line uses countdown when resets at is available`() {
        let now = Date(timeIntervalSince1970: 1_000_000)
        let reset = now.addingTimeInterval(10 * 60 + 1)
        let window = RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: reset, resetDescription: "Resets soon")
        let text = UsageFormatter.resetLine(for: window, style: .countdown, now: now)
        #expect(text == "Resets in 11m")
    }

    @Test
    func `reset line falls back to provided description`() {
        let window = RateWindow(
            usedPercent: 0,
            windowMinutes: nil,
            resetsAt: nil,
            resetDescription: "Resets at 23:30 (UTC)")
        let countdown = UsageFormatter.resetLine(for: window, style: .countdown)
        let absolute = UsageFormatter.resetLine(for: window, style: .absolute)
        #expect(countdown == "Resets at 23:30 (UTC)")
        #expect(absolute == "Resets at 23:30 (UTC)")
    }

    @Test
    func `model display name strips trailing dates`() {
        #expect(UsageFormatter.modelDisplayName("claude-opus-4-5-20251101") == "claude-opus-4-5")
        #expect(UsageFormatter.modelDisplayName("gpt-4o-2024-08-06") == "gpt-4o")
        #expect(UsageFormatter.modelDisplayName("Claude Opus 4.5 2025 1101") == "Claude Opus 4.5")
        #expect(UsageFormatter.modelDisplayName("claude-sonnet-4-5") == "claude-sonnet-4-5")
        #expect(UsageFormatter.modelDisplayName("gpt-5.3-codex-spark") == "gpt-5.3-codex-spark")
        #expect(UsageFormatter.modelDisplayName("unknown") == "Unknown model")
    }

    @Test
    func `model display name labels codex auto review role`() {
        #expect(UsageFormatter.modelDisplayName("codex-auto-review") == "Codex Auto Review")
        #expect(UsageFormatter.modelDisplayName("gpt-5.6-sol") == "gpt-5.6-sol")
    }

    @Test
    func `model cost detail uses research preview label`() {
        #expect(
            UsageFormatter.modelCostDetail("gpt-5.3-codex-spark", costUSD: 0, totalTokens: nil) == "Research Preview")
        #expect(UsageFormatter.modelCostDetail("gpt-5.2-codex", costUSD: 0.42, totalTokens: nil) == "$0.42")
    }

    @Test
    func `model cost detail includes token counts when present`() {
        #expect(UsageFormatter.modelCostDetail("gpt-5.2-codex", costUSD: 0.42, totalTokens: 1200) == "$0.42 · 1.2K")
        #expect(
            UsageFormatter.modelCostDetail("gpt-5.3-codex-spark", costUSD: 0, totalTokens: 1500)
                == "Research Preview · 1.5K")
        #expect(UsageFormatter.modelCostDetail("custom-model", costUSD: nil, totalTokens: 987) == "987")
    }

    @Test
    func `token count string formats small values without grouping`() {
        #expect(UsageFormatter.tokenCountString(0) == "0")
        #expect(UsageFormatter.tokenCountString(987) == "987")
        #expect(UsageFormatter.tokenCountString(-42) == "-42")
    }

    @Test
    func `clean plan maps O auth to ollama`() {
        #expect(UsageFormatter.cleanPlanName("oauth") == "Ollama")
    }

    // MARK: - Currency Formatting

    @Test
    func `currency string formats USD correctly`() {
        // Should produce "$54.72" without space after symbol
        let result = UsageFormatter.currencyString(54.72, currencyCode: "USD")
        #expect(result == "$54.72")
        #expect(!result.contains("$ ")) // No space after symbol
    }

    @Test
    func `currency string handles large values`() {
        let result = UsageFormatter.currencyString(1234.56, currencyCode: "USD")
        // For USD, we use direct string formatting with thousand separators
        #expect(result == "$1,234.56")
        #expect(!result.contains("$ ")) // No space after symbol
    }

    @Test
    func `currency string handles very large values`() {
        let result = UsageFormatter.currencyString(1_234_567.89, currencyCode: "USD")
        #expect(result == "$1,234,567.89")
    }

    @Test
    func `currency string handles negative values`() {
        // Negative sign should come before the dollar sign: -$54.72 (not $-54.72)
        let result = UsageFormatter.currencyString(-54.72, currencyCode: "USD")
        #expect(result == "-$54.72")
    }

    @Test
    func `currency string handles negative large values`() {
        let result = UsageFormatter.currencyString(-1234.56, currencyCode: "USD")
        #expect(result == "-$1,234.56")
    }

    @Test
    func `usd string matches currency string`() {
        // usdString should produce identical output to currencyString for USD
        #expect(UsageFormatter.usdString(54.72) == UsageFormatter.currencyString(54.72, currencyCode: "USD"))
        #expect(UsageFormatter.usdString(-1234.56) == UsageFormatter.currencyString(-1234.56, currencyCode: "USD"))
        #expect(UsageFormatter.usdString(0) == UsageFormatter.currencyString(0, currencyCode: "USD"))
    }

    @Test
    func `currency string handles zero`() {
        let result = UsageFormatter.currencyString(0, currencyCode: "USD")
        #expect(result == "$0.00")
    }

    @Test(arguments: [
        (0.0, "$0"),
        (0.50, "$0.50"),
        (12.56, "$13"),
        (1515.0, "$1,515"),
    ])
    func `compact currency keeps cents only below one unit`(value: Double, expected: String) {
        #expect(UsageFormatter.compactCurrencyString(value, currencyCode: "USD") == expected)
    }

    @Test
    func `currency string handles non USD currencies`() {
        // FormatStyle handles all currencies with proper symbols
        let eur = UsageFormatter.currencyString(54.72, currencyCode: "EUR")
        #expect(eur == "€54.72")

        let gbp = UsageFormatter.currencyString(54.72, currencyCode: "GBP")
        #expect(gbp == "£54.72")

        // Negative non-USD
        let negEur = UsageFormatter.currencyString(-1234.56, currencyCode: "EUR")
        #expect(negEur == "-€1,234.56")
    }

    @Test
    func `currency string handles small values`() {
        // Values smaller than 0.01 should round to $0.00
        let tiny = UsageFormatter.currencyString(0.001, currencyCode: "USD")
        #expect(tiny == "$0.00")

        // Values at 0.005 should round to $0.01 (banker's rounding)
        let halfCent = UsageFormatter.currencyString(0.005, currencyCode: "USD")
        #expect(halfCent == "$0.00" || halfCent == "$0.01") // Rounding behavior may vary

        // One cent
        let oneCent = UsageFormatter.currencyString(0.01, currencyCode: "USD")
        #expect(oneCent == "$0.01")
    }

    @Test
    func `currency string handles boundary values`() {
        // Just under 1000 (no comma)
        let under1k = UsageFormatter.currencyString(999.99, currencyCode: "USD")
        #expect(under1k == "$999.99")

        // Exactly 1000 (first comma)
        let exact1k = UsageFormatter.currencyString(1000.00, currencyCode: "USD")
        #expect(exact1k == "$1,000.00")

        // Just over 1000
        let over1k = UsageFormatter.currencyString(1000.01, currencyCode: "USD")
        #expect(over1k == "$1,000.01")
    }

    @Test
    func `credits string formats correctly`() {
        let result = UsageFormatter.creditsString(from: 42.5)
        #expect(result == "42.5 left")
    }

    @Test
    func `byte count string formats binary units`() {
        #expect(UsageFormatter.byteCountString(0) == "0 B")
        #expect(UsageFormatter.byteCountString(512) == "512 B")
        #expect(UsageFormatter.byteCountString(1536) == "1.5 KB")
        #expect(UsageFormatter.byteCountString(10 * 1024) == "10 KB")
        #expect(UsageFormatter.byteCountString(5 * 1024 * 1024) == "5 MB")
        #expect(UsageFormatter.byteCountString(Int64(1536 * 1024 * 1024)) == "1.5 GB")
        #expect(UsageFormatter.byteCountString(.min) == "-8589934592 GB")
    }

    @Test
    func `long byte count string localizes units and handles boundaries`() {
        UsageFormatter.clearLocalizationProvider()
        #expect(UsageFormatter.byteCountStringLong(1024 * 1024) == "1 megabyte")

        UsageFormatter.setLocalizationProvider { "[\($0)]" }
        defer { UsageFormatter.clearLocalizationProvider() }

        #expect(UsageFormatter.byteCountStringLong(1) == "1 [byte_unit_byte]")
        #expect(UsageFormatter.byteCountStringLong(2) == "2 [byte_unit_bytes]")
        #expect(UsageFormatter.byteCountStringLong(1536) == "1.5 [byte_unit_kilobytes]")
        #expect(UsageFormatter.byteCountStringLong(1024 * 1024) == "1 [byte_unit_megabyte]")
        #expect(UsageFormatter.byteCountStringLong(1024 * 1024 + 1) == "1.0 [byte_unit_megabyte]")
        #expect(UsageFormatter.byteCountStringLong(.min) == "-8589934592 [byte_unit_gigabytes]")
    }

    @Test
    func `currency exchange converts rates and formats correctly`() {
        let exchange = CurrencyExchange.shared
        let epsilon = 1e-9
        // USD → USD is identity
        #expect(abs((exchange.convert(usdAmount: 10.0, to: "USD") ?? 0) - 10.0) < epsilon)

        // Cross-currency conversion via USD pivot
        let gbpRate = exchange.rate(for: "GBP") ?? 0.79
        let eurRate = exchange.rate(for: "EUR") ?? 0.92
        #expect(abs((exchange.convert(usdAmount: 10.0, to: "GBP") ?? 0) - 10.0 * gbpRate) < epsilon)
        #expect(abs((exchange.convert(usdAmount: 10.0, to: "EUR") ?? 0) - 10.0 * eurRate) < epsilon)

        // Cross-currency: GBP → EUR
        let gbpToEur = exchange.convert(amount: 10.0, from: "GBP", to: "EUR")
        let expectedGbpToEur = 10.0 / gbpRate * eurRate
        #expect(abs((gbpToEur ?? 0) - expectedGbpToEur) < epsilon)

        // GBP → USD cross-currency
        let gbpToUsd = exchange.convert(amount: 10.0, from: "GBP", to: "USD")
        #expect(abs((gbpToUsd ?? 0) - 10.0 / gbpRate) < epsilon)

        // Formatting
        let gbpFormatted = UsageFormatter.convertedCostString(10.0, targetCurrency: "GBP")
        #expect(gbpFormatted.contains("£"))

        let usdFormatted = UsageFormatter.convertedCostString(10.0, targetCurrency: "USD")
        #expect(usdFormatted == "$10.00")

        // Smart conversion with preferred currency
        let autoResult = UsageFormatter.convertedCostString(10.0, preferredCurrency: "auto", providerCurrency: "GBP")
        #expect(autoResult.contains("£"))

        let explicitCNY = UsageFormatter.convertedCostString(10.0, preferredCurrency: "CNY", providerCurrency: "USD")
        #expect(explicitCNY.contains("¥"))

        let krwRate = exchange.rate(for: "KRW") ?? 1428.90
        #expect(abs((exchange.convert(usdAmount: 10.0, to: "KRW") ?? 0) - 10.0 * krwRate) < epsilon)
        let explicitKRW = UsageFormatter.convertedCostString(10.0, preferredCurrency: "KRW", providerCurrency: "USD")
        #expect(explicitKRW.contains("₩"))
        #expect(!explicitKRW.contains("."))

        let czkRate = exchange.rate(for: "CZK") ?? 21.0
        #expect(abs((exchange.convert(usdAmount: 10.0, to: "CZK") ?? 0) - 10.0 * czkRate) < epsilon)
        let explicitCZK = UsageFormatter.convertedCostString(10.0, preferredCurrency: "CZK", providerCurrency: "USD")
        #expect(explicitCZK.contains("CZK"))
        #expect(explicitCZK.contains("."))

        #expect(exchange.convert(amount: 10.0, from: "CHF", to: "USD") == nil)
        let unavailable = UsageFormatter.convertedCostString(
            10.0,
            preferredCurrency: "USD",
            providerCurrency: "CHF")
        #expect(unavailable.contains("CHF"))
        #expect(!unavailable.contains("$"))
    }

    @Test
    func `live exchange rates require an explicit non USD currency`() {
        #expect(!CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "USD"))
        #expect(!CurrencyExchange.requiresLiveRates(preferredCurrencyCode: " usd "))
        #expect(!CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "auto"))
        #expect(!CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "CHF"))
        #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "GBP"))
        #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: " eur "))
        #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "KRW"))
        #expect(CurrencyExchange.requiresLiveRates(preferredCurrencyCode: "CZK"))
    }

    @Test
    func `usage formatter localization keys exist in en and zh Hans with matching placeholders`() throws {
        let root = URL(fileURLWithPath: #filePath)
            .deletingLastPathComponent()
            .deletingLastPathComponent()
            .deletingLastPathComponent()

        let enURL = root.appendingPathComponent("Sources/CodexBar/Resources/en.lproj/Localizable.strings")
        let zhURL = root.appendingPathComponent("Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings")

        let en = try Self.readStringsTable(at: enURL)
        let zh = try Self.readStringsTable(at: zhURL)

        for key in Self.usageFormatterLocalizationKeys {
            let enValue = try #require(en[key], "Missing en key: \(key)")
            let zhValue = try #require(zh[key], "Missing zh-Hans key: \(key)")
            #expect(
                Self.placeholderTokens(in: enValue) == Self.placeholderTokens(in: zhValue),
                "Placeholder mismatch for key '\(key)': en='\(enValue)' zh='\(zhValue)'")
        }
    }

    private static func readStringsTable(at url: URL) throws -> [String: String] {
        guard let dict = NSDictionary(contentsOf: url) as? [String: String] else {
            throw NSError(
                domain: "UsageFormatterTests",
                code: 1,
                userInfo: [NSLocalizedDescriptionKey: "Failed to parse strings file at \(url.path)"])
        }
        return dict
    }

    private static func placeholderTokens(in value: String) -> [String] {
        guard let regex = try? NSRegularExpression(pattern: "%(?:\\d+\\$)?[@dDuUxXfFeEgGcCsSpaA]") else {
            return []
        }
        let nsRange = NSRange(value.startIndex..<value.endIndex, in: value)
        return regex
            .matches(in: value, options: [], range: nsRange)
            .compactMap { Range($0.range, in: value).map { String(value[$0]) } }
    }
}
