package com.ponzischeme89.memby.ui

import com.ponzischeme89.memby.data.HomeCache
import com.ponzischeme89.memby.data.Settings
import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.HomeRow
import com.ponzischeme89.memby.data.model.UserItemData
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * The home screen is composed by the gateway. These pin the parts of that contract the
 * client is responsible for: honouring the user's section toggles, always showing rows
 * the server invented, and surviving a cold start from cache.
 */
class ServerHomeRowsTest {
    @Test
    fun `profile row preferences hide pin and order server rows`() {
        val serverRows = listOf(
            HomeRow("continue", "Continue", "continue", emptyList()),
            HomeRow("recommended", "Recommended", "recommendation", emptyList()),
            HomeRow("latest-movies", "Latest", "latest", emptyList()),
            HomeRow("seasonal", "Seasonal", "recommendation", emptyList()),
        )
        val settings = Settings(
            homeRowOrder = "seasonal\nrecommended\ncontinue",
            homePinnedRows = "recommended",
            homeHiddenRows = "latest-movies",
        )

        val result = serverHomeRows(
            HomeUiState(rows = serverRows, loading = emptySet()),
            settings,
        )

        assertEquals(listOf("recommended", "seasonal", "continue"), result.map { it.id })
    }


    private val json = Json { ignoreUnknownKeys = true }

    private fun row(id: String, kind: String, vararg itemIds: String) = HomeRow(
        id = id,
        title = id.replaceFirstChar(Char::uppercase),
        kind = kind,
        items = itemIds.map { BaseItem(id = it) },
    )

    private val serverRows = listOf(
        row("continue", "continue", "a"),
        row("favorites", "favorites", "c"),
        row("latest-movies", "latest", "d"),
        row("similar:sev", "similar", "e"),
        row("recommended", "recommended", "f"),
    )

    @Test
    fun `server row order and titles are preserved`() {
        val rows = serverHomeRows(HomeUiState(rows = serverRows, loading = emptySet()), Settings())

        assertEquals(
            listOf("continue", "favorites", "latest-movies", "similar:sev", "recommended"),
            rows.map { it.id },
        )
        assertEquals("Recommended", rows.last().title)
    }

    /**
     * A gateway that predates the merge — or, far more commonly, the home cache written by
     * the previous build, which is what a TV draws before its first refresh lands — still
     * carries a `nextup` row. Its episodes belong on the end of Continue Watching rather
     * than in a row of their own.
     */
    @Test
    fun `a legacy next up row folds into continue watching`() {
        val legacy = listOf(
            row("continue", "continue", "a"),
            row("next-up", "nextup", "b"),
            row("favorites", "favorites", "c"),
        )

        val rows = serverHomeRows(HomeUiState(rows = legacy, loading = emptySet()), Settings())

        assertEquals(listOf("continue", "favorites"), rows.map { it.id })
        assertEquals(listOf("a", "b"), rows.first().items.map { it.id })
    }

    /** A show already part-way through must not also appear as the episode after it. */
    @Test
    fun `folding a legacy next up row drops shows already in progress`() {
        val legacy = listOf(
            HomeRow(
                id = "continue",
                title = "Continue Watching",
                kind = "continue",
                items = listOf(BaseItem(id = "s1e3", seriesId = "show")),
            ),
            HomeRow(
                id = "next-up",
                title = "Next Up",
                kind = "nextup",
                items = listOf(
                    BaseItem(id = "s1e4", seriesId = "show"),
                    BaseItem(id = "other-e1", seriesId = "other"),
                ),
            ),
        )

        val rows = serverHomeRows(HomeUiState(rows = legacy, loading = emptySet()), Settings())

        assertEquals(listOf("s1e3", "other-e1"), rows.single().items.map { it.id })
    }

    @Test
    fun `favorites row uses the friendly profile name`() {
        val peter = serverHomeRows(
            HomeUiState(rows = serverRows, loading = emptySet()),
            Settings(username = "PeterC"),
        )
        val paul = serverHomeRows(
            HomeUiState(rows = serverRows, loading = emptySet()),
            Settings(username = "PaulR"),
        )

        assertEquals("Peter's Favourites", peter.first { it.id == "favorites" }.title)
        assertEquals("Paul's Favourites", paul.first { it.id == "favorites" }.title)
    }

    @Test
    fun `friendly profile name only removes a clear trailing initial`() {
        assertEquals("Peter", friendlyProfileName("PeterC"))
        assertEquals("Paul", friendlyProfileName(" PaulR "))
        assertEquals("MattCohen", friendlyProfileName("MattCohen"))
        assertEquals("CHRIS", friendlyProfileName("CHRIS"))
        assertEquals("PJ", friendlyProfileName("PJ"))
        assertEquals("Chris' Favourites", personalisedFavouritesTitle("Chris"))
        assertEquals("Favourites", personalisedFavouritesTitle(null))
    }

    @Test
    fun `successful login welcome uses authenticated username`() {
        assertTrue(loginWelcomeMessage(" Matt ").startsWith("Welcome to Memby, Matt. "))
    }

