import Foundation
import Testing
import NucleicProtocol

@testable import NucleicCore

/// The composite is what lets the host accept over LAN and the tailnet at once (the phone
/// picks whichever works). It must merge every child's accepted connections into one
/// stream, survive one child ending, and stop all children together.
@Suite struct CompositeSyncListenerTests {
    @Test func mergesConnectionsFromAllChildren() async throws {
        let first = ManualListener()
        let second = ManualListener()
        let composite = CompositeSyncListener([first, second])
        let stream = try composite.start()

        let (a, _) = MemoryChannel.pair()
        let (b, _) = MemoryChannel.pair()
        first.push(a)
        second.push(b)

        var received = 0
        for await _ in stream {
            received += 1
            if received == 2 { break }
        }
        #expect(received == 2)
        composite.stop()
    }

    @Test func oneChildEndingDoesNotEndTheMergedStream() async throws {
        let dying = ManualListener()
        let healthy = ManualListener()
        let composite = CompositeSyncListener([dying, healthy])
        let stream = try composite.start()

        dying.stop()  // e.g. the LAN listener fails — the tailnet path must keep accepting

        let (channel, _) = MemoryChannel.pair()
        healthy.push(channel)

        var received: (any FrameChannel)?
        for await connection in stream {
            received = connection
            break
        }
        #expect(received != nil)
        composite.stop()
    }

    @Test func stopFinishesTheMergedStream() async throws {
        let child = ManualListener()
        let composite = CompositeSyncListener([child])
        let stream = try composite.start()
        composite.stop()
        var received = 0
        for await _ in stream { received += 1 }
        #expect(received == 0)
    }

    // MARK: Partial-failure tolerance (mesh P1) — with multiple user-selected methods, a
    // pending Tailscale login must not take down LAN.

    @Test func childStartFailureDegradesInsteadOfFailingTheMerge() async throws {
        let failing = FailingListener()
        let healthy = ManualListener()
        let reported = ReportedFailures()
        let composite = CompositeSyncListener([failing, healthy]) { index, _ in
            reported.record(index)
        }
        let stream = try composite.start()

        let (channel, _) = MemoryChannel.pair()
        healthy.push(channel)

        var received: (any FrameChannel)?
        for await connection in stream {
            received = connection
            break
        }
        #expect(received != nil)
        #expect(reported.snapshot() == [0])
        composite.stop()
    }

    @Test func allChildrenFailingThrows() {
        let composite = CompositeSyncListener([FailingListener(), FailingListener()])
        #expect(throws: SyncTransportError.self) { _ = try composite.start() }
    }
}

/// A listener whose start always fails (e.g. a port bind error).
private final class FailingListener: SyncListener, @unchecked Sendable {
    func start() throws -> AsyncStream<any FrameChannel> {
        throw SyncTransportError.listenerFailed("bind failed")
    }
    func stop() {}
}

/// Thread-safe capture of the composite's failure callbacks (they fire on the caller's
/// executor mid-`start`).
private final class ReportedFailures: @unchecked Sendable {
    private let lock = NSLock()
    private var indices: [Int] = []
    func record(_ index: Int) {
        lock.lock(); defer { lock.unlock() }
        indices.append(index)
    }
    func snapshot() -> [Int] {
        lock.lock(); defer { lock.unlock() }
        return indices
    }
}
