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.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
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 铁律) for the W5 git surface: the two NEW reads
 * (`/projects/pr`, `/projects/log`) carry **no** Origin; the six writes (worktree×3, git×3) carry a
 * byte-equal Origin and a JSON body — including a `DELETE /projects/worktree` that carries a body
 * (the highest-risk integration gotcha). A route reclassified read↔write turns this red.
 */
class GitRouteShapeTest {
    private companion object {
        const val BASE = "http://192.168.1.5:3000"
        const val ORIGIN = "http://192.168.1.5:3000"
    }

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

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

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

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

    // ── reads: no Origin, correct verb + strict-encoded query ────────────────────────────────

    @Test
    fun `projectPr is a read-only GET with a strict-encoded path and no Origin`() = runTest {
        val path = "/home/me/my repo/a+b&c"
        val url = "$BASE/projects/pr?path=%2Fhome%2Fme%2Fmy%20repo%2Fa%2Bb%26c"
        transport.queueSuccess(url = url, body = """{"availability":"no-pr"}""".toByteArray())

        client.projectPr(path)

        val r = last()
        assertEquals(HttpMethod.GET, r.method)
        assertEquals(url, r.url)
        assertReadOnly(r)
    }

    @Test
    fun `projectLog omits n when null and appends a clamped n when set`() = runTest {
        transport.queueSuccess(url = "$BASE/projects/log?path=%2Fp", body = """{"commits":[],"truncated":false}""".toByteArray())
        client.projectLog("/p", n = null)
        assertEquals("$BASE/projects/log?path=%2Fp", last().url)
        assertReadOnly(last())

        // n above GIT_LOG_MAX (50) clamps to 50; below 1 clamps to 1.
        transport.queueSuccess(url = "$BASE/projects/log?path=%2Fp&n=50", body = """{"commits":[],"truncated":false}""".toByteArray())
        client.projectLog("/p", n = 999)
        assertEquals("$BASE/projects/log?path=%2Fp&n=50", last().url)

        transport.queueSuccess(url = "$BASE/projects/log?path=%2Fp&n=1", body = """{"commits":[],"truncated":false}""".toByteArray())
        client.projectLog("/p", n = 0)
        assertEquals("$BASE/projects/log?path=%2Fp&n=1", last().url)
    }

    // ── writes: Origin stamped, correct verb, JSON body ──────────────────────────────────────

    @Test
    fun `createWorktree is a guarded POST with a path-branch-base body`() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/worktree", body = """{"ok":true}""".toByteArray())
        client.createWorktree("/repo", "feat/x", base = "main")

        val r = last()
        assertEquals(HttpMethod.POST, r.method)
        assertEquals("$BASE/projects/worktree", r.url)
        assertGuarded(r)
        assertEquals(ContentType.JSON, r.headers[HeaderName.CONTENT_TYPE])
        assertEquals("""{"path":"/repo","branch":"feat/x","base":"main"}""", r.body?.decodeToString())
    }

    @Test
    fun `createWorktree omits base when null`() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/worktree", body = """{"ok":true}""".toByteArray())
        client.createWorktree("/repo", "feat/x", base = null)
        assertEquals("""{"path":"/repo","branch":"feat/x"}""", last().body?.decodeToString())
    }

    @Test
    fun `removeWorktree is a guarded DELETE that CARRIES a JSON body`() = runTest {
        transport.queueSuccess(method = HttpMethod.DELETE, url = "$BASE/projects/worktree", body = """{"ok":true}""".toByteArray())
        client.removeWorktree("/repo", "/repo-worktrees/x", force = true)

        val r = last()
        assertEquals(HttpMethod.DELETE, r.method)
        assertEquals("$BASE/projects/worktree", r.url)
        assertGuarded(r)
        assertNotNull(r.body, "DELETE /projects/worktree MUST carry a request body")
        assertEquals("""{"path":"/repo","worktreePath":"/repo-worktrees/x","force":true}""", r.body?.decodeToString())
    }

    @Test
    fun `pruneWorktrees is a guarded POST with a path body`() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/worktree/prune", body = """{"ok":true,"pruned":[]}""".toByteArray())
        client.pruneWorktrees("/repo")

        val r = last()
        assertEquals(HttpMethod.POST, r.method)
        assertEquals("$BASE/projects/worktree/prune", r.url)
        assertGuarded(r)
        assertEquals("""{"path":"/repo"}""", r.body?.decodeToString())
    }

    @Test
    fun `gitStage commit push are guarded POSTs with exact bodies`() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/stage", body = """{"ok":true}""".toByteArray())
        client.gitStage("/repo", listOf("a.kt", "b.kt"), stage = true)
        assertEquals("$BASE/projects/git/stage", last().url)
        assertGuarded(last())
        assertEquals("""{"path":"/repo","files":["a.kt","b.kt"],"stage":true}""", last().body?.decodeToString())

        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/commit", body = """{"ok":true,"commit":"x"}""".toByteArray())
        client.gitCommit("/repo", "a message")
        assertEquals("""{"path":"/repo","message":"a message"}""", last().body?.decodeToString())
        assertGuarded(last())

        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/push", body = """{"ok":true}""".toByteArray())
        client.gitPush("/repo")
        assertEquals("$BASE/projects/git/push", last().url)
        assertEquals("""{"path":"/repo"}""", last().body?.decodeToString())
        assertGuarded(last())
    }

    @Test
    fun `every guarded write carries Origin and every read does not (batch invariant)`() = runTest {
        transport.queueSuccess(url = "$BASE/projects/pr?path=%2Fr", body = """{"availability":"ok"}""".toByteArray())
        transport.queueSuccess(url = "$BASE/projects/log?path=%2Fr", body = """{"commits":[],"truncated":false}""".toByteArray())
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/worktree", body = """{"ok":true}""".toByteArray())
        transport.queueSuccess(method = HttpMethod.DELETE, url = "$BASE/projects/worktree", body = """{"ok":true}""".toByteArray())
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/worktree/prune", body = """{"ok":true}""".toByteArray())
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/stage", body = """{"ok":true}""".toByteArray())
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/commit", body = """{"ok":true}""".toByteArray())
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/push", body = """{"ok":true}""".toByteArray())

        client.projectPr("/r"); client.projectLog("/r", null)
        assertReadOnly(transport.recordedRequests[0])
        assertReadOnly(transport.recordedRequests[1])

        client.createWorktree("/r", "b", null)
        client.removeWorktree("/r", "/r/x", false)
        client.pruneWorktrees("/r")
        client.gitStage("/r", listOf("f"), true)
        client.gitCommit("/r", "m")
        client.gitPush("/r")
        assertTrue(transport.recordedRequests.drop(2).all { it.headers.containsKey(HeaderName.ORIGIN) })
    }
}
