package library import ( "testing" "time" ) func TestNextFire_Off(t *testing.T) { c := ScheduleConfig{Mode: "off"} if got := c.NextFire(time.Now()); !got.IsZero() { t.Errorf("NextFire = %v, want zero time", got) } } func TestNextFire_Interval(t *testing.T) { c := ScheduleConfig{Mode: "interval", IntervalHours: 6} now := time.Date(2026, 5, 6, 14, 0, 0, 0, time.UTC) want := time.Date(2026, 5, 6, 20, 0, 0, 0, time.UTC) if got := c.NextFire(now); !got.Equal(want) { t.Errorf("NextFire = %v, want %v", got, want) } } func TestNextFire_Daily_Tomorrow(t *testing.T) { c := ScheduleConfig{Mode: "daily", TimeOfDay: "03:00"} now := time.Date(2026, 5, 6, 14, 0, 0, 0, time.UTC) want := time.Date(2026, 5, 7, 3, 0, 0, 0, time.UTC) if got := c.NextFire(now); !got.Equal(want) { t.Errorf("NextFire = %v, want %v", got, want) } } func TestNextFire_Daily_Today(t *testing.T) { c := ScheduleConfig{Mode: "daily", TimeOfDay: "03:00"} now := time.Date(2026, 5, 6, 2, 0, 0, 0, time.UTC) want := time.Date(2026, 5, 6, 3, 0, 0, 0, time.UTC) if got := c.NextFire(now); !got.Equal(want) { t.Errorf("NextFire = %v, want %v", got, want) } } func TestNextFire_Weekly_UpcomingDay(t *testing.T) { // Tuesday 2pm, schedule = Sunday (ISO 7) 03:00 → upcoming Sunday at 03:00. c := ScheduleConfig{Mode: "weekly", WeeklyDay: 7, TimeOfDay: "03:00"} now := time.Date(2026, 5, 5, 14, 0, 0, 0, time.UTC) // 2026-05-05 = Tuesday want := time.Date(2026, 5, 10, 3, 0, 0, 0, time.UTC) // 2026-05-10 = Sunday if got := c.NextFire(now); !got.Equal(want) { t.Errorf("NextFire = %v, want %v", got, want) } } func TestNextFire_Weekly_TodayBeforeTime(t *testing.T) { c := ScheduleConfig{Mode: "weekly", WeeklyDay: 2, TimeOfDay: "03:00"} // ISO 2 = Tuesday now := time.Date(2026, 5, 5, 2, 0, 0, 0, time.UTC) want := time.Date(2026, 5, 5, 3, 0, 0, 0, time.UTC) if got := c.NextFire(now); !got.Equal(want) { t.Errorf("NextFire = %v, want %v", got, want) } } func TestNextFire_Weekly_TodayAfterTime(t *testing.T) { c := ScheduleConfig{Mode: "weekly", WeeklyDay: 2, TimeOfDay: "03:00"} now := time.Date(2026, 5, 5, 14, 0, 0, 0, time.UTC) want := time.Date(2026, 5, 12, 3, 0, 0, 0, time.UTC) if got := c.NextFire(now); !got.Equal(want) { t.Errorf("NextFire = %v, want %v", got, want) } } func TestNextFire_InvalidConfigs(t *testing.T) { cases := []ScheduleConfig{ {Mode: "interval", IntervalHours: 0}, {Mode: "interval", IntervalHours: -1}, {Mode: "daily", TimeOfDay: "bad"}, {Mode: "daily", TimeOfDay: "25:00"}, {Mode: "weekly", TimeOfDay: "03:00", WeeklyDay: 0}, {Mode: "weekly", TimeOfDay: "03:00", WeeklyDay: 8}, {Mode: "unknown"}, } now := time.Now() for _, c := range cases { if got := c.NextFire(now); !got.IsZero() { t.Errorf("NextFire(%+v) = %v, want zero", c, got) } } } func TestParseHHMM(t *testing.T) { cases := []struct { in string h, m int wantOK bool }{ {"03:00", 3, 0, true}, {"23:59", 23, 59, true}, {"00:00", 0, 0, true}, {"24:00", 0, 0, false}, {"23:60", 0, 0, false}, {"3:00", 0, 0, false}, {"03-00", 0, 0, false}, {"ab:cd", 0, 0, false}, } for _, c := range cases { h, m, ok := parseHHMM(c.in) if ok != c.wantOK || (ok && (h != c.h || m != c.m)) { t.Errorf("parseHHMM(%q) = (%d, %d, %v), want (%d, %d, %v)", c.in, h, m, ok, c.h, c.m, c.wantOK) } } } func TestScheduler_Refresh_NonBlocking(t *testing.T) { s := &Scheduler{ refresh: make(chan struct{}, 1), stop: make(chan struct{}), } s.Refresh() done := make(chan struct{}) go func() { s.Refresh() s.Refresh() close(done) }() select { case <-done: case <-time.After(100 * time.Millisecond): t.Errorf("Refresh blocked when channel was full") } }