package wang.yaojia.webterm.wire

import org.junit.jupiter.api.Assertions.assertDoesNotThrow
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import kotlin.random.Random

/**
 * A4 decode vectors: [MessageCodec.decodeServer] maps an UNTRUSTED server text frame to a
 * [ServerMessage] or null (drop). Transcribed from the iOS `ServerVectorTests` + the server's
 * own `test/protocol.test.ts`. It must never throw, tolerate unknown keys, treat wrong-typed
 * OPTIONAL fields as absent, and drop on any missing/wrong-typed REQUIRED field.
 *
 * Control characters in expected VALUES are built with `Char(code)` (never raw source bytes);
 * JSON escapes inside frame literals use `\\uXXXX`.
 */
class MessageCodecDecodeServerTest {
    private val validUuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
    private val esc = Char(0x1B).toString()

    // ─── attached ────────────────────────────────────────────────────────────────

    @Test
    fun `decodes attached and normalizes an uppercase uuid to lowercase`() {
        // Arrange / Act / Assert
        assertEquals(
            ServerMessage.Attached(validUuid),
            MessageCodec.decodeServer("""{"type":"attached","sessionId":"$validUuid"}"""),
        )
        assertEquals(
            ServerMessage.Attached(validUuid),
            MessageCodec.decodeServer("""{"type":"attached","sessionId":"F47AC10B-58CC-4372-A567-0E02B2C3D479"}"""),
        )
    }

    // ─── output ──────────────────────────────────────────────────────────────────

    @Test
    fun `decodes output with ansi bytes verbatim into data`() {
        // Arrange / Act
        val decoded = MessageCodec.decodeServer("""{"type":"output","data":"\u001b[1;32mHello\u001b[0m"}""")

        // Assert
        assertEquals(ServerMessage.Output(esc + "[1;32mHello" + esc + "[0m"), decoded)
    }

    @Test
    fun `ignores unknown top-level keys on a server frame`() {
        // Arrange / Act / Assert — ignoreUnknownKeys tolerance
        assertEquals(
            ServerMessage.Output("hi"),
            MessageCodec.decodeServer("""{"type":"output","data":"hi","extra":"ignored"}"""),
        )
    }

    // ─── exit ────────────────────────────────────────────────────────────────────

    @Test
    fun `decodes exit with code only and with code plus reason`() {
        // Arrange / Act / Assert
        assertEquals(ServerMessage.Exit(0, null), MessageCodec.decodeServer("""{"type":"exit","code":0}"""))

        val spawnFail = MessageCodec.decodeServer("""{"type":"exit","code":-1,"reason":"spawn failed: /bin/badshell"}""")
        assertEquals(ServerMessage.Exit(WireConstants.SPAWN_FAILED_EXIT_CODE, "spawn failed: /bin/badshell"), spawnFail)
    }

    // ─── status ──────────────────────────────────────────────────────────────────

    @Test
    fun `decodes every ClaudeStatus with safe defaults for detail pending gate`() {
        for (raw in listOf("working", "waiting", "idle", "unknown", "stuck")) {
            // Act
            val decoded = MessageCodec.decodeServer("""{"type":"status","status":"$raw"}""")

            // Assert
            val expected = ServerMessage.Status(ClaudeStatus.fromWire(raw)!!, null, false, null)
            assertEquals(expected, decoded, "status=$raw")
        }
    }

    @Test
    fun `decodes status carrying detail pending and a tool or plan gate`() {
        assertEquals(
            ServerMessage.Status(ClaudeStatus.WAITING, "Bash", true, GateKind.TOOL),
            MessageCodec.decodeServer(
                """{"type":"status","status":"waiting","detail":"Bash","pending":true,"gate":"tool"}""",
            ),
        )
        assertEquals(
            ServerMessage.Status(ClaudeStatus.WAITING, null, true, GateKind.PLAN),
            MessageCodec.decodeServer("""{"type":"status","status":"waiting","pending":true,"gate":"plan"}"""),
        )
    }

    @Test
    fun `tolerates an unknown gate value and a non-boolean pending`() {
        // Unknown gate -> null but the pending signal is kept (frame not dropped).
        assertEquals(
            ServerMessage.Status(ClaudeStatus.WAITING, null, true, null),
            MessageCodec.decodeServer("""{"type":"status","status":"waiting","pending":true,"gate":"alien"}"""),
        )
        // Non-boolean pending -> safe default false.
        assertEquals(
            ServerMessage.Status(ClaudeStatus.WORKING, null, false, null),
            MessageCodec.decodeServer("""{"type":"status","status":"working","pending":"yes"}"""),
        )
    }

    // ─── telemetry ───────────────────────────────────────────────────────────────

