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

import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertSame
import org.junit.Test
import androidx.compose.material3.Typography as MaterialTypography

/**
 * [ShardTypeface.resolve] and [shardTypography] — the font third of the theme
 * (§5.3).
 *
 * Like the shape scale and unlike the color scheme, material3's [MaterialTypography]
 * implements `equals` (checked in the 1.3.0 bytecode), so the no-op proof is one
 * comparison against a **verbatim copy of the pre-M12 scale** kept in this file.
 * Copying it rather than referencing `shardTypography(Shipped)` is the point: the
 * assertion is against what the app used to draw, so a stray edit to a size or a
 * letter-spacing in `Type.kt` fails here rather than quietly redefining "shipped".
 */
class ShardTypefaceTest {

    // ── the stacks, exactly as the server publishes them ──────────────────────

    private val runicGateway = mapOf(
        "--serif" to "Georgia, \"Times New Roman\", serif",
        "--display" to "Cinzel, Georgia, serif",
        "--sans" to "\"Helvetica Neue\", Arial, sans-serif",
    )

    private val fantasy = mapOf(
        "--serif" to "'EB Garamond', Georgia, serif",
        "--display" to "Cinzel, Georgia, serif",
        "--sans" to "'EB Garamond', Georgia, serif",
    )

    private val modern = mapOf(
        "--serif" to "Inter, Arial, sans-serif",
        "--display" to "'Work Sans', Arial, sans-serif",
        "--sans" to "Inter, Arial, sans-serif",
    )

    // ── the no-op proof (AC-1) ────────────────────────────────────────────────

    @Test
    fun `no theme resolves to the shipped families`() {
        assertEquals(ShardTypeface.Shipped, ShardTypeface.resolve(emptyMap()))
        assertSame(Cinzel, ShardTypeface.Shipped.display)
        assertSame(FontFamily.Serif, ShardTypeface.Shipped.serif)
        assertSame(FontFamily.SansSerif, ShardTypeface.Shipped.sans)
    }

    @Test
    fun `the shipped scale is the pre-M12 scale`() {
        assertEquals(preM12Typography, shardTypography(ShardTypeface.Shipped))
    }

    @Test
    fun `the runic-gateway preset is a no-op`() {
        assertEquals(ShardTypeface.Shipped, ShardTypeface.resolve(runicGateway))
        assertEquals(preM12Typography, shardTypography(ShardTypeface.resolve(runicGateway)))
    }

    // ── the presets that bypass the per-role dropdown ─────────────────────────

    @Test
    fun `the fantasy preset puts EB Garamond in the serif and sans roles`() {
        val faces = ShardTypeface.resolve(fantasy)
        assertSame(Cinzel, faces.display)
        assertSame(EBGaramond, faces.serif)
        // Not an option the sans dropdown offers — a preset's tokens are copied
        // verbatim and never pass through it.
        assertSame(EBGaramond, faces.sans)
    }

    @Test
    fun `the modern preset puts Work Sans in the display role and Inter in the serif role`() {
        val faces = ShardTypeface.resolve(modern)
        // Neither of these is in its role's FONT_OPTIONS list.
        assertSame(WorkSans, faces.display)
        assertSame(Inter, faces.serif)
        assertSame(Inter, faces.sans)
    }

    // ── every option on the shortlist ─────────────────────────────────────────

    @Test
    fun `every serif option resolves`() {
        assertSame(EBGaramond, serifFor("'EB Garamond', Georgia, serif"))
        assertSame(Merriweather, serifFor("Merriweather, Georgia, serif"))
        assertSame(PlayfairDisplay, serifFor("'Playfair Display', Georgia, serif"))
        assertSame(IMFellEnglish, serifFor("'IM Fell English', Georgia, serif"))
        assertSame(FontFamily.Serif, serifFor("Georgia, \"Times New Roman\", serif"))
    }

    @Test
    fun `every display option resolves`() {
        assertSame(Cinzel, displayFor("Cinzel, Georgia, serif"))
        assertSame(PlayfairDisplay, displayFor("'Playfair Display', Georgia, serif"))
        assertSame(EBGaramond, displayFor("'EB Garamond', Georgia, serif"))
        assertSame(IMFellEnglish, displayFor("'IM Fell English', Georgia, serif"))
    }

    @Test
    fun `every sans option resolves`() {
        assertSame(Inter, sansFor("Inter, Arial, sans-serif"))
        assertSame(WorkSans, sansFor("'Work Sans', Arial, sans-serif"))
        assertSame(SourceSans3, sansFor("'Source Sans 3', Arial, sans-serif"))
        assertSame(FontFamily.SansSerif, sansFor("\"Helvetica Neue\", Arial, sans-serif"))
    }

    // ── parsing: only the first name carries the choice ───────────────────────

    @Test
    fun `the fallback chain after the first comma is ignored`() {
        // Same family however the web fallbacks are written, and a serif stack in
        // the sans role still resolves to what it names.
        assertSame(Merriweather, sansFor("Merriweather, Georgia, serif"))
        assertSame(Merriweather, sansFor("Merriweather"))
        assertSame(Merriweather, sansFor("Merriweather , whatever , serif"))
    }

