package router

import (
	"context"
	"testing"
	"time"
)

func refNow() time.Time { return time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC) }

// seedClassifier — the spec's "~10 examples/intent" bootstrap, trimmed for the
// test. Real surface words ⇒ the hash embedder gives same-words-similar-vectors
// ⇒ the centroid math routes correctly without the ONNX model.
func seedClassifier(t *testing.T, c *Classifier) {
	t.Helper()
	ctx := context.Background()
	acts := []string{"restart nginx", "restart the backup", "stop nginx", "run the backup now"}
	reminders := []string{"remind me at seven", "wake me at seven", "remind me in four hours", "wake me tuesday"}
	facts := []string{"drank water", "i drank water", "ate lunch", "slept six hours", "had a meal"}
	notes := []string{"gpu driver fixed the flicker", "prefer backups at three am", "note that the router reboots on tuesday"}
	queries := []string{"is the backup up", "when did i last eat", "is nginx running", "how much water today"}
	chats := []string{"what do you think about", "tell me something interesting", "how are you", "расскажи что-нибудь", "что ты думаешь", "как дела"}
	for _, x := range acts {
		if err := c.AddExample(ctx, IntentAct, x); err != nil {
			t.Fatalf("seed act %q: %v", x, err)
		}
	}
	for _, x := range reminders {
		if err := c.AddExample(ctx, IntentReminder, x); err != nil {
			t.Fatalf("seed reminder %q: %v", x, err)
		}
	}
	for _, x := range facts {
		if err := c.AddExample(ctx, IntentFact, x); err != nil {
			t.Fatalf("seed fact %q: %v", x, err)
		}
	}
	for _, x := range notes {
		if err := c.AddExample(ctx, IntentNote, x); err != nil {
			t.Fatalf("seed note %q: %v", x, err)
		}
	}
	for _, x := range queries {
		if err := c.AddExample(ctx, IntentQuery, x); err != nil {
			t.Fatalf("seed query %q: %v", x, err)
		}
	}
	for _, x := range chats {
		if err := c.AddExample(ctx, IntentChat, x); err != nil {
			t.Fatalf("seed chat %q: %v", x, err)
		}
	}
}

func newTestRouter(t *testing.T, threshold float64) *Router {
	t.Helper()
	// dim 1024: a hash embedder is bag-of-words, so collisions across intents
	// would mask the real centroid math. 1024 buckets over ~40 tokens makes
	// collisions negligible — the test exercises the cascade, not the hash.
	emb := NewHashEmbedder(1024)
	c := NewClassifier(emb)
	seedClassifier(t, c)
	acts := DefaultActMatcher{Fns: []string{"restart", "stop", "run", "backup"}}
	ex := Extractor{
		Time:  StubDateTimeParser{},
		Acts:  acts,
		Facts: DefaultFactParser{},
	}
	return New(Config{
		Grammars:   DefaultGrammars(acts),
		Classifier: c,
		Extractor:  ex,
		Threshold:  threshold,
	})
}

// ----------------------------- stage 0 ---------------------------------------

