package scheduler

import (
	"context"
	"io"
	"log"
	"testing"
	"time"
)

func discardScheduler() *Scheduler { return New(log.New(io.Discard, "", 0)) }

func noopJob(context.Context) error { return nil }

// farFuture returns an HH:MM roughly n hours ahead in Budapest, so a registered daily job parks on a
// long timer (never fires during a sub-second test) yet is a valid, distinct schedule to switch to.
func farFuture(hoursAhead int) string {
	return time.Now().In(getBudapestLocation()).Add(time.Duration(hoursAhead) * time.Hour).Format("15:04")
}

// Group C — UpdateDaily mutates the schedule and delivers the reschedule signal. Asserted on a
// NOT-started scheduler so no goroutine competes for the buffered signal (race-free).
func TestUpdateDaily_MutatesAndSignals(t *testing.T) {
	s := discardScheduler()
	s.Daily("db-dump", "02:30", noopJob)

	if !s.UpdateDaily("db-dump", "22:00") {
		t.Fatal("UpdateDaily returned false for a valid change")
	}
	if got := s.jobs[0].Schedule; got != "22:00" {
		t.Errorf("Schedule = %q, want 22:00", got)
	}
	// The buffered reschedule signal must be present (no goroutine drained it).
	select {
	case <-s.jobs[0].resched:
	default:
		t.Error("no reschedule signal was sent by UpdateDaily")
	}
}

// Group C — unknown or non-daily job name, and an invalid time, both return false without panicking
// and leave everything untouched.
func TestUpdateDaily_UnknownAndInvalid(t *testing.T) {
	s := discardScheduler()
	s.Daily("db-dump", "02:30", noopJob)

	if s.UpdateDaily("does-not-exist", "03:00") {
		t.Error("UpdateDaily on an unknown job returned true")
	}
	if s.UpdateDaily("db-dump", "25:99") {
		t.Error("UpdateDaily with an invalid time returned true")
	}
	if got := s.jobs[0].Schedule; got != "02:30" {
		t.Errorf("Schedule was mutated by a rejected update: %q", got)
	}
}

// Group C — a running daily goroutine picks up the new time on the next pass: it CONSUMES the
// reschedule signal from the select and recomputes. Red-proof: remove the `case <-job.resched`
// from runDailyJob's select and this fails (the signal stays buffered — never consumed).
func TestUpdateDaily_GoroutineConsumesReschedule(t *testing.T) {
	s := discardScheduler()
	s.Daily("db-dump", farFuture(6), noopJob) // parked on a ~6h timer

	ctx, cancel := context.WithCancel(context.Background())
	s.Start(ctx)

	if !s.UpdateDaily("db-dump", farFuture(8)) {
		t.Fatal("UpdateDaily returned false")
	}
	time.Sleep(150 * time.Millisecond) // let the goroutine wake on resched and recompute
	cancel()
	s.Stop() // waits for the goroutine to exit — after this, reading resched is race-free

	select {
	case <-s.jobs[0].resched:
		t.Fatal("reschedule signal still buffered — runDailyJob never consumed it (no resched case)")
	default:
		// drained by the goroutine → immediacy works
	}
}
