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

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

/**
 * Decode tests for the four Protocol 3.0 content DTOs, against payloads captured from a
 * **live** server rather than hand-written to match the Kotlin types.
 *
 * These exist because the fakes in `data/api/fake/` construct DTOs directly, so no test
 * in the suite ever fed one real JSON — and `AtlasCreatureDto.places` shipped typed
 * `List<String>` while the server sends objects. That decodes to an exception, the
 * screen renders "something went wrong on the server", and 336 green tests say nothing.
 * Nullable-with-defaults protects against a *missing* field, never a *wrong type*.
 */
class ShardContentDtoTest {

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

    // ── Spawn atlas (§6) ─────────────────────────────────────────────────

    /** Trimmed from `GET /api/v1/public/atlas/creatures/seaserpent` on a real shard. */
    private val seaSerpent = """
        {"slug":"seaserpent","name":"SeaSerpent","total":6048,"points":477,
         "facets":{"Felucca":237,"Trammel":240},"art":"seaserpent.png",
         "places":[{"facet":"Felucca","label":"Wilderness","spawners":91,"maxAlive":1194},
                   {"facet":"Trammel","label":"Britain","spawners":12,"maxAlive":96}],
         "spawners":[{"id":1617,"facet":"Felucca","name":"SeaLife#68","x":1691,"y":1623,
                      "width":350,"height":350,"range":175,"maxCount":15,"minDelay":300,
                      "maxDelay":600,"todStart":0,"todEnd":0,"todMode":0,
                      "region":"Britain","landmark":null,"label":"Britain"}],
         "spawnersTruncated":true,
         "alsoHere":[{"slug":"waterelemental","name":"WaterElemental","shared":242}]}
    """.trimIndent()

    @Test fun atlasCreatureDetailDecodesTheRealPayload() {
        val creature = json.decodeFromString<AtlasCreatureDto>(seaSerpent)

        assertEquals("seaserpent", creature.slug)
        assertEquals(6048, creature.total)
        assertEquals(477, creature.points)
        assertEquals(240, creature.facets["Trammel"])
        assertTrue(creature.spawnersTruncated)
        assertEquals("seaserpent.png", creature.art)
    }

    @Test fun atlasPlacesAreObjectsNotStrings() {
        // The regression. `places` is the aggregate the screen exists to show, and it
        // arrives as {facet,label,spawners,maxAlive} — never as a bare place name.
        val places = json.decodeFromString<AtlasCreatureDto>(seaSerpent).places

        assertEquals(2, places.size)
        assertEquals("Wilderness", places[0].label)
        assertEquals("Felucca", places[0].facet)
        assertEquals(91, places[0].spawners)
        assertEquals(1194, places[0].maxAlive)
    }

    @Test fun atlasCreatureSurvivesAProjectedOrEmptyPayload() {
        // The search route sends no `places`/`spawners`/`art`, and the visibility
        // framework can drop any field from any of them.
        val lean = json.decodeFromString<AtlasCreatureDto>("""{"slug":"orc"}""")
        assertEquals("orc", lean.slug)
        assertTrue(lean.places.isEmpty())
        assertTrue(lean.spawners.isEmpty())
        assertNull(lean.art)

        val bare = json.decodeFromString<AtlasCreatureDto>("""{"places":[{}]}""")
        assertNull(bare.places[0].label)
        assertNull(bare.places[0].spawners)
    }

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

    @Test fun marketListingDecodesWithItsNestedVendorAndLocation() {
        // `location` nests on the wire so one visibility rule covers map/x/y/region/house.
        val listing = json.decodeFromString<MarketListingDto>(
            """{"serial":"0x40014A57","itemId":3937,"hue":1878,"amount":1,"price":115,
                "name":null,"cliloc":1023937,"displayName":"longsword","child":false,
                "vendor":{"serial":"0x2CB","shopName":"Seed Shop 225","ownerSerial":"0x201",
                          "ownerName":"Seed004A",
                          "location":{"map":"Felucca","x":1562,"y":1604,"z":0,
                                      "region":"Britain","house":"Seed House 4"}}}""",
        )

        assertEquals("longsword", listing.displayName)
        assertEquals(115L, listing.price)
        assertEquals("Seed Shop 225", listing.vendor?.shopName)
        assertEquals("Felucca", listing.vendor?.location?.map)
    }

    @Test fun marketListingSurvivesTheFieldsAVisitorMayNotSee() {
        // Below the `staff` rung the server omits ownerName/ownerSerial, and below
        // `player` the whole nested location. Neither may break the decode.
        val projected = json.decodeFromString<MarketListingDto>(
            """{"serial":"0x40014A57","price":115,"displayName":"longsword",
                "vendor":{"serial":"0x2CB","shopName":"Seed Shop 225"}}""",
        )

        assertEquals("Seed Shop 225", projected.vendor?.shopName)
        assertNull(projected.vendor?.ownerName)
        assertNull(projected.vendor?.location)
    }

    // ── Points boards (§7) ───────────────────────────────────────────────

    @Test fun pointsBoardDecodesAnEmptyBoardAndItsCap() {
        // A shard with nothing scored yet is the common case, not an error, and
        // maxPoints 0 is the "uncapped" sentinel rather than a cap of zero.
        val board = json.decodeFromString<PointsBoardDto>(
            """{"kind":"points.board","system":"QueensLoyalty","nameNumber":1095163,
                "nameString":null,"players":0,"maxPoints":15000,"showOnGump":true,
                "top":[],"t":1785556444154,"updatedAt":"2026-08-01T03:54:04.000Z"}""",
        )

        assertEquals("QueensLoyalty", board.system)
        assertEquals(15000L, board.maxPoints)
        assertNull(board.nameString)
        assertTrue(board.top.isEmpty())
    }

    // ── Ruleset (§5) ─────────────────────────────────────────────────────

    @Test fun rulesetDecodesTheNestedSectionsAndTolerantlySkipsUnknownOnes() {
        // The frame is built from an allowlist that grows with the shard's config; a
        // key this client has never heard of must not break the rules page.
        val ruleset = json.decodeFromString<RulesetDto>(
            """{"kind":"world.ruleset","shard":"My Shard","expansion":"EJ",
                "caps":{"skill":1000,"totalSkill":7000,"stat":225,"str":125},
                "systems":{"factions":false,"vvv":true,"siege":false},
                "accounts":{"charSlots":7,"perIp":3},
                "somethingAddedLater":{"nested":true}}""",
        )

        assertEquals("My Shard", ruleset.shard)
        assertEquals("EJ", ruleset.expansion)
        // Caps arrive in tenths; the DTO's computed property is what the screen shows.
        assertEquals(7000, ruleset.caps?.totalSkill)
        assertEquals(700.0, ruleset.caps?.totalSkillCap!!, 0.0)
        assertEquals(false, ruleset.systems["factions"])
        assertEquals(true, ruleset.systems["vvv"])
    }
}
