package skill

import (
	"context"
	"errors"
	"hash/fnv"
	"strings"
	"sync"
	"testing"
	"time"
)

// bowEmbedder deterministik bir "bag-of-words" sahte embedder'dır: her sözcük sabit bir kovaya düşer,
// böylece ortak sözcük çok olan metinler daha benzer çıkar (gerçek Ollama gerekmez).
type bowEmbedder struct {
	mu    sync.Mutex
	calls int
	err   error
	hook  func() // her çağrıda (hata kontrolünden önce) çalışır; eşzamanlı düzenleme simülasyonu için
}

const bowDim = 512

func (e *bowEmbedder) Embed(_ context.Context, text string) ([]float32, error) {
	e.mu.Lock()
	e.calls++
	err, hook := e.err, e.hook
	e.mu.Unlock()
	if hook != nil {
		hook()
	}
	if err != nil {
		return nil, err
	}
	v := make([]float32, bowDim)
	for _, tok := range tokenize(text) {
		h := fnv.New32a()
		_, _ = h.Write([]byte(tok))
		v[h.Sum32()%bowDim]++
	}
	return v, nil
}

func (e *bowEmbedder) setErr(err error) {
	e.mu.Lock()
	e.err = err
	e.mu.Unlock()
}

func (e *bowEmbedder) callCount() int {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.calls
}

func searchInput(name, desc string, status Status, examples ...string) Skill {
	s := Skill{Name: name, Description: desc, Status: status}
	if status == StatusActive {
		s.ExecutorType = ExecutorTool
		s.ExecutorConfig = map[string]any{"tool": "demo." + name}
	}
	for _, ex := range examples {
		s.Examples = append(s.Examples, Example{Input: ex})
	}
	return s
}

// seedSearch dört skill kaydeder: üçü aktif, biri taslak (taslak asla sonuçta görünmemeli).
func seedSearch(t *testing.T, svc *Service) {
	t.Helper()
	ctx := context.Background()
	for _, in := range []Skill{
		searchInput("create_plane", "Seçilen eksen sistemi üzerinden offset plane oluşturur.", StatusActive, "30 mm geriye plane koy"),
		searchInput("delete_file", "Diskteki bir dosyayı siler.", StatusActive, "dosyayı sil"),
		searchInput("send_email", "Belirtilen alıcıya e-posta gönderir.", StatusActive, "Ahmet'e mail at"),
		searchInput("draft_plane", "Taslak plane skill'i; henüz hazır değil.", StatusDraft, "plane oluştur"),
	} {
		if _, err := svc.Register(ctx, in); err != nil {
			t.Fatalf("seed %s: %v", in.Name, err)
		}
	}
}

func names(rs []SearchResult) []string {
	out := make([]string, len(rs))
	for i, r := range rs {
		out[i] = r.Skill.Name
	}
	return out
}

func TestEmbedding_RegisterStoresVector(t *testing.T) {
	svc, repo, _ := newTestService(t)
	emb := &bowEmbedder{}
	svc.SetEmbedder(emb)

	sk, err := svc.Register(context.Background(), searchInput("create_plane", "Plane oluşturur.", StatusActive))
	if err != nil {
		t.Fatal(err)
	}
	if len(sk.Embedding) != bowDim {
		t.Fatalf("yanıttaki embedding boyutu = %d, beklenen %d", len(sk.Embedding), bowDim)
	}
	stored, _ := repo.Get(context.Background(), sk.ID)
	if len(stored.Embedding) != bowDim {
		t.Fatalf("depodaki embedding boyutu = %d", len(stored.Embedding))
	}
}

func TestEmbedding_FailureDoesNotFailRegister(t *testing.T) {
	svc, _, _ := newTestService(t)
	svc.SetEmbedder(&bowEmbedder{err: errors.New("ollama kapalı")})

	sk, err := svc.Register(context.Background(), searchInput("create_plane", "Plane oluşturur.", StatusActive))
	if err != nil {
		t.Fatalf("embedding hatası kaydı düşürmemeli: %v", err)
	}
	if len(sk.Embedding) != 0 {
		t.Fatal("hata durumunda embedding boş kalmalı")
	}
}

