package wang.yaojia.webterm.viewmodels

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
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.enroll.DeviceEnrollmentError
import wang.yaojia.webterm.clienttls.CertificateSummary
import java.security.KeyStoreException
import java.time.Instant
import java.time.ZoneId

/**
 * [EnrollmentViewModel] / [validControlPlaneUrl] / [classifyEnroll] (B4) — the JVM-tested zero-`.p12`
 * enrollment core, mirroring iOS `EnrollmentViewModelTests`. The enroll flow itself is covered headlessly
 * in `DeviceEnrollerTest`; these cover the VM's boundary validation, success bookkeeping (summary +
 * password-clear), and the error→[EnrollError] mapping — including that the password never lingers after
 * an attempt. The keystore / network / Compose shell is device-QA (plan §7); THIS is the pure core.
 */
@OptIn(ExperimentalCoroutinesApi::class)
class EnrollmentViewModelTest {

    /** Records enroll invocations and replays a scripted result/throwable (mirrors iOS EnrollScript). */
    private class EnrollScript(private val result: Result<CertificateSummary>) {
        val calls = mutableListOf<Call>()
        data class Call(val password: String, val subdomain: String, val deviceName: String, val url: String)

        suspend fun run(password: String, subdomain: String, deviceName: String, url: String): CertificateSummary {
            calls.add(Call(password, subdomain, deviceName, url))
            return result.getOrThrow()
        }
    }

    private val fixedNow = Instant.parse("2026-01-01T00:00:00Z")
    private val utc = ZoneId.of("UTC")

    private fun summary(
        subject: String? = "alice-pixel",
        issuer: String? = "webterm-device-ca",
        notAfter: Instant? = Instant.parse("2026-10-06T00:00:00Z"),
    ) = CertificateSummary(subjectCommonName = subject, issuerCommonName = issuer, notAfter = notAfter)

    private fun newVm(
        script: EnrollScript,
        installed: CertificateSummary? = null,
        controlPlaneUrl: String = "https://cp.terminal.yaojia.wang",
        subdomain: String = "alice",
        deviceName: String = "Pixel 8",
    ): EnrollmentViewModel {
        val vm = EnrollmentViewModel(
            enrollOperation = { p, s, d, u -> script.run(p, s, d, u) },
            loadSummary = { installed },
            defaultControlPlaneUrl = controlPlaneUrl,
            defaultDeviceName = deviceName,
            zone = utc,
            now = { fixedNow },
        )
        vm.onSubdomainChange(subdomain)
        return vm
    }

    // ── Success ─────────────────────────────────────────────────────────────────────────────────────

    @Test
    fun `a successful enroll sets the summary, flags success and clears the password`() =
        runTest(UnconfinedTestDispatcher()) {
            val script = EnrollScript(Result.success(summary(subject = "alice-pixel")))
            val vm = newVm(script)
            vm.bind(backgroundScope)
            vm.onPasswordChange("operator-secret")

            vm.enroll()

            val state = vm.uiState.value
            assertEquals(1, script.calls.size)
            assertEquals("operator-secret", script.calls.single().password, "the entered password reaches the flow")
            assertEquals("alice-pixel", state.summary?.subjectCommonName)
            assertTrue(state.didSucceed)
            assertNull(state.error)
            assertEquals("", state.password, "the password must never linger after a successful enroll")
            assertEquals(EnrollPhase.IDLE, state.phase)
        }

    // ── Boundary validation (no network) ──────────────────────────────────────────────────────────────

    @Test
    fun `a non-https control-plane URL is rejected before any network`() =
        runTest(UnconfinedTestDispatcher()) {
            val script = EnrollScript(Result.success(summary()))
            val vm = newVm(script, controlPlaneUrl = "http://cp.terminal.yaojia.wang")
            vm.bind(backgroundScope)
            vm.onPasswordChange("pw")

            vm.enroll()

            assertTrue(script.calls.isEmpty(), "an insecure URL must never hit the network")
            assertEquals(EnrollError.INVALID_URL, vm.uiState.value.error)
            assertFalse(vm.uiState.value.didSucceed)
        }

    @Test
    fun `missing fields are rejected before any network`() = runTest(UnconfinedTestDispatcher()) {
        val script = EnrollScript(Result.success(summary()))
        val vm = newVm(script, subdomain = "   ") // whitespace-only subdomain
        vm.bind(backgroundScope)
        vm.onPasswordChange("pw")

        vm.enroll()

        assertTrue(script.calls.isEmpty())
        assertEquals(EnrollError.MISSING_FIELDS, vm.uiState.value.error)
    }

    // ── Error → copy mapping (password still cleared) ─────────────────────────────────────────────────

