package ctxmgr

import (
	"math/rand"
	"strings"
	"testing"

	"fiko-ai/internal/llm"
)

func sys(s string) llm.Message  { return llm.Message{Role: llm.RoleSystem, Content: s} }
func user(s string) llm.Message { return llm.Message{Role: llm.RoleUser, Content: s} }
func asst(s string) llm.Message { return llm.Message{Role: llm.RoleAssistant, Content: s} }

func contents(ms []llm.Message) []string {
	out := make([]string, len(ms))
	for i, m := range ms {
		out[i] = m.Content
	}
	return out
}

func TestEstimateTokens(t *testing.T) {
	if EstimateTokens("") != 0 {
		t.Fatal("boş metin 0 token olmalı")
	}
	ascii := strings.Repeat("a", 90)
	turk := strings.Repeat("ş", 90)
	cjk := strings.Repeat("字", 90)
	a, tr, c := EstimateTokens(ascii), EstimateTokens(turk), EstimateTokens(cjk)
	if !(a < tr && tr < c) {
		t.Fatalf("sıra ascii<türkçe<cjk olmalı: %d %d %d", a, tr, c)
	}
	if a != 30 || tr != 45 || c != 90 {
		t.Fatalf("beklenmeyen tahmin: %d %d %d", a, tr, c)
	}
	if EstimateTokens("abc") < 1 {
		t.Fatal("kısa metin en az 1 token olmalı (yukarı yuvarlama)")
	}
}

func TestFit_EverythingFits(t *testing.T) {
	m := New(Config{})
	in := []llm.Message{sys("sistem"), user("a"), asst("b"), user("c")}
	r := m.Fit(in, llm.Options{ContextSize: 4096})
	if r.Dropped != 0 || r.Clipped {
		t.Fatalf("atma/kırpma olmamalı: %+v", r)
	}
	got := contents(r.Messages)
	want := []string{"sistem", "a", "b", "c"}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("sıra bozuldu: %v", got)
		}
	}
}

func TestFit_DropsOldestKeepsNewestAndStartsWithUser(t *testing.T) {
	m := New(Config{})
	long := strings.Repeat("kelime ", 100) // ≈ 233+ token
	var in []llm.Message
	in = append(in, sys("sistem"))
	for i := 0; i < 10; i++ {
		in = append(in, user("U"+string(rune('0'+i))+long), asst("A"+string(rune('0'+i))+long))
	}
	in = append(in, user("SON"))

	r := m.Fit(in, llm.Options{ContextSize: 1024, MaxTokens: 256})
	if r.Dropped == 0 {
		t.Fatal("eski mesajlar atılmalıydı")
	}
	last := r.Messages[len(r.Messages)-1]
	if last.Content != "SON" || last.Role != llm.RoleUser {
		t.Fatalf("son mesaj korunmalı: %+v", last)
	}
	// system + (kısaltılmış) geçmiş + son
	if r.Messages[0].Role != llm.RoleSystem {
		t.Fatal("sistem istemi başta kalmalı")
	}
	if !strings.Contains(r.Messages[0].Content, "Bağlam notu") {
		t.Fatal("atılan mesaj varsa sistem istemine not eklenmeli")
	}
	if len(r.Messages) > 1 && r.Messages[1].Role != llm.RoleUser {
		t.Fatalf("geçmiş user ile başlamalı, ilk=%s", r.Messages[1].Role)
	}
	// toplam = girdi - korunanlar
	if r.Dropped+len(r.Messages)-2 != len(in)-2 { // system ve son hariç
		t.Fatalf("Dropped tutarsız: dropped=%d out=%d in=%d", r.Dropped, len(r.Messages), len(in))
	}
	// sürekli kuyruk: korunan geçmiş, girdinin son dilimidir
	kept := r.Messages[1 : len(r.Messages)-1]
	tail := in[len(in)-1-len(kept) : len(in)-1]
	for i := range kept {
		if kept[i] != tail[i] {
			t.Fatal("korunan geçmiş kesintisiz bir kuyruk olmalı")
		}
	}
}

