package main

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/kami/maven/internal/decision"
	"github.com/kami/maven/internal/dialogue"
	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/router"
	"github.com/kami/maven/internal/store"
	"github.com/kami/maven/internal/tts"
)

func TestParseReminderCancelRequest(t *testing.T) {
	for _, tc := range []struct {
		text string
		ok   bool
	}{
		{"отмени напоминание про врача", true},
		{"убери моё напоминание о визите", true},
		{"удали будильник на девять", true},
		{"пожалуйста, Maven, cancel the reminder about doctor", true},
		{"отмени напоминание, которое стоит на завтра", true},
		{"напоминание про врача", false},
		{"отмени задачу про врача", false},
		{"я отменил напоминание про врача", false},
		{"как отменить напоминание про врача?", false},
		{"можно отменить напоминание про врача?", false},
		{"он сказал: отмени напоминание про врача", false},
		{"how to cancel the reminder about doctor?", false},
		{"can you cancel the reminder about doctor?", false},
		{"убери со стола", false},
		{"отмена", false},
	} {
		_, ok := parseReminderCancelRequest(tc.text)
		if ok != tc.ok {
			t.Errorf("parseReminderCancelRequest(%q) ok = %v, want %v", tc.text, ok, tc.ok)
		}
	}
}

func TestReminderCancellationTermsPreserveIdentity(t *testing.T) {
	for _, tc := range []struct {
		text    string
		hasTime bool
		want    []string
	}{
		{"отмени напоминание про врача", false, []string{"врача"}},
		{"отмени напоминание не звонить врачу", false, []string{"не", "звонить", "врачу"}},
		{"отмени напоминание принять две таблетки", false, []string{"принять", "две", "таблетки"}},
		{"отмени напоминание принять две таблетки на девять", true, []string{"принять", "две", "таблетки"}},
		{"cancel the reminder to take 2 pills at 21:30", true, []string{"take", "2", "pills"}},
	} {
		got := reminderCancellationTerms(tc.text, tc.hasTime)
		if strings.Join(got, "|") != strings.Join(tc.want, "|") {
			t.Errorf("reminderCancellationTerms(%q) = %v, want %v", tc.text, got, tc.want)
		}
	}
}

func seedVoiceReminder(t *testing.T, st *store.Store, fire time.Time, text string) int64 {
	t.Helper()
	id, err := st.CreateReminder(context.Background(), fire, `{"text":"`+text+`"}`, "")
	if err != nil {
		t.Fatalf("create reminder: %v", err)
	}
	return id
}

func reminderStatuses(t *testing.T, st *store.Store) map[int64]string {
	t.Helper()
	rows, err := st.ListReminders(context.Background(), 100)
	if err != nil {
		t.Fatalf("list reminders: %v", err)
	}
	out := make(map[int64]string, len(rows))
	for _, row := range rows {
		out[row.ID] = row.Status
	}
	return out
}

func TestReminderCancellationResolvesSubjectByMorphology(t *testing.T) {
	h, st, now := newClarifyHandler(t)
	h.timeParser = router.StubDateTimeParser{}
	doctor := seedVoiceReminder(t, st, now.Add(3*time.Hour), "позвонить врачу")
	bread := seedVoiceReminder(t, st, now.Add(4*time.Hour), "купить хлеб")

	reply, handled := h.resolveReminderCancellation(context.Background(), "отмени напоминание про врача")
	if !handled || !strings.Contains(reply, "отменила") || !strings.Contains(reply, "позвонить врачу") {
		t.Fatalf("reply = %q, handled=%v", reply, handled)
	}
	statuses := reminderStatuses(t, st)
	if statuses[doctor] != store.ReminderCancelled || statuses[bread] != store.ReminderPending {
		t.Fatalf("statuses = %+v, want doctor cancelled and bread pending", statuses)
	}
}

