package memory

// tools_test.go — bellek tool'larının davranışı.
//
// Kapsam: remember/update/recall/ask tool'larının doğru çalıştığı, `memory.ask` in
// ASENKRON döndüğü (yanıt beklemez) ve hiçbir yolda yanlış kayıt yapılmadığı doğrulanır.

import (
	"context"
	"strings"
	"testing"

	"fiko-ai/internal/tool"
)

// askStub, soruları yakalar (WebSocket yerine).
type askStub struct {
	store *QuestionStore
	got   []Question
}

func (a *askStub) Ask(userID, conversationID string, q Question) string {
	a.got = append(a.got, q)
	a.store.Put(q) // üretimde de soru depoya yazılır
	return q.ID
}

// toolCtx, gerçek kullanıcı kimliği taşıyan bir tool tanımı üretir (executor'ın yaptığı gibi).
func toolCtx(userID string) *tool.Tool {
	return &tool.Tool{Name: "memory.remember", Config: map[string]any{
		"userId": userID, "conversationId": "c1",
	}}
}

func newTools(t *testing.T) (*ToolSet, *Service, *MemoryRepo, *askStub) {
	t.Helper()
	s, repo := newSvc(t, nil, Options{})
	questions := NewQuestionStore()
	ask := &askStub{store: questions}
	return NewToolSet(s, questions, ask), s, repo, ask
}

func TestToolRemember_Saves(t *testing.T) {
	ts, _, repo, _ := newTools(t)
	out, err := ts.remember(context.Background(), toolCtx("u"),
		map[string]any{"content": "Kullanıcı Angular kullanıyor."})
	if err != nil {
		t.Fatalf("hata: %v", err)
	}
	if out["status"] != string(Created) {
		t.Errorf("created beklenirdi: %v", out["status"])
	}
	if out["id"] == "" {
		t.Error("yeni kaydın id'si dönmeli")
	}
	list, _ := repo.List(context.Background(), Filter{UserID: "u"}, 10, 0)
	if len(list) != 1 {
		t.Fatalf("bir kayıt beklenirdi: %d", len(list))
	}
}

func TestToolUpdate_ReplacesTarget(t *testing.T) {
	ts, _, repo, _ := newTools(t)
	ctx := context.Background()
	if _, err := ts.remember(ctx, toolCtx("u"),
		map[string]any{"content": "Kullanıcının kızı Begüm 2014 doğumlu."}); err != nil {
		t.Fatal(err)
	}
	list, _ := repo.List(ctx, Filter{UserID: "u"}, 10, 0)
	if len(list) != 1 {
		t.Fatalf("bir kayıt beklenirdi: %d", len(list))
	}
	out, err := ts.update(ctx, toolCtx("u"), map[string]any{
		"targetId": list[0].ID,
		"content":  "Kullanıcının kızı Begüm 2013 doğumlu.",
	})
	if err != nil {
		t.Fatalf("update hatası: %v", err)
	}
	if out["status"] != string(Updated) {
		t.Errorf("updated beklenirdi: %v", out["status"])
	}
	after, _ := repo.List(ctx, Filter{UserID: "u"}, 10, 0)
	if len(after) != 1 {
		t.Fatalf("yeni kayıt açılmamalı: %d", len(after))
	}
	if !strings.Contains(after[0].Content, "2013") || strings.Contains(after[0].Content, "2014") {
		t.Errorf("kayıt güncellenmeliydi: %q", after[0].Content)
	}
}

