package agent

import (
	"context"
	"fmt"
	"slices"
	"strings"
	"testing"

	"fiko-ai/internal/llm"
) // ---- Faz 5c: SKILL RETRIEVAL → TOOL RETRIEVAL ----

// fakeRetriever, sorguya göre sabit skill listesi döndürür (son çağrıyı kaydeder).
type fakeRetriever struct {
	skills []RetrievedSkill
	seen   []string
}

func (f *fakeRetriever) TopSkills(_ context.Context, query string, _ int) []RetrievedSkill {
	f.seen = append(f.seen, query)
	return f.skills
}

func TestSelectSkills_LimitsAndFilters(t *testing.T) {
	all := []RetrievedSkill{
		{Name: "a", Tool: "t.a", Score: 0.9},
		{Name: "b", Tool: "t.a", Score: 0.8}, // aynı tool → tekrar (elenmeli)
		{Name: "c", Tool: "t.c", Score: 0.7},
		{Name: "d", Score: 0.6},              // tool'suz bilgi skill'i kalabilir
		{Name: "e", Tool: "t.e", Score: 0.1}, // eşiğin altı → elenir
	}
	got := selectSkills(all)
	if len(got) > MaxSkillsPerTurn {
		t.Fatalf("üst sınır aşıldı: %d", len(got))
	}
	for _, s := range got {
		if s.Name == "e" {
			t.Error("eşik altındaki skill seçildi")
		}
	}
	tools := toolsOfSkills(got)
	if len(tools) == 0 {
		t.Fatalf("seçilen skill'lerden tool adı türetilemedi: %+v", got)
	}
	seen := map[string]int{}
	for _, n := range tools {
		seen[n]++
	}
	for n, c := range seen {
		if c > 1 {
			t.Errorf("tool %s tekrar ediyor (%d)", n, c)
		}
	}
}

func TestSelectSkills_CapsAtMax(t *testing.T) {
	var all []RetrievedSkill
	for i := 0; i < MaxSkillsPerTurn+5; i++ {
		all = append(all, RetrievedSkill{Name: string(rune('a' + i)), Tool: "t" + string(rune('a'+i)), Score: 0.9})
	}
	if got := selectSkills(all); len(got) != MaxSkillsPerTurn {
		t.Errorf("sınır uygulanmadı: %d", len(got))
	}
}

func TestFilterTools(t *testing.T) {
	specs := []llm.ToolSpec{{Name: "fiko.echo"}, {Name: "fiko.now"}, {Name: "fs.read_text"}}

	// Boş izin listesi = sınır YOK (retrieval boş döndüğünde eski davranış).
	if got := filterTools(specs, nil); len(got) != len(specs) {
		t.Errorf("boş listede tüm tool'lar sunulmalı: %d", len(got))
	}
	got := filterTools(specs, []string{"fiko.echo", "olmayan.tool"})
	if len(got) != 1 || got[0].Name != "fiko.echo" {
		t.Errorf("süzme yanlış: %+v", got)
	}
	// Hiç eşleşme yoksa liste boş kalır → ajan tool'suz tek tura düşer (döngü kurulmaz).
	if got := filterTools(specs, []string{"yok"}); len(got) != 0 {
		t.Errorf("eşleşmeyen filtre boş olmalı: %+v", got)
	}
}

func TestPlan_SkillsNarrowTools(t *testing.T) {
	// Spec sırası: önce skill, sonra yalnızca o skill'in tool'u modele sunulmalı.
	llmFake := &toolLLM{final: "cevap"}
	runner := &fakeRunner{out: map[string]any{"ok": true}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{twoSpecs()}, Config{})
	ret := &fakeRetriever{skills: []RetrievedSkill{{Name: "echo_skill", Description: "yankı yapar", Tool: "fiko.echo", Score: 0.9}}}
	a.SetSkills(ret)

	sendText(a, rec, conv, "bana yankı yap")

	if len(ret.seen) == 0 {
		t.Fatal("retrieval çağrılmadı")
	}
	if ret.seen[0] != "bana yankı yap" {
		t.Errorf("kullanıcı mesajı sorguya gitmeliydi: %q", ret.seen[0])
	}
	// Modele sunulan tek tool: fiko.echo
	given := llmFake.specs[0]
	if len(given) != 1 || given[0].Name != "fiko.echo" {
		t.Errorf("tool retrieval skill ile daralmalıydı: %+v", given)
	}
	// Sistem isteminde skill anlatılıyor mu?
	sys := ""
	for _, m := range llmFake.msgs[0] {
		if m.Role == llm.RoleSystem {
			sys = m.Content
		}
	}
	if !strings.Contains(sys, "echo_skill") || !strings.Contains(sys, "fiko.echo") {
		t.Errorf("sistem istemi seçilen skill'i içermeliydi:\n%s", sys)
	}
}