func TestReminderCancellationKeepsNegationAndQuantityDistinct(t *testing.T) {
	t.Run("negation", func(t *testing.T) {
		h, st, now := newClarifyHandler(t)
		positive := seedVoiceReminder(t, st, now.Add(time.Hour), "звонить врачу")
		negative := seedVoiceReminder(t, st, now.Add(2*time.Hour), "не звонить врачу")

		reply, handled := h.resolveReminderCancellation(context.Background(), "отмени напоминание не звонить врачу")
		if !handled || !strings.Contains(reply, "не звонить врачу") {
			t.Fatalf("reply = %q, handled=%v", reply, handled)
		}
		statuses := reminderStatuses(t, st)
		if statuses[positive] != store.ReminderPending || statuses[negative] != store.ReminderCancelled {
			t.Fatalf("negation selected the wrong row: %+v", statuses)
		}
	})

	t.Run("quantity", func(t *testing.T) {
		h, st, now := newClarifyHandler(t)
		one := seedVoiceReminder(t, st, now.Add(time.Hour), "принять одну таблетку")
		two := seedVoiceReminder(t, st, now.Add(2*time.Hour), "принять две таблетки")

		reply, handled := h.resolveReminderCancellation(context.Background(), "удали напоминание принять две таблетки")
		if !handled || !strings.Contains(reply, "две таблетки") {
			t.Fatalf("reply = %q, handled=%v", reply, handled)
		}
		statuses := reminderStatuses(t, st)
		if statuses[one] != store.ReminderPending || statuses[two] != store.ReminderCancelled {
			t.Fatalf("quantity selected the wrong row: %+v", statuses)
		}
	})
}

func TestReminderCancellationQuestionNeverMutates(t *testing.T) {
	h, st, now := newClarifyHandler(t)
	id := seedVoiceReminder(t, st, now.Add(time.Hour), "позвонить врачу")
	for _, text := range []string{
		"как отменить напоминание про врача?",
		"можно отменить напоминание про врача?",
		"он сказал: отмени напоминание про врача",
	} {
		if reply, handled := h.resolveReminderCancellation(context.Background(), text); handled || reply != "" {
			t.Fatalf("non-command %q was claimed: reply=%q handled=%v", text, reply, handled)
		}
		if got := reminderStatuses(t, st)[id]; got != store.ReminderPending {
			t.Fatalf("non-command %q changed reminder to %q", text, got)
		}
	}
}

func TestReminderCancellationUsesClockAndAsksWhenStateIsAmbiguous(t *testing.T) {
	t.Run("one matching half of day is enough", func(t *testing.T) {
		h, st, now := newClarifyHandler(t)
		h.timeParser = router.StubDateTimeParser{}
		evening := seedVoiceReminder(t, st, time.Date(now.Year(), now.Month(), now.Day(), 21, 0, 0, 0, now.Location()), "вечернее лекарство")
		seedVoiceReminder(t, st, now.Add(2*time.Hour), "купить хлеб")

		reply, handled := h.resolveReminderCancellation(context.Background(), "убери напоминание на девять")
		if !handled || !strings.Contains(reply, "отменила") {
			t.Fatalf("reply = %q, handled=%v", reply, handled)
		}
		if got := reminderStatuses(t, st)[evening]; got != store.ReminderCancelled {
			t.Fatalf("21:00 status = %q, want cancelled", got)
		}
	})

	t.Run("two matching halves are offered and ordinal is bound", func(t *testing.T) {
		h, st, now := newClarifyHandler(t)
		h.timeParser = router.StubDateTimeParser{}
		evening := seedVoiceReminder(t, st, time.Date(now.Year(), now.Month(), now.Day(), 21, 0, 0, 0, now.Location()), "вечернее лекарство")
		morning := seedVoiceReminder(t, st, time.Date(now.Year(), now.Month(), now.Day()+1, 9, 0, 0, 0, now.Location()), "утреннее лекарство")

		reply, handled := h.resolveReminderCancellation(context.Background(), "убери напоминание на девять")
		if !handled || !strings.Contains(reply, "порядковым словом") {
			t.Fatalf("ambiguous reply = %q, handled=%v", reply, handled)
		}
		statuses := reminderStatuses(t, st)
		if statuses[evening] != store.ReminderPending || statuses[morning] != store.ReminderPending {
			t.Fatalf("ambiguous command mutated rows: %+v", statuses)
		}
		sess := h.dialogueSessions.Get(dialogueIDOf(context.Background()), h.now())
		if sess == nil || len(sess.Candidates) != 2 || sess.Candidates[1].Ref != morning {
			t.Fatalf("bound candidates = %+v", sess)
		}

		reply, handled = h.resolveCandidate(context.Background(), "второе", sourceVoice)
		if !handled || !strings.Contains(reply, "утреннее лекарство") {
			t.Fatalf("ordinal reply = %q, handled=%v", reply, handled)
		}
		statuses = reminderStatuses(t, st)
		if statuses[evening] != store.ReminderPending || statuses[morning] != store.ReminderCancelled {
			t.Fatalf("ordinal cancelled the wrong row: %+v", statuses)
		}
		if sess := h.dialogueSessions.Get(dialogueIDOf(context.Background()), h.now()); sess == nil || len(sess.Candidates) != 0 {
			t.Fatalf("spent candidates survived: %+v", sess)
		}
	})
}

