package memory

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

	"fiko-ai/internal/llm"
)

// ---- sahteler ----

type fakeLLM struct {
	reply string
	err   error
	calls atomic.Int32
	last  llm.Request
}

func (f *fakeLLM) Name() string { return "fake" }
func (f *fakeLLM) Generate(_ context.Context, req llm.Request) (llm.Result, error) {
	f.calls.Add(1)
	f.last = req
	return llm.Result{Content: f.reply}, f.err
}
func (f *fakeLLM) GenerateStream(ctx context.Context, req llm.Request, _ llm.DeltaFunc) (llm.Result, error) {
	return f.Generate(ctx, req)
}
func (f *fakeLLM) ListModels(context.Context) ([]llm.ModelInfo, error) { return nil, nil }

// fakeEmb: table'da varsa o vektör, yoksa sözcük torbası (hash) vektörü döner.
type fakeEmb struct {
	table map[string][]float32
	err   error
	calls atomic.Int32
}

func (f *fakeEmb) Embed(_ context.Context, text string) ([]float32, error) {
	f.calls.Add(1)
	if f.err != nil {
		return nil, f.err
	}
	if v, ok := f.table[Normalize(text)]; ok {
		return v, nil
	}
	return bagVec(text), nil
}

func bagVec(text string) []float32 {
	v := make([]float32, 64)
	for _, tok := range tokenize(text) {
		h := fnv.New32a()
		_, _ = h.Write([]byte(tok))
		v[h.Sum32()%64]++
	}
	return v
}

func newSvc(t *testing.T, emb *fakeEmb, opt Options) (*Service, *MemoryRepo) {
	t.Helper()
	repo := NewMemoryRepo()
	s := NewService(repo, nil, opt)
	if emb != nil {
		s.SetEmbedder(emb, "emb-1")
	}
	t.Cleanup(func() { _ = s.Close(context.Background()) })
	return s, repo
}

// ---- depo sözleşmesi ----

func TestMemoryRepoContract(t *testing.T) {
	RepositoryContract(t, func() Repository { return NewMemoryRepo() })
}

// ---- Add ----

func TestAddValidation(t *testing.T) {
	s, _ := newSvc(t, nil, Options{})
	ctx := context.Background()
	tooLong := strings.Repeat("a", MaxContentRunes+1)
	cases := []struct {
		name  string
		in    AddInput
		field string
	}{
		{"kullanıcı yok", AddInput{Content: "Geçerli içerik"}, "userId"},
		{"içerik kısa", AddInput{UserID: "u", Content: "ab"}, "content"},
		{"içerik uzun", AddInput{UserID: "u", Content: tooLong}, "content"},
		{"kind geçersiz", AddInput{UserID: "u", Content: "Geçerli içerik", Kind: "x"}, "kind"},
		{"origin geçersiz", AddInput{UserID: "u", Content: "Geçerli içerik", Origin: "x"}, "origin"},
		{"önem aralık dışı", AddInput{UserID: "u", Content: "Geçerli içerik", Importance: 2}, "importance"},
		{"çok etiket", AddInput{UserID: "u", Content: "Geçerli içerik", Tags: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}}, "tags"},
	}
	for _, c := range cases {
		_, _, err := s.Add(ctx, c.in)
		var ve *ValidationError
		if !errors.As(err, &ve) {
			t.Errorf("%s: ValidationError beklenirdi: %v", c.name, err)
			continue
		}
		found := false
		for _, f := range ve.Errors {
			found = found || f.Field == c.field
		}
		if !found {
			t.Errorf("%s: %q alanı hatası yok: %+v", c.name, c.field, ve.Errors)
		}
	}
}

func TestAddDefaultsAndNormalize(t *testing.T) {
	s, _ := newSvc(t, nil, Options{})
	m, out, err := s.Add(context.Background(), AddInput{UserID: "u", Content: "  Kullanıcı   Angular\nkullanıyor ", Tags: []string{" Web ", "web", "UI"}})
	if err != nil || out != Created {
		t.Fatalf("Created beklenirdi: %v %v", out, err)
	}
	if m.Content != "Kullanıcı Angular kullanıyor" || m.Kind != KindLongTerm || m.Origin != OriginUser || m.Importance != 0.8 {
		t.Errorf("varsayılanlar/temizlik bozuk: %+v", m)
	}
	if len(m.Tags) != 2 || m.Tags[0] != "web" || m.Tags[1] != "ui" {
		t.Errorf("etiketler küçük harf ve tekrarsız olmalı: %v", m.Tags)
	}
	if len(m.Embedding) != 0 {
		t.Error("dönen anıda embedding olmamalı")
	}
}