    @Test
    fun `decodes a full telemetry sample field by field`() {
        // Arrange
        val frame =
            "{\"type\":\"telemetry\",\"telemetry\":{" +
                "\"contextUsedPct\":42.5,\"costUsd\":1.23," +
                "\"linesAdded\":10,\"linesRemoved\":2," +
                "\"model\":\"Fable 5\",\"effort\":\"high\"," +
                "\"pr\":{\"number\":7,\"url\":\"https://github.com/x/y/pull/7\",\"reviewState\":\"APPROVED\"}," +
                "\"rate\":{\"fiveHourPct\":12.5,\"sevenDayPct\":33},\"at\":1751600000000}}"

        // Act
        val decoded = MessageCodec.decodeServer(frame)

        // Assert
        val expected = ServerMessage.Telemetry(
            StatusTelemetry(
                contextUsedPct = 42.5,
                costUsd = 1.23,
                linesAdded = 10,
                linesRemoved = 2,
                model = "Fable 5",
                effort = "high",
                pr = PrInfo(7, "https://github.com/x/y/pull/7", "APPROVED"),
                rate = RateInfo(12.5, 33.0),
                at = 1_751_600_000_000L,
            ),
        )
        assertEquals(expected, decoded)
    }

    @Test
    fun `decodes telemetry with only the required at field`() {
        assertEquals(
            ServerMessage.Telemetry(StatusTelemetry(at = 123L)),
            MessageCodec.decodeServer("""{"type":"telemetry","telemetry":{"at":123}}"""),
        )
    }

    @Test
    fun `tolerates wrong-typed and unknown optional telemetry fields`() {
        // costUsd string + pr non-object are treated as absent; the frame is kept.
        assertEquals(
            ServerMessage.Telemetry(StatusTelemetry(at = 5L)),
            MessageCodec.decodeServer("""{"type":"telemetry","telemetry":{"at":5,"costUsd":"lots","pr":42}}"""),
        )
        // Unknown nested key ignored.
        assertEquals(
            ServerMessage.Telemetry(StatusTelemetry(at = 9L)),
            MessageCodec.decodeServer("""{"type":"telemetry","telemetry":{"at":9,"mystery":true}}"""),
        )
    }

    // ─── invalid frames -> null (never throws) ───────────────────────────────────

    @Test
    fun `drops every malformed server frame as null`() {
        val invalid = listOf(
            "not json at all", "", "null", "[]", "42", "true",
            """{"type":"ping"}""", """{"data":"hello"}""", """{"type":42}""",
            // attached: non-uuid / v1 / missing / wrong-typed (M7)
            """{"type":"attached","sessionId":"abc123"}""",
            """{"type":"attached","sessionId":"550e8400-e29b-11d4-a716-446655440000"}""",
            """{"type":"attached"}""",
            """{"type":"attached","sessionId":42}""",
            // output: data missing / wrong-typed
            """{"type":"output"}""",
            """{"type":"output","data":42}""",
            // exit: code missing / string / non-integer
            """{"type":"exit"}""",
            """{"type":"exit","code":"0"}""",
            """{"type":"exit","code":1.5}""",
            // status: status missing / unknown value / wrong-typed
            """{"type":"status"}""",
            """{"type":"status","status":"sleeping"}""",
            """{"type":"status","status":42}""",
            // telemetry: key missing / non-object / at missing / at wrong-typed
            """{"type":"telemetry"}""",
            """{"type":"telemetry","telemetry":"x"}""",
            """{"type":"telemetry","telemetry":{"costUsd":1}}""",
            """{"type":"telemetry","telemetry":{"at":"now"}}""",
        )

        for (frame in invalid) {
            assertNull(MessageCodec.decodeServer(frame), "should drop: $frame")
        }
    }

    @Test
    fun `treats client frame types as non-server frames`() {
        val clientFrames = listOf(
            ClientMessage.Attach(null),
            ClientMessage.Input("x"),
            ClientMessage.Resize(80, 24),
            ClientMessage.Approve(ApproveMode.PLAN),
            ClientMessage.Reject,
        )
        for (m in clientFrames) {
            assertNull(MessageCodec.decodeServer(MessageCodec.encode(m)), "client frame not a server frame: $m")
        }
    }

    // ─── fuzz: never throws on hostile input ─────────────────────────────────────

    @Test
    fun `survives random byte fuzz without throwing`() {
        val rng = Random(0xDEADBEEF)
        repeat(300) {
            val len = rng.nextInt(0, 81)
            val bytes = ByteArray(len) { rng.nextInt(0, 256).toByte() }
            val text = String(bytes, Charsets.UTF_8)
            assertDoesNotThrow { MessageCodec.decodeServer(text) }
        }
    }

    @Test
    fun `survives single-byte mutations of valid frames without throwing`() {
        val rng = Random(0xFEEDFACE)
        val seeds = listOf(
            """{"type":"attached","sessionId":"$validUuid"}""",
            """{"type":"output","data":"\u001b[1;32mHello\u001b[0m"}""",
            """{"type":"exit","code":-1,"reason":"spawn failed"}""",
            """{"type":"status","status":"waiting","pending":true,"gate":"plan"}""",
            """{"type":"telemetry","telemetry":{"at":1751600000000,"costUsd":1.5}}""",
        )
        for (seed in seeds) {
            repeat(40) {
                val bytes = seed.toByteArray(Charsets.UTF_8)
                bytes[rng.nextInt(0, bytes.size)] = rng.nextInt(0, 256).toByte()
                assertDoesNotThrow { MessageCodec.decodeServer(String(bytes, Charsets.UTF_8)) }
            }
        }
    }
}
