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.assertInstanceOf
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.api.pairing.PairingError
import wang.yaojia.webterm.api.pairing.PairingProbeResult
import wang.yaojia.webterm.hostregistry.InMemoryHostStore
import wang.yaojia.webterm.wire.HostEndpoint

/**
 * [PairingViewModel] (A19) — the JVM-tested pairing core: §5.4 warning-tier mapping, the
 * confirm-before-network gate, the public-host explicit-ack gate, and the tunnel-host cert-gate choke
 * point that retry cannot bypass. Camera/Compose/permission UI is device-QA (plan §7). Pure `runTest`
 * with an [UnconfinedTestDispatcher] so a probe launched into `backgroundScope` runs inline.
 */
@OptIn(ExperimentalCoroutinesApi::class)
class PairingViewModelTest {

    /** Records every probe call so "no probe before confirm / cert-gate refuses" is directly assertable. */
    private class FakeProber(var result: (HostEndpoint) -> PairingProbeResult) : PairingProber {
        val calls = mutableListOf<HostEndpoint>()
        override suspend fun probe(endpoint: HostEndpoint): PairingProbeResult {
            calls += endpoint
            return result(endpoint)
        }
    }

    private fun endpoint(url: String): HostEndpoint =
        requireNotNull(HostEndpoint.fromBaseUrl(url)) { "fixture URL must validate: $url" }

    private fun success(): (HostEndpoint) -> PairingProbeResult = { PairingProbeResult.Success(it) }

    private fun newVm(
        prober: PairingProber,
        hasCert: Boolean = false,
        store: InMemoryHostStore = InMemoryHostStore(),
    ) = PairingViewModel(
        hostStore = store,
        prober = prober,
        hasDeviceCert = { hasCert },
        newId = { "fixed-id" },
    )

    // ── §5.4 warning-tier mapping (each host class → tier + which needs explicit ack) ────────────────

    @Test
    fun `each host class maps to the correct warning tier`() {
        val table = mapOf(
            "http://localhost:3000" to WarningTier.LOOPBACK,
            "http://127.0.0.1:3000" to WarningTier.LOOPBACK,
            "http://10.0.0.5:3000" to WarningTier.PRIVATE_LAN,
            "http://192.168.1.10:3000" to WarningTier.PRIVATE_LAN,
            "http://172.16.9.9:3000" to WarningTier.PRIVATE_LAN,
            "http://mac.local:3000" to WarningTier.PRIVATE_LAN,
            "http://100.64.0.1:3000" to WarningTier.TAILSCALE,
            "https://box.ts.net" to WarningTier.TAILSCALE,
            "https://star.terminal.yaojia.wang" to WarningTier.TUNNEL,
            "https://example.org:3000" to WarningTier.PUBLIC,
            "https://8.8.8.8" to WarningTier.PUBLIC,
        )
        table.forEach { (url, expected) ->
            assertEquals(expected, PairingTiers.tierFor(endpoint(url)), "tier for $url")
        }
    }

    @Test
    fun `an unclassifiable host is public (fail-safe)`() {
        assertEquals(WarningTier.PUBLIC, PairingTiers.tierFor(endpoint("https://weird-unknown.internal")))
    }

    @Test
    fun `only a public host needs the explicit acknowledge and only the tunnel host is cert-gated`() {
        assertFalse(WarningTier.LOOPBACK.needsExplicitAck)
        assertFalse(WarningTier.PRIVATE_LAN.needsExplicitAck)
        assertFalse(WarningTier.TAILSCALE.needsExplicitAck)
        assertFalse(WarningTier.TUNNEL.needsExplicitAck)
        assertTrue(WarningTier.PUBLIC.needsExplicitAck)

        assertTrue(WarningTier.TUNNEL.isCertGated)
        assertFalse(WarningTier.PUBLIC.isCertGated)
        assertFalse(WarningTier.PRIVATE_LAN.isCertGated)
    }

    // ── Confirm-before-network gate ─────────────────────────────────────────────────────────────────

    @Test
    fun `a scanned URL is validated and shown for confirmation but never probed`() =
        runTest(UnconfinedTestDispatcher()) {
            val prober = FakeProber(success())
            val vm = newVm(prober)
            vm.bind(backgroundScope)

            vm.onQrScanned("http://10.0.0.5:3000")

            val state = vm.uiState.value
            assertInstanceOf(PairingUiState.Confirming::class.java, state)
            assertEquals(WarningTier.PRIVATE_LAN, (state as PairingUiState.Confirming).tier)
            assertTrue(prober.calls.isEmpty(), "confirm-before-network: no probe until confirm")
        }