func TestReminderCancellationChoiceRequiresAWholeAffirmativeOrdinal(t *testing.T) {
	unsafe := []string{
		"почему второе?",
		"не второе",
		"второе не отменяй",
		"первое и второе",
		"напомни мне первого сентября оплатить счёт",
	}
	for _, answer := range unsafe {
		t.Run(answer, func(t *testing.T) {
			h, st, now := newClarifyHandler(t)
			h.timeParser = router.StubDateTimeParser{}
			first := seedVoiceReminder(t, st, now.Add(time.Hour), "первое лекарство")
			second := seedVoiceReminder(t, st, now.Add(2*time.Hour), "второе лекарство")
			if _, handled := h.resolveReminderCancellation(context.Background(), "отмени напоминание"); !handled {
				t.Fatal("ambiguous cancellation was not offered")
			}
			if reply, handled := h.resolveCandidate(context.Background(), answer, sourceVoice); handled || reply != "" {
				t.Fatalf("unsafe answer was claimed: reply=%q handled=%v", reply, handled)
			}
			statuses := reminderStatuses(t, st)
			if statuses[first] != store.ReminderPending || statuses[second] != store.ReminderPending {
				t.Fatalf("unsafe answer mutated rows: %+v", statuses)
			}
		})
	}
}

func TestReminderCancellationChoiceCanBeAbandoned(t *testing.T) {
	for _, answer := range []string{"отмена", "не надо", "no"} {
		t.Run(answer, func(t *testing.T) {
			h, st, now := newClarifyHandler(t)
			first := seedVoiceReminder(t, st, now.Add(time.Hour), "первое")
			second := seedVoiceReminder(t, st, now.Add(2*time.Hour), "второе")
			if _, handled := h.resolveReminderCancellation(context.Background(), "отмени напоминание"); !handled {
				t.Fatal("ambiguous cancellation was not offered")
			}
			reply, handled := h.resolveCandidate(context.Background(), answer, sourceVoice)
			if !handled || !strings.Contains(reply, "ничего не отменяю") {
				t.Fatalf("cancel answer = %q handled=%v", reply, handled)
			}
			statuses := reminderStatuses(t, st)
			if statuses[first] != store.ReminderPending || statuses[second] != store.ReminderPending {
				t.Fatalf("abandoning the choice mutated rows: %+v", statuses)
			}
			if sess := h.dialogueSessions.Get(dialogueIDOf(context.Background()), h.now()); sess == nil || len(sess.Candidates) != 0 {
				t.Fatalf("abandoned candidates survived: %+v", sess)
			}
		})
	}
}

