import Foundation
import Testing
import NucleicProtocol

@testable import NucleicCore

/// The host-side inbound liveness watchdog: a peer that goes silent (a suspended phone, a half-open
/// tunnel that never delivers EOF) is reaped so the host stops reporting it as connected — which is
/// what lets its approval wakes and the LAN-drop reconnect nudge fire instead of being suppressed by
/// a zombie handler. An actively-sending peer is never falsely dropped.
@Suite struct SyncHostLivenessTests {
    /// Pair a real client over an in-memory LAN pipe and wait until it's welcomed (registered).
    private func connect(host: SyncHost, deviceID: String) async throws -> SyncClient {
        let (clientCh, serverCh) = MemoryChannel.pair(transport: .lan)
        try await host.start(listener: OneShotListener(serverCh))
        let payload = await host.beginPairing()
        let client = SyncClient(
            channel: clientCh, identity: DeviceIdentity(), hostStaticKey: payload.hostStaticKey,
            mode: .pair(secret: payload.pairingSecret), deviceID: deviceID, deviceLabel: "Phone")
        let recorder = EventRecorder()
        await recorder.consume(client.start())
        _ = await recorder.waitFor { if case .ready = $0 { return true } else { return false } }
        return client
    }

    @Test func silentConnectionIsReaped() async throws {
        let host = SyncHost(
            identity: DeviceIdentity(), bridge: FakeSyncBridge(sessions: []),
            store: InMemoryPairedDeviceStore(),
            connectionLivenessDeadline: .milliseconds(200))
        let client = try await connect(host: host, deviceID: "iphone-silent")
        // Connected right after the handshake…
        #expect(await host.connectedDeviceIDs() == ["iphone-silent"])
        // …but the client sends nothing further (its real keepalive ping is 20s away), so the host's
        // 200ms deadline lapses and the zombie is reaped.
        let gone = await poll { await host.connectedDeviceIDs().isEmpty ? [true] : [] }
        #expect(gone.first == true)
        await client.disconnect()
        await host.stop()
    }

    @Test func activelyPingingConnectionSurvivesThenReapedWhenSilent() async throws {
        let host = SyncHost(
            identity: DeviceIdentity(), bridge: FakeSyncBridge(sessions: []),
            store: InMemoryPairedDeviceStore(),
            connectionLivenessDeadline: .milliseconds(400))
        let client = try await connect(host: host, deviceID: "iphone-live")
        // Keep the link fresh with pings well inside the deadline — it must NOT be reaped.
        for _ in 0..<8 {
            await client.send(.ping)
            try await Task.sleep(for: .milliseconds(120))
        }
        #expect(await host.connectedDeviceIDs() == ["iphone-live"])
        // Now go silent → the host reaps it once the deadline lapses.
        let gone = await poll(timeout: .seconds(3)) {
            await host.connectedDeviceIDs().isEmpty ? [true] : []
        }
        #expect(gone.first == true)
        await client.disconnect()
        await host.stop()
    }
}
