#if os(macOS)
import Darwin
import Foundation
import Testing
import WireProtocol
@testable import SessionCore

/// T-iOS-9 · `URLSessionTermTransport` against an in-process scripted WS
/// server (`ScriptedWSServer`: raw-TCP loopback, ephemeral port — macOS-only
/// test infra per plan §7 T-iOS-9; the real-Node-server pass is T-iOS-16).
///
/// These are real-I/O loopback tests: there are NO pacing sleeps — every wait
/// is continuation/stream-driven, and `withTimeout` is a fail-fast bound, not
/// a synchronization primitive.
@Suite("URLSessionTermTransport", .timeLimit(.minutes(3)))
struct URLSessionTermTransportTests {

    /// Named test constants (no magic numbers).
    private enum TestTunables {
        /// Fail-fast upper bound for any single loopback wait.
        static let ioTimeout: Duration = .seconds(15)
        /// Frame count for the re-arm/ordering test (RED list: 100).
        static let orderedFrameCount = 100
        /// Internal transport seam value: shrunken `maximumMessageSize` so the
        /// oversize→EMSGSIZE path is deterministic without 16 MiB fixtures.
        static let tinyMessageCap = 64 * 1024
        /// 2 × `tinyMessageCap` — guaranteed over the shrunken cap.
        static let oversizePayloadBytes = 128 * 1024
        /// RFC 6455 normal-closure status code.
        static let normalCloseCode: UInt16 = 1000
        /// Arbitrary non-text payload for the binary-frame drop test.
        static let binaryPayload: [UInt8] = [0xDE, 0xAD, 0xBE, 0xEF]
    }

    private struct TimedOut: Error {}

    // MARK: - Helpers

    private static func withTimeout<T: Sendable>(
        _ timeout: Duration = TestTunables.ioTimeout,
        _ operation: @escaping @Sendable () async throws -> T
    ) async throws -> T {
        try await withThrowingTaskGroup(of: T.self) { group in
            group.addTask { try await operation() }
            group.addTask {
                try await Task.sleep(for: timeout)
                throw TimedOut()
            }
            guard let first = try await group.next() else { throw TimedOut() }
            group.cancelAll()
            return first
        }
    }

    private static func makeEndpoint(port: UInt16) throws -> HostEndpoint {
        let url = try #require(URL(string: "http://127.0.0.1:\(port)"))
        return try #require(HostEndpoint(baseURL: url))
    }

    private static func startServer() async throws
        -> (server: ScriptedWSServer, endpoint: HostEndpoint) {
        let server = ScriptedWSServer()
        let port = try await server.start()
        return (server, try makeEndpoint(port: port))
    }

    private static func firstUpgrade(
        _ server: ScriptedWSServer
    ) async throws -> ScriptedWSServer.Upgrade {
        try await withTimeout {
            var iterator = server.upgrades.makeAsyncIterator()
            guard let upgrade = await iterator.next() else { throw TimedOut() }
            return upgrade
        }
    }

    private static func collect(
        _ count: Int, from frames: AsyncThrowingStream<String, any Error>
    ) async throws -> [String] {
        var collected: [String] = []
        for try await frame in frames {
            collected.append(frame)
            if collected.count == count { return collected }
        }
        return collected
    }

    // MARK: - RED list (plan §7 T-iOS-9 Steps)

