//
// ServerHarness.swift — T-iOS-16 集成测试基础设施：真 Node 服务器自举
//
// 由 T-iOS-2 的 OriginSpikeTests.swift（591 行单文件）拆分而来（plan §7 T-iOS-16
// 明确批准 harness + suites 拆分）。本文件只管服务器生命周期：
//   - spawn 一次本仓库真服务器（`node_modules/.bin/tsx src/server.ts`），全部
//     suite 复用同一进程（会话彼此独立，测试并行安全）；
//   - 就绪探测（GET /live-sessions 返回 JSON 数组）；
//   - 进程回收：death-pipe watchdog + atexit 双保险（Swift Testing runner 退出
//     时不可靠地跑 atexit，watchdog 是主机制）。
//
// 运行方式（可复现）：
//   A. 默认自举：`swift test --package-path ios/IntegrationTests`
//      repo 根由 #filePath 推导（可用 WEBTERM_REPO_ROOT 覆盖），绑定 127.0.0.1
//      的空闲端口，SHELL_PATH=/bin/bash（确定性输出）、USE_TMUX=0。无需
//      ALLOWED_ORIGINS：服务器的 deriveAllowedOrigins 恒包含
//      http://127.0.0.1:<port>（src/config.ts:187-226）。
//      日志落在 $TMPDIR/webterm-integration-server-<port>.log。
//   B. 外部服务器：WEBTERM_SERVER_URL=http://127.0.0.1:<port>（须为 loopback，
//      这样其自身 origin 天然在白名单内）。
//
// 与 spike 版的关键差异：WireProtocol 已冻结（T-iOS-3），Origin/wsURL 一律由
// `HostEndpoint` 单点派生（plan §5.1 铁律：禁止手拼），不再本地复刻常量。

import Darwin
import Foundation
import WireProtocol
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

// ─── 常量（无魔法数字；线协议常量一律取 WireProtocol.Tunables/WireConstants） ──

enum HarnessTunables {
    /// 灌入的普通输出字节数：> URLSessionWebSocketTask 默认 1 MiB（1_048_576）。
    static let bulkOutputBytes = 1_500_000
    /// 对抗用例："\u{1B}[0m"（4 字节）重复次数 → 原始 1.6 MB，
    /// JSON 转义后 ≈ ×2.25 ≈ 3.6 MB（>1MiB、<16MiB、<2MiB ring 容量）。
    static let escSequenceRepeats = 400_000
    static let escSequenceRawBytes = escSequenceRepeats * 4
    /// Origin 失配用端口（若真实端口撞车则退避到 fallback）。
    static let mismatchedOriginPort = 9999
    static let mismatchedOriginPortFallback = 9998
    static let serverReadyTimeout: Duration = .seconds(40)
    static let serverReadyPollInterval: Duration = .milliseconds(200)
    /// 单帧等待上限（attach→attached 等短往返）。
    static let frameTimeout: Duration = .seconds(15)
    /// 等待某个 shell 输出标记出现的上限（含 bash 冷启动）。
    static let markerTimeout: Duration = .seconds(30)
    /// 大流量累计（>1MiB 灌入/回放）的上限。
    static let outputAccumulationTimeout: Duration = .seconds(90)
    /// 等待服务器主动关闭 WS 的上限（kill 路径）。
    static let closeObserveTimeout: Duration = .seconds(30)
}

enum HarnessError: Error, CustomStringConvertible {
    case setup(String)
    case timeout(String)

    var description: String {
        switch self {
        case .setup(let detail): return "setup: \(detail)"
        case .timeout(let detail): return "timeout: \(detail)"
        }
    }
}

// ─── 服务器端点快照（不可变；派生全部走 HostEndpoint 冻结契约） ────────────────

struct TestServer: Sendable {
    let endpoint: HostEndpoint

    var baseURL: URL { endpoint.baseURL }
    var wsURL: URL { endpoint.wsURL }
    /// 白名单内的 Origin —— 由 HostEndpoint 单点派生（plan §5.1，禁止手拼）。
    var origin: String { endpoint.originHeader }
    var port: Int { endpoint.baseURL.port ?? (endpoint.baseURL.scheme == "https" ? 443 : 80) }

