import BitFoundation
import Foundation
import Testing
@testable import bitchat

@Suite("BLE peer registry tests")
struct BLEPeerRegistryTests {
    @Test("upserted announces track new, reconnect, and rename transitions")
    func upsertVerifiedAnnounceTracksTransitions() throws {
        var registry = BLEPeerRegistry()
        let peerID = PeerID(str: "1122334455667788")
        let firstSeen = Date(timeIntervalSince1970: 100)

        let firstResult = registry.upsertVerifiedAnnounce(
            peerID: peerID,
            nickname: "alice",
            noisePublicKey: Data([1, 2, 3]),
            signingPublicKey: Data([4, 5, 6]),
            isConnected: true,
            now: firstSeen
        )
        let first = try #require(firstResult)

        #expect(first.isNewPeer)
        #expect(!first.wasDisconnected)
        #expect(first.previousNickname == nil)
        #expect(registry.connectedPeerIDs == [peerID])
        #expect(registry.nickname(for: peerID, connectedOnly: true) == "alice")

        registry.markDisconnected(peerID)
        let reconnectResult = registry.upsertVerifiedAnnounce(
            peerID: peerID,
            nickname: "alice-renamed",
            noisePublicKey: Data([1, 2, 3]),
            signingPublicKey: Data([4, 5, 6]),
            isConnected: true,
            now: firstSeen.addingTimeInterval(1)
        )
        let reconnect = try #require(reconnectResult)

        #expect(!reconnect.isNewPeer)
        #expect(reconnect.wasDisconnected)
        #expect(reconnect.previousNickname == "alice")
        #expect(registry.info(for: peerID)?.nickname == "alice-renamed")
    }

    @Test("pinned signing key cannot be silently replaced by a later announce")
    func upsertVerifiedAnnounceRefusesToReplacePinnedSigningKey() throws {
        var registry = BLEPeerRegistry()
        let peerID = PeerID(str: "1122334455667788")
        let noiseKey = Data(repeating: 0x11, count: 32)
        let victimSigningKey = Data(repeating: 0x42, count: 32)
        let attackerSigningKey = Data(repeating: 0x66, count: 32)
        let firstSeen = Date(timeIntervalSince1970: 100)

        let pinResult = registry.upsertVerifiedAnnounce(
            peerID: peerID,
            nickname: "victim",
            noisePublicKey: noiseKey,
            signingPublicKey: victimSigningKey,
            isConnected: true,
            now: firstSeen
        )
        #expect(pinResult != nil)

        // Attacker replays the victim's noiseKey/peerID with their own
        // signing key and nickname; the upsert must be refused wholesale.
        let attack = registry.upsertVerifiedAnnounce(
            peerID: peerID,
            nickname: "attacker",
            noisePublicKey: noiseKey,
            signingPublicKey: attackerSigningKey,
            isConnected: true,
            now: firstSeen.addingTimeInterval(1)
        )

        #expect(attack == nil)
        let info = try #require(registry.info(for: peerID))
        #expect(info.nickname == "victim")
        #expect(info.signingPublicKey == victimSigningKey)

        // A legitimate re-announce with the pinned key is still accepted.
        let legit = registry.upsertVerifiedAnnounce(
            peerID: peerID,
            nickname: "victim-renamed",
            noisePublicKey: noiseKey,
            signingPublicKey: victimSigningKey,
            isConnected: true,
            now: firstSeen.addingTimeInterval(2)
        )
        #expect(legit != nil)
        #expect(registry.info(for: peerID)?.nickname == "victim-renamed")
    }

    @Test("announce without a signing key keeps the pinned key")
    func upsertVerifiedAnnounceKeepsPinnedSigningKeyWhenAnnounceOmitsIt() throws {
        var registry = BLEPeerRegistry()
        let peerID = PeerID(str: "1122334455667788")
        let noiseKey = Data(repeating: 0x11, count: 32)
        let signingKey = Data(repeating: 0x42, count: 32)
        let firstSeen = Date(timeIntervalSince1970: 100)

        let initialResult = registry.upsertVerifiedAnnounce(
            peerID: peerID,
            nickname: "alice",
            noisePublicKey: noiseKey,
            signingPublicKey: signingKey,
            isConnected: true,
            now: firstSeen
        )
        #expect(initialResult != nil)

        let update = registry.upsertVerifiedAnnounce(
            peerID: peerID,
            nickname: "alice",
            noisePublicKey: noiseKey,
            signingPublicKey: nil,
            isConnected: true,
            now: firstSeen.addingTimeInterval(1)
        )

        #expect(update != nil)
        #expect(registry.info(for: peerID)?.signingPublicKey == signingKey)
    }

