package store

import (
	"context"
	"testing"
	"time"
)

// A recurring reminder keeps its wall-clock hour in the owner's zone. The cron
// walk used to run on the UTC instant scanReminder returns, so "0 9 * * *"
// created for 09:00 Moscow rescheduled to 09:00 UTC — noon, and noon every day
// after that (V-616).
func TestRescheduleKeepsWallClockHour(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	msk := time.FixedZone("MSK", 3*60*60)

	fire := time.Date(2026, 7, 1, 9, 0, 0, 0, msk)
	id, err := s.CreateReminder(ctx, fire, `{"text":"стендап"}`, "0 9 * * *")
	if err != nil {
		t.Fatal(err)
	}
	if err := s.rescheduleReminderIn(ctx, id, fire, msk); err != nil {
		t.Fatalf("reschedule: %v", err)
	}

	want := time.Date(2026, 7, 2, 9, 0, 0, 0, msk)
	got := nextFire(t, s, id)
	if !got.Equal(want) {
		t.Fatalf("next fire: want %s, got %s", want, got.In(msk))
	}
}

// A daily reminder survives a daemon that was down for days. The past
// occurrences are skipped, not replayed, and the reminder stays pending.
func TestRescheduleRollsForwardAfterDowntime(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	msk := time.FixedZone("MSK", 3*60*60)

	fire := time.Date(2026, 7, 1, 9, 0, 0, 0, msk)
	id, err := s.CreateReminder(ctx, fire, `{"text":"стендап"}`, "0 9 * * *")
	if err != nil {
		t.Fatal(err)
	}
	// The box comes back three days later, mid-afternoon.
	now := time.Date(2026, 7, 4, 15, 0, 0, 0, msk)
	if err := s.rescheduleReminderIn(ctx, id, now, msk); err != nil {
		t.Fatalf("reschedule: %v", err)
	}

	rs, err := s.ListReminders(ctx, 10)
	if err != nil || len(rs) != 1 {
		t.Fatalf("list: %d reminders, %v", len(rs), err)
	}
	if rs[0].Status != ReminderPending {
		t.Fatalf("status: want %s, got %s — downtime killed the recurrence", ReminderPending, rs[0].Status)
	}
	want := time.Date(2026, 7, 5, 9, 0, 0, 0, msk)
	if got := nextFire(t, s, id); !got.Equal(want) {
		t.Fatalf("next fire: want %s, got %s", want, got.In(msk))
	}
	// And exactly one fire is owed, not a backlog of four.
	due, err := s.DueReminders(ctx, want.Add(time.Second))
	if err != nil || len(due) != 1 {
		t.Fatalf("due after roll-forward: want 1, got %d (%v)", len(due), err)
	}
}

// A daily 03:00 reminder does not drift across the spring-forward changeover:
// the hour is wall clock, so the interval is 23 hours that day, not 24.
func TestRescheduleAcrossDSTKeepsHour(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	berlin, err := time.LoadLocation("Europe/Berlin")
	if err != nil {
		t.Skipf("tzdata unavailable: %v", err)
	}

	fire := time.Date(2027, 3, 27, 3, 0, 0, 0, berlin) // CET, day before the change
	id, err := s.CreateReminder(ctx, fire, `{"text":"бэкап"}`, "0 3 * * *")
	if err != nil {
		t.Fatal(err)
	}
	if err := s.rescheduleReminderIn(ctx, id, fire, berlin); err != nil {
		t.Fatalf("reschedule: %v", err)
	}
	got := nextFire(t, s, id).In(berlin)
	if got.Hour() != 3 || got.Day() != 28 {
		t.Fatalf("next fire: want 2027-03-28 03:00 local, got %s", got)
	}
	if d := got.Sub(fire); d != 23*time.Hour {
		t.Fatalf("gap across spring forward: want 23h, got %s", d)
	}
}

func nextFire(t *testing.T, s *Store, id int64) time.Time {
	t.Helper()
	var ms int64
	if err := s.db.QueryRow("SELECT next_fire_ts FROM reminders WHERE id = ?", id).Scan(&ms); err != nil {
		t.Fatalf("read next_fire_ts: %v", err)
	}
	return time.UnixMilli(ms).UTC()
}