    /// 显式校验 + 派生（服务器/env 是不可信输入，边界处全验证）。
    static func make(baseURL: URL) throws -> TestServer {
        guard let endpoint = HostEndpoint(baseURL: baseURL) else {
            throw HarnessError.setup("server URL 必须是带 host 的 http(s)，got \(baseURL)")
        }
        return TestServer(endpoint: endpoint)
    }
}

// ─── 服务器进程回收（death-pipe watchdog + atexit 双保险，防孤儿 tsx） ────────
//
// 实测（T-iOS-2）：Swift Testing runner 退出时不跑 atexit handler（首轮运行留下
// 孤儿 tsx），所以主回收机制是 death-pipe：spawn 一个 /bin/sh watchdog，其 stdin
// 是本测试进程持有的管道写端 —— 测试进程无论以何种方式退出（正常/崩溃），写端
// 关闭 → watchdog 的 `read` 收到 EOF → kill 服务器 pid。atexit 仅作快速路径兜底。

private enum ServerProcessRegistry {
    private static let lock = NSLock()
    nonisolated(unsafe) private static var process: Process?
    /// 持有写端防 deinit 关闭；测试进程死亡时由内核关闭 → watchdog 触发。
    nonisolated(unsafe) private static var deathPipe: Pipe?
    nonisolated(unsafe) private static var installedAtexit = false

    static func register(_ p: Process) {
        lock.lock()
        defer { lock.unlock() }
        process = p
        deathPipe = try? spawnDeathWatchdog(serverPid: p.processIdentifier)
        guard !installedAtexit else { return }
        installedAtexit = true
        atexit { ServerProcessRegistry.killNow() }
    }

    static func killNow() {
        lock.lock()
        defer { lock.unlock() }
        if let p = process, p.isRunning { p.terminate() }
        process = nil
    }

    private static func spawnDeathWatchdog(serverPid: Int32) throws -> Pipe {
        let pipe = Pipe()
        let watchdog = Process()
        watchdog.executableURL = URL(fileURLWithPath: "/bin/sh")
        watchdog.arguments = ["-c", "read _; kill \(serverPid) 2>/dev/null"]
        watchdog.standardInput = pipe
        watchdog.standardOutput = FileHandle.nullDevice
        watchdog.standardError = FileHandle.nullDevice
        try watchdog.run()
        return pipe
    }
}

// ─── 服务器 harness（actor 单例：spawn 一次，全部 suite 复用） ─────────────────
//
// Swift Testing 默认跨 suite 并行 → 多个 suite 会并发调 server()。actor 是可重入
// 的：首个调用在 awaitReady 挂起时，后来者会重入进来 —— 若用"缓存成品"模式就会
// 重复 spawn。所以缓存的是 in-flight Task（同步写入，后来者 await 同一个）。

