package com.ponzischeme89.memby.ui

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.unit.dp
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import com.github.takahirom.roborazzi.captureRoboImage
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.EmbyPerson
import com.ponzischeme89.memby.data.model.MediaStream
import com.ponzischeme89.memby.data.model.Studio
import com.ponzischeme89.memby.data.model.UserItemData
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode

/**
 * Renders an episode's own detail page to PNGs under `build/screenshots/episode-detail/`.
 *
 * ```powershell
 * .\gradlew.bat :app:testDebugUnitTest --tests "*EpisodeDetailScreenshotTest"
 * ```
 *
 * The viewer here is part-way through season 3 of a four-season show, which is the state
 * the season scroller exists for: seasons 1 and 2 dimmed and ticked, 3 selected and
 * flagged, 4 still ahead. As in [DetailPageScreenshotTest] there is no network, so every
 * artwork URL resolves to null and the page renders on its own scrims — the worst case and
 * the one worth looking at.
 */
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [34], qualifiers = "w960dp-h540dp-television-xhdpi")
class EpisodeDetailScreenshotTest {

    @get:Rule
    val compose = createComposeRule()

    @Before
    fun locator() {
        ServiceLocator.init(ApplicationProvider.getApplicationContext())
    }

    /** The opening frame: series logo, season and episode number, episode title. */
    @Test
    fun `episode page`() {
        capture("df_episode-detail") { EpisodePage() }
    }

    /** Part-watched, which is how an episode reaches this page from Continue Watching. */
    @Test
    fun `episode page resuming`() {
        capture("df_episode-detail-resuming") {
            EpisodePage(item = current.copy(userData = UserItemData(playbackPositionTicks = 21L * TICKS_PER_MINUTE)))
        }
    }

    /** The first frame, before the series' episode list lands. The hero must be whole. */
    @Test
    fun `episode page loading`() {
        capture("df_episode-detail-loading") { EpisodePage(episodes = null) }
    }

    /** Season 1: behind the viewer, so its stop is ticked and its episodes all watched. */
    @Test
    fun `episode page browsing an earlier season`() {
        captureSeason("df_episode-detail-earlier-season", season = 1) { EpisodePage() }
    }

    /** Season 4: still ahead, and nothing in it is dimmed. */
    @Test
    fun `episode page browsing a later season`() {
        captureSeason("df_episode-detail-later-season", season = 4) { EpisodePage() }
    }

    /** Specials, which sort first and are the one stop that is not "Season n". */
    @Test
    fun `episode page browsing specials`() {
        captureSeason("df_episode-detail-specials", season = 0) {
            EpisodePage(episodes = library + episode(0, 1, "The Making of Signal Hill"))
        }
    }

    /**
     * The pane at the geometry the scaffold gives it — the band the D-pad reaches by
     * pressing Down twice, and the only place the episode this page is about is flagged.
     */
    @Test
    fun `episode list pane at its real slot geometry`() {
        capturePane("df_episode-detail-pane") {
            EpisodeSeasonPane(
                episodes = library,
                seasonEpisodes = library.filter { it.parentIndexNumber == 3 },
                currentEpisodeId = current.id,
                loadFailed = false,
                firstEpisodeFocusRequester = FocusRequester(),
                emptyFocusRequester = FocusRequester(),
                aboveEpisodes = FocusRequester.Default,
                // Where the page opens the list: on the episode it is about, not at the
                // top of the season.
                listState = LazyListState(firstVisibleItemIndex = 3),
                onPlay = {},
            )
        }
    }

    /** A season the household is done with: every row ticked, none of them flagged. */
    @Test
    fun `finished season pane`() {
        capturePane("df_episode-detail-pane-watched") {
            EpisodeSeasonPane(
                episodes = library,
                seasonEpisodes = library.filter { it.parentIndexNumber == 1 },
                currentEpisodeId = current.id,
                loadFailed = false,
                firstEpisodeFocusRequester = FocusRequester(),
                emptyFocusRequester = FocusRequester(),
                aboveEpisodes = FocusRequester.Default,
                listState = LazyListState(),
                onPlay = {},
            )
        }
    }