// ANAHTAR TEST: `memory.ask` ASENKRON'dur — soruyu sorar, kayıt YAPMAZ ve HEMEN döner.
// (Kullanıcı kararı "A": cevap beklenmez, sohbet donmaz.)
func TestToolAsk_DoesNotWaitOrSave(t *testing.T) {
	ts, _, repo, ask := newTools(t)
	ctx := context.Background()

	// Hedef kayıt önce var olsun (güncelleme sorusu).
	if _, err := ts.remember(ctx, toolCtx("u"),
		map[string]any{"content": "Kullanıcının kızı Begüm 2014 doğumlu."}); err != nil {
		t.Fatal(err)
	}
	list, _ := repo.List(ctx, Filter{UserID: "u"}, 10, 0)

	out, err := ts.askUser(ctx, toolCtx("u"), map[string]any{
		"question": "Begüm 2013 doğumlu olmalı, güncelleyeyim mi?",
		// Seçenekler kararlarıyla birlikte verilir; arayüz metinden tahmin yapmaz.
		"options": []any{
			map[string]any{"text": "Evet, güncelle", "confirmed": true},
			map[string]any{"text": "Hayır, yeni kayıt aç", "confirmed": true, "saveAsNew": true},
		},
		"content":  "Kullanıcının kızı Begüm 2013 doğumlu.",
		"targetId": list[0].ID,
	})
	if err != nil {
		t.Fatalf("ask hatası: %v", err)
	}
	if out["status"] != "asked" {
		t.Errorf("asked beklenirdi: %v", out["status"])
	}
	if out["questionId"] == "" {
		t.Error("questionId dönmeli")
	}
	// Soru HEMEN üretilmiş olmalı (bekleme yok).
	if len(ask.got) != 1 {
		t.Fatalf("soru üretilmeliydi: %d", len(ask.got))
	}
	q := ask.got[0]
	if q.Text != "Begüm 2013 doğumlu olmalı, güncelleyeyim mi?" {
		t.Errorf("soru metni modelin yazdığı gibi olmalı: %q", q.Text)
	}
	if len(q.Options) != 2 || q.Options[0].Text != "Evet, güncelle" {
		t.Errorf("seçenekler korunmalı: %v", q.Options)
	}
	// Karar sunucudan gelir; metinden tahmin edilmez. "Hayır..." metni burada ONAY
	// demektir (kullanıcı ayrı kayıt açmak istiyor).
	if !q.Options[0].Confirmed {
		t.Errorf("onay seçeneği confirmed taşımalı: %v", q.Options[0])
	}
	if !q.Options[1].SaveAsNew {
		t.Errorf("saveAsNew taşınmalı: %v", q.Options[1])
	}
	if q.Previous != "Kullanıcının kızı Begüm 2014 doğumlu." {
		t.Errorf("önceki metin arayüze taşınmalı: %q", q.Previous)
	}
	// EN ÖNEMLİSİ: soru soruldu ama KAYIT DEĞİŞMEDİ (2014 duruyor).
	after, _ := repo.List(ctx, Filter{UserID: "u"}, 10, 0)
	if len(after) != 1 || !strings.Contains(after[0].Content, "2014") {
		t.Errorf("ask kararında kayıt değişmemeliydi: %+v", after)
	}
}

// Kullanıcı onaylarsa soru uygulanır; reddederse hiçbir şey olmaz.
func TestToolAsk_AnswerAppliesOrRejects(t *testing.T) {
	ts, _, repo, ask := newTools(t)
	ctx := context.Background()
	if _, err := ts.remember(ctx, toolCtx("u"),
		map[string]any{"content": "Kullanıcının kızı Begüm 2014 doğumlu."}); err != nil {
		t.Fatal(err)
	}
	list, _ := repo.List(ctx, Filter{UserID: "u"}, 10, 0)

	// 1) Onaylı soru uygulanır.
	if _, err := ts.askUser(ctx, toolCtx("u"), map[string]any{
		"question": "güncelleyeyim mi?", "content": "Kullanıcının kızı Begüm 2013 doğumlu.",
		"targetId": list[0].ID,
	}); err != nil {
		t.Fatal(err)
	}
	store := ts.questions
	if _, err := ApplyAnswer(ctx, ts.svc, store, "u", ask.got[0].ID, Answer{Confirmed: true}); err != nil {
		t.Fatalf("onay uygulanmalıydı: %v", err)
	}
	after, _ := repo.List(ctx, Filter{UserID: "u"}, 10, 0)
	if !strings.Contains(after[0].Content, "2013") {
		t.Errorf("onaydan sonra güncellenmeliydi: %q", after[0].Content)
	}

	// 2) Reddedilen soru hiçbir şey yapmaz.
	if _, err := ts.askUser(ctx, toolCtx("u"), map[string]any{
		"question": "kaydedeyim mi?", "content": "Kullanıcı vejetaryen.",
	}); err != nil {
		t.Fatal(err)
	}
	if _, err := ApplyAnswer(ctx, ts.svc, store, "u", ask.got[1].ID, Answer{Confirmed: false}); err != nil {
		t.Fatalf("reddetme sessiz olmalı: %v", err)
	}
	final, _ := repo.List(ctx, Filter{UserID: "u"}, 10, 0)
	if len(final) != 1 {
		t.Fatalf("reddedilen bilgi kaydedilmemeliydi: %d kayıt", len(final))
	}
}

