package wang.yaojia.webterm.push

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.assertTrue
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.hostregistry.Host
import wang.yaojia.webterm.hostregistry.InMemoryHostStore

/**
 * [PushRegistrar] fan-out + self-heal contract (plan §5 A31, §4.5). Pure JVM under `runTest`: a
 * recording [HostPushApi] fake stands in for [ApiClient][wang.yaojia.webterm.api.routes.ApiClient]
 * (one per host via the `apiFor` seam) and an [InMemoryHostStore] is the paired-host set. Asserts:
 *  - every paired host gets exactly one POST carrying the token;
 *  - self-heal — token rotation re-registers all, a new host registers just it, removal DELETEs just it;
 *  - re-registering the same token is idempotent (client re-POSTs, no crash);
 *  - best-effort — one host's failure never blocks the others, and the error sink never sees the token.
 */
class PushRegistrarTest {

    private sealed interface Call {
        val hostId: String
        val token: String

        data class Register(override val hostId: String, override val token: String) : Call
        data class Unregister(override val hostId: String, override val token: String) : Call
    }

    /** Records every register/unregister, tagged with the host it was minted for. */
    private class RecordingApi(
        private val hostId: String,
        private val calls: MutableList<Call>,
        private val failWith: Throwable? = null,
    ) : HostPushApi {
        override suspend fun register(token: String) {
            calls.add(Call.Register(hostId, token))
            failWith?.let { throw it }
        }

        override suspend fun unregister(token: String) {
            calls.add(Call.Unregister(hostId, token))
            failWith?.let { throw it }
        }
    }

    private fun host(id: String): Host =
        Host.create(id = id, name = "host-$id", baseUrl = "http://192.168.1.$id:3000")!!

    private val token = "fMr0ck3t-token_AB-cd"
    private val rotated = "n3w-r0tat3d_token-XY"

    @Test
    fun `registers the token to every paired host, once each`() = runTest {
        val calls = mutableListOf<Call>()
        val hosts = InMemoryHostStore(listOf(host("10"), host("11"), host("12")))
        val registrar = PushRegistrar(hosts, apiFor = { RecordingApi(it.id, calls) }, onError = { _, _ -> })

        registrar.registerAllHosts(token)

        assertEquals(
            listOf(
                Call.Register("10", token),
                Call.Register("11", token),
                Call.Register("12", token),
            ),
            calls,
        )
    }

    @Test
    fun `token rotation re-registers every host with the new token`() = runTest {
        val calls = mutableListOf<Call>()
        val hosts = InMemoryHostStore(listOf(host("10"), host("11")))
        val registrar = PushRegistrar(hosts, apiFor = { RecordingApi(it.id, calls) }, onError = { _, _ -> })

        registrar.registerAllHosts(token)
        calls.clear()
        registrar.registerAllHosts(rotated) // FcmService.onNewToken → self-heal

        assertEquals(
            listOf(Call.Register("10", rotated), Call.Register("11", rotated)),
            calls,
        )
    }

    @Test
    fun `a newly paired host is registered on its own`() = runTest {
        val calls = mutableListOf<Call>()
        val hosts = InMemoryHostStore(listOf(host("10")))
        val registrar = PushRegistrar(hosts, apiFor = { RecordingApi(it.id, calls) }, onError = { _, _ -> })

        registrar.registerHost(host("20"), token)

        assertEquals(listOf(Call.Register("20", token)), calls)
    }

    @Test
    fun `a removed host is unregistered (DELETE) on its own`() = runTest {
        val calls = mutableListOf<Call>()
        val hosts = InMemoryHostStore(listOf(host("10")))
        val registrar = PushRegistrar(hosts, apiFor = { RecordingApi(it.id, calls) }, onError = { _, _ -> })

        registrar.unregisterHost(host("10"), token)

        assertEquals(listOf(Call.Unregister("10", token)), calls)
    }

    @Test
    fun `re-registering the same token is idempotent (re-POSTs, never crashes)`() = runTest {
        val calls = mutableListOf<Call>()
        val hosts = InMemoryHostStore(listOf(host("10")))
        val registrar = PushRegistrar(hosts, apiFor = { RecordingApi(it.id, calls) }, onError = { _, _ -> })

        registrar.registerAllHosts(token)
        registrar.registerAllHosts(token) // app-start again on top of an already-registered token

        assertEquals(
            listOf(Call.Register("10", token), Call.Register("10", token)),
            calls,
        )
    }

    @Test
    fun `one host failing does not block the others and never leaks the token to onError`() = runTest {
        val calls = mutableListOf<Call>()
        val errors = mutableListOf<Pair<String, Throwable>>()
        val boom = IllegalStateException("host-11 unreachable")
        val hosts = InMemoryHostStore(listOf(host("10"), host("11"), host("12")))
        val registrar = PushRegistrar(
            hosts,
            apiFor = { h -> RecordingApi(h.id, calls, failWith = if (h.id == "11") boom else null) },
            onError = { h, e -> errors.add(h.id to e) },
        )

        registrar.registerAllHosts(token)

        // All three attempted (best-effort); the middle one failed but 10 and 12 still went out.
        assertEquals(
            listOf(
                Call.Register("10", token),
                Call.Register("11", token),
                Call.Register("12", token),
            ),
            calls,
        )
        assertEquals(1, errors.size)
        assertEquals("11", errors.single().first)
        assertTrue(errors.single().second === boom)
        // The error sink is host+throwable only — the token can never be reconstructed from it.
        assertFalse(errors.single().second.message!!.contains(token))
    }

    @Test
    fun `an empty host list is a no-op, never crashes`() = runTest {
        val calls = mutableListOf<Call>()
        val registrar = PushRegistrar(InMemoryHostStore(), apiFor = { RecordingApi(it.id, calls) }, onError = { _, _ -> })

        registrar.registerAllHosts(token)

        assertTrue(calls.isEmpty())
    }
}