    @Test
    fun `quoting and casing do not matter`() {
        assertSame(WorkSans, sansFor("'Work Sans', Arial, sans-serif"))
        assertSame(WorkSans, sansFor("\"Work Sans\", Arial, sans-serif"))
        assertSame(WorkSans, sansFor("Work Sans, Arial, sans-serif"))
        assertSame(WorkSans, sansFor("  'WORK SANS' , Arial  "))
    }

    // ── per-field fallback (AC-2) ─────────────────────────────────────────────

    @Test
    fun `an unreadable role falls back to its own shipped family, not another role's`() {
        val faces = ShardTypeface.resolve(
            mapOf(
                "--display" to "'Playfair Display', Georgia, serif",
                "--serif" to "Comic Sans MS, cursive",
                "--sans" to "",
            ),
        )
        assertSame(PlayfairDisplay, faces.display)
        assertSame(ShardTypeface.Shipped.serif, faces.serif)
        assertSame(ShardTypeface.Shipped.sans, faces.sans)
    }

    @Test
    fun `a theme with no font tokens keeps all three shipped families`() {
        // A non-empty theme that touches only colors must not disturb the type.
        assertEquals(
            ShardTypeface.Shipped,
            ShardTypeface.resolve(mapOf("--accent" to "#c9a227")),
        )
    }

    @Test
    fun `a blank or comma-only stack falls back`() {
        assertSame(ShardTypeface.Shipped.sans, sansFor("   "))
        assertSame(ShardTypeface.Shipped.sans, sansFor(","))
        assertSame(ShardTypeface.Shipped.sans, sansFor("'', Arial, sans-serif"))
    }

    // ── the scale itself only ever changes family ─────────────────────────────

    @Test
    fun `a themed scale differs from the shipped one only in its families`() {
        val themed = shardTypography(ShardTypeface.resolve(fantasy))
        assertNotEquals(preM12Typography, themed)
        assertEquals(
            preM12Typography.bodyLarge,
            themed.bodyLarge.copy(fontFamily = ShardTypeface.Shipped.serif),
        )
        assertEquals(
            preM12Typography.labelLarge,
            themed.labelLarge.copy(fontFamily = ShardTypeface.Shipped.sans),
        )
        assertEquals(
            preM12Typography.displayLarge,
            themed.displayLarge.copy(fontFamily = ShardTypeface.Shipped.display),
        )
    }

    @Test
    fun `each role reaches the styles it owns`() {
        val themed = shardTypography(ShardTypeface.resolve(modern))
        // display/headline/title
        assertSame(WorkSans, themed.displaySmall.fontFamily)
        assertSame(WorkSans, themed.headlineMedium.fontFamily)
        assertSame(WorkSans, themed.titleSmall.fontFamily)
        // body
        assertSame(Inter, themed.bodyLarge.fontFamily)
        assertSame(Inter, themed.bodySmall.fontFamily)
        // labels
        assertSame(Inter, themed.labelLarge.fontFamily)
        assertSame(Inter, themed.labelSmall.fontFamily)
    }

    private fun serifFor(stack: String) =
        ShardTypeface.resolve(mapOf("--serif" to stack)).serif

    private fun displayFor(stack: String) =
        ShardTypeface.resolve(mapOf("--display" to stack)).display

    private fun sansFor(stack: String) =
        ShardTypeface.resolve(mapOf("--sans" to stack)).sans

    /**
     * The type scale exactly as `ui/theme/Type.kt` declared it before M12, with the
     * three families it named directly.
     */
    private val preM12Typography = MaterialTypography(
        displayLarge = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 40.sp, lineHeight = 46.sp, letterSpacing = 0.4.sp,
        ),
        displayMedium = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.3.sp,
        ),
        displaySmall = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.2.sp,
        ),
        headlineLarge = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 26.sp, lineHeight = 34.sp, letterSpacing = 0.2.sp,
        ),
        headlineMedium = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 24.sp, lineHeight = 32.sp, letterSpacing = 0.2.sp,
        ),
        headlineSmall = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.2.sp,
        ),
        titleLarge = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 20.sp, lineHeight = 26.sp, letterSpacing = 0.2.sp,
        ),
        titleMedium = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 17.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp,
        ),
        titleSmall = TextStyle(
            fontFamily = Cinzel, fontWeight = FontWeight.SemiBold,
            fontSize = 15.sp, lineHeight = 22.sp, letterSpacing = 0.1.sp,
        ),
        bodyLarge = TextStyle(
            fontFamily = FontFamily.Serif, fontWeight = FontWeight.Normal,
            fontSize = 16.sp, lineHeight = 26.sp, letterSpacing = 0.15.sp,
        ),
        bodyMedium = TextStyle(
            fontFamily = FontFamily.Serif, fontWeight = FontWeight.Normal,
            fontSize = 15.sp, lineHeight = 24.sp, letterSpacing = 0.15.sp,
        ),
        bodySmall = TextStyle(
            fontFamily = FontFamily.Serif, fontWeight = FontWeight.Normal,
            fontSize = 13.sp, lineHeight = 20.sp, letterSpacing = 0.2.sp,
        ),
        labelLarge = TextStyle(
            fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Bold,
            fontSize = 14.sp, lineHeight = 18.sp, letterSpacing = 0.45.sp,
        ),
        labelMedium = TextStyle(
            fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Medium,
            fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp,
        ),
        labelSmall = TextStyle(
            fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Medium,
            fontSize = 11.sp, lineHeight = 15.sp, letterSpacing = 0.5.sp,
        ),
    )
}
