package api import ( "strings" "testing" "time" ) func airedItem(id, name, availability string, airsAt time.Time) sonarrScheduleItem { return sonarrScheduleItem{ ID: id, Name: name, MembyEpisodeCode: "S02E04", MembyEpisodeTitle: "The Crossing", MembyAirsAt: airsAt.Format(time.RFC3339), MembyAvailability: availability, MembyAvailabilityText: availability, ImageTags: map[string]string{"Primary": "sonarr"}, } } func TestBuildSonarrAlertsAnnouncesOnlyEpisodesNotYetInEmby(t *testing.T) { location := time.FixedZone("NZST", 12*60*60) now := time.Date(2026, 7, 27, 21, 30, 0, 0, location) aired := now.Add(-30 * time.Minute) alerts := buildSonarrAlerts([]sonarrScheduleItem{ airedItem("sonarr:7:42", "Northbound", "awaiting", aired), airedItem("sonarr:8:43", "Grabbed Show", "downloading", aired.Add(-time.Minute)), airedItem("sonarr:9:44", "Already Here", "available", aired), airedItem("sonarr:10:45", "Not Watched", "unmonitored", aired), airedItem("sonarr:11:46", "Later Tonight", "upcoming", now.Add(90*time.Minute)), airedItem("sonarr:12:47", "This Morning", "awaiting", now.Add(-8*time.Hour)), }, now, 3*time.Hour) if len(alerts) != 2 { t.Fatalf("expected 2 alerts, got %d: %+v", len(alerts), alerts) } first := alerts[0] if first.ID != "sonarr:7:42:aired" || first.Kind != alertKindSonarrAired { t.Fatalf("unexpected alert identity: %+v", first) } if first.Title != "Northbound" || first.ItemID != "sonarr:7:42" || first.ImageTag != "sonarr" { t.Fatalf("unexpected alert payload: %+v", first) } if !strings.Contains(first.Message, "S02E04 — The Crossing aired at 9:00 PM") || !strings.Contains(first.Message, "in Emby soon") { t.Fatalf("unexpected awaiting message: %q", first.Message) } if !strings.Contains(alerts[1].Message, "downloading now") { t.Fatalf("unexpected downloading message: %q", alerts[1].Message) } } func TestBuildSonarrAlertsSortsNewestFirstAndCaps(t *testing.T) { location := time.FixedZone("NZST", 12*60*60) now := time.Date(2026, 7, 27, 22, 0, 0, 0, location) items := []sonarrScheduleItem{ airedItem("sonarr:1:1", "Oldest", "awaiting", now.Add(-100*time.Minute)), airedItem("sonarr:2:2", "Newest", "awaiting", now.Add(-5*time.Minute)), airedItem("sonarr:3:3", "Middle", "awaiting", now.Add(-40*time.Minute)), airedItem("sonarr:4:4", "Older", "awaiting", now.Add(-80*time.Minute)), } alerts := buildSonarrAlerts(items, now, 3*time.Hour) if len(alerts) != maxAlerts { t.Fatalf("expected the list capped at %d, got %d", maxAlerts, len(alerts)) } if alerts[0].Title != "Newest" || alerts[1].Title != "Middle" || alerts[2].Title != "Older" { t.Fatalf("alerts are not newest-first: %+v", alerts) } } func TestBuildSonarrAlertsDisabledByZeroWindow(t *testing.T) { now := time.Date(2026, 7, 27, 22, 0, 0, 0, time.UTC) items := []sonarrScheduleItem{airedItem("sonarr:1:1", "Northbound", "awaiting", now.Add(-time.Minute))} if alerts := buildSonarrAlerts(items, now, 0); alerts != nil { t.Fatalf("a zero window must disable alerts, got %+v", alerts) } }