package store

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"
)

func createCancellationReminder(t *testing.T, s *Store, fire time.Time, text string) Reminder {
	t.Helper()
	id, err := s.CreateReminder(context.Background(), fire, `{"text":"`+text+`"}`, "")
	if err != nil {
		t.Fatal(err)
	}
	rows, err := s.ListReminders(context.Background(), 1)
	if err != nil || len(rows) != 1 || rows[0].ID != id {
		t.Fatalf("created reminder %d, list=%+v err=%v", id, rows, err)
	}
	return rows[0]
}

func TestListPendingRemindersForCancellation(t *testing.T) {
	ctx := context.Background()
	s := newTestStore(t)
	now := time.Date(2026, 8, 15, 9, 0, 0, 0, time.UTC)
	later := createCancellationReminder(t, s, now.Add(2*time.Hour), "later")
	earlier := createCancellationReminder(t, s, now.Add(time.Hour), "earlier")
	fired := createCancellationReminder(t, s, now.Add(3*time.Hour), "already fired")
	if err := s.MarkReminder(ctx, fired.ID, ReminderFired); err != nil {
		t.Fatal(err)
	}

	all, err := s.ListPendingReminders(ctx, 0)
	if err != nil {
		t.Fatal(err)
	}
	if len(all) != 2 || all[0].ID != earlier.ID || all[1].ID != later.ID {
		t.Fatalf("pending firing order = %+v", all)
	}
	one, err := s.ListPendingReminders(ctx, 1)
	if err != nil || len(one) != 1 || one[0].ID != earlier.ID {
		t.Fatalf("limited pending = %+v, %v", one, err)
	}
}

func TestCancelReminderLifecycle(t *testing.T) {
	ctx := context.Background()
	s := newTestStore(t)
	if err := s.CancelReminder(ctx, 999); !errors.Is(err, ErrReminderNotFound) {
		t.Fatalf("missing cancellation = %v", err)
	}
	r := createCancellationReminder(t, s, time.Now().Add(time.Hour), "doctor")
	if err := s.CancelReminder(ctx, r.ID); err != nil {
		t.Fatal(err)
	}
	rows, err := s.ListReminders(ctx, 1)
	if err != nil || len(rows) != 1 || rows[0].Status != ReminderCancelled {
		t.Fatalf("cancelled row = %+v, %v", rows, err)
	}
	if err := s.CancelReminder(ctx, r.ID); !errors.Is(err, ErrReminderState) {
		t.Fatalf("second cancellation = %v", err)
	}
}

func TestMarkReminderCannotBypassCancellationSafety(t *testing.T) {
	ctx := context.Background()
	s := newTestStore(t)
	r := createCancellationReminder(t, s, time.Now().Add(time.Hour), "doctor")
	if err := s.MarkReminder(ctx, r.ID, ReminderCancelled); !errors.Is(err, ErrReminderState) {
		t.Fatalf("legacy cancelled transition = %v, want ErrReminderState", err)
	}
	if got := reminderStatusesForTest(t, s)[r.ID]; got != ReminderPending {
		t.Fatalf("legacy transition changed status to %q", got)
	}
}

func reminderStatusesForTest(t *testing.T, s *Store) map[int64]string {
	t.Helper()
	rows, err := s.ListReminders(context.Background(), 100)
	if err != nil {
		t.Fatal(err)
	}
	out := make(map[int64]string, len(rows))
	for _, row := range rows {
		out[row.ID] = row.Status
	}
	return out
}

func TestCancelReminderInvalidatesCollapsedPhrase(t *testing.T) {
	ctx := context.Background()
	s := newTestStore(t)
	now := time.Date(2026, 8, 15, 9, 0, 0, 0, time.UTC)
	a := createCancellationReminder(t, s, now.Add(time.Hour), "doctor")
	b := createCancellationReminder(t, s, now.Add(2*time.Hour), "bread")
	originals, err := s.ListPendingReminders(ctx, 0)
	if err != nil {
		t.Fatal(err)
	}
	const group = "reminder:collapsed-cancel-test"
	if err := s.CacheReminderPhrase(ctx, originals, group, "two reminders", "two", "neutral"); err != nil {
		t.Fatal(err)
	}
	attempt, err := s.BeginDeliveryAttempt(ctx, "reminder", "", a.ID, group, "telegram", "hash", now)
	if err != nil {
		t.Fatal(err)
	}
	if err := s.CompleteDeliveryAttempt(ctx, attempt, DeliveryFailed, now); err != nil {
		t.Fatal(err)
	}
	if err := s.CancelReminder(ctx, a.ID); err != nil {
		t.Fatal(err)
	}

	rows, err := s.ListReminders(ctx, 10)
	if err != nil {
		t.Fatal(err)
	}
	byID := make(map[int64]Reminder, len(rows))
	for _, row := range rows {
		byID[row.ID] = row
	}
	if got := byID[a.ID]; got.Status != ReminderCancelled || got.HasDeliveryPhrase() {
		t.Fatalf("cancelled member retained presentation: %+v", got)
	}
	if got := byID[b.ID]; got.Status != ReminderPending || got.HasDeliveryPhrase() || got.DeliveryGroup != "" {
		t.Fatalf("surviving member retained stale bundle: %+v", got)
	}
}

