package store

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

func TestCreateAndAcceptProposedRoutine(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()

	// Create
	id, err := s.CreateProposedRoutine(ctx, "refill", "cat_water", 7.0, now)
	if err != nil {
		t.Fatalf("CreateProposedRoutine: %v", err)
	}
	if id == 0 {
		t.Fatal("expected non-zero id")
	}

	// Duplicate should fail
	_, err = s.CreateProposedRoutine(ctx, "refill", "cat_water", 7.0, now)
	if !errors.Is(err, ErrProposedRoutineExists) {
		t.Fatalf("want ErrProposedRoutineExists, got %v", err)
	}

	// Lookup
	r, err := s.LookupProposedRoutine(ctx, "refill", "cat_water")
	if err != nil {
		t.Fatalf("LookupProposedRoutine: %v", err)
	}
	if r == nil {
		t.Fatal("want non-nil routine")
	}
	if r.Action != "refill" || r.Object != "cat_water" || r.Status != "proposed" {
		t.Fatalf("got %+v", r)
	}
	if r.IntervalDays != 7.0 {
		t.Fatalf("want interval_days=7.0, got %f", r.IntervalDays)
	}

	// Accept
	if err := s.AcceptProposedRoutine(ctx, id, now); err != nil {
		t.Fatalf("AcceptProposedRoutine: %v", err)
	}

	// Verify accepted
	r, err = s.LookupProposedRoutine(ctx, "refill", "cat_water")
	if err != nil {
		t.Fatalf("LookupProposedRoutine: %v", err)
	}
	if r.Status != "accepted" {
		t.Fatalf("want status=accepted, got %s", r.Status)
	}
	if r.AcceptedTs == nil || !r.AcceptedTs.Equal(now.Truncate(time.Millisecond)) {
		t.Fatalf("want accepted_ts=%v, got %v", now, r.AcceptedTs)
	}
	if r.LastFiredTs != nil {
		t.Fatalf("a freshly accepted routine has not fired yet, got %v", r.LastFiredTs)
	}
}

// TestAcceptedRoutineFiredTimestamp — the tick loop's two reads: the accepted
// list, and the last-fired stamp it writes back after a nudge.
func TestAcceptedRoutineFiredTimestamp(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC().Truncate(time.Millisecond)

	id, err := s.CreateProposedRoutine(ctx, "полить", "цветы", 3.0, now)
	if err != nil {
		t.Fatalf("CreateProposedRoutine: %v", err)
	}
	if err := s.AcceptProposedRoutine(ctx, id, now); err != nil {
		t.Fatalf("AcceptProposedRoutine: %v", err)
	}

	list, err := s.ListAcceptedRoutines(ctx)
	if err != nil {
		t.Fatalf("ListAcceptedRoutines: %v", err)
	}
	if len(list) != 1 || list[0].ID != id {
		t.Fatalf("want the one accepted routine, got %+v", list)
	}
	if list[0].IntervalDays != 3.0 {
		t.Fatalf("want interval_days=3, got %v", list[0].IntervalDays)
	}

	fired := now.Add(3 * 24 * time.Hour)
	if err := s.MarkRoutineFired(ctx, id, fired); err != nil {
		t.Fatalf("MarkRoutineFired: %v", err)
	}
	list, err = s.ListAcceptedRoutines(ctx)
	if err != nil {
		t.Fatalf("ListAcceptedRoutines: %v", err)
	}
	if list[0].LastFiredTs == nil || !list[0].LastFiredTs.Equal(fired) {
		t.Fatalf("want last_fired_ts=%v, got %v", fired, list[0].LastFiredTs)
	}
}

func TestDismissProposedRoutine(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()

	id, err := s.CreateProposedRoutine(ctx, "feed", "cat", 1.0, now)
	if err != nil {
		t.Fatalf("CreateProposedRoutine: %v", err)
	}

	if err := s.DismissProposedRoutine(ctx, id); err != nil {
		t.Fatalf("DismissProposedRoutine: %v", err)
	}

	r, err := s.LookupProposedRoutine(ctx, "feed", "cat")
	if err != nil {
		t.Fatalf("LookupProposedRoutine: %v", err)
	}
	if r.Status != "dismissed" {
		t.Fatalf("want status=dismissed, got %s", r.Status)
	}
}

func TestListProposedRoutines(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()

	// No routines yet
	list, err := s.ListProposedRoutines(ctx)
	if err != nil {
		t.Fatalf("ListProposedRoutines: %v", err)
	}
	if len(list) != 0 {
		t.Fatalf("want 0, got %d", len(list))
	}

	// Create two
	_, err = s.CreateProposedRoutine(ctx, "refill", "water", 7.0, now)
	if err != nil {
		t.Fatalf("CreateProposedRoutine: %v", err)
	}
	_, err = s.CreateProposedRoutine(ctx, "feed", "cat", 1.0, now.Add(time.Hour))
	if err != nil {
		t.Fatalf("CreateProposedRoutine: %v", err)
	}

	list, err = s.ListProposedRoutines(ctx)
	if err != nil {
		t.Fatalf("ListProposedRoutines: %v", err)
	}
	if len(list) != 2 {
		t.Fatalf("want 2, got %d", len(list))
	}

	// Dismiss one
	_ = s.DismissProposedRoutine(ctx, list[0].ID)
	list, err = s.ListProposedRoutines(ctx)
	if err != nil {
		t.Fatalf("ListProposedRoutines: %v", err)
	}
	if len(list) != 1 {
		t.Fatalf("want 1 proposed after dismissing one, got %d", len(list))
	}
}

