package com.fabledsword.minstrel.cache.mutations

import com.fabledsword.minstrel.cache.db.entities.CachedMutationEntity
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

/**
 * Collapse rules for desired-state toggles in the offline queue.
 *
 * The hazard this guards against is real and silent: without collapsing, a
 * queued snooze that replays AFTER the user's undo re-hides an artist they
 * asked to see again, and nothing surfaces the contradiction.
 */
class SupersededToggleIdsTest {

    private val json = Json { ignoreUnknownKeys = true }

    private fun snoozeRow(id: Long, mbid: String, desiredSnoozed: Boolean) = CachedMutationEntity(
        id = id,
        kind = MutationKind.SUGGESTION_SNOOZE_TOGGLE,
        payload = json.encodeToString(
            SuggestionSnoozeTogglePayload.serializer(),
            SuggestionSnoozeTogglePayload(mbid, "Name", desiredSnoozed),
        ),
    )

    private fun likeRow(id: Long, entityId: String, desired: Boolean) = CachedMutationEntity(
        id = id,
        kind = MutationKind.LIKE_TOGGLE,
        payload = json.encodeToString(
            LikeTogglePayload.serializer(),
            LikeTogglePayload("artist", entityId, desired),
        ),
    )

    @Test
    fun `a snooze followed by its undo drops the snooze`() {
        val rows = listOf(
            snoozeRow(1, "mb-a", desiredSnoozed = true),
            snoozeRow(2, "mb-a", desiredSnoozed = false),
        )
        // Only the later intent (the undo) survives to be replayed.
        assertEquals(setOf(1L), supersededToggleIds(rows, json))
    }

    @Test
    fun `toggles for different candidates never collapse into each other`() {
        val rows = listOf(
            snoozeRow(1, "mb-a", desiredSnoozed = true),
            snoozeRow(2, "mb-b", desiredSnoozed = true),
        )
        assertTrue(supersededToggleIds(rows, json).isEmpty())
    }

    @Test
    fun `only the newest of several toggles for one candidate survives`() {
        val rows = listOf(
            snoozeRow(1, "mb-a", desiredSnoozed = true),
            snoozeRow(2, "mb-a", desiredSnoozed = false),
            snoozeRow(3, "mb-a", desiredSnoozed = true),
        )
        assertEquals(setOf(1L, 2L), supersededToggleIds(rows, json))
    }

    // The kind is part of the collapse key, so a snooze and a like that happen
    // to share an id string must not shadow one another.
    @Test
    fun `a like and a snooze on the same id string do not collide`() {
        val rows = listOf(
            likeRow(1, "same-id", desired = true),
            snoozeRow(2, "same-id", desiredSnoozed = true),
        )
        assertTrue(supersededToggleIds(rows, json).isEmpty())
    }

    @Test
    fun `like toggles still collapse — the pre-existing behaviour is intact`() {
        val rows = listOf(
            likeRow(1, "artist-1", desired = true),
            likeRow(2, "artist-1", desired = false),
        )
        assertEquals(setOf(1L), supersededToggleIds(rows, json))
    }

    @Test
    fun `non-toggle kinds are never collapsed, even repeated for one entity`() {
        // Two appends to the same playlist are two real actions, not one
        // desired state — collapsing them would lose a write.
        val rows = listOf(
            CachedMutationEntity(
                id = 1,
                kind = MutationKind.PLAYLIST_APPEND,
                payload = json.encodeToString(
                    PlaylistAppendPayload.serializer(),
                    PlaylistAppendPayload("pl-1", listOf("t1")),
                ),
            ),
            CachedMutationEntity(
                id = 2,
                kind = MutationKind.PLAYLIST_APPEND,
                payload = json.encodeToString(
                    PlaylistAppendPayload.serializer(),
                    PlaylistAppendPayload("pl-1", listOf("t2")),
                ),
            ),
        )
        assertTrue(supersededToggleIds(rows, json).isEmpty())
    }

    // A row whose payload won't decode gets no key at all, rather than sharing
    // a "corrupt" bucket — otherwise one bad row could suppress a good one
    // behind it. The dispatcher DROPs the bad row on its own.
    @Test
    fun `an undecodable payload does not suppress a valid later row`() {
        val rows = listOf(
            CachedMutationEntity(
                id = 1,
                kind = MutationKind.SUGGESTION_SNOOZE_TOGGLE,
                payload = "{ not json",
            ),
            snoozeRow(2, "mb-a", desiredSnoozed = true),
        )
        assertTrue(supersededToggleIds(rows, json).isEmpty())
    }

    @Test
    fun `an empty queue collapses nothing`() {
        assertTrue(supersededToggleIds(emptyList(), json).isEmpty())
    }
}
