package agent

import (
	"slices"
	"strconv"
	"strings"
	"testing"

	"fiko-ai/internal/ctxmgr"
	"fiko-ai/internal/llm"
)

// canliKatalog, üretimdeki GERÇEK araç listesidir (builtin + biz + notify + ui).
// skills_test.go'ındaki gerçekKatalog() 28 araçtır ve notify.*/ui.form/memory.ask'i içermez;
// bu yüzden canlıdaki kesilme davranışı ölçülemez.
func canliKatalog() []llm.ToolSpec {
	names := []string{
		"biz.add", "biz.delete", "biz.find", "biz.plan", "biz.project", "biz.summary", "biz.update", "biz.vat",
		"calc.eval", "date.add", "date.diff", "date.workdays",
		"fiko.echo", "fiko.now",
		"fs.read_text", "fs.write_text", "fs.list", "fs.search", "fs.stat",
		"memory.ask", "memory.recall", "memory.remember", "memory.update",
		"notify.answers", "notify.ask", "notify.send", "notify.users",
		"text.stats", "ui.form",
		"unit.convert", "unit.list",
		"web.exchange_rate", "web.fetch", "web.search",
	}
	out := make([]llm.ToolSpec, 0, len(names))
	for _, n := range names {
		out = append(out, llm.ToolSpec{Name: n, Description: n + " açıklaması"})
	}
	return out
}

// TestPickTools_ButunAraclarErisilebilir her aracın, mesajda kendi anahtar
// sözcüğü geçtiğinde modele sunulabildiğini kilitler.
// CANLI HATA (05.10.2026): "araçlar filtreleniyor, yz bulamıyorum".
// 34 araçlık katalog modele en çok 14 araç sunuluyor; kalanı ilgi sırasında
// kesiliyordu. İlgi puanını üreten iki kaynaktan biri (overlapScore) yanlış
// koşulla yalnızca öncelik tablosunda OLMAYAN araçlara uygulanıyordu — yerleşik
// araçların hepsi tabloda olduğu için bu dal HİÇ çalışmıyordu. Üstüne
// memory.ask ve fiko.echo tabloda hiç yoktu, priorityOf onlara 50 döndürüyor
// ve en sonda kesiliyorlardı. Sonuç: araç kodda var, çalışıyor, izinli — ama
// model onu hiç göremiyordu.
func TestPickTools_ButunAraclarErisilebilir(t *testing.T) {
	kat := canliKatalog()
	for _, s := range kat {
		t.Run(s.Name, func(t *testing.T) {
			if !picksUp(kat, s.Name) {
				t.Errorf("%q hiçbir mesajda modele sunulamıyor", s.Name)
			}
		})
	}
}

// TestPickTools_KatalogunTumuSunulur katalogun HİÇBİR aracının kesilmediğini kilitler.
// MaxToolSpecs=50, canlı katalog 34 araç: aralar artık boşluk yoktur.
// Bu test MaxToolSpecs katalogun altına düşürülürse kırmızıya döner —
// "araç görünmez" hatasına sessizce dönüşmemesi için.
func TestPickTools_KatalogunTumuSunulur(t *testing.T) {
	kat := canliKatalog()
	if MaxToolSpecs < len(kat) {
		t.Fatalf("MaxToolSpecs=%d, canlı katalog %d araç: bazıları kesilecek", MaxToolSpecs, len(kat))
	}
	for _, msg := range []string{"merhaba", "yz", "dolar kuru ne", "bu projeyi sil", ""} {
		got := adlar(pickTools(kat, msg, nil))
		if len(got) != len(kat) {
			t.Errorf("mesaj %q: %d/%d araç sunuldu, bazıları kesildi", msg, len(got), len(kat))
		}
	}
}