func TestFit_DoesNotStartWithAssistant(t *testing.T) {
	m := New(Config{DefaultWindow: 512})
	// bütçe: 512 - 256 (reserve yarıya sınırlı) - 48 margin ≈ 208
	big := strings.Repeat("x", 600) // 200 token + overhead → tek başına sığmaz
	in := []llm.Message{user("eski " + big), asst("cevap"), user("SON")}
	r := m.Fit(in, llm.Options{})
	// asistan mesajı sığar ama önündeki user sığmaz → asistan da atılmalı (user ile başlama kuralı)
	if r.Dropped != 2 {
		t.Fatalf("hem büyük user hem baştaki asistan atılmalı (2), dropped=%d", r.Dropped)
	}
	for _, msg := range r.Messages {
		if msg.Role == llm.RoleAssistant {
			t.Fatalf("asistan mesajı kalmamalı: %+v", r.Messages)
		}
	}
}

func TestFit_HugeLastMessageIsClippedNotDropped(t *testing.T) {
	m := New(Config{})
	head, tail := "BAŞLANGIÇ-", "-SONUÇ"
	huge := head + strings.Repeat("ö", 20000) + tail
	r := m.Fit([]llm.Message{sys("s"), user("eski"), asst("e"), user(huge)}, llm.Options{ContextSize: 1024, MaxTokens: 256})
	if !r.Clipped {
		t.Fatal("dev mesaj kırpılmalı")
	}
	last := r.Messages[len(r.Messages)-1]
	if !strings.HasPrefix(last.Content, head[:5]) || !strings.HasSuffix(last.Content, tail[1:]) {
		t.Fatalf("baş ve son korunmalı: %q…%q", last.Content[:12], last.Content[len(last.Content)-8:])
	}
	if !strings.Contains(last.Content, "[…]") {
		t.Fatal("kırpma işareti olmalı")
	}
	if r.EstimatedTokens > r.Window {
		t.Fatalf("pencereyi aşıyor: %d > %d", r.EstimatedTokens, r.Window)
	}
	if r.Dropped != 2 {
		t.Fatalf("eski geçmiş atılmalıydı (2), dropped=%d", r.Dropped)
	}
}

func TestFit_DefaultWindowWhenContextSizeZero(t *testing.T) {
	m := New(Config{DefaultWindow: 2048})
	r := m.Fit([]llm.Message{user("x")}, llm.Options{})
	if r.Window != 2048 {
		t.Fatalf("varsayılan pencere kullanılmalı: %d", r.Window)
	}
	r2 := New(Config{}).Fit([]llm.Message{user("x")}, llm.Options{})
	if r2.Window != DefaultWindow {
		t.Fatalf("paket varsayılanı: %d", r2.Window)
	}
}

func TestFit_MaxTokensReducesBudget(t *testing.T) {
	m := New(Config{})
	_, small := m.budget(llm.Options{ContextSize: 4096, MaxTokens: 256})
	_, big := m.budget(llm.Options{ContextSize: 4096, MaxTokens: 2048})
	if !(big < small) {
		t.Fatalf("daha büyük cevap payı bütçeyi düşürmeli: small=%d big=%d", small, big)
	}
	// aşırı büyük MaxTokens bütçeyi sıfırlamaz: pencerenin yarısıyla sınırlı
	_, capped := m.budget(llm.Options{ContextSize: 4096, MaxTokens: 100000})
	if capped < 4096/4 {
		t.Fatalf("istem bütçesi çok küçük: %d", capped)
	}
}

func TestFit_TinyWindowIsRaisedToMin(t *testing.T) {
	r := New(Config{}).Fit([]llm.Message{user("x")}, llm.Options{ContextSize: 10})
	if r.Window != MinWindow {
		t.Fatalf("pencere MinWindow'a çekilmeli: %d", r.Window)
	}
}

