package com.ponzischeme89.memby.data

import com.ponzischeme89.memby.data.model.EmbyChapter
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * The credits rule, pinned against the same cases as the gateway's copy (`credits_test.go`).
 * The two exist separately because with no gateway there is nobody to ask, and the picture
 * must not start shrinking at a different moment depending on whether the container is up —
 * so when one of these changes, the other has to change with it.
 *
 * The cases come from a survey of a real 20,000-item library, and the shape of that survey is
 * why the rule looks the way it does. Emby 4.10 wrote **no `CreditsStart` at all** — only
 * `Chapter`, `IntroStart` and `IntroEnd` — while 216 items carried a chapter *named* like
 * credits at 90–98% of runtime. Two of them carried "Opening Credits" at 0–1%, which is the
 * case the position floor exists for and the one worth never regressing.
 */
class CreditsTest {
    private fun chapter(seconds: Long, marker: String) = EmbyChapter(
        startPositionTicks = seconds * 1_000L * TICKS_PER_MS,
        markerType = marker,
        name = marker,
    )

    /**
     * An ordinary chapter carrying a name, which is where the coverage actually comes from:
     * Emby 4.10 writes no `CreditsStart`, but plenty of media carries "End Credits".
     */
    private fun named(seconds: Long, name: String) = EmbyChapter(
        startPositionTicks = seconds * 1_000L * TICKS_PER_MS,
        markerType = "Chapter",
        name = name,
    )

    @Test
    fun `finds the marker on a real episode`() {
        assertEquals(
            1_900_000L,
            creditsStartFrom(
                listOf(
                    chapter(0, "Chapter"),
                    chapter(463, "IntroStart"),
                    chapter(583, "IntroEnd"),
                    chapter(1200, "Chapter"),
                    chapter(1900, "CreditsStart"),
                ),
                RUNTIME_MS,
            ),
        )
    }

    /**
     * A film commonly has the credits and no intro at all. This is the case that would have
     * been lost had the two features shared one availability flag.
     */
    @Test
    fun `finds credits on a title with no intro`() {
        assertEquals(
            1_850_000L,
            creditsStartFrom(
                listOf(chapter(1400, "Chapter"), chapter(1850, "CreditsStart")),
                RUNTIME_MS,
            ),
        )
    }

    @Test
    fun `refuses a list with no markers`() {
        assertNull(
            creditsStartFrom(listOf(chapter(0, "Chapter"), chapter(300, "Chapter")), RUNTIME_MS),
        )
        assertNull(creditsStartFrom(emptyList(), RUNTIME_MS))
    }

    /** An intro pair is a different feature and must never be read as credits. */
    @Test
    fun `refuses intro markers`() {
        assertNull(
            creditsStartFrom(
                listOf(chapter(463, "IntroStart"), chapter(583, "IntroEnd")),
                RUNTIME_MS,
            ),
        )
    }

    /**
     * A marker at zero says the whole file is credits, which is not something Emby means and
     * not something worth shrinking a picture for.
     */
    @Test
    fun `refuses a marker at the very beginning`() {
        assertNull(
            creditsStartFrom(
                listOf(chapter(0, "CreditsStart"), chapter(300, "Chapter")),
                RUNTIME_MS,
            ),
        )
    }

    /**
     * The last marker wins, where [introSegmentFrom] takes the first. Two starts mean the
     * markers are untrustworthy, and the two features are damaged in opposite directions: an
     * intro skip that fires late throws somebody past the story, so the earlier marker is
     * safer there; the credits pane firing early runs the last scene past somebody at double
     * speed, so the later marker is safer here.
     */
    @Test
    fun `the later of two markers wins`() {
        assertEquals(
            1_900_000L,
            creditsStartFrom(
                listOf(chapter(1700, "CreditsStart"), chapter(1900, "CreditsStart")),
                RUNTIME_MS,
            ),
        )
        // Order in the array is not trusted to be sorted.
        assertEquals(
            1_700_000L,
            creditsStartFrom(
                listOf(chapter(1900, "CreditsStart"), chapter(1700, "CreditsStart")),
                RUNTIME_MS,
            ),
        )
    }

    /**
     * Squid Game, as the library actually holds it: no marker of any kind, one ordinary
     * chapter named "Credits" at 90% of runtime. This is where every bit of this feature's
     * coverage comes from today.
     */
    @Test
    fun `finds a chapter named Credits, which is all Emby 4-10 gives`() {
        assertEquals(
            1_800_000L,
            creditsStartFrom(
                listOf(
                    chapter(0, "Chapter"),
                    chapter(463, "IntroStart"),
                    chapter(583, "IntroEnd"),
                    named(1800, "Credits"),
                ),
                RUNTIME_MS,
            ),
        )
        assertEquals(
            1_920_000L,
            creditsStartFrom(listOf(named(1920, "End Credits")), RUNTIME_MS),
        )
    }