    @Test
    fun `disabling a section hides its rows but never the recommendations`() {
        val settings = Settings(homeSections = "continue")

        val rows = serverHomeRows(HomeUiState(rows = serverRows, loading = emptySet()), settings)

        assertEquals(
            listOf("continue", "similar:sev", "recommended"),
            rows.map { it.id },
        )
    }

    @Test
    fun `recommendation rows render as poster cards`() {
        val rows = serverHomeRows(HomeUiState(rows = serverRows, loading = emptySet()), Settings())
            .associateBy { it.id }

        assertEquals(MediaRowKind.CONTINUE, rows.getValue("continue").kind)
        assertEquals(MediaRowKind.FAVORITES, rows.getValue("favorites").kind)
        assertEquals(MediaRowKind.MOVIES, rows.getValue("recommended").kind)
        assertEquals(MediaRowKind.MOVIES, rows.getValue("similar:sev").kind)
    }

    @Test
    fun `For You destination keeps reasons visible and exposes loading placeholder`() {
        val loading = forYouBrowseRows(ForYouUiState(loading = true))
        assertEquals(1, loading.size)
        assertTrue(loading.single().loading)

        val rows = forYouBrowseRows(
            ForYouUiState(
                rows = listOf(
                    row("for-you:picks", "for-you", "pick"),
                    row("for-you:because:six-feet-under", "for-you", "because"),
                    row("for-you:genre:drama", "for-you", "genre"),
                ),
                availableMinutes = 60,
            ),
        )
        assertEquals(
            listOf(
                "for-you:picks",
                "for-you:because:six-feet-under",
                "for-you:genre:drama",
            ),
            rows.map { it.id },
        )
        assertTrue(rows.all { it.kind == MediaRowKind.MOVIES })
        assertTrue(rows.all { it.showSecondaryMetadata })
    }

    @Test
    fun `shows destination includes empty favorites and ranked server shelves`() {
        val rankedRows = listOf(
            row("curated:comedy-shows", "shows", "c1"),
            row("curated:horror-shows", "shows", "h1"),
            row("curated:drama-shows", "shows", "d1"),
        )
        val state = HomeUiState(
            rows = serverRows + rankedRows,
            favorites = emptyList(),
            loading = emptySet(),
        )

        val rows = homeRowsFor(BrowseDestination.SHOWS, state, Settings())

        assertEquals(
            listOf(
                "continue-shows",
                "favourite-shows",
                "curated:comedy-shows",
                "curated:horror-shows",
                "curated:drama-shows",
            ),
            rows.map { it.id },
        )
        assertTrue(rows.first { it.id == "favourite-shows" }.items.isEmpty())
    }

    @Test
    fun `movies destination uses personalised genre and studio shelves`() {
        val rankedRows = listOf(
            row("curated:movies:studio:pixar", "movies", "p1"),
            row("curated:movies:genre:animation", "movies", "a1"),
            row("curated:drama-shows", "shows", "s1"),
            row("recommended", "recommended", "mixed"),
        )
        val state = HomeUiState(
            rows = serverRows + rankedRows,
            favorites = listOf(BaseItem(id = "f1", type = "Movie")),
            latestMovies = listOf(BaseItem(id = "latest", type = "Movie")),
            loading = emptySet(),
        )

        val rows = homeRowsFor(BrowseDestination.MOVIES, state, Settings())

        assertEquals(
            listOf(
                "curated:movies:studio:pixar",
                "curated:movies:genre:animation",
                "favourite-movies",
            ),
            rows.map { it.id },
        )
        assertTrue(rows.none { it.id == "latest-movies" || it.id == "recommended" })
    }

    @Test
    fun `home and library destinations have distinct shelves`() {
        val state = HomeUiState(
            rows = serverRows + listOf(
                row("curated:drama-shows", "shows", "show"),
                row("curated:movies:genre:drama", "movies", "movie"),
            ),
            loading = emptySet(),
        )

        val home = homeRowsFor(BrowseDestination.HOME, state, Settings())
        val shows = homeRowsFor(BrowseDestination.SHOWS, state, Settings())
        val movies = homeRowsFor(BrowseDestination.MOVIES, state, Settings())

        assertTrue(home.none { it.id.startsWith("curated:") })
        assertTrue(shows.any { it.id == "curated:drama-shows" })
        assertTrue(movies.any { it.id == "curated:movies:genre:drama" })
    }

    @Test
    fun `discovery shelves do not repeat a continued series or played title`() {
        val continued = BaseItem(id = "episode", type = "Episode", seriesId = "hard-man")
        val repeatedSeries = BaseItem(id = "hard-man", type = "Series")
        val playedMovie = BaseItem(
            id = "played",
            type = "Movie",
            userData = UserItemData(played = true),
        )
        val fresh = (1..4).map { BaseItem(id = "fresh-$it", type = "Series") }
        val rows = deduplicateBrowseRows(
            listOf(
                HomeBrowseRow(
                    id = "continue",
                    title = "Continue",
                    items = listOf(continued),
                    kind = MediaRowKind.CONTINUE,
                    emptyMessage = "empty",
                ),
                HomeBrowseRow(
                    id = "curated:drama-shows",
                    title = "Drama",
                    items = listOf(repeatedSeries, playedMovie) + fresh,
                    kind = MediaRowKind.SHOWS,
                    emptyMessage = "empty",
                ),
            ),
        )

        assertEquals(
            fresh.map { it.id },
            rows.last().items.map { it.id },
        )
    }

