package wang.yaojia.webterm.terminalview

import kotlinx.coroutines.ExperimentalCoroutinesApi
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.wire.ClientMessage

/**
 * S1 GATE (plan §5 S1 / §6.1) — proves the Termux terminal-emulator seam is drivable HEADLESS: a
 * [RemoteTerminalSession] instantiates a `TerminalEmulator` with NO subprocess / NO JNI, we append
 * canned WS-shaped ANSI bytes, and read the expected cells back out of `TerminalBuffer`. It also proves
 * the OUTBOUND seam: emulator-originated bytes (a DSR reply) flow back through `engineSend`, and typed
 * input maps to `Input(...)`. If this fails, A16 is BLOCKED (→ §6.8 fallback).
 */
@OptIn(ExperimentalCoroutinesApi::class)
class RemoteTerminalSessionSeamTest {

    // Canned WS-shaped bytes: ESC[0m (soft reset) · ESC[32m (green) · "hello" · ESC[0m — the shape a
    // ring replay begins with. \u001b is the ESC byte.
    private val cannedAnsi = "\u001b[0m\u001b[32mhello\u001b[0m"

    @Test
    fun `canned ANSI bytes render into the TerminalBuffer`() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val session = RemoteTerminalSession(
            engineSend = {},
            appendDispatcher = dispatcher,
            mainDispatcher = dispatcher,
        )
        session.bind {} // bind so output appends live rather than queueing

        session.feedRemote(cannedAnsi.toByteArray(Charsets.UTF_8))
        advanceUntilIdle()

        val text = session.screenTextForTest().trim()
        assertTrue(text.contains("hello"), "emulator did not render 'hello'; got: '$text'")
        assertEquals(5, session.emulator.cursorCol, "cursor should advance past 'hello'")

        session.close()
        advanceUntilIdle()
    }

    @Test
    fun `emulator-originated reply (DSR) flows out through engineSend`() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val sent = mutableListOf<ClientMessage>()
        val session = RemoteTerminalSession(
            engineSend = { sent += it },
            appendDispatcher = dispatcher,
            mainDispatcher = dispatcher,
        )
        session.bind {}

        // ESC[6n = Device Status Report (cursor position). The emulator answers ESC[<row>;<col>R via
        // TerminalOutput.write → engineSend(Input) — the DA/DSR path §6.1 calls out.
        session.feedRemote("\u001b[6n".toByteArray(Charsets.UTF_8))
        advanceUntilIdle()

        val reply = sent.filterIsInstance<ClientMessage.Input>().firstOrNull { it.data.contains('R') }
        assertTrue(reply != null, "expected a DSR reply Input containing 'R'; sent: $sent")

        session.close()
        advanceUntilIdle()
    }

    @Test
    fun `a command that throws is survived and later output still renders`() = runTest {
        // FIX 2: a malformed / hostile escape byte that makes the emulator throw must NOT kill the
        // single confined consumer and permanently freeze all further output. Here the injected onBell
        // throws on the first invocation (standing in for the emulator choking on a hostile byte): the
        // BEL byte 0x07 makes the emulator call onBell synchronously mid-append on the confined thread.
        val dispatcher = StandardTestDispatcher(testScheduler)
        var bellCount = 0
        val session = RemoteTerminalSession(
            engineSend = {},
            onBell = {
                bellCount++
                if (bellCount == 1) throw RuntimeException("hostile-byte boom")
            },
            appendDispatcher = dispatcher,
            mainDispatcher = dispatcher,
        )
        session.bind {}

        // This feed throws while being processed on the confined thread.
        session.feedRemote("\u0007".toByteArray(Charsets.UTF_8))
        advanceUntilIdle()

        // The consumer survived — a subsequent feed still appends and renders.
        session.feedRemote("OK".toByteArray(Charsets.UTF_8))
        advanceUntilIdle()

        assertTrue(
            session.screenTextForTest().trim().contains("OK"),
            "consumer must survive a throwing command and keep rendering; got: '${session.screenTextForTest().trim()}'",
        )

        session.close()
        advanceUntilIdle()
    }

    @Test
    fun `writeInput maps verbatim to Input`() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val sent = mutableListOf<ClientMessage>()
        val session = RemoteTerminalSession(
            engineSend = { sent += it },
            appendDispatcher = dispatcher,
            mainDispatcher = dispatcher,
        )

        session.writeInput("ls -la\r")

        assertEquals(listOf<ClientMessage>(ClientMessage.Input("ls -la\r")), sent)
        session.close()
        advanceUntilIdle()
    }
}
