/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.data.api.dto

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonPrimitive
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Decoding tests for a player's own game-data DTOs (PLAN.md §6.3). Shapes come from
 * the website's `player/shard.controller.js` + `docs/link/INTEGRATION.md` §5. The
 * roster / char / vendor reads are permissive objects, so the parser must ignore
 * unknown keys (additive fields, §8) and tolerate hex-string serials.
 */
class PlayerShardDtoTest {

    private val json = Json {
        ignoreUnknownKeys = true
        explicitNulls = false
        coerceInputValues = true
    }

    @Test fun rosterDecodesCharsIncludingOffline() {
        val dto = json.decodeFromString<RosterDto>(
            """{"kind":"account.roster","acct":"whitlocktech",
               "chars":[{"slot":0,"serial":"0x24C","name":"Darrow","body":400,"online":false}]}""",
        )
        assertEquals("whitlocktech", dto.acct)
        assertEquals(1, dto.chars.size)
        assertEquals("0x24C", dto.chars[0].serial)
        assertEquals("Darrow", dto.chars[0].name)
    }

    @Test fun charProfileDecodesStatsSkillsEquipmentAndTitles() {
        val dto = json.decodeFromString<CharProfileDto>(
            """{"kind":"char.profile","serial":"0x24C","name":"Darrow","online":true,"acct":"wt",
               "stats":{"str":100,"dex":90,"int":75,"hits":100,"hitsMax":110,
                        "resist":{"phys":70,"fire":68,"cold":60,"pois":55,"energy":50}},
               "skills":[{"n":"Swordsmanship","base":100.0,"value":120.0,"cap":120.0},
                         {"n":"Anatomy","base":0.0,"value":0.0,"cap":100.0}],
               "equipment":[{"serial":"0x40","layer":"OneHanded","itemId":5040,"hue":1153,
                             "mods":{"DamageIncrease":50,"HitLightning":30}}],
               "titles":{"selected":0,"reward":["Knight"],"fameKarma":"The Great"},
               "guild":{"name":"Knights","abbr":"KNT"},"governorOf":["Britain"]}""",
        )
        assertEquals("Darrow", dto.name)
        assertTrue(dto.online)
        assertEquals(100, dto.stats?.str)
        assertEquals(70, dto.stats?.resist?.phys)
        assertEquals(2, dto.skills.size)
        assertEquals(120.0, dto.skills[0].value!!, 0.0)
        val mods = dto.equipment[0].mods
        assertNotNull(mods)
        assertEquals("50", mods!!["DamageIncrease"]!!.jsonPrimitive.content)
        assertEquals("KNT", dto.guild?.abbr)
        assertEquals(listOf("Britain"), dto.governorOf)
    }

    @Test fun vendorSnapshotDecodesListings() {
        val dto = json.decodeFromString<VendorSnapshotDto>(
            """{"kind":"vendor.snapshot","acct":"seed_000",
               "vendors":[{"serial":"0x2C0","shopName":"Seed Shop","holdGold":24186,
                 "ownerSerial":"0x1F5","map":"Felucca","x":1402,"y":1604,
                 "listings":[{"serial":"0x4001440F","itemId":3937,"amount":1,"price":69819,"forSale":true}]}]}""",
        )
        assertEquals(1, dto.vendors.size)
        assertEquals(24186L, dto.vendors[0].holdGold)
        assertEquals(69819L, dto.vendors[0].listings[0].price)
        assertTrue(dto.vendors[0].listings[0].forSale)
    }

    @Test fun vendorSaleDecodes() {
        val dto = json.decodeFromString<VendorSaleDto>(
            """{"t":1783720195626,"itemType":"Longsword","amount":1,"price":100,
               "commission":5,"ownerAcct":"whitlocktech"}""",
        )
        assertEquals(1783720195626L, dto.t)
        assertEquals("Longsword", dto.itemType)
        assertEquals(100L, dto.price)
    }

    @Test fun playerHouseDecodesHexSerialAndIdoc() {
        val dto = json.decodeFromString<PlayerHouseDto>(
            """{"serial":"0x4004705F","stage":"IDOC","map":"Trammel","x":1,"y":2,"z":3,
               "region":"Britain","name":"An Unnamed House","isIdoc":true,
               "ownerAcct":"wt","updatedAt":"2026-07-19T22:00:00Z"}""",
        )
        assertEquals("0x4004705F", dto.serial)
        assertEquals("IDOC", dto.stage)
        assertTrue(dto.isIdoc)
    }

    @Test fun linkResultDecodes() {
        val dto = json.decodeFromString<ShardLinkResultDto>("""{"linked":true,"account":"whitlocktech"}""")
        assertTrue(dto.linked)
        assertEquals("whitlocktech", dto.account)
    }

    // ── Protocol 3.0 additions to char.profile ───────────────────────────

    @Test fun charProfileDecodesThePointsBlock() {
        // Shaped like a real shard's reply: an uncapped board (maxPoints 0), a
        // cliloc-named board (nameString null), and no `rank` unless opted in.
        val dto = json.decodeFromString<CharProfileDto>(
            """{"serial":"0x24C","name":"Darrow",
               "points":[{"system":"QueensLoyalty","nameString":"Queen's Loyalty",
                          "points":29500,"maxPoints":30000,"rank":3},
                         {"system":"VoidPool","nameString":null,"points":180,"maxPoints":0}]}""",
        )
        assertEquals(2, dto.points.size)
        val queens = dto.points[0]
        assertEquals("Queen's Loyalty", queens.nameString)
        assertEquals(29500L, queens.points)
        assertEquals(30000L, queens.cap)
        assertEquals(3, queens.rank)

        val voidPool = dto.points[1]
        assertNull("maxPoints 0 means uncapped, not a zero cap", voidPool.cap)
        assertNull("rank is absent unless the shard opts in", voidPool.rank)
        assertNull(voidPool.nameString)
    }

    @Test fun charProfileWithoutAPointsBlockDecodesToEmpty() {
        // A shard plugin that predates Protocol 3.0 sends no `points` key at all.
        val dto = json.decodeFromString<CharProfileDto>("""{"serial":"0x24C","name":"Darrow"}""")
        assertEquals(emptyList<CharPointsDto>(), dto.points)
    }

    @Test fun equipmentDecodesTheServerResolvedClilocName() {
        val dto = json.decodeFromString<CharProfileDto>(
            """{"serial":"0x24C",
               "equipment":[{"serial":"0x40","layer":"OneHanded","itemId":5040,"cliloc":1023721,
                             "clilocName":"hatchet"},
                            {"serial":"0x41","layer":"Shirt","name":"Bob's lucky shirt",
                             "clilocName":"fancy shirt"}]}""",
        )
        assertEquals("hatchet", dto.equipment[0].label)
        assertEquals("Bob's lucky shirt", dto.equipment[1].label)
    }

    @Test fun titlesDecodeTheParallelResolvedArrayIncludingItsNulls() {
        // rewardResolved carries a null where the cliloc table had nothing; the array
        // must stay positionally aligned with `reward`.
        val dto = json.decodeFromString<TitlesDto>(
            """{"selected":1,"reward":["1049565","1049566"],
               "rewardResolved":[null,"Knight of Trinsic"]}""",
        )
        assertEquals(listOf("1049565", "1049566"), dto.reward)
        assertEquals(listOf(null, "Knight of Trinsic"), dto.rewardResolved)
    }
}