    @Test("upgrade request carries Origin == endpoint.originHeader (single source, §5.1)")
    func upgradeRequestCarriesEndpointOriginHeader() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }

        let connection = try await URLSessionTermTransport().connect(to: endpoint)
        let upgrade = try await Self.firstUpgrade(server)

        #expect(upgrade.headers["origin"] == endpoint.originHeader)
        #expect(upgrade.requestLine.hasPrefix("GET \(WireConstants.wsPath) "))
        await connection.close()
    }

    @Test("connected task uses Tunables.maxWSMessageBytes, not the 1 MiB platform default")
    func connectedTaskUsesFrozenMaxMessageSize() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }

        let connection = try await URLSessionTermTransport().openConnection(to: endpoint)

        #expect(connection.task.maximumMessageSize == Tunables.maxWSMessageBytes)
        await connection.close()
    }

    @Test("receive loop re-arms continuously: 100 frames arrive, in order")
    func receiveLoopRearmsAcross100OrderedFrames() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }
        let connection = try await URLSessionTermTransport().connect(to: endpoint)
        let expected = (0..<TestTunables.orderedFrameCount).map { "frame-\($0)" }

        for frame in expected { server.send(text: frame) }
        let frames = connection.frames
        let received = try await Self.withTimeout {
            try await Self.collect(TestTunables.orderedFrameCount, from: frames)
        }

        #expect(received == expected)
        await connection.close()
    }

    @Test("server close frame → frames stream finishes cleanly (no throw)")
    func serverCloseFrameFinishesStream() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }
        let connection = try await URLSessionTermTransport().connect(to: endpoint)

        server.sendClose(code: TestTunables.normalCloseCode)
        let frames = connection.frames
        let leftover = try await Self.withTimeout {
            var collected: [String] = []
            for try await frame in frames { collected.append(frame) }
            return collected
        }

        #expect(leftover.isEmpty)
    }

    @Test("abrupt TCP kill → frames stream throws (distinguishable; NOT replayTooLarge)")
    func abruptTerminationThrowsTransportError() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }
        let connection = try await URLSessionTermTransport().connect(to: endpoint)

        server.abort()
        let frames = connection.frames
        var thrown: (any Error)?
        do {
            try await Self.withTimeout { for try await _ in frames {} }
        } catch {
            thrown = error
        }

        let failure = try #require(thrown, "stream must throw after abrupt termination")
        #expect(!(failure is TimedOut), "stream neither finished nor threw in time")
        #expect((failure as? TermTransportError) != .replayTooLarge)
    }

    @Test("oversize frame → typed TermTransportError.replayTooLarge (EMSGSIZE 40 / ENOBUFS 55)")
    func oversizeFrameThrowsTypedReplayTooLarge() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }
        let transport = URLSessionTermTransport(maxMessageBytes: TestTunables.tinyMessageCap)
        let connection = try await transport.connect(to: endpoint)

        server.send(text: String(repeating: "x", count: TestTunables.oversizePayloadBytes))
        let frames = connection.frames

        await #expect(throws: TermTransportError.replayTooLarge) {
            try await Self.withTimeout { for try await _ in frames {} }
        }
    }

    @Test("send after close() → explicit TermTransportError.sendAfterClose, no crash")
    func sendAfterCloseThrowsExplicitError() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }
        let connection = try await URLSessionTermTransport().connect(to: endpoint)

        await connection.close()

        await #expect(throws: TermTransportError.sendAfterClose) {
            try await connection.send(MessageCodec.encode(.input(data: "x")))
        }
    }

    @Test("binary frame is dropped (counted) and receiving continues")
    func binaryFrameDroppedAndReceivingContinues() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }
        let wsConnection = try await URLSessionTermTransport().openConnection(to: endpoint)
        let connection = wsConnection.transportConnection()

        server.send(binary: TestTunables.binaryPayload)
        server.send(text: "still-alive")
        let frames = connection.frames
        let received = try await Self.withTimeout {
            try await Self.collect(1, from: frames)
        }

        #expect(received == ["still-alive"])
        #expect(wsConnection.droppedBinaryFrames == 1)
        await connection.close()
    }

    @Test("send() delivers the client text frame to the server verbatim")
    func sendDeliversTextFrameToServer() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }
        let connection = try await URLSessionTermTransport().connect(to: endpoint)
        let frame = MessageCodec.encode(.attach(sessionId: nil, cwd: nil))

        try await connection.send(frame)
        let received = try await Self.withTimeout {
            var iterator = server.clientTextFrames.makeAsyncIterator()
            guard let first = await iterator.next() else { throw TimedOut() }
            return first
        }

        #expect(received == frame)
        await connection.close()
    }

    @Test("internal ping hook (non-frozen extension point): sendPing resolves pong")
    func internalPingHookResolvesPong() async throws {
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }

        let (connection, pinger) =
            try await URLSessionTermTransport().connectPingable(to: endpoint)
        let isPongReceived = try await Self.withTimeout { await pinger.ping() }

        #expect(isPongReceived)
        await connection.close()
    }

    @Test("identity provider is resolved PER CONNECT (no-relaunch pickup, not captured once)")
    func identityProviderResolvedPerConnect() async throws {
        // A cert imported mid-run must take effect on the NEXT connection. The
        // transport therefore re-consults its provider on every `connect` rather
        // than capturing a snapshot — proven here by a provider whose invocation
        // count grows once per connect (nil identity: ScriptedWSServer is plain
        // ws, so the connection succeeds regardless).
        let (server, endpoint) = try await Self.startServer()
        defer { server.stop() }
        let calls = CallCounter()
        let transport = URLSessionTermTransport(identityProvider: {
            calls.increment()
            return nil
        })

        let first = try await transport.connect(to: endpoint)
        await first.close()
        let second = try await transport.connect(to: endpoint)
        await second.close()

        #expect(calls.value == 2)
    }

    @Test("connect to a dead port throws (handshake failure path, no hang)")
    func connectToDeadPortThrows() async throws {
        let server = ScriptedWSServer()
        let port = try await server.start()
        await server.stopAndWait()
        let endpoint = try Self.makeEndpoint(port: port)

        await #expect(throws: (any Error).self) {
            _ = try await URLSessionTermTransport().connect(to: endpoint)
        }
    }

    /// Thread-safe invocation counter for the `@Sendable` identity provider
    /// (URLSession may consult it off the test's isolation domain).
    private final class CallCounter: @unchecked Sendable {
        private let lock = NSLock()
        private var count = 0
        func increment() { lock.withLock { count += 1 } }
        var value: Int { lock.withLock { count } }
    }
}
#endif
