package wang.yaojia.webterm.nav

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.wiring.ColdStartRoute

/**
 * A32 · DeepLinkRouter (pure whitelist parser) + NavGraph route mappers.
 *
 * Android port of iOS `DeepLinkRouterTests`: `webterminal://open?host=&join=` (custom scheme) AND the
 * verified `https://terminal.yaojia.wang/open?…` App Link both parse; every field is v4-UUID
 * whitelisted; ANY invalid / ambiguous / missing part → [DeepLinkRoute.Ignore] (never partially
 * applied) + tallied; push-tap payloads share the same router. Plain-string inputs — no Compose, no
 * device (NavHost composition is device-QA).
 */
class DeepLinkRouterTest {

    // v4 UUIDs (version nibble = 4, variant nibble = 8/9/a/b) → pass Validation.isValidSessionId.
    private val validHostId = "11111111-2222-4333-8444-555555555555"
    private val validSessionId = "0f5a1f2e-3b4c-4d5e-8f6a-7b8c9d0e1f2a"

    // version nibble = 1: a syntactic UUID that the server's v4-only SESSION_ID_RE rejects — pins
    // "reuse Validation, never re-regex".
    private val v1StyleId = "11111111-2222-1333-8444-555555555555"

    // variant nibble = c (a legal v4 requires 8/9/a/b).
    private val badVariantId = "11111111-2222-4333-c444-555555555555"

    private fun customUrl(host: String = validHostId, join: String = validSessionId): String =
        "webterminal://open?host=$host&join=$join"

    private fun appLinkUrl(host: String = validHostId, join: String = validSessionId): String =
        "https://terminal.yaojia.wang/open?host=$host&join=$join"

    // ── Custom scheme: valid paths ───────────────────────────────────────────────────────────────────

    @Test
    fun `valid custom-scheme link routes to OpenSession with both ids`() {
        assertEquals(
            DeepLinkRoute.OpenSession(hostId = validHostId, sessionId = validSessionId),
            DeepLinkRouter.route(customUrl()),
        )
    }

    @Test
    fun `valid https App Link routes to OpenSession with both ids`() {
        assertEquals(
            DeepLinkRoute.OpenSession(hostId = validHostId, sessionId = validSessionId),
            DeepLinkRouter.route(appLinkUrl()),
        )
    }

    @Test
    fun `uppercase UUIDs are accepted (Validation is case-insensitive)`() {
        assertEquals(
            DeepLinkRoute.OpenSession(hostId = validHostId.uppercase(), sessionId = validSessionId.uppercase()),
            DeepLinkRouter.route(customUrl(validHostId.uppercase(), validSessionId.uppercase())),
        )
    }

    @Test
    fun `unknown extra query keys are ignored and the link still routes`() {
        assertEquals(
            DeepLinkRoute.OpenSession(hostId = validHostId, sessionId = validSessionId),
            DeepLinkRouter.route("webterminal://open?host=$validHostId&join=$validSessionId&utm=x&foo=bar"),
        )
    }

    @Test
    fun `query key order does not matter`() {
        assertEquals(
            DeepLinkRoute.OpenSession(hostId = validHostId, sessionId = validSessionId),
            DeepLinkRouter.route("webterminal://open?join=$validSessionId&host=$validHostId"),
        )
    }

    @Test
    fun `trailing-slash action path is accepted`() {
        assertEquals(
            DeepLinkRoute.OpenSession(hostId = validHostId, sessionId = validSessionId),
            DeepLinkRouter.route("webterminal://open/?host=$validHostId&join=$validSessionId"),
        )
    }

    @Test
    fun `App Link trailing-slash path is accepted`() {
        assertEquals(
            DeepLinkRoute.OpenSession(hostId = validHostId, sessionId = validSessionId),
            DeepLinkRouter.route("https://terminal.yaojia.wang/open/?host=$validHostId&join=$validSessionId"),
        )
    }

    // ── Whitelist rejections (any invalid part → Ignore) ─────────────────────────────────────────────