    @Test
    fun `an invalid URL yields an entry error and no probe`() = runTest(UnconfinedTestDispatcher()) {
        val prober = FakeProber(success())
        val vm = newVm(prober)
        vm.bind(backgroundScope)

        vm.onManualEntry("not a url")

        assertEquals(PairingUiState.Entry(EntryError.INVALID_URL), vm.uiState.value)
        assertTrue(prober.calls.isEmpty())
    }

    @Test
    fun `confirming a LAN host probes it and saves on success`() = runTest(UnconfinedTestDispatcher()) {
        val prober = FakeProber(success())
        val store = InMemoryHostStore()
        val vm = newVm(prober, store = store)
        vm.bind(backgroundScope)

        vm.onManualEntry("http://10.0.0.5:3000")
        vm.confirm()

        assertEquals(listOf(endpoint("http://10.0.0.5:3000")), prober.calls)
        val state = vm.uiState.value
        assertInstanceOf(PairingUiState.Paired::class.java, state)
        val saved = store.loadAll()
        assertEquals(1, saved.size)
        assertEquals("http://10.0.0.5:3000", saved.single().endpoint.baseUrl)
        assertEquals("fixed-id", saved.single().id)
    }

    @Test
    fun `a probe failure maps to inert copy and is retryable`() = runTest(UnconfinedTestDispatcher()) {
        val prober = FakeProber({ PairingProbeResult.Failure(PairingError.HttpOkButNotWebTerminal) })
        val vm = newVm(prober)
        vm.bind(backgroundScope)

        vm.onManualEntry("http://10.0.0.5:3000")
        vm.confirm()

        val state = vm.uiState.value
        assertInstanceOf(PairingUiState.Failed::class.java, state)
        assertEquals(PairingError.HttpOkButNotWebTerminal, (state as PairingUiState.Failed).error)
        assertTrue(state.message.isNotBlank())
    }

    // ── Public-host explicit acknowledge ────────────────────────────────────────────────────────────

    @Test
    fun `a public host is not probed until the risk is explicitly acknowledged`() =
        runTest(UnconfinedTestDispatcher()) {
            val prober = FakeProber(success())
            val vm = newVm(prober)
            vm.bind(backgroundScope)

            vm.onManualEntry("https://example.org:3000")

            // Confirm WITHOUT acknowledgement → no probe, ack reminder re-armed.
            vm.confirm(acknowledgedPublicRisk = false)
            assertTrue(prober.calls.isEmpty(), "public host must not probe without an explicit ack")
            assertTrue((vm.uiState.value as PairingUiState.Confirming).awaitingAck)

            // Confirm WITH acknowledgement → probes.
            vm.confirm(acknowledgedPublicRisk = true)
            assertEquals(1, prober.calls.size)
        }

    // ── Tunnel-host cert-gate (the choke point retry cannot bypass) ──────────────────────────────────

    @Test
    fun `a tunnel host is refused without a device cert and retry cannot bypass the gate`() =
        runTest(UnconfinedTestDispatcher()) {
            val prober = FakeProber(success())
            val vm = newVm(prober, hasCert = false)
            vm.bind(backgroundScope)

            vm.onManualEntry("https://star.terminal.yaojia.wang")

            vm.confirm()
            assertInstanceOf(PairingUiState.CertRequired::class.java, vm.uiState.value)
            assertTrue(prober.calls.isEmpty(), "cert-gate: no network I/O without a device cert")

            // Retrying funnels through the SAME choke point — it still refuses, still no probe.
            vm.retry()
            assertInstanceOf(PairingUiState.CertRequired::class.java, vm.uiState.value)
            assertTrue(prober.calls.isEmpty(), "retry must not bypass the tunnel cert-gate")
        }

    @Test
    fun `a tunnel host with a device cert installed probes normally`() =
        runTest(UnconfinedTestDispatcher()) {
            val prober = FakeProber(success())
            val store = InMemoryHostStore()
            val vm = newVm(prober, hasCert = true, store = store)
            vm.bind(backgroundScope)

            vm.onManualEntry("https://star.terminal.yaojia.wang")
            vm.confirm()

            assertEquals(1, prober.calls.size)
            assertInstanceOf(PairingUiState.Paired::class.java, vm.uiState.value)
            assertEquals(1, store.loadAll().size)
        }

    @Test
    fun `a tunnel TLS failure is re-mapped to a client-cert message`() =
        runTest(UnconfinedTestDispatcher()) {
            val prober = FakeProber({ PairingProbeResult.Failure(PairingError.TlsFailure) })
            val vm = newVm(prober, hasCert = true)
            vm.bind(backgroundScope)

            vm.onManualEntry("https://star.terminal.yaojia.wang")
            vm.confirm()

            val state = vm.uiState.value as PairingUiState.Failed
            assertTrue(state.message.contains("客户端证书"), "tunnel TLS failure → client-cert copy")
        }
}
