package morning

import (
	"strings"
	"testing"
	"time"
	"unicode"

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

// planAt is at() for the plan tests' day (2026-08-03, a Monday); the existing
// at() in morning_test.go is pinned to a different date.
func planAt(now time.Time, hh, mm int) time.Time {
	y, m, d := now.Date()
	return time.Date(y, m, d, hh, mm, 0, 0, now.Location())
}

func planFixture(t *testing.T) (Plan, time.Time) {
	t.Helper()
	now := time.Date(2026, 8, 3, 9, 0, 0, 0, time.UTC)
	routines := []Routine{{
		Name:        "утро",
		WindowStart: "07:00",
		WindowEnd:   "11:00",
		NudgeAt:     "10:30",
		Items: []Item{
			{Key: "water", FactKey: "drank_water", Label: "выпить воды"},
			{Key: "pills", FactKey: "took_pills", Label: "витамины"},
		},
	}}
	facts := map[string]store.Fact{
		"drank_water": {Ts: planAt(now, 8, 0)},
	}
	events := []PlanEntry{
		{At: planAt(now, 14, 0), Text: "Планёрка @ 14:00-14:30", Kind: PlanEvent, Uncertain: true},
		{At: planAt(now, 10, 0), Text: "Standup @ 10:00-10:30", Kind: PlanEvent},
	}
	reminders := []PlanEntry{
		{At: planAt(now, 18, 30), Text: "позвонить маме", Kind: PlanReminder},
	}
	return BuildPlan(routines, facts, events, reminders, now), now
}

func TestBuildPlanOrdersTheDay(t *testing.T) {
	p, now := planFixture(t)

	if !p.Date.Equal(planAt(now, 0, 0)) {
		t.Errorf("Date = %v, want midnight of now's day", p.Date)
	}
	want := []struct {
		hhmm string
		kind PlanKind
	}{
		{"10:00", PlanEvent},
		{"10:30", PlanChecklist},
		{"14:00", PlanEvent},
		{"18:30", PlanReminder},
	}
	if len(p.Items) != len(want) {
		t.Fatalf("got %d items, want %d: %+v", len(p.Items), len(want), p.Items)
	}
	for i, w := range want {
		if got := p.Items[i].At.Format("15:04"); got != w.hhmm {
			t.Errorf("item %d at %s, want %s", i, got, w.hhmm)
		}
		if p.Items[i].Kind != w.kind {
			t.Errorf("item %d kind %q, want %q", i, p.Items[i].Kind, w.kind)
		}
	}
}

// The checklist line says what is LEFT. An item already evidenced today must
// not be read back as outstanding.
func TestBuildPlanChecklistListsOnlyMissing(t *testing.T) {
	p, _ := planFixture(t)
	var line string
	for _, it := range p.Items {
		if it.Kind == PlanChecklist {
			line = it.Text
		}
	}
	if line == "" {
		t.Fatal("no checklist line in the plan")
	}
	if !strings.Contains(line, "витамины") {
		t.Errorf("missing item not listed: %q", line)
	}
	if strings.Contains(line, "выпить воды") {
		t.Errorf("a completed item must not be read back as outstanding: %q", line)
	}
	if !strings.HasPrefix(line, "утро — осталось:") {
		t.Errorf("line = %q", line)
	}
}

func TestBuildPlanSkipsCompleteAndInactiveRoutines(t *testing.T) {
	now := time.Date(2026, 8, 3, 9, 0, 0, 0, time.UTC)
	routines := []Routine{
		{
			Name: "утро", WindowStart: "07:00", WindowEnd: "11:00",
			Items: []Item{{Key: "water", FactKey: "drank_water", Label: "выпить воды"}},
		},
		{
			// Not in its window at 09:00.
			Name: "вечер", WindowStart: "20:00", WindowEnd: "23:00",
			Items: []Item{{Key: "walk", FactKey: "walked", Label: "прогулка"}},
		},
	}
	facts := map[string]store.Fact{"drank_water": {Ts: planAt(now, 8, 0)}}
	p := BuildPlan(routines, facts, nil, nil, now)
	if len(p.Items) != 0 {
		t.Fatalf("a complete routine and an out-of-window one must contribute nothing: %+v", p.Items)
	}
	if got, want := p.FormatRU(), "на 03.08.2026 ничего не запланировано."; got != want {
		t.Errorf("got  %q\nwant %q", got, want)
	}
}

// A plan for today that includes tomorrow's meeting is worse than terse.
func TestBuildPlanDropsOtherDays(t *testing.T) {
	now := time.Date(2026, 8, 3, 9, 0, 0, 0, time.UTC)
	events := []PlanEntry{
		{At: planAt(now, 10, 0), Text: "today", Kind: PlanEvent},
		{At: planAt(now, 10, 0).AddDate(0, 0, 1), Text: "tomorrow", Kind: PlanEvent},
		{At: planAt(now, 10, 0).AddDate(0, 0, -1), Text: "yesterday", Kind: PlanEvent},
		{At: planAt(now, 12, 0), Text: "  ", Kind: PlanEvent},
	}
	p := BuildPlan(nil, nil, events, nil, now)
	if len(p.Items) != 1 || p.Items[0].Text != "today" {
		t.Fatalf("got %+v", p.Items)
	}
}

func TestPlanFormatRU(t *testing.T) {
	p, _ := planFixture(t)
	got := p.FormatRU()
	want := "план на 03.08.2026: 10:00 — Standup @ 10:00-10:30; " +
		"10:30 — утро — осталось: витамины; " +
		"похоже, 14:00 — Планёрка @ 14:00-14:30; " +
		"18:30 — позвонить маме"
	if got != want {
		t.Errorf("got  %q\nwant %q", got, want)
	}
	// Persona: she recites, she does not exhort, and she never speaks of
	// herself in the masculine or addresses him formally.
	for _, bad := range []string{"рад ", "понял", "вы ", "ваш", "милый", "дорогой", "давай же", "не забудь"} {
		if strings.Contains(strings.ToLower(got), bad) {
			t.Errorf("plan text contains %q: %q", bad, got)
		}
	}
}

func TestPlanAfter(t *testing.T) {
	p, now := planFixture(t)
	rest := p.After(planAt(now, 11, 0))
	if len(rest.Items) != 2 {
		t.Fatalf("got %d items, want the 14:00 and 18:30 ones: %+v", len(rest.Items), rest.Items)
	}
	if !rest.Date.Equal(p.Date) {
		t.Error("After must keep the date, so an empty rest-of-day still knows which day")
	}
	empty := p.After(planAt(now, 23, 0))
	if len(empty.Items) != 0 {
		t.Errorf("got %+v", empty.Items)
	}
	// An empty rest-of-day is not an empty day. Saying "на 03.08.2026 ничего
	// не запланировано" at 23:00 denies the day he just lived.
	if got, want := empty.FormatRU(), "на сегодня больше ничего не запланировано."; got != want {
		t.Errorf("empty rest-of-day reads %q, want %q", got, want)
	}
}

// The plan says what today still has not got done, and a closed window does not
// make a skipped routine untrue. Evaluate reports Active only inside the
// window, so keying the checklist line off it meant the one thing the plan can
// tell him that the calendar cannot went silent at 11:00.
func TestBuildPlanKeepsAClosedWindowOutstanding(t *testing.T) {
	now := time.Date(2026, 8, 3, 14, 0, 0, 0, time.UTC)
	routines := []Routine{{
		Name: "утро", WindowStart: "07:00", WindowEnd: "11:00", NudgeAt: "10:30",
		Items: []Item{
			{Key: "water", FactKey: "drank_water", Label: "выпить воды"},
			{Key: "pills", FactKey: "took_pills", Label: "витамины"},
		},
	}}
	facts := map[string]store.Fact{"drank_water": {Ts: planAt(now, 8, 0)}}

	p := BuildPlan(routines, facts, nil, nil, now)
	if len(p.Items) != 1 {
		t.Fatalf("got %+v, want the unfinished morning routine", p.Items)
	}
	it := p.Items[0]
	if it.Kind != PlanChecklist {
		t.Errorf("kind = %q", it.Kind)
	}
	// Placed at the nudge time, so it sorts to the top of the day rather than
	// to the moment of asking.
	if got := it.At.Format("15:04"); got != "10:30" {
		t.Errorf("placed at %s, want 10:30", got)
	}
	if !strings.Contains(it.Text, "витамины") || strings.Contains(it.Text, "выпить воды") {
		t.Errorf("line = %q", it.Text)
	}
}

// A routine whose window has not opened yet is not outstanding. Nothing has
// been skipped at 06:00.
func TestBuildPlanIgnoresAnUnopenedWindow(t *testing.T) {
	now := time.Date(2026, 8, 3, 6, 0, 0, 0, time.UTC)
	routines := []Routine{{
		Name: "утро", WindowStart: "07:00", WindowEnd: "11:00",
		Items: []Item{{Key: "water", FactKey: "drank_water", Label: "выпить воды"}},
	}}
	if p := BuildPlan(routines, nil, nil, nil, now); len(p.Items) != 0 {
		t.Fatalf("got %+v", p.Items)
	}
}

// plan_uncertain nests one rendered line inside another sentence: «похоже, » in
// front of what this loop already built. That reads as one sentence only while
// what arrives starts lowercase, and it does here because every line starts with
// the clock time. A capital after the hedge would be «похоже, Планёрка».
func TestTheUncertainHedgeRunsIntoLowercase(t *testing.T) {
	now := time.Date(2026, 8, 3, 9, 0, 0, 0, time.UTC)
	p := Plan{Date: now, Items: []PlanEntry{
		{At: planAt(now, 14, 0), Text: "Планёрка", Kind: PlanEvent, Uncertain: true},
	}}
	got := p.FormatRU()
	const hedge = "похоже, "
	i := strings.Index(got, hedge)
	if i < 0 {
		t.Fatalf("%q does not hedge an uncertain item", got)
	}
	for _, r := range got[i+len(hedge):] {
		if unicode.IsUpper(r) {
			t.Fatalf("the hedge runs into a capital: %q", got)
		}
		break
	}
}
