import Foundation
import Testing

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif

@testable import NucleicProtocol

/// Wire + crypto contracts for the Antimatter runner (docs/ANTIMATTER_RUNNER.md §5–6):
/// intelligence delegation, the sealed credential mesh, and the refresh-lease arbitration.
@Suite struct RunnerMessagesTests {
    private func roundTrip<T: Codable & Equatable>(_ value: T) throws -> T {
        try CBORDecoder().decode(T.self, from: CBOREncoder().encode(value))
    }

    // MARK: Capability flags (additive, decode-defaulted)

    @Test func wireCapabilitiesLegacyShapeDecodesRunnerFlagsFalse() throws {
        // A host that predates the runner omits both keys — they must read as false, exactly
        // the canFetchDiff pattern.
        let legacy = Data(#"{"canModifyToolInput":true,"allowAlwaysScopes":[]}"#.utf8)
        let caps = try JSONDecoder().decode(WireCapabilities.self, from: legacy)
        #expect(!caps.canDelegateIntelligence)
        #expect(!caps.canReceiveSealedCredentials)
    }

    @Test func wireCapabilitiesRunnerFlagsRoundTrip() throws {
        let caps = WireCapabilities(
            canModifyToolInput: false, allowAlwaysScopes: [],
            canDelegateIntelligence: true, canReceiveSealedCredentials: true)
        let back = try roundTrip(caps)
        #expect(back.canDelegateIntelligence)
        #expect(back.canReceiveSealedCredentials)
    }

    @Test func clientCapabilitiesLegacyShapeDecodesProvideIntelligenceFalse() throws {
        let legacy = Data(#"{"mesh":1,"canHost":true}"#.utf8)
        let caps = try JSONDecoder().decode(WireClientCapabilities.self, from: legacy)
        #expect(!caps.canProvideIntelligence)
        #expect(caps.canHost)
    }

    @Test func clientCapabilitiesProvideIntelligenceRoundTrips() throws {
        let caps = WireClientCapabilities(mesh: 1, canProvideIntelligence: true)
        #expect(try roundTrip(caps).canProvideIntelligence)
    }

    // MARK: Welcome.hostKind (a runner declares itself .cloud)

    @Test func welcomeCarriesHostKind() throws {
        let welcome = Welcome(
            grantedScope: .control, host: HostInfo(hostID: "h", hostName: "Cloud Runner"),
            capabilities: WireCapabilities(canModifyToolInput: false, allowAlwaysScopes: []),
            hostKind: PeerKind.cloud.rawValue)
        let back = try roundTrip(welcome)
        #expect(back.hostKind == "cloud")
        #expect(back.kind == .cloud)
    }

    @Test func welcomeWithoutHostKindDefaultsToMac() throws {
        // A host that predates the field (omits hostKind) must pin as .mac, not throw.
        let json =
            #"{"protocolVersion":1,"grantedScope":"control","host":{"hostID":"h","hostName":"Mac"},"#
            + #""capabilities":{"canModifyToolInput":true,"allowAlwaysScopes":[]}}"#
        let welcome = try JSONDecoder().decode(Welcome.self, from: Data(json.utf8))
        #expect(welcome.hostKind == nil)
        #expect(welcome.kind == .mac)
    }

    // MARK: Envelope wiring

    @Test func intelligenceRequestAndResultRoundTripThroughEnvelope() throws {
        let request = WireIntelligenceRequest(
            id: "req-1", kind: .classifyTurn, inputs: ["done — merged the branch"],
            context: nil, deadlineSeconds: 8)
        let host = HostMsg.intelligenceRequest(request)
        #expect(try roundTrip(host) == host)

        let result = WireIntelligenceResult(id: "req-1", outputs: ["completed"])
        let client = ClientMsg.intelligenceResult(result)
        #expect(try roundTrip(client) == client)
    }

    @Test func unknownIntelligenceKindDecodesToItsRawValue() throws {
        // Raw-string forward compat: a future kind never throws on an old device.
        let request = WireIntelligenceRequest(
            id: "r", kind: IntelligenceRequestKind(rawValue: "someFutureKind"), inputs: [])
        #expect(try roundTrip(request).kind.rawValue == "someFutureKind")
    }

    @Test func credentialVerbsRoundTripThroughEnvelope() throws {
        let need = WireCredentialNeed(
            kinds: [.claudeOAuth, .githubToken], sealingPublicKey: Data(repeating: 7, count: 32))
        #expect(try roundTrip(HostMsg.credentialNeeded(need)) == .credentialNeeded(need))

        let manifest = CredentialManifest(
            records: [
                CredentialRecordDescriptor(
                    kind: .codexAuth, updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
                    provenanceDeviceID: "dev-a")
            ],
            leases: [
                CredentialRefreshLease(
                    kind: .claudeOAuth, holderDeviceID: "dev-a",
                    acquiredAt: Date(timeIntervalSince1970: 1_700_000_000),
                    expiresAt: Date(timeIntervalSince1970: 1_700_003_600))
            ])
        #expect(try roundTrip(ClientMsg.credentialManifest(manifest)) == .credentialManifest(manifest))

        let record = SealedCredentialRecord(
            kind: .githubToken, updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
            box: SealedCredentialBox(
                ephemeralPublicKey: Data(repeating: 1, count: 32),
                ciphertext: Data(repeating: 2, count: 48)))
        let envelope = SealedCredentialEnvelope(records: [record])
        #expect(try roundTrip(ClientMsg.credentialProvision(envelope)) == .credentialProvision(envelope))
        #expect(try roundTrip(HostMsg.credentialUpdate(envelope)) == .credentialUpdate(envelope))
    }

    // MARK: Remote project creation (CLOUD_RUNTIME §4.3)

    @Test func wireCapabilitiesLegacyShapeDecodesCreateProjectsFalse() throws {
        // A host that predates the verb omits the key — it must read as false so a client
        // never sends the tag at a host that would throw on it.
        let legacy = Data(#"{"canModifyToolInput":true,"allowAlwaysScopes":[]}"#.utf8)
        let caps = try JSONDecoder().decode(WireCapabilities.self, from: legacy)
        #expect(!caps.canCreateProjects)
    }

    @Test func wireCapabilitiesCreateProjectsRoundTrips() throws {
        let caps = WireCapabilities(
            canModifyToolInput: false, allowAlwaysScopes: [], canCreateProjects: true)
        #expect(try roundTrip(caps).canCreateProjects)
    }

    @Test func createProjectRoundTripsThroughEnvelope() throws {
        let request = WireCreateProjectRequest(
            gitURL: "git@github.com:me/proj.git", branch: "main", name: "Proj")
        #expect(try roundTrip(ClientMsg.createProject(request)) == .createProject(request))
    }

    @Test func createProjectRequestWithoutOptionalsDecodes() throws {
        // A minimal request (URL only) from a build that never sets branch/name/requestID must
        // decode, not throw — the decode-defaulted-optionals discipline.
        let minimal = Data(#"{"gitURL":"https://github.com/me/proj"}"#.utf8)
        let request = try JSONDecoder().decode(WireCreateProjectRequest.self, from: minimal)
        #expect(request.gitURL == "https://github.com/me/proj")
        #expect(request.branch == nil)
        #expect(request.name == nil)
        #expect(request.requestID == nil)
    }

    @Test func projectCreatedRoundTripsThroughEnvelope() throws {
        let success = WireProjectCreated(
            requestID: "req-1", projectID: ProjectID(rawValue: UUID().uuidString), name: "Proj")
        #expect(try roundTrip(HostMsg.projectCreated(success)) == .projectCreated(success))

        let failure = WireProjectCreated(requestID: "req-2", error: "clone failed: no such repo")
        let back = try roundTrip(HostMsg.projectCreated(failure))
        #expect(back == .projectCreated(failure))
        if case .projectCreated(let outcome) = back {
            #expect(outcome.projectID == nil)
            #expect(outcome.error == "clone failed: no such repo")
        }
    }

    @Test func manifestWithoutLeasesDecodes() throws {
        // A manifest from a build that predates leases reads as lease-free, not a throw.
        let legacy = Data(#"{"records":[]}"#.utf8)
        let manifest = try JSONDecoder().decode(CredentialManifest.self, from: legacy)
        #expect(manifest.leases.isEmpty)
    }

    // MARK: Sealed boxes

    @Test func sealedBoxRoundTrips() throws {
        let recipient = Curve25519.KeyAgreement.PrivateKey()
        let secret = Data("{\"claudeAiOauth\":{\"accessToken\":\"abc\"}}".utf8)
        let aad = Data("claude-oauth".utf8)
        let box = try SealedCredentialBox.seal(
            secret, to: recipient.publicKey.rawRepresentation, additionalData: aad)
        let opened = try box.open(with: recipient, additionalData: aad)
        #expect(opened == secret)
    }

    @Test func sealedBoxRejectsWrongRecipient() throws {
        let recipient = Curve25519.KeyAgreement.PrivateKey()
        let interloper = Curve25519.KeyAgreement.PrivateKey()
        let box = try SealedCredentialBox.seal(
            Data("secret".utf8), to: recipient.publicKey.rawRepresentation)
        #expect(throws: SealedCredentialBox.SealedCredentialError.decryptFailed) {
            _ = try box.open(with: interloper)
        }
    }

    @Test func sealedBoxBindsAdditionalData() throws {
        // A box sealed as one credential kind must not open as another (replay-as-github-token).
        let recipient = Curve25519.KeyAgreement.PrivateKey()
        let box = try SealedCredentialBox.seal(
            Data("secret".utf8), to: recipient.publicKey.rawRepresentation,
            additionalData: SealedCredentialRecord(
                kind: .claudeOAuth, updatedAt: Date(), box: .init(ephemeralPublicKey: Data(), ciphertext: Data())
            ).additionalData)
        let wrongKindAAD = Data(CredentialKind.githubToken.rawValue.utf8)
        #expect(throws: SealedCredentialBox.SealedCredentialError.decryptFailed) {
            _ = try box.open(with: recipient, additionalData: wrongKindAAD)
        }
    }

    @Test func sealedBoxRejectsTamperedCiphertext() throws {
        let recipient = Curve25519.KeyAgreement.PrivateKey()
        let box = try SealedCredentialBox.seal(
            Data("secret".utf8), to: recipient.publicKey.rawRepresentation)
        var flipped = box.ciphertext
        flipped[flipped.count - 1] ^= 0x01
        let tampered = SealedCredentialBox(
            ephemeralPublicKey: box.ephemeralPublicKey, ciphertext: flipped)
        #expect(throws: SealedCredentialBox.SealedCredentialError.decryptFailed) {
            _ = try tampered.open(with: recipient)
        }
    }

    @Test func sealedBoxRejectsMalformedRecipientKey() {
        #expect(throws: SealedCredentialBox.SealedCredentialError.malformedKey) {
            _ = try SealedCredentialBox.seal(Data("x".utf8), to: Data(repeating: 0, count: 16))
        }
    }

    // MARK: Refresh-lease arbitration

    private func lease(
        holder: String, acquired: TimeInterval, expires: TimeInterval
    ) -> CredentialRefreshLease {
        CredentialRefreshLease(
            kind: .claudeOAuth, holderDeviceID: holder,
            acquiredAt: Date(timeIntervalSince1970: acquired),
            expiresAt: Date(timeIntervalSince1970: expires))
    }

    @Test func liveLeaseBeatsExpiredLease() {
        let now = Date(timeIntervalSince1970: 1_000)
        let dead = lease(holder: "a", acquired: 0, expires: 500)
        let live = lease(holder: "b", acquired: 600, expires: 2_000)
        #expect(CredentialRefreshLease.merged(dead, live, at: now)?.holderDeviceID == "b")
        #expect(CredentialRefreshLease.merged(live, dead, at: now)?.holderDeviceID == "b")
    }

    @Test func earlierClaimKeepsTheLease() {
        // First writer wins — a newer claim must NOT steal a live lease, or two devices would
        // both refresh (and revoke each other's rotated tokens).
        let now = Date(timeIntervalSince1970: 1_000)
        let first = lease(holder: "a", acquired: 100, expires: 2_000)
        let second = lease(holder: "b", acquired: 200, expires: 3_000)
        #expect(CredentialRefreshLease.merged(first, second, at: now)?.holderDeviceID == "a")
        #expect(CredentialRefreshLease.merged(second, first, at: now)?.holderDeviceID == "a")
    }

    @Test func exactTieBreaksByDeviceID() {
        let now = Date(timeIntervalSince1970: 1_000)
        let a = lease(holder: "aaa", acquired: 100, expires: 2_000)
        let b = lease(holder: "bbb", acquired: 100, expires: 2_000)
        #expect(CredentialRefreshLease.merged(a, b, at: now)?.holderDeviceID == "aaa")
        #expect(CredentialRefreshLease.merged(b, a, at: now)?.holderDeviceID == "aaa")
    }

    @Test func expiredLeasesClearToNil() {
        let now = Date(timeIntervalSince1970: 10_000)
        let a = lease(holder: "a", acquired: 0, expires: 500)
        let b = lease(holder: "b", acquired: 100, expires: 900)
        #expect(CredentialRefreshLease.merged(a, b, at: now) == nil)
        #expect(CredentialRefreshLease.merged(a, nil, at: now) == nil)
        #expect(CredentialRefreshLease.merged(nil, nil, at: now) == nil)
    }
}
