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.HookDecision
import wang.yaojia.webterm.testsupport.FakeHttpTransport
import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wire.HttpMethod
import java.io.IOException
import java.util.UUID

/** Status-code → typed [ApiClientError] mapping per route, plus transport errors propagating raw. */
class ApiClientErrorMappingTest {
    private companion object {
        const val BASE = "http://h: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 suspend fun errorOf(block: suspend () -> Unit): Throwable? = runCatching { block() }.exceptionOrNull()

    @Test
    fun killSessionMapsNotFoundAndForbidden() = runTest {
        transport.queueSuccess(method = HttpMethod.DELETE, url = "$BASE/live-sessions/$ID_STR", status = 404)
        assertEquals(ApiClientError.SessionNotFound, errorOf { client.killSession(ID) })

        transport.queueSuccess(method = HttpMethod.DELETE, url = "$BASE/live-sessions/$ID_STR", status = 403)
        assertEquals(ApiClientError.Forbidden, errorOf { client.killSession(ID) })
    }

    @Test
    fun hookDecisionMapsForbiddenToRejectedAnd429ToRateLimited() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/hook/decision", status = 403)
        assertEquals(ApiClientError.DecisionRejected, errorOf { client.hookDecision(ID, HookDecision.DENY, "t") })

        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/hook/decision", status = 429)
        assertEquals(ApiClientError.RateLimited, errorOf { client.hookDecision(ID, HookDecision.DENY, "t") })
    }

    @Test
    fun projectDetailMapsEmptyPathAndAllServerErrorStatuses() = runTest {
        // Empty path is rejected BEFORE any I/O.
        assertEquals(ApiClientError.ProjectPathInvalid, errorOf { client.projectDetail("") })
        assertTrue(transport.recordedRequests.isEmpty(), "empty path must not hit the network")

        val url = "$BASE/projects/detail?path=%2Fp"
        transport.queueSuccess(url = url, status = 400)
        assertEquals(ApiClientError.ProjectPathInvalid, errorOf { client.projectDetail("/p") })
        transport.queueSuccess(url = url, status = 404)
        assertEquals(ApiClientError.ProjectNotFound, errorOf { client.projectDetail("/p") })
        transport.queueSuccess(url = url, status = 500)
        assertEquals(ApiClientError.ProjectDetailUnavailable, errorOf { client.projectDetail("/p") })
    }

    @Test
    fun putPrefsMapsForbiddenAndUnexpected() = runTest {
        transport.queueSuccess(method = HttpMethod.PUT, url = "$BASE/prefs", status = 403)
        assertEquals(ApiClientError.Forbidden, errorOf { client.putPrefs(wang.yaojia.webterm.api.models.UiPrefs.create()) })

        transport.queueSuccess(method = HttpMethod.PUT, url = "$BASE/prefs", status = 418)
        assertEquals(ApiClientError.UnexpectedStatus(418), errorOf { client.putPrefs(wang.yaojia.webterm.api.models.UiPrefs.create()) })
    }

    @Test
    fun readOnlyRoutesMapNonOkToUnexpectedOrSessionNotFound() = runTest {
        transport.queueSuccess(url = "$BASE/live-sessions", status = 500, body = "boom".toByteArray())
        assertEquals(ApiClientError.UnexpectedStatus(500), errorOf { client.liveSessions() })

        // preview/events/uiConfig go through requireOk: 404 → SessionNotFound.
        transport.queueSuccess(url = "$BASE/live-sessions/$ID_STR/preview", status = 404)
        assertEquals(ApiClientError.SessionNotFound, errorOf { client.preview(ID) })
    }

    @Test
    fun transportLevelErrorsPropagateUnwrapped() = runTest {
        transport.queueFailure(url = "$BASE/live-sessions", error = IOException("connection refused"))
        val error = errorOf { client.liveSessions() }
        assertTrue(error is IOException)
        assertEquals("connection refused", error?.message)
    }
}