    @Test
    fun `TV schedule rows use show cards and cannot be hidden by old preferences`() {
        val schedule = row("sonarr-airing-today", "schedule", "sonarr:7:42")

        val rows = serverHomeRows(
            HomeUiState(rows = listOf(schedule), loading = emptySet()),
            Settings(homeSections = ""),
        )

        assertEquals(1, rows.size)
        assertEquals(MediaRowKind.SHOWS, rows.single().kind)
        assertEquals("No monitored shows are airing in the next 5 days", rows.single().emptyMessage)
    }

    @Test
    fun `TV schedule cards are ordered by their air date`() {
        val friday = BaseItem(
            id = "friday",
            membyAirsAt = "2026-08-07T20:00:00+12:00",
        )
        val wednesday = BaseItem(
            id = "wednesday",
            membyAirsAt = "2026-08-05T20:00:00+12:00",
        )
        val schedule = HomeRow(
            id = "sonarr-airing-today",
            title = "Shows airing in the next 5 days",
            kind = "schedule",
            items = listOf(friday, wednesday),
        )

        val cards = serverHomeRows(
            HomeUiState(rows = listOf(schedule), loading = emptySet()),
            Settings(),
        ).single().items

        assertEquals(listOf("wednesday", "friday"), cards.map { it.id })
    }

    @Test
    fun `Radarr schedule rows use movie cards and explain an empty digital window`() {
        val schedule = row("radarr-upcoming-movies", "movie-schedule", "radarr:7")

        val rows = serverHomeRows(
            HomeUiState(rows = listOf(schedule), loading = emptySet()),
            Settings(homeSections = ""),
        )

        assertEquals(1, rows.size)
        assertEquals(MediaRowKind.MOVIES, rows.single().kind)
        assertEquals(
            "No monitored movies have a digital release in the next 5 days",
            rows.single().emptyMessage,
        )
    }

    /**
     * The hero row is the four featured cards above the shelves. HomeMovieHero consumes
     * it, so letting it through here would print the same four titles a second time as an
     * unnamed row of posters directly beneath the hero they are already in.
     */
    @Test
    fun `the hero row is consumed by the hero, never drawn as a shelf`() {
        val rows = serverHomeRows(
            HomeUiState(
                rows = listOf(
                    row("hero", SERVER_HERO_ROW_KIND, "featured"),
                    row("latest-movies", "latest", "d"),
                ),
                loading = emptySet(),
            ),
            Settings(),
        )

        assertEquals(listOf("latest-movies"), rows.map { it.id })
    }

    @Test
    fun `an unknown row kind from a newer server still renders`() {
        val rows = serverHomeRows(
            HomeUiState(rows = listOf(row("collection:halloween", "collection", "x")), loading = emptySet()),
            Settings(),
        )

        assertEquals(1, rows.size)
        assertEquals(MediaRowKind.MOVIES, rows.single().kind)
    }

    @Test
    fun `only empty rows show a loading state while a refresh is running`() {
        val rows = serverHomeRows(
            HomeUiState(
                rows = listOf(row("continue", "continue", "a"), row("recommended", "recommended")),
                loading = setOf(HomeSection.CONTINUE),
            ),
            Settings(),
        ).associateBy { it.id }

        assertEquals(false, rows.getValue("continue").loading)
        assertEquals(true, rows.getValue("recommended").loading)
    }

    @Test
    fun `rows survive a round trip through the on-device cache`() {
        val state = HomeUiState(rows = serverRows, loading = emptySet())

        val encoded = Json.encodeToString(HomeCache.serializer(), state.toCache())
        val restored = HomeUiState.from(Json.decodeFromString<HomeCache>(encoded))

        assertEquals(serverRows.map { it.id }, restored.rows.map { it.id })
        assertEquals("f", restored.rows.last().items.single().id)
    }

    @Test
    fun `maintenance is a distinct state from an ordinary refresh error`() {
        val offline = HomeUiState(rows = serverRows, maintenanceMessage = "Back at 9pm", hasRefreshError = true)
        val slow = HomeUiState(rows = serverRows, hasRefreshError = true)

        // The screen keys off maintenanceMessage; a slow connection must not trigger it.
        assertEquals("Back at 9pm", offline.maintenanceMessage)
        assertEquals(null, slow.maintenanceMessage)

        // Rows survive underneath, so returning from maintenance does not start empty.
        assertEquals(serverRows.size, serverHomeRows(offline, Settings()).size)
    }

    @Test
    fun `a cache written before rows existed still decodes`() {
        val legacy = """{"continueWatching":[{"Id":"a"}],"favorites":[],"nextUp":[],"latestMovies":[]}"""

        val restored = HomeUiState.from(json.decodeFromString<HomeCache>(legacy))

        assertTrue(restored.rows.isEmpty())
        assertEquals("a", restored.continueWatching.single().id)
    }
}
