package tasks

import (
	"strings"
	"testing"
	"time"

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

func at(y int, m time.Month, d int) *time.Time {
	t := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
	return &t
}

func now() time.Time { return time.Date(2026, 8, 1, 14, 0, 0, 0, time.UTC) }

func texts(rs []Ranked) []string {
	out := make([]string, len(rs))
	for i, r := range rs {
		out[i] = r.Text
	}
	return out
}

func TestRankOrdersByDeadline(t *testing.T) {
	items := []Item{
		{ID: 1, Text: "через неделю", Status: StatusOpen, Due: at(2026, 8, 7), Created: now()},
		{ID: 2, Text: "просрочено", Status: StatusOpen, Due: at(2026, 7, 28), Created: now()},
		{ID: 3, Text: "без срока", Status: StatusOpen, Created: now()},
		{ID: 4, Text: "сегодня", Status: StatusOpen, Due: at(2026, 8, 1), Created: now()},
		{ID: 5, Text: "завтра", Status: StatusOpen, Due: at(2026, 8, 2), Created: now()},
	}
	got := texts(Rank(items, now()))
	want := []string{"просрочено", "сегодня", "завтра", "через неделю", "без срока"}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("order = %v, want %v", got, want)
		}
	}
}

func TestRankCandidatesNeverOutrankOpenWork(t *testing.T) {
	items := []Item{
		{ID: 1, Text: "его задача", Status: StatusOpen, Created: now()},
		// Everything about this one screams urgent — and it is still a guess.
		{ID: 2, Text: "из письма", Status: StatusCandidate, Due: at(2026, 7, 1), Weight: 3, Created: now()},
	}
	got := Rank(items, now())
	if got[0].Text != "его задача" {
		t.Errorf("order = %v, want his own work first", texts(got))
	}
}

func TestRankWeightLiftsUndatedWork(t *testing.T) {
	items := []Item{
		{ID: 1, Text: "обычная", Status: StatusOpen, Created: now()},
		{ID: 2, Text: "важная", Status: StatusOpen, Weight: 2, Created: now()},
	}
	got := Rank(items, now())
	if got[0].Text != "важная" {
		t.Errorf("order = %v, want the weighted task first", texts(got))
	}
	if got[0].Reason != "важно" {
		t.Errorf("reason = %q, want важно", got[0].Reason)
	}
	// A deadline still beats a weight: a date is a fact, a weight is a feeling.
	items = append(items, Item{ID: 3, Text: "сегодня", Status: StatusOpen, Due: at(2026, 8, 1), Created: now()})
	got = Rank(items, now())
	if got[0].Text != "сегодня" {
		t.Errorf("order = %v, want the dated task first", texts(got))
	}
}

func TestRankOldestFirstOnATie(t *testing.T) {
	old := now().AddDate(0, 0, -3)
	items := []Item{
		{ID: 1, Text: "новая", Status: StatusOpen, Created: now()},
		{ID: 2, Text: "старая", Status: StatusOpen, Created: old},
	}
	got := Rank(items, now())
	if got[0].Text != "старая" {
		t.Errorf("order = %v, want FIFO on equal urgency", texts(got))
	}
}

func TestRankNoInventedReason(t *testing.T) {
	got := Rank([]Item{{ID: 1, Text: "что-то", Status: StatusOpen, Created: now()}}, now())
	if got[0].Reason != "" {
		t.Errorf("reason = %q — nothing distinguished this task, so there is nothing to say", got[0].Reason)
	}
	if got[0].Score != 0 {
		t.Errorf("score = %v, want 0", got[0].Score)
	}
}

func TestRankAgeIsCappedAndNamed(t *testing.T) {
	items := []Item{
		{ID: 1, Text: "прошлогодняя", Status: StatusOpen, Created: now().AddDate(-1, 0, 0)},
		{ID: 2, Text: "трёхнедельная", Status: StatusOpen, Created: now().AddDate(0, 0, -21)},
	}
	got := Rank(items, now())
	if got[0].Score != scoreAgeCap {
		t.Errorf("oldest score = %v, want the cap %v", got[0].Score, float64(scoreAgeCap))
	}
	if got[0].Reason != "давно в списке" {
		t.Errorf("reason = %q", got[0].Reason)
	}
}

// A task due at 23:00 today is due today, not overdue since this morning.
func TestRankDueTodayIsNotOverdue(t *testing.T) {
	due := time.Date(2026, 8, 1, 23, 0, 0, 0, time.UTC)
	got := Rank([]Item{{ID: 1, Text: "вечером", Status: StatusOpen, Due: &due, Created: now()}}, now())
	if got[0].Reason != "сегодня" {
		t.Errorf("reason = %q, want сегодня", got[0].Reason)
	}
}

func TestRankOverdueDaysAreCounted(t *testing.T) {
	got := Rank([]Item{
		{ID: 1, Text: "вчера", Status: StatusOpen, Due: at(2026, 7, 31), Created: now()},
		{ID: 2, Text: "давно", Status: StatusOpen, Due: at(2026, 7, 20), Created: now()},
	}, now())
	if got[0].Text != "давно" {
		t.Errorf("order = %v, want the later-overdue task first", texts(got))
	}
	if got[0].Reason != "просрочено на 12 дней" {
		t.Errorf("reason = %q", got[0].Reason)
	}
	// One day goes through the same entry as twelve: «на 1 день» is what the
	// count helper says, so there is no reason_overdue_day any more.
	if got[1].Reason != "просрочено на 1 день" {
		t.Errorf("reason = %q", got[1].Reason)
	}
}