func TestEmbedding_UpdateReembedsOnlyWhenTextChanges(t *testing.T) {
	ctx := context.Background()
	svc, _, _ := newTestService(t)
	emb := &bowEmbedder{}
	svc.SetEmbedder(emb)

	sk, err := svc.Register(ctx, searchInput("create_plane", "Plane oluşturur.", StatusActive))
	if err != nil {
		t.Fatal(err)
	}
	if emb.callCount() != 1 {
		t.Fatalf("kayıtta 1 çağrı beklenirdi: %d", emb.callCount())
	}

	// Embedding'e girmeyen alan değişti (sürüm): yeniden üretilmemeli, vektör korunmalı.
	same := sk.Clone()
	same.Version = "2.0.0"
	got, err := svc.Update(ctx, sk.ID, same)
	if err != nil {
		t.Fatal(err)
	}
	if emb.callCount() != 1 || len(got.Embedding) != bowDim {
		t.Fatalf("metin değişmedi: çağrı=%d embedding=%d", emb.callCount(), len(got.Embedding))
	}

	// Açıklama değişti: yeniden üretilmeli ve vektör farklı olmalı.
	changed := got.Clone()
	changed.Description = "Tamamen farklı bir açıklama: dosya siler."
	got2, err := svc.Update(ctx, sk.ID, changed)
	if err != nil {
		t.Fatal(err)
	}
	if emb.callCount() != 2 {
		t.Fatalf("metin değişti: 2. çağrı beklenirdi: %d", emb.callCount())
	}
	if cosine(got.Embedding, got2.Embedding) > 0.99 {
		t.Fatal("yeni vektör eskisinden farklı olmalı")
	}

	// Metin değişti ama embedding başarısız: ESKİ vektör tutulmamalı (yanlış temsil eder).
	emb.setErr(errors.New("ollama kapalı"))
	changed2 := got2.Clone()
	changed2.Description = "Bir kez daha değişti."
	got3, err := svc.Update(ctx, sk.ID, changed2)
	if err != nil {
		t.Fatalf("embedding hatası güncellemeyi düşürmemeli: %v", err)
	}
	if len(got3.Embedding) != 0 {
		t.Fatal("başarısız yeniden üretimde eski vektör korunmamalı")
	}
}

func TestSearch_SemanticRanksRelevantFirstAndSkipsNonActive(t *testing.T) {
	svc, _, _ := newTestService(t)
	svc.SetEmbedder(&bowEmbedder{})
	seedSearch(t, svc)

	res, err := svc.Search(context.Background(), SearchOptions{Query: "30 mm geriye plane oluştur", Mode: ModeSemantic})
	if err != nil {
		t.Fatal(err)
	}
	if len(res) == 0 || res[0].Skill.Name != "create_plane" {
		t.Fatalf("ilk sonuç create_plane olmalı: %v", names(res))
	}
	for _, r := range res {
		if r.Skill.Name == "draft_plane" {
			t.Fatal("taslak skill aramada görünmemeli")
		}
		if r.Score <= 0 || r.Score > 1 {
			t.Fatalf("skor 0..1 aralığında olmalı: %v", r.Score)
		}
	}
	for i := 1; i < len(res); i++ {
		if res[i].Score > res[i-1].Score {
			t.Fatalf("skora göre azalan olmalı: %v", names(res))
		}
	}
}

func TestSearch_LimitAndMinScore(t *testing.T) {
	svc, _, _ := newTestService(t)
	svc.SetEmbedder(&bowEmbedder{})
	seedSearch(t, svc)
	ctx := context.Background()

	res, err := svc.Search(ctx, SearchOptions{Query: "plane dosya e-posta", Mode: ModeKeyword, Limit: 1})
	if err != nil {
		t.Fatal(err)
	}
	if len(res) != 1 {
		t.Fatalf("limit=1: %d sonuç", len(res))
	}

	res, err = svc.Search(ctx, SearchOptions{Query: "plane", Mode: ModeSemantic, MinScore: 0.99})
	if err != nil {
		t.Fatal(err)
	}
	if len(res) != 0 {
		t.Fatalf("minScore=0.99 hiçbir şeyi geçirmemeli: %v", names(res))
	}
}

func TestSearch_KeywordWorksWithoutEmbedder(t *testing.T) {
	svc, _, _ := newTestService(t) // embedder yok
	seedSearch(t, svc)

	res, err := svc.Search(context.Background(), SearchOptions{Query: "dosyayı sil", Mode: ModeKeyword})
	if err != nil {
		t.Fatal(err)
	}
	if len(res) != 1 || res[0].Skill.Name != "delete_file" || res[0].Score != 1 {
		t.Fatalf("yalnızca delete_file (skor 1) beklenirdi: %v", names(res))
	}

	// Varsayılan mod hybrid'dir ve embedder yokken anahtar kelimeye düşer (hata değil).
	res, err = svc.Search(context.Background(), SearchOptions{Query: "dosyayı sil"})
	if err != nil || len(res) != 1 {
		t.Fatalf("hybrid embedder'sız anahtar kelimeye düşmeli: %v %v", names(res), err)
	}
}

