import Foundation
import Testing
import NucleicProtocol

@testable import NucleicCore

/// The Covalence Mesh Viewer's topology model: which hosts are nodes, which stale identities are
/// folded away, which links exist (our dials, inbound connections, and the peer-to-peer links our
/// peers report), and where the ring layout puts them.
@Suite("Mesh graph")
struct MeshGraphTests {
    private let t0 = Date(timeIntervalSince1970: 1_700_000_000)

    private func presence(
        _ name: String, kind: PeerKind = .mac, active: Int = 0, max: Int? = nil,
        accepts: Bool = true, projects: Int = 0, updatedAt: Date? = nil
    ) -> RunnerPresence {
        RunnerPresence(
            hostName: name, kind: kind, canRunAgents: true, acceptsMeshDispatch: accepts,
            activeSessionCount: active, maxConcurrent: max, backends: [],
            projects: (0..<projects).map {
                ProjectDescriptor(
                    projectID: ProjectID(rawValue: "p\($0)"), name: "P\($0)",
                    rootCommitSHA: nil, normalizedRemote: nil, defaultBranch: "main")
            },
            updatedAt: updatedAt ?? t0)
    }

    private func peer(
        _ deviceID: String, label: String, kind: PeerKind = .mac, connected: Bool = true,
        transport: SyncTransportHint? = .lan, remotePeers: [PeerSummary] = [],
        canReceiveSessionTransfer: Bool = false, lastError: String? = nil
    ) -> PeerClient.PeerPresence {
        PeerClient.PeerPresence(
            deviceID: deviceID, label: label, kind: kind, connected: connected,
            transport: connected ? transport : nil, remotePeers: remotePeers,
            canReceiveSessionTransfer: canReceiveSessionTransfer, lastError: lastError)
    }

    private func device(
        _ deviceID: String, label: String, kind: PeerKind = .iphone, lastSeenAt: Date? = nil
    ) -> PairedDevice {
        PairedDevice(
            deviceID: deviceID, label: label, staticPublicKey: Data(), pairedAt: t0,
            lastSeenAt: lastSeenAt, kind: kind)
    }

    /// The label chain a store applies: peer label, then presence name, then the id prefix.
    private func hostLabel(
        peers: [PeerClient.PeerPresence] = [], presence: [String: RunnerPresence] = [:]
    ) -> (String) -> String {
        { id in
            if let peer = peers.first(where: { $0.deviceID == id }), !peer.label.isEmpty {
                return peer.label
            }
            if let name = presence[id]?.hostName, !name.isEmpty { return name }
            return String(id.prefix(8))
        }
    }

    // MARK: Nodes

    @Test func foldsEveryHostSourceIntoNodes() {
        let peers = [
            peer("peer-mac", label: "Studio", transport: .relay),
            peer("peer-off", label: "Laptop", connected: false, lastError: "no route"),
        ]
        let presence = [
            "peer-mac": presence("Studio", active: 2, max: 4),
            "card-only": presence("Runner", kind: .cloud),
        ]
        let graph = MeshGraph.build(
            selfID: "self-id", selfName: "This Mac", peers: peers, presenceByHost: presence,
            devices: [device("phone-1", label: "iPhone")], inbound: ["phone-1"],
            hostLabel: hostLabel(peers: peers, presence: presence))

        #expect(graph.nodes.map(\.id) == ["self-id", "peer-mac", "peer-off", "card-only", "phone-1"])

        let this = graph.node("self-id")!
        #expect(this.isSelf)
        #expect(this.online)
        #expect(this.label == "This Mac")

        // A dialed peer with a card carries both halves: its transport and its dispatch posture.
        let studio = graph.node("peer-mac")!
        #expect(studio.online)
        #expect(studio.transport == .relay)
        #expect(studio.hasPresenceCard)
        #expect(studio.acceptsMeshDispatch)
        #expect(studio.loadRatio == 0.5)
        #expect(studio.activeSessionCount == 2)

        // A peer we can't reach stays a node — with the dial error that explains why.
        let laptop = graph.node("peer-off")!
        #expect(!laptop.online)
        #expect(laptop.transport == nil)
        #expect(laptop.lastError == "no route")
        #expect(!laptop.hasPresenceCard)

        // A host known only by its mirrored card, and a phone known only by its pin + live
        // inbound connection — the two sources a peer list alone would miss.
        #expect(graph.node("card-only")?.kind == .cloud)
        let phone = graph.node("phone-1")!
        #expect(phone.kind == .iphone)
        #expect(phone.online)
        #expect(phone.inbound)
        #expect(phone.label == "iPhone")
    }

