package com.ponzischeme89.memby.data

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

private fun json(text: String): JsonObject = Json.parseToJsonElement(text) as JsonObject

class UserPreferencesTest {

    /**
     * The document has to survive the round trip unchanged, or two televisions could
     * disagree about settings they had just agreed on — and the disagreement would push
     * itself back and forth forever, since a difference is what triggers a push.
     */
    @Test
    fun `encoding and decoding is a fixed point`() {
        val original = UserPreferences(
            homeSections = listOf("latest", "continue"),
            homeCardDensity = "large",
            homeArtworkStyle = "poster",
            showHomeCardMetadata = false,
            showRatingsStrip = false,
            hideWatchedMovies = true,
            showTitleLogo = false,
            welcomeQuoteStyle = "homicidal",
            autoPlayNextEpisode = false,
            showTenMinuteReminder = false,
            seekIntervalSeconds = 30,
            skipIntroMode = SKIP_INTRO_AUTO,
            forYouMinutes = 60,
            homeRowOrder = listOf("recommended", "latest"),
            homePinnedRows = listOf("continue"),
            homeHiddenRows = listOf("studio-a24"),
        )
        assertEquals(original, decodeUserPreferences(original.encode()))
    }

    /**
     * A key the server did not send must leave that setting alone. This is what lets the
     * gateway grow a setting before every television in the house has the release that
     * knows about it — the alternative is a silent reset of anything not yet understood.
     */
    @Test
    fun `a missing key keeps the local value`() {
        val local = UserPreferences(homeCardDensity = "compact", hideWatchedMovies = true)
        val decoded = decodeUserPreferences(json("""{"showRatingsStrip":false}"""), local)

        assertEquals("compact", decoded.homeCardDensity)
        assertTrue(decoded.hideWatchedMovies)
        assertEquals(false, decoded.showRatingsStrip)
    }

    /** A value of the wrong type is as good as absent, and must not become a reset. */
    @Test
    fun `a wrongly typed value keeps the local value`() {
        val local = UserPreferences(showTitleLogo = false, forYouMinutes = 120)
        val decoded = decodeUserPreferences(
            json("""{"showTitleLogo":"false","forYouMinutes":"120"}"""),
            local,
        )

        assertEquals(false, decoded.showTitleLogo)
        assertEquals(120, decoded.forYouMinutes)
    }

    /**
     * The skip interval is the one number both ends normalise. A value this build has no
     * vocabulary for must become the default rather than reaching the player as a step
     * size — the gateway's catalogue is allowed to grow one before every TV understands it.
     */
    @Test
    fun `an unknown skip interval falls back to the default`() {
        assertEquals(
            DEFAULT_SEEK_INTERVAL_SECONDS,
            decodeUserPreferences(json("""{"seekIntervalSeconds":45}""")).seekIntervalSeconds,
        )
        assertEquals(
            30,
            decodeUserPreferences(json("""{"seekIntervalSeconds":30}""")).seekIntervalSeconds,
        )
        assertEquals(
            DEFAULT_SEEK_INTERVAL_SECONDS,
            Settings(seekIntervalSeconds = 45).toUserPreferences().seekIntervalSeconds,
        )
    }

    /**
     * The same rule for the intro mode, and the stake is higher: an unreadable value that
     * fell through to "auto" would jump through somebody's episode on the strength of a
     * string this build cannot parse. It falls back to the button instead.
     */
    @Test
    fun `an unknown intro mode falls back to the button`() {
        assertEquals(
            DEFAULT_SKIP_INTRO_MODE,
            decodeUserPreferences(json("""{"skipIntroMode":"immediately"}""")).skipIntroMode,
        )
        assertEquals(
            SKIP_INTRO_AUTO,
            decodeUserPreferences(json("""{"skipIntroMode":"auto"}""")).skipIntroMode,
        )
        assertEquals(
            DEFAULT_SKIP_INTRO_MODE,
            Settings(skipIntroMode = "sometimes").toUserPreferences().skipIntroMode,
        )
    }

    /** An empty row selection would be a launcher with nothing on it. */
    @Test
    fun `an empty section list falls back rather than emptying the launcher`() {
        val local = UserPreferences(homeSections = listOf("favorites"))
        val decoded = decodeUserPreferences(json("""{"homeSections":[]}"""), local)

        assertEquals(listOf("favorites"), decoded.homeSections)
    }

    /** Row ids are stored newline-separated, so a blank entry would become a phantom row. */
    @Test
    fun `blank and duplicate row ids are dropped`() {
        val decoded = decodeUserPreferences(
            json("""{"homeRowOrder":["recommended","  ","recommended","latest",""]}"""),
        )
        assertEquals(listOf("recommended", "latest"), decoded.homeRowOrder)
    }

    /**
     * The flat active-profile keys are what every screen renders from, so they are what a
     * push must carry — including the encoding used for the row lists.
     */
    @Test
    fun `settings project onto the document the server holds`() {
        val settings = Settings(
            homeSections = "continue,latest",
            homeCardDensity = "compact",
            homeArtworkStyle = "backdrop",
            showHomeCardMetadata = false,
            showRatingsStrip = false,
            hideWatchedMovies = true,
            showTitleLogo = false,
            welcomeQuoteStyle = "positive",
            autoPlayNextEpisode = false,
            showTenMinuteReminder = false,
            forYouMinutes = 30,
            homeRowOrder = "recommended\nlatest",
            homePinnedRows = "continue",
            homeHiddenRows = "",
        )

        assertEquals(
            UserPreferences(
                homeSections = listOf("continue", "latest"),
                homeCardDensity = "compact",
                homeArtworkStyle = "backdrop",
                showHomeCardMetadata = false,
                showRatingsStrip = false,
                hideWatchedMovies = true,
                showTitleLogo = false,
                welcomeQuoteStyle = "positive",
                autoPlayNextEpisode = false,
                showTenMinuteReminder = false,
                forYouMinutes = 30,
                homeRowOrder = listOf("recommended", "latest"),
                homePinnedRows = listOf("continue"),
                homeHiddenRows = emptyList(),
            ),
            settings.toUserPreferences(),
        )
    }

    /**
     * Nothing that identifies a *television* may reach the wire. Carrying the device name
     * or the update token across would rename someone's other set, or hand a private
     * credential to whichever server the document came from.
     */
    @Test
    fun `device identity never enters the document`() {
        val encoded = Settings(
            deviceName = "Living room",
            deviceId = "device-1",
            updateToken = "gitea-secret",
            updateBaseUrl = "https://git.example",
            token = "session-token",
            ringColorHex = "FF0000",
            rotationIntervalSeconds = 45,
            confirmExitMemby = true,
        ).toUserPreferences().encode().toString()

        listOf(
            "Living room",
            "device-1",
            "gitea-secret",
            "git.example",
            "session-token",
            "confirmExitMemby",
        )
            .forEach { assertTrue("$it reached the wire", it !in encoded) }
    }
}