func TestAddExactDuplicateIsUnchangedAndUserUpgrades(t *testing.T) {
	s, repo := newSvc(t, nil, Options{})
	ctx := context.Background()
	first, out, _ := s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcı Ankara'da yaşıyor", Origin: OriginAuto})
	if out != Created || first.Origin != OriginAuto {
		t.Fatalf("auto anı: %v %+v", out, first)
	}
	// Otomatik yeniden görme: değişmez.
	_, out, err := s.Add(ctx, AddInput{UserID: "u", Content: "kullanıcı ankara'da YAŞIYOR", Origin: OriginAuto})
	if err != nil || out != Unchanged {
		t.Errorf("tekrar Unchanged olmalı: %v %v", out, err)
	}
	// Kullanıcı aynısını elle yazarsa anı ona ait olur.
	m, out, _ := s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcı Ankara'da yaşıyor", Origin: OriginUser})
	if out != Updated || m.Origin != OriginUser {
		t.Errorf("kullanıcı onayı Updated + origin=user olmalı: %v %+v", out, m)
	}
	if n, _ := repo.Count(ctx, "u"); n != 1 {
		t.Errorf("tek anı kalmalı: %d", n)
	}
}

func TestAddLimitPerUser(t *testing.T) {
	s, _ := newSvc(t, nil, Options{MaxPerUser: 2})
	ctx := context.Background()
	for _, c := range []string{"Birinci anı", "İkinci anı"} {
		if _, _, err := s.Add(ctx, AddInput{UserID: "u", Content: c}); err != nil {
			t.Fatal(err)
		}
	}
	if _, _, err := s.Add(ctx, AddInput{UserID: "u", Content: "Üçüncü anı"}); !errors.Is(err, ErrLimit) {
		t.Errorf("sınır aşımı ErrLimit olmalı: %v", err)
	}
	// Başka kullanıcının sınırı ayrıdır; tekrar (aynı içerik) sınırda bile çalışır.
	if _, _, err := s.Add(ctx, AddInput{UserID: "u2", Content: "Üçüncü anı"}); err != nil {
		t.Errorf("başka kullanıcı etkilenmemeli: %v", err)
	}
	if _, out, err := s.Add(ctx, AddInput{UserID: "u", Content: "birinci ANI"}); err != nil || out != Unchanged {
		t.Errorf("sınırdayken bilinen bilgi tekrarı hata olmamalı: %v %v", out, err)
	}
}

func TestEmbeddingFailureStillSaves(t *testing.T) {
	emb := &fakeEmb{err: errors.New("ollama kapalı")}
	s, repo := newSvc(t, emb, Options{})
	m, out, err := s.Add(context.Background(), AddInput{UserID: "u", Content: "Kullanıcı Angular kullanıyor"})
	if err != nil || out != Created {
		t.Fatalf("embedding hatası kaydı düşürmemeli: %v %v", out, err)
	}
	got, _ := repo.Get(context.Background(), "u", m.ID)
	if len(got.Embedding) != 0 {
		t.Error("vektör üretilemediyse boş kalmalı")
	}
	// Vektörsüz anı anahtar kelimeyle bulunur.
	hits, _ := s.Search(context.Background(), SearchOptions{UserID: "u", Query: "angular"})
	if len(hits) != 1 {
		t.Errorf("vektörsüz anı anahtar kelimeyle bulunmalı: %+v", hits)
	}
}

// ---- Update / Delete ----

func TestUpdateReembedsAndOwnsMemory(t *testing.T) {
	emb := &fakeEmb{}
	s, repo := newSvc(t, emb, Options{})
	ctx := context.Background()
	m, _, _ := s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcı Angular kullanıyor", Origin: OriginAuto})
	before := emb.calls.Load()

	newText := "Kullanıcı React kullanıyor"
	got, err := s.Update(ctx, UpdateInput{UserID: "u", ID: m.ID, Content: &newText})
	if err != nil {
		t.Fatal(err)
	}
	if got.Content != newText || got.Origin != OriginUser {
		t.Errorf("içerik değişmeli ve anı kullanıcıya ait olmalı: %+v", got)
	}
	if emb.calls.Load() != before+1 {
		t.Error("içerik değişince yeniden embedding üretilmeli")
	}
	stored, _ := repo.Get(ctx, "u", m.ID)
	if !stored.HasEmbedding("emb-1") || stored.Hash != HashOf(newText) {
		t.Errorf("hash/vektör güncellenmeli: %+v", stored)
	}

	// Yalnızca etiket: yeniden embedding yok.
	calls := emb.calls.Load()
	tags := []string{"Web"}
	if _, err := s.Update(ctx, UpdateInput{UserID: "u", ID: m.ID, Tags: &tags}); err != nil {
		t.Fatal(err)
	}
	if emb.calls.Load() != calls {
		t.Error("içerik değişmediyse embedding yeniden üretilmemeli")
	}
}

