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

import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertSame
import org.junit.Test

/**
 * [ShardStructure.resolve] — the radius ratio (§5.2) and the shadow map (§5.4).
 *
 * The no-op proof is easier here than it was for the palette: material3's
 * [Shapes] *does* implement `equals` (unlike `ColorScheme`), so a whole shape
 * scale can be compared in one assertion. As in [ShardColorSchemeTest] the
 * expected value is a **verbatim copy of the pre-M12 scale**, kept here rather
 * than referenced, so the proof is against what the app used to draw and not
 * against whatever [ShippedShapes] says today.
 */
class ShardStructureTest {

    /** The scale exactly as `ui/theme/Theme.kt` declared it before M12. */
    private val preM12Shapes = Shapes(
        extraSmall = RoundedCornerShape(8.dp),
        small = RoundedCornerShape(8.dp),
        medium = RoundedCornerShape(12.dp),
        large = RoundedCornerShape(16.dp),
        extraLarge = RoundedCornerShape(24.dp),
    )

    /** The `runic-gateway` preset's structure tokens, as the server publishes them. */
    private val runicGateway = mapOf(
        "--radius-pill" to "999px",
        "--radius-panel" to "12px",
        "--radius-card" to "10px",
        "--radius-input" to "8px",
        "--shadow-card" to "0 14px 34px rgba(0, 0, 0, 0.3)",
    )

    private val fantasy = mapOf(
        "--radius-pill" to "4px",
        "--radius-panel" to "3px",
        "--radius-card" to "2px",
        "--radius-input" to "2px",
        "--shadow-card" to "0 16px 38px rgba(0, 0, 0, 0.45)",
    )

    private val modern = mapOf(
        "--radius-pill" to "8px",
        "--radius-panel" to "8px",
        "--radius-card" to "6px",
        "--radius-input" to "6px",
        "--shadow-card" to "0 8px 20px rgba(0, 0, 0, 0.25)",
    )

    @Test
    fun `the shipped scale is the pre-M12 scale`() {
        assertEquals(preM12Shapes, ShardStructure.Shipped.shapes)
        assertSame(CircleShape, ShardStructure.Shipped.pill)
    }

    @Test
    fun `no theme resolves to the shipped structure`() {
        assertEquals(ShardStructure.Shipped, ShardStructure.resolve(emptyMap()))
    }

    @Test
    fun `a theme with no structure tokens resolves to the shipped structure`() {
        // A shard that themed its colors only still draws the app's own radii:
        // every ratio is 1.0 because every token is absent.
        val colorsOnly = mapOf("--accent" to "#7f99bd", "--bg" to "#0b1220")
        assertEquals(ShardStructure.Shipped, ShardStructure.resolve(colorsOnly))
    }

    @Test
    fun `the runic-gateway preset is a no-op`() {
        // AC-1 for the structure half: an admin who explicitly picks the preset
        // the app was drawn from gets ratio 1.0 on all four fields.
        assertEquals(ShardStructure.Shipped, ShardStructure.resolve(runicGateway))
    }

    @Test
    fun `fantasy scales the app's own dp, it does not adopt the web's`() {
        val s = ShardStructure.resolve(fantasy)
        // 2/8 -> 8dp becomes 2dp; the web's own value is also 2px, coincidentally.
        assertEquals(RoundedCornerShape(2.dp), s.shapes.extraSmall)
        assertEquals(RoundedCornerShape(2.dp), s.shapes.small)
        // 2/10 -> 12dp * 0.2 = 2.4, rounded.
        assertEquals(RoundedCornerShape(2.dp), s.shapes.medium)
        // 3/12 -> 16dp * 0.25. The web value is 3px; the app's is 4dp, which is
        // the whole point of the ratio.
        assertEquals(RoundedCornerShape(4.dp), s.shapes.large)
        // extraLarge has no web counterpart and follows the panel ratio.
        assertEquals(RoundedCornerShape(6.dp), s.shapes.extraLarge)
        assertEquals(RoundedCornerShape(4.dp), s.pill)
        // 38px blur is nearer Default's 34 than Deep's 44.
        assertEquals(4.dp, s.cardElevation)
    }

