package wang.yaojia.webterm.api.routes

import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.api.models.HookDecision
import wang.yaojia.webterm.api.models.UiPrefs
import wang.yaojia.webterm.testsupport.FakeHttpTransport
import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wire.HttpMethod
import wang.yaojia.webterm.wire.HttpRequest
import java.util.UUID

/**
 * Request-shape + Origin-iff-guarded (plan §4.3 铁律) across all 12 routes. Verifies method, URL,
 * the presence/absence of the `Origin` header, and the exact JSON body for the mutating routes.
 */
class ApiRouteShapeTest {
    private companion object {
        const val BASE = "http://192.168.1.5:3000"
        const val ORIGIN = "http://192.168.1.5:3000"
        val ID: UUID = UUID.fromString("11111111-2222-4333-8444-555555555555")
        const val ID_STR = "11111111-2222-4333-8444-555555555555"
    }

    private val transport = FakeHttpTransport()
    private val client = ApiClient(HostEndpoint.fromBaseUrl(BASE)!!, transport)

    private fun lastRequest(): HttpRequest = transport.recordedRequests.last()

    private fun assertGuarded(request: HttpRequest) =
        assertEquals(ORIGIN, request.headers[HeaderName.ORIGIN], "guarded route must stamp byte-equal Origin")

    private fun assertReadOnly(request: HttpRequest) =
        assertFalse(request.headers.containsKey(HeaderName.ORIGIN), "read-only route must NOT stamp Origin")

    // ── read-only routes: correct verb+url, NO Origin ───────────────────────────────────────

    @Test
    fun liveSessionsIsPlainReadOnlyGet() = runTest {
        transport.queueSuccess(url = "$BASE/live-sessions", body = "[]".toByteArray())
        client.liveSessions()

        val request = lastRequest()
        assertEquals(HttpMethod.GET, request.method)
        assertEquals("$BASE/live-sessions", request.url)
        assertReadOnly(request)
        assertNull(request.body)
    }

    @Test
    fun previewEventsUiConfigProjectsPrefsAreReadOnlyGets() = runTest {
        transport.queueSuccess(
            url = "$BASE/live-sessions/$ID_STR/preview",
            body = """{"id":"$ID_STR","cols":80,"rows":24,"data":""}""".toByteArray(),
        )
        transport.queueSuccess(url = "$BASE/live-sessions/$ID_STR/events", body = "[]".toByteArray())
        transport.queueSuccess(url = "$BASE/config/ui", body = """{"allowAutoMode":true}""".toByteArray())
        transport.queueSuccess(url = "$BASE/projects", body = "[]".toByteArray())
        transport.queueSuccess(url = "$BASE/prefs", body = "{}".toByteArray())

        client.preview(ID)
        client.events(ID)
        client.uiConfig()
        client.projects()
        client.prefs()

        val urls = transport.recordedRequests.map { it.url }
        assertEquals(
            listOf(
                "$BASE/live-sessions/$ID_STR/preview",
                "$BASE/live-sessions/$ID_STR/events",
                "$BASE/config/ui",
                "$BASE/projects",
                "$BASE/prefs",
            ),
            urls,
        )
        transport.recordedRequests.forEach {
            assertEquals(HttpMethod.GET, it.method)
            assertReadOnly(it)
        }
    }

    @Test
    fun projectDetailStrictlyPercentEncodesPathInTheQuery() = runTest {
        val path = "/home/me/my repo/a+b&c"
        transport.queueSuccess(
            url = "$BASE/projects/detail?path=%2Fhome%2Fme%2Fmy%20repo%2Fa%2Bb%26c",
            body = """{"name":"repo","path":"$path","isGit":true}""".toByteArray(),
        )

        client.projectDetail(path)

        val request = lastRequest()
        assertEquals(HttpMethod.GET, request.method)
        assertEquals("$BASE/projects/detail?path=%2Fhome%2Fme%2Fmy%20repo%2Fa%2Bb%26c", request.url)
        assertReadOnly(request)
    }

    // ── guarded routes: correct verb+url, Origin stamped, exact body ────────────────────────

    @Test
    fun killSessionIsGuardedDeleteWithNoBody() = runTest {
        transport.queueSuccess(method = HttpMethod.DELETE, url = "$BASE/live-sessions/$ID_STR", status = 204)
        client.killSession(ID)

        val request = lastRequest()
        assertEquals(HttpMethod.DELETE, request.method)
        assertEquals("$BASE/live-sessions/$ID_STR", request.url)
        assertGuarded(request)
        assertNull(request.body)
    }

    @Test
    fun hookDecisionIsGuardedPostWithExactBodyAndJsonContentType() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/hook/decision", status = 204)
        client.hookDecision(ID, HookDecision.ALLOW, "cap-tok-123")

        val request = lastRequest()
        assertEquals(HttpMethod.POST, request.method)
        assertEquals("$BASE/hook/decision", request.url)
        assertGuarded(request)
        assertEquals(ContentType.JSON, request.headers[HeaderName.CONTENT_TYPE])
        assertEquals(
            """{"sessionId":"$ID_STR","decision":"allow","token":"cap-tok-123"}""",
            request.body?.decodeToString(),
        )
    }

    @Test
    fun putPrefsIsGuardedPutEchoingTheFullBlob() = runTest {
        val body = """{"favourites":["/a"],"collapsed":{}}"""
        transport.queueSuccess(method = HttpMethod.PUT, url = "$BASE/prefs", body = body.toByteArray())

        client.putPrefs(UiPrefs.create(favourites = listOf("/a")))

        val request = lastRequest()
        assertEquals(HttpMethod.PUT, request.method)
        assertEquals("$BASE/prefs", request.url)
        assertGuarded(request)
        assertEquals(body, request.body?.decodeToString())
    }

    @Test
    fun fcmTokenRegisterAndUnregisterAreGuardedWithTokenBody() = runTest {
        val token = "fVoT9x-abc_DEF:api-901"
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/push/fcm-token", status = 204)
        transport.queueSuccess(method = HttpMethod.DELETE, url = "$BASE/push/fcm-token", status = 204)

        client.registerFcmToken(token)
        client.unregisterFcmToken(token)

        val post = transport.recordedRequests[0]
        val delete = transport.recordedRequests[1]
        assertEquals(HttpMethod.POST, post.method)
        assertEquals(HttpMethod.DELETE, delete.method)
        assertTrue(post.url.endsWith("/push/fcm-token"))
        assertGuarded(post)
        assertGuarded(delete)
        assertEquals("""{"token":"$token"}""", post.body?.decodeToString())
        assertEquals("""{"token":"$token"}""", delete.body?.decodeToString())
    }
}
