import Foundation
import Testing
@testable import CodexBarCore

struct Issue2037ScannerIntegrationTests {
    /// Locks that `#1164` inherited-totals accounting matches parent-owns-prefix
    /// scanner units for the sanitized ordinary fork family when the parent file
    /// is present in the scan window. Missing-parent / interleaved Ultra shapes
    /// need separate goldens.
    @Test
    func `archived fork family scanner matches parent-owns-prefix oracle`() throws {
        let fixture = try Issue2037FixtureHarness.load(named: "archived-fork-33ce-3869")
        let sanitized = try SanitizedForkFamilyFixture.load(named: "archived-fork-33ce-3869")
        let oracle = sanitized.manifest.oracle
        let prefixLength = try #require(sanitized.manifest.copiedPrefixes.first).length

        let parentEvents = try sanitized.events(named: "parent")
        let childEvents = try sanitized.events(named: "child")
        let expectedScannerUnits = parentEvents.map(\.last.scannerUnits).reduce(0, +)
            + childEvents.dropFirst(prefixLength).map(\.last.scannerUnits).reduce(0, +)
        let naiveScannerUnits = parentEvents.map(\.last.scannerUnits).reduce(0, +)
            + childEvents.map(\.last.scannerUnits).reduce(0, +)

        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }
        try Issue2037FixtureHarness.install(fixture, into: env)

        let since = try env.makeLocalNoon(year: 2030, month: 1, day: 1)
        let until = try env.makeLocalNoon(year: 2030, month: 1, day: 2)
        var options = CostUsageScanner.Options(
            codexSessionsRoot: env.codexSessionsRoot,
            cacheRoot: env.cacheRoot)
        options.forceRescan = true
        options.refreshMinIntervalSeconds = 0

        let report = CostUsageScanner.loadDailyReport(
            provider: .codex,
            since: since,
            until: until,
            now: until,
            options: options)

        var scannedUnits = 0
        var dayKeys: [String] = []
        for day in report.data {
            dayKeys.append(day.date)
            scannedUnits += day.inputTokens ?? 0
            scannedUnits += day.cacheReadTokens ?? 0
            scannedUnits += day.outputTokens ?? 0
        }

