package wang.yaojia.webterm.api.routes

import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.api.models.GitWriteOutcome
import wang.yaojia.webterm.api.models.PrAvailability
import wang.yaojia.webterm.testsupport.FakeHttpTransport
import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wire.HttpMethod

/**
 * Status-code → outcome mapping for the W5 git surface (plan Phase A.5): PR 200/400/404; log
 * decode + errors; each guarded write 200→Ok, 403→Rejected(body.error), 409→Rejected, 429→
 * RateLimited. Also asserts the transport RECEIVED an Origin on writes and NOT on reads.
 */
class ApiClientGitTest {
    private companion object {
        const val BASE = "http://h:3000"
    }

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

    private suspend fun errorOf(block: suspend () -> Unit): Throwable? = runCatching { block() }.exceptionOrNull()

    // ── PR (RO; degrade lives in the 200 body, not the status) ───────────────────────────────

    @Test
    fun `projectPr decodes a 200 degrade body and maps 400 404`() = runTest {
        transport.queueSuccess(url = "$BASE/projects/pr?path=%2Fr", body = """{"availability":"not-installed"}""".toByteArray())
        assertEquals(PrAvailability.NOT_INSTALLED, client.projectPr("/r").availability)

        transport.queueSuccess(url = "$BASE/projects/pr?path=%2Fr", status = 400)
        assertEquals(ApiClientError.ProjectPathInvalid, errorOf { client.projectPr("/r") })

        transport.queueSuccess(url = "$BASE/projects/pr?path=%2Fr", status = 404)
        assertEquals(ApiClientError.ProjectNotFound, errorOf { client.projectPr("/r") })

        // Empty path is rejected before any I/O.
        assertEquals(ApiClientError.ProjectPathInvalid, errorOf { client.projectPr("") })
    }

    @Test
    fun `projectPr never treats a garbled 200 body as an error (degrades to ERROR)`() = runTest {
        transport.queueSuccess(url = "$BASE/projects/pr?path=%2Fr", body = "not json".toByteArray())
        assertEquals(PrAvailability.ERROR, client.projectPr("/r").availability)
    }

    // ── log ──────────────────────────────────────────────────────────────────────────────────

    @Test
    fun `projectLog decodes 200 and maps 404 and 500`() = runTest {
        transport.queueSuccess(
            url = "$BASE/projects/log?path=%2Fr",
            body = """{"commits":[{"hash":"h","at":1,"subject":"s"}],"truncated":false}""".toByteArray(),
        )
        assertEquals(1, client.projectLog("/r").commits.size)

        transport.queueSuccess(url = "$BASE/projects/log?path=%2Fr", status = 404)
        assertEquals(ApiClientError.ProjectNotFound, errorOf { client.projectLog("/r") })

        transport.queueSuccess(url = "$BASE/projects/log?path=%2Fr", status = 500)
        assertEquals(ApiClientError.GitLogUnavailable, errorOf { client.projectLog("/r") })
    }

    // ── guarded writes: outcome mapping ──────────────────────────────────────────────────────

    @Test
    fun `a guarded write 200 yields Ok with the decoded payload`() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/commit", body = """{"ok":true,"commit":"abc"}""".toByteArray())
        val outcome = client.gitCommit("/r", "msg")
        assertTrue(outcome is GitWriteOutcome.Ok)
        assertEquals("abc", (outcome as GitWriteOutcome.Ok).payload.commit)
        // The write stamped an Origin.
        assertTrue(transport.recordedRequests.last().headers.containsKey(HeaderName.ORIGIN))
    }

    @Test
    fun `403 disabled and 409 both surface Rejected with the safe error string`() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/projects/worktree",
            status = 403, body = """{"error":"Worktree creation is disabled."}""".toByteArray(),
        )
        val disabled = client.createWorktree("/r", "b", null)
        assertEquals(GitWriteOutcome.Rejected(403, "Worktree creation is disabled."), disabled)

        transport.queueSuccess(
            method = HttpMethod.DELETE, url = "$BASE/projects/worktree",
            status = 409, body = """{"error":"Worktree has uncommitted changes; force required."}""".toByteArray(),
        )
        val dirty = client.removeWorktree("/r", "/r/x", false)
        assertEquals(GitWriteOutcome.Rejected(409, "Worktree has uncommitted changes; force required."), dirty)
    }

    @Test
    fun `429 yields RateLimited and never auto-retries`() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/push", status = 429, body = """{"error":"Too many requests."}""".toByteArray())
        assertEquals(GitWriteOutcome.RateLimited, client.gitPush("/r"))
        assertEquals(1, transport.recordedRequests.size) // exactly one attempt
    }

    @Test
    fun `a stage 200 decodes staged and count and threads the files body`() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/projects/git/stage", body = """{"ok":true,"staged":true,"count":2}""".toByteArray())
        val outcome = client.gitStage("/r", listOf("a", "b"), stage = true)
        assertTrue(outcome is GitWriteOutcome.Ok)
        assertEquals(2, (outcome as GitWriteOutcome.Ok).payload.count)
        assertEquals("""{"path":"/r","files":["a","b"],"stage":true}""", transport.recordedRequests.last().body?.decodeToString())
    }
}
