package wang.yaojia.webterm.testsupport

import kotlinx.coroutines.flow.toList
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.wire.HostEndpoint

/** Ports iOS `FakeTransportTests`, plus the pending-flush / reconnect / termination behaviors. */
class FakeTermTransportTest {
    private fun endpoint(): HostEndpoint =
        requireNotNull(HostEndpoint.fromBaseUrl("http://192.168.1.5:3000"))

    @Test
    fun scriptsFailuresDeliversFramesAndRecordsTraffic() = runTest {
        // Arrange
        val transport = FakeTermTransport()
        val endpoint = endpoint()
        val attachFrame = """{"type":"attach","sessionId":null}"""

        // Act + Assert: a scripted failure makes the next connect throw it.
        transport.scriptConnectFailure()
        val failure = runCatching { transport.connect(endpoint) }.exceptionOrNull()
        assertEquals(FakeTransportError.ScriptedConnectFailure, failure)

        // Act: connect for real, send one frame, inject one frame, close cleanly.
        val connection = transport.connect(endpoint)
        connection.send(attachFrame)
        transport.emit("server-frame-1")
        transport.finishFrames()

        val received = connection.frames.toList()
        connection.close()

        // Assert: injected frames arrived in order and ended with a clean finish.
        assertEquals(listOf("server-frame-1"), received)
        // Assert: the double recorded everything the client did.
        assertEquals(listOf(attachFrame), transport.sentFrames)
        assertEquals(1, transport.closeCallCount)
        assertEquals(listOf(endpoint, endpoint), transport.connectAttempts)
    }

    @Test
    fun emitErrorTerminatesStreamWithThatError() = runTest {
        val transport = FakeTermTransport()
        val connection = transport.connect(endpoint())
        val boom = IllegalStateException("boom")

        transport.emit("f1")
        transport.emitError(boom)

        // The buffered frame is delivered, then the stream throws the transport error.
        val thrown = runCatching { connection.frames.toList() }.exceptionOrNull()
        assertTrue(thrown is IllegalStateException)
        assertEquals("boom", thrown?.message)
    }

    @Test
    fun sendAfterCloseThrows() = runTest {
        val transport = FakeTermTransport()
        val connection = transport.connect(endpoint())
        connection.close()

        val thrown = runCatching { connection.send("late") }.exceptionOrNull()
        assertEquals(FakeTransportError.SendAfterClose, thrown)
        assertEquals(1, transport.closeCallCount)
    }

    @Test
    fun queuesEventsBeforeConnectAndFlushesInOrderOnNextConnect() = runTest {
        val transport = FakeTermTransport()
        // Script server behavior up front, before any connection exists (attach->replay pattern).
        transport.emit("replay-1")
        transport.emit("replay-2")
        transport.finishFrames()

        val connection = transport.connect(endpoint())
        assertEquals(listOf("replay-1", "replay-2"), connection.frames.toList())
    }

    @Test
    fun reattachFrameLandsOnSecondConnection() = runTest {
        val transport = FakeTermTransport()
        val endpoint = endpoint()

        val c0 = transport.connect(endpoint)
        c0.send("attach-0")
        transport.finishFrames() // connection 0 terminated (server close)

        val c1 = transport.connect(endpoint)
        c1.send("attach-1")

        assertEquals(listOf(listOf("attach-0"), listOf("attach-1")), transport.sentFramesByConnection)
        assertEquals(listOf("attach-0", "attach-1"), transport.sentFrames)
        assertEquals(listOf(endpoint, endpoint), transport.connectAttempts)
    }
}