actor ServerHarness {
    static let shared = ServerHarness()
    private var bootTask: Task<TestServer, any Error>?

    func server() async throws -> TestServer {
        if let bootTask { return try await bootTask.value }
        let task = Task { try await Self.bootstrap() }
        bootTask = task
        return try await task.value
    }

    private static func bootstrap() async throws -> TestServer {
        if let external = ProcessInfo.processInfo.environment["WEBTERM_SERVER_URL"] {
            guard let url = URL(string: external) else {
                throw HarnessError.setup("WEBTERM_SERVER_URL 不是合法 URL: \(external)")
            }
            let server = try TestServer.make(baseURL: url)
            try await awaitReady(server, process: nil, logURL: nil)
            return server
        }
        let spawned = try spawnLocalServer()
        ServerProcessRegistry.register(spawned.process)
        do {
            try await awaitReady(spawned.server, process: spawned.process, logURL: spawned.logURL)
        } catch {
            ServerProcessRegistry.killNow()
            throw error
        }
        return spawned.server
    }

    private static func spawnLocalServer() throws -> (server: TestServer, process: Process, logURL: URL) {
        let repoRoot = try locateRepoRoot()
        let tsx = repoRoot.appending(path: "node_modules/.bin/tsx")
        guard FileManager.default.isExecutableFile(atPath: tsx.path) else {
            throw HarnessError.setup("找不到 \(tsx.path) — 先在 repo 根目录跑 npm install/npm ci")
        }
        let port = try findFreeLoopbackPort()
        guard let baseURL = URL(string: "http://127.0.0.1:\(port)") else {
            throw HarnessError.setup("无法构造 baseURL, port=\(port)")
        }
        let server = try TestServer.make(baseURL: baseURL)

        let logURL = FileManager.default.temporaryDirectory
            .appending(path: "webterm-integration-server-\(port).log")
        FileManager.default.createFile(atPath: logURL.path, contents: nil)
        let logHandle = try FileHandle(forWritingTo: logURL)

        let process = Process()
        process.executableURL = tsx
        process.arguments = ["src/server.ts"]
        process.currentDirectoryURL = repoRoot
        var env = ProcessInfo.processInfo.environment
        env["PORT"] = String(port)
        env["BIND_HOST"] = "127.0.0.1" // 测试期间绝不监听 0.0.0.0
        env["SHELL_PATH"] = "/bin/bash" // 确定性输出（不吃用户 zsh 配置）
        env["USE_TMUX"] = "0"
        env["PATH"] = (env["PATH"] ?? "") + ":/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"
        process.environment = env
        process.standardOutput = logHandle
        process.standardError = logHandle
        try process.run()
        return (server, process, logURL)
    }

    private static func awaitReady(_ server: TestServer, process: Process?, logURL: URL?) async throws {
        let probeURL = server.baseURL.appending(path: "live-sessions")
        let deadline = ContinuousClock.now + HarnessTunables.serverReadyTimeout
        while ContinuousClock.now < deadline {
            if let process, !process.isRunning {
                throw HarnessError.setup(
                    "服务器进程提前退出（exit \(process.terminationStatus)）— 日志: \(logURL?.path ?? "n/a")")
            }
            if let (data, response) = try? await URLSession.shared.data(from: probeURL),
                (response as? HTTPURLResponse)?.statusCode == 200,
                (try? JSONSerialization.jsonObject(with: data)) is [Any] {
                return
            }
            try await Task.sleep(for: HarnessTunables.serverReadyPollInterval)
        }
        throw HarnessError.timeout(
            "服务器 \(server.baseURL) 在 \(HarnessTunables.serverReadyTimeout) 内未就绪 — 日志: \(logURL?.path ?? "n/a")")
    }
}

func locateRepoRoot() throws -> URL {
    if let override = ProcessInfo.processInfo.environment["WEBTERM_REPO_ROOT"] {
        return URL(fileURLWithPath: override, isDirectory: true)
    }
    // #filePath = <repo>/ios/IntegrationTests/ServerHarness.swift
    return URL(fileURLWithPath: #filePath)
        .deletingLastPathComponent() // IntegrationTests
        .deletingLastPathComponent() // ios
        .deletingLastPathComponent() // repo root
}

/// bind(port=0) 拿一个空闲 loopback 端口（close 后交给服务器绑定；竞态窗口可忽略，
/// 若真撞车 awaitReady 会用日志报错）。
func findFreeLoopbackPort() throws -> Int {
    let fd = socket(AF_INET, SOCK_STREAM, 0)
    guard fd >= 0 else { throw HarnessError.setup("socket() 失败: errno \(errno)") }
    defer { close(fd) }

    var addr = sockaddr_in()
    addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
    addr.sin_family = sa_family_t(AF_INET)
    addr.sin_port = 0
    addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))
    let bound = withUnsafePointer(to: &addr) {
        $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
            bind(fd, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
        }
    }
    guard bound == 0 else { throw HarnessError.setup("bind() 失败: errno \(errno)") }

    var out = sockaddr_in()
    var len = socklen_t(MemoryLayout<sockaddr_in>.size)
    let named = withUnsafeMutablePointer(to: &out) {
        $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { getsockname(fd, $0, &len) }
    }
    guard named == 0 else { throw HarnessError.setup("getsockname() 失败: errno \(errno)") }
    return Int(UInt16(bigEndian: out.sin_port))
}