func TestFormatRUNamesReasonsAndSeparatesCandidates(t *testing.T) {
	ranked := Rank([]Item{
		{ID: 1, Text: "оплатить интернет", Status: StatusOpen, Due: at(2026, 8, 1), Created: now()},
		{ID: 2, Text: "купить молоко", Status: StatusOpen, Created: now()},
		{ID: 3, Text: "продлить страховку", Status: StatusCandidate, Due: at(2026, 7, 1), Created: now()},
	}, now())
	got := FormatRU(ranked)
	if !strings.HasPrefix(got, "сначала: оплатить интернет (сегодня)") {
		t.Errorf("reply = %q", got)
	}
	if !strings.Contains(got, "не подтверждал: продлить страховку") {
		t.Errorf("candidate not named as unconfirmed: %q", got)
	}
	// A candidate's due date is Maven's reading of a mail, not his statement.
	if strings.Contains(got, "продлить страховку (") {
		t.Errorf("a candidate must be listed without a reason: %q", got)
	}
	// Persona: nothing masculine, no pet names, informal address only.
	for _, bad := range []string{"рад ", "понял ", "милый", "дорогой", "вам", "ваши"} {
		if strings.Contains(got, bad) {
			t.Errorf("reply %q contains %q", got, bad)
		}
	}
}

func TestFormatRUCapsTheSpokenList(t *testing.T) {
	var items []Item
	for i := 0; i < SpokenLimit+3; i++ {
		items = append(items, Item{ID: int64(i), Text: "задача", Status: StatusOpen, Created: now()})
	}
	got := FormatRU(Rank(items, now()))
	if !strings.Contains(got, "и ещё 3") {
		t.Errorf("reply = %q, want the tail summarised", got)
	}
	if strings.Count(got, "задача") != SpokenLimit {
		t.Errorf("reply = %q, want exactly %d named", got, SpokenLimit)
	}
}

func TestFormatRUEmpty(t *testing.T) {
	if got := FormatRU(nil); got != "задач нет." {
		t.Errorf("reply = %q", got)
	}
}

// A due date read back from the store is a UTC instant, so comparing calendar
// days in ITS location put every date a day out east of Greenwich: the row said
// "сегодня" for a task due tomorrow, and "просрочено на 1 день" on the due date
// itself while the due column one cell over said otherwise.
func TestRankComparesDaysInTheCallersLocation(t *testing.T) {
	tz := time.FixedZone("UTC+4", 4*3600)
	// Entered on the web form as 2026-08-02 local, stored and read back as UTC.
	due := time.Date(2026, 8, 2, 0, 0, 0, 0, tz).UTC()
	local := time.Date(2026, 8, 1, 10, 0, 0, 0, tz)

	got := Rank([]Item{{ID: 1, Text: "оплатить интернет", Status: StatusOpen, Due: &due, Created: local}}, local)
	if got[0].Reason != "завтра" {
		t.Errorf("reason = %q, want завтра on the day before", got[0].Reason)
	}
	// The morning of the due date itself.
	onTheDay := time.Date(2026, 8, 2, 10, 0, 0, 0, tz)
	got = Rank([]Item{{ID: 1, Text: "оплатить интернет", Status: StatusOpen, Due: &due, Created: local}}, onTheDay)
	if got[0].Reason != "сегодня" {
		t.Errorf("reason = %q, want сегодня on the due date", got[0].Reason)
	}
	if got[0].Score != scoreDueToday {
		t.Errorf("score = %v, want %v", got[0].Score, float64(scoreDueToday))
	}
}

// "срочно" and "важно" are two rungs and the read-back said "важно" for both,
// which reports a word he did not say.
func TestRankNamesTheUrgencyHeStated(t *testing.T) {
	got := Rank([]Item{
		{ID: 1, Text: "оплатить интернет", Status: StatusOpen, Weight: 3, Created: now()},
		{ID: 2, Text: "починить кран", Status: StatusOpen, Weight: 2, Created: now()},
	}, now())
	if got[0].Reason != "срочно" {
		t.Errorf("reason = %q, want срочно", got[0].Reason)
	}
	if got[1].Reason != "важно" {
		t.Errorf("reason = %q, want важно", got[1].Reason)
	}
}

// The package doc guarantees a class ordering. Age used to invert it: an
// undated task at the age cap outscored a dated one three weeks out.
func TestRankDatedWorkBeatsAgeAlone(t *testing.T) {
	got := Rank([]Item{
		{ID: 1, Text: "старьё", Status: StatusOpen, Created: now().AddDate(0, 0, -70)},
		{ID: 2, Text: "через три недели", Status: StatusOpen, Due: at(2026, 8, 22), Created: now()},
	}, now())
	if got[0].Text != "через три недели" {
		t.Errorf("order = %v, want the dated task first", texts(got))
	}
}

// A bare "и ещё 5" trails off when spoken.
func TestFormatRUTailCarriesTheNoun(t *testing.T) {
	var items []Item
	for i := 0; i < SpokenLimit+3; i++ {
		items = append(items, Item{ID: int64(i), Text: "дело", Status: StatusOpen, Created: now()})
	}
	if got := FormatRU(Rank(items, now())); !strings.Contains(got, "и ещё 3 задачи") {
		t.Errorf("reply = %q, want the count with its noun", got)
	}
	for n, want := range map[int]string{1: "задача", 2: "задачи", 5: "задач", 11: "задач", 21: "задача"} {
		if got := say.CountWord(n, "задача", "задачи", "задач"); got != want {
			t.Errorf("CountWord(%d) = %q, want %q", n, got, want)
		}
	}
}
