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.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.onNodeWithText
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.RelatedContent
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 com.ponzischeme89.memby.ui.detail.AiringNotice
import com.ponzischeme89.memby.ui.detail.creditRows
import com.ponzischeme89.memby.ui.detail.technicalSpecs
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 the movie and series detail pages to PNGs under `build/screenshots/detail-page/`.
 *
 * ```powershell
 * .\gradlew.bat :app:testDebugUnitTest --tests "*DetailPageScreenshotTest"
 * ```
 *
 * There is no network here, so every artwork URL resolves to null and the pages render on
 * their own scrims. That is the point: it is the check that the poster column, the fact row
 * and the episode strip hold their shape when Emby has no backdrop and no poster to give —
 * the worst case, and the one a real library hits often enough to matter.
 *
 * Like [ServiceAlertBannerScreenshotTest], this is a `*ScreenshotTest.kt` file and is
 * allowed the Robolectric dependency; what these pages *say* lives in pure functions in
 * `ui/detail/DetailFacts.kt` and is covered by [SeriesDetailsTest] in plain JUnit.
 */
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [34], qualifiers = "w960dp-h540dp-television-xhdpi")
class DetailPageScreenshotTest {

    @get:Rule
    val compose = createComposeRule()

    @Before
    fun locator() {
        // The hero asks the repository for its artwork URLs; without a token it answers
        // null, which is exactly the fallback case being captured.
        ServiceLocator.init(ApplicationProvider.getApplicationContext())
    }

    @Test
    fun `movie page`() {
        capture("df_detail-movie") {
            MediaDetailContent(
                item = movie,
                onPlay = {},
                onToggleFavorite = { _, _ -> },
                onTogglePlayed = { _, _ -> },
                related = related,
            )
        }
    }

    /** Part-watched: the progress bar and "Resume" wording only appear in this state. */
    @Test
    fun `movie page part watched`() {
        capture("df_detail-movie-resumable") {
            MediaDetailContent(
                item = movie.copy(
                    userData = UserItemData(playbackPositionTicks = 47L * 600_000_000L),
                ),
                onPlay = {},
                onToggleFavorite = { _, _ -> },
                onTogglePlayed = { _, _ -> },
                related = related,
            )
        }
    }

    @Test
    fun `series page`() {
        capture("df_detail-series") {
            SeriesDetailContent(
                item = series,
                episodes = episodes,
                loadFailed = false,
                onPlay = {},
                onToggleFavorite = { _, _ -> },
                isMyShow = false,
                onToggleMyShow = { _, _ -> },
                related = related,
            )
        }
    }

    /**
     * Opened from the "Shows airing in the next 5 days" row. The band takes the accent line
     * the recommendation reason holds on every other route into this page — compare against
     * `df_detail-series`, which is the same show reached any other way.
     */
    @Test
    fun `series page opened from the airing row`() {
        capture("df_detail-series-airing") {
            SeriesDetailContent(
                item = series,
                episodes = episodes,
                loadFailed = false,
                onPlay = {},
                onToggleFavorite = { _, _ -> },
                isMyShow = false,
                onToggleMyShow = { _, _ -> },
                related = related,
                airingNotice = AiringNotice(
                    label = "AIRING TOMORROW",
                    headline = "S02E04  •  The Crossing",
                    detail = "Tomorrow: 9:00 PM  •  Awaiting download",
                ),
            )
        }
    }

    /** The first frame, before the episode request lands. The header must already be whole. */
    @Test
    fun `series page loading`() {
        capture("df_detail-series-loading") {
            SeriesDetailContent(
                item = series,
                episodes = null,
                loadFailed = false,
                onPlay = {},
                onToggleFavorite = { _, _ -> },
                isMyShow = true,
                onToggleMyShow = { _, _ -> },
            )
        }
    }

    /**
     * The other panes. Clicking a tab is the same path a remote takes — the strip selects
     * on focus — so this also proves the panes swap without disturbing the header.
     */
    @Test
    fun `series episodes tab`() {
        captureTab("df_detail-series-episodes", "Episodes") {
            SeriesDetailContent(
                item = series,
                episodes = episodes,
                loadFailed = false,
                onPlay = {},
                onToggleFavorite = { _, _ -> },
                isMyShow = false,
                onToggleMyShow = { _, _ -> },
                related = related,
            )
        }
    }

    @Test
    fun `series cast tab`() {
        captureTab("df_detail-series-cast-details", "Cast & Details") {
            SeriesDetailContent(
                item = series,
                episodes = episodes,
                loadFailed = false,
                onPlay = {},
                onToggleFavorite = { _, _ -> },
                isMyShow = false,
                onToggleMyShow = { _, _ -> },
                related = related,
            )
        }
    }

    @Test
    fun `movie details tab`() {
        captureTab("df_detail-movie-more-like-this", "More Like This") {
            MediaDetailContent(
                item = movie,
                onPlay = {},
                onToggleFavorite = { _, _ -> },
                onTogglePlayed = { _, _ -> },
                related = related,
            )
        }
    }

