package routine

import (
	"testing"
	"time"
)

func TestDueAcceptedFiresOncePerInterval(t *testing.T) {
	accepted := time.Date(2026, 7, 1, 9, 0, 0, 0, time.UTC)
	fired := accepted.Add(3 * 24 * time.Hour)
	rs := []Accepted{
		{ID: 1, Name: "полить цветы", IntervalDays: 3, Accepted: accepted},
		{ID: 2, Name: "покормить рыб", IntervalDays: 3, Accepted: accepted, LastFired: &fired},
		{ID: 3, Name: "битый интервал", IntervalDays: 0, Accepted: accepted},
	}

	// One day in: nothing has waited a full interval.
	if got := DueAccepted(rs, accepted.Add(24*time.Hour)); len(got) != 0 {
		t.Fatalf("want nothing due after 1 day, got %+v", got)
	}

	// Three days in: the never-fired one is due. The one that already fired at
	// day 3 starts its next three days from there. A zero interval never fires.
	got := DueAccepted(rs, fired)
	if len(got) != 1 || got[0].ID != 1 {
		t.Fatalf("want only routine 1 due at day 3, got %+v", got)
	}

	// Six days in: both real routines are due.
	if got := DueAccepted(rs, accepted.Add(6*24*time.Hour)); len(got) != 2 {
		t.Fatalf("want both routines due at day 6, got %+v", got)
	}

	// A month later the zero-interval routine is still silent.
	for _, r := range DueAccepted(rs, accepted.Add(30*24*time.Hour)) {
		if r.ID == 3 {
			t.Fatal("a routine with a zero interval must never fire")
		}
	}
}

func TestValidate(t *testing.T) {
	ok := []Routine{{Name: "morning", Cron: "0 8 * * *", Body: "доброе утро"}}
	if err := Validate(ok); err != nil {
		t.Fatalf("valid routine rejected: %v", err)
	}

	cases := []struct {
		name string
		r    Routine
	}{
		{"missing name", Routine{Cron: "0 8 * * *", Body: "x"}},
		{"missing body", Routine{Name: "m", Cron: "0 8 * * *"}},
		{"bad cron", Routine{Name: "m", Cron: "not a cron", Body: "x"}},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			if err := Validate([]Routine{c.r}); err == nil {
				t.Error("expected an error, got nil")
			}
		})
	}
}

func TestDue(t *testing.T) {
	// a routine that fires at 08:00 every day.
	rs := []Routine{{Name: "morning", Cron: "0 8 * * *", Body: "доброе утро", Severity: 1}}

	t.Run("first sight seeds without firing (cold-start guard)", func(t *testing.T) {
		last := map[string]time.Time{}
		now := time.Date(2026, 7, 6, 8, 0, 0, 0, time.UTC) // exactly on schedule
		got := Due(rs, last, now)
		if len(got) != 0 {
			t.Errorf("routine fired on first sight: %v", got)
		}
		if _, seen := last["morning"]; !seen {
			t.Error("first sight did not seed the last-fired map")
		}
	})

	t.Run("fires once the schedule crosses", func(t *testing.T) {
		last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 7, 30, 0, 0, time.UTC)}
		now := time.Date(2026, 7, 6, 8, 0, 30, 0, time.UTC) // just past 08:00
		got := Due(rs, last, now)
		if len(got) != 1 || got[0].Name != "morning" {
			t.Fatalf("expected morning to fire, got %v", got)
		}
		if !last["morning"].Equal(now) {
			t.Errorf("last-fired not advanced to now: %v", last["morning"])
		}
	})

	t.Run("does not refire before the next crossing", func(t *testing.T) {
		last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 8, 0, 0, 0, time.UTC)}
		now := time.Date(2026, 7, 6, 8, 5, 0, 0, time.UTC) // same morning, later
		if got := Due(rs, last, now); len(got) != 0 {
			t.Errorf("routine refired within the same window: %v", got)
		}
	})

	t.Run("missed schedule on restart fires at most once, not per-tick", func(t *testing.T) {
		// booted at 07:00, seeded then; next tick is 09:00 (08:00 already passed).
		last := map[string]time.Time{"morning": time.Date(2026, 7, 6, 7, 0, 0, 0, time.UTC)}
		now := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC)
		if got := Due(rs, last, now); len(got) != 1 {
			t.Fatalf("missed 08:00 should fire once at 09:00, got %v", got)
		}
		// immediately after, it must not fire again.
		if got := Due(rs, last, now.Add(time.Minute)); len(got) != 0 {
			t.Errorf("routine fired twice for one missed schedule: %v", got)
		}
	})

	t.Run("bad cron is skipped, not fired every tick", func(t *testing.T) {
		bad := []Routine{{Name: "broken", Cron: "nonsense", Body: "x"}}
		last := map[string]time.Time{"broken": time.Date(2026, 7, 6, 7, 0, 0, 0, time.UTC)}
		now := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC)
		if got := Due(bad, last, now); len(got) != 0 {
			t.Errorf("unparseable cron fired: %v", got)
		}
	})
}
