package wang.yaojia.webterm.api.models

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

/**
 * The prefs round-trip correctness trap: `PUT /prefs` replaces the WHOLE blob, so decode → mutate
 * one known key → encode MUST carry every unknown top-level key through verbatim (including exact
 * integer formatting), or an Android write clobbers web/iOS/future-server prefs.
 */
class UiPrefsTest {
    @Test
    fun sanitizesFavouritesAndCollapsedLikeTheWebClient() {
        val prefs = UiPrefs.decode(
            """
            {"favourites":["/a","/b","/a","",123],"collapsed":{"g1":true,"g2":false,"g3":"true","":true}}
            """.trimIndent().toByteArray(),
        )!!

        // favourites: non-empty strings only, de-duplicated, order preserved; the number 123 dropped.
        assertEquals(listOf("/a", "/b"), prefs.favourites)
        // collapsed: only literal `true`; false, string "true", and the empty key are all dropped.
        assertEquals(mapOf("g1" to true), prefs.collapsed)
    }

    @Test
    fun mutatingOneKeyPreservesUnknownKeysAndIntegerFormatting() {
        val original = UiPrefs.decode(
            """
            {"favourites":["/old"],"collapsed":{"g":true},"schemaVersion":7,"vendor":{"nested":42,"ratio":1.5}}
            """.trimIndent().toByteArray(),
        )!!

        val mutated = original.withFavourites(listOf("/new"))
        val encoded = mutated.encodeBody().decodeToString()

        // Known key was replaced...
        assertEquals(listOf("/new"), UiPrefs.decode(encoded.toByteArray())!!.favourites)
        // ...collapsed (untouched known key) survived...
        assertEquals(mapOf("g" to true), UiPrefs.decode(encoded.toByteArray())!!.collapsed)
        // ...and every unknown key survived verbatim, integers still integers (42, not 42.0).
        assertTrue(encoded.contains("\"schemaVersion\":7"), "unknown scalar key must round-trip: $encoded")
        assertTrue(encoded.contains("\"nested\":42"), "nested integer must not become 42.0: $encoded")
        assertTrue(encoded.contains("\"ratio\":1.5"), "nested double must round-trip: $encoded")
    }

    @Test
    fun withCollapsedReplacesOnlyThatKey() {
        val original = UiPrefs.decode("""{"favourites":["/keep"],"extra":"x"}""".toByteArray())!!
        val encoded = original.withCollapsed(mapOf("ns" to true)).encodeBody().decodeToString()

        assertEquals(listOf("/keep"), UiPrefs.decode(encoded.toByteArray())!!.favourites)
        assertEquals(mapOf("ns" to true), UiPrefs.decode(encoded.toByteArray())!!.collapsed)
        assertTrue(encoded.contains("\"extra\":\"x\""))
    }

    @Test
    fun createBuildsAFreshBlobWithBothKnownKeys() {
        val encoded = UiPrefs.create(favourites = listOf("/a"), collapsed = mapOf("g" to true))
            .encodeBody().decodeToString()
        assertEquals("""{"favourites":["/a"],"collapsed":{"g":true}}""", encoded)
    }

    @Test
    fun nonObjectBodyDecodesToNull() {
        assertNull(UiPrefs.decode("[]".toByteArray()))
        assertNull(UiPrefs.decode("\"hi\"".toByteArray()))
        assertNull(UiPrefs.decode("not json".toByteArray()))
    }
}