func TestFit_NoSystemAndEmptyInput(t *testing.T) {
	m := New(Config{})
	if r := m.Fit(nil, llm.Options{}); len(r.Messages) != 0 {
		t.Fatal("boş girdi boş çıktı vermeli")
	}
	r := m.Fit([]llm.Message{user("a"), asst("b"), user("c")}, llm.Options{ContextSize: 600, MaxTokens: 100})
	if len(r.Messages) != 3 || r.Messages[0].Role != llm.RoleUser {
		t.Fatalf("sistemsiz akış bozuldu: %+v", r.Messages)
	}
}

func TestFit_NoteWithoutSystemCreatesSystemMessage(t *testing.T) {
	m := New(Config{DefaultWindow: 512})
	big := strings.Repeat("y", 450) // ≈150 token
	in := []llm.Message{user(big), asst(big), user(big), asst(big), user("SON")}
	r := m.Fit(in, llm.Options{})
	if r.Dropped == 0 {
		t.Fatal("atma beklenirdi")
	}
	if r.Messages[0].Role != llm.RoleSystem || !strings.Contains(r.Messages[0].Content, "Bağlam notu") {
		t.Fatalf("not için system mesajı oluşmalı: %+v", r.Messages[0])
	}
}

func TestFit_DoesNotMutateInput(t *testing.T) {
	m := New(Config{DefaultWindow: 512})
	big := strings.Repeat("z", 450)
	in := []llm.Message{sys("sistem"), user(big), asst(big), user(big), asst(big), user("SON")}
	snapshot := append([]llm.Message(nil), in...)
	r := m.Fit(in, llm.Options{})
	if r.Dropped == 0 {
		t.Fatal("atma beklenirdi")
	}
	for i := range in {
		if in[i] != snapshot[i] {
			t.Fatalf("girdi değişti: [%d] %q → %q (not, girdi diliminin system mesajına yazılmamalı)", i, snapshot[i].Content, in[i].Content)
		}
	}
}

// Özellik testi: hangi geçmiş gelirse gelsin sonuç pencereye sığar, son mesaj korunur,
// sistem istemi başta kalır, ilk geçmiş mesajı user'dır.
func TestFit_Property(t *testing.T) {
	rng := rand.New(rand.NewSource(42))
	m := New(Config{})
	words := []string{"merhaba", "dünya", "ağaç", "şeker", "işlem", "çalışma", "x", "kod", "go"}
	for iter := 0; iter < 300; iter++ {
		n := rng.Intn(40)
		in := []llm.Message{sys(strings.Repeat("sistem ", 1+rng.Intn(20)))}
		for i := 0; i < n; i++ {
			text := strings.Repeat(words[rng.Intn(len(words))]+" ", 1+rng.Intn(300))
			if i%2 == 0 {
				in = append(in, user(text))
			} else {
				in = append(in, asst(text))
			}
		}
		in = append(in, user(strings.Repeat("soru ", 1+rng.Intn(2000))))

		opts := llm.Options{ContextSize: 600 + rng.Intn(6000), MaxTokens: rng.Intn(800)}
		r := m.Fit(in, opts)

		if r.EstimatedTokens > r.Window {
			t.Fatalf("iter=%d pencereyi aştı: %d > %d", iter, r.EstimatedTokens, r.Window)
		}
		if r.Messages[0].Role != llm.RoleSystem {
			t.Fatalf("iter=%d sistem başta değil", iter)
		}
		if len(r.Messages) > 1 && r.Messages[1].Role == llm.RoleAssistant {
			t.Fatalf("iter=%d geçmiş asistanla başlıyor", iter)
		}
		last := r.Messages[len(r.Messages)-1]
		if last.Role != llm.RoleUser || !strings.HasPrefix(last.Content, "soru") {
			t.Fatalf("iter=%d son mesaj kayıp", iter)
		}
		// gerçek toplam tahmin ile raporlanan tutarlı olmalı (tolerans: kırpma işareti yuvarlaması)
		sum := 0
		for _, msg := range r.Messages {
			sum += EstimateTokens(msg.Content) + perMessageOverhead
		}
		if sum > r.Window {
			t.Fatalf("iter=%d yeniden hesaplanan toplam pencereyi aştı: %d > %d", iter, sum, r.Window)
		}
	}
}
