/*
 * 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.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Decoding tests for the shard DTOs (PLAN.md §6.2). Shapes come from the website's
 * `public/shard.controller.js`; the parser must ignore unknown keys (additive
 * backend fields, §8) and decode the live `*.update` SSE frames — which carry a
 * `kind` and category-specific extras — into the same board DTOs.
 */
class ShardDtoTest {

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

    @Test fun statusDecodesAndDerivesOnline() {
        val dto = json.decodeFromString<ShardStatusDto>(
            """{"enabled":true,"status":"connected","pluginConnected":true,
               "lastEventAt":"2026-07-19T22:00:00Z","onlineCount":42,
               "economy":{"accounts":900,"gold":123456789.0,"t":1721426400000}}""",
        )
        assertTrue(dto.isOnline)
        assertEquals(42, dto.onlineCount)
        assertEquals(123456789.0, dto.economy?.gold!!, 0.0)
    }

    @Test fun statusOfflineWhenPluginDisconnected() {
        val dto = json.decodeFromString<ShardStatusDto>(
            """{"enabled":true,"status":"disconnected","pluginConnected":false,"onlineCount":0}""",
        )
        assertFalse(dto.isOnline)
    }

    @Test fun champUpdateFrameDecodesWithKindAndExtras() {
        // A live champ.update frame: has `kind`, a hex-string `serial` (INTEGRATION.md
        // §1 — serials are opaque hex keys, never numbers), and category extras. The
        // `kind`/`rank`/`autoRestart` fields are ignored (not on the DTO); extras decode.
        val dto = json.decodeFromString<ChampDto>(
            """{"kind":"champ.update","serial":"0x40012345","category":"champion","name":"Barracoon",
               "status":"active","active":true,"level":10,"rank":3,"maxKills":250,"kills":120,
               "autoRestart":true,"bossUp":false,"map":"Felucca","x":5571,"y":1379,"z":0,"t":1721426400000}""",
        )
        assertEquals("0x40012345", dto.serial)
        assertEquals("champion", dto.category)
        assertEquals(120, dto.kills)
        assertTrue(dto.active)
    }

    @Test fun guildFrameDecodesLeaderActor() {
        // The leader actor carries a hex-string serial and a string webId (the linked
        // site-user id) — the exact wire shape from INTEGRATION.md §7.
        val dto = json.decodeFromString<GuildDto>(
            """{"kind":"guild.update","id":7,"name":"Knights","abbr":"KNT","members":12,
               "online":3,"alliance":"Light",
               "leader":{"serial":"0x1A2B","name":"Arthur","acct":"art","webId":"9931","player":true}}""",
        )
        assertEquals(7L, dto.id)
        assertEquals("0x1A2B", dto.leader?.serial)
        assertEquals("9931", dto.leader?.webId)
        assertEquals("Arthur", dto.leader?.label)
        assertEquals(12, dto.members)
    }

    @Test fun governorFrameDecodesNullGovernor() {
        val dto = json.decodeFromString<GovernorDto>(
            """{"kind":"city.update","city":"Britain","governor":null,"electionPhase":"nominations"}""",
        )
        assertEquals("Britain", dto.city)
        assertNull(dto.governor)
        assertEquals("nominations", dto.electionPhase)
    }

    @Test fun presenceDecodesMaps() {
        val dto = json.decodeFromString<PresenceDto>(
            """{"count":37,"byFacet":{"Felucca":10,"Trammel":27},"byRegion":{"Britain":5},"t":1}""",
        )
        assertEquals(37, dto.count)
        assertEquals(27, dto.byFacet["Trammel"])
        assertEquals(5, dto.byRegion["Britain"])
    }

    @Test fun houseDecodesPublicIdocShape() {
        val dto = json.decodeFromString<HouseDto>(
            """{"serial":"0x40001234","name":"Tower","region":"Britain","map":"Felucca",
               "x":1,"y":2,"z":3,"isIdoc":true}""",
        )
        assertEquals("0x40001234", dto.serial)
        assertTrue(dto.isIdoc)
    }

    @Test fun onlineStaffDecodesHexSerial() {
        val dto = json.decodeFromString<OnlineStaffDto>(
            """{"serial":"0x24C","name":"Darrow"}""",
        )
        assertEquals("0x24C", dto.serial)
        assertEquals("Darrow", dto.name)
    }

    @Test fun actorLabelFallsBackToAcctThenSomeone() {
        assertEquals("bob", ActorDto(acct = "bob").label)
        assertEquals("Someone", ActorDto().label)
    }

    @Test fun actorArrivesWithoutAcctOrWebIdBelowTheAdminRung() {
        // Those two fields are locked to `admin` by the visibility framework and are
        // stripped from every response below it — the app must decode their absence,
        // not depend on them (docs/link/v3.md §3.4 rule 1).
        val dto = json.decodeFromString<ActorDto>("""{"serial":"0x24C","name":"Darrow","player":true}""")
        assertEquals("Darrow", dto.label)
        assertNull(dto.acct)
        assertNull(dto.webId)
    }

    @Test fun shardFeaturesDecodesTheRungAndVisibleSet() {
        val dto = json.decodeFromString<ShardFeaturesDto>(
            """{"level":"player","features":["status","champs","guilds","market"]}""",
        )
        assertEquals("player", dto.level)
        assertTrue(dto.features.contains("market"))
        assertEquals(4, dto.features.size)
    }

    @Test fun shardFeaturesDecodesAnEmptySet() {
        // A fully-gated shard: every feature switched off for this viewer. Distinct
        // from the lookup failing, which the repository represents as null.
        val dto = json.decodeFromString<ShardFeaturesDto>("""{"level":"anonymous","features":[]}""")
        assertEquals(emptyList<String>(), dto.features)
    }
}