    @Test
    fun `a 403 subdomain-not-owned maps to actionable copy and clears the password`() =
        runTest(UnconfinedTestDispatcher()) {
            val script = EnrollScript(Result.failure(DeviceEnrollmentError.Http(403, "rejected")))
            val vm = newVm(script)
            vm.bind(backgroundScope)
            vm.onPasswordChange("operator-secret")

            vm.enroll()

            val state = vm.uiState.value
            assertEquals(EnrollError.SUBDOMAIN_NOT_OWNED, state.error)
            assertFalse(state.didSucceed)
            assertEquals("", state.password, "the password is cleared even after a failed enroll")
        }

    @Test
    fun `a 401 login maps to the bad-credential copy`() = runTest(UnconfinedTestDispatcher()) {
        val script = EnrollScript(Result.failure(DeviceEnrollmentError.Http(401, "rejected")))
        val vm = newVm(script)
        vm.bind(backgroundScope)
        vm.onPasswordChange("wrong")

        vm.enroll()

        assertEquals(EnrollError.BAD_CREDENTIAL, vm.uiState.value.error)
    }

    @Test
    fun `a hardware keystore fault maps to the keygen copy`() = runTest(UnconfinedTestDispatcher()) {
        val script = EnrollScript(Result.failure(KeyStoreException("no StrongBox / TEE")))
        val vm = newVm(script)
        vm.bind(backgroundScope)
        vm.onPasswordChange("operator-secret")

        vm.enroll()

        assertEquals(EnrollError.KEYGEN, vm.uiState.value.error)
    }

    // ── canEnroll gating ──────────────────────────────────────────────────────────────────────────────

    @Test
    fun `canEnroll requires every field including a non-empty password`() =
        runTest(UnconfinedTestDispatcher()) {
            val vm = newVm(EnrollScript(Result.success(summary())))
            vm.bind(backgroundScope)

            assertFalse(vm.uiState.value.canEnroll, "password empty → disabled")
            vm.onPasswordChange("pw")
            assertTrue(vm.uiState.value.canEnroll)
            vm.onSubdomainChange("   ")
            assertFalse(vm.uiState.value.canEnroll, "whitespace-only subdomain → disabled")
        }

    @Test
    fun `clearError dismisses a surfaced error`() = runTest(UnconfinedTestDispatcher()) {
        val vm = newVm(newFailing())
        vm.bind(backgroundScope)
        vm.onPasswordChange("pw")
        vm.enroll()
        assertEquals(EnrollError.REJECTED, vm.uiState.value.error)

        vm.clearError()

        assertNull(vm.uiState.value.error)
    }

    private fun newFailing() = EnrollScript(Result.failure(DeviceEnrollmentError.Http(400, "rejected")))

    // ── Pure helpers ──────────────────────────────────────────────────────────────────────────────────

    @Test
    fun `validControlPlaneUrl accepts https with a host and rejects everything else`() {
        assertEquals("https://cp.terminal.yaojia.wang", validControlPlaneUrl("  https://cp.terminal.yaojia.wang  "))
        assertNull(validControlPlaneUrl("http://cp.terminal.yaojia.wang"), "http is rejected")
        assertNull(validControlPlaneUrl("https://"), "no host is rejected")
        assertNull(validControlPlaneUrl("not a url"), "unparseable is rejected")
        assertNull(validControlPlaneUrl(""), "empty is rejected")
    }

    @Test
    fun `classifyEnroll maps each throwable to its coarse EnrollError`() {
        assertEquals(EnrollError.BAD_CREDENTIAL, classifyEnroll(DeviceEnrollmentError.Http(401, null)))
        assertEquals(EnrollError.SUBDOMAIN_NOT_OWNED, classifyEnroll(DeviceEnrollmentError.Http(403, null)))
        assertEquals(EnrollError.RATE_LIMITED, classifyEnroll(DeviceEnrollmentError.Http(429, null)))
        assertEquals(EnrollError.REJECTED, classifyEnroll(DeviceEnrollmentError.Http(400, null)))
        assertEquals(EnrollError.ENROLL_FAILED, classifyEnroll(DeviceEnrollmentError.Http(500, null)))
        assertEquals(EnrollError.SERVER, classifyEnroll(DeviceEnrollmentError.MalformedResponse))
        assertEquals(EnrollError.ENROLL_FAILED, classifyEnroll(DeviceEnrollmentError.InvalidRequest))
        assertEquals(EnrollError.KEYGEN, classifyEnroll(KeyStoreException("x")))
        assertEquals(EnrollError.UNKNOWN, classifyEnroll(IllegalStateException("x")))
    }
}
