package wang.yaojia.webterm.wiring

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.session.Connection
import wang.yaojia.webterm.session.ConnectionState
import wang.yaojia.webterm.session.Output
import wang.yaojia.webterm.session.SessionEngine
import wang.yaojia.webterm.session.SessionEvent
import wang.yaojia.webterm.testsupport.FakeTermTransport
import wang.yaojia.webterm.wire.HostEndpoint

/**
 * FIX 1 — control events are MULTI-SUBSCRIBABLE at the controller surface. The reconnect banner AND the
 * gate presenter (AND, later, the A28 timeline) each call [TerminalSessionController.controlEvents]; each
 * MUST receive the FULL control-event stream. A single shared `Channel.receiveAsFlow()` mailbox would
 * SPLIT events (each event reaches only one of two competing collectors) — this test proves the fix:
 * every call mints its OWN [EventBus] mailbox, so N collectors all see every control event, in order.
 *
 * Driven through a REAL [SessionEngine] over a [FakeTermTransport] (so the whole
 * controlEvents→EventBus→engine pump is exercised), on a virtual-time scope (mirrors the holder test).
 */
@OptIn(ExperimentalCoroutinesApi::class)
class TerminalSessionControllerTest {

    private fun endpoint(): HostEndpoint = HostEndpoint.fromBaseUrl("http://localhost:3000")!!

    @Test
    fun `two controlEvents collectors both receive every control event (no split)`() = runTest {
        val scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob())
        val transport = FakeTermTransport()
        val bus = EventBus(decodeDispatcher = StandardTestDispatcher(testScheduler))
        val engine = SessionEngine(transport = transport, endpoint = endpoint(), scope = scope)
        val controller = DefaultTerminalSessionController(engine, bus, scope)

        // TWO independent control consumers — each its OWN mailbox, registered EAGERLY before start().
        val gotA = mutableListOf<SessionEvent>()
        val gotB = mutableListOf<SessionEvent>()
        val a = controller.controlEvents()
        val b = controller.controlEvents()
        scope.launch { a.collect { gotA += it } }
        scope.launch { b.collect { gotB += it } }

        // start() wires the engine→bus pump, THEN the engine: Connecting → (connect) → Connected.
        controller.start()
        advanceUntilIdle()
        // Detach: Closed (server PTY survives). Gives a 3-event control stream.
        controller.close()
        advanceUntilIdle()

        // NO SPLIT: BOTH collectors saw the SAME, ordered control stream — not one event each.
        assertEquals(gotA, gotB)
        assertTrue(gotA.size >= 2, "expected multiple control events, got $gotA")
        assertTrue(gotA.contains(Connection(ConnectionState.Connected)), "missing Connected in $gotA")
        assertTrue(gotA.contains(Connection(ConnectionState.Closed)), "missing Closed in $gotA")
        // Control mailboxes must NOT carry Output (that is the single [output] stream's job, FIX 4).
        assertTrue(gotA.none { it is Output }, "control stream leaked Output frames: $gotA")

        scope.cancel()
    }

    @Test
    fun `a third controlEvents subscriber also receives the full stream (A28 extensibility)`() = runTest {
        // Adding a THIRD consumer needs no change to the pump — just another controlEvents() mailbox,
        // still lossless and un-split.
        val scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob())
        val transport = FakeTermTransport()
        val bus = EventBus(decodeDispatcher = StandardTestDispatcher(testScheduler))
        val engine = SessionEngine(transport = transport, endpoint = endpoint(), scope = scope)
        val controller = DefaultTerminalSessionController(engine, bus, scope)

        val streams = List(3) { controller.controlEvents() }
        val received = List(3) { mutableListOf<SessionEvent>() }
        streams.forEachIndexed { i, flow -> scope.launch { flow.collect { received[i] += it } } }

        controller.start()
        advanceUntilIdle()
        controller.close()
        advanceUntilIdle()

        assertEquals(received[0], received[1])
        assertEquals(received[1], received[2])
        assertTrue(received[0].contains(Connection(ConnectionState.Connected)))

        scope.cancel()
    }
}