// A dismissed routine must never be proposed again. The detector will keep
// finding the same pattern; the store is what stops maven nagging about it.
func TestDismissedProposedRoutineStaysDismissed(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()

	id, err := s.CreateProposedRoutine(ctx, "clean", "litter_box", 3.0, now)
	if err != nil {
		t.Fatalf("CreateProposedRoutine: %v", err)
	}
	if err := s.DismissProposedRoutine(ctx, id); err != nil {
		t.Fatalf("DismissProposedRoutine: %v", err)
	}

	// The detector re-proposes the same pattern.
	_, err = s.CreateProposedRoutine(ctx, "clean", "litter_box", 3.0, now.Add(24*time.Hour))
	if !errors.Is(err, ErrProposedRoutineExists) {
		t.Fatalf("want ErrProposedRoutineExists on re-propose, got %v", err)
	}

	// And it must not reappear on the review page.
	list, err := s.ListProposedRoutines(ctx)
	if err != nil {
		t.Fatalf("ListProposedRoutines: %v", err)
	}
	if len(list) != 0 {
		t.Fatalf("want 0 proposed, got %d", len(list))
	}

	// Dismissing again is a no-op, and accepting is refused.
	if err := s.DismissProposedRoutine(ctx, id); err != nil {
		t.Fatalf("second DismissProposedRoutine: %v", err)
	}
	if err := s.AcceptProposedRoutine(ctx, id, time.Now().UTC()); !errors.Is(err, ErrProposedRoutineNotFound) {
		t.Fatalf("want ErrProposedRoutineNotFound accepting a dismissed routine, got %v", err)
	}
	r, err := s.LookupProposedRoutine(ctx, "clean", "litter_box")
	if err != nil {
		t.Fatalf("LookupProposedRoutine: %v", err)
	}
	if r.Status != RoutineDismissed {
		t.Fatalf("want status=dismissed, got %s", r.Status)
	}
}

func TestListProposedRoutinesByStatus(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()
	now := time.Now().UTC()

	keep, err := s.CreateProposedRoutine(ctx, "water", "plants", 4.0, now)
	if err != nil {
		t.Fatalf("CreateProposedRoutine: %v", err)
	}
	drop, err := s.CreateProposedRoutine(ctx, "walk", "dog", 1.0, now.Add(time.Hour))
	if err != nil {
		t.Fatalf("CreateProposedRoutine: %v", err)
	}
	if err := s.AcceptProposedRoutine(ctx, keep, now); err != nil {
		t.Fatalf("AcceptProposedRoutine: %v", err)
	}
	if err := s.DismissProposedRoutine(ctx, drop); err != nil {
		t.Fatalf("DismissProposedRoutine: %v", err)
	}

	cases := []struct {
		status RoutineStatus
		want   int
	}{
		{RoutineProposed, 0},
		{RoutineAccepted, 1},
		{RoutineDismissed, 1},
		{"", 2}, // empty status ⇒ every row
	}
	for _, c := range cases {
		list, err := s.ListProposedRoutinesByStatus(ctx, c.status)
		if err != nil {
			t.Fatalf("ListProposedRoutinesByStatus(%q): %v", c.status, err)
		}
		if len(list) != c.want {
			t.Fatalf("status %q: want %d, got %d", c.status, c.want, len(list))
		}
	}
}

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

	r, err := s.LookupProposedRoutine(ctx, "nonexistent", "nothing")
	if err != nil {
		t.Fatalf("LookupProposedRoutine: %v", err)
	}
	if r != nil {
		t.Fatal("want nil for missing routine")
	}
}

// Every legal status must survive the database. The status column is written
// by three different UPDATE statements with the value spelled inline, so a
// constant that drifts from its SQL is exactly the failure this catches: the
// row would come back in a state no Go code compares equal to.
func TestRoutineStatusRoundTrip(t *testing.T) {
	ctx := context.Background()
	s := newTestStore(t)
	now := time.Now().UTC()

	write := map[RoutineStatus]func(id int64) error{
		RoutineProposed:  func(int64) error { return nil },
		RoutineAccepted:  func(id int64) error { return s.AcceptProposedRoutine(ctx, id, now) },
		RoutineDismissed: func(id int64) error { return s.DismissProposedRoutine(ctx, id) },
	}

	for _, want := range RoutineStatuses {
		if !want.Valid() {
			t.Fatalf("%q is in RoutineStatuses but not Valid()", want)
		}
		object := "object-" + want.String()
		id, err := s.CreateProposedRoutine(ctx, "полить", object, 3, now)
		if err != nil {
			t.Fatalf("CreateProposedRoutine(%s): %v", want, err)
		}
		if err := write[want](id); err != nil {
			t.Fatalf("move to %s: %v", want, err)
		}
		got, err := s.LookupProposedRoutine(ctx, "полить", object)
		if err != nil || got == nil {
			t.Fatalf("LookupProposedRoutine(%s): %v", want, err)
		}
		if got.Status != want {
			t.Errorf("status = %q, want %q", got.Status, want)
		}
		list, err := s.ListProposedRoutinesByStatus(ctx, want)
		if err != nil {
			t.Fatalf("ListProposedRoutinesByStatus(%s): %v", want, err)
		}
		if len(list) != 1 {
			t.Errorf("status %s: listed %d rows, want 1", want, len(list))
		}
	}
}

// A typo used to read as "nothing is in that state", which is the same answer
// a correct query gives on an empty table.
func TestListByStatusRefusesAnUnknownStatus(t *testing.T) {
	s := newTestStore(t)
	if _, err := s.ListProposedRoutinesByStatus(context.Background(), "accpeted"); !errors.Is(err, ErrRoutineStatus) {
		t.Fatalf("err = %v, want ErrRoutineStatus", err)
	}
	if RoutineStatus("accpeted").Valid() {
		t.Error("a typo must not be Valid()")
	}
}
