package wang.yaojia.webterm.wire

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

/**
 * A4 boundary-validation vectors: [MessageCodec.decodeClient] is the Android mirror of the
 * server's `parseClientMessage` accept/reject decision, and the exact inverse of
 * [MessageCodec.encode]. Accept/reject vectors transcribed from `test/protocol.test.ts` /
 * the iOS `ServerVectorTests` server-mirror; the round-trip block proves encode/decode symmetry.
 */
class MessageCodecDecodeClientTest {
    private val validUuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479"

    // ─── accept: valid client frames decode to the right message ─────────────────

    @Test
    fun `decodes attach with explicit null and with a uuid`() {
        assertEquals(
            ClientMessage.Attach(null),
            MessageCodec.decodeClient("""{"type":"attach","sessionId":null}"""),
        )
        assertEquals(
            ClientMessage.Attach(validUuid),
            MessageCodec.decodeClient("""{"type":"attach","sessionId":"$validUuid"}"""),
        )
    }

    @Test
    fun `decodes attach with an absolute cwd and normalizes an uppercase sessionId`() {
        assertEquals(
            ClientMessage.Attach(null, "/Users/dev/proj"),
            MessageCodec.decodeClient("""{"type":"attach","sessionId":null,"cwd":"/Users/dev/proj"}"""),
        )
        assertEquals(
            ClientMessage.Attach(validUuid),
            MessageCodec.decodeClient("""{"type":"attach","sessionId":"F47AC10B-58CC-4372-A567-0E02B2C3D479"}"""),
        )
    }

    @Test
    fun `decodes input resize approve and reject, ignoring unknown keys`() {
        assertEquals(ClientMessage.Input("ls -la"), MessageCodec.decodeClient("""{"type":"input","data":"ls -la"}"""))
        assertEquals(ClientMessage.Resize(1, 1), MessageCodec.decodeClient("""{"type":"resize","cols":1,"rows":1}"""))
        assertEquals(
            ClientMessage.Resize(1000, 1000),
            MessageCodec.decodeClient("""{"type":"resize","cols":1000,"rows":1000}"""),
        )
        assertEquals(
            ClientMessage.Resize(80, 40),
            MessageCodec.decodeClient("""{"type":"resize","cols":80,"rows":40,"extra":"ignored"}"""),
        )
        assertEquals(ClientMessage.Approve(), MessageCodec.decodeClient("""{"type":"approve"}"""))
        assertEquals(ClientMessage.Reject, MessageCodec.decodeClient("""{"type":"reject"}"""))
    }

    @Test
    fun `recovers a known approve mode and tolerates an unknown one as absent`() {
        assertEquals(
            ClientMessage.Approve(ApproveMode.ACCEPT_EDITS),
            MessageCodec.decodeClient("""{"type":"approve","mode":"acceptEdits"}"""),
        )
        assertEquals(
            ClientMessage.Approve(ApproveMode.DEFAULT),
            MessageCodec.decodeClient("""{"type":"approve","mode":"default"}"""),
        )
        assertEquals(
            ClientMessage.Approve(null),
            MessageCodec.decodeClient("""{"type":"approve","mode":"alien"}"""),
        )
    }

    // ─── reject: invalid client frames -> null ───────────────────────────────────

    @Test
    fun `rejects every invalid client frame as null`() {
        val invalid = listOf(
            "not json at all", "", "null", "[]", "42",
            """{"type":"ping"}""", """{"data":"hello"}""", """{"type":42}""",
            """{"type":"resize","cols":0,"rows":40}""",
            """{"type":"resize","cols":1001,"rows":40}""",
            """{"type":"resize","cols":80,"rows":0}""",
            """{"type":"resize","cols":80,"rows":1001}""",
            """{"type":"resize","cols":80.5,"rows":40}""",
            """{"type":"resize","cols":80,"rows":24.9}""",
            """{"type":"resize","cols":"80","rows":40}""",
            """{"type":"resize","rows":40}""",
            """{"type":"resize","cols":80}""",
            """{"type":"input","data":42}""",
            """{"type":"input","data":null}""",
            """{"type":"input"}""",
            """{"type":"input","data":{}}""",
            """{"type":"attach","sessionId":"abc123"}""",
            """{"type":"attach","sessionId":""}""",
            """{"type":"attach","sessionId":42}""",
            """{"type":"attach"}""",
            """{"type":"attach","sessionId":null,"cwd":"relative"}""",
            """{"type":"attach","sessionId":null,"cwd":42}""",
            // the removed 'blur' type must be rejected
            """{"type":"blur"}""",
        )
        for (frame in invalid) {
            assertNull(MessageCodec.decodeClient(frame), "should reject: $frame")
        }
    }

    // ─── round-trip: decodeClient(encode(m)) == m ────────────────────────────────

    @Test
    fun `encode then decodeClient is the identity for canonical messages`() {
        val messages = listOf(
            ClientMessage.Attach(null),
            ClientMessage.Attach(validUuid),
            ClientMessage.Attach(null, "/Users/dev/proj"),
            ClientMessage.Attach(validUuid, "/srv/app"),
            ClientMessage.Input("ls -la"),
            // control bytes + quote/backslash survive the round trip
            ClientMessage.Input("hi" + Char(0x0D) + Char(0x1B) + "[A"),
            ClientMessage.Input("a\"b\\c"),
            ClientMessage.Resize(80, 24),
            ClientMessage.Resize(1, 1),
            ClientMessage.Resize(1000, 1000),
            ClientMessage.Approve(null),
            ClientMessage.Approve(ApproveMode.DEFAULT),
            ClientMessage.Approve(ApproveMode.ACCEPT_EDITS),
            ClientMessage.Approve(ApproveMode.PLAN),
            ClientMessage.Approve(ApproveMode.AUTO),
            ClientMessage.Reject,
        )
        for (m in messages) {
            assertEquals(m, MessageCodec.decodeClient(MessageCodec.encode(m)), "round-trip: $m")
        }
    }
}
