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

/**
 * The pendingOutput invariant (plan §6.2): output that arrives BEFORE the view binds (a ring replay can
 * land first) is queued in submission order and flushed the instant the view binds — nothing dropped,
 * nothing reordered, and the `ESC[0m` soft-reset replay prefix is passed through, never stripped.
 */
@OptIn(ExperimentalCoroutinesApi::class)
class PendingOutputTest {

    @Test
    fun `output before bind is queued then flushed in submission order`() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val session = RemoteTerminalSession(
            engineSend = {},
            appendDispatcher = dispatcher,
            mainDispatcher = dispatcher,
        )

        // Two chunks arrive BEFORE any view binds. The first carries the ESC[0m replay prefix.
        session.feedRemote("\u001b[0mAAA".toByteArray(Charsets.UTF_8))
        session.feedRemote("BBB".toByteArray(Charsets.UTF_8))
        advanceUntilIdle()

        // Nothing appended yet — both chunks are held.
        assertEquals(2, session.pendingChunkCountForTest(), "both chunks should be pending pre-bind")
        assertEquals("", session.screenTextForTest().trim(), "emulator must be untouched pre-bind")

        // Binding flushes in submission order: AAA then BBB, ESC[0m preserved (produces no glyphs).
        session.bind {}
        advanceUntilIdle()

        assertEquals(0, session.pendingChunkCountForTest(), "queue drained on bind")
        assertEquals("AAABBB", session.screenTextForTest().trim(), "flush order must be AAA then BBB")

        session.close()
        advanceUntilIdle()
    }

    @Test
    fun `emulator is published to the renderer only after pending output is in the buffer`() = runTest {
        // FIX 1 (bind-time data race): the emulator must NOT be handed to the stock renderer until the
        // initial pendingOutput flush has COMPLETED — otherwise the UI-thread draw can read
        // TerminalBuffer while the confined thread is still appending the ring replay.
        val dispatcher = StandardTestDispatcher(testScheduler)
        val session = RemoteTerminalSession(
            engineSend = {},
            appendDispatcher = dispatcher,
            mainDispatcher = dispatcher,
        )

        // A ring-replay chunk lands BEFORE any view binds — it is queued, buffer untouched.
        session.feedRemote("\u001b[0mHELLO".toByteArray(Charsets.UTF_8))
        advanceUntilIdle()
        assertEquals(1, session.pendingChunkCountForTest(), "chunk should be pending pre-bind")
        assertEquals("", session.screenTextForTest().trim(), "buffer must be untouched pre-bind")

        // publishEmulator stands in for `view.bindEmulator(emulator)` (exposing the buffer to the
        // renderer). Record what the buffer holds AT THE MOMENT it is published.
        var textAtPublish: String? = null
        var pendingAtPublish = -1
        session.bind(
            publishEmulator = {
                textAtPublish = session.screenTextForTest().trim()
                pendingAtPublish = session.pendingChunkCountForTest()
            },
        ) { }
        advanceUntilIdle()

        // The publish happened strictly AFTER the flush: the replay bytes are already in the buffer and
        // the queue is drained. Under the old synchronous publish this read "" on an empty buffer while
        // the confined thread was still flushing — the bind-moment single-writer-vs-UI-read race.
        assertEquals("HELLO", textAtPublish, "emulator must be published only after the flush completed")
        assertEquals(0, pendingAtPublish, "queue must be drained before the emulator is published")

        session.close()
        advanceUntilIdle()
    }

    @Test
    fun `screen-update sink fires on bind flush`() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val session = RemoteTerminalSession(
            engineSend = {},
            appendDispatcher = dispatcher,
            mainDispatcher = dispatcher,
        )
        var updates = 0

        session.feedRemote("hi".toByteArray(Charsets.UTF_8))
        advanceUntilIdle()
        session.bind { updates++ }
        advanceUntilIdle()

        assertTrue(updates >= 1, "binding a pending session must post at least one screen update")

        session.close()
        advanceUntilIdle()
    }
}