func TestSearch_SemanticWithoutEmbedderIsError(t *testing.T) {
	svc, _, _ := newTestService(t)
	seedSearch(t, svc)
	_, err := svc.Search(context.Background(), SearchOptions{Query: "plane", Mode: ModeSemantic})
	if !errors.Is(err, ErrNoEmbedder) {
		t.Fatalf("ErrNoEmbedder beklenirdi: %v", err)
	}
}

func TestSearch_EmbedFailure_HybridDegradesSemanticFails(t *testing.T) {
	svc, _, _ := newTestService(t)
	emb := &bowEmbedder{}
	svc.SetEmbedder(emb)
	seedSearch(t, svc)

	boom := errors.New("ollama kapalı")
	emb.setErr(boom) // kayıttan SONRA çöktü: skill'lerin vektörü var ama sorgu vektörü üretilemiyor

	res, err := svc.Search(context.Background(), SearchOptions{Query: "dosyayı sil", Mode: ModeHybrid})
	if err != nil || len(res) != 1 || res[0].Skill.Name != "delete_file" {
		t.Fatalf("hybrid anahtar kelimeye düşmeli: %v %v", names(res), err)
	}
	if _, err := svc.Search(context.Background(), SearchOptions{Query: "dosyayı sil", Mode: ModeSemantic}); !errors.Is(err, boom) {
		t.Fatalf("semantic hatayı yüzeye çıkarmalı (sessizce düşmemeli): %v", err)
	}
}

func TestSearch_DimensionMismatchFallsBackToKeyword(t *testing.T) {
	svc, repo, _ := newTestService(t)
	svc.SetEmbedder(&bowEmbedder{})
	seedSearch(t, svc)

	// Embedding modeli değişmiş gibi: bir skill'in vektörü farklı boyutta.
	sk, _ := repo.GetByName(context.Background(), "delete_file")
	sk.Embedding = []float32{1, 2, 3}
	if err := repo.Update(context.Background(), sk); err != nil {
		t.Fatal(err)
	}

	res, err := svc.Search(context.Background(), SearchOptions{Query: "dosyayı sil", Mode: ModeHybrid})
	if err != nil {
		t.Fatal(err)
	}
	found := false
	for _, r := range res {
		found = found || r.Skill.Name == "delete_file"
	}
	if !found {
		t.Fatalf("boyutu uyuşmayan skill hibritte anahtar kelimeyle bulunmalı: %v", names(res))
	}
	// Semantik modda ise aranamaz (karşılaştırılamaz vektör), ama panik de olmamalı.
	res, err = svc.Search(context.Background(), SearchOptions{Query: "dosyayı sil", Mode: ModeSemantic})
	if err != nil {
		t.Fatal(err)
	}
	for _, r := range res {
		if r.Skill.Name == "delete_file" {
			t.Fatal("semantic modda uyuşmayan boyutlu skill dönmemeli")
		}
	}
}

func TestSearch_Validation(t *testing.T) {
	svc, _, _ := newTestService(t)
	cases := []struct {
		name  string
		opt   SearchOptions
		field string
	}{
		{"boş sorgu", SearchOptions{Query: "   "}, "query"},
		{"geçersiz mod", SearchOptions{Query: "x", Mode: "fuzzy"}, "mode"},
		{"minScore > 1", SearchOptions{Query: "x", MinScore: 1.5}, "minScore"},
		{"minScore < 0", SearchOptions{Query: "x", MinScore: -0.1}, "minScore"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			_, err := svc.Search(context.Background(), c.opt)
			var ve *ValidationError
			if !errors.As(err, &ve) || !hasField(ve.Errors, c.field) {
				t.Fatalf("%s alanında ValidationError beklenirdi: %v", c.field, err)
			}
		})
	}
}

func TestSearch_LimitIsCapped(t *testing.T) {
	svc, _, _ := newTestService(t)
	ctx := context.Background()
	for i := 0; i < MaxSearchLimit+5; i++ {
		in := searchInput("plane_skill_"+string(rune('a'+i%26))+string(rune('a'+i/26)), "plane oluşturur", StatusActive)
		if _, err := svc.Register(ctx, in); err != nil {
			t.Fatal(err)
		}
	}
	res, err := svc.Search(ctx, SearchOptions{Query: "plane", Mode: ModeKeyword, Limit: 10_000})
	if err != nil {
		t.Fatal(err)
	}
	if len(res) != MaxSearchLimit {
		t.Fatalf("limit %d ile sınırlanmalı: %d", MaxSearchLimit, len(res))
	}
}