// Soru akışı kapalıyken `memory.ask` HİÇ sunulmaz: model onu göremez.
func TestToolAsk_NotOfferedWhenSinkMissing(t *testing.T) {
	s, _ := newSvc(t, nil, Options{})
	set := NewToolSet(s, NewQuestionStore(), nil) // ask = nil
	for _, d := range set.Definitions() {
		if d.Tool.Name == "memory.ask" {
			t.Fatal("soru akışı kapalıyken memory.ask sunulmamalı")
		}
	}
	if len(set.Definitions()) < 3 {
		t.Errorf("temel bellek tool'ları sunulmalı: %d", len(set.Definitions()))
	}
}

// Kullanıcı kimliği modelden GELMEZ: executor yazar. Kimlik yoksa kayıt REDDEDİLİR —
// model "başka bir kullanıcı" diyerek kayıt açamaz (çok kullanıcılı sistemde
// başkasının belleğine yazma riski).
func TestTool_RejectsMissingUserID(t *testing.T) {
	ts, _, repo, _ := newTools(t)
	ctx := context.Background()
	if _, err := ts.remember(ctx, &tool.Tool{Name: "memory.remember"},
		map[string]any{"content": "Kullanıcı Python kullanıyor."}); err == nil {
		t.Fatal("kimliksiz kayıt reddedilmeliydi")
	}
	if n, _ := repo.Count(ctx, ""); n != 0 {
		t.Fatalf("hiçbir kayıt yazılmamalıydı: %d", n)
	}
	// Kimlik executor tarafından verildiğinde kayıt çalışır.
	if _, err := ts.remember(ctx, toolCtx("u"),
		map[string]any{"content": "Kullanıcı Python kullanıyor."}); err != nil {
		t.Fatalf("kimlikli kayıt çalışmalıydı: %v", err)
	}
}