    @Test
    fun `modern scales the app's own dp`() {
        val s = ShardStructure.resolve(modern)
        assertEquals(RoundedCornerShape(6.dp), s.shapes.extraSmall)
        assertEquals(RoundedCornerShape(7.dp), s.shapes.medium)
        assertEquals(RoundedCornerShape(11.dp), s.shapes.large)
        assertEquals(RoundedCornerShape(16.dp), s.shapes.extraLarge)
        assertEquals(RoundedCornerShape(8.dp), s.pill)
        assertEquals(2.dp, s.cardElevation)
    }

    @Test
    fun `a bad radius costs only its own field`() {
        val s = ShardStructure.resolve(
            mapOf(
                "--radius-input" to "8",      // no unit; the server never writes this
                "--radius-card" to "huge",
                "--radius-panel" to "3px",    // good, and must still apply
                "--radius-pill" to "",
            ),
        )
        assertEquals(preM12Shapes.extraSmall, s.shapes.extraSmall)
        assertEquals(preM12Shapes.medium, s.shapes.medium)
        assertEquals(RoundedCornerShape(4.dp), s.shapes.large)
        assertSame(CircleShape, s.pill)
    }

    @Test
    fun `a zero radius squares the corner off rather than clamping to the shipped value`() {
        val s = ShardStructure.resolve(mapOf("--radius-card" to "0px", "--radius-input" to "0px"))
        assertEquals(RoundedCornerShape(0.dp), s.shapes.medium)
        assertEquals(RoundedCornerShape(0.dp), s.shapes.extraSmall)
        assertNotEquals(preM12Shapes, s.shapes)
    }

    @Test
    fun `the pill keeps its circle until the site is squared off`() {
        assertSame(CircleShape, ShardStructure.resolve(mapOf("--radius-pill" to "999px")).pill)
        assertSame(CircleShape, ShardStructure.resolve(mapOf("--radius-pill" to "500px")).pill)
        assertEquals(RoundedCornerShape(499.dp), ShardStructure.resolve(mapOf("--radius-pill" to "499px")).pill)
        assertEquals(RoundedCornerShape(0.dp), ShardStructure.resolve(mapOf("--radius-pill" to "0px")).pill)
    }

    @Test
    fun `every shadow option lands on its step`() {
        fun elevation(shadow: String) = ShardStructure.resolve(mapOf("--shadow-card" to shadow)).cardElevation
        assertEquals(0.dp, elevation("none"))
        assertEquals(2.dp, elevation("0 8px 20px rgba(0, 0, 0, 0.25)"))
        assertEquals(4.dp, elevation("0 14px 34px rgba(0, 0, 0, 0.3)"))
        assertEquals(8.dp, elevation("0 18px 44px rgba(0, 0, 0, 0.45)"))
    }

    @Test
    fun `a shadow the options do not contain lands on the nearest step`() {
        // The reason the match is on blur and not on the exact string: the
        // Fantasy preset's own --shadow-card is not one of SHADOW_OPTIONS'
        // four values, because a preset's tokens never pass through that
        // dropdown. An exact match would have dropped it on the floor.
        fun elevation(shadow: String) = ShardStructure.resolve(mapOf("--shadow-card" to shadow)).cardElevation
        assertEquals(4.dp, elevation("0 16px 38px rgba(0, 0, 0, 0.45)"))
        assertEquals(8.dp, elevation("0 20px 60px rgba(0, 0, 0, 0.5)"))
        assertEquals(2.dp, elevation("0 2px 4px rgba(0, 0, 0, 0.2)"))
    }

    @Test
    fun `an unreadable shadow keeps the shipped depth`() {
        fun elevation(shadow: String) = ShardStructure.resolve(mapOf("--shadow-card" to shadow)).cardElevation
        assertEquals(ShardStructure.Shipped.cardElevation, elevation("inset 0 0 nonsense"))
        assertEquals(ShardStructure.Shipped.cardElevation, elevation(""))
        // One length is an offset, not a blur — an incomplete value is not a
        // reason to flatten every card on the shard.
        assertEquals(ShardStructure.Shipped.cardElevation, elevation("0 14px"))
    }

    @Test
    fun `the shipped depth is the runic-gateway default, not flat`() {
        // The one deliberate departure from §2: the app has been flat since M5
        // (material3's filled Card is Level0 and FeatureCard drew no shadow),
        // while the preset the app was drawn from selects the "Default" shadow.
        // §5.4 is applied as written, so an untouched instance gains this depth.
        assertEquals(4.dp, ShardStructure.Shipped.cardElevation)
    }
}
