package ui import ( "strings" "testing" "time" "git.stinnesbeck.com/nils/arr-tui/internal/radarr" "git.stinnesbeck.com/nils/arr-tui/internal/sonarr" ) func day(y int, m time.Month, d int) time.Time { return time.Date(y, m, d, 0, 0, 0, 0, time.Local) } func TestMovieEntriesPerReleaseDate(t *testing.T) { from, to := day(2026, 10, 20), day(2026, 11, 2) now := day(2026, 10, 21).Add(10 * time.Hour) m := radarr.Movie{ ID: 4, Title: "Dune [Part 2]", Year: 2024, Monitored: true, InCinemas: "2026-10-22T00:00:00Z", // inside DigitalRelease: "2026-11-01T00:00:00Z", // inside // physical release is after the window PhysicalRelease: "2026-12-01T00:00:00Z", } es := movieEntries(m, from, to, coverRef{}, now) if len(es) != 2 { t.Fatalf("%d entries, want 2: %+v", len(es), es) } if es[0].sub != "In cinemas" || !es[0].day.Equal(day(2026, 10, 22)) || es[1].sub != "Digital release" || !es[1].day.Equal(day(2026, 11, 1)) { t.Errorf("entries = %+v", es) } if !es[0].movie || es[0].missing || !strings.Contains(es[0].label, "Dune [Part 2[]") { t.Errorf("flags/label wrong: %+v", es[0]) } if es[0].key() == es[1].key() { t.Error("the same movie on two dates must have distinct keys") } if !strings.Contains(es[0].details.head, "Release: In cinemas, Thu 22 Oct 2026") { t.Errorf("details: %q", es[0].details.head) } // A release that already happened and is not downloaded counts as missing. past := movieEntries(radarr.Movie{ID: 5, Title: "Old", Monitored: true, DigitalRelease: "2026-10-20T00:00:00Z"}, from, to, coverRef{}, now) if len(past) != 1 || !past[0].missing { t.Errorf("past release: %+v", past) } // Dates are taken as written: midnight UTC stays on that calendar day in every zone. if es := movieEntries(radarr.Movie{ID: 6, Title: "Z", InCinemas: "2026-10-25T00:00:00Z"}, from, to, coverRef{}, now); len(es) != 1 || es[0].day.Day() != 25 { t.Errorf("timezone shifted the release date: %+v", es) } if es := movieEntries(radarr.Movie{ID: 7, Title: "NoDates"}, from, to, coverRef{}, now); len(es) != 0 { t.Errorf("movie without dates: %+v", es) } } func TestEpisodeEntry(t *testing.T) { from, to := day(2026, 10, 20), day(2026, 11, 2) now := day(2026, 10, 21).Add(12 * time.Hour) at := time.Date(2026, 10, 22, 19, 30, 0, 0, time.UTC) e := sonarr.Episode{ ID: 9, SeasonNumber: 2, EpisodeNumber: 5, Title: "Welcome", Monitored: true, AirDateUTC: at.Format(time.RFC3339), Overview: "Episode plot", Series: &sonarr.Series{Title: "Andor", Overview: "Series plot"}, } got, ok := episodeEntry(e, from, to, coverRef{}, now) if !ok { t.Fatal("episode in the window rejected") } if got.sub != "S02E05 Welcome" || got.title != "Andor" || got.movie || got.episodeID != 9 || got.at.IsZero() { t.Errorf("entry = %+v", got) } if !got.day.Equal(localDay(at.Local())) { t.Errorf("day = %v, want the local date of the airing", got.day) } if got.details.plot != "Episode plot" || !strings.Contains(got.details.head, "Andor") || !strings.Contains(got.details.head, "S02E05") { t.Errorf("details = %+v", got.details) } if got.missing { t.Error("an episode that has not aired yet is not missing") } // Falls back to the series plot, and to the date-only air date. e2 := sonarr.Episode{ID: 10, Title: "X", AirDate: "2026-10-19", Monitored: true, Series: &sonarr.Series{Title: "S", Overview: "Series plot"}} if _, ok := episodeEntry(e2, from, to, coverRef{}, now); ok { t.Error("an episode before the window must be left out") } e2.AirDate = "2026-10-20" got, ok = episodeEntry(e2, from, to, coverRef{}, now) if !ok || got.details.plot != "Series plot" || !got.at.IsZero() || !got.missing { t.Errorf("date-only episode: ok=%v %+v", ok, got) } if _, ok := episodeEntry(sonarr.Episode{ID: 11}, from, to, coverRef{}, now); ok { t.Error("an episode without any date has no place in the calendar") } } func TestSortEntriesAndDayLabel(t *testing.T) { d1, d2 := day(2026, 10, 21), day(2026, 10, 22) es := []calEntry{ {day: d2, title: "b"}, {day: d1, at: d1.Add(20 * time.Hour), title: "late"}, {day: d1, title: "dateonly"}, {day: d1, at: d1.Add(9 * time.Hour), title: "early"}, {day: d1, at: d1.Add(9 * time.Hour), title: "Alpha"}, } sortEntries(es) var got []string for _, e := range es { got = append(got, e.title) } if strings.Join(got, ",") != "dateonly,Alpha,early,late,b" { t.Errorf("order = %v", got) } today := day(2026, 10, 21) type label struct{ date, rel string } for d, want := range map[time.Time]label{ today: {"Wed 21 Oct", "Today"}, today.AddDate(0, 0, 1): {"Thu 22 Oct", "Tomorrow"}, today.AddDate(0, 0, -1): {"Tue 20 Oct", "Yesterday"}, today.AddDate(0, 0, 5): {"Mon 26 Oct", ""}, } { if date, rel := dayLabel(d, today); (label{date, rel}) != want { t.Errorf("dayLabel(%v) = %q, %q, want %v", d, date, rel, want) } } }