// TestEmbedCache_ReusesVector: AYNI sorgu ikinci kez arandığında embedder ÇAĞRILMAZ.
//
// Neden? Skill retrieval HER mesajda çalışır (agent.plan). Önbellek olmazsa her mesaj
// Ollama'ya gider ve 4.7 GB'lık embedding modeli devreye girer; bu, gerçek zamanlı
// sohbeti saniyelerce yavaşlatır.
func TestEmbedCache_ReusesVector(t *testing.T) {
	ctx := context.Background()
	svc, _, _ := newTestService(t)
	seedSearch(t, svc)

	emb := &bowEmbedder{}
	svc.SetEmbedder(emb)

	q := "bir kere aranan sorgu"
	before := emb.callCount()
	for i := 0; i < 3; i++ {
		if _, err := svc.Search(ctx, SearchOptions{Query: q, Mode: ModeHybrid}); err != nil {
			t.Fatalf("arama %d başarısız: %v", i, err)
		}
	}
	if got := emb.callCount() - before; got != 1 {
		t.Fatalf("önbellek çalışmadı: %d çağrı beklenen 1", got)
	}

	// FARKLI sorgu yeni çağrı demektir (önbellek tüm sorguları yutmamalı).
	if _, err := svc.Search(ctx, SearchOptions{Query: "baska bir metin", Mode: ModeHybrid}); err != nil {
		t.Fatalf("farklı sorgu başarısız: %v", err)
	}
	if got := emb.callCount() - before; got != 2 {
		t.Fatalf("farklı sorgu önbellekten dönmemeliydi: %d çağrı beklenen 2", got)
	}
}

// TestEmbedCache_ClearedOnNewEmbedder: sağlayıcı değişince önbellek temizlenir.
//
// Neden? Farklı embedding modellerinin vektörleri aynı uzayda değildir; eski vektör
// yeni modelle karşılaştırılırsa arama bozuk sonuç verir.
func TestEmbedCache_ClearedOnNewEmbedder(t *testing.T) {
	ctx := context.Background()
	svc, _, _ := newTestService(t)
	seedSearch(t, svc)

	svc.SetEmbedder(&bowEmbedder{})
	if _, err := svc.Search(ctx, SearchOptions{Query: "sorgu", Mode: ModeHybrid}); err != nil {
		t.Fatal(err)
	}
	emb := &bowEmbedder{}
	svc.SetEmbedder(emb)
	if _, err := svc.Search(ctx, SearchOptions{Query: "sorgu", Mode: ModeHybrid}); err != nil {
		t.Fatal(err)
	}
	if emb.callCount() == 0 {
		t.Fatal("yeni embedder değişiminde önbellek temizlenmedi")
	}
}

// TestEmbedCache_TTLExpires: eski kayıt TTL sonrası düşer.
//
// Neden? Uzun süre açık kalan sunucuda model çıktısı eskimeye devam eder; kalıcı
// önbellek eski/güncel olmayan vektörleri sonsuza dek servis ederdi.
func TestEmbedCache_TTLExpires(t *testing.T) {
	now := time.Now()
	c := newEmbedCache(func() time.Time { return now })
	c.put("k", []float32{1, 2, 3})
	if _, ok := c.get("k"); !ok {
		t.Fatal("yeni kayıt bulunamadı")
	}
	now = now.Add(cacheTTL + time.Minute) // TTL aşıldı
	if _, ok := c.get("k"); ok {
		t.Fatal("süresi dolmuş kayıt döndürüldü")
	}
}

// TestEmbedCache_EvictsOldestWhenFull: kapasite dolunca bellek sınırsız büyümez.
func TestEmbedCache_EvictsOldestWhenFull(t *testing.T) {
	now := time.Now()
	c := newEmbedCache(func() time.Time { return now })
	c.max = 2
	c.put("a", []float32{1})
	now = now.Add(time.Second)
	c.put("b", []float32{2})
	now = now.Add(time.Second)
	c.put("c", []float32{3}) // kapasite aşıldı: en eski ("a") atılmalı

	if _, ok := c.get("a"); ok {
		t.Error("en eski kayıt atılmadı")
	}
	if _, ok := c.get("c"); !ok {
		t.Error("yeni kayıt eklenmedi")
	}
}

