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 intro rule, pinned against the same cases as the gateway's copy (`intro_test.go`).
 *
 * The two implementations exist separately because with no gateway there is nobody to ask,
 * and a skip must not land somewhere different depending on whether the container is up —
 * so when one of these changes, the other has to change with it.
 *
 * The cases are taken from what Emby actually writes for FROM: intro markers arrive as
 * ordinary chapters carrying a marker type, in playback order beside the real ones.
 */
class IntroTest {

    private fun chapter(seconds: Long, marker: String) = EmbyChapter(
        startPositionTicks = seconds * 1_000L * 10_000L,
        markerType = marker,
        name = marker,
    )

    @Test
    fun `finds the segment in a real episode`() {
        val segment = introSegmentFrom(
            listOf(
                chapter(0, "Chapter"),
                chapter(300, "Chapter"),
                chapter(463, "IntroStart"),
                chapter(583, "IntroEnd"),
                chapter(600, "Chapter"),
            ),
        )

        assertEquals(IntroSegment(startMs = 463_000L, endMs = 583_000L), segment)
    }

    @Test
    fun `an intro can begin at the very start of the file`() {
        val segment = introSegmentFrom(
            listOf(chapter(0, "IntroStart"), chapter(95, "IntroEnd")),
        )

        // Which is exactly why availability is never inferred from a zero start.
        assertEquals(IntroSegment(startMs = 0L, endMs = 95_000L), segment)
    }

    @Test
    fun `an episode with no markers has no segment`() {
        assertNull(introSegmentFrom(listOf(chapter(0, "Chapter"), chapter(300, "Chapter"))))
        assertNull(introSegmentFrom(emptyList()))
    }

    @Test
    fun `half a pair is not a segment`() {
        // There is no honest end to skip to, and guessing one is how a viewer lands in the
        // middle of a scene.
        assertNull(introSegmentFrom(listOf(chapter(112, "IntroStart"), chapter(300, "Chapter"))))
        assertNull(introSegmentFrom(listOf(chapter(0, "Chapter"), chapter(246, "IntroEnd"))))
    }

    @Test
    fun `a pair in the wrong order is refused`() {
        assertNull(introSegmentFrom(listOf(chapter(300, "IntroStart"), chapter(120, "IntroEnd"))))
    }

    @Test
    fun `a segment too short to notice is refused`() {
        // A button that moves the picture imperceptibly reads as a broken button, so there
        // is deliberately no button at all.
        assertNull(introSegmentFrom(listOf(chapter(100, "IntroStart"), chapter(103, "IntroEnd"))))
    }

    @Test
    fun `a segment too long to be an intro is refused`() {
        // Far more likely two unrelated markers read as a range than a ten-minute title
        // sequence, and honouring it would throw the viewer past the story.
        assertNull(introSegmentFrom(listOf(chapter(60, "IntroStart"), chapter(660, "IntroEnd"))))
    }

    @Test
    fun `the first start wins`() {
        // Two starts mean the markers are already untrustworthy, and taking the later one
        // would pick the larger, more damaging skip of the two.
        val segment = introSegmentFrom(
            listOf(
                chapter(100, "IntroStart"),
                chapter(160, "IntroStart"),
                chapter(220, "IntroEnd"),
            ),
        )

        assertEquals(IntroSegment(startMs = 100_000L, endMs = 220_000L), segment)
    }

    @Test
    fun `a later pair is ignored once one has been found`() {
        val segment = introSegmentFrom(
            listOf(
                chapter(100, "IntroStart"),
                chapter(220, "IntroEnd"),
                chapter(1_800, "IntroStart"),
                chapter(1_900, "IntroEnd"),
            ),
        )

        assertEquals(IntroSegment(startMs = 100_000L, endMs = 220_000L), segment)
    }

    @Test
    fun `credit markers are not intros`() {
        assertNull(
            introSegmentFrom(
                listOf(chapter(2_800, "CreditsStart"), chapter(2_900, "CreditsEnd")),
            ),
        )
    }

    @Test
    fun `the window holds the playhead only while the titles are running`() {
        val segment = IntroSegment(startMs = 60_000L, endMs = 180_000L)

        assertFalse(segment.contains(59_999L))
        assertTrue(segment.contains(60_000L))
        assertTrue(segment.contains(179_999L))
        assertFalse(segment.contains(180_000L))
    }

    @Test
    fun `the tail keeps the offer off the last moment of the sequence`() {
        val segment = IntroSegment(startMs = 60_000L, endMs = 180_000L)

        // Offering a two-second skip is offering nothing; the button has to disappear
        // before it becomes indistinguishable from doing nothing.
        assertTrue(segment.contains(177_000L, lead = 2_000L))
        assertFalse(segment.contains(178_500L, lead = 2_000L))
    }

    @Test
    fun `an unknown mode falls back to the button rather than to a silent skip`() {
        // The gateway's catalogue may grow a mode before every set in the house has the
        // release that understands it. Costing that set its button is recoverable; jumping
        // through somebody's episode on a value this build cannot read is not.
        assertEquals(SKIP_INTRO_PROMPT, normalizeSkipIntroMode("immediately"))
        assertEquals(SKIP_INTRO_PROMPT, normalizeSkipIntroMode(null))
        assertEquals(SKIP_INTRO_PROMPT, normalizeSkipIntroMode(""))
        assertEquals(SKIP_INTRO_AUTO, normalizeSkipIntroMode("auto"))
        // Case and whitespace are the operator's, not the viewer's problem.
        assertEquals(SKIP_INTRO_OFF, normalizeSkipIntroMode("  OFF "))
    }
}