func TestUpdateErrors(t *testing.T) {
	s, _ := newSvc(t, nil, Options{})
	ctx := context.Background()
	a, _, _ := s.Add(ctx, AddInput{UserID: "u", Content: "Birinci anı"})
	_, _, _ = s.Add(ctx, AddInput{UserID: "u", Content: "İkinci anı"})

	clash := "ikinci ANI"
	if _, err := s.Update(ctx, UpdateInput{UserID: "u", ID: a.ID, Content: &clash}); !errors.Is(err, ErrDuplicate) {
		t.Errorf("başka anıyla çakışma ErrDuplicate olmalı: %v", err)
	}
	if _, err := s.Update(ctx, UpdateInput{UserID: "başkası", ID: a.ID, Content: &clash}); !errors.Is(err, ErrNotFound) {
		t.Errorf("başkasının anısı ErrNotFound olmalı: %v", err)
	}
	short := "x"
	var ve *ValidationError
	if _, err := s.Update(ctx, UpdateInput{UserID: "u", ID: a.ID, Content: &short}); !errors.As(err, &ve) {
		t.Errorf("geçersiz içerik ValidationError olmalı: %v", err)
	}
	bad := Kind("x")
	if _, err := s.Update(ctx, UpdateInput{UserID: "u", ID: a.ID, Kind: &bad}); !errors.As(err, &ve) {
		t.Errorf("geçersiz tür ValidationError olmalı: %v", err)
	}
}

func TestDeleteAndDeleteAll(t *testing.T) {
	s, _ := newSvc(t, nil, Options{})
	ctx := context.Background()
	a, _, _ := s.Add(ctx, AddInput{UserID: "u", Content: "Birinci anı"})
	_, _, _ = s.Add(ctx, AddInput{UserID: "u", Content: "İkinci anı"})
	_, _, _ = s.Add(ctx, AddInput{UserID: "v", Content: "Başkasının anısı"})

	if err := s.Delete(ctx, "v", a.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("başkasının anısı silinemez: %v", err)
	}
	if err := s.Delete(ctx, "u", a.ID); err != nil {
		t.Fatal(err)
	}
	n, err := s.DeleteAll(ctx, "u")
	if err != nil || n != 1 {
		t.Errorf("kalan 1 anı silinmeli: %d %v", n, err)
	}
	if left, _ := s.Count(ctx, "v"); left != 1 {
		t.Error("başka kullanıcının anısı silinmemeli")
	}
	if _, err := s.DeleteAll(ctx, " "); err == nil {
		t.Error("kullanıcısız DeleteAll reddedilmeli")
	}
}

// ---- Search / Recall ----

func TestSearchKeywordOnlyTurkishFold(t *testing.T) {
	s, _ := newSvc(t, nil, Options{})
	ctx := context.Background()
	_, _, _ = s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcı tezgâhı CNC ile işliyor"})
	_, _, _ = s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcının köpeği var"})
	_, _, _ = s.Add(ctx, AddInput{UserID: "v", Content: "Başkası tezgâh kullanıyor"})

	hits, err := s.Search(ctx, SearchOptions{UserID: "u", Query: "tezgahi"})
	if err != nil || len(hits) != 1 || !strings.Contains(hits[0].Content, "tezgâhı") {
		t.Fatalf("Türkçe katlama ile bulunmalı, başka kullanıcınınki gelmemeli: %+v %v", hits, err)
	}
	if _, err := s.Search(ctx, SearchOptions{UserID: "u", Query: " "}); err == nil {
		t.Error("boş sorgu reddedilmeli")
	}
	if _, err := s.Search(ctx, SearchOptions{UserID: "u", Query: "x", MinScore: 2}); err == nil {
		t.Error("geçersiz minScore reddedilmeli")
	}
}

func TestSearchSemanticOrderingAndMinScore(t *testing.T) {
	emb := &fakeEmb{table: map[string][]float32{
		"Kullanıcı Angular kullanıyor": {1, 0, 0},
		"Kullanıcının köpeği var":      {0, 1, 0},
		"hangi framework":              {0.9, 0.1, 0},
	}}
	s, _ := newSvc(t, emb, Options{})
	ctx := context.Background()
	_, _, _ = s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcı Angular kullanıyor"})
	_, _, _ = s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcının köpeği var"})

	hits, err := s.Search(ctx, SearchOptions{UserID: "u", Query: "hangi framework"})
	if err != nil || len(hits) == 0 || hits[0].Content != "Kullanıcı Angular kullanıyor" {
		t.Fatalf("anlamca yakın anı önce gelmeli: %+v %v", hits, err)
	}
	strict, _ := s.Search(ctx, SearchOptions{UserID: "u", Query: "hangi framework", MinScore: 0.5})
	if len(strict) != 1 {
		t.Errorf("MinScore ilgisizi elemeli: %+v", strict)
	}
	one, _ := s.Search(ctx, SearchOptions{UserID: "u", Query: "hangi framework", Limit: 1})
	if len(one) != 1 {
		t.Errorf("Limit uygulanmalı: %d", len(one))
	}
}