    @Test("registry preserves absent versus explicit empty capabilities")
    func capabilitiesPresenceIsPreserved() {
        var registry = BLEPeerRegistry()
        let oldPeer = PeerID(str: "1122334455667788")
        let modernPeer = PeerID(str: "8877665544332211")

        _ = registry.upsertVerifiedAnnounce(
            peerID: oldPeer,
            nickname: "old",
            noisePublicKey: Data(repeating: 0x11, count: 32),
            signingPublicKey: Data(repeating: 0x12, count: 32),
            isConnected: true,
            now: Date(),
            capabilities: nil
        )
        _ = registry.upsertVerifiedAnnounce(
            peerID: modernPeer,
            nickname: "modern",
            noisePublicKey: Data(repeating: 0x21, count: 32),
            signingPublicKey: Data(repeating: 0x22, count: 32),
            isConnected: true,
            now: Date(),
            capabilities: []
        )

        #expect(registry.capabilities(for: oldPeer).isEmpty)
        #expect(!registry.capabilitiesWereExplicitlyAdvertised(for: oldPeer))
        #expect(registry.capabilities(for: modernPeer).isEmpty)
        #expect(registry.capabilitiesWereExplicitlyAdvertised(for: modernPeer))
    }

    @Test("reachability keeps recent verified offline peers only when mesh is attached")
    func reachabilityRequiresMeshAttachmentForOfflinePeers() {
        let offlinePeer = PeerID(str: "1122334455667788")
        let connectedPeer = PeerID(str: "8877665544332211")
        let now = Date()

        var isolatedRegistry = BLEPeerRegistry()
        isolatedRegistry.upsert(BLEPeerInfo(
            peerID: offlinePeer,
            nickname: "offline",
            isConnected: false,
            noisePublicKey: nil,
            signingPublicKey: nil,
            isVerifiedNickname: true,
            lastSeen: now
        ))

        #expect(!isolatedRegistry.isReachable(offlinePeer, now: now))

        var attachedRegistry = isolatedRegistry
        attachedRegistry.upsert(BLEPeerInfo(
            peerID: connectedPeer,
            nickname: "connected",
            isConnected: true,
            noisePublicKey: nil,
            signingPublicKey: nil,
            isVerifiedNickname: true,
            lastSeen: now
        ))

        #expect(attachedRegistry.isReachable(offlinePeer, now: now))
    }

    @Test("connectivity reconciliation disconnects inactive peers and prunes expired offline peers")
    func reconcileConnectivityUpdatesAndPrunesPeerState() {
        var registry = BLEPeerRegistry()
        let inactiveConnectedPeer = PeerID(str: "1122334455667788")
        let expiredOfflinePeer = PeerID(str: "8877665544332211")
        let now = Date()

        registry.upsert(BLEPeerInfo(
            peerID: inactiveConnectedPeer,
            nickname: "inactive",
            isConnected: true,
            noisePublicKey: nil,
            signingPublicKey: nil,
            isVerifiedNickname: true,
            lastSeen: now.addingTimeInterval(-TransportConfig.blePeerInactivityTimeoutSeconds - 1)
        ))
        registry.upsert(BLEPeerInfo(
            peerID: expiredOfflinePeer,
            nickname: "expired",
            isConnected: false,
            noisePublicKey: nil,
            signingPublicKey: nil,
            isVerifiedNickname: true,
            lastSeen: .distantPast
        ))

        let changes = registry.reconcileConnectivity(now: now, linkStates: [:])

        #expect(changes.disconnectedPeerIDs == [inactiveConnectedPeer])
        #expect(changes.removedPeers.map(\.peerID) == [expiredOfflinePeer])
        #expect(registry.info(for: inactiveConnectedPeer)?.isConnected == false)
        #expect(registry.info(for: expiredOfflinePeer) == nil)
    }
}