func TestReminderCancellationOfferStartsFreshAndNamesTruncation(t *testing.T) {
	h, st, now := newClarifyHandler(t)
	id := dialogueIDOf(context.Background())
	h.dialogueSessions.Put(id, &dialogue.Session{
		Intent:    dialogue.IntentReminder,
		Slots:     dialogue.Slots{Text: "stale subject", HasTime: true, Time: now.Add(time.Hour)},
		Timestamp: now.Add(-time.Minute),
	})
	for i := 0; i < 6; i++ {
		seedVoiceReminder(t, st, now.Add(time.Duration(i+1)*time.Hour), fmt.Sprintf("row %d", i+1))
	}
	reply, handled := h.resolveReminderCancellation(context.Background(), "отмени напоминание")
	if !handled || !strings.Contains(reply, "первые пять") || !strings.Contains(reply, "уточни текст или время") {
		t.Fatalf("truncated offer = %q handled=%v", reply, handled)
	}
	sess := h.dialogueSessions.Get(id, h.now())
	if sess == nil || sess.Intent != dialogue.IntentSystem || sess.Slots.Text != "" ||
		len(sess.Candidates) != 5 || sess.Utterance != "отмени напоминание" {
		t.Fatalf("offer reused stale dialogue state: %+v", sess)
	}
}

func TestReminderCancellationNeverGuesses(t *testing.T) {
	t.Run("bare command over several rows", func(t *testing.T) {
		h, st, now := newClarifyHandler(t)
		first := seedVoiceReminder(t, st, now.Add(time.Hour), "первое")
		second := seedVoiceReminder(t, st, now.Add(2*time.Hour), "второе")
		reply, handled := h.resolveReminderCancellation(context.Background(), "отмени напоминание")
		if !handled || !strings.Contains(reply, "порядковым словом") {
			t.Fatalf("reply = %q, handled=%v", reply, handled)
		}
		statuses := reminderStatuses(t, st)
		if statuses[first] != store.ReminderPending || statuses[second] != store.ReminderPending {
			t.Fatalf("bare ambiguous command mutated rows: %+v", statuses)
		}
	})

	t.Run("unread time", func(t *testing.T) {
		h, st, now := newClarifyHandler(t)
		h.timeParser = router.StubDateTimeParser{}
		id := seedVoiceReminder(t, st, now.Add(time.Hour), "позвонить врачу")
		reply, handled := h.resolveReminderCancellation(context.Background(), "отмени напоминание через вечность")
		if !handled || !strings.Contains(reply, "не смогла разобрать время") {
			t.Fatalf("reply = %q, handled=%v", reply, handled)
		}
		if got := reminderStatuses(t, st)[id]; got != store.ReminderPending {
			t.Fatalf("unread time cancelled reminder: %q", got)
		}
	})
}

type cancelReminderAPI struct {
	ipc.UnimplementedCoreAPI

	rows      []ipc.Reminder
	listErr   error
	cancelErr error
	calls     []int64
}

func (a *cancelReminderAPI) ListPendingReminders(context.Context, int) ([]ipc.Reminder, error) {
	return a.rows, a.listErr
}

func (a *cancelReminderAPI) CancelReminder(_ context.Context, id int64) error {
	a.calls = append(a.calls, id)
	return a.cancelErr
}

func cancelHandler(api ipc.CoreAPI) *reactiveHandler {
	now := time.Date(2026, 8, 15, 9, 0, 0, 0, time.UTC)
	parser := router.StubDateTimeParser{}
	return &reactiveHandler{
		api: api, now: func() time.Time { return now }, timeParser: parser,
		extractor:        router.Extractor{Time: parser},
		dialogueSessions: dialogue.NewSessionStore(2 * time.Minute),
	}
}

