import WireProtocol

/// Errors `FakeTransport` produces on its own (Equatable so tests can
/// `#expect(throws:)` on exact values).
public enum FakeTransportError: Error, Equatable, Sendable {
    /// Default error thrown by a scripted connect failure.
    case scriptedConnectFailure
    /// `send` was called on a connection that already terminated
    /// (client `close()`, server finish, or server error).
    case sendAfterClose
}

/// In-memory `TermTransport` double (T-iOS-4). `SessionEngine` cannot tell it
/// apart from `URLSessionTermTransport` — same frozen contract (plan §3.2).
///
/// Capabilities:
/// - **Scripted connect failures**: `scriptConnectFailure(_:)` queues errors
///   thrown by subsequent `connect` calls, FIFO (reconnect/backoff tests).
/// - **Manual frame injection**: `emit(frame:)` / `emitError(_:)` /
///   `finishFrames()` drive the latest connection's `frames` stream. With no
///   live connection the event is queued and flushed, in order, into the next
///   successful `connect` — so tests can script server behavior up front
///   (mirrors attach→replay) and nothing is ever silently dropped.
/// - **Recording**: every connect attempt (including scripted failures), every
///   sent frame (per connection and flattened), and every `close()` call.
///
/// Async-safe by construction (actor), Sendable, zero real-time sleeps:
/// injected frames buffer unboundedly until the consumer iterates.
public actor FakeTransport: TermTransport {
    private enum ServerEvent {
        case frame(String)
        case failure(any Error)
        case finish
    }

    private struct ConnectionState {
        let continuation: AsyncThrowingStream<String, any Error>.Continuation
        var sentFrames: [String] = []
        var isTerminated = false
    }

    private var scriptedConnectFailures: [any Error] = []
    private var pendingEvents: [ServerEvent] = []
    private var connections: [ConnectionState] = []

    /// Every endpoint `connect` was called with, in order — scripted failures
    /// included (so backoff tests can count attempts).
    public private(set) var connectAttempts: [HostEndpoint] = []
    /// Total `close()` calls across all connections (double-close included).
    public private(set) var closeCallCount = 0

    public init() {}

    // MARK: - Scripting

    /// Queue an error for the next `connect` call (FIFO across calls).
    public func scriptConnectFailure(
        _ error: any Error = FakeTransportError.scriptedConnectFailure
    ) {
        scriptedConnectFailures.append(error)
    }

    // MARK: - TermTransport

    public func connect(to endpoint: HostEndpoint) async throws -> TransportConnection {
        connectAttempts.append(endpoint)
        if !scriptedConnectFailures.isEmpty {
            throw scriptedConnectFailures.removeFirst()
        }
        let (stream, continuation) = AsyncThrowingStream<String, any Error>.makeStream()
        let index = connections.count
        connections.append(ConnectionState(continuation: continuation))
        flushPendingEvents(into: index)
        return TransportConnection(
            frames: stream,
            send: { [self] frame in try await self.recordSend(frame, connection: index) },
            close: { [self] in await self.recordClose(connection: index) }
        )
    }

    // MARK: - Manual injection (server side of the wire)

    /// Yield one server JSON text frame into the latest connection
    /// (or queue it for the next connect — see type doc).
    public func emit(frame: String) {
        deliver(.frame(frame))
    }

    /// Terminate the latest connection's stream with `error`
    /// (transport failure — the "stream throw" half of the contract).
    public func emitError(_ error: any Error) {
        deliver(.failure(error))
    }

    /// Finish the latest connection's stream cleanly
    /// (server close — the "stream finish" half of the contract).
    public func finishFrames() {
        deliver(.finish)
    }

    // MARK: - Recorded traffic

    /// All frames sent by the client, flattened in connection order.
    public var sentFrames: [String] { connections.flatMap(\.sentFrames) }

    /// Frames sent by the client, grouped per successful connection
    /// (reconnect tests assert the re-attach frame landed on connection 1).
    public var sentFramesByConnection: [[String]] { connections.map(\.sentFrames) }

    // MARK: - Internals

    private func deliver(_ event: ServerEvent) {
        guard let index = liveConnectionIndex() else {
            pendingEvents.append(event)
            return
        }
        apply(event, to: index)
    }

    private func liveConnectionIndex() -> Int? {
        guard let last = connections.indices.last, !connections[last].isTerminated else {
            return nil
        }
        return last
    }

    private func apply(_ event: ServerEvent, to index: Int) {
        switch event {
        case .frame(let frame):
            connections[index].continuation.yield(frame)
        case .failure(let error):
            connections[index].isTerminated = true
            connections[index].continuation.finish(throwing: error)
        case .finish:
            connections[index].isTerminated = true
            connections[index].continuation.finish()
        }
    }

    private func flushPendingEvents(into index: Int) {
        let events = pendingEvents
        pendingEvents = []
        for event in events {
            apply(event, to: index)
        }
    }

    private func recordSend(_ frame: String, connection index: Int) throws {
        guard !connections[index].isTerminated else {
            throw FakeTransportError.sendAfterClose
        }
        connections[index].sentFrames.append(frame)
    }

    private func recordClose(connection index: Int) {
        closeCallCount += 1
        guard !connections[index].isTerminated else { return }
        connections[index].isTerminated = true
        connections[index].continuation.finish()
    }
}
