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

import com.runicgateway.app.data.api.dto.CharPointsDto
import com.runicgateway.app.data.api.dto.CharProfileDto
import com.runicgateway.app.data.api.dto.EquipmentDto
import com.runicgateway.app.data.api.dto.TitlesDto
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

/**
 * Unit tests for the character-sheet display helpers (PLAN.md §6.3), mirroring the
 * website's `CharacterSheet.jsx`: title selection over the server's cliloc-resolved
 * parallel array, item naming precedence, and the Protocol 3.0 points block.
 */
class CharacterSheetHelpersTest {

    @Test fun formatSkillDropsTrailingZero() {
        assertEquals("100", formatSkill(100.0))
        assertEquals("85.3", formatSkill(85.3))
    }

    @Test fun displayTitlesEmptyForNull() {
        assertEquals(emptyList<String>(), displayTitles(null))
    }

    @Test fun displayTitlesUsesFameKarmaSkillAndSelectedLiteralReward() {
        val titles = TitlesDto(
            selected = 1,
            reward = listOf("1049565", "Slayer of Dragons"),
            fameKarma = "The Great",
            skill = "Grandmaster Swordsman",
        )
        assertEquals(
            listOf("The Great", "Grandmaster Swordsman", "Slayer of Dragons"),
            displayTitles(titles),
        )
    }

    @Test fun displayTitlesSkipsNumericSelectedReward() {
        // Selected points at a bare cliloc number → skipped (no cliloc table).
        val titles = TitlesDto(selected = 0, reward = listOf("1049565"))
        assertEquals(emptyList<String>(), displayTitles(titles))
    }

    @Test fun displayTitlesFallsBackToFirstLiteralWhenNoneSelected() {
        val titles = TitlesDto(selected = -1, reward = listOf("1049565", "Champion"))
        assertEquals(listOf("Champion"), displayTitles(titles))
    }

    @Test fun displayTitlesDeduplicates() {
        val titles = TitlesDto(selected = 0, reward = listOf("The Great"), fameKarma = "The Great")
        assertEquals(listOf("The Great"), displayTitles(titles))
    }

    // ── Cliloc-resolved titles (Protocol 3.0 §8.6) ───────────────────────

    @Test fun displayTitlesPrefersTheServerResolvedRewardName() {
        // The website resolves the numeric entries against its own cliloc table and
        // sends a parallel array; the raw number is no longer the only thing we have.
        val titles = TitlesDto(
            selected = 0,
            reward = listOf("1049565"),
            rewardResolved = listOf("Knight of Trinsic"),
        )
        assertEquals(listOf("Knight of Trinsic"), displayTitles(titles))
    }

    @Test fun displayTitlesKeepsSelectedAlignedWhenAnEntryDoesNotResolve() {
        // rewardResolved is POSITIONAL. An entry the table had nothing for is null and
        // must be skipped WITHOUT shifting `selected` onto its neighbour — otherwise
        // the sheet confidently shows the wrong title.
        val titles = TitlesDto(
            selected = 1,
            reward = listOf("1049565", "1049566"),
            rewardResolved = listOf(null, "Knight of Trinsic"),
        )
        assertEquals(listOf("Knight of Trinsic"), displayTitles(titles))
    }

    @Test fun displayTitlesFallsBackWhenTheSelectedTitleDidNotResolve() {
        val titles = TitlesDto(
            selected = 0,
            reward = listOf("1049565", "1049566"),
            rewardResolved = listOf(null, "Bane of Dragons"),
        )
        assertEquals(listOf("Bane of Dragons"), displayTitles(titles))
    }

    @Test fun displayTitlesStillSkipsNumbersWhenNothingResolved() {
        // A shard that configures no cliloc table sends no rewardResolved at all —
        // the pre-3.0 behavior, unchanged.
        val titles = TitlesDto(selected = 0, reward = listOf("1049565"), rewardResolved = emptyList())
        assertEquals(emptyList<String>(), displayTitles(titles))
    }

    // ── Equipment names ──────────────────────────────────────────────────

    @Test fun itemLabelPrefersAPlayerGivenNameOverTheResolvedTypeName() {
        // "Bob's lucky axe" must not be relabelled "hatchet".
        val item = EquipmentDto(layer = "OneHanded", name = "Bob's lucky axe", clilocName = "hatchet")
        assertEquals("Bob's lucky axe", item.label)
    }

    @Test fun itemLabelFallsBackThroughClilocNameThenLayer() {
        assertEquals("hatchet", EquipmentDto(layer = "OneHanded", clilocName = "hatchet").label)
        assertEquals("OneHanded", EquipmentDto(layer = "OneHanded").label)
        assertNull(EquipmentDto().label)
    }

    // ── Loyalty & points (Protocol 3.0 §7.3) ─────────────────────────────

    @Test fun pointsLabelUsesTheHumanisedKeyWhenTheNameIsACliloc() {
        // The PRIMARY path on a real shard: most systems name themselves with a
        // cliloc, so nameString comes back null.
        assertEquals("Queens Loyalty", pointsLabel(CharPointsDto(system = "QueensLoyalty")))
        assertEquals("Clean Up Britannia", pointsLabel(CharPointsDto(system = "CleanUpBritannia")))
        assertEquals("Void Pool", pointsLabel(CharPointsDto(system = "VoidPool")))
    }

    @Test fun pointsLabelPrefersTheShardsOwnNameWhenItHasOne() {
        val entry = CharPointsDto(system = "QueensLoyalty", nameString = "Queen's Loyalty")
        assertEquals("Queen's Loyalty", pointsLabel(entry))
    }

    @Test fun anUncappedSystemReportsNoCap() {
        // maxPoints 0 means UNCAPPED and is the common case — three of five live
        // boards on a real shard. Nothing may divide by it.
        assertNull(CharPointsDto(points = 900, maxPoints = 0).cap)
        assertNull(CharPointsDto(points = 900, maxPoints = null).cap)
        assertEquals(30000L, CharPointsDto(points = 900, maxPoints = 30000).cap)
    }

    @Test fun displayPointsDropsZeroesAndSortsByStandingDescending() {
        val char = CharProfileDto(
            points = listOf(
                CharPointsDto(system = "A", points = 10),
                CharPointsDto(system = "Zero", points = 0),
                CharPointsDto(system = "B", points = 500),
                CharPointsDto(system = "Null", points = null),
            ),
        )
        assertEquals(listOf("B", "A"), displayPoints(char).map { it.system })
    }

    @Test fun displayPointsIsEmptyForAProfileWithNoPointsBlock() {
        // A pre-3.0 shard plugin sends none, and a new character has earned nothing —
        // both render as nothing at all rather than an empty card.
        assertEquals(emptyList<CharPointsDto>(), displayPoints(CharProfileDto()))
    }
}
