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

import com.runicgateway.app.data.api.dto.AtlasCreatureDto
import com.runicgateway.app.data.api.dto.AtlasPlaceDto
import com.runicgateway.app.data.api.dto.AtlasSpawnerDto
import com.runicgateway.app.data.api.dto.MarketListingDto
import com.runicgateway.app.data.api.dto.MarketLocationDto
import com.runicgateway.app.data.api.dto.PointsBoardDto
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

/**
 * The pure display helpers behind the four Protocol 3.0 screens (PLAN.md §9 M11).
 * Each one exists because the raw wire value would be wrong or misleading on screen —
 * units in tenths, delays in seconds, an "uncapped" cap of zero, a gated field.
 */
class ShardContentHelpersTest {

    // ── Rules (§5) ───────────────────────────────────────────────────────

    @Test fun skillCapsConvertOutOfTenths() {
        // 1000 is 100.0. Showing the raw number reads as a shard with ten times the
        // usual limit, which is worse than showing nothing.
        assertEquals("100", formatSkillCap(1000 / 10.0))
        assertEquals("72.5", formatSkillCap(725 / 10.0))
    }

    @Test fun systemKeysHumanise() {
        // Word boundaries become spaces and the inner capital is kept, matching the
        // website's `humanise` — "City Loyalty" is the system's actual name.
        assertEquals("City Loyalty", humaniseSystem("cityLoyalty"))
        assertEquals("Vvv", humaniseSystem("vvv"))
        assertEquals("Treasure Maps", humaniseSystem("treasureMaps"))
    }

    @Test fun theRestartScheduleOnlyShowsWhenTheShardRunsOne() {
        assertEquals("04:30", formatRestart(enabled = true, hour = 4, minute = 30))
        assertEquals("04:00", formatRestart(enabled = true, hour = 4, minute = null))
        assertNull(formatRestart(enabled = false, hour = 4, minute = 30))
        assertNull(formatRestart(enabled = true, hour = null, minute = 30))
    }

    // ── Leaderboards (§7) ────────────────────────────────────────────────

    @Test fun boardLabelFallsBackToTheHumanisedKey() {
        // The PRIMARY path: four of five boards on a real shard name themselves with a
        // cliloc and send nameString null.
        assertEquals("Queens Loyalty", boardLabel(PointsBoardDto(system = "QueensLoyalty")))
        assertEquals(
            "Queen's Loyalty",
            boardLabel(PointsBoardDto(system = "QueensLoyalty", nameString = "Queen's Loyalty")),
        )
        assertEquals("Clean Up Britannia", boardLabel(PointsBoardDto(system = "CleanUpBritannia", nameString = "  ")))
    }

    @Test fun anUncappedBoardReportsNoCap() {
        assertNull(PointsBoardDto(maxPoints = 0).cap)
        assertEquals(30000L, PointsBoardDto(maxPoints = 30000).cap)
    }

    @Test fun boardsOrderByContestedThenName() {
        val boards = listOf(
            PointsBoardDto(system = "Quiet", players = 2),
            PointsBoardDto(system = "Busy", players = 900),
            PointsBoardDto(system = "AlsoQuiet", players = 2),
        )
        assertEquals(listOf("Busy", "Also Quiet", "Quiet"), orderBoards(boards).map { boardLabel(it) })
    }

    @Test fun boardsTheShardHidesFromItsOwnGumpAreDropped() {
        // `showOnGump` is the shard's own "is this player-facing?" signal.
        val boards = listOf(
            PointsBoardDto(system = "Shown", players = 1, showOnGump = true),
            PointsBoardDto(system = "Internal", players = 99, showOnGump = false),
        )
        assertEquals(listOf("Shown"), orderBoards(boards).map { it.system })
    }

    // ── Market (§8) ──────────────────────────────────────────────────────

    @Test fun listingTitlePrefersAPlayerNameThenTheResolvedOne() {
        assertEquals("Bob's axe", listingTitle(MarketListingDto(name = "Bob's axe", displayName = "hatchet")))
        assertEquals("hatchet", listingTitle(MarketListingDto(displayName = "hatchet")))
    }

    @Test fun listingTitleIsNullWithoutAnyName() {
        // A shard with no cliloc table configured publishes neither, and the screen
        // falls back to the item id rather than inventing a label.
        assertNull(listingTitle(MarketListingDto(itemId = 3922)))
    }

