package com.ponzischeme89.memby.data

import com.ponzischeme89.memby.data.analytics.RowAnalytics
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class RowAnalyticsTest {

    /** A controllable clock, so dwell assertions are exact rather than timing-dependent. */
    private class FakeClock(var millis: Long = 1_700_000_000_000) {
        fun advance(by: Long) { millis += by }
    }

    private fun analytics(clock: FakeClock, maxBuffered: Int = 200) =
        RowAnalytics(now = { clock.millis }, maxBuffered = maxBuffered)

    @Test
    fun `dwell is measured when focus leaves a row`() {
        val clock = FakeClock()
        val collector = analytics(clock)

        collector.rowFocused("recommended", "MOVIES", "item-1")
        clock.advance(5_000)
        collector.rowFocused("favorites", "FAVORITES", "item-2")

        val focus = collector.drain().single { it.event == RowAnalytics.EVENT_FOCUS }
        assertEquals("recommended", focus.rowId)
        assertEquals(5_000L, focus.dwellMs)
        assertEquals("item-1", focus.itemId)
    }

    @Test
    fun `moving between cards inside one row keeps measuring the same dwell`() {
        val clock = FakeClock()
        val collector = analytics(clock)

        collector.rowFocused("recommended", "MOVIES", "item-1")
        clock.advance(3_000)
        collector.rowFocused("recommended", "MOVIES", "item-2")
        clock.advance(3_000)
        collector.endFocus()

        val focuses = collector.drain().filter { it.event == RowAnalytics.EVENT_FOCUS }
        assertEquals(1, focuses.size)
        assertEquals(6_000L, focuses.single().dwellMs)
    }

    @Test
    fun `passing through a row is not counted as attention`() {
        val clock = FakeClock()
        val collector = analytics(clock)

        collector.rowFocused("continue", "CONTINUE", "a")
        clock.advance(RowAnalytics.MIN_DWELL_MS - 1)
        collector.rowFocused("favorites", "FAVORITES", "b")

        assertTrue(
            "a sub-threshold glance should produce no focus event",
            collector.drain().none { it.event == RowAnalytics.EVENT_FOCUS },
        )
    }

    @Test
    fun `an impression is recorded once per row`() {
        val collector = analytics(FakeClock())

        collector.rowImpression("favorites", "FAVORITES")
        collector.rowImpression("favorites", "FAVORITES")
        collector.rowImpression("recommended", "MOVIES")

        val impressions = collector.drain().filter { it.event == RowAnalytics.EVENT_IMPRESSION }
        assertEquals(listOf("favorites", "recommended"), impressions.map { it.rowId })
    }

    @Test
    fun `visible posters receive deduplicated item impressions`() {
        val collector = analytics(FakeClock())

        collector.rowImpression("recommended", "MOVIES", listOf("one", "two"))
        collector.rowImpression("recommended", "MOVIES", listOf("one", "two"))

        val itemImpressions = collector.drain().filter {
            it.event == RowAnalytics.EVENT_IMPRESSION && it.itemId.isNotEmpty()
        }
        assertEquals(listOf("one", "two"), itemImpressions.map { it.itemId })
    }

    @Test
    fun `focusing a row that was never reported still records the impression`() {
        val collector = analytics(FakeClock())

        collector.rowFocused("similar:sev", "MOVIES", "item-1")

        val events = collector.drain()
        assertEquals(1, events.count { it.event == RowAnalytics.EVENT_IMPRESSION })
        assertEquals("similar:sev", events.single().rowId)
    }

    @Test
    fun `selections are recorded with their item`() {
        val collector = analytics(FakeClock())

        collector.rowSelected("recommended", "MOVIES", "item-9")

        val select = collector.drain().single()
        assertEquals(RowAnalytics.EVENT_SELECT, select.event)
        assertEquals("item-9", select.itemId)
        assertEquals("MOVIES", select.rowKind)
    }

    @Test
    fun `draining clears the buffer`() {
        val collector = analytics(FakeClock())
        collector.rowImpression("favorites", "FAVORITES")

        assertEquals(1, collector.drain().size)
        assertEquals(0, collector.drain().size)
    }

    @Test
    fun `the buffer is bounded and keeps the most recent events`() {
        val collector = analytics(FakeClock(), maxBuffered = 3)

        repeat(6) { collector.rowImpression("row-$it", "MOVIES") }

        val events = collector.drain()
        assertEquals(3, events.size)
        assertEquals(listOf("row-3", "row-4", "row-5"), events.map { it.rowId })
    }

    @Test
    fun `timestamps are sent in the format the gateway parses`() {
        val collector = analytics(FakeClock())
        collector.rowImpression("favorites", "FAVORITES")

        val occurredAt = collector.drain().single().occurredAt
        assertTrue(
            "expected RFC3339 UTC, got $occurredAt",
            occurredAt.matches(Regex("""\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z""")),
        )
    }

    @Test
    fun `reset forgets buffered events and seen rows`() {
        val collector = analytics(FakeClock())
        collector.rowImpression("favorites", "FAVORITES")

        collector.reset()
        collector.rowImpression("favorites", "FAVORITES")

        assertEquals(1, collector.drain().size)
    }
}
