package wang.yaojia.webterm.wire

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Test

/**
 * A4 golden vectors: [MessageCodec.encode] must be BYTE-IDENTICAL to the web client /
 * server `JSON.stringify` for every [ClientMessage]. Expected strings transcribed from
 * `src/protocol.ts` / `public/terminal-session.ts` and cross-checked against the iOS
 * `CodecRoundtripTests` (same wire). Any drift here is a cross-implementation break.
 *
 * Control bytes are written as explicit `\u` escapes (never raw bytes) so the vectors stay
 * copy-safe.
 */
class MessageCodecEncodeTest {
    private val validUuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479"

    @Test
    fun `attach without sessionId still carries explicit sessionId null`() {
        // Arrange / Act
        val frame = MessageCodec.encode(ClientMessage.Attach(sessionId = null))

        // Assert
        assertEquals("""{"type":"attach","sessionId":null}""", frame)
    }

    @Test
    fun `attach with uuid serializes lowercased uuid string`() {
        // Arrange / Act
        val frame = MessageCodec.encode(ClientMessage.Attach(sessionId = validUuid))

        // Assert
        assertEquals("""{"type":"attach","sessionId":"$validUuid"}""", frame)
    }

    @Test
    fun `attach lowercases an uppercase uuid so the wire is canonical`() {
        // Arrange / Act
        val frame = MessageCodec.encode(
            ClientMessage.Attach(sessionId = "F47AC10B-58CC-4372-A567-0E02B2C3D479"),
        )

        // Assert
        assertEquals("""{"type":"attach","sessionId":"$validUuid"}""", frame)
    }

    @Test
    fun `attach cwd key appears only when present, after sessionId`() {
        // Arrange / Act
        val withCwd = MessageCodec.encode(ClientMessage.Attach(sessionId = null, cwd = "/Users/dev/proj"))
        val withoutCwd = MessageCodec.encode(ClientMessage.Attach(sessionId = null))

        // Assert
        assertEquals("""{"type":"attach","sessionId":null,"cwd":"/Users/dev/proj"}""", withCwd)
        assertFalse(withoutCwd.contains("cwd"))
    }

    @Test
    fun `input passes raw keyboard bytes through, escaping control bytes like JSON stringify`() {
        // Arrange — Esc / Up / ^C / CR / Tab / Shift+Tab verbatim vector (test/protocol.test.ts)
        val raw = "\u001B[A\u0003\r\t\u001B[Z"

        // Act
        val frame = MessageCodec.encode(ClientMessage.Input(raw))

        // Assert — ESC/^C -> \u001b / \u0003, CR -> \r, Tab -> \t
        assertEquals("{\"type\":\"input\",\"data\":\"\\u001b[A\\u0003\\r\\t\\u001b[Z\"}", frame)
    }

    @Test
    fun `input escapes quote and backslash and control bytes exactly like JSON stringify`() {
        // Arrange / Act
        val frame = MessageCodec.encode(ClientMessage.Input("hi\r\u001B[A\"\\"))

        // Assert — byte-identical to the JS client's JSON.stringify output
        assertEquals("{\"type\":\"input\",\"data\":\"hi\\r\\u001b[A\\\"\\\\\"}", frame)
    }

    @Test
    fun `input uses the JSON short escapes for backspace tab newline formfeed carriage-return`() {
        // Arrange / Act
        val frame = MessageCodec.encode(ClientMessage.Input("\u0008\u0009\u000A\u000C\u000D"))

        // Assert
        assertEquals("{\"type\":\"input\",\"data\":\"\\b\\t\\n\\f\\r\"}", frame)
    }

    @Test
    fun `input long-form-escapes the other C0 control bytes as lowercase hex`() {
        // Arrange — NUL / BEL / unit-separator have no short escape
        val frame = MessageCodec.encode(ClientMessage.Input("\u0000\u0007\u001F"))

        // Assert
        assertEquals("{\"type\":\"input\",\"data\":\"\\u0000\\u0007\\u001f\"}", frame)
    }

