package wang.yaojia.webterm.transport

import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.SocketPolicy
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import wang.yaojia.webterm.wire.HttpMethod
import wang.yaojia.webterm.wire.HttpRequest
import java.io.IOException

/**
 * A7 REST contract over MockWebServer: 2xx returns status+body; non-2xx is returned (not thrown);
 * a transport failure throws; the Origin split (guarded POST carries the caller's Origin verbatim,
 * a read-only GET carries none) is preserved byte-for-byte.
 */
class OkHttpHttpTransportTest {

    private lateinit var server: MockWebServer
    private lateinit var transport: OkHttpHttpTransport
    private val client = OkHttpClientFactory.create()

    @BeforeEach
    fun setUp() {
        server = MockWebServer()
        server.start()
        transport = OkHttpHttpTransport(client)
    }

    @AfterEach
    fun tearDown() {
        server.shutdown()
        client.dispatcher.executorService.shutdown()
        client.connectionPool.evictAll()
    }

    private fun url(path: String): String = server.url(path).toString()

    @Test
    fun returnsStatusAndBodyForA2xxResponse() = runBlocking {
        // Arrange
        server.enqueue(MockResponse().setResponseCode(200).setBody("[]"))

        // Act
        val response = transport.send(HttpRequest(HttpMethod.GET, url("/live-sessions")))

        // Assert
        assertEquals(200, response.status)
        assertEquals("[]", String(response.body, Charsets.UTF_8))
    }

    @Test
    fun returnsNon2xxStatusWithoutThrowing() = runBlocking {
        // Arrange: a 403 is meaningful to the caller (Origin guard), never an exception.
        server.enqueue(MockResponse().setResponseCode(403).setBody("forbidden"))

        // Act
        val response = transport.send(
            HttpRequest(
                method = HttpMethod.POST,
                url = url("/hook/decision"),
                headers = mapOf("Origin" to "http://host:3000", "Content-Type" to "application/json"),
                body = """{"x":1}""".toByteArray(Charsets.UTF_8),
            ),
        )

        // Assert
        assertEquals(403, response.status)
        assertEquals("forbidden", String(response.body, Charsets.UTF_8))
    }

    @Test
    fun guardedPostCarriesCallerOriginAndBodyVerbatim() = runBlocking {
        // Arrange
        server.enqueue(MockResponse().setResponseCode(204))
        val origin = "http://192.168.1.5:3000"
        val payload = """{"sessionId":"s","decision":"allow","token":"t"}"""

        // Act
        transport.send(
            HttpRequest(
                method = HttpMethod.POST,
                url = url("/hook/decision"),
                headers = mapOf("Origin" to origin, "Content-Type" to "application/json"),
                body = payload.toByteArray(Charsets.UTF_8),
            ),
        )

        // Assert: the server saw the exact Origin, Content-Type, method and body.
        val recorded = server.takeRequest()
        assertEquals("POST", recorded.method)
        assertEquals(origin, recorded.getHeader("Origin"))
        assertEquals("application/json", recorded.getHeader("Content-Type"))
        assertEquals(payload, recorded.body.readUtf8())
    }

    @Test
    fun readOnlyGetCarriesNoOriginHeader() = runBlocking {
        // Arrange
        server.enqueue(MockResponse().setResponseCode(200).setBody("{}"))

        // Act
        transport.send(HttpRequest(HttpMethod.GET, url("/config/ui")))

        // Assert: the CSWSH split — read-only GET must NOT carry Origin.
        val recorded = server.takeRequest()
        assertEquals("GET", recorded.method)
        assertNull(recorded.getHeader("Origin"))
    }

    @Test
    fun throwsOnTransportLevelFailure() {
        // Arrange: the server drops the socket at connect → an IOException, not a returned status.
        server.enqueue(MockResponse().apply { socketPolicy = SocketPolicy.DISCONNECT_AT_START })

        // Act + Assert
        assertThrows<IOException> {
            runBlocking { transport.send(HttpRequest(HttpMethod.GET, url("/live-sessions"))) }
        }
    }
}
