package router

import (
	"context"
	"reflect"
	"testing"
	"time"

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

func TestClaimOfBands(t *testing.T) {
	cases := []struct {
		name string
		dec  Decision
		want claim.Band
	}{
		{
			name: "stage 0 is anchored",
			dec: Decision{
				Utterance: "сколько времени", Stage: 0, Intent: IntentSystem,
				Confidence: 1.0, Slots: Slots{Text: "сколько времени"},
			},
			want: claim.BandAnchored,
		},
		{
			name: "the llm path is structural",
			dec: Decision{
				Utterance: "выпил воды", Stage: 1, Intent: IntentFact,
				Confidence: llmFullConfidence,
				Slots:      Slots{Key: "water", Value: "1", HasKey: true, Text: "выпил воды"},
			},
			want: claim.BandStructural,
		},
		{
			name: "the classifier is nearest",
			dec: Decision{
				Utterance: "что нового", Stage: 2, Intent: IntentQuery,
				Confidence: 0.91, Slots: Slots{Text: "что нового"},
			},
			want: claim.BandNearest,
		},
		{
			name: "a structural hole vetoes whoever found it",
			dec: Decision{
				Utterance: "запиши", Stage: 1, Intent: IntentFact,
				Confidence: llmThinConfidence, Slots: Slots{Text: "запиши"},
			},
			want: claim.BandVetoed,
		},
		{
			name: "clarify vetoes a classifier decision",
			dec: Decision{
				Utterance: "сделай это", Stage: 3, Intent: IntentNote,
				Confidence: 0.2, Clarify: true, Slots: Slots{Text: "сделай это"},
			},
			want: claim.BandVetoed,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := ClaimOf("test", tc.dec)
			if got.Band != tc.want {
				t.Errorf("band = %v, want %v (veto %q)", got.Band, tc.want, got.Veto)
			}
		})
	}
}

// The veto has to name the hole. Folding all three arms into llmThinConfidence
// is what lost the reason, and 0.3 tells a reader that something was wrong but
// never which thing.
func TestClaimOfVetoNamesTheHole(t *testing.T) {
	cases := []struct {
		name string
		dec  Decision
		want string
	}{
		{"keyless fact", Decision{Intent: IntentFact}, "fact with no key"},
		{"act with no fn", Decision{Intent: IntentAct}, "act with no allowlisted fn"},
		{"subjectless reminder", Decision{Intent: IntentReminder, Slots: Slots{Text: "напомни"}}, "reminder with no subject"},
		{"below the gate", Decision{Intent: IntentQuery, Clarify: true}, "below the confidence gate"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := ClaimOf("test", tc.dec)
			if !got.Vetoed() {
				t.Fatalf("no veto, want one about %q", tc.want)
			}
			if len(got.Veto) < len(tc.want) || got.Veto[:len(tc.want)] != tc.want {
				t.Errorf("veto = %q, want it to start with %q", got.Veto, tc.want)
			}
		})
	}
}

// A route with every slot filled must NOT be vetoed. The three arms are
// structural holes, not a tax on every decision.
func TestClaimOfCompleteRouteIsNotVetoed(t *testing.T) {
	d := Decision{
		Utterance: "напомни позвонить маме в семь", Stage: 1, Intent: IntentReminder,
		Confidence: llmFullConfidence,
		Slots:      Slots{Text: "позвонить маме", Time: time.Now(), HasTime: true},
	}
	c := ClaimOf("llm", d)
	if c.Vetoed() {
		t.Errorf("complete reminder vetoed: %q", c.Veto)
	}
	if c.Band != claim.BandStructural {
		t.Errorf("band = %v, want structural", c.Band)
	}
}

func TestClaimOfCoverageAndFilledSlots(t *testing.T) {
	d := Decision{
		Utterance: "напомни позвонить маме", Stage: 0, Intent: IntentReminder,
		Confidence: 1.0, Slots: Slots{Text: "позвонить маме"},
	}
	c := ClaimOf("reminder-wakeword", d)
	// The grammar captures what follows the verb, so "напомни" itself is
	// unexplained. Under-reporting is the safe direction.
	if len(c.Consumed) != 2 || len(c.Unexplained) != 1 {
		t.Errorf("consumed %q / unexplained %q, want 2 and 1", c.Consumed, c.Unexplained)
	}
	if got := c.Coverage(); got < 0.66 || got > 0.67 {
		t.Errorf("coverage = %v, want about 2/3", got)
	}
	if c.Intent != string(IntentReminder) {
		t.Errorf("intent = %q", c.Intent)
	}
	if len(c.Filled) != 1 || c.Filled[0] != "text" {
		t.Errorf("filled = %q, want [text]", c.Filled)
	}
	if c.Claimant != "reminder-wakeword" {
		t.Errorf("claimant = %q", c.Claimant)
	}
}

// The point of V-565's "additive" constraint, asserted rather than trusted:
// building a claim reads a Decision and changes nothing about it, so the
// classifier floor and the two consumers of Confidence are untouched.
func TestClaimOfLeavesTheDecisionAlone(t *testing.T) {
	r := New(Config{
		Grammars:  []Grammar{ReminderGrammar()},
		Extractor: Extractor{},
		Threshold: 0.55,
	})
	before, err := r.Route(context.Background(), "напомни полить цветы", time.Now())
	if err != nil {
		t.Fatalf("Route: %v", err)
	}
	after := before
	_ = ClaimOf("reminder-wakeword", after)
	if !reflect.DeepEqual(after, before) {
		t.Errorf("ClaimOf mutated the decision: %+v vs %+v", after, before)
	}
	if before.Confidence != 1.0 {
		t.Errorf("stage 0 confidence = %v, want 1.0 — the float still has to work", before.Confidence)
	}
}
