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

import com.runicgateway.app.core.net.ShardStreamEvent
import com.runicgateway.app.data.api.dto.ChampDto
import com.runicgateway.app.data.api.dto.GovernorDto
import com.runicgateway.app.data.api.dto.GuildDto
import com.runicgateway.app.data.api.dto.HouseDto
import com.runicgateway.app.data.api.dto.PresenceDto
import com.runicgateway.app.data.api.dto.ShardStatusDto
import com.runicgateway.app.data.api.fake.FakePublicApi
import com.runicgateway.app.data.api.fake.FakeShardStream
import com.runicgateway.app.data.repository.ShardFeaturesRepository
import com.runicgateway.app.data.repository.ShardRepository
import com.runicgateway.app.ui.UiState
import com.runicgateway.app.util.MainDispatcherRule
import com.runicgateway.app.util.httpError
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test

/**
 * The live-board ViewModels (§6.2): a snapshot load kept live by merging SSE
 * frames. Tests use a [FakeShardStream] that emits a fixed script and completes,
 * so both the snapshot path and the frame-merge path are covered deterministically.
 */
class ShardBoardViewModelTest {

    @get:Rule val mainDispatcher = MainDispatcherRule()

    private val api = FakePublicApi()
    private val json = Json { ignoreUnknownKeys = true; explicitNulls = false; coerceInputValues = true }

    private fun repo(stream: FakeShardStream = FakeShardStream()) = ShardRepository(api, stream, json)

    // The hub reads the feature set only to filter its board tiles; these tests
    // exercise loading, so the answer stays at its "unknown" default (show all).
    private fun features() = ShardFeaturesRepository(api)

    // ── Champs: snapshot + live upsert/remove ─────────────────────────────
    @Test fun champsSeedsSnapshotAndMergesLiveFrames() {
        api.champs = listOf(ChampDto(serial = "0x1", category = "champion", name = "Rikktor"))
        val stream = FakeShardStream(
            listOf(
                ShardStreamEvent.Open,
                ShardStreamEvent.Frame(
                    "champ.update",
                    buildJsonObject { put("serial", "0x2"); put("category", "mini"); put("name", "Barracoon") },
                ),
                ShardStreamEvent.Frame("champ.remove", buildJsonObject { put("serial", "0x1") }),
            ),
        )
        val vm = ChampsViewModel(repo(stream))
        val rows = (vm.state.value as UiState.Success).data
        // 0x1 removed, 0x2 upserted.
        assertEquals(listOf("0x2"), rows.map { it.serial })
        assertTrue(vm.connected.value) // Open was seen
    }

    @Test fun champsServerErrorIsUiError() {
        api.error = httpError(503)
        assertTrue(ChampsViewModel(repo()).state.value is UiState.Error)
    }

    // ── Guilds ────────────────────────────────────────────────────────────
    @Test fun guildsSnapshotThenLiveUpdate() {
        api.guilds = listOf(GuildDto(id = 1, name = "Knights"))
        val stream = FakeShardStream(
            listOf(ShardStreamEvent.Frame("guild.update", buildJsonObject { put("id", 2); put("name", "Mages") })),
        )
        val vm = GuildsViewModel(repo(stream))
        val names = (vm.state.value as UiState.Success).data.map { it.name }
        assertTrue(names.contains("Knights"))
        assertTrue(names.contains("Mages"))
    }

    // ── Governors (+ history) ─────────────────────────────────────────────
    @Test fun governorsSnapshotAndHistory() {
        api.governors = listOf(GovernorDto(city = "Britain"))
        api.governorHistory = listOf() // empty is fine
        val vm = GovernorsViewModel(repo())
        assertTrue(vm.state.value is UiState.Success)
        vm.loadHistory("Britain")
        assertTrue(vm.history.value.containsKey("Britain"))
    }

    // ── Houses (IDOC board) ───────────────────────────────────────────────
    @Test fun housesSnapshotLoads() {
        api.houses = listOf(HouseDto(serial = "0x40", name = "Tower", isIdoc = true))
        val vm = HousesViewModel(repo())
        assertEquals("Tower", (vm.state.value as UiState.Success).data.first().name)
    }

    @Test fun housesNetworkErrorIsUiError() {
        api.error = java.io.IOException("offline")
        assertTrue(HousesViewModel(repo()).state.value is UiState.Error)
    }

    // ── Shard hub (status primary, presence/online best-effort, live feed) ─
    @Test fun shardHubLoadsStatusPresenceOnlineAndSeedsFeed() {
        api.shardStatus = ShardStatusDto(enabled = true, pluginConnected = true, onlineCount = 5)
        api.shardPresence = PresenceDto(count = 5)
        val stream = FakeShardStream(
            listOf(
                ShardStreamEvent.Closed,
                ShardStreamEvent.Frame("presence.online", buildJsonObject { put("count", 9) }),
            ),
        )
        val vm = ShardViewModel(repo(stream), features())
        val hub = (vm.state.value as UiState.Success).data
        assertTrue(hub.status.isOnline)
        // presence.online frame patched the count in place.
        assertEquals(9, hub.presence!!.count)
        assertFalse(vm.connected.value) // last lifecycle event was Closed
    }

    @Test fun shardHubStatusErrorIsUiError() {
        api.error = httpError(503)
        assertTrue(ShardViewModel(repo(), features()).state.value is UiState.Error)
    }
}
