import Foundation
import Testing

@testable import NucleicProtocol

/// Wire compatibility for the transport fields added to the pairing QR (SYNC §4.2): new
/// fields must round-trip, old payloads must decode as LAN, and old decoders must ignore
/// the new keys — both directions of the version skew.
@Suite struct PairingPayloadTransportTests {
    private let key = Data(repeating: 7, count: 32)
    private let secret = Data(repeating: 9, count: 32)

    @Test func tailnetHintsRoundTripThroughQR() throws {
        let payload = PairingPayload(
            hostName: "Mac", hostStaticKey: key, pairingSecret: secret,
            transport: SyncTransportHint.tailnet.rawValue,
            tailnetHost: "100.64.0.7", tailnetPort: 43_753)
        let decoded = try PairingPayload(qrString: payload.qrString())
        #expect(decoded == payload)
        #expect(decoded.transportHint == .tailnet)
        #expect(decoded.tailnetHost == "100.64.0.7")
        #expect(decoded.tailnetPort == 43_753)
    }

    /// The exact wire shape hosts encoded before transports existed.
    private struct LegacyPayload: Codable {
        var protocolVersion = SyncProtocol.version
        let hostName: String
        let hostStaticKey: Data
        let pairingSecret: Data
        var lanHost: String?
        var lanPort: UInt16?
    }

    @Test func payloadFromPreTransportHostDecodesAsLAN() throws {
        let legacy = LegacyPayload(
            hostName: "Mac", hostStaticKey: key, pairingSecret: secret,
            lanHost: "192.168.1.5", lanPort: 4_242)
        let decoded = try CBORDecoder().decode(PairingPayload.self, from: CBOREncoder().encode(legacy))
        #expect(decoded.transport == nil)
        #expect(decoded.transportHint == .lan)
        #expect(decoded.lanHost == "192.168.1.5")
        #expect(decoded.tailnetHost == nil)
    }

    @Test func preTransportDecoderIgnoresNewFields() throws {
        let payload = PairingPayload(
            hostName: "Mac", hostStaticKey: key, pairingSecret: secret,
            transport: SyncTransportHint.tailnet.rawValue,
            tailnetHost: "100.64.0.7", tailnetPort: 43_753)
        let decoded = try CBORDecoder().decode(LegacyPayload.self, from: CBOREncoder().encode(payload))
        #expect(decoded.hostName == "Mac")
        #expect(decoded.pairingSecret == secret)
    }

    /// A transport this build doesn't know yields nil (callers say "update the app"), not a
    /// silent LAN fallback that would dial the wrong way.
    @Test func unknownFutureTransportIsNilNotLAN() {
        let payload = PairingPayload(
            hostName: "Mac", hostStaticKey: key, pairingSecret: secret, transport: "quic")
        #expect(payload.transportHint == nil)
    }

    // MARK: Relay bootstrap fields (mesh P2)

    @Test func relayFieldsRoundTripThroughQR() throws {
        let payload = PairingPayload(
            hostName: "Mac", hostStaticKey: key, pairingSecret: secret,
            transport: SyncTransportHint.relay.rawValue,
            relayRoomID: "abc123", relayMembershipToken: "m.token", relayURL: "https://relay.example")
        let decoded = try PairingPayload(qrString: payload.qrString())
        #expect(decoded == payload)
        #expect(decoded.relayRoomID == "abc123")
        #expect(decoded.relayMembershipToken == "m.token")
        #expect(decoded.relayURL == "https://relay.example")
    }

    @Test func preRelayDecoderIgnoresRelayFields() throws {
        // A payload carrying relay fields must still decode on a build that predates them.
        let payload = PairingPayload(
            hostName: "Mac", hostStaticKey: key, pairingSecret: secret,
            relayRoomID: "abc123", relayMembershipToken: "m.token", relayURL: "https://relay.example")
        let decoded = try CBORDecoder().decode(LegacyPayload.self, from: CBOREncoder().encode(payload))
        #expect(decoded.hostName == "Mac")
        #expect(decoded.pairingSecret == secret)
    }

    @Test func payloadWithoutRelayFieldsDecodesThemAsNil() throws {
        let legacy = LegacyPayload(hostName: "Mac", hostStaticKey: key, pairingSecret: secret)
        let decoded = try CBORDecoder().decode(PairingPayload.self, from: CBOREncoder().encode(legacy))
        #expect(decoded.relayRoomID == nil)
        #expect(decoded.relayMembershipToken == nil)
        #expect(decoded.relayURL == nil)
    }
}