func TestStage0WakeWordAct(t *testing.T) {
	r := newTestRouter(t, 0.0)
	d, err := r.Route(context.Background(), "maven, restart nginx", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Stage != 0 || d.Intent != IntentAct || d.Confidence != 1.0 {
		t.Fatalf("stage0: want stage=0 act conf=1.0, got %+v", d)
	}
	if !d.Slots.HasFn || d.Slots.Fn != "restart" || len(d.Slots.Args) != 1 || d.Slots.Args[0] != "nginx" {
		t.Fatalf("stage0 slots: want fn=restart args=[nginx], got %+v", d.Slots)
	}
}

func TestStage0WakeWordFallsThroughOnUnknownAct(t *testing.T) {
	// wakeword prefix alone doesn't guarantee a known command. "maven, i'm tired"
	// is a fact-ish utterance → falls through to the classifier.
	r := newTestRouter(t, 0.0)
	d, err := r.Route(context.Background(), "maven, i drank water", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Stage == 0 {
		t.Fatalf("unknown act should fall through, got stage0 %+v", d)
	}
}

func TestStage0GrammarFiresThroughCyrillicWakeWord(t *testing.T) {
	// The STT is a Russian model — it transcribes the spoken wake word
	// phonetically ("Мэйвен"), never as the Latin "maven". Grammars that
	// don't expect a wake prefix (time/date) must still fire when one is
	// there, otherwise these fall through to the classifier and get
	// misrouted into IntentReminder (see stage0.go's wakeToken comment).
	r := newTestRouter(t, 0.0)
	r.grammars = append(r.grammars, SystemTimeDateGrammars()...)

	d, err := r.Route(context.Background(), "Мэйвен который час", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Stage != 0 || d.Intent != IntentSystem {
		t.Fatalf("want stage0 system, got %+v", d)
	}
}

// TestStage0ReminderCarriesTheHourHeSaid — "напомни в 11:00 позвонить маме" is
// the commonest reminder there is, and it used to reach the daemon with HasTime
// false, because ReminderGrammar builds its slots by hand and the router ran no
// extraction over a stage-0 decision. The daemon read the silence as absence and
// asked "Когда?" about an hour he had just said (V-572).
func TestStage0ReminderCarriesTheHourHeSaid(t *testing.T) {
	r := newTestRouter(t, 0.0)
	r.grammars = append(r.grammars, ReminderGrammar())

	d, err := r.Route(context.Background(), "напомни в 11:00 позвонить маме", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Stage != 0 || d.Intent != IntentReminder {
		t.Fatalf("want stage0 reminder, got %+v", d)
	}
	if !d.Slots.HasTime {
		t.Fatalf("the hour was spoken, so the slot must be filled: %+v", d.Slots)
	}
	if got, want := d.Slots.Time.Format("15:04"), "11:00"; got != want {
		t.Errorf("fire time = %s, want %s", got, want)
	}
	// The subject is the grammar's, not the extractor's: Slots.Text is what she
	// says at the hour, and Extract would have overwritten it with the sentence.
	if d.Slots.Text != "в 11:00 позвонить маме" {
		t.Errorf("Text = %q, want the grammar's capture", d.Slots.Text)
	}
}

// TestStage0MatchedSlotBeatsTheExtractor — a grammar that matched a literal
// pattern outranks a parser that guessed. The wake-word act names its fn from
// the remainder after the wake token; extraction over the raw utterance must not
// be able to replace it.
func TestStage0MatchedSlotBeatsTheExtractor(t *testing.T) {
	r := newTestRouter(t, 0.0)
	d, err := r.Route(context.Background(), "maven, restart nginx", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Slots.Fn != "restart" || len(d.Slots.Args) != 1 || d.Slots.Args[0] != "nginx" {
		t.Fatalf("matched fn was overwritten: %+v", d.Slots)
	}
	if d.Slots.Text != "restart nginx" {
		t.Errorf("Text = %q, want the grammar's remainder", d.Slots.Text)
	}
}

// TestStage0QueryKeepsAnEmptyText — agendaQueryBuild deliberately leaves Text
// empty so the query chain reads the utterance itself. Extraction fills Time,
// Key and Fn and never Text, or every stage-0 query would start carrying the
// whole sentence in a slot that means something narrower.
func TestStage0QueryKeepsAnEmptyText(t *testing.T) {
	r := newTestRouter(t, 0.0)
	r.grammars = append(r.grammars, AgendaQueryGrammars()...)

	d, err := r.Route(context.Background(), "что у меня сегодня", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Stage != 0 || d.Intent != IntentQuery {
		t.Fatalf("want stage0 query, got %+v", d)
	}
	if d.Slots.Text != "" {
		t.Errorf("Text = %q, want it left empty", d.Slots.Text)
	}
}

// ----------------------------- stage 1 ---------------------------------------

func TestStage1ClassifiesAct(t *testing.T) {
	r := newTestRouter(t, 0.0)
	d, err := r.Route(context.Background(), "restart the backup now", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Intent != IntentAct {
		t.Fatalf("want act, got %s (conf %f)", d.Intent, d.Confidence)
	}
	if !d.Slots.HasFn || d.Slots.Fn != "restart" {
		t.Fatalf("act slots: want fn=restart, got %+v", d.Slots)
	}
}

func TestStage1ClassifiesFact(t *testing.T) {
	r := newTestRouter(t, 0.0)
	d, err := r.Route(context.Background(), "i drank water", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Intent != IntentFact {
		t.Fatalf("want fact, got %s (conf %f)", d.Intent, d.Confidence)
	}
	if !d.Slots.HasKey || d.Slots.Key != "water" {
		t.Fatalf("fact slots: want key=water, got %+v", d.Slots)
	}
}

func TestStage1ClassifiesNoteAndQuery(t *testing.T) {
	r := newTestRouter(t, 0.0)
	cases := []struct {
		in   string
		want Intent
	}{
		{"prefer backups at three am", IntentNote},
		{"gpu driver fixed the flicker", IntentNote},
		{"is the backup up", IntentQuery},
		{"when did i last eat", IntentQuery},
	}
	for _, c := range cases {
		d, err := r.Route(context.Background(), c.in, refNow())
		if err != nil {
			t.Fatalf("route %q: %v", c.in, err)
		}
		if d.Intent != c.want {
			t.Errorf("%q: want %s, got %s (conf %f)", c.in, c.want, d.Intent, d.Confidence)
		}
	}
}

// ----------------------------- stage 2 ---------------------------------------

func TestStage2ReminderSlotExtraction(t *testing.T) {
	r := newTestRouter(t, 0.0)
	now := refNow()
	d, err := r.Route(context.Background(), "remind me in four hours", now)
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Intent != IntentReminder {
		t.Fatalf("want reminder, got %s", d.Intent)
	}
	if !d.Slots.HasTime {
		t.Fatalf("reminder: want HasTime, got %+v", d.Slots)
	}
	want := now.Add(4 * time.Hour)
	if !d.Slots.Time.Equal(want) {
		t.Fatalf("reminder time: want %v, got %v", want, d.Slots.Time)
	}
}

func TestStage2ReminderAtClockRollsToTomorrow(t *testing.T) {
	// "wake me at 7" said at 12:00 → fires tomorrow 07:00 (already past today).
	r := newTestRouter(t, 0.0)
	now := refNow()
	d, err := r.Route(context.Background(), "wake me at 7", now)
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Intent != IntentReminder {
		t.Fatalf("want reminder, got %s", d.Intent)
	}
	want := time.Date(2026, 7, 1, 7, 0, 0, 0, time.UTC)
	if !d.Slots.Time.Equal(want) {
		t.Fatalf("wake-at-7: want %v, got %v", want, d.Slots.Time)
	}
}

func TestStage2FactSleptDuration(t *testing.T) {
	r := newTestRouter(t, 0.0)
	d, err := r.Route(context.Background(), "slept 6h", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Intent != IntentFact {
		t.Fatalf("want fact, got %s", d.Intent)
	}
	if d.Slots.Key != "sleep" || d.Slots.Value != `"6h"` {
		t.Fatalf("slept slots: want key=sleep value=\"6h\", got %+v", d.Slots)
	}
}

// ----------------------------- stage 3 ---------------------------------------

func TestStage3ClarifyBelowThreshold(t *testing.T) {
	// high threshold ⇒ even a well-classified utterance is gated to clarify.
	// "shuts up when uncertain": a misrouted fact is a confident wrong write.
	r := newTestRouter(t, 0.99)
	d, err := r.Route(context.Background(), "i drank water", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if !d.Clarify || d.Stage != 3 {
		t.Fatalf("want stage3 clarify, got stage=%d clarify=%v (conf %f)", d.Stage, d.Clarify, d.Confidence)
	}
	// the best-guess intent + slots still travel with the decision so the
	// clarify prompt can use them ("did you mean — you drank water?")
	if d.Intent != IntentFact {
		t.Fatalf("clarify should still carry best guess, got %s", d.Intent)
	}
}

func TestStage3PassesAboveThreshold(t *testing.T) {
	r := newTestRouter(t, 0.0) // threshold 0 ⇒ nothing gated
	d, err := r.Route(context.Background(), "i drank water", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if d.Clarify {
		t.Fatalf("threshold 0 should never clarify, got %+v", d)
	}
}

// ----------------------------- cold boot -------------------------------------

func TestColdBootNoIntents(t *testing.T) {
	// unseeded classifier → free-form input cannot be routed. stage 0 still
	// works (grammar path). "shuts up when uncertain" for routing.
	emb := NewHashEmbedder(128)
	c := NewClassifier(emb)
	r := New(Config{Classifier: c, Threshold: 0})
	if _, err := r.Route(context.Background(), "something freeform", refNow()); err != ErrNoIntents {
		t.Fatalf("cold boot: want ErrNoIntents, got %v", err)
	}
}

// ----------------------------- misroute correction ---------------------------

func TestCorrectMisrouteGrowsClassifier(t *testing.T) {
	r := newTestRouter(t, 0.4)
	// "note the backup is broken" looks note-ish but the user meant a fact
	// (loop should know the backup is down). Without correction it routes note.
	before, err := r.Route(context.Background(), "backup is broken", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if before.Intent == IntentFact {
		t.Fatalf("precondition: expected non-fact, got %s", before.Intent)
	}
	// user corrects → append a new example for the corrected intent.
	if err := r.CorrectMisroute(context.Background(), "backup is broken", IntentFact); err != nil {
		t.Fatalf("correct: %v", err)
	}
	// a few reinforcements so the centroid shifts decisively.
	for _, x := range []string{"backup is down", "backup failed", "backup broken now"} {
		if err := r.CorrectMisroute(context.Background(), x, IntentFact); err != nil {
			t.Fatalf("correct: %v", err)
		}
	}
	after, err := r.Route(context.Background(), "backup is broken", refNow())
	if err != nil {
		t.Fatalf("route: %v", err)
	}
	if after.Intent != IntentFact {
		t.Fatalf("after correction: want fact, got %s (conf %f)", after.Intent, after.Confidence)
	}
}

// ----------------------------- classifier unit -------------------------------

func TestClassifierDeterministicOrdering(t *testing.T) {
	emb := NewHashEmbedder(64)
	c := NewClassifier(emb)
	ctx := context.Background()
	_ = c.AddExample(ctx, IntentAct, "restart nginx")
	_ = c.AddExample(ctx, IntentFact, "drank water")
	r1, _ := c.Classify(ctx, "restart nginx")
	r2, _ := c.Classify(ctx, "restart nginx")
	if len(r1) != len(r2) {
		t.Fatalf("non-deterministic length")
	}
	for i := range r1 {
		if r1[i] != r2[i] {
			t.Fatalf("non-deterministic ordering at %d: %v vs %v", i, r1[i], r2[i])
		}
	}
	if r1[0].Intent != IntentAct {
		t.Fatalf("best match should be act, got %s", r1[0].Intent)
	}
}

func TestStage1ClassifiesChat(t *testing.T) {
	r := newTestRouter(t, 0.0)
	cases := []struct {
		in   string
		want Intent
	}{
		{"what do you think about ai", IntentChat},
		{"tell me something interesting", IntentChat},
		{"как дела", IntentChat},
		{"расскажи что-нибудь", IntentChat},
	}
	for _, c := range cases {
		d, err := r.Route(context.Background(), c.in, refNow())
		if err != nil {
			t.Fatalf("route %q: %v", c.in, err)
		}
		if d.Intent != c.want {
			t.Errorf("%q: want %s, got %s (conf %f)", c.in, c.want, d.Intent, d.Confidence)
		}
	}
}

func TestClassifierIntentsSorted(t *testing.T) {
	emb := NewHashEmbedder(32)
	c := NewClassifier(emb)
	ctx := context.Background()
	_ = c.AddExample(ctx, IntentQuery, "q")
	_ = c.AddExample(ctx, IntentAct, "a")
	_ = c.AddExample(ctx, IntentFact, "f")
	got := c.Intents()
	want := []Intent{IntentAct, IntentFact, IntentQuery}
	if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] || got[2] != want[2] {
		t.Fatalf("intents sort: want %v, got %v", want, got)
	}
}