func TestSearchIgnoresVectorsOfOtherModel(t *testing.T) {
	// "framework" sorgusunun anahtar kelime örtüşmesi YOK: yalnızca vektör eşleşirse bulunur.
	emb := &fakeEmb{table: map[string][]float32{"Kullanıcı Angular kullanıyor": {1, 0}, "framework": {1, 0}}}
	s, repo := newSvc(t, emb, Options{})
	ctx := context.Background()
	m, _, _ := s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcı Angular kullanıyor"})

	if hits, _ := s.Search(ctx, SearchOptions{UserID: "u", Query: "framework"}); len(hits) != 1 {
		t.Fatalf("aynı modelin vektörü eşleşmeli: %+v", hits)
	}
	// Model değişti: eski vektör bayat sayılır ve KARŞILAŞTIRILMAZ.
	s.SetEmbedder(emb, "emb-2")
	if hits, _ := s.Search(ctx, SearchOptions{UserID: "u", Query: "framework"}); len(hits) != 0 {
		t.Fatalf("başka modelin vektörü skorlamada kullanılmamalı: %+v", hits)
	}
	// Anahtar kelime yolu yine çalışır.
	if hits, _ := s.Search(ctx, SearchOptions{UserID: "u", Query: "angular"}); len(hits) != 1 {
		t.Errorf("bayat vektörlü anı anahtar kelimeyle bulunmalı: %+v", hits)
	}
	// Reindex(force) yeni modelin vektörünü yazar; sonra anlamca yeniden bulunur.
	if _, err := s.Reindex(ctx, "u", true); err != nil {
		t.Fatal(err)
	}
	deadline := time.Now().Add(3 * time.Second)
	for time.Now().Before(deadline) {
		if got, _ := repo.Get(ctx, "u", m.ID); got.HasEmbedding("emb-2") {
			if hits, _ := s.Search(ctx, SearchOptions{UserID: "u", Query: "framework"}); len(hits) != 1 {
				t.Errorf("yeniden indekslenen anı anlamca bulunmalı: %+v", hits)
			}
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Error("yeniden indeksleme vektörü yazmadı")
}

func TestRecallTouchesAndSkipsWhenEmpty(t *testing.T) {
	emb := &fakeEmb{}
	s, repo := newSvc(t, emb, Options{})
	ctx := context.Background()

	if got := s.Recall(ctx, "u", "merhaba", 5, 0); got != nil {
		t.Errorf("anı yoksa nil dönmeli: %v", got)
	}
	if emb.calls.Load() != 0 {
		t.Error("anı yokken sorgu embedding'i HİÇ üretilmemeli (ses gecikmesi)")
	}

	m, _, _ := s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcı Angular kullanıyor"})
	hits := s.Recall(ctx, "u", "angular nedir", 5, 0)
	if len(hits) != 1 {
		t.Fatalf("1 isabet beklenirdi: %+v", hits)
	}
	if err := s.Close(ctx); err != nil { // Close arka plan Touch'ını bekler
		t.Fatal(err)
	}
	got, _ := repo.Get(ctx, "u", m.ID)
	if got.UseCount != 1 || got.LastUsedAt == nil {
		t.Errorf("kullanım sayacı artmalı: %+v", got)
	}
}

func TestReindexFillsMissingVectors(t *testing.T) {
	s, repo := newSvc(t, nil, Options{})
	ctx := context.Background()
	m, _, _ := s.Add(ctx, AddInput{UserID: "u", Content: "Kullanıcı Angular kullanıyor"}) // embedder yok → vektörsüz
	if _, err := s.Reindex(ctx, "u", false); !errors.Is(err, ErrNoEmbedder) {
		t.Fatalf("embedder yokken ErrNoEmbedder beklenirdi: %v", err)
	}
	s.SetEmbedder(&fakeEmb{}, "emb-1")
	rep, err := s.Reindex(ctx, "u", false)
	if err != nil || rep.Total != 1 || rep.Queued != 1 {
		t.Fatalf("1 anı kuyruğa girmeli: %+v %v", rep, err)
	}
	deadline := time.Now().Add(3 * time.Second)
	for time.Now().Before(deadline) {
		if got, _ := repo.Get(ctx, "u", m.ID); got.HasEmbedding("emb-1") {
			if rep2, _ := s.Reindex(ctx, "u", false); rep2.Queued != 0 {
				t.Errorf("hazır vektör yeniden kuyruğa girmemeli: %+v", rep2)
			}
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Error("vektör arka planda üretilmedi")
}