func TestPlan_NoSkillsFallsBackToAllTools(t *testing.T) {
	// Retrieval BOŞ döndüğünde davranış değişmemeli (geri düşüş).
	llmFake := &toolLLM{final: "cevap"}
	runner := &fakeRunner{out: map[string]any{"ok": true}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{twoSpecs()}, Config{})
	a.SetSkills(&fakeRetriever{skills: nil})

	sendText(a, rec, conv, "merhaba")

	if len(llmFake.specs[0]) != 2 {
		t.Errorf("retrieval boşken tüm tool'lar sunulmalı: %+v", llmFake.specs[0])
	}
}

func TestPlan_NoRetrieverKeepsAllTools(t *testing.T) {
	// Retriever hiç bağlanmadıysa (SetSkills çağrılmadı) eski davranış.
	llmFake := &toolLLM{final: "cevap"}
	runner := &fakeRunner{out: map[string]any{"ok": true}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{twoSpecs()}, Config{})

	sendText(a, rec, conv, "merhaba")

	if len(llmFake.specs[0]) != 2 {
		t.Errorf("retriever yokken tüm tool'lar sunulmalı: %+v", llmFake.specs[0])
	}
}

func twoSpecs() []llm.ToolSpec {
	return []llm.ToolSpec{
		{Name: "fiko.echo", Description: "yankı"},
		{Name: "fiko.now", Description: "saat"},
	}
}

func TestToolSpecsLimited(t *testing.T) {
	many := make([]llm.ToolSpec, MaxToolSpecs+5)
	for i := range many {
		many[i] = llm.ToolSpec{Name: string(rune('a'+i%26)) + ".t"}
	}
	if got := toolSpecs(many); len(got) != MaxToolSpecs {
		t.Errorf("tool sınırı uygulanmadı: %d", len(got))
	}
	if got := toolSpecs(many[:3]); len(got) != 3 {
		t.Errorf("kısa liste bozuldu: %d", len(got))
	}
}

// ---- Faz 5c revizyon: tool seçimi İLGİYE GÖRE (toolrank.go) ----

// gerçekKatalog, canlı sistemdeki 28 araçlık katalogun küçük bir benzeridir.
func gerçekKatalog() []llm.ToolSpec {
	names := []string{
		"biz.add", "biz.find", "biz.update", "biz.delete", "biz.summary", "biz.vat",
		"biz.project", "biz.plan",
		"web.search", "web.fetch", "web.exchange_rate",
		"fs.read_text", "fs.write_text", "fs.list", "fs.search", "fs.stat",
		"date.diff", "date.add", "date.workdays",
		"unit.convert", "unit.list",
		"text.stats", "calc.eval",
		"memory.remember", "memory.recall", "memory.update",
		"fiko.now", "fiko.echo",
	}
	out := make([]llm.ToolSpec, 0, len(names))
	for _, n := range names {
		out = append(out, llm.ToolSpec{Name: n, Description: n + " açıklaması"})
	}
	return out
}

func adlar(specs []llm.ToolSpec) []string {
	out := make([]string, 0, len(specs))
	for _, s := range specs {
		out = append(out, s.Name)
	}
	return out
}

// CANLI HATA (04.10.2026) — bu test o hatayı kilitler:
// "dolar kurunu bulamıyor, araçları bulamıyor". Sebep: liste ada göre sıralanıp [:20]
// ile kesiliyordu; alfabetik sonda kalan web.*/unit.*/text.*/memory.* HİÇ sunulmuyordu.
func TestPickTools_RateQuestionKeepsExchangeRate(t *testing.T) {
	got := adlar(pickTools(gerçekKatalog(), "dolar kuru ne", nil))
	if len(got) > MaxToolSpecs {
		t.Fatalf("tool sınırı aşıldı: %d", len(got))
	}
	if !slices.Contains(got, "web.exchange_rate") {
		t.Errorf("dolar sorusunda döviz aracı SUNULMALIYDI: %v", got)
	}
	if got[0] != "web.exchange_rate" {
		t.Errorf("en ilgili araç başta olmalıydı: %v", got)
	}
}