    @Test
    fun `input leaves ordinary and non-ascii characters unescaped`() {
        // Arrange / Act — '/' is NOT escaped by JSON.stringify; unicode >= 0x20 passes through
        val frame = MessageCodec.encode(ClientMessage.Input("a/b 日本語 é"))

        // Assert
        assertEquals("""{"type":"input","data":"a/b 日本語 é"}""", frame)
    }

    @Test
    fun `attach escapes a sessionId containing a quote like JSON stringify`() {
        // A canonical id is a UUID, but encode must stay TOTAL + byte-identical to JSON.stringify
        // for ANY string — defence against JSON injection via an unescaped id (the encodeAttach fix).
        val frame = MessageCodec.encode(ClientMessage.Attach(sessionId = "x\"y"))

        assertEquals("""{"type":"attach","sessionId":"x\"y"}""", frame)
    }

    @Test
    fun `attach escapes a sessionId containing a backslash like JSON stringify`() {
        val frame = MessageCodec.encode(ClientMessage.Attach(sessionId = "a\\b"))

        // JSON output is {"type":"attach","sessionId":"a\\b"}
        assertEquals("{\"type\":\"attach\",\"sessionId\":\"a\\\\b\"}", frame)
    }

    @Test
    fun `input escapes a lone high surrogate as lowercase u-escape like JSON stringify`() {
        // node: JSON.stringify("\uD800x") === "\"\\ud800x\"" (ES2019 well-formed JSON.stringify)
        val frame = MessageCodec.encode(ClientMessage.Input("\uD800x"))

        assertEquals("{\"type\":\"input\",\"data\":\"\\ud800x\"}", frame)
    }

    @Test
    fun `input escapes a lone low surrogate as lowercase u-escape`() {
        val frame = MessageCodec.encode(ClientMessage.Input("\uDC00"))

        assertEquals("{\"type\":\"input\",\"data\":\"\\udc00\"}", frame)
    }

    @Test
    fun `input keeps a valid surrogate pair verbatim like JSON stringify`() {
        // 😀 = U+1F600 = high D83D + low DE00; JSON.stringify emits the astral char unescaped.
        val frame = MessageCodec.encode(ClientMessage.Input("😀"))

        assertEquals("{\"type\":\"input\",\"data\":\"😀\"}", frame)
    }

    @Test
    fun `resize emits bare integer cols and rows in order`() {
        // Arrange / Act / Assert
        assertEquals("""{"type":"resize","cols":120,"rows":40}""", MessageCodec.encode(ClientMessage.Resize(120, 40)))
        assertEquals("""{"type":"resize","cols":1,"rows":1}""", MessageCodec.encode(ClientMessage.Resize(1, 1)))
        assertEquals(
            """{"type":"resize","cols":1000,"rows":1000}""",
            MessageCodec.encode(ClientMessage.Resize(1000, 1000)),
        )
    }

    @Test
    fun `approve without mode is a bare frame, reject is a bare frame`() {
        // Arrange / Act / Assert
        assertEquals("""{"type":"approve"}""", MessageCodec.encode(ClientMessage.Approve()))
        assertEquals("""{"type":"reject"}""", MessageCodec.encode(ClientMessage.Reject))
    }

    @Test
    fun `approve mode is a top-level key with the server whitelist spelling`() {
        // Arrange / Act / Assert — every ApproveMode wire value, mode at the top level
        assertEquals(
            """{"type":"approve","mode":"default"}""",
            MessageCodec.encode(ClientMessage.Approve(ApproveMode.DEFAULT)),
        )
        assertEquals(
            """{"type":"approve","mode":"acceptEdits"}""",
            MessageCodec.encode(ClientMessage.Approve(ApproveMode.ACCEPT_EDITS)),
        )
        assertEquals(
            """{"type":"approve","mode":"plan"}""",
            MessageCodec.encode(ClientMessage.Approve(ApproveMode.PLAN)),
        )
        assertEquals(
            """{"type":"approve","mode":"auto"}""",
            MessageCodec.encode(ClientMessage.Approve(ApproveMode.AUTO)),
        )
    }
}