// TestPickTools_EnIlgiliAracBasta sıralamanın boyuttan BAĞIMSIZ olduğunu kilitler.
// MaxToolSpecs 50'ye çıkınca "sınır aşılmadı, sıralamaya gerek yok" diyen eski
// kısa devre devreye girip listeyi alfabetik sırayla döndürüyordu: "dolar kuru ne"
// sorusunda web.exchange_rate 11. sıraya düşüyordu. Bu test o hatanın dönüşünü engeller.
func TestPickTools_EnIlgiliAracBasta(t *testing.T) {
	kat := canliKatalog()
	cases := []struct{ msg, want string }{
		{"merhaba", "biz.find"},
		{"dolar kuru ne", "web.exchange_rate"},
		{"yz yaz", "fs.write_text"},
		{"bu projeyi sil", "biz.delete"},
		{"kdv hesapla", "biz.vat"},
		{"bunu hatırla", "memory.remember"},
		{"notlar/plan.txt oku", "fs.read_text"},
	}
	for _, tc := range cases {
		got := adlar(pickTools(kat, tc.msg, nil))
		if len(got) == 0 {
			t.Errorf("%q: liste boş", tc.msg)
			continue
		}
		if got[0] != tc.want {
			t.Errorf("%q: en ilgili araç %q olmalıydı, %q geldi", tc.msg, tc.want, got[0])
		}
	}
}

// picksUp, aracın adı/açıklaması geçen bir mesajla sunulup sunulmadığını ölçer.
// Önce kendi adıyla (kullanıcı aracı adıyla anabilir), sonra intentRules
// anahtar sözcükleriyle, son olarak tanımındaki tekil sözcüklerle dener.
// TestFitTools_AracMaliyetiButcedenDusulur araç tanımlarının bütçeden düşüldüğünü
// kilitler. CANLI HATA (05.10.2026): "mesajları yarım bırakmaya başladı" —
// cevaplar "...Hangi" diye kesiliyordu. Sebep: tool tanımları isteğin "tools"
// alanında gider ve bağlamda yer tutar, ama ctxmgr yalnız mesajları sayıyordu.
// Bütçe "sığıyor" görünüyor, gerçekte num_ctx aşılıyor ve model sessizce kesiyordu.
func TestFitTools_AracMaliyetiButcedenDusulur(t *testing.T) {
	a, _, _ := setup(t, &fakeLLM{}, Config{})

	tools := canliKatalog()
	msgs := []llm.Message{
		{Role: llm.RoleSystem, Content: "Sistem istemi."},
		{Role: llm.RoleUser, Content: "merhaba"},
	}
	opts := llm.Options{ContextSize: 8192}

	if got := ctxmgr.ToolTokenCost(tools); got <= 0 {
		t.Fatalf("araç maliyeti hesaplanmalı: %d", got)
	}
	// Sığan durumda liste DOKUNULMAZ.
	kept, _ := a.fitTools(msgs, tools, opts)
	if len(kept) != len(tools) {
		t.Errorf("sığan listede araç kırpılmamalı: %d/%d", len(kept), len(tools))
	}

	// Sığmayan durumda: ya pencere büyür ya araç azalır, ama ASLA taşmaz.
	kucuk := llm.Options{ContextSize: 3000}
	kept2, opts2 := a.fitTools(msgs, tools, kucuk)
	if len(kept2) == 0 {
		t.Fatal("araç listesi boşalmamalı")
	}
	room := opts2.ContextSize - ctxmgr.TotalCost(msgs) - 1024 - opts2.ContextSize/20
	if c := ctxmgr.ToolTokenCost(kept2); c > room {
		t.Errorf("kırpıldıktan SONRA da sığmıyor: %d > %d", c, room)
	}
	// Kırpma İLGİ SIRASINDAN olur: en ilgili (ilk) araç korunur.
	if kept2[0].Name != tools[0].Name {
		t.Errorf("en ilgili araç düşürüldü: %q yerine %q", tools[0].Name, kept2[0].Name)
	}
}