// İlgi puanı yüksek araçlar kesilmemeli (her mesajda farklı bir araç kritik).
func TestPickTools_RelevantToolsSurvive(t *testing.T) {
	cases := []struct {
		msg  string
		want string
	}{
		{"bu projeyi sil", "biz.delete"},
		{"kdv hesapla", "biz.vat"},
		{"3 km kaç mil", "unit.convert"},
		{"bunu hatırla", "memory.remember"},
		{"15 + 4 kaç", "calc.eval"},
		{"müşterileri listele", "biz.find"},
		{"kaç gün kaldı", "date.diff"},
		{"notlar/plan.txt oku", "fs.read_text"},
	}
	for _, tc := range cases {
		got := adlar(pickTools(gerçekKatalog(), tc.msg, nil))
		if len(got) > MaxToolSpecs {
			t.Errorf("%q: tool sınırı aşıldı: %d", tc.msg, len(got))
		}
		if !slices.Contains(got, tc.want) {
			t.Errorf("%q → %q sunulmadı. Sunulan: %v", tc.msg, tc.want, got)
		}
	}
}

// İlgisiz mesajda sabit öncelik listesi devreye girer (web.exchange_rate yine de hazır).
func TestPickTools_IdleMessageUsesPriority(t *testing.T) {
	kat := gerçekKatalog()
	got := adlar(pickTools(kat, "merhaba", nil))
	// Sınır katalogdan büyükse (MaxToolSpecs=50, katalog 28) listenin TAMAMI döner:
	// kesme yalnızca gerçekten sınır aşıldığında devreye girer.
	if len(got) != len(kat) {
		t.Errorf("ilgisiz mesajda katalogun tamamı sunulmalı: %d/%d", len(got), len(kat))
	}
	if got[0] != "biz.find" {
		t.Errorf("en yüksek öncelikli araç başta olmalı: %v", got)
	}
}

// Sınır katalogdan KÜÇÜK olduğunda kesme yine de uygulanır (kod yolu boş kalmaz).
func TestPickTools_AppliesCapWhenCatalogIsLarger(t *testing.T) {
	big := make([]llm.ToolSpec, 0, MaxToolSpecs+10)
	for i := 0; i < MaxToolSpecs+10; i++ {
		big = append(big, llm.ToolSpec{Name: fmt.Sprintf("x%02d.tool", i), Description: "genel araç"})
	}
	got := adlar(pickTools(big, "merhaba", nil))
	if len(got) != MaxToolSpecs {
		t.Errorf("büyük katalogda sınır uygulanmalı: %d", len(got))
	}
}

// YANLIŞ skill eşleşmesi doğru aracı düşürmemeli (canlı hata sınıfı).
func TestPickTools_SkillDoesNotHideStrongIntent(t *testing.T) {
	got := adlar(pickTools(gerçekKatalog(), "dolar kuru ne", []string{"fiko.echo"}))
	if !slices.Contains(got, "web.exchange_rate") {
		t.Errorf("güçlü ilgi skill daraltmasına rağmen korunmalıydı: %v", got)
	}
	if !slices.Contains(got, "fiko.echo") {
		t.Errorf("skill'in aracı da korunmalıydı: %v", got)
	}
}

// Skill'in işaret ettiği araç kullanıcıya açık değilse liste BOŞ KALMAZ.
func TestPickTools_SkillToolUnavailableFallsBack(t *testing.T) {
	got := adlar(pickTools(gerçekKatalog(), "merhaba", []string{"yok.bir.arac"}))
	if len(got) == 0 {
		t.Fatal("skill aracı yoksa liste boşalmamalı, tüm katalog ilgi sırasıyla sunulmalı")
	}
}

// Sonuç KESİN LİSTEDİR: tekrarlanan araç veya sınır ihlali olmaz.
func TestPickTools_NoDuplicatesAndBounded(t *testing.T) {
	got := adlar(pickTools(gerçekKatalog(), "faturayı sil ve kdv hesapla", []string{"biz.find"}))
	seen := map[string]int{}
	for _, n := range got {
		seen[n]++
	}
	for n, c := range seen {
		if c > 1 {
			t.Errorf("%s birden çok kez sunuldu: %d", n, c)
		}
	}
	if len(got) > MaxToolSpecs {
		t.Errorf("sınır aşıldı: %d", len(got))
	}
}
