package store

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

func userVersion(t *testing.T, s *Store) int {
	t.Helper()
	var v int
	if err := s.db.QueryRowContext(context.Background(), "PRAGMA user_version").Scan(&v); err != nil {
		t.Fatalf("read user_version: %v", err)
	}
	return v
}

func TestMigrateAppliesOnceAndIsIdempotent(t *testing.T) {
	ctx := context.Background()
	s := newTestStore(t)

	// The 1 migration in the built-in slice (tools scope) was applied on Open.
	startVer := len(migrations)
	if v := userVersion(t, s); v != startVer {
		t.Fatalf("fresh DB user_version = %d, want %d", v, startVer)
	}

	// Append a fake migration and run it: creates a throwaway table, bumps by 1.
	migrations = append(migrations, `CREATE TABLE migrate_probe (id INTEGER PRIMARY KEY)`)
	t.Cleanup(func() { migrations = migrations[:len(migrations)-1] })

	if err := migrate(ctx, s.db); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	want := startVer + 1
	if v := userVersion(t, s); v != want {
		t.Fatalf("after migrate user_version = %d, want %d", v, want)
	}
	if _, err := s.db.ExecContext(ctx, "INSERT INTO migrate_probe DEFAULT VALUES"); err != nil {
		t.Fatalf("probe table not created: %v", err)
	}

	// Second run is a no-op — re-running the CREATE would error (no IF NOT EXISTS).
	if err := migrate(ctx, s.db); err != nil {
		t.Fatalf("migrate second run not idempotent: %v", err)
	}
	if v := userVersion(t, s); v != want {
		t.Fatalf("after re-migrate user_version = %d, want %d", v, want)
	}
}

// Migration #18 clears the calendar keys written while safeKey dropped
// Cyrillic. Those rows are indistinguishable from real events on read, so
// leaving them would recite one meeting as several (Vikunja #443).
func TestCollapsedCalendarKeysAreDropped(t *testing.T) {
	ctx := context.Background()
	s := newTestStore(t)

	rows := []string{
		"calendar_event_20260804_--",             // "Встреча с Аней" under the old rule
		"calendar_event_20260804_",               // a one-word Russian summary
		"calendar_event_20260804_Встреча-с-Аней", // the new format
		"calendar_event_20260804_Standup",        // an ASCII summary, always fine
	}
	for _, key := range rows {
		if _, err := s.db.ExecContext(ctx,
			`INSERT INTO facts (ts, kind, key, value, source, confidence) VALUES (0, 'env', ?, 'x', 'poll:caldav', 1.0)`,
			key); err != nil {
			t.Fatalf("seed %q: %v", key, err)
		}
	}
	if _, err := s.db.ExecContext(ctx, migrations[17]); err != nil {
		t.Fatalf("migration 18: %v", err)
	}

	var got int
	if err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM facts WHERE key LIKE 'calendar_event_%'`).Scan(&got); err != nil {
		t.Fatal(err)
	}
	if got != 2 {
		t.Fatalf("%d calendar rows left, want the 2 that identify their event", got)
	}
}

// TestStuckRoutinesAreBackfilled — routines accepted before the fire-forever
// fix have accepted_ts NULL and a live reminder, so the tick loop skips them
// and they have been silent ever since (Vikunja #377). The migration touches
// live reminders, which is why it is tested against a real store.
func TestStuckRoutinesAreBackfilled(t *testing.T) {
	ctx := context.Background()
	s := newTestStore(t)
	created := time.Date(2026, 7, 1, 9, 0, 0, 0, time.UTC)

	rem, err := s.CreateReminder(ctx, created.Add(time.Hour), "полить цветы", "")
	if err != nil {
		t.Fatal(err)
	}
	healthy, err := s.CreateReminder(ctx, created.Add(2*time.Hour), "не трогать", "")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := s.db.ExecContext(ctx,
		`INSERT INTO proposed_routines (action, object, interval_days, status, created_ts, reminder_id, accepted_ts)
		 VALUES ('water', 'plants', 7, 'accepted', ?, ?, NULL)`,
		created.UnixMilli(), rem); err != nil {
		t.Fatal(err)
	}
	// An already-healthy accepted row, and a still-open proposal: neither is
	// this migration's business.
	if _, err := s.db.ExecContext(ctx,
		`INSERT INTO proposed_routines (action, object, interval_days, status, created_ts, accepted_ts)
		 VALUES ('feed', 'cat', 1, 'accepted', ?, ?)`,
		created.UnixMilli(), created.UnixMilli()); err != nil {
		t.Fatal(err)
	}

	// Index 19, version 20: standing lists landed on the same number first
	// (Vikunja #453), so this one moved down one.
	if _, err := s.db.ExecContext(ctx, migrations[19]); err != nil {
		t.Fatalf("migration 20: %v", err)
	}

	accepted, err := s.ListAcceptedRoutines(ctx)
	if err != nil || len(accepted) != 2 {
		t.Fatalf("ListAcceptedRoutines = %d rows, err=%v, want 2", len(accepted), err)
	}
	stuck := accepted[0]
	if stuck.Object != "plants" {
		stuck = accepted[1]
	}
	if stuck.AcceptedTs == nil || !stuck.AcceptedTs.Equal(created) {
		t.Fatalf("accepted_ts = %v, want the creation time", stuck.AcceptedTs)
	}
	if stuck.ReminderID != nil {
		t.Fatalf("reminder_id = %v, want it let go", stuck.ReminderID)
	}
	// The reminder it was holding is cancelled, and nothing else is.
	var status string
	if err := s.db.QueryRowContext(ctx, `SELECT status FROM reminders WHERE id = ?`, rem).Scan(&status); err != nil {
		t.Fatal(err)
	}
	if status != ReminderCancelled {
		t.Fatalf("linked reminder status = %q, want cancelled", status)
	}
	if err := s.db.QueryRowContext(ctx, `SELECT status FROM reminders WHERE id = ?`, healthy).Scan(&status); err != nil {
		t.Fatal(err)
	}
	if status != "pending" {
		t.Fatalf("unrelated reminder status = %q, want it untouched", status)
	}
}
