/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.data.appearance

import com.runicgateway.app.data.api.dto.SettingsDto
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * [SiteAppearance.from] — the coercion between the settings payload and what the
 * theme and the drawer read (THEMING_AND_NAV.md §2, §3).
 *
 * The claims that matter here are the two the milestone rests on: an untouched
 * instance resolves to *nothing* (so the shipped app renders), and a bad token
 * costs exactly its own token.
 */
class SiteAppearanceTest {

    private val json = Json {
        ignoreUnknownKeys = true
        explicitNulls = false
        coerceInputValues = true
    }

    private fun appearanceOf(body: String) =
        SiteAppearance.from(json.decodeFromString<SettingsDto>(body))

    @Test fun untouchedInstanceResolvesToNoOverrides() {
        // No theme_visual row, no nav_public row: the shipped app, exactly (§2).
        val appearance = appearanceOf("""{"brand":{"name":"UOMysticmoon"}}""")
        assertTrue(appearance.theme.isEmpty())
        assertNull(appearance.navPublic)
        assertEquals("UOMysticmoon", appearance.brand?.name)
    }

    @Test fun failedSettingsCallIsTheSameAsNoOverrides() {
        assertSame(SiteAppearance.NONE, SiteAppearance.from(null))
        assertNull(SiteAppearance.NONE.brand)
        assertTrue(SiteAppearance.NONE.theme.isEmpty())
        assertNull(SiteAppearance.NONE.navPublic)
    }

    @Test fun resolvedThemeTokensAreReadAsAMap() {
        val appearance = appearanceOf(
            """{"theme":{"--accent":"#c8a45c","--bg":"#1a1410","--radius-card":"10px",
                         "--shadow-card":"none","--sans":"Inter, sans-serif"}}""",
        )
        assertEquals("#c8a45c", appearance.theme["--accent"])
        assertEquals("#1a1410", appearance.theme["--bg"])
        assertEquals("10px", appearance.theme["--radius-card"])
        assertEquals("none", appearance.theme["--shadow-card"])
        assertEquals("Inter, sans-serif", appearance.theme["--sans"])
    }

    @Test fun anEmptyThemeMapIsTheSameAsAbsent() {
        // The server returns null rather than {} — the app must not depend on that.
        assertTrue(appearanceOf("""{"theme":{}}""").theme.isEmpty())
    }

    @Test fun aBadTokenCostsOnlyItself() {
        // AC-2 in miniature at the decode boundary: a non-string or blank value is
        // dropped field-by-field, and its neighbours still apply.
        val appearance = appearanceOf(
            """{"theme":{"--accent":"#c8a45c","--bg":7,"--line":null,"--ink":"  "}}""",
        )
        assertEquals(mapOf("--accent" to "#c8a45c"), appearance.theme)
    }

    @Test fun aThemeOfTheWrongKindDoesNotCostTheBrand() {
        // The whole reason `theme` is modeled as a raw JsonElement: one unexpected
        // value must not fail the decode and take brand and push down with it.
        val appearance = appearanceOf(
            """{"theme":"not an object","brand":{"name":"UOMysticmoon","accent":"#7f99bd"},
                "push":{"ntfyUrl":"https://ntfy.example.com"}}""",
        )
        assertTrue(appearance.theme.isEmpty())
        assertEquals("#7f99bd", appearance.brand?.accent)
    }

    @Test fun navPublicIsParsedASecondTime() {
        // It arrives as a JSON string inside a JSON object, because settings.value
        // is TEXT.
        val appearance = appearanceOf("""{"nav_public":"{\"/wiki\":{\"label\":\"Codex\"}}"}""")
        assertEquals(setOf("/wiki"), appearance.navPublic?.keys)
    }

    @Test fun aMalformedNavPublicDoesNotCostTheTheme() {
        val appearance = appearanceOf("""{"nav_public":"{oops","theme":{"--accent":"#c8a45c"}}""")
        assertNull(appearance.navPublic)
        assertEquals("#c8a45c", appearance.theme["--accent"])
    }
}