    /// Capacity is reported as the scorer reads it: an unbounded host is scored against 8.
    @Test func unboundedCapacityScoresAsEight() {
        let presence = ["peer": presence("Studio", active: 2)]
        let graph = MeshGraph.build(
            selfID: "self", selfName: "Mac", peers: [], presenceByHost: presence, devices: [],
            inbound: [], hostLabel: hostLabel(presence: presence))
        #expect(graph.node("peer")?.maxConcurrent == nil)
        #expect(graph.node("peer")?.loadRatio == 0.25)
    }

    @Test func collapsesSupersededMacIdentitiesButKeepsDistinctOnlineOnes() {
        let peers = [
            peer("live", label: "Studio"),
            peer("stale", label: "Studio", connected: false),
            peer("other", label: "Studio"),
            peer("gone-old", label: "Laptop", connected: false),
            peer("gone-new", label: "Laptop", connected: false),
        ]
        let presence = [
            "gone-old": presence("Laptop", updatedAt: t0),
            "gone-new": presence("Laptop", updatedAt: t0.addingTimeInterval(60)),
        ]
        let graph = MeshGraph.build(
            selfID: "self", selfName: "Mac", peers: peers, presenceByHost: presence, devices: [],
            inbound: [], hostLabel: hostLabel(peers: peers, presence: presence))

        let ids = graph.nodes.map(\.id)
        // The offline "Studio" is that live Mac's stale key — hidden. Two genuinely different
        // Macs that both answer to "Studio" while online stay separate rows.
        #expect(!ids.contains("stale"))
        #expect(ids.contains("live"))
        #expect(ids.contains("other"))
        // Two offline "Laptop" identities collapse to the one with the fresher card.
        #expect(ids.contains("gone-new"))
        #expect(!ids.contains("gone-old"))
    }

    @Test func phonesAreNeverCollapsedByName() {
        let devices = [
            device("phone-a", label: "iPhone"),
            device("phone-b", label: "iPhone"),
        ]
        let graph = MeshGraph.build(
            selfID: "self", selfName: "Mac", peers: [], presenceByHost: [:], devices: devices,
            inbound: [], hostLabel: hostLabel())
        #expect(graph.nodes.filter { $0.kind == .iphone }.count == 2)
    }

    // MARK: Edges

    @Test func dialsInboundAndHearsayBecomeEdges() {
        let peers = [
            peer(
                "peer-a", label: "Studio", transport: .tailnet,
                remotePeers: [
                    PeerSummary(deviceID: "peer-b", label: "Mini", kind: .mac, online: true),
                    PeerSummary(deviceID: "far", label: "Far Mac", kind: .mac, online: true),
                    PeerSummary(deviceID: "sleeping", label: "Asleep", kind: .mac, online: false),
                ]),
            peer("peer-b", label: "Mini", connected: false, lastError: "timed out"),
        ]
        let graph = MeshGraph.build(
            selfID: "self", selfName: "Mac", peers: peers, presenceByHost: [:],
            devices: [device("phone", label: "iPhone")], inbound: ["phone"],
            hostLabel: hostLabel(peers: peers))

        let dialA = graph.edges.first { $0.to == "peer-a" }!
        #expect(dialA.kind == .dialed)
        #expect(dialA.online)
        #expect(dialA.transport == .tailnet)

        // A down dial is still an edge — "we know the way there and it isn't working" is the
        // thing worth seeing — and it carries the error rather than a transport.
        let dialB = graph.edges.first { $0.to == "peer-b" }!
        #expect(dialB.kind == .dialed)
        #expect(!dialB.online)
        #expect(dialB.transport == nil)
        #expect(dialB.detail == "timed out")

        let inbound = graph.edges.first { $0.kind == .inbound }!
        #expect(inbound.from == "self" && inbound.to == "phone")

        // Hearsay: the peer↔peer link we're not an endpoint of, and the host we learn about only
        // because a peer listed it. An offline entry in that roster is not a live link.
        let reported = graph.edges.filter { $0.kind == .reported }
        #expect(reported.map(\.to).sorted() == ["far", "peer-b"])
        #expect(reported.allSatisfy { $0.from == "peer-a" })
        #expect(reported.first?.detail == "reported by Studio")
        #expect(!graph.edges.contains { $0.to == "sleeping" })

        // A host only a peer knows is a node, flagged so the viewer's list can skip it.
        let far = graph.node("far")!
        #expect(far.isIndirect)
        #expect(far.online)
        #expect(far.label == "Far Mac")
        #expect(graph.node("peer-a")?.isIndirect == false)
    }

