package router

import "testing"

// A lifecycle word alone is ordinary speech. Praxis mutations need an item named
// too, and the two agreement words mean different states (Vikunja #516).
func TestPraxisLifecycleNeedsAnItem(t *testing.T) {
	for _, utt := range []string{
		"готово",
		"принято",
		"сделано, спасибо",
		"понял",
		"неважно",
		"я всё сделал",
	} {
		if c, ok := ParsePraxisLifecycle(utt); ok {
			t.Errorf("%q claimed as %+v; a bare lifecycle word must not transition an item", utt, c)
		}
	}
}

func TestPraxisLifecycleVerbPicksTheCapability(t *testing.T) {
	cases := []struct {
		utt, fn, ref string
	}{
		{"отметь второй пункт", "acknowledge_item", "2"},
		{"принято по первому пункту", "acknowledge_item", "1"},
		{"пункт три готово", "resolve_item", "3"},
		{"закрой последний пункт", "resolve_item", "last"},
		{"игнорируй второй пункт", "ignore_item", "2"},
		{"закрепи третий пункт", "pin_item", "3"},
		{"resolve item_ab12", "resolve_item", "item_ab12"},
		// The noun with no position: the capability asks which one, which is
		// better than guessing and better than falling to the model.
		{"отметь пункт", "acknowledge_item", ""},
	}
	for _, c := range cases {
		got, ok := ParsePraxisLifecycle(c.utt)
		if !ok {
			t.Errorf("%q was not claimed", c.utt)
			continue
		}
		if got.Fn != c.fn || got.Ref != c.ref {
			t.Errorf("%q = %+v, want fn=%s ref=%s", c.utt, got, c.fn, c.ref)
		}
	}
}

// A demonstrative stands in for the item noun, because "отметь это как
// сделанное" is what he says to a digest she just read. Which item it is, is the
// daemon's question — the router only says that he pointed at one.
func TestPraxisLifecycleAcceptsADemonstrative(t *testing.T) {
	for _, c := range []struct{ utt, fn string }{
		{"отметь это как сделанное", "resolve_item"},
		{"принято, я это видел", "acknowledge_item"},
		{"игнорировать это пока", "ignore_item"},
		{"закрепи это", "pin_item"},
	} {
		got, ok := ParsePraxisLifecycle(c.utt)
		if !ok {
			t.Errorf("%q was not claimed", c.utt)
			continue
		}
		if got.Fn != c.fn || got.Ref != "this" {
			t.Errorf("%q = %+v, want fn=%s ref=this", c.utt, got, c.fn)
		}
	}
}

func TestPraxisAttentionGrammar(t *testing.T) {
	g := grammarByName(t, "praxis-attention")
	for _, utt := range []string{"что требует внимания", "что сейчас требует внимания?", "что нового по проектам"} {
		d, ok := matchGrammar(g, utt)
		if !ok {
			t.Errorf("%q was not claimed", utt)
			continue
		}
		if d.Slots.Fn != "list_attention" {
			t.Errorf("%q = fn %q", utt, d.Slots.Fn)
		}
	}
	// The feeds own the unqualified form.
	for _, utt := range []string{"что нового", "что нового в мире"} {
		if _, ok := matchGrammar(g, utt); ok {
			t.Errorf("%q belongs to the feeds, not Praxis", utt)
		}
	}
}

// An imperative is addressed to her, so it claims the turn with an empty slot and
// the capability asks which пункт. A stative word in the same position does not.
func TestImperativeClaimsAndAsksButStativeDoesNot(t *testing.T) {
	for _, c := range []struct{ utt, fn string }{
		{"закрывай", "resolve_item"},
		{"игнорируй пока", "ignore_item"},
		{"закрепи", "pin_item"},
	} {
		got, ok := ParsePraxisLifecycle(c.utt)
		if !ok || got.Fn != c.fn || got.Ref != "" {
			t.Errorf("%q = %+v, %v; want fn=%s with an empty ref", c.utt, got, ok, c.fn)
		}
	}
	// An imperative with an object of its own is about that object: "закрой
	// шторы" closes the curtains through Hexis and is not a Praxis turn.
	for _, utt := range []string{"закрой шторы в комнате", "закрой дверь", "пропусти песню"} {
		if got, ok := ParsePraxisLifecycle(utt); ok {
			t.Errorf("%q claimed as %+v; it names its own object", utt, got)
		}
	}
	for _, utt := range []string{"готово", "принято", "неважно", "решено"} {
		if got, ok := ParsePraxisLifecycle(utt); ok {
			t.Errorf("%q claimed as %+v; a stative word needs an item named", utt, got)
		}
	}
}

