package wang.yaojia.webterm.api.enroll

import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
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.testsupport.FakeHttpTransport
import wang.yaojia.webterm.wire.HttpMethod
import wang.yaojia.webterm.wire.HttpRequest
import java.time.Instant
import java.util.Base64

/**
 * B4 · DeviceEnrollmentClient request-building + response-mapping against the pinned login/enroll
 * contract, driven by the shared `FakeHttpTransport` (no network). Mirrors the iOS
 * `DeviceEnrollmentClientTests`, extended with the login step.
 */
class DeviceEnrollmentClientTest {
    private companion object {
        const val BASE = "https://cp.terminal.yaojia.wang"
        const val BEARER = "device-enroll-token-abc"
    }

    private val transport = FakeHttpTransport()
    private val client = DeviceEnrollmentClient(BASE, transport)

    private fun bodyObject(request: HttpRequest): JsonObject =
        Json.parseToJsonElement(request.body!!.decodeToString()) as JsonObject

    private fun enrollResponse(
        deviceId: String = "dev-1",
        cert: ByteArray = byteArrayOf(0x30, 0x01, 0x02),
        caChain: List<ByteArray> = listOf(byteArrayOf(0x30, 0xAA.toByte())),
        notBefore: String = "2026-07-08T00:00:00.000Z",
        notAfter: String = "2026-10-06T00:00:00.000Z",
        renewAfter: String = "2026-09-05T00:00:00.000Z",
    ): ByteArray {
        val b64 = Base64.getEncoder()
        val chainJson = caChain.joinToString(",") { "\"${b64.encodeToString(it)}\"" }
        return """
            {"deviceId":"$deviceId","cert":"${b64.encodeToString(cert)}","caChain":[$chainJson],
             "notBefore":"$notBefore","notAfter":"$notAfter","renewAfter":"$renewAfter"}
        """.trimIndent().toByteArray()
    }

    // ── login ────────────────────────────────────────────────────────────────────────────────