    @Composable
    private fun EpisodePage(
        item: BaseItem = current,
        episodes: List<BaseItem>? = library,
    ) {
        EpisodeDetailContent(
            item = item,
            episodes = episodes,
            loadFailed = false,
            onPlay = {},
            onToggleFavorite = { _, _ -> },
            onTogglePlayed = { _, _ -> },
        )
    }

    private fun capturePane(name: String, pane: @Composable () -> Unit) {
        compose.setContent {
            PreviewSurface(alignment = Alignment.TopStart) {
                Box(
                    Modifier
                        .fillMaxWidth()
                        .padding(horizontal = DetailSideGutter, vertical = 16.dp)
                        .height(detailPaneHeight(540.dp)),
                ) { pane() }
            }
        }
        compose.onRoot().captureRoboImage("$DIR/$name.png")
    }

    private fun capture(name: String, content: @Composable () -> Unit) {
        compose.setContent { PreviewSurface(alignment = Alignment.TopStart) { content() } }
        compose.onRoot().captureRoboImage("$DIR/$name.png")
    }

    private fun captureSeason(name: String, season: Int, content: @Composable () -> Unit) {
        compose.setContent { PreviewSurface(alignment = Alignment.TopStart) { content() } }
        compose.onNodeWithTag("season-stop-$season").performClick()
        compose.onRoot().captureRoboImage("$DIR/$name.png")
    }

    private val cast = listOf(
        person("Aria Vance", "Detective Iris Kell"),
        person("Marcus Oyelaran", "Samuel Reed"),
        person("Nina Kowalczyk", "Dr. Halvorsen"),
    )

    private val streams = listOf(
        MediaStream(type = "Video", codec = "hevc", width = 3840, height = 2160, videoRange = "HDR", videoRangeType = "HDR10"),
        MediaStream(type = "Audio", codec = "eac3", channels = 6, language = "eng"),
        MediaStream(type = "Subtitle", codec = "subrip", language = "eng"),
    )

    private val current = episode(
        season = 3,
        number = 4,
        title = "The Long Count",
        overview = "The night shift picks up a voice reading tomorrow's shipping forecast, " +
            "and the log book from 1974 says the same thing happened before. Iris takes the " +
            "tape to the only person who was on the hill that winter.",
    ).copy(
        productionYear = 2024,
        officialRating = "TV-MA",
        communityRating = 8.7,
        genres = listOf("Thriller", "Drama"),
        studios = listOf(Studio(name = "Harbour Line")),
        mediaStreams = streams,
        people = cast,
    )

    private val seasonOneTitles = listOf(
        "Carrier Wave", "Dead Air", "Nightingale", "Six Weeks Out", "The Hill", "Landfall",
    )

    private val seasonThreeTitles = listOf(
        "Shortwave", "Ground Truth", "The Operator", "The Long Count",
        "Blackout", "Tape Nine", "What the Log Says", "Signal Hill",
    )

    private val library = buildList {
        (1..6).forEach { add(episode(1, it, seasonOneTitles[it - 1], played = true)) }
        (1..8).forEach { add(episode(2, it, "Season Two, Part $it", played = it <= 6)) }
        (1..8).forEach { add(episode(3, it, seasonThreeTitles[it - 1], played = it < 4)) }
        (1..6).forEach { add(episode(4, it, "Season Four, Part $it")) }
    }

    private fun episode(
        season: Int,
        number: Int,
        title: String,
        played: Boolean = false,
        overview: String = "A coastal radio station keeps receiving a broadcast that has " +
            "not been transmitted yet, and the night operator has started writing it down.",
    ) = BaseItem(
        id = "s${season}e$number",
        name = title,
        type = "Episode",
        seriesId = "series-1",
        seriesName = "Signal Hill",
        parentIndexNumber = season,
        indexNumber = number,
        runTimeTicks = 48L * TICKS_PER_MINUTE,
        overview = overview,
        userData = UserItemData(played = played),
    )

    private fun person(name: String, role: String) = EmbyPerson(
        id = name.filter(Char::isLetter),
        name = name,
        role = role,
        type = "Actor",
    )

    private companion object {
        const val DIR = "build/screenshots/episode-detail"
        const val TICKS_PER_MINUTE = 600_000_000L
    }
}
