package com.ponzischeme89.memby.ui

import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.GatewayCalendar
import com.ponzischeme89.memby.data.model.GatewayCalendarDay
import com.ponzischeme89.memby.ui.calendar.CALENDAR_COLUMNS
import com.ponzischeme89.memby.ui.calendar.CalendarUiState
import com.ponzischeme89.memby.ui.calendar.calendarDate
import com.ponzischeme89.memby.ui.calendar.calendarAgendaWeekDate
import com.ponzischeme89.memby.ui.calendar.calendarAgendaWeekIndex
import com.ponzischeme89.memby.ui.calendar.calendarAgendaWeeks
import com.ponzischeme89.memby.ui.calendar.calendarDayHeading
import com.ponzischeme89.memby.ui.calendar.calendarEpisodeCount
import com.ponzischeme89.memby.ui.calendar.calendarEpisodeCountLabel
import com.ponzischeme89.memby.ui.calendar.calendarInitialDate
import com.ponzischeme89.memby.ui.calendar.calendarWeeks
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class CalendarGridTest {

    private fun episode(id: String, name: String) = BaseItem(id = id, name = name, type = "MembySonarrEpisode")

    // August 2026: 31 days, beginning on a Saturday.
    private fun august(days: List<GatewayCalendarDay> = emptyList(), today: String = "2026-08-11") =
        GatewayCalendar(
            available = true,
            month = "2026-08",
            label = "August 2026",
            previous = "2026-07",
            next = "2026-09",
            today = today,
            firstWeekday = 6,
            dayCount = 31,
            days = days,
        )

    @Test
    fun `the grid pads to whole weeks`() {
        val weeks = calendarWeeks(august())
        // Six leading blanks plus thirty-one days is thirty-seven cells: six rows of seven.
        assertEquals(6, weeks.size)
        assertTrue(weeks.all { it.size == CALENDAR_COLUMNS })
        assertTrue(weeks.first().take(6).all { it.isPad })
        assertEquals(1, weeks.first().last().day)
        assertEquals(31, weeks.flatten().count { !it.isPad })
    }

    @Test
    fun `the weekly guide uses the gateway month shape`() {
        val weeks = calendarAgendaWeeks(
            august(
                days = listOf(
                    GatewayCalendarDay(
                        date = "2026-08-12",
                        day = 12,
                        items = listOf(episode("a", "Northbound"), episode("b", "Harbour")),
                    ),
                ),
            ),
        )

        assertEquals(6, weeks.size)
        assertEquals("9–15 August", weeks[2].label)
        assertEquals(2, weeks[2].episodeCount)
        assertEquals(2, calendarAgendaWeekIndex(weeks, "2026-08-12"))
        assertEquals("2026-08-11", calendarAgendaWeekDate(weeks[2]))
    }

    @Test
    fun `another weekly page opens on its first programme`() {
        val weeks = calendarAgendaWeeks(
            august(
                today = "",
                days = listOf(
                    GatewayCalendarDay(
                        date = "2026-08-18",
                        day = 18,
                        items = listOf(episode("a", "Northbound")),
                    ),
                ),
            ),
        )

        assertEquals("2026-08-18", calendarAgendaWeekDate(weeks[3]))
    }

    // A pad is drawn but never focusable: there is nothing on the 0th of August to be told
    // about, and a D-pad that stopped there would read as the grid having stuck.
    @Test
    fun `pads are never focusable`() {
        val weeks = calendarWeeks(august())
        assertFalse(weeks.first().first().isFocusable)
        assertTrue(weeks.first().last().isFocusable)
    }

    @Test
    fun `days carry their episodes and their date`() {
        val weeks = calendarWeeks(
            august(
                days = listOf(
                    GatewayCalendarDay(
                        date = "2026-08-12",
                        day = 12,
                        items = listOf(episode("a", "Northbound"), episode("b", "Harbour")),
                    ),
                ),
            ),
        )
        val twelfth = weeks.flatten().first { it.day == 12 }
        assertEquals("2026-08-12", twelfth.date)
        assertEquals(2, twelfth.items.size)
        assertTrue(weeks.flatten().first { it.day == 13 }.items.isEmpty())
    }

    @Test
    fun `today is marked and only today`() {
        val cells = calendarWeeks(august()).flatten()
        assertEquals(listOf(11), cells.filter { it.today }.map { it.day })
    }

    @Test
    fun `a month the household is not in marks no day`() {
        val cells = calendarWeeks(august(today = "")).flatten()
        assertTrue(cells.none { it.today })
    }

    @Test
    fun `a leap February still lays out`() {
        val weeks = calendarWeeks(
            GatewayCalendar(available = true, month = "2028-02", dayCount = 29, firstWeekday = 2),
        )
        assertEquals(29, weeks.flatten().count { !it.isPad })
        assertTrue(weeks.all { it.size == CALENDAR_COLUMNS })
    }

    @Test
    fun `an unavailable month lays out nothing`() {
        assertTrue(calendarWeeks(GatewayCalendar()).isEmpty())
    }

    // Today is what somebody opening a guide is asking about, whether or not anything airs.
    @Test
    fun `the page opens on today when the household is in this month`() {
        assertEquals("2026-08-11", calendarInitialDate(august()))
    }

    // Travelling forward has no today to land on, and landing on the 1st of a month whose
    // news is on the 14th means walking a fortnight of empty cells to find out there was any.
    @Test
    fun `another month opens on its first airing`() {
        val calendar = august(
            today = "",
            days = listOf(
                GatewayCalendarDay(date = "2026-08-14", day = 14, items = listOf(episode("a", "Northbound"))),
            ),
        )
        assertEquals("2026-08-14", calendarInitialDate(calendar))
    }

    @Test
    fun `a month with nothing on opens on its first day`() {
        assertEquals("2026-08-01", calendarInitialDate(august(today = "")))
    }

    @Test
    fun `counts describe the month`() {
        val calendar = august(
            days = listOf(
                GatewayCalendarDay(date = "2026-08-12", day = 12, items = listOf(episode("a", "A"), episode("b", "B"))),
                GatewayCalendarDay(date = "2026-08-13", day = 13, items = listOf(episode("c", "C"))),
            ),
        )
        assertEquals(3, calendarEpisodeCount(calendar))
        assertEquals("3 episodes", calendarEpisodeCountLabel(3))
        assertEquals("1 episode", calendarEpisodeCountLabel(1))
        assertEquals("Nothing scheduled", calendarEpisodeCountLabel(0))
    }

    // The weekday is counted off the grid's own offset rather than from a date library, for
    // the same reason the grid is built from firstWeekday at all.
    @Test
    fun `the day heading names the weekday`() {
        assertEquals("Wednesday 12 August", calendarDayHeading(august(), "2026-08-12"))
        assertEquals("Saturday 1 August", calendarDayHeading(august(), "2026-08-01"))
        assertEquals("Monday 31 August", calendarDayHeading(august(), "2026-08-31"))
    }

    @Test
    fun `a heading is refused for a day outside the month`() {
        assertEquals("", calendarDayHeading(august(), ""))
        assertEquals("", calendarDayHeading(august(), "2026-08-45"))
    }

    // Every date on this page is compared for equality with every other, so a month the
    // gateway sent as something unreadable has to produce *nothing* rather than a set of
    // plausible-looking strings that agree with none of the days the episodes are in.
    @Test
    fun `a month that is not a month yields no dates`() {
        assertEquals("", calendarDate("", 12))
        assertEquals("", calendarDate("2026", 12))
        assertEquals("", calendarDate("not-a-month", 12))
        assertEquals("", calendarDate("20xx-08", 12))
        assertEquals("", calendarDate("2026/08", 12))
        assertEquals("2026-08-12", calendarDate("2026-08", 12))
        assertEquals("2026-08-01", calendarDate("2026-08", 1))
    }

    @Test
    fun `no cell is dated when the month is unreadable`() {
        val broken = august().copy(month = "nonsense", today = "")
        assertTrue(calendarWeeks(broken).flatten().none { it.date.isNotEmpty() })
        assertEquals("", calendarInitialDate(broken))
    }

    // Selection is by date string, so a blank one has to match nothing at all. Matching the
    // first day that also failed to produce a date would list one arbitrary day's episodes
    // under every cell in the grid.
    @Test
    fun `a blank selection lists nothing`() {
        val state = CalendarUiState(
            calendar = august(
                today = "",
                days = listOf(
                    GatewayCalendarDay(date = "2026-08-14", day = 14, items = listOf(episode("a", "A"))),
                ),
            ),
            selectedDate = "",
            isLoading = false,
        )
        assertTrue(state.selectedItems.isEmpty())
        assertEquals(
            listOf("a"),
            state.copy(selectedDate = "2026-08-14").selectedItems.map { it.id },
        )
    }
}