    /**
     * THE case. Belfast carries "Opening Credits" at 1% of runtime and Game of Thrones at 0%.
     * Matching a name without testing the position starts the pane in the first minute of a
     * film and runs its opening at double speed — the worst thing this feature could do, and
     * the reason the position floor exists.
     */
    @Test
    fun `Opening Credits at the start of a film is never the credit roll`() {
        assertNull(
            creditsStartFrom(
                listOf(named(20, "Opening Credits"), chapter(600, "Chapter")),
                RUNTIME_MS,
            ),
        )
        // Belfast in full: the opening one is rejected and the closing one found.
        assertEquals(
            1_900_000L,
            creditsStartFrom(
                listOf(
                    named(20, "Opening Credits"),
                    chapter(600, "Chapter"),
                    named(1900, "End Credits"),
                ),
                RUNTIME_MS,
            ),
        )
    }

    /**
     * "The Pitt" carries both "Credits" and "End Credits" seconds apart, describing one roll.
     * The *earliest* qualifying chapter wins here — the opposite of the marker rule — because
     * the roll begins at the first of them and taking the last would skip part of it.
     */
    @Test
    fun `two credits chapters describing one roll take the earlier`() {
        assertEquals(
            1_900_000L,
            creditsStartFrom(
                listOf(named(1920, "End Credits"), named(1900, "Credits")),
                RUNTIME_MS,
            ),
        )
    }

    /**
     * Anything in the first three quarters is refused however it is worded, and the intro's own
     * chapters — named "Intro Start"/"Intro End" in this library — are never a credit roll.
     */
    @Test
    fun `refuses a credits-named chapter too early to be the roll`() {
        assertNull(creditsStartFrom(listOf(named(900, "Credits")), RUNTIME_MS))
        assertNull(
            creditsStartFrom(
                listOf(named(1800, "Intro Start"), named(1900, "Intro End")),
                RUNTIME_MS,
            ),
        )
    }

    /**
     * With no runtime an explicit marker is still honoured — it is Emby asserting a position
     * rather than this inferring one — but a name is refused, because nothing can then tell an
     * opening credit sequence from a closing one.
     */
    @Test
    fun `an unknown runtime honours a marker and refuses a name`() {
        assertEquals(
            1_900_000L,
            creditsStartFrom(listOf(chapter(1900, "CreditsStart")), runtimeMs = 0L),
        )
        assertNull(creditsStartFrom(listOf(named(1900, "End Credits")), runtimeMs = 0L))
    }

    /**
     * A marker below the floor is a mis-detection whoever wrote it, so it gives way to a name
     * that does qualify rather than being honoured on authority.
     */
    @Test
    fun `a marker below the floor falls through to a name that qualifies`() {
        assertEquals(
            1_900_000L,
            creditsStartFrom(
                listOf(chapter(200, "CreditsStart"), named(1900, "End Credits")),
                RUNTIME_MS,
            ),
        )
    }

    /**
     * The duration guard is a second, separate refusal: the rule above says *where* the roll
     * is, and this says whether there is enough of it left to be worth moving the picture for.
     */
    @Test
    fun `refuses a marker too near the end to be worth a transition`() {
        val duration = 2_760_000L
        assertTrue(creditsWorthShowing(2_700_000L, duration))
        // Twenty seconds of credits is the last card of a roll, not something to sit beside a
        // panel — the transition would be most of what was left.
        assertFalse(creditsWorthShowing(2_740_000L, duration))
    }

    @Test
    fun `refuses a marker past the end of the file`() {
        assertFalse(creditsWorthShowing(3_000_000L, 2_760_000L))
        assertFalse(creditsWorthShowing(2_760_000L, 2_760_000L))
    }

    /** An unknown duration is not a reason to guess. */
    @Test
    fun `refuses when the duration is unknown or the marker absent`() {
        assertFalse(creditsWorthShowing(2_700_000L, 0L))
        assertFalse(creditsWorthShowing(null, 2_760_000L))
    }

    private companion object {
        const val TICKS_PER_MS = 10_000L

        /** A two-thousand-second episode, so a percentage of runtime reads as a round number. */
        const val RUNTIME_MS = 2_000_000L
    }
}
