package lexicon

import "testing"

// TestClosedSetsAreComplete — the point of the package. A closed class can be
// finished, so the test names the members that were MISSING from the inline Go
// lists this package replaced (Vikunja #525) and every one of them has to be
// there. Add to this list when a form turns up unhandled.
func TestClosedSetsAreComplete(t *testing.T) {
	inter := map[string]bool{}
	for _, w := range Interrogatives() {
		inter[w] = true
	}
	// Instrumental and prepositional cases of что and кто, and the declined
	// какой. The old list had что/чего and nothing else, so "чем ты занята" and
	// "в каком часу" carried no interrogative at all.
	for _, w := range []string{"чем", "чём", "чему", "кем", "ком", "каком", "какими", "насколько"} {
		if !inter[w] {
			t.Errorf("interrogatives is missing %q", w)
		}
	}
	locative := map[string]bool{}
	for _, w := range LocativeInterrogatives() {
		locative[w] = true
		if !inter[w] {
			t.Errorf("locative interrogative %q is not in the full interrogative set", w)
		}
	}
	for _, w := range []string{"где", "куда", "откуда", "докуда", "where"} {
		if !locative[w] {
			t.Errorf("locative_interrogatives is missing %q", w)
		}
	}

	for _, tc := range []struct {
		word string
		want int
	}{
		{"ноль", 0}, {"одна", 1}, {"две", 2}, {"одиннадцать", 11},
		{"пятнадцать", 15}, {"двадцать", 20}, {"сорок", 40}, {"девяносто", 90},
		{"сто", 100}, {"twelve", 12},
	} {
		got, ok := Cardinal(tc.word)
		if !ok || got != tc.want {
			t.Errorf("Cardinal(%q) = %d, %v; want %d, true", tc.word, got, ok, tc.want)
		}
	}
	if _, ok := Cardinal("бэкап"); ok {
		t.Error("Cardinal must not answer for a word that is not a number")
	}

	// A spoken hour declines, and one to four decline further than the rest:
	// "к двум часам" and "к трём" are hours, and only the dative says so (V-581).
	for _, tc := range []struct {
		word string
		want int
	}{
		{"одному", 1}, {"двум", 2}, {"двумя", 2}, {"трём", 3}, {"трем", 3},
		{"четырём", 4}, {"четырем", 4}, {"пяти", 5}, {"семи", 7},
	} {
		if got, ok := Cardinal(tc.word); !ok || got != tc.want {
			t.Errorf("Cardinal(%q) = %d, %v; want %d, true", tc.word, got, ok, tc.want)
		}
	}
}

// TestWeekdaysAreOneList — the second copy of a closed class is the bug (V-581).
// Weekdays lived in four files outside this one, so the list is handed out whole
// and the English forms, which the Russian dictionary cannot lemmatise, are here.
func TestWeekdaysAreOneList(t *testing.T) {
	days := Weekdays()
	if len(days) != 7 || days[0] != "воскресенье" || days[1] != "понедельник" {
		t.Fatalf("Weekdays() = %v; want the seven, Sunday first", days)
	}
	for i, name := range days {
		if Weekday(i) != name {
			t.Errorf("Weekdays()[%d] = %q, but Weekday(%d) = %q", i, name, i, Weekday(i))
		}
	}
	for _, tc := range []struct {
		word string
		want int
	}{{"sunday", 0}, {"monday", 1}, {"mondays", 1}, {"Friday", 5}, {"saturdays", 6}} {
		if got, ok := WeekdayEnglish(tc.word); !ok || got != tc.want {
			t.Errorf("WeekdayEnglish(%q) = %d, %v; want %d, true", tc.word, got, ok, tc.want)
		}
	}
	if _, ok := WeekdayEnglish("понедельник"); ok {
		t.Error("WeekdayEnglish answered for a Russian word; that side is morph's")
	}
}

// TestDayOffsetHasNoOrderingTrap — the defect a lookup removes. The callers this
// replaced used strings.Contains in a switch, so "послезавтра" had to be tested
// before "завтра" by hand or the day after tomorrow read as tomorrow.
func TestDayOffsetHasNoOrderingTrap(t *testing.T) {
	for _, tc := range []struct {
		text string
		want int
		ok   bool
	}{
		{"послезавтра", 2, true},
		{"завтра", 1, true},
		{"сегодня", 0, true},
		{"вчера", -1, true},
		{"позавчера", -2, true},
		{"встреча послезавтра в 14:30", 2, true},
		{"Tomorrow at 09:00", 1, true},
		{"не сегодня, а послезавтра", 2, true},
		{"в четверг", 0, false},
		{"завтраком", 0, false},
	} {
		got, ok := DayOffsetIn(tc.text)
		if ok != tc.ok || (ok && got != tc.want) {
			t.Errorf("DayOffsetIn(%q) = %d, %v; want %d, %v", tc.text, got, ok, tc.want, tc.ok)
		}
	}
	// "сегодня" is offset 0, which is also the zero value, so the second return
	// is the only thing that separates a hit from a miss.
	if n, ok := DayOffset("сегодня"); n != 0 || !ok {
		t.Errorf("DayOffset(сегодня) = %d, %v; want 0, true", n, ok)
	}
}

