package api

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/ponzischeme89/memby/server/internal/sonarr"
)

func calendarEpisode(id int, air time.Time, title string) sonarr.Episode {
	utc := air.UTC()
	return sonarr.Episode{
		ID:            id,
		SeriesID:      id,
		SeasonNumber:  1,
		EpisodeNumber: id,
		Title:         title,
		AirDateUTC:    &utc,
		Monitored:     true,
		Series:        sonarr.Series{ID: id, Title: title},
	}
}

func TestBuildCalendarMonthDescribesTheGrid(t *testing.T) {
	location := time.UTC
	// August 2026 begins on a Saturday and runs 31 days.
	month := time.Date(2026, 8, 1, 0, 0, 0, 0, location)
	now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)

	calendar := buildCalendarMonth(nil, month, now, location, nil)

	if !calendar.Available {
		t.Fatal("a configured Sonarr must report the month as available")
	}
	if calendar.Month != "2026-08" || calendar.Label != "August 2026" {
		t.Fatalf("unexpected identity: %+v", calendar)
	}
	if calendar.FirstWeekday != int(time.Saturday) || calendar.DayCount != 31 {
		t.Fatalf("unexpected grid shape: %+v", calendar)
	}
	if calendar.Previous != "2026-07" || calendar.Next != "2026-09" {
		t.Fatalf("unexpected neighbours: %+v", calendar)
	}
	if calendar.Today != "2026-08-11" {
		t.Fatalf("today was not marked: %+v", calendar)
	}
	if calendar.Days == nil {
		t.Fatal("days must encode as an array rather than null")
	}
}

func TestBuildCalendarMonthCountsFebruaryInALeapYear(t *testing.T) {
	location := time.UTC
	now := time.Date(2028, 2, 1, 0, 0, 0, 0, location)
	calendar := buildCalendarMonth(nil, now, now, location, nil)
	if calendar.DayCount != 29 {
		t.Fatalf("expected 29 days, got %d", calendar.DayCount)
	}
}

func TestBuildCalendarMonthOmitsTodayOutsideTheMonth(t *testing.T) {
	location := time.UTC
	month := time.Date(2026, 10, 1, 0, 0, 0, 0, location)
	now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
	if today := buildCalendarMonth(nil, month, now, location, nil).Today; today != "" {
		t.Fatalf("a month the household is not in must carry no today: %q", today)
	}
}

// The arrows are what stop a held D-pad walking Sonarr into the next decade, so the edge of
// the range must be reported as having no neighbour rather than as one more request.
func TestBuildCalendarMonthStopsAtTheTravelLimit(t *testing.T) {
	location := time.UTC
	now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
	last := time.Date(2027, 8, 1, 0, 0, 0, 0, location)
	if next := buildCalendarMonth(nil, last, now, location, nil).Next; next != "" {
		t.Fatalf("expected no next month at the limit, got %q", next)
	}
	first := time.Date(2025, 8, 1, 0, 0, 0, 0, location)
	if previous := buildCalendarMonth(nil, first, now, location, nil).Previous; previous != "" {
		t.Fatalf("expected no previous month at the limit, got %q", previous)
	}
}

func TestBuildCalendarMonthGroupsEpisodesByLocalDay(t *testing.T) {
	location := time.FixedZone("NZST", 12*60*60)
	month := time.Date(2026, 8, 1, 0, 0, 0, 0, location)
	now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)

	// Two on the 12th local, one on the 13th, and one that is the 31st in UTC but already
	// September where the household lives.
	episodes := []sonarr.Episode{
		calendarEpisode(2, time.Date(2026, 8, 12, 21, 0, 0, 0, location), "Harbour"),
		calendarEpisode(1, time.Date(2026, 8, 12, 20, 0, 0, 0, location), "Northbound"),
		calendarEpisode(3, time.Date(2026, 8, 13, 20, 0, 0, 0, location), "Deep Water"),
		calendarEpisode(4, time.Date(2026, 8, 31, 20, 0, 0, 0, time.UTC), "Rolled Over"),
	}

	calendar := buildCalendarMonth(episodes, month, now, location, nil)

	if len(calendar.Days) != 2 {
		t.Fatalf("expected two populated days, got %d: %+v", len(calendar.Days), calendar.Days)
	}
	if calendar.Days[0].Date != "2026-08-12" || calendar.Days[0].Day != 12 {
		t.Fatalf("unexpected first day: %+v", calendar.Days[0])
	}
	if len(calendar.Days[0].Items) != 2 {
		t.Fatalf("expected two episodes on the 12th: %+v", calendar.Days[0])
	}
	// Within a day the order is Sonarr's air time, which is the order they will be watched.
	var first sonarrScheduleItem
	if err := json.Unmarshal(calendar.Days[0].Items[0], &first); err != nil {
		t.Fatal(err)
	}
	if first.Name != "Northbound" {
		t.Fatalf("episodes within a day must be ordered by air time: %+v", first)
	}
	if first.Type != "MembySonarrEpisode" || first.MembyPlayable {
		t.Fatalf("a calendar card must be the schedule row's informational item: %+v", first)
	}
	if calendar.Days[1].Date != "2026-08-13" {
		t.Fatalf("unexpected second day: %+v", calendar.Days[1])
	}
}

func TestParseCalendarMonthDefaultsToThePresent(t *testing.T) {
	location := time.UTC
	now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
	month, ok := parseCalendarMonth("  ", now, location)
	if !ok || month.Format("2006-01") != "2026-08" {
		t.Fatalf("unexpected default month: %v %v", month, ok)
	}
}

func TestParseCalendarMonthRefusesNonsenseAndDistantMonths(t *testing.T) {
	location := time.UTC
	now := time.Date(2026, 8, 11, 9, 0, 0, 0, location)
	for _, value := range []string{"2026-13", "August", "2026-08-11", "2031-01", "2020-01"} {
		if _, ok := parseCalendarMonth(value, now, location); ok {
			t.Fatalf("expected %q to be refused", value)
		}
	}
	for _, value := range []string{"2026-09", "2027-08", "2025-08"} {
		if _, ok := parseCalendarMonth(value, now, location); !ok {
			t.Fatalf("expected %q to be accepted", value)
		}
	}
}