func TestReminderCancellationReportsStoreOutcomes(t *testing.T) {
	row := ipc.Reminder{
		ID: 7, FireTs: time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC),
		NextFireTs: time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC),
		Payload:    `{"text":"позвонить врачу"}`, Status: store.ReminderPending,
	}
	for _, tc := range []struct {
		name string
		err  error
		want string
	}{
		{"already terminal", ipc.ErrReminderState, "уже не ожидает"},
		{"delivery in flight", ipc.ErrReminderInFlight, "уже начала отправлять"},
		{"transport", errors.New("socket closed"), "не получилось отменить"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			api := &cancelReminderAPI{rows: []ipc.Reminder{row}, cancelErr: tc.err}
			reply, handled := cancelHandler(api).resolveReminderCancellation(context.Background(), "отмени напоминание про врача")
			if !handled || !strings.Contains(reply, tc.want) || len(api.calls) != 1 || api.calls[0] != 7 {
				t.Fatalf("reply=%q handled=%v calls=%v", reply, handled, api.calls)
			}
		})
	}

	t.Run("list failure", func(t *testing.T) {
		api := &cancelReminderAPI{listErr: errors.New("offline")}
		reply, handled := cancelHandler(api).resolveReminderCancellation(context.Background(), "отмени напоминание")
		if !handled || !strings.Contains(reply, "не получилось посмотреть") || len(api.calls) != 0 {
			t.Fatalf("reply=%q handled=%v calls=%v", reply, handled, api.calls)
		}
	})

	t.Run("nothing pending", func(t *testing.T) {
		api := &cancelReminderAPI{}
		reply, handled := cancelHandler(api).resolveReminderCancellation(context.Background(), "отмени напоминание")
		if !handled || !strings.Contains(reply, "ожидающих напоминаний нет") || len(api.calls) != 0 {
			t.Fatalf("reply=%q handled=%v calls=%v", reply, handled, api.calls)
		}
	})
}

func TestReminderCancellationIsAPreRouteTurnAndDoesNotGetSwallowedByClarify(t *testing.T) {
	h, st, now := newClarifyHandler(t)
	h.timeParser = router.StubDateTimeParser{}
	h.decisions = decision.NewRing()
	id := seedVoiceReminder(t, st, now.Add(time.Hour), "позвонить врачу")
	ctx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web"))
	h.clarifyStore.Put(dialogueIDOf(ctx), &dialogue.PendingQuestion{
		Intent: dialogue.IntentReminder, Missing: []dialogue.Slot{dialogue.SlotTime},
		Utterance: "напомни позвонить маме", Asked: h.now(), TTL: clarifyTTL,
	})

	reply := h.runTurn(ctx, "отмени напоминание про врача", sourceText)
	if !strings.Contains(reply, clarifyDropped) || !strings.Contains(reply, "отменила напоминание") {
		t.Fatalf("reply = %q, want dropped clarify notice and cancellation", reply)
	}
	if h.clarifyStore.Get(dialogueIDOf(ctx), h.now()) != nil {
		t.Fatal("the superseded clarify question survived the cancellation request")
	}
	if got := reminderStatuses(t, st)[id]; got != store.ReminderCancelled {
		t.Fatalf("status = %q, want cancelled", got)
	}
	recs := h.decisions.Recent(1)
	if len(recs) != 1 {
		t.Fatalf("decision records = %d, want 1", len(recs))
	}
	claim := findClaim(recs[0], "reminder-cancel")
	if claim == nil || claim.Outcome != decision.Won {
		t.Fatalf("reminder-cancel claim = %+v, want pre-route winner", claim)
	}
}

func TestReminderCancellationThroughPushToTalk(t *testing.T) {
	h, st, now := newClarifyHandler(t)
	h.stt = simTranscriber{text: "отмени напоминание про врача"}
	h.tts = tts.NewStub()
	h.timeParser = router.StubDateTimeParser{}
	h.router = buildRouter(router.NewHashEmbedder(64), h.matcher, 0.55, nil, nil)
	doctor := seedVoiceReminder(t, st, now.Add(time.Hour), "позвонить врачу")
	bread := seedVoiceReminder(t, st, now.Add(2*time.Hour), "купить хлеб")

	resp, err := h.HandlePushToTalk(context.Background(), voicePTT(), 0)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(resp.ReplyText, "отменила напоминание") || len(resp.ReplyAudio.Bytes) == 0 {
		t.Fatalf("PTT response = text %q audio=%d bytes", resp.ReplyText, len(resp.ReplyAudio.Bytes))
	}
	statuses := reminderStatuses(t, st)
	if statuses[doctor] != store.ReminderCancelled || statuses[bread] != store.ReminderPending {
		t.Fatalf("PTT cancellation changed the wrong rows: %+v", statuses)
	}
}