// TestToolsViaExecutor_ReachTheRightUsersMemory, bellek tool'larının GERÇEK executor
// üzerinden de çalıştığını kanıtlar.
//
// Canlı regresyonun kaynağı: unit testler handler'ı elle
// toolCtx("u") ile çağırıyordu; oysa üretimde handler'a yalnızca KAYIT DEFTERİNDEKİ
// tool gider. Executor kullanıcı kimliğini Request'ten alıp tool.Config'e yazmadığı
// için kimlik BOŞ geliyordu ve canlıda hiçbir bellek tool'ı işe yaramıyordu
// (tool.start görülüyor, sonuç gelmiyordu). Bu test o köprüyü gerçek yoldan geçer.
//
// Kapsam: executor -> handler -> bellek deposu -> soru sink'i.
func TestToolsViaExecutor_ReachTheRightUsersMemory(t *testing.T) {
	ts, _, repo, ask := newTools(t)
	reg := tool.NewRegistry(tool.NewMemoryRepo(), nil)
	for _, d := range ts.Definitions() {
		if _, _, err := reg.EnsureBuiltin(context.Background(), d.Tool, d.Handler, true); err != nil {
			t.Fatalf("EnsureBuiltin(%s): %v", d.Tool.Name, err)
		}
	}
	ex := tool.NewExecutor(reg, nil, nil, nil, tool.ExecutorOptions{})

	// 1) Kaydet: kayıt gerçekten "alice" kapsamına düşmeli.
	if _, err := ex.Execute(context.Background(), tool.Request{
		Ref: "memory.remember", UserID: "alice",
		Input: map[string]any{"content": "Kullanıcı Angular kullanıyor."},
	}); err != nil {
		t.Fatalf("memory.remember: %v", err)
	}
	list, _ := repo.List(context.Background(), Filter{UserID: "alice"}, 10, 0)
	if len(list) != 1 {
		t.Fatalf("alice kapsamında 1 kayıt beklenirdi, %d bulundu", len(list))
	}

	// 2) Başka kullanıcı göremez (kapsam sızıntısı olmamalı).
	if _, err := ex.Execute(context.Background(), tool.Request{
		Ref: "memory.recall", UserID: "bob", Input: map[string]any{"query": "Angular"},
	}); err != nil {
		t.Fatalf("memory.recall(bob): %v", err)
	}

	// 3) Sor: soru alice adına üretilmeli ve sink'e ULAŞMALI.
	res, err := ex.Execute(context.Background(), tool.Request{
		Ref: "memory.ask", UserID: "alice", ConversationID: "c9",
		Input: map[string]any{
			"question": "Doğum yılı 2013 mü?",
			"content":  "Begüm 2013 doğumlu.",
			"targetId": list[0].ID,
		},
	})
	if err != nil {
		t.Fatalf("memory.ask: %v", err)
	}
	if len(ask.got) != 1 {
		t.Fatalf("kullanıcıya 1 soru gitmeliydi, %d gitti", len(ask.got))
	}
	q := ask.got[0]
	if q.UserID != "alice" {
		t.Errorf("soru yanlış kullanıcıya gitti: %q", q.UserID)
	}
	if q.Previous != "Kullanıcı Angular kullanıyor." {
		t.Errorf("önceki metin okunamadı: %q", q.Previous)
	}
	if out, _ := res.Output["questionId"].(string); out == "" {
		t.Error("questionId dönmeli")
	}
	// 4) Soru ANINDA döner: yanıt beklenmez ve kayıt YAZILMAZ.
	if saved, _ := repo.List(context.Background(), Filter{UserID: "alice"}, 10, 0); len(saved) != 1 {
		t.Errorf("onay gelmeden kayıt değişmemeliydi, %d kayıt var", len(saved))
	}
}

// TestToolsViaExecutor_AskDeliversWithoutHandWrittenConfig, soru akışının executor
// köprüsü olmadan da çalışmadığını (yani canlıdaki kırılmanın gerçekten bu yolda
// olduğunu) sabitler.
func TestToolsViaExecutor_AskDeliversWithoutHandWrittenConfig(t *testing.T) {
	ts, _, _, ask := newTools(t)
	reg := tool.NewRegistry(tool.NewMemoryRepo(), nil)
	var askDef *ToolDef
	for _, d := range ts.Definitions() {
		if d.Tool.Name == "memory.ask" {
			dd := d
			askDef = &dd
		}
	}
	if askDef == nil {
		t.Fatal("memory.ask tanımı yok")
	}
	if _, _, err := reg.EnsureBuiltin(context.Background(), askDef.Tool, askDef.Handler, true); err != nil {
		t.Fatalf("EnsureBuiltin: %v", err)
	}
	ex := tool.NewExecutor(reg, nil, nil, nil, tool.ExecutorOptions{})

	// Config elle YAZILMAZ; kimlik yalnızca Request'ten gelir (canlı yol).
	if _, err := ex.Execute(context.Background(), tool.Request{
		Ref: "memory.ask", UserID: "carol",
		Input: map[string]any{"question": "Bu bilgi doğru mu?", "content": "Bilgi şu."},
	}); err != nil {
		t.Fatalf("memory.ask: %v", err)
	}
	if len(ask.got) != 1 || ask.got[0].UserID != "carol" {
		t.Fatalf("kimlik köprüsü çalışmadı: %+v", ask.got)
	}
}

