package wang.yaojia.webterm.testsupport

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wire.TermTransport
import wang.yaojia.webterm.wire.TransportConnection

/**
 * Errors [FakeTermTransport] raises on its own (sealed so tests can match exact values, e.g.
 * `assertFailsWith<FakeTransportError.SendAfterClose>`). Android analogue of iOS
 * `FakeTransportError` (there an `Equatable` enum).
 */
public sealed class FakeTransportError(message: String) : Exception(message) {
    /** Default error thrown by a scripted connect failure. */
    public data object ScriptedConnectFailure :
        FakeTransportError("FakeTermTransport: scripted connect failure")

    /**
     * `send` was called on a connection that already terminated (client `close()`, server finish,
     * or server error) — mirrors iOS `FakeTransportError.sendAfterClose`.
     */
    public data object SendAfterClose :
        FakeTransportError("FakeTermTransport: send on a terminated connection")
}

/**
 * In-memory [TermTransport] double (A3, mirrors iOS `FakeTransport`). `SessionEngine` (A14) cannot
 * tell it apart from `:transport-okhttp`'s real impl — 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** (the server side of the wire): [emit] / [emitError] /
 *   [finishFrames] drive the latest connection's [TransportConnection.frames] stream. With no live
 *   connection the event is queued and flushed, in submission order, into the next successful
 *   [connect] — so a test can script "attach then replay" up front and nothing is silently dropped.
 * - **Recording**: every [connect] attempt (scripted failures included), every sent frame (per
 *   connection and flattened), and every [TransportConnection.close] call.
 *
 * Thread-safe by construction: all mutable state is guarded by one JVM monitor and every critical
 * section is synchronous (no suspension inside `synchronized`), so it is safe under `runTest`
 * virtual time and under real multi-thread dispatch alike. Frames buffer unboundedly
 * ([Channel.UNLIMITED]) until the consumer collects — zero real-time sleeps.
 *
 * `open` so the pingable variant ([FakePingableTermTransport]) can reuse [connect]. NOTE: this base
 * intentionally does NOT implement `PingableTermTransport` — matching the frozen contract note in
 * `TermTransport.kt`, so engine tests can exercise the closure-driven ping path. Use
 * [FakePingableTermTransport] when a test wants the `connectPingable` path instead.
 */
public open class FakeTermTransport : TermTransport {
    private sealed interface ServerEvent {
        data class Frame(val frame: String) : ServerEvent
        data class Failure(val error: Throwable) : ServerEvent
        data object Finish : ServerEvent
    }

    private class ConnectionState {
        val channel: Channel<String> = Channel(Channel.UNLIMITED)
        // consumeAsFlow() is single-collection by contract, matching "one live WS, consumed once".
        val framesFlow: Flow<String> = channel.consumeAsFlow()
        val sentFrames: MutableList<String> = mutableListOf()
        var isTerminated: Boolean = false
    }

    private val lock = Any()
    private val scriptedConnectFailures = ArrayDeque<Throwable>()
    private val pendingEvents = ArrayDeque<ServerEvent>()
    private val connections = mutableListOf<ConnectionState>()
    private val connectAttemptsInternal = mutableListOf<HostEndpoint>()
    private var closeCount = 0

    /** Every endpoint [connect] was called with, in order — scripted failures included. */
    public val connectAttempts: List<HostEndpoint>
        get() = synchronized(lock) { connectAttemptsInternal.toList() }

    /** Total [TransportConnection.close] calls across all connections (double-close included). */
    public val closeCallCount: Int
        get() = synchronized(lock) { closeCount }

    /** All frames sent by the client, flattened in connection order. */
    public val sentFrames: List<String>
        get() = synchronized(lock) { connections.flatMap { it.sentFrames.toList() } }

    /**
     * Frames sent by the client, grouped per successful connection (reconnect tests assert the
     * re-attach frame landed on connection index 1, not 0).
     */
    public val sentFramesByConnection: List<List<String>>
        get() = synchronized(lock) { connections.map { it.sentFrames.toList() } }

    // ── Scripting ────────────────────────────────────────────────────────────────────────

    /** Queue an error for the next [connect] call (FIFO across calls). */
    public fun scriptConnectFailure(
        error: Throwable = FakeTransportError.ScriptedConnectFailure,
    ) {
        synchronized(lock) { scriptedConnectFailures.addLast(error) }
    }

    // ── TermTransport ────────────────────────────────────────────────────────────────────

    override suspend fun connect(endpoint: HostEndpoint): TransportConnection {
        val index: Int
        val state: ConnectionState
        synchronized(lock) {
            connectAttemptsInternal.add(endpoint)
            scriptedConnectFailures.removeFirstOrNull()?.let { throw it }
            state = ConnectionState()
            index = connections.size
            connections.add(state)
            // Flush anything scripted before this connection existed, in submission order.
            while (pendingEvents.isNotEmpty()) {
                apply(pendingEvents.removeFirst(), state)
            }
        }
        return object : TransportConnection {
            override val frames: Flow<String> = state.framesFlow
            override suspend fun send(frame: String): Unit = recordSend(frame, index)
            override suspend fun close(): Unit = recordClose(index)
        }
    }

    // ── Manual injection (server side of the wire) ─────────────────────────────────────────

    /** Yield one server JSON text frame into the latest connection (or queue for the next connect). */
    public fun emit(frame: String) {
        deliver(ServerEvent.Frame(frame))
    }

    /** Terminate the latest connection's stream with [error] (the "flow throws" half of the contract). */
    public fun emitError(error: Throwable) {
        deliver(ServerEvent.Failure(error))
    }

    /** Finish the latest connection's stream cleanly (the "flow completes normally" half). */
    public fun finishFrames() {
        deliver(ServerEvent.Finish)
    }

    // ── Internals (all callers hold, or take, the monitor) ─────────────────────────────────

    private fun deliver(event: ServerEvent) {
        synchronized(lock) {
            val live = connections.lastOrNull()?.takeUnless { it.isTerminated }
            if (live == null) {
                pendingEvents.addLast(event)
            } else {
                apply(event, live)
            }
        }
    }

    private fun apply(event: ServerEvent, state: ConnectionState) {
        when (event) {
            is ServerEvent.Frame -> state.channel.trySend(event.frame)
            is ServerEvent.Failure -> {
                state.isTerminated = true
                state.channel.close(event.error)
            }
            ServerEvent.Finish -> {
                state.isTerminated = true
                state.channel.close()
            }
        }
    }

    private fun recordSend(frame: String, index: Int) {
        synchronized(lock) {
            val state = connections[index]
            if (state.isTerminated) throw FakeTransportError.SendAfterClose
            state.sentFrames.add(frame)
        }
    }

    private fun recordClose(index: Int) {
        synchronized(lock) {
            closeCount++
            val state = connections[index]
            if (state.isTerminated) return
            state.isTerminated = true
            state.channel.close()
        }
    }
}
