package morning

import (
	"testing"
	"time"

	"github.com/kami/maven/internal/store"
)

func mkRoutine() Routine {
	return Routine{
		Name:        "weekday_morning",
		Weekdays:    []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday},
		WindowStart: "08:00",
		WindowEnd:   "11:00",
		Items: []Item{
			{Key: "medicine", FactKey: "medicine", Label: "лекарство"},
			{Key: "water", FactKey: "water", Label: "вода"},
			{Key: "pets", FactKey: "pets", Label: "кот"},
		},
	}
}

func at(h, m int) time.Time {
	return time.Date(2026, 7, 20, h, m, 0, 0, time.UTC) // 2026-07-20 is a Monday
}

func fact(ts time.Time) store.Fact { return store.Fact{Ts: ts} }

func TestValidate(t *testing.T) {
	r := mkRoutine()
	if err := Validate([]Routine{r}); err != nil {
		t.Fatalf("valid routine rejected: %v", err)
	}

	bad := r
	bad.Items = nil
	if err := Validate([]Routine{bad}); err == nil {
		t.Fatal("expected error for no items")
	}

	bad = r
	bad.WindowStart = "25:00"
	if err := Validate([]Routine{bad}); err == nil {
		t.Fatal("expected error for bad window_start")
	}

	bad = r
	bad.WindowStart, bad.WindowEnd = "11:00", "08:00"
	if err := Validate([]Routine{bad}); err == nil {
		t.Fatal("expected error for inverted window")
	}

	bad = r
	bad.Items = append(bad.Items, Item{Key: "medicine", FactKey: "x"})
	if err := Validate([]Routine{bad}); err == nil {
		t.Fatal("expected error for duplicate item key")
	}

	// Due keys its once-a-day map by name, so two routines sharing one would
	// suppress each other's nudge instead of both firing.
	if err := Validate([]Routine{r, r}); err == nil {
		t.Fatal("expected error for duplicate routine name")
	}

	// Arithmetic alone let a stray character cancel out: "2 :00" read as 04:00.
	bad = r
	bad.WindowStart = "2 :00"
	if err := Validate([]Routine{bad}); err == nil {
		t.Fatal("expected error for a non-digit in window_start")
	}
}

func TestEvaluateInactiveOutsideWindow(t *testing.T) {
	r := mkRoutine()
	st := Evaluate(r, nil, at(7, 59))
	if st.Active {
		t.Fatal("expected inactive before window opens")
	}
	st = Evaluate(r, nil, at(11, 0))
	if st.Active {
		t.Fatal("expected inactive at/after window closes")
	}
}

func TestEvaluateInactiveOnWrongWeekday(t *testing.T) {
	r := mkRoutine() // weekdays only
	saturday := time.Date(2026, 7, 25, 9, 0, 0, 0, time.UTC)
	if Evaluate(r, nil, saturday).Active {
		t.Fatal("expected inactive on a weekend day not in Weekdays")
	}
}

func TestEvaluateMissingAndCompleted(t *testing.T) {
	r := mkRoutine()
	facts := map[string]store.Fact{
		"medicine": fact(at(8, 30)),
	}
	st := Evaluate(r, facts, at(9, 0))
	if !st.Active {
		t.Fatal("expected active within window")
	}
	if len(st.Completed) != 1 || st.Completed[0].Key != "medicine" {
		t.Fatalf("expected medicine completed, got %+v", st.Completed)
	}
	if len(st.Missing) != 2 {
		t.Fatalf("expected 2 missing, got %+v", st.Missing)
	}
}

func TestEvaluateEvidenceBeforeWindowDoesNotCount(t *testing.T) {
	r := mkRoutine()
	facts := map[string]store.Fact{
		"medicine": fact(at(7, 0)), // before window opened today
	}
	st := Evaluate(r, facts, at(9, 0))
	for _, it := range st.Completed {
		if it.Key == "medicine" {
			t.Fatal("stale (pre-window) evidence should not count as completion")
		}
	}
}

func TestDueFiresOnlyAtNudgeTimeWithMissingItems(t *testing.T) {
	r := mkRoutine() // NudgeAt empty -> defaults to WindowEnd (11:00)
	facts := map[string]store.Fact{
		"medicine": fact(at(8, 30)),
		"water":    fact(at(8, 40)),
		// pets missing
	}
	last := map[string]time.Time{}

	if out := Due([]Routine{r}, facts, last, at(9, 0)); len(out) != 0 {
		t.Fatalf("expected no candidate before nudge time, got %+v", out)
	}

	out := Due([]Routine{r}, facts, last, at(11, 0))
	if len(out) != 1 {
		t.Fatalf("expected 1 candidate at nudge time, got %d", len(out))
	}
	if len(out[0].Missing) != 1 || out[0].Missing[0].Key != "pets" {
		t.Fatalf("expected only pets missing, got %+v", out[0].Missing)
	}
}