// TestToolAsk_CarriesExplicitDecisionPerOption, her seçeneğin kararının SUNUCUDAN
// geldiğini kanıtlar — metinden tahmin edilmez.
//
// Canlı regresyon: seçenekler `[]string` taşıyordu ve arayüz kararı metinden
// çıkarıyordu ("hayır" geçiyorsa ret). Model "Hayır, farklı bir yıl" dediğinde bu
// seçenek yanlışlıkla RET sayılıyordu; oysa kullanıcı o butona basarak kaydı
// güncellemek istiyordu. Şimdi her seçenek `confirmed` ile gelir.
func TestToolAsk_CarriesExplicitDecisionPerOption(t *testing.T) {
	ts, _, _, ask := newTools(t)

	out, err := ts.askUser(context.Background(), toolCtx("u"), map[string]any{
		"question": "Doğum yılı hangisi?",
		"options": []any{
			// Ret görünen metin ama ONAYLANMASI gerekiyor: kullanıcı 2014 DEĞİL diyor.
			map[string]any{"text": "Hayır, farklı bir yıl (2014 değil)", "confirmed": true},
			map[string]any{"text": "Emin değilim, kaydetme", "confirmed": false},
		},
		"content": "Kullanıcının kızı 2013 doğumlu.",
	})
	if err != nil {
		t.Fatalf("askUser: %v", err)
	}
	if out["status"] != "asked" {
		t.Fatalf("soru gönderilmeliydi: %v", out)
	}
	opts := ask.got[0].Options
	if len(opts) != 2 {
		t.Fatalf("2 seçenek beklenirdi: %+v", opts)
	}
	if !opts[0].Confirmed {
		t.Error("'Hayır...' metinli seçenek ONAY taşımalı; metinden tahmin yanlış karar verir")
	}
	if opts[1].Confirmed {
		t.Error("'Emin değilim' seçeneği RET olmalı")
	}
}

// TestOptionsOfSkipsUndecided, kararı AÇIKÇA bildirilmemiş seçeneğin ATLANDIĞINI
// kanıtlar — metinden tahmin yapılmaz.
//
// Neden önemli? Eskiden "hayır/kaydetme" geçen metinler otomatik ret sayılıyordu. Model
// "Hayır, farklı bir yıl (2014 değil)" yazınca kullanıcı onaylamak istediği halde ret
// uygulanıyordu. Şimdi karar modelden gelir; verilmezse seçenek düşer ve
// varsayılanlara düşülür — yanlış KAYIT ya da yanlış RET'ten iyidir.
func TestOptionsOfSkipsUndecided(t *testing.T) {
	opts := optionsOf([]any{
		// Düz metin (eski biçim): karar yok → ATLANIR.
		"Evet, kaydet",
		// confirmed yok → ATLANIR.
		map[string]any{"text": "Emin değilim"},
		// Kararı açık → KEEP.
		map[string]any{"text": "Hayır, 2014 değil", "confirmed": true},
	})
	if len(opts) != 1 {
		t.Fatalf("yalnızca kararı bildirilen seçenek kalmalı: %+v", opts)
	}
	if opts[0].Text != "Hayır, 2014 değil" || !opts[0].Confirmed {
		t.Errorf("karar korunmalı: %+v", opts[0])
	}
}

// TestOptionsOfCapsCount, buton satırının taşmaması için seçenek sayısı sınırlanır.
func TestOptionsOfCapsCount(t *testing.T) {
	var in []any
	for i := 0; i < 8; i++ {
		in = append(in, map[string]any{"text": "s", "confirmed": true})
	}
	if got := len(optionsOf(in)); got > maxQuestionOptions {
		t.Errorf("%d seçenek beklenirdi, %d geldi", maxQuestionOptions, got)
	}
}