    @Test
    fun loginPostsPasswordAndMapsThe201Bearer() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST,
            url = "$BASE/auth/login",
            status = 201,
            body = """{"enrollToken":"tok-xyz","accountId":"acct-1","expiresIn":600}""".toByteArray(),
        )

        val result = client.login("hunter2")

        val request = transport.recordedRequests.single()
        assertEquals(HttpMethod.POST, request.method)
        assertEquals("$BASE/auth/login", request.url)
        assertEquals("application/json", request.headers["Content-Type"])
        assertNull(request.headers["Authorization"], "login carries no bearer")
        assertEquals("hunter2", bodyObject(request)["password"]!!.jsonPrimitive.content)

        assertEquals("tok-xyz", result.enrollToken)
        assertEquals("acct-1", result.accountId)
        assertEquals(600L, result.expiresInSeconds)
    }

    @Test
    fun loginRejectsAnEmptyPasswordBeforeAnyNetworkIo() = runTest {
        val error = runCatching { client.login("") }.exceptionOrNull()
        assertEquals(DeviceEnrollmentError.InvalidRequest, error)
        assertTrue(transport.recordedRequests.isEmpty(), "must not hit the network for an empty password")
    }

    @Test
    fun loginSurfacesA401AsHttpWithTheServerCode() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST,
            url = "$BASE/auth/login",
            status = 401,
            body = """{"error":"rejected"}""".toByteArray(),
        )
        val error = runCatching { client.login("wrong") }.exceptionOrNull()
        assertEquals(DeviceEnrollmentError.Http(401, "rejected"), error)
    }

    // ── enroll ───────────────────────────────────────────────────────────────────────────────

    @Test
    fun enrollBuildsABearerAuthenticatedPostWithTheContractBody() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/device/enroll", status = 201, body = enrollResponse(),
        )
        val csr = byteArrayOf(0xDE.toByte(), 0xAD.toByte(), 0xBE.toByte(), 0xEF.toByte())

        client.enroll(BEARER, csr, subdomain = "alice", deviceName = "Alice Pixel")

        val request = transport.recordedRequests.single()
        assertEquals(HttpMethod.POST, request.method)
        assertEquals("$BASE/device/enroll", request.url)
        assertEquals("Bearer $BEARER", request.headers["Authorization"])
        assertEquals("application/json", request.headers["Content-Type"])

        val obj = bodyObject(request)
        assertEquals(Base64.getEncoder().encodeToString(csr), obj["csr"]!!.jsonPrimitive.content)
        assertEquals("ec-p256", obj["keyAlg"]!!.jsonPrimitive.content)
        assertEquals("alice", obj["subdomain"]!!.jsonPrimitive.content)
        assertEquals("Alice Pixel", obj["deviceName"]!!.jsonPrimitive.content)
        assertFalse(obj.containsKey("attestation"), "attestation is omitted when not provided")
    }

    @Test
    fun enrollForwardsAttestationWhenProvided() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/device/enroll", status = 201, body = enrollResponse())
        client.enroll(BEARER, byteArrayOf(0x01), "a", "d", attestation = "attest-blob")
        assertEquals("attest-blob", bodyObject(transport.recordedRequests.single())["attestation"]!!.jsonPrimitive.content)
    }

    @Test
    fun enrollMapsA201IntoATypedResult() = runTest {
        val cert = byteArrayOf(0x30, 0x82.toByte(), 0x01, 0x23)
        val ca = byteArrayOf(0x30, 0x82.toByte(), 0x02, 0x00)
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/device/enroll", status = 201,
            body = enrollResponse(deviceId = "dev-xyz", cert = cert, caChain = listOf(ca)),
        )

        val result = client.enroll(BEARER, byteArrayOf(0x01), "alice", "Pixel")

        assertEquals("dev-xyz", result.deviceId)
        assertArrayEquals(cert, result.certificate)
        assertEquals(1, result.caChain.size)
        assertArrayEquals(ca, result.caChain.single())
        assertTrue(result.renewAfter!!.isBefore(result.notAfter))
    }

    @Test
    fun enrollRejectsEmptyRequiredFieldsBeforeAnyNetworkIo() = runTest {
        assertEquals(DeviceEnrollmentError.InvalidRequest, runCatching { client.enroll("", byteArrayOf(1), "a", "d") }.exceptionOrNull())
        assertEquals(DeviceEnrollmentError.InvalidRequest, runCatching { client.enroll(BEARER, ByteArray(0), "a", "d") }.exceptionOrNull())
        assertEquals(DeviceEnrollmentError.InvalidRequest, runCatching { client.enroll(BEARER, byteArrayOf(1), "", "d") }.exceptionOrNull())
        assertEquals(DeviceEnrollmentError.InvalidRequest, runCatching { client.enroll(BEARER, byteArrayOf(1), "a", "") }.exceptionOrNull())
        assertTrue(transport.recordedRequests.isEmpty())
    }

    @Test
    fun enrollSurfacesA403SubdomainNotOwnedWithTheServerCode() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/device/enroll", status = 403,
            body = """{"error":"rejected"}""".toByteArray(),
        )
        assertEquals(
            DeviceEnrollmentError.Http(403, "rejected"),
            runCatching { client.enroll(BEARER, byteArrayOf(1), "bob", "d") }.exceptionOrNull(),
        )
    }

    @Test
    fun enrollSurfacesA429RateLimited() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/device/enroll", status = 429,
            body = """{"error":"rate_limited"}""".toByteArray(),
        )
        assertEquals(
            DeviceEnrollmentError.Http(429, "rate_limited"),
            runCatching { client.enroll(BEARER, byteArrayOf(1), "a", "d") }.exceptionOrNull(),
        )
    }

    @Test
    fun enrollThrowsMalformedResponseOnANonJson201Body() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/device/enroll", status = 201, body = "not json".toByteArray())
        assertEquals(
            DeviceEnrollmentError.MalformedResponse,
            runCatching { client.enroll(BEARER, byteArrayOf(1), "a", "d") }.exceptionOrNull(),
        )
    }

    @Test
    fun enrollThrowsMalformedResponseWhenTheCertIsNotValidBase64() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/device/enroll", status = 201,
            body = """{"deviceId":"d","cert":"@@not-base64@@","caChain":[]}""".toByteArray(),
        )
        assertEquals(
            DeviceEnrollmentError.MalformedResponse,
            runCatching { client.enroll(BEARER, byteArrayOf(1), "a", "d") }.exceptionOrNull(),
        )
    }

    @Test
    fun enrollDegradesAbsentDatesToNull() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/device/enroll", status = 201,
            body = """{"deviceId":"d","cert":"MAEC","caChain":[]}""".toByteArray(),
        )
        val result = client.enroll(BEARER, byteArrayOf(1), "a", "d")
        assertNull(result.notAfter)
        assertNull(result.renewAfter)
        assertFalse(result.isRenewalDue(Instant.parse("2030-01-01T00:00:00Z")), "absent renewAfter never triggers")
    }

    // ── renew (silent rotation seam — mTLS-only, NO bearer) ─────────────────────────────────────

    @Test
    fun renewTargetsDeviceIdRenewWithTheMinimalBodyAndNoAuthorizationHeader() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/device/dev-9/renew", status = 201, body = enrollResponse(deviceId = "dev-9"),
        )
        val csr = byteArrayOf(0x02)

        // Production renew passes NO bearer — the endpoint authenticates by the current device cert (mTLS).
        val result = client.renew("dev-9", csr)

        val request = transport.recordedRequests.single()
        assertEquals("$BASE/device/dev-9/renew", request.url)
        assertNull(request.headers["Authorization"], "renew authenticates by mTLS — it must send NO Authorization header")
        val obj = bodyObject(request)
        assertEquals(Base64.getEncoder().encodeToString(csr), obj["csr"]!!.jsonPrimitive.content)
        // The server's /device/:id/renew authenticates by the presented mTLS device cert and its body
        // schema is `{ csr }` ONLY (.strict()) — any enroll-only extra (keyAlg/subdomain/deviceName)
        // is rejected. The renew wire body must therefore carry the single `csr` key and nothing else.
        assertEquals(setOf("csr"), obj.keys, "renew body is {csr}-only — no keyAlg/subdomain/deviceName")
        assertFalse(obj.containsKey("keyAlg"), "renew must not send the enroll-only keyAlg field")
        assertFalse(obj.containsKey("subdomain"), "renew body carries no subdomain/deviceName")
        assertEquals("dev-9", result.deviceId)
    }

    @Test
    fun renewForwardsAnExplicitBearerWhenTheOptionalSeamIsUsed() = runTest {
        transport.queueSuccess(
            method = HttpMethod.POST, url = "$BASE/device/dev-9/renew", status = 201, body = enrollResponse(deviceId = "dev-9"),
        )

        // The bearer is optional/absent by default; when a caller DOES pass one it rides as a header.
        client.renew("dev-9", byteArrayOf(0x02), bearerToken = BEARER)

        assertEquals("Bearer $BEARER", transport.recordedRequests.single().headers["Authorization"])
    }

    @Test
    fun renewRejectsEmptyDeviceIdOrCsrBeforeAnyNetworkIo() = runTest {
        assertEquals(DeviceEnrollmentError.InvalidRequest, runCatching { client.renew("", byteArrayOf(1)) }.exceptionOrNull())
        assertEquals(DeviceEnrollmentError.InvalidRequest, runCatching { client.renew("d", ByteArray(0)) }.exceptionOrNull())
        assertTrue(transport.recordedRequests.isEmpty())
    }

    // ── isRenewalDue seam ──────────────────────────────────────────────────────────────────────

    @Test
    fun isRenewalDueFlipsAtRenewAfter() = runTest {
        transport.queueSuccess(method = HttpMethod.POST, url = "$BASE/device/enroll", status = 201, body = enrollResponse())
        val result = client.enroll(BEARER, byteArrayOf(1), "a", "d")
        assertFalse(result.isRenewalDue(Instant.parse("2026-09-04T00:00:00Z")))
        assertTrue(result.isRenewalDue(Instant.parse("2026-09-06T00:00:00Z")))
    }

    private fun assertArrayEquals(expected: ByteArray, actual: ByteArray) =
        org.junit.jupiter.api.Assertions.assertArrayEquals(expected, actual)
}
