import APIClient
import Foundation
import HostRegistry
import SessionCore
import Testing
import WireProtocol
@testable import WebTerm

/// T-iOS-15 · Hosted live-server smoke — the automated stand-in for the manual
/// 配对→列表→attach walkthrough (plan §7 T-iOS-15; the true device tap-through
/// + app-switcher shade visual check remain DEFERRED to T-iOS-18).
///
/// Drives the REAL production DI graph end-to-end against the repo's real
/// Node server: `runPairingProbe` over `URLSessionHTTPTransport` +
/// `URLSessionTermTransport` (WS upgrade with Origin + guarded kill
/// round-trip) → Host stored (InMemory per task) → `APIClient.liveSessions` →
/// `SessionEngine.open(nil)` → input echo → output marker → `close()`.
@Suite("Live-server smoke (production DI graph)", .serialized)
struct LiveServerSmokeTests {
    /// Collects engine output off the single-consumer stream.
    private actor OutputObserver {
        private(set) var adoptedId: UUID?
        private var buffer = ""
        private static let tailLength = 400

        func recordAdopted(_ id: UUID) { adoptedId = id }

        /// Appends a chunk; true once the accumulated output contains `marker`.
        func appendAndCheck(_ chunk: String, marker: String) -> Bool {
            buffer += chunk
            return buffer.contains(marker)
        }

        var tail: String { String(buffer.suffix(Self.tailLength)) }
    }

    @Test("探针→host 入库→liveSessions→attach(null)→echo→output→close")
    func productionGraphEndToEnd() async throws {
        let endpoint = try await SimServerHarness.shared.endpoint()

        // Production transports — the same instances AppEnvironment wires.
        let http = URLSessionHTTPTransport()
        let ws = URLSessionTermTransport()

        // ① Pairing probe (two-step: RO GET, WS attach + Origin, guarded kill).
        let probed: HostEndpoint
        switch await runPairingProbe(endpoint: endpoint, http: http, ws: ws) {
        case .failure(let error):
            Issue.record("pairing probe failed: \(error)")
            return
        case .success(let validated):
            probed = validated
        }

        // ② Host stored (InMemory stand-in is explicitly allowed here).
        let store = InMemoryHostStore()
        let host = HostRegistry.Host(id: UUID(), name: "smoke", endpoint: probed)
        _ = try await store.upsert(host)
        #expect(try await store.loadAll().map(\.id) == [host.id])

        // ③ Session list over the real HTTP path (RO — no Origin).
        let api = APIClient(endpoint: probed, http: http)
        _ = try await api.liveSessions()

        // ④ Real engine: attach(null) → shell echo → observe evaluated marker.
        // Arithmetic dedup: the TYPED command contains "$((...))", only the
        // EVALUATED output contains the final marker string.
        let base = 4200
        let offset = Int.random(in: 1...999)
        let marker = "smoke-\(base + offset)"
        let command = "echo smoke-$((\(base)+\(offset)))\r"

        let engine = SessionEngine(
            transport: ws, clock: ContinuousClock(), endpoint: probed,
            eventsSource: { id in try await api.events(id: id) }
        )
        let observer = OutputObserver()
        let consumeTask = Task { () -> Bool in
            for await event in engine.events {
                switch event {
                case .adopted(let id):
                    await observer.recordAdopted(id)
                case .output(let chunk):
                    if await observer.appendAndCheck(chunk, marker: marker) { return true }
                default:
                    break
                }
            }
            return false
        }

        await engine.open(sessionId: nil, cwd: nil)
        await engine.send(.input(data: command)) // queues until attach-ready

        let watchdog = Task {
            try? await Task.sleep(for: SmokeTunables.outputTimeout)
            consumeTask.cancel() // AsyncStream iteration honors cancellation
        }
        let sawMarker = await consumeTask.value
        watchdog.cancel()
        await engine.close()

        let outputTail = await observer.tail
        #expect(sawMarker, "output 未包含 \(marker)；tail: \(outputTail)")

        // Cleanup: kill the smoke session via the guarded G route (Origin).
        let adopted = try #require(await observer.adoptedId, "attached 帧未到达")
        do {
            try await api.killSession(id: adopted)
        } catch APIClientError.sessionNotFound {
            // Already gone — cleanup goal reached.
        }
    }
}