func TestDueDoesNotRepeatSameDay(t *testing.T) {
	r := mkRoutine()
	facts := map[string]store.Fact{} // nothing done
	last := map[string]time.Time{}

	if out := Due([]Routine{r}, facts, last, at(11, 0)); len(out) != 1 {
		t.Fatalf("expected first nudge to fire, got %d", len(out))
	}
	if out := Due([]Routine{r}, facts, last, at(11, 30)); len(out) != 0 {
		t.Fatalf("expected no repeat nudge same day, got %d", len(out))
	}
}

func TestDueFiresAgainNextDay(t *testing.T) {
	r := mkRoutine()
	facts := map[string]store.Fact{}
	last := map[string]time.Time{}

	Due([]Routine{r}, facts, last, at(11, 0))

	tomorrow := time.Date(2026, 7, 21, 11, 0, 0, 0, time.UTC) // Tuesday
	if out := Due([]Routine{r}, facts, last, tomorrow); len(out) != 1 {
		t.Fatalf("expected nudge to fire again on a new day, got %d", len(out))
	}
}

func TestDueSkipsWhenAllItemsComplete(t *testing.T) {
	r := mkRoutine()
	facts := map[string]store.Fact{
		"medicine": fact(at(8, 30)),
		"water":    fact(at(8, 40)),
		"pets":     fact(at(8, 50)),
	}
	last := map[string]time.Time{}
	if out := Due([]Routine{r}, facts, last, at(11, 0)); len(out) != 0 {
		t.Fatalf("expected no nudge when all items complete, got %+v", out)
	}
}

func TestDueRespectsExplicitNudgeAt(t *testing.T) {
	r := mkRoutine()
	r.NudgeAt = "10:00"
	facts := map[string]store.Fact{}
	last := map[string]time.Time{}

	if out := Due([]Routine{r}, facts, last, at(9, 30)); len(out) != 0 {
		t.Fatalf("expected no candidate before explicit nudge_at, got %+v", out)
	}
	if out := Due([]Routine{r}, facts, last, at(10, 0)); len(out) != 1 {
		t.Fatalf("expected candidate at explicit nudge_at, got %d", len(out))
	}
}

// TestOptionalItemsDoNotEarnANudge — behaviour 1 of #280, which could not hold
// while every item was implicitly required (Vikunja #473).
func TestOptionalItemsDoNotEarnANudge(t *testing.T) {
	r := Routine{
		Name:        "утро",
		WindowStart: "07:00",
		WindowEnd:   "10:00",
		Items: []Item{
			{Key: "meds", FactKey: "meds", Label: "таблетки"},
			{Key: "stretch", FactKey: "stretch", Label: "растяжка", Optional: true},
		},
	}
	now := time.Date(2026, 8, 4, 10, 0, 0, 0, time.UTC)
	took := map[string]store.Fact{"meds": {Key: "meds", Ts: now.Add(-2 * time.Hour)}}

	// Only the stretch was skipped: nothing to say.
	if due := Due([]Routine{r}, took, map[string]time.Time{}, now); len(due) != 0 {
		t.Fatalf("an optional item alone must not nudge, got %+v", due)
	}
	// The medication was skipped: she says so, and mentions the stretch too.
	due := Due([]Routine{r}, map[string]store.Fact{}, map[string]time.Time{}, now)
	if len(due) != 1 {
		t.Fatalf("a missing required item must nudge, got %+v", due)
	}
	if got := Required(due[0].Missing); len(got) != 1 || got[0].Key != "meds" {
		t.Fatalf("Required = %+v, want the meds item alone", got)
	}
	if got := OptionalOnly(due[0].Missing); len(got) != 1 || got[0].Key != "stretch" {
		t.Fatalf("OptionalOnly = %+v, want the stretch item alone", got)
	}
	// The window still reports it as missing — optional is not invisible.
	st := Evaluate(r, map[string]store.Fact{}, now.Add(-time.Hour))
	if len(st.Missing) != 2 {
		t.Fatalf("Evaluate must still list both, got %+v", st.Missing)
	}
}