    @Test
    fun `wrong scheme is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("myapp://open?host=$validHostId&join=$validSessionId"))
    }

    @Test
    fun `wrong custom-scheme action is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("webterminal://kill?host=$validHostId&join=$validSessionId"))
    }

    @Test
    fun `custom-scheme with a non-empty path is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("webterminal://open/extra?host=$validHostId&join=$validSessionId"))
    }

    @Test
    fun `App Link on a foreign host is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("https://evil.example/open?host=$validHostId&join=$validSessionId"))
    }

    @Test
    fun `App Link on the wrong path is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("https://terminal.yaojia.wang/kill?host=$validHostId&join=$validSessionId"))
    }

    @Test
    fun `https with the custom-scheme action as host does not leak through`() {
        // https://open?... parses to host=open — must NOT be mistaken for the custom-scheme action.
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("https://open?host=$validHostId&join=$validSessionId"))
    }

    @Test
    fun `missing host key is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("webterminal://open?join=$validSessionId"))
    }

    @Test
    fun `missing join key is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("webterminal://open?host=$validHostId"))
    }

    @Test
    fun `duplicate host key is ambiguous and ignored (never partial)`() {
        assertEquals(
            DeepLinkRoute.Ignore,
            DeepLinkRouter.route("webterminal://open?host=$validHostId&host=$validHostId&join=$validSessionId"),
        )
    }

    @Test
    fun `non-v4 UUID in either field is ignored (reuses Validation, never re-regexed)`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(customUrl(host = v1StyleId)))
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(customUrl(join = v1StyleId)))
    }

    @Test
    fun `bad UUID variant nibble is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(customUrl(join = badVariantId)))
    }

    @Test
    fun `a valid host with an invalid session is never partially applied`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(customUrl(host = validHostId, join = "not-a-uuid")))
    }

    @Test
    fun `an invalid host with a valid session is never partially applied`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(customUrl(host = "not-a-uuid", join = validSessionId)))
    }

    @Test
    fun `empty values, empty query, and garbage are ignored without crashing`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("webterminal://open?host=&join="))
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("webterminal://open"))
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(customUrl(host = "not-a-uuid")))
        // Illegal characters make java.net.URI throw — must be caught → Ignore, never a crash.
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("webterminal://open?host=x&join='; DROP TABLE sessions;--"))
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(""))
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route("not even a uri"))
    }

    // ── Push-tap payload (A30 reuses the same router) ────────────────────────────────────────────────

    @Test
    fun `push payload with a valid sessionId routes to GateSession (extra keys ignored)`() {
        val payload = mapOf("sessionId" to validSessionId, "cls" to "gate", "token" to "opaque")
        assertEquals(DeepLinkRoute.GateSession(sessionId = validSessionId), DeepLinkRouter.route(payload))
    }

    @Test
    fun `push payload without sessionId or non-v4 is ignored`() {
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(emptyMap()))
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(mapOf("cls" to "gate")))
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(mapOf("sessionId" to v1StyleId)))
        assertEquals(DeepLinkRoute.Ignore, DeepLinkRouter.route(mapOf("sessionId" to "")))
    }

    // ── DeepLinkTally: invalid links are counted, valid ones are not ─────────────────────────────────

    @Test
    fun `tally increments only on Ignore and is immutable`() {
        val start = DeepLinkTally()
        val afterIgnore = start.record(DeepLinkRoute.Ignore)
        assertEquals(1, afterIgnore.ignored)
        assertEquals(0, start.ignored) // original untouched (immutable reducer)

        val afterOpen = afterIgnore.record(DeepLinkRoute.OpenSession(validHostId, validSessionId))
        val afterGate = afterOpen.record(DeepLinkRoute.GateSession(validSessionId))
        assertEquals(1, afterGate.ignored) // valid routes never bump the counter

        val afterSecondIgnore = afterGate.record(DeepLinkRouter.route("webterminal://bad"))
        assertEquals(2, afterSecondIgnore.ignored)
    }

    // ── NavGraph pure route mappers ──────────────────────────────────────────────────────────────────

    @Test
    fun `cold-start route maps to the pairing or sessions destination`() {
        assertEquals(NavRoutes.PAIRING, NavRoutes.startRouteFor(ColdStartRoute.PAIRING))
        assertEquals(NavRoutes.SESSIONS, NavRoutes.startRouteFor(ColdStartRoute.SESSIONS))
    }

    @Test
    fun `forDeepLink builds the terminal route for an OpenSession`() {
        assertEquals(
            "terminal/$validHostId/$validSessionId",
            NavRoutes.forDeepLink(DeepLinkRoute.OpenSession(validHostId, validSessionId)),
        )
    }

    @Test
    fun `forDeepLink builds the gate route for a GateSession`() {
        assertEquals("gate/$validSessionId", NavRoutes.forDeepLink(DeepLinkRoute.GateSession(validSessionId)))
    }

    @Test
    fun `forDeepLink returns null for Ignore (caller does nothing)`() {
        assertNull(NavRoutes.forDeepLink(DeepLinkRoute.Ignore))
    }

    @Test
    fun `terminal route pattern args line up with the built route`() {
        // The concrete route the mapper builds must fill exactly the pattern's declared segments.
        assertEquals("terminal/{hostId}/{sessionId}", NavRoutes.TERMINAL_PATTERN)
        assertEquals("gate/{sessionId}", NavRoutes.GATE_PATTERN)
    }
}