func TestCancelReminderRefusesAmbiguousDelivery(t *testing.T) {
	for _, status := range []string{DeliveryPending, DeliveryUnknown, DeliverySent} {
		t.Run(status, func(t *testing.T) {
			ctx := context.Background()
			s := newTestStore(t)
			now := time.Date(2026, 8, 15, 9, 0, 0, 0, time.UTC)
			r := createCancellationReminder(t, s, now.Add(time.Hour), status)
			group := "reminder:" + status
			if err := s.CacheReminderPhrase(ctx, []Reminder{r}, group, status, status, "neutral"); err != nil {
				t.Fatal(err)
			}
			attempt, err := s.BeginDeliveryAttempt(ctx, "reminder", "", r.ID, group, "telegram", "hash", now)
			if err != nil {
				t.Fatal(err)
			}
			switch status {
			case DeliveryUnknown:
				if _, err := s.ReconcileStaleDeliveryAttempts(ctx, now.Add(time.Minute)); err != nil {
					t.Fatal(err)
				}
			case DeliverySent:
				if err := s.CompleteDeliveryAttempt(ctx, attempt, DeliverySent, now); err != nil {
					t.Fatal(err)
				}
			}
			if err := s.CancelReminder(ctx, r.ID); !errors.Is(err, ErrReminderInFlight) {
				t.Fatalf("cancel with %s attempt = %v", status, err)
			}
		})
	}
}

func TestCancelReminderScopesLegacyAttemptsToTheCurrentOccurrence(t *testing.T) {
	ctx := context.Background()
	now := time.Date(2026, 8, 15, 9, 0, 0, 0, time.UTC)

	t.Run("historical recurring send does not block the series", func(t *testing.T) {
		s := newTestStore(t)
		fire := now.Add(24 * time.Hour)
		id, err := s.CreateReminder(ctx, fire, `{"text":"daily medicine"}`, "0 9 * * *")
		if err != nil {
			t.Fatal(err)
		}
		if _, err := s.db.ExecContext(ctx, `
			INSERT INTO delivery_attempts
				(kind, reminder_id, delivery_group, channel, body_hash, status, created_ts, completed_ts)
			VALUES ('reminder', ?, '', 'telegram', 'legacy', 'sent', ?, ?)`,
			id, now.UnixMilli(), now.UnixMilli()); err != nil {
			t.Fatal(err)
		}
		if err := s.CancelReminder(ctx, id); err != nil {
			t.Fatalf("historical blank-group attempt blocked recurrence: %v", err)
		}
	})

	t.Run("current legacy ambiguity still refuses", func(t *testing.T) {
		s := newTestStore(t)
		fire := now.Add(time.Hour)
		r := createCancellationReminder(t, s, fire, "legacy current")
		if _, err := s.db.ExecContext(ctx, `
			INSERT INTO delivery_attempts
				(kind, reminder_id, delivery_group, channel, body_hash, status, created_ts)
			VALUES ('reminder', ?, '', 'telegram', 'legacy', 'unknown', ?)`,
			r.ID, fire.Add(time.Second).UnixMilli()); err != nil {
			t.Fatal(err)
		}
		if err := s.CancelReminder(ctx, r.ID); !errors.Is(err, ErrReminderInFlight) {
			t.Fatalf("current blank-group ambiguity = %v, want ErrReminderInFlight", err)
		}
	})
}

func TestReminderCancellationAndDeliveryBeginAreMutuallyExclusive(t *testing.T) {
	for i := 0; i < 20; i++ {
		s := newTestStore(t)
		ctx := context.Background()
		now := time.Date(2026, 8, 15, 9, 0, 0, i, time.UTC)
		r := createCancellationReminder(t, s, now.Add(time.Hour), "race")
		group := "reminder:race"
		if err := s.CacheReminderPhrase(ctx, []Reminder{r}, group, "race", "race", "neutral"); err != nil {
			t.Fatal(err)
		}

		start := make(chan struct{})
		var cancelErr, beginErr error
		var wg sync.WaitGroup
		wg.Add(2)
		go func() {
			defer wg.Done()
			<-start
			cancelErr = s.CancelReminder(ctx, r.ID)
		}()
		go func() {
			defer wg.Done()
			<-start
			_, beginErr = s.BeginDeliveryAttempt(ctx, "reminder", "", r.ID, group, "telegram", "hash", now)
		}()
		close(start)
		wg.Wait()

		if (cancelErr == nil) == (beginErr == nil) {
			t.Fatalf("iteration %d: cancel=%v begin=%v; exactly one must win", i, cancelErr, beginErr)
		}
		if cancelErr == nil && !errors.Is(beginErr, ErrReminderState) {
			t.Fatalf("iteration %d: cancellation won, begin=%v", i, beginErr)
		}
		if beginErr == nil && !errors.Is(cancelErr, ErrReminderInFlight) {
			t.Fatalf("iteration %d: delivery won, cancel=%v", i, cancelErr)
		}
	}
}