        #expect(!report.data.isEmpty)
        #expect(naiveScannerUnits > expectedScannerUnits)
        #expect(oracle.naiveLastTokens > oracle.dedupedLastTokens)
        #expect(
            scannedUnits == expectedScannerUnits,
            """
            scanned=\(scannedUnits) expectedDeduped=\(expectedScannerUnits) \
            naive=\(naiveScannerUnits) days=\(dayKeys)
            """)
    }

    /// Second parent-present golden from a local Sol/Terra-adjacent fork
    /// (`4d90→52bf`). Parent is truncated to the copied prefix so `#1164`
    /// inheritance has a clean resolved-fork baseline.
    ///
    /// Scanner units follow `total_token_usage` deltas (not `sum(last)`): this
    /// corpus has a flat-total row with non-zero `last` at parent ordinal 120.
    @Test
    func `live fork 4d90 family scanner matches parent-owns-prefix oracle`() throws {
        let fixture = try Issue2037FixtureHarness.load(named: "live-fork-4d90-52bf")
        let sanitized = try SanitizedForkFamilyFixture.load(named: "live-fork-4d90-52bf")
        let scannerOracle = try #require(fixture.manifest.scannerOracle)
        let prefixLength = try #require(sanitized.manifest.copiedPrefixes.first).length

        let parentEvents = try sanitized.events(named: "parent")
        let childEvents = try sanitized.events(named: "child")
        let parentTotalUnits = try #require(parentEvents.last).total.scannerUnits
        let prefixEndTotalUnits = try #require(childEvents.dropFirst(prefixLength - 1).first).total.scannerUnits
        let childEndTotalUnits = try #require(childEvents.last).total.scannerUnits
        let expectedScannerUnits = parentTotalUnits + max(0, childEndTotalUnits - prefixEndTotalUnits)

        #expect(parentTotalUnits == prefixEndTotalUnits)
        #expect(expectedScannerUnits == childEndTotalUnits)
        #expect(expectedScannerUnits == scannerOracle.dedupedScannerUnits)
        #expect(scannerOracle.naiveScannerUnits > scannerOracle.dedupedScannerUnits)
        // Corpus anomaly: sum(last) overcounts vs total-delta scanner units.
        #expect(parentEvents.map(\.last.scannerUnits).reduce(0, +) > parentTotalUnits)

        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }
        try Issue2037FixtureHarness.install(fixture, into: env)

        let since = try env.makeLocalNoon(year: 2030, month: 1, day: 1)
        let until = try env.makeLocalNoon(year: 2030, month: 1, day: 2)
        var options = CostUsageScanner.Options(
            codexSessionsRoot: env.codexSessionsRoot,
            cacheRoot: env.cacheRoot)
        options.forceRescan = true
        options.refreshMinIntervalSeconds = 0

        let report = CostUsageScanner.loadDailyReport(
            provider: .codex,
            since: since,
            until: until,
            now: until,
            options: options)

        var scannedUnits = 0
        var dayKeys: [String] = []
        for day in report.data {
            dayKeys.append(day.date)
            scannedUnits += day.inputTokens ?? 0
            scannedUnits += day.cacheReadTokens ?? 0
            scannedUnits += day.outputTokens ?? 0
        }

        #expect(!report.data.isEmpty)
        #expect(
            scannedUnits == scannerOracle.dedupedScannerUnits,
            """
            scanned=\(scannedUnits) expectedDeduped=\(scannerOracle.dedupedScannerUnits) \
            naive=\(scannerOracle.naiveScannerUnits) days=\(dayKeys)
            """)
    }

    @Test
    func `missing parent equal counter siblings fail closed`() throws {
        let env = try CostUsageTestEnvironment()
        defer { env.cleanup() }

        let day = try env.makeLocalNoon(year: 2030, month: 7, day: 1)
        let firstTimestamp = env.isoString(for: day)
        let secondTimestamp = env.isoString(for: day.addingTimeInterval(3600))

        func siblingContents(id: String, model: String, timestamp: String) -> String {
            let metadata = "{\"type\":\"session_meta\",\"timestamp\":\"\(timestamp)\",\"payload\":{"
                + "\"id\":\"\(id)\",\"forked_from_id\":\"missing-parent\","
                + "\"timestamp\":\"\(timestamp)\"}}"
            let context = "{\"type\":\"turn_context\",\"timestamp\":\"\(timestamp)\","
                + "\"payload\":{\"model\":\"\(model)\"}}"
            let first = "{\"type\":\"event_msg\",\"timestamp\":\"\(timestamp)\",\"payload\":{"
                + "\"type\":\"token_count\",\"info\":{"
                + "\"last_token_usage\":{\"input_tokens\":10,\"cached_input_tokens\":0,"
                + "\"output_tokens\":0},\"total_token_usage\":{\"input_tokens\":10,"
                + "\"cached_input_tokens\":0,\"output_tokens\":0}}}}"
            let second = "{\"type\":\"event_msg\",\"timestamp\":\"\(timestamp)\",\"payload\":{"
                + "\"type\":\"token_count\",\"info\":{"
                + "\"last_token_usage\":{\"input_tokens\":5,\"cached_input_tokens\":0,"
                + "\"output_tokens\":0},\"total_token_usage\":{\"input_tokens\":15,"
                + "\"cached_input_tokens\":0,\"output_tokens\":0}}}}"
            return [metadata, context, first, second].joined(separator: "\n") + "\n"
        }

        _ = try env.writeCodexArchivedSessionFile(
            filename: "sibling-a.jsonl",
            contents: siblingContents(id: "sibling-a", model: "fixture-model-a", timestamp: firstTimestamp))
        _ = try env.writeCodexArchivedSessionFile(
            filename: "sibling-b.jsonl",
            contents: siblingContents(id: "sibling-b", model: "fixture-model-b", timestamp: secondTimestamp))

        var options = CostUsageScanner.Options(
            codexSessionsRoot: env.codexSessionsRoot,
            cacheRoot: env.cacheRoot)
        options.forceRescan = true
        options.refreshMinIntervalSeconds = 0
        let report = CostUsageScanner.loadDailyReport(
            provider: .codex,
            since: day,
            until: day,
            now: day,
            options: options)
        let scannedUnits = report.data.reduce(0) { partial, row in
            partial + (row.inputTokens ?? 0) + (row.cacheReadTokens ?? 0) + (row.outputTokens ?? 0)
        }

        // Neither child has a trustworthy inherited baseline. Equal token vectors are not
        // sufficient cross-file identity, so both children stay uncounted until the parent
        // snapshot is available from its file or the persistent token index.
        #expect(scannedUnits == 0)
    }
}
