import Foundation
import Testing

@testable import NucleicCore

/// Decodes the real captured native streams committed under fixtures/claude/
/// (recorded by the M0 spike on 2026-06-12, claude 2.1.167). Re-recording on a
/// CLI upgrade and diffing against these is the drift report (OBSERVABILITY B.9);
/// a new native shape failing `knownRawTypes` here is the same signal
/// `unknown_events_total` raises in the app.
@Suite struct RealCaptureDriftTests {
    static let pinnedVersion = "2.1.167"

    /// Native shapes claude 2.1.167 emits that the protocol intentionally
    /// passes through as .raw (not yet modeled).
    static let knownRawTypes: Set<String> = [
        "system/status",
        "system/post_turn_summary",
        "user",  // echo of our own stdin user message
    ]

    static var fixturesRoot: URL {
        // Tests/NucleicCoreTests/RealCaptureDriftTests.swift → repo root → fixtures/
        URL(fileURLWithPath: #filePath)
            .deletingLastPathComponent()  // NucleicCoreTests
            .deletingLastPathComponent()  // Tests
            .deletingLastPathComponent()  // repo root
            .appendingPathComponent("fixtures/claude/\(pinnedVersion)")
    }

    func decodeCapture(_ name: String) throws -> [ClaudeStreamDecoder.Decoded] {
        let url = Self.fixturesRoot.appendingPathComponent("\(name).ndjson")
        let data = try Data(contentsOf: url)
        let decoder = ClaudeStreamDecoder()
        return data.split(separator: 0x0A, omittingEmptySubsequences: true)
            .flatMap { decoder.decode(line: Data($0)) }
    }

    func assertNoUnknownDrift(_ decoded: [ClaudeStreamDecoder.Decoded]) {
        for event in decoded {
            if case .raw = event.kind {
                let type = event.nativeType ?? "<non-json>"
                #expect(
                    Self.knownRawTypes.contains(type),
                    "unmodeled native shape (CLI drift?): \(type)")
            }
        }
    }

    @Test func simpleTurnWithPartialStreaming() throws {
        let decoded = try decodeCapture("01-simple-turn-partial")
        assertNoUnknownDrift(decoded)
        let kinds = decoded.map(\.kind)

        guard case .sessionStarted(let started) = kinds.first else {
            Issue.record("expected sessionStarted first")
            return
        }
        #expect(!started.backendSessionID.isEmpty)
        #expect(!started.toolNames.isEmpty)

        let partials = kinds.filter {
            if case .assistantText(let chunk) = $0 { return chunk.isPartial }
            return false
        }
        #expect(!partials.isEmpty, "expected stream_event text deltas with --partial")

        let fullTexts = kinds.compactMap { kind -> TextChunk? in
            if case .assistantText(let chunk) = kind, !chunk.isPartial { return chunk }
            return nil
        }
        #expect(fullTexts.map(\.text).contains("M0 contract proven"))

        // A successful `result` decodes to a turn boundary; the terminal runFinished
        // is the adapter's, synthesized on process exit (not present in the raw capture).
        guard case .turnCompleted(let turn) = kinds.last else {
            Issue.record("expected turnCompleted last, got \(String(describing: kinds.last))")
            return
        }
        #expect(turn.stopReason == "end_turn")
        #expect(turn.usage?.costUSD != nil)
    }

    @Test func rateLimitEventIsModeled() throws {
        // Every M0 capture carries a rate_limit_event; it must decode to a populated
        // .rateLimit (M0 finding 6), not pass through as .raw.
        let decoded = try decodeCapture("01-simple-turn-partial")
        let rateLimits = decoded.compactMap { d -> RateLimit? in
            if case .rateLimit(let rl) = d.kind { return rl }
            return nil
        }
        #expect(!rateLimits.isEmpty, "expected a modeled rate_limit_event")
        #expect(rateLimits.first?.rateLimitType == "five_hour")
        #expect(rateLimits.first?.resetsAt != nil)
        // And none of them leaked through as raw.
        assertNoUnknownDrift(decoded)
    }

    @Test func gatedWriteAllowed() throws {
        let decoded = try decodeCapture("02-gated-write-allow")
        assertNoUnknownDrift(decoded)
        let kinds = decoded.map(\.kind)

        let write = kinds.compactMap { kind -> ToolCall? in
            if case .toolCallCompleted(let call) = kind, call.name == "Write" { return call }
            return nil
        }.first
        #expect(write?.input["file_path"]?.stringValue == "/private/tmp/nucleic-m0-demo/approved.txt")

        let results = kinds.compactMap { kind -> ToolResult? in
            if case .toolResult(let result) = kind { return result }
            return nil
        }
        #expect(results.count == 1)
        #expect(results[0].isError == false)

        // Inferred fileChange follows the successful result.
        let changes = kinds.compactMap { kind -> FileChange? in
            if case .fileChange(let change) = kind { return change }
            return nil
        }
        #expect(changes.map(\.path) == ["/private/tmp/nucleic-m0-demo/approved.txt"])
    }

    @Test func gatedWriteDenied() throws {
        let decoded = try decodeCapture("03-gated-write-deny")
        assertNoUnknownDrift(decoded)
        let kinds = decoded.map(\.kind)

        let results = kinds.compactMap { kind -> ToolResult? in
            if case .toolResult(let result) = kind { return result }
            return nil
        }
        #expect(results.count == 1)
        #expect(results[0].isError == true)
        #expect(results[0].content.stringValue?.contains("Denied by spike policy") == true)

        // A denied Write must not synthesize a fileChange.
        let hasChange = kinds.contains {
            if case .fileChange = $0 { return true }
            return false
        }
        #expect(!hasChange)

        // The run completed successfully despite the denial → a turn boundary.
        guard case .turnCompleted = kinds.last else {
            Issue.record("expected turnCompleted last, got \(String(describing: kinds.last))")
            return
        }
    }
}
