import Foundation
import Testing

@testable import NucleicProtocol

/// CARBON_SHARDING §8.1: the Carbon wire verbs round-trip through the envelope codec, and
/// every new field/bit decode-defaults so an old peer's frames still parse (legacy tolerance —
/// the WireMessageTests discipline).
@Suite("Carbon wire messages")
struct CarbonWireTests {
    private func roundTrip<T: Codable & Equatable>(_ value: T) throws -> T {
        try CBORDecoder().decode(T.self, from: CBOREncoder().encode(value))
    }

    private let projectUUID = UUID(uuidString: "3D4E5F60-7182-4934-A5B6-C7D8E9F00112")!

    private var head: CarbonHead {
        CarbonHead(
            key: CarbonStreamKey(projectUUID: projectUUID, stream: .worktreeSnapshot, sessionID: "s1"),
            manifestID: String(repeating: "a", count: 64), ownerGeneration: 3, snapshotSeq: 17,
            keyMissing: true)
    }

    @Test func clientVerbsRoundTrip() throws {
        let cases: [ClientMsg] = [
            .fetchCarbonHeads(CarbonHeadsFetch(requestID: "r1", projectUUID: projectUUID)),
            .fetchCarbonHeads(CarbonHeadsFetch(requestID: "r2", projectUUID: nil)),
            .fetchCarbonManifests(CarbonBlobsFetch(
                requestID: "r3", projectUUID: projectUUID, ids: ["m1", "m2"])),
            .fetchCarbonShards(CarbonBlobsFetch(
                requestID: "r4", projectUUID: projectUUID, ids: ["s1"])),
            .carbonShardAck(CarbonShardAck(requestID: "r4", ids: ["s1", "s2"])),
        ]
        for original in cases {
            #expect(try roundTrip(original) == original)
        }
    }

    @Test func hostRepliesRoundTrip() throws {
        let blob = Data((0..<64).map { UInt8($0) })
        let cases: [HostMsg] = [
            .carbonHeads(CarbonHeadsReply(requestID: "r1", heads: [head])),
            .carbonHeadAdvanced(head),
            .carbonManifestData(CarbonBlobData(
                requestID: "r3", projectUUID: projectUUID, id: "m1", blob: blob)),
            .carbonManifestUnavailable(CarbonBlobUnavailable(
                requestID: "r3", id: "m2", reason: .notHeld)),
            .carbonShardData(CarbonBlobData(
                requestID: "r4", projectUUID: projectUUID, id: "s1", blob: blob)),
            .carbonShardUnavailable(CarbonBlobUnavailable(
                requestID: "r4", id: nil, reason: .unsupportedFormat)),
        ]
        for original in cases {
            #expect(try roundTrip(original) == original)
        }
    }

    @Test func lockVerbsRoundTrip() throws {
        // CARBON_SHARDING §17.5: the lock verbs ride the same envelope codec.
        let clientCases: [ClientMsg] = [
            .carbonLockAcquire(CarbonLockRequest(
                requestID: "l1", projectUUID: projectUUID, domainRef: "dev",
                sessionID: "sess-1", paths: ["src/a.swift", "src/b.swift"])),
            .carbonLockRelease(CarbonLockRequest(
                requestID: "l2", projectUUID: projectUUID, domainRef: "dev",
                sessionID: "sess-1", paths: nil, requiredHeads: [head])),
        ]
        for original in clientCases {
            #expect(try roundTrip(original) == original)
        }
        let hostCases: [HostMsg] = [
            .carbonLockDecision(CarbonLockDecisionMsg(
                requestID: "l1", projectUUID: projectUUID, domainRef: "dev",
                sessionID: "sess-1", outcome: .queued)),
            .carbonLockDecision(CarbonLockDecisionMsg(
                requestID: "l1", projectUUID: projectUUID, domainRef: "dev",
                sessionID: "sess-1", outcome: .granted)),
            .carbonLockLedgerAdvanced(head),
        ]
        for original in hostCases {
            #expect(try roundTrip(original) == original)
        }
    }

    @Test func headKeyMissingDecodeDefaultsFalse() throws {
        // An old peer's head frame omits `keyMissing` — it must decode as false, not throw.
        let json = """
            {"key":{"projectUUID":"\(projectUUID.uuidString)","stream":"history"},
             "manifestID":"abc","ownerGeneration":1,"snapshotSeq":2}
            """
        let decoded = try JSONDecoder().decode(CarbonHead.self, from: Data(json.utf8))
        #expect(decoded.keyMissing == false)
        #expect(decoded.key.sessionID == nil)
    }

    @Test func capabilityBitsDecodeDefaultFalse() throws {
        // A pre-Carbon host's capabilities omit `canServeCarbon`.
        let hostJSON = """
            {"canModifyToolInput":true,"allowAlwaysScopes":[]}
            """
        let caps = try JSONDecoder().decode(WireCapabilities.self, from: Data(hostJSON.utf8))
        #expect(caps.canServeCarbon == false)

        // A pre-Carbon client's hello omits `canMirrorCarbon`/`alwaysOn`.
        let clientJSON = """
            {"mesh":1,"canHost":true}
            """
        let clientCaps = try JSONDecoder().decode(
            WireClientCapabilities.self, from: Data(clientJSON.utf8))
        #expect(clientCaps.canMirrorCarbon == false)
        #expect(clientCaps.alwaysOn == false)
    }

    @Test func newBitsSurviveRoundTrip() throws {
        let caps = WireCapabilities(
            canModifyToolInput: false, allowAlwaysScopes: [], canServeCarbon: true)
        #expect(try roundTrip(caps).canServeCarbon == true)
        let clientCaps = WireClientCapabilities(canMirrorCarbon: true, alwaysOn: true)
        let decoded = try roundTrip(clientCaps)
        #expect(decoded.canMirrorCarbon == true)
        #expect(decoded.alwaysOn == true)
    }
}