    @Test fun aStackShowsItsCount() {
        // "12 × ingot" and "ingot" at the same price are very different offers.
        assertEquals("12 × ingot", listingTitle(MarketListingDto(displayName = "ingot", amount = 12)))
        assertEquals("ingot", listingTitle(MarketListingDto(displayName = "ingot", amount = 1)))
        assertEquals("ingot", listingTitle(MarketListingDto(displayName = "ingot", amount = null)))
    }

    @Test fun locationPrefersTheHouseThenTheRegion() {
        assertEquals(
            "Darrow's Tower, Felucca",
            locationLine(MarketLocationDto(map = "Felucca", region = "Britain", house = "Darrow's Tower")),
        )
        assertEquals("Britain, Felucca", locationLine(MarketLocationDto(map = "Felucca", region = "Britain")))
        assertEquals("Felucca", locationLine(MarketLocationDto(map = "Felucca")))
    }

    @Test fun aGatedLocationIsNullRatherThanAHalfAnswer() {
        // The block is nested precisely so one admin rule takes the facet, the
        // coordinates, the region and the house together — there is no partial state
        // to render.
        assertNull(locationLine(null))
        assertNull(locationLine(MarketLocationDto(x = 100, y = 200)))
    }

    // ── Atlas (§6) ───────────────────────────────────────────────────────

    @Test fun respawnDelaysAreReadAsSeconds() {
        // The API normalises XmlSpawner's mixed minutes/seconds, so these ARE seconds.
        assertEquals("30s", formatRespawn(30, 30))
        assertEquals("5m", formatRespawn(300, 300))
        assertEquals("5m–10m", formatRespawn(300, 600))
        assertEquals("1m 30s", formatRespawn(90, 90))
    }

    @Test fun aHalfSpecifiedRespawnStillReads() {
        assertEquals("5m", formatRespawn(300, null))
        assertEquals("5m", formatRespawn(null, 300))
        assertNull(formatRespawn(null, null))
    }

    @Test fun spawnerPlacePrefersTheServersPlacementLabel() {
        // The point-in-rect transform is the reason this feature exists: it turns
        // "5411,1234" into "Despise, Felucca".
        val spawner = AtlasSpawnerDto(
            label = "Despise, Felucca",
            region = "Despise",
            facet = "Felucca",
            x = 5411,
            y = 1234,
        )
        assertEquals("Despise, Felucca", spawnerPlace(spawner))
    }

    @Test fun spawnerPlaceFallsBackThroughRegionLandmarkThenCoordinates() {
        assertEquals(
            "Despise, Felucca",
            spawnerPlace(AtlasSpawnerDto(region = "Despise", facet = "Felucca")),
        )
        assertEquals(
            "Yew Crossroads, Trammel",
            spawnerPlace(AtlasSpawnerDto(landmark = "Yew Crossroads", facet = "Trammel")),
        )
        // ~17% of stock spawns resolve to no named place; coordinates are honest there.
        assertEquals(
            "Felucca 5411, 1234",
            spawnerPlace(AtlasSpawnerDto(facet = "Felucca", x = 5411, y = 1234)),
        )
    }

    @Test fun placeLabelUsesTheServersResolvedNameAndFallsBackToTheFacet() {
        assertEquals(
            "Isamu-Jima",
            placeLabel(AtlasPlaceDto(facet = "Tokuno", label = "Isamu-Jima", spawners = 4)),
        )
        // The server already falls back to "Wilderness", so a label-less place is the
        // degenerate case; the facet still says something, an empty row does not.
        assertEquals("Tokuno", placeLabel(AtlasPlaceDto(facet = "Tokuno")))
        assertEquals("Tokuno", placeLabel(AtlasPlaceDto(facet = "Tokuno", label = "  ")))
    }

    @Test fun facetSummaryLeadsWithWhereItMostlyIs() {
        val creature = AtlasCreatureDto(
            slug = "lizardman",
            facets = mapOf("Trammel" to 4, "Felucca" to 30, "Ilshenar" to 12),
        )
        assertEquals("Felucca, Ilshenar, Trammel", facetSummary(creature))
        assertNull(facetSummary(AtlasCreatureDto(slug = "unique")))
    }
}