func TestReindex_FillsMissingSkipsExistingForceRedoes(t *testing.T) {
	ctx := context.Background()
	svc, _, _ := newTestService(t)
	seedSearch(t, svc) // embedder yok → vektörsüz kayıtlar

	if _, err := svc.Reindex(ctx, false); !errors.Is(err, ErrNoEmbedder) {
		t.Fatalf("embedder yokken ErrNoEmbedder beklenirdi: %v", err)
	}

	emb := &bowEmbedder{}
	svc.SetEmbedder(emb)

	rep, err := svc.Reindex(ctx, false)
	if err != nil || rep.Total != 4 || rep.Embedded != 4 || rep.Failed != 0 {
		t.Fatalf("eksikler doldurulmalı (taslak dahil): %+v %v", rep, err)
	}
	rep, err = svc.Reindex(ctx, false)
	if err != nil || rep.Embedded != 0 || rep.Skipped != 4 {
		t.Fatalf("ikinci çalıştırma hiçbir şey üretmemeli: %+v %v", rep, err)
	}
	before := emb.callCount()
	rep, err = svc.Reindex(ctx, true)
	if err != nil || rep.Embedded != 4 || emb.callCount() != before+4 {
		t.Fatalf("force hepsini yeniden üretmeli: %+v %v", rep, err)
	}

	emb.setErr(errors.New("ollama kapalı"))
	rep, err = svc.Reindex(ctx, true)
	if err != nil || rep.Failed != 4 || rep.Embedded != 0 {
		t.Fatalf("başarısızlıklar sayılmalı, hata döndürmemeli: %+v %v", rep, err)
	}
}

func TestReindex_DoesNotOverwriteConcurrentEdit(t *testing.T) {
	ctx := context.Background()
	svc, repo, _ := newTestService(t)
	if _, err := svc.Register(ctx, searchInput("create_plane", "Plane oluşturur.", StatusActive)); err != nil {
		t.Fatal(err)
	}

	// Embedding üretilirken kullanıcı skill'i düzenliyor (UpdatedAt ilerliyor).
	edited := false
	emb := &bowEmbedder{hook: func() {
		if edited {
			return
		}
		edited = true
		cur, _ := repo.GetByName(ctx, "create_plane")
		cur.Description = "Kullanıcı bu sırada düzenledi."
		cur.UpdatedAt = cur.UpdatedAt.Add(time.Second)
		if err := repo.Update(ctx, cur); err != nil {
			t.Error(err)
		}
	}}
	svc.SetEmbedder(emb)

	rep, err := svc.Reindex(ctx, false)
	if err != nil || rep.Embedded != 0 || rep.Skipped != 1 {
		t.Fatalf("düzenlenen skill atlanmalı: %+v %v", rep, err)
	}
	cur, _ := repo.GetByName(ctx, "create_plane")
	if cur.Description != "Kullanıcı bu sırada düzenledi." {
		t.Fatal("kullanıcının düzenlemesi ezilmemeli")
	}
	if len(cur.Embedding) != 0 {
		t.Fatal("eski metne ait vektör yazılmamalı")
	}
}

func TestReindex_StopsOnContextCancel(t *testing.T) {
	svc, _, _ := newTestService(t)
	seedSearch(t, svc)
	svc.SetEmbedder(&bowEmbedder{})
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if _, err := svc.Reindex(ctx, false); !errors.Is(err, context.Canceled) {
		t.Fatalf("iptal edilmiş bağlamda context.Canceled beklenirdi: %v", err)
	}
}

func TestCosine(t *testing.T) {
	cases := []struct {
		name string
		a, b []float32
		want float64
	}{
		{"özdeş", []float32{1, 2, 3}, []float32{1, 2, 3}, 1},
		{"dik", []float32{1, 0}, []float32{0, 1}, 0},
		{"zıt yön 0'a çekilir", []float32{1, 0}, []float32{-1, 0}, 0},
		{"boyut farkı", []float32{1, 2}, []float32{1, 2, 3}, 0},
		{"sıfır vektör", []float32{0, 0}, []float32{1, 1}, 0},
		{"boş", nil, nil, 0},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := cosine(c.a, c.b)
			if got < c.want-1e-9 || got > c.want+1e-9 {
				t.Fatalf("cosine = %v, beklenen %v", got, c.want)
			}
		})
	}
}

func TestEmbeddingText_IncludesNameTagsAndExamples(t *testing.T) {
	sk := &Skill{
		Name: "create_plane", Description: "Offset plane oluşturur.",
		Tags:     []string{"catia", "geometri"},
		Examples: []Example{{Input: "30 mm geriye plane koy"}, {Input: ""}},
	}
	txt := EmbeddingText(sk)
	for _, want := range []string{"create plane", "Offset plane oluşturur.", "catia", "geometri", "30 mm geriye plane koy"} {
		if !strings.Contains(txt, want) {
			t.Fatalf("embedding metni %q içermeli: %q", want, txt)
		}
	}
}