// TestIndexedSetsLineUpWithTheirCallers — weekdays start at Sunday because Go's
// time.Weekday does, and months are 1-indexed so a month number needs no
// arithmetic. Off-by-one here is a wrong date spoken out loud.
func TestIndexedSetsLineUpWithTheirCallers(t *testing.T) {
	if got := Weekday(0); got != "воскресенье" {
		t.Errorf("Weekday(0) = %q, want воскресенье", got)
	}
	if got := Weekday(1); got != "понедельник" {
		t.Errorf("Weekday(1) = %q, want понедельник", got)
	}
	if got := MonthGenitive(1); got != "января" {
		t.Errorf("MonthGenitive(1) = %q, want января", got)
	}
	if got := MonthGenitive(12); got != "декабря" {
		t.Errorf("MonthGenitive(12) = %q, want декабря", got)
	}
	if got := MonthGenitive(0); got != "" {
		t.Errorf("MonthGenitive(0) = %q, want the empty slot", got)
	}
	if got := HourSpoken(23); got != "двадцать три" {
		t.Errorf("HourSpoken(23) = %q, want двадцать три", got)
	}
	for _, i := range []int{-1, 7, 13, 24} {
		if got := Weekday(i); i >= 7 && got != "" {
			t.Errorf("Weekday(%d) = %q, want empty", i, got)
		}
	}
	if got := HourSpoken(24); got != "" {
		t.Errorf("HourSpoken(24) = %q, want empty", got)
	}
}

// TestCallerCannotEditTheLexicon — the sets are handed out as copies. A caller
// that sorted the slice it was given would otherwise reorder weekdays for
// everybody.
func TestCallerCannotEditTheLexicon(t *testing.T) {
	first := Interrogatives()
	first[0] = "мутировало"
	if again := Interrogatives(); again[0] == "мутировало" {
		t.Fatal("the lexicon handed out its own backing array")
	}
}

// The positions carry gender and oblique forms, because "второй пункт" and
// "закрепи вторым" name one position (Vikunja #516). "last" is a position and not
// a count, so it is -1 rather than a large number.
func TestOrdinalsSpanGenderAndCase(t *testing.T) {
	for _, w := range []string{"второй", "вторая", "второе", "вторым", "второго", "second"} {
		n, ok := Ordinal(w)
		if !ok || n != 2 {
			t.Errorf("Ordinal(%q) = %d, %v; want 2, true", w, n, ok)
		}
	}
	for _, w := range []string{"последний", "последнюю", "last"} {
		if n, ok := Ordinal(w); !ok || n != -1 {
			t.Errorf("Ordinal(%q) = %d, %v; want -1, true", w, n, ok)
		}
	}
	// A weekday shares a stem with a position and is not one.
	if n, ok := Ordinal("вторник"); ok {
		t.Errorf("Ordinal(\"вторник\") = %d; a weekday is not a position", n)
	}
}

// Earliest wins, not map order: the same sentence must answer the same way twice.
func TestOrdinalInTakesTheFirstPosition(t *testing.T) {
	for i := 0; i < 50; i++ {
		n, ok := OrdinalIn("отметь первый и второй пункт")
		if !ok || n != 1 {
			t.Fatalf("run %d: OrdinalIn = %d, %v; want 1, true", i, n, ok)
		}
	}
	if _, ok := OrdinalIn("отметь пункт"); ok {
		t.Error("a sentence with no position reported one")
	}
}

// Ordinals is the escape hatch for the cases the file does not list, so it must
// hand out every entry and hand out the same order twice.
func TestOrdinalsListIsCompleteAndStable(t *testing.T) {
	a, b := Ordinals(), Ordinals()
	if len(a) != len(ru.Sets["ordinals"].Values) {
		t.Errorf("Ordinals returned %d of %d entries", len(a), len(ru.Sets["ordinals"].Values))
	}
	for i := range a {
		if a[i] != b[i] {
			t.Fatalf("Ordinals order is not stable at %d: %v vs %v", i, a[i], b[i])
		}
	}
}
