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

import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.ui.graphics.Color
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * **AC-1, the no-op proof** (THEMING_AND_NAV.md §7). An instance with no
 * `theme_visual` row must resolve to a color scheme equal to the one the app
 * shipped before M12 — not "close enough", identical, because the app's M5
 * palette *is* the website's `runic-gateway` preset.
 *
 * Two things this test has to work around:
 *
 * - **`ColorScheme` does not implement `equals`** (checked against material3
 *   1.3.0), so "the full scheme, not a spot check" is a field-by-field compare.
 *   It is done by reflection over every color-valued getter rather than by a
 *   hand-written list, so a role that is added to Material — or one the mapping
 *   forgets — cannot silently escape the assertion.
 * - The expected value is a **verbatim copy of the pre-M12 `ShardColorScheme`**,
 *   kept here rather than referenced, so the proof is against what the app used
 *   to do and not against whatever [shardColorScheme] does today.
 */
class ShardColorSchemeTest {

    /** The scheme exactly as `ui/theme/Theme.kt` declared it before M12. */
    private val preM12Scheme = darkColorScheme(
        primary = ShardCta,
        onPrimary = ShardOnCta,
        secondary = ShardAccent,
        onSecondary = ShardOnCta,
        tertiary = ShardAccent,
        onTertiary = ShardOnCta,
        background = ShardPage,
        onBackground = ShardBody,
        surface = ShardSurface,
        onSurface = ShardBody,
        surfaceVariant = ShardElevated,
        onSurfaceVariant = ShardMuted,
        surfaceContainer = ShardElevated,
        surfaceContainerHigh = ShardElevated,
        surfaceContainerLow = ShardSurface,
        outline = ShardOutline,
        outlineVariant = ShardDivider,
        secondaryContainer = ShardPillBg,
        onSecondaryContainer = ShardPillFg,
        error = ShardDanger,
        onError = ShardOnCta,
        errorContainer = ShardDangerBg,
        onErrorContainer = ShardDanger,
    )

    /**
     * The roles phase 8 deliberately moves off Material's defaults, and the values
     * they move to.
     *
     * `surfaceContainerHighest` is the one that matters: it is
     * `FilledCardTokens.ContainerColor`, so it is what every `ShardCard` paints with.
     * Leaving it unmapped meant all 26 of them drew in `darkColorScheme()`'s grey
     * rather than `--panel-a` — on themed instances *and* on untouched ones, which is
     * why this is a visible change to the shipped app and not only a theming fix. It
     * had been that way since M5; the AC-5 walk in phase 8 is what surfaced it,
     * because M12 themed everything around the cards and left them behind.
     *
     * `surfaceContainerLowest` has no reader in this app today (the phase 8 sweep
     * checked every Material component the app draws) and is mapped for consistency
     * with `surfaceContainerLow`, not to fix anything.
     *
     * Everything else stays exactly where it was — that is what the test below is for.
     */
    private val deliberatelyChanged = mapOf(
        "surfaceContainerHighest" to ShardElevated,
        "surfaceContainerLowest" to ShardSurface,
    )

    @Test
    fun `the shipped palette reproduces the pre-M12 color scheme but for the card container`() {
        assertPreM12ApartFromTheCardContainer(shardColorScheme(ShardPalette.Shipped))
    }

    /** The same claim from the other end: an absent theme map is the shipped app. */
    @Test
    fun `an absent theme map reproduces the pre-M12 color scheme`() {
        assertPreM12ApartFromTheCardContainer(shardColorScheme(ShardPalette.resolve(emptyMap())))
    }

    /**
     * Every role but [deliberatelyChanged] is byte-for-byte the pre-M12 value, and
     * each of those really did move — asserting the new value alone would still pass
     * if Material's default happened to equal it.
     */
    private fun assertPreM12ApartFromTheCardContainer(actual: ColorScheme) {
        val before = roles(preM12Scheme)
        val after = roles(actual)
        assertEquals(before - deliberatelyChanged.keys, after - deliberatelyChanged.keys)
        for ((role, expected) in deliberatelyChanged) {
            assertEquals("$role should follow the palette", expected, after[role])
            assertNotEquals("$role was already the palette's value", expected, before[role])
        }
    }

    /** Sanity: the comparison is capable of failing, and covers the whole scheme. */
    @Test
    fun `the role comparison sees every color role`() {
        val roles = roles(preM12Scheme)
        // material3 1.3.0 declares 36 color roles; fewer than that means the
        // reflection has stopped seeing them and the comparison above went hollow.
        assertTrue("expected the full Material role set, got ${roles.keys}", roles.size >= 36)
        assertEquals(ShardCta, roles["primary"])
        assertNotEquals(
            roles,
            roles(shardColorScheme(ShardPalette.Shipped.copy(cta = Color(0xFFC9973F)))),
        )
    }

    /** A themed shard moves the roles its tokens own, and only those. */
    @Test
    fun `a theme token reaches its Material role`() {
        val themed = shardColorScheme(
            ShardPalette.resolve(mapOf("--accent" to "#c9973f", "--bg-deep" to "#120c07")),
        )
        val roles = roles(themed)
        assertEquals(Color(0xFFC9973F), roles["secondary"])
        assertEquals(Color(0xFFC9973F), roles["tertiary"])
        assertEquals(Color(0xFF120C07), roles["background"])
        assertEquals(Color(0xFF120C07), roles["onPrimary"]) // derived: onCta tracks --bg-deep
        assertEquals(ShardCta, roles["primary"])            // untouched by these two tokens
    }

    /**
     * The phase 8 fix, stated as the thing a shard operator actually sees: set
     * `--panel-flat` and the app's cards follow. This is the assertion that would have
     * failed before the AC-5 walk, when `surfaceContainerHighest` — Material's filled
     * `Card` container — was left at `darkColorScheme()`'s grey.
     */
    @Test
    fun `--panel-flat reaches the Material card container`() {
        val roles = roles(shardColorScheme(ShardPalette.resolve(mapOf("--panel-flat" to "#1f160d"))))
        val panel = Color(0xFF1F160D)
        assertEquals(panel, roles["surfaceContainerHighest"]) // CardDefaults.cardColors()
        assertEquals(panel, roles["surfaceVariant"])
        assertEquals(panel, roles["surfaceContainer"])
        assertEquals(panel, roles["surfaceContainerHigh"])
    }

    /**
     * Every color role of a scheme, by name. `Color` is a value class, so the
     * roles are the `long`-returning getters and their names carry Kotlin's
     * mangling suffix (`getPrimary-0d7_KjU`), which is stripped here.
     */
    private fun roles(scheme: ColorScheme): Map<String, Color> =
        ColorScheme::class.java.declaredMethods
            .filter {
                it.name.startsWith("get") &&
                    it.returnType == java.lang.Long.TYPE &&
                    it.parameterCount == 0
            }
            .associate { method ->
                val name = method.name.removePrefix("get").substringBefore('-')
                    .replaceFirstChar { it.lowercase() }
                // The getter hands back Color's packed ULong bits, not an ARGB int.
                name to Color((method.invoke(scheme) as Long).toULong())
            }
}