// TestCtxmgr_ToolMaliyetiButceyeGirer ctxmgr'nin araçları saydığını doğrular.
func TestCtxmgr_ToolMaliyetiButceyeGirer(t *testing.T) {
	m := ctxmgr.New(ctxmgr.Config{})
	msgs := []llm.Message{{Role: llm.RoleSystem, Content: "kısa istem"}}
	tools := canliKatalog()
	opts := llm.Options{ContextSize: 8192}

	aracsiz := m.Fit(msgs, opts)
	aracli := m.FitWithTools(msgs, tools, opts)

	if aracli.ToolTokens == 0 {
		t.Fatal("araç token maliyeti ölçülmeli")
	}
	if aracli.Budget >= aracsiz.Budget {
		t.Errorf("araçlar bütçeyi küçültmeli: %d >= %d", aracli.Budget, aracsiz.Budget)
	}
	if aracli.Budget+aracli.ToolTokens != aracsiz.Budget {
		t.Errorf("bütçe = bütçe − araç maliyeti olmalı: %d + %d != %d",
			aracli.Budget, aracli.ToolTokens, aracsiz.Budget)
	}
}

// TestCtxmgr_TarihceAraclardanEtkilenmez araçların tarihçe bütçesini DÜŞÜRMEDİĞini
// kilitler.
//
// CANLI HATA (05.10.2026): "AI her mesajda sıfırlıyor". Araç maliyetini (~2.2k
// token) doğrudan istem bütçesinden düşünce tarihçe alanı 6759 → 4526'ya düştü ve
// kısa sohbetlerde geçmiş atılmaya başladı. Kullanıcı ayar vermediyse pencere
// araçların sığacağı kadar BÜYÜMELİ; tarihçe bütçesi aynı kalmalıdır.
func TestCtxmgr_TarihceAraclardanEtkilenmez(t *testing.T) {
	m := ctxmgr.New(ctxmgr.Config{})
	tools := canliKatalog()
	msgs := []llm.Message{
		{Role: llm.RoleSystem, Content: "sistem istemi"},
		{Role: llm.RoleUser, Content: "merhaba"},
		{Role: llm.RoleAssistant, Content: strings.Repeat("selam ", 200)},
		{Role: llm.RoleUser, Content: "teşekkürler"},
	}

	// Kullanıcı ayar vermedi (ContextSize=0) → tarihçe bütçesi KORUNUR.
	serbest := m.FitWithTools(msgs, tools, llm.Options{})
	tabansiz := m.Fit(msgs, llm.Options{})
	if serbest.ToolTokens == 0 {
		t.Fatal("araç maliyeti ölçülmeli")
	}
	if serbest.Budget < tabansiz.Budget {
		t.Errorf("tarihçe bütçesi araçlardan küçülemez: %d < %d", serbest.Budget, tabansiz.Budget)
	}
	if serbest.Window < serbest.ToolTokens+tabansiz.Budget {
		t.Errorf("pencere araç + tarihçe + rezervi kapsamalı: %d < %d",
			serbest.Window, serbest.ToolTokens+tabansiz.Budget)
	}

	// Kullanıcı SABİT pencere verdi → o değere saygı, araçlar bütçeden düşer.
	sabit := m.FitWithTools(msgs, tools, llm.Options{ContextSize: 8192})
	taban8192 := m.Fit(msgs, llm.Options{ContextSize: 8192})
	if sabit.Window != 8192 {
		t.Errorf("kullanıcının penceresi değişmemeli: %d", sabit.Window)
	}
	if sabit.Budget+ sabit.ToolTokens != taban8192.Budget {
		t.Errorf("sabit pencerede bütçe = bütçe − araç: %d + %d != %d",
			sabit.Budget, sabit.ToolTokens, taban8192.Budget)
	}
}