    /// First-hand truth wins: our own dial to a peer isn't replaced by that peer's report of the
    /// same link back to us, and one link is never drawn twice.
    @Test func firsthandEdgeWinsOverHearsayForTheSamePair() {
        let peers = [
            peer(
                "peer-a", label: "Studio", transport: .lan,
                remotePeers: [PeerSummary(deviceID: "self", label: "Mac", kind: .mac, online: true)]),
        ]
        let graph = MeshGraph.build(
            selfID: "self", selfName: "Mac", peers: peers, presenceByHost: [:], devices: [],
            inbound: [], hostLabel: hostLabel(peers: peers))
        #expect(graph.edges.count == 1)
        #expect(graph.edges.first?.kind == .dialed)
        #expect(graph.edges(touching: "peer-a").count == 1)
    }

    /// An edge to a node the collapse folded away would draw a line to nowhere.
    @Test func edgesToCollapsedIdentitiesAreDropped() {
        let peers = [
            peer("live", label: "Studio"),
            peer("stale", label: "Studio", connected: false),
        ]
        let graph = MeshGraph.build(
            selfID: "self", selfName: "Mac", peers: peers, presenceByHost: [:], devices: [],
            inbound: [], hostLabel: hostLabel(peers: peers))
        let ids = Set(graph.nodes.map(\.id))
        #expect(graph.edges.allSatisfy { ids.contains($0.from) && ids.contains($0.to) })
        #expect(!graph.edges.contains { $0.to == "stale" })
    }

    // MARK: Layout

    @Test func layoutCentersSelfAndRingsByReachability() {
        let peers = [
            peer("on-1", label: "A"),
            peer("on-2", label: "B"),
            peer("off-1", label: "C", connected: false),
        ]
        let graph = MeshGraph.build(
            selfID: "self", selfName: "Mac", peers: peers, presenceByHost: [:], devices: [],
            inbound: [], hostLabel: hostLabel(peers: peers))
        let positions = graph.layout()

        #expect(positions.count == graph.nodes.count)
        #expect(positions["self"] == MeshGraphPoint(x: 0, y: 0))

        func radius(_ id: String) -> Double {
            let p = positions[id]!
            return (p.x * p.x + p.y * p.y).squareRoot()
        }
        // Reachable hosts ring the center; the unreachable one sits further out.
        #expect(abs(radius("on-1") - radius("on-2")) < 0.0001)
        #expect(radius("off-1") > radius("on-1"))
        // Deterministic: the same snapshot lays out the same way every refresh.
        #expect(graph.layout() == positions)
    }

    /// With nothing offline the single ring pulls inward rather than hugging the rim.
    @Test func aLoneRingSitsInsideTheRim() {
        let peers = [peer("on-1", label: "A")]
        let graph = MeshGraph.build(
            selfID: "self", selfName: "Mac", peers: peers, presenceByHost: [:], devices: [],
            inbound: [], hostLabel: hostLabel(peers: peers))
        let p = graph.layout()["on-1"]!
        #expect(abs((p.x * p.x + p.y * p.y).squareRoot() - 0.72) < 0.0001)
    }

    @Test func emptyMeshLaysOutWithoutACenter() {
        let graph = MeshGraph.build(
            selfID: nil, selfName: "Mac", peers: [], presenceByHost: [:], devices: [],
            inbound: [], hostLabel: hostLabel())
        #expect(graph.nodes.isEmpty)
        #expect(graph.layout().isEmpty)
    }
}