// "отметь" records a state and names none, so it cannot pick the transition by
// itself: with a state in the sentence the state wins, without one it is the
// weaker of the two.
func TestMarkerVerbTakesTheStateFromTheSentence(t *testing.T) {
	if got, _ := ParsePraxisLifecycle("отметь второй пункт как сделанный"); got.Fn != "resolve_item" {
		t.Errorf("a named state should win, got %+v", got)
	}
	if got, _ := ParsePraxisLifecycle("отметь второй пункт"); got.Fn != "acknowledge_item" {
		t.Errorf("a marker with no state should acknowledge, got %+v", got)
	}
	// The marker is also a capture verb, so it must not claim a note.
	if got, ok := ParsePraxisLifecycle("отметь что молоко закончилось"); ok {
		t.Errorf("a note was claimed as a Praxis turn: %+v", got)
	}
}

// Two transitions in one sentence are not one transition.
func TestPraxisLifecycleRefusesTwoVerbs(t *testing.T) {
	if c, ok := ParsePraxisLifecycle("первый пункт принято, готово"); ok {
		t.Errorf("two lifecycle verbs resolved to %+v instead of declining", c)
	}
}

// "готов" must not fire inside "готовлю": tokens, not substrings.
func TestPraxisLifecycleDoesNotMatchInsideAWord(t *testing.T) {
	if _, ok := ParsePraxisLifecycle("готовлю первый пункт меню"); ok {
		t.Error("готовлю matched the resolve verb готов")
	}
}

func TestPraxisChangesGrammar(t *testing.T) {
	g := grammarByName(t, "praxis-changes")
	for _, utt := range []string{"что изменилось?", "какие изменения?", "покажи изменения", "what changed?"} {
		if _, ok := matchGrammar(g, utt); !ok {
			t.Errorf("%q was not claimed by praxis-changes", utt)
		}
	}
	// The feeds source owns "что нового", and two paths to one answer disagree.
	for _, utt := range []string{"что нового?", "что нового в мире?", "изменения погоды не волнуют"} {
		if _, ok := matchGrammar(g, utt); ok {
			t.Errorf("%q should not be a Praxis changes turn", utt)
		}
	}
}

func TestPraxisEntityAttentionNeedsASubject(t *testing.T) {
	g := grammarByName(t, "praxis-entity-attention")
	for _, c := range []struct{ utt, subject string }{
		{"статус мавена", "мавена"},
		{"как дела у праксиса?", "праксиса"},
		{"status of hexis", "of hexis"},
	} {
		d, ok := matchGrammar(g, c.utt)
		if !ok {
			t.Errorf("%q was not claimed", c.utt)
			continue
		}
		if d.Slots.Fn != "entity_attention" || d.Slots.Text != c.subject {
			t.Errorf("%q = fn %q text %q, want entity_attention / %q", c.utt, d.Slots.Fn, d.Slots.Text, c.subject)
		}
	}
	// A greeting has no subject and must not reach Nexus.
	for _, utt := range []string{"как дела?", "как дела у тебя", "статус", "привет"} {
		if _, ok := matchGrammar(g, utt); ok && utt != "как дела у тебя" {
			t.Errorf("%q claimed as scoped attention", utt)
		}
	}
}

func grammarByName(t *testing.T, name string) Grammar {
	t.Helper()
	for _, g := range PraxisGrammars() {
		if g.Name == name {
			return g
		}
	}
	t.Fatalf("no grammar named %q", name)
	return Grammar{}
}

func matchGrammar(g Grammar, utt string) (Decision, bool) {
	m := g.Pattern.FindStringSubmatch(utt)
	if m == nil {
		return Decision{}, false
	}
	return g.Build(m)
}
