package memory

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"
)

// RepositoryContract her Repository uygulamasının sağlaması gereken davranışlardır.
// ÜRETİM dosyasındadır (_test.go değil): mongostore gibi dış paketler de çağırabilsin diye
// (conversation/skill/tool/knowledge sözleşmeleriyle aynı desen).
func RepositoryContract(t *testing.T, newRepo func() Repository) {
	t.Helper()
	ctx := context.Background()
	base := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)

	mk := func(id, user, content string, at time.Time) *Memory {
		return &Memory{
			ID: id, UserID: user, Kind: KindLongTerm, Content: content, Tags: []string{"a"},
			Origin: OriginAuto, Importance: 0.6, Hash: HashOf(content), CreatedAt: at, UpdatedAt: at,
		}
	}

	t.Run("create-get-isolation", func(t *testing.T) {
		r := newRepo()
		m := mk("m1", "u1", "Kullanıcı Angular kullanıyor", base)
		m.Embedding = []float32{0.5, -0.25}
		m.EmbedModel = "emb"
		if err := r.Create(ctx, m); err != nil {
			t.Fatal(err)
		}
		got, err := r.Get(ctx, "u1", "m1")
		if err != nil || got.Content != m.Content || len(got.Embedding) != 2 || got.Embedding[1] != -0.25 || got.EmbedModel != "emb" {
			t.Fatalf("round-trip bozuk: %+v %v", got, err)
		}
		if _, err := r.Get(ctx, "u2", "m1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("başka kullanıcının anısı görülmemeli (404): %v", err)
		}
		if _, err := r.Get(ctx, "u1", "yok"); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan anı ErrNotFound olmalı: %v", err)
		}
		// Dönen değeri değiştirmek depoyu etkilememeli.
		got.Content = "değişti"
		got.Tags[0] = "x"
		again, _ := r.Get(ctx, "u1", "m1")
		if again.Content == "değişti" || again.Tags[0] == "x" {
			t.Error("depo iç durumu sızdırmamalı (derin kopya)")
		}
	})

	t.Run("duplicate-hash-per-user", func(t *testing.T) {
		r := newRepo()
		if err := r.Create(ctx, mk("m1", "u1", "Aynı içerik", base)); err != nil {
			t.Fatal(err)
		}
		if err := r.Create(ctx, mk("m2", "u1", "aynı İÇERİK", base)); !errors.Is(err, ErrDuplicate) {
			t.Errorf("aynı kullanıcıda aynı özet ErrDuplicate olmalı: %v", err)
		}
		if err := r.Create(ctx, mk("m3", "u2", "Aynı içerik", base)); err != nil {
			t.Errorf("başka kullanıcıda aynı içerik serbest olmalı: %v", err)
		}
		if err := r.Create(ctx, mk("m1", "u1", "Başka içerik", base)); !errors.Is(err, ErrDuplicate) {
			t.Errorf("aynı kimlik ErrDuplicate olmalı: %v", err)
		}
		got, err := r.GetByHash(ctx, "u1", HashOf("aynı içerik"))
		if err != nil || got.ID != "m1" {
			t.Errorf("GetByHash bulamadı: %v %v", got, err)
		}
		if _, err := r.GetByHash(ctx, "u2", HashOf("başka")); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan özet ErrNotFound olmalı: %v", err)
		}
	})

	t.Run("update-delete", func(t *testing.T) {
		r := newRepo()
		_ = r.Create(ctx, mk("m1", "u1", "Birinci anı", base))
		_ = r.Create(ctx, mk("m2", "u1", "İkinci anı", base))

		upd := mk("m1", "u1", "Birinci anı güncel", base.Add(time.Minute))
		if err := r.Update(ctx, upd); err != nil {
			t.Fatal(err)
		}
		if got, _ := r.Get(ctx, "u1", "m1"); got.Content != "Birinci anı güncel" {
			t.Errorf("güncelleme yazılmadı: %+v", got)
		}
		clash := mk("m1", "u1", "İkinci anı", base)
		if err := r.Update(ctx, clash); !errors.Is(err, ErrDuplicate) {
			t.Errorf("başka anının özetine çakışan güncelleme ErrDuplicate olmalı: %v", err)
		}
		if err := r.Update(ctx, mk("m1", "u2", "Hırsız", base)); !errors.Is(err, ErrNotFound) {
			t.Errorf("başkasının anısı güncellenemez: %v", err)
		}
		if err := r.Update(ctx, mk("yok", "u1", "Yok", base)); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan anı ErrNotFound olmalı: %v", err)
		}
		if err := r.Delete(ctx, "u2", "m1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("başkasının anısı silinemez: %v", err)
		}
		if err := r.Delete(ctx, "u1", "m1"); err != nil {
			t.Fatal(err)
		}
		if err := r.Delete(ctx, "u1", "m1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("ikinci silme ErrNotFound olmalı: %v", err)
		}
	})

	t.Run("list-order-filter-paging", func(t *testing.T) {
		r := newRepo()
		for i := 0; i < 5; i++ {
			// ASCII: liste süzgeci büyük/küçük harfi duyarsızdır ama Türkçe İ/ı katlaması YAPMAZ
			// (Mongo $regex "i" seçeneği de yapmaz); Türkçe duyarlı arama Service.Search'tedir.
			m := mk(fmt.Sprintf("m%d", i), "u1", fmt.Sprintf("Memory number %d Angular", i), base.Add(time.Duration(i)*time.Minute))
			if i == 3 {
				m.Kind = KindEpisodic
				m.Tags = []string{"olay"}
			}
			_ = r.Create(ctx, m)
		}
		_ = r.Create(ctx, mk("x", "u2", "Başkasının anısı", base))

		all, err := r.List(ctx, Filter{UserID: "u1"}, 0, 0)
		if err != nil || len(all) != 5 {
			t.Fatalf("5 anı beklenirdi: %d %v", len(all), err)
		}
		if all[0].ID != "m4" || all[4].ID != "m0" {
			t.Errorf("en yeni önce olmalı: %s … %s", all[0].ID, all[4].ID)
		}
		for _, m := range all {
			if len(m.Embedding) != 0 {
				t.Error("listede embedding taşınmamalı")
			}
		}
		page, _ := r.List(ctx, Filter{UserID: "u1"}, 2, 1)
		if len(page) != 2 || page[0].ID != "m3" {
			t.Errorf("sayfalama bozuk: %+v", page)
		}
		if beyond, _ := r.List(ctx, Filter{UserID: "u1"}, 10, 99); beyond == nil || len(beyond) != 0 {
			t.Errorf("aşan offset boş (nil değil) dilim olmalı: %v", beyond)
		}
		if ep, _ := r.List(ctx, Filter{UserID: "u1", Kind: KindEpisodic}, 0, 0); len(ep) != 1 || ep[0].ID != "m3" {
			t.Errorf("tür süzgeci bozuk: %+v", ep)
		}
		if q, _ := r.List(ctx, Filter{UserID: "u1", Query: "NUMBER 2"}, 0, 0); len(q) != 1 || q[0].ID != "m2" {
			t.Errorf("içerik araması büyük/küçük harf duyarsız olmalı: %+v", q)
		}
		if q, _ := r.List(ctx, Filter{UserID: "u1", Query: "a.c(", Tag: ""}, 0, 0); len(q) != 0 {
			t.Errorf("sorgu düz metindir (regex değil): %+v", q)
		}
		if tg, _ := r.List(ctx, Filter{UserID: "u1", Tag: "olay"}, 0, 0); len(tg) != 1 || tg[0].ID != "m3" {
			t.Errorf("etiket süzgeci bozuk: %+v", tg)
		}
		if n, _ := r.Count(ctx, "u1"); n != 5 {
			t.Errorf("Count 5 olmalı: %d", n)
		}
	})

	t.Run("deleteall-scoped", func(t *testing.T) {
		r := newRepo()
		_ = r.Create(ctx, mk("a", "u1", "Bir", base))
		_ = r.Create(ctx, mk("b", "u1", "İki", base))
		_ = r.Create(ctx, mk("c", "u2", "Üç", base))
		n, err := r.DeleteAll(ctx, "u1")
		if err != nil || n != 2 {
			t.Fatalf("2 silinmeliydi: %d %v", n, err)
		}
		if left, _ := r.Count(ctx, "u2"); left != 1 {
			t.Error("başka kullanıcının anısı silinmemeli")
		}
	})

	t.Run("touch-and-embedding", func(t *testing.T) {
		r := newRepo()
		_ = r.Create(ctx, mk("m1", "u1", "Kullanılacak anı", base))
		at := base.Add(time.Hour)
		if err := r.Touch(ctx, "u1", []string{"m1", "yok"}, at); err != nil {
			t.Fatal(err)
		}
		_ = r.Touch(ctx, "u2", []string{"m1"}, at) // başkasının dokunuşu etkisiz
		_ = r.Touch(ctx, "u1", []string{"m1"}, at)
		got, _ := r.Get(ctx, "u1", "m1")
		if got.UseCount != 2 || got.LastUsedAt == nil || !got.LastUsedAt.Equal(at) {
			t.Errorf("Touch sayacı bozuk: %+v", got)
		}
		if err := r.SetEmbedding(ctx, "u1", "m1", []float32{1, 2, 3}, "emb2"); err != nil {
			t.Fatal(err)
		}
		if err := r.SetEmbedding(ctx, "u2", "m1", []float32{9}, "x"); !errors.Is(err, ErrNotFound) {
			t.Errorf("başkasının anısına vektör yazılamaz: %v", err)
		}
		cands, _ := r.Searchable(ctx, "u1")
		if len(cands) != 1 || len(cands[0].Embedding) != 3 || !cands[0].HasEmbedding("emb2") || cands[0].HasEmbedding("emb") {
			t.Errorf("Searchable embedding'i taşımalı: %+v", cands)
		}
		if cands[0].UseCount != 2 {
			t.Error("SetEmbedding diğer alanları bozmamalı")
		}
	})
}