    /**
     * The tab panes at the geometry the scaffold gives them on a 540dp TV — the slot the
     * audit found clipping every technical spec off the bottom of Cast & Details, which is
     * the whole reason that tab exists. A tab does not scroll, so everything it has to say
     * has to be inside this box.
     */
    @Test
    fun `cast and details pane at its real slot geometry`() {
        capturePane("df_detail-pane-cast-details") {
            DetailCastAndDetailsPane(
                item = movie,
                credits = creditRows(movie),
                specs = technicalSpecs(movie),
                detailsLoaded = true,
                focusRequester = FocusRequester(),
            )
        }
    }

    @Test
    fun `overview pane at its real slot geometry`() {
        capturePane("df_detail-pane-overview") {
            DetailOverviewPane(
                item = series,
                credits = creditRows(series),
                focusRequester = FocusRequester(),
                supportingText = "Up next  S1 E3 · The Long Count",
            )
        }
    }

    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("build/screenshots/detail-page/$name.png")
    }

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

    private fun captureTab(name: String, tab: String, content: @Composable () -> Unit) {
        compose.setContent {
            PreviewSurface(alignment = Alignment.TopStart) { content() }
        }
        compose.onNodeWithText(tab).performClick()
        compose.onRoot().captureRoboImage("build/screenshots/detail-page/$name.png")
    }

    /**
     * What the gateway returns for a warm profile: the reason strip above the tabs and the
     * carousel under the page. Both are the point of these captures now — they are the two
     * bands that decide whether the page still fits on one screen.
     */
    private val related = RelatedContent(
        reasons = listOf(
            "Because you watch Thriller",
            "You've watched Aria Vance before",
            "Well rated (8.4)",
        ),
        items = List(8) { index ->
            BaseItem(
                id = "related-$index",
                name = listOf(
                    "The Quiet Meridian",
                    "Harbour Lights",
                    "Nine Days of Rain",
                    "The Cartographer's Wife",
                    "Winterline",
                    "A Country of Small Rivers",
                    "The Last Broadcast",
                    "Northbound",
                )[index],
                type = "Movie",
                productionYear = 2019 + index % 5,
            )
        },
    )

    private val cast = listOf(
        person("Aria Vance", "Detective Iris Kell"),
        person("Marcus Oyelaran", "Samuel Reed"),
        person("Nina Kowalczyk", "Dr. Halvorsen"),
        person("Tomas Brandt", "The Cartographer"),
        person("Ines Ferreira", "Captain Ruiz"),
        person("Daniel Cho", "Weatherman"),
    )

    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", title = "Dolby Atmos"),
        MediaStream(type = "Subtitle", codec = "subrip", language = "eng"),
        MediaStream(type = "Subtitle", codec = "subrip", language = "fra"),
    )

    private val movie = BaseItem(
        id = "movie-1",
        name = "The Longest Northbound Winter",
        type = "Movie",
        overview = "A cartographer chasing a river that no longer exists finds the last " +
            "village on the map still waiting for him, and has to decide whether telling " +
            "them the truth is a kindness. Shot over four winters in a valley that floods " +
            "every spring, and assembled from what survived.",
        taglines = listOf("Some maps are promises."),
        productionYear = 2024,
        officialRating = "PG-13",
        communityRating = 8.4,
        genres = listOf("Drama", "Adventure", "Mystery"),
        runTimeTicks = 134L * 600_000_000L,
        studios = listOf(Studio(name = "Northlight Pictures"), Studio(name = "Kestrel")),
        mediaStreams = streams,
        people = cast,
    )

    private val series = BaseItem(
        id = "series-1",
        name = "Signal Hill",
        type = "Series",
        overview = "A coastal radio station keeps receiving a broadcast that has not been " +
            "transmitted yet. Six weeks before the storm, the night operator starts writing " +
            "down what she hears.",
        taglines = listOf("Listen closely."),
        productionYear = 2022,
        officialRating = "TV-MA",
        communityRating = 8.9,
        genres = listOf("Thriller", "Drama"),
        studios = listOf(Studio(name = "Harbour Line")),
        mediaStreams = streams,
        people = cast,
    )

    private val episodes = listOf(
        episode(1, 1, "Carrier Wave", played = true),
        episode(1, 2, "Dead Air", played = true),
        episode(1, 3, "The Long Count", position = 12L * 600_000_000L),
        episode(1, 4, "Nightingale"),
        episode(1, 5, "Six Weeks Out"),
        episode(2, 1, "Landfall"),
    )

    private fun episode(
        season: Int,
        number: Int,
        title: String,
        played: Boolean = false,
        position: Long = 0L,
    ) = BaseItem(
        id = "s${season}e$number",
        name = title,
        type = "Episode",
        seriesName = "Signal Hill",
        parentIndexNumber = season,
        indexNumber = number,
        runTimeTicks = 48L * 600_000_000L,
        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.",
        userData = UserItemData(played = played, playbackPositionTicks = position),
    )

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