// TestCtxmgr_VarsayilanPencereAraclaraYeter tool tanımları sığacak kadar büyüktür.
// 4096 pencerede 34 araç tek başına yarıyı geçiyordu (canlı hata 05.10.2026).
func TestCtxmgr_VarsayilanPencereAraclaraYeter(t *testing.T) {
	m := ctxmgr.New(ctxmgr.Config{})
	tools := canliKatalog()
	toolCost := ctxmgr.ToolTokenCost(tools)
	if toolCost*2 > ctxmgr.DefaultWindow {
		t.Errorf("varsayılan pencere araçlara iki katı yer bırakmalı: %d*2 > %d",
			toolCost, ctxmgr.DefaultWindow)
	}
	// Gerçek bir sohbet geçmişiyle de düşen mesaj olmamalı.
	msgs := []llm.Message{{Role: llm.RoleSystem, Content: "sistem"}}
	for i := 0; i < 6; i++ {
		msgs = append(msgs,
			llm.Message{Role: llm.RoleUser, Content: "soru " + strconv.Itoa(i)},
			llm.Message{Role: llm.RoleAssistant, Content: "cevap " + strconv.Itoa(i)},
		)
	}
	if r := m.FitWithTools(msgs, tools, llm.Options{}); r.Dropped > 0 {
		t.Errorf("kısa sohbette mesaj düşmemeli: %d düştü", r.Dropped)
	}
}

// TestCtxmgr_128kPencereGecmisiAlir 128k pencerenin uzun geçmişi ALDIĞINI kilitler.
//
// Kullanıcı isteği: pencere 131072 (128k). 4096'da araç tanımları geçmişi
// sıkıştırıyor, araçları da büyüttükçe "AI her mesajda sıfırlanıyor" hatası
// vardı. 128k'ta hem 34 araç hem de 40 tur geçmiş aynı anda sığmalıdır.
func TestCtxmgr_128kPencereGecmisiAlir(t *testing.T) {
	m := ctxmgr.New(ctxmgr.Config{})
	tools := canliKatalog()

	msgs := []llm.Message{{Role: llm.RoleSystem, Content: "sistem istemi"}}
	for i := 0; i < 40; i++ {
		msgs = append(msgs,
			llm.Message{Role: llm.RoleUser, Content: "soru " + strconv.Itoa(i)},
			llm.Message{Role: llm.RoleAssistant, Content: "cevap " + strconv.Itoa(i)},
		)
	}
	r := m.FitWithTools(msgs, tools, llm.Options{})
	if r.Window != 131072 {
		t.Errorf("pencere 131072 olmalı: %d", r.Window)
	}
	if r.ToolTokens == 0 {
		t.Fatal("araç maliyeti ölçülmeli")
	}
	if r.Dropped > 0 {
		t.Errorf("128k pencerede uzun geçmiş sığmalı: %d mesaj düştü", r.Dropped)
	}
	if r.Clipped {
		t.Error("son mesaj kırpılmamalı")
	}
}

func picksUp(kat []llm.ToolSpec, name string) bool {
	if slices.Contains(adlar(pickTools(kat, name, nil)), name) {
		return true
	}
	msgs := append(intentMsgsFor(name), adimSözcükleri(name)...)
	for _, m := range msgs {
		if slices.Contains(adlar(pickTools(kat, m, nil)), name) {
			return true
		}
	}
	return false
}

func intentMsgsFor(tool string) []string {
	var out []string
	for _, r := range intentRules {
		if r.tool != tool {
			continue
		}
		for _, st := range r.stems {
			out = append(out, strings.TrimPrefix(st, "="))
		}
	}
	return out
}

// adimSözcükleri, "fs.read_text" → {"fs", "read", "text"}: kullanıcı aracın
// tek bir gövdesini söyleyebilir ("stok", "kuru") ve o da yeterli olmalıdır.
func adimSözcükleri(name string) []string {
	parts := strings.Split(name, ".")
	if len(parts) < 2 {
		return nil
	}
	return parts[1:]
}
