package claim

import (
	"context"
	"errors"
	"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
// (memory/conversation/skill sözleşmeleriyle aynı desen).
func RepositoryContract(t *testing.T, newRepo func() Repository) {
	t.Helper()
	ctx := context.Background()
	base := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)

	mk := func(id, kind, name, text, author string, at time.Time) *Claim {
		return &Claim{
			ID: id, Text: text, Kind: kind, Key: NormalizeKey(name),
			AuthorID: author, AuthorName: "Yazar " + author,
			CreatedAt: at, UpdatedAt: at, Status: StatusActive,
		}
	}
	ids := func(cs []Claim) []string {
		out := make([]string, len(cs))
		for i, c := range cs {
			out[i] = c.ID
		}
		return out
	}
	same := func(got, want []string) bool {
		if len(got) != len(want) {
			return false
		}
		for i := range got {
			if got[i] != want[i] {
				return false
			}
		}
		return true
	}

	t.Run("create-get-roundtrip", func(t *testing.T) {
		r := newRepo()
		c := mk("c1", KindFirma, "Acme Kalip", "Acme Kalip bir musteridir", "u1", base)
		c.Embedding = []float32{0.5, -0.25}
		c.EmbedModel = "bge-m3"
		c.Supersedes = "eski"
		c.ConflictsWith = "celisen"
		if err := r.Create(ctx, c); err != nil {
			t.Fatal(err)
		}
		got, err := r.Get(ctx, "c1")
		if err != nil || got.Text != c.Text || got.Key != "acme kalip" || got.Supersedes != "eski" || got.ConflictsWith != "celisen" ||
			len(got.Embedding) != 2 || got.Embedding[1] != -0.25 || !got.HasEmbedding("bge-m3") ||
			got.AuthorID != "u1" || got.AuthorName != "Yazar u1" || !got.CreatedAt.Equal(base) {
			t.Fatalf("round-trip bozuk: %+v %v", got, err)
		}
		if _, err := r.Get(ctx, "yok"); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan kayıt ErrNotFound olmalı: %v", err)
		}
		if err := r.Create(ctx, mk("c1", KindFirma, "Baska", "Baska metin", "u2", base)); !errors.Is(err, ErrDuplicate) {
			t.Errorf("aynı kimlik ErrDuplicate olmalı: %v", err)
		}
		// Aynı Key + farklı kimlik serbest: güncellenmiş/çelişkili kayıtlar anahtarı paylaşır.
		if err := r.Create(ctx, mk("c2", KindFirma, "Acme Kalip", "Acme Kalip artik tedarikci", "u2", base.Add(time.Minute))); err != nil {
			t.Errorf("aynı anahtarlı ikinci kayıt serbest olmalı: %v", err)
		}
		if err := r.Create(ctx, &Claim{ID: "x", Text: "yazarsiz"}); err == nil {
			t.Error("yazarsız kayıt reddedilmeli")
		}
		// Dönen değeri değiştirmek depoyu etkilememeli.
		got.Text = "degisti"
		got.Embedding[0] = 9
		again, _ := r.Get(ctx, "c1")
		if again.Text == "degisti" || again.Embedding[0] == 9 {
			t.Error("depo iç durumu sızdırmamalı (derin kopya)")
		}
	})

	t.Run("update-status-embedding-delete", func(t *testing.T) {
		r := newRepo()
		c := mk("c1", KindGeneral, "", "Birinci bilgi", "u1", base)
		c.Embedding = []float32{1, 2, 3}
		c.EmbedModel = "m1"
		_ = r.Create(ctx, c)

		upd := mk("c1", KindGeneral, "", "Birinci bilgi guncel", "u1", base)
		upd.UpdatedAt = base.Add(time.Minute)
		if err := r.Update(ctx, upd); err != nil {
			t.Fatal(err)
		}
		got, _ := r.Get(ctx, "c1")
		if got.Text != "Birinci bilgi guncel" || !got.UpdatedAt.Equal(base.Add(time.Minute)) || len(got.Embedding) != 0 {
			t.Errorf("Update TAMAMEN değiştirmeli (vektör dahil): %+v", got)
		}
		if err := r.Update(ctx, mk("yok", KindGeneral, "", "Yok", "u1", base)); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan kayıt güncellenemez: %v", err)
		}

		if err := r.SetEmbedding(ctx, "c1", []float32{4, 5}, "m2"); err != nil {
			t.Fatal(err)
		}
		at := base.Add(time.Hour)
		if err := r.SetStatus(ctx, "c1", StatusConflict, at); err != nil {
			t.Fatal(err)
		}
		got, _ = r.Get(ctx, "c1")
		if got.Status != StatusConflict || !got.UpdatedAt.Equal(at) {
			t.Errorf("SetStatus durumu ve zamanı yazmalı: %+v", got)
		}
		if got.Text != "Birinci bilgi guncel" || !got.HasEmbedding("m2") || len(got.Embedding) != 2 || got.AuthorID != "u1" {
			t.Errorf("SetStatus diğer alanları ezmemeli: %+v", got)
		}
		if err := r.SetStatus(ctx, "yok", StatusInvalid, at); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan kaydın durumu yazılamaz: %v", err)
		}
		if err := r.SetEmbedding(ctx, "yok", []float32{1}, "m"); !errors.Is(err, ErrNotFound) {
			t.Errorf("olmayan kayda vektör yazılamaz: %v", err)
		}

		if err := r.Delete(ctx, "c1"); err != nil {
			t.Fatal(err)
		}
		if err := r.Delete(ctx, "c1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("ikinci silme ErrNotFound olmalı: %v", err)
		}
		if _, err := r.Get(ctx, "c1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("silinen kayıt okunamaz: %v", err)
		}
	})

	t.Run("list-stable-order-and-paging", func(t *testing.T) {
		r := newRepo()
		// Karışık ekleme sırası; beklenen sıra: key artan, aynı key'de createdAt artan, eşitlikte ID.
		_ = r.Create(ctx, mk("d", KindFirma, "Zeta", "Zeta", "u1", base))
		_ = r.Create(ctx, mk("b", KindFirma, "Alfa", "Alfa ikinci", "u1", base.Add(time.Minute)))
		_ = r.Create(ctx, mk("a", KindFirma, "Alfa", "Alfa birinci", "u1", base))
		_ = r.Create(ctx, mk("c", KindFirma, "Beta", "Beta", "u1", base))
		_ = r.Create(ctx, mk("e", KindFirma, "Beta", "Beta ayni zaman", "u1", base))
		all, err := r.List(ctx, Filter{}, 0, 0)
		if err != nil {
			t.Fatal(err)
		}
		if want := []string{"a", "b", "c", "e", "d"}; !same(ids(all), want) {
			t.Fatalf("sıra %v, beklenen %v", ids(all), want)
		}
		for _, c := range all {
			if len(c.Embedding) != 0 {
				t.Error("listede embedding taşınmamalı")
			}
		}
		page, _ := r.List(ctx, Filter{}, 2, 1)
		if want := []string{"b", "c"}; !same(ids(page), want) {
			t.Errorf("sayfalama %v, beklenen %v", ids(page), want)
		}
		if beyond, _ := r.List(ctx, Filter{}, 10, 99); beyond == nil || len(beyond) != 0 {
			t.Errorf("aşan offset boş (nil değil) dilim olmalı: %v", beyond)
		}
		if empty, _ := newRepo().List(ctx, Filter{}, 0, 0); empty == nil || len(empty) != 0 {
			t.Errorf("boş depo boş (nil değil) dilim dönmeli: %v", empty)
		}
	})

	t.Run("filters", func(t *testing.T) {
		r := newRepo()
		_ = r.Create(ctx, mk("f1", KindFirma, "Acme", "Acme Angular kullanir", "u1", base))
		_ = r.Create(ctx, mk("f2", KindFirma, "Beta", "Beta firmasi", "u2", base))
		g := mk("g1", KindGorev, "", "Cuma gunu rapor hazirla", "u1", base)
		g.Status = StatusConflict
		_ = r.Create(ctx, g)
		inv := mk("k1", KindKisi, "Ali", "Ali muhendis", "u2", base)
		inv.Status = StatusInvalid
		_ = r.Create(ctx, inv)

		check := func(name string, f Filter, want ...string) {
			t.Helper()
			got, err := r.List(ctx, f, 0, 0)
			if err != nil {
				t.Fatal(err)
			}
			if !same(ids(got), want) {
				t.Errorf("%s: %v, beklenen %v", name, ids(got), want)
			}
			if n, _ := r.Count(ctx, f); n != len(want) {
				t.Errorf("%s: Count=%d, beklenen %d", name, n, len(want))
			}
		}
		// Sıra: key artan; anahtarsız kayıt (g1, key "") en başta, sonra acme, ali, beta.
		check("hepsi", Filter{}, "g1", "f1", "k1", "f2")
		check("kind firma", Filter{Kinds: []string{KindFirma}}, "f1", "f2")
		check("kind firma+kisi", Filter{Kinds: []string{KindFirma, KindKisi}}, "f1", "k1", "f2")
		check("durum celiskili", Filter{Statuses: []Status{StatusConflict}}, "g1")
		check("durum aktif+celiskili", Filter{Statuses: []Status{StatusActive, StatusConflict}}, "g1", "f1", "f2")
		check("kind+durum", Filter{Kinds: []string{KindKisi}, Statuses: []Status{StatusActive}})
		check("key", Filter{Key: "beta"}, "f2")
		check("yazar", Filter{AuthorID: "u1"}, "g1", "f1")
		check("metin duyarsız", Filter{Query: "ANGULAR"}, "f1")
		check("metin düz (regex değil)", Filter{Query: "a.c(", Kinds: []string{KindFirma}})
		check("birleşik", Filter{Kinds: []string{KindFirma}, AuthorID: "u2", Query: "firma"}, "f2")
	})

	t.Run("empty-allowlist-matches-nothing", func(t *testing.T) {
		// Faz 8 yetki filtresinin dayanağı: nil = süzme yok, boş dilim = hiçbiri izinli.
		r := newRepo()
		_ = r.Create(ctx, mk("p1", KindFirma, "Acme", "Acme", "u1", base))
		c := mk("p2", KindKisi, "Ali", "Ali", "u1", base)
		c.Embedding = []float32{1, 0}
		c.EmbedModel = "m"
		_ = r.Create(ctx, c)

		for name, f := range map[string]Filter{
			"kinds":    {Kinds: []string{}},
			"statuses": {Statuses: []Status{}},
		} {
			if got, _ := r.List(ctx, f, 0, 0); len(got) != 0 {
				t.Errorf("%s: boş izin listesi List'te hiçbir şey döndürmemeli: %v", name, ids(got))
			}
			if n, _ := r.Count(ctx, f); n != 0 {
				t.Errorf("%s: boş izin listesi Count=0 olmalı: %d", name, n)
			}
			if got, _ := r.Searchable(ctx, f); len(got) != 0 {
				t.Errorf("%s: boş izin listesi Searchable'da hiçbir şey döndürmemeli (yetki sızıntısı!): %v", name, ids(got))
			}
		}
		if n, _ := r.Count(ctx, Filter{}); n != 2 {
			t.Errorf("nil süzgeç hepsini saymalı: %d", n)
		}
	})

	t.Run("own-only-private-kinds", func(t *testing.T) {
		// Faz 8: OwnOnly türlerinde yalnızca subjectId == ViewerID olan kayıt görünür; ViewerID boşsa hiçbiri.
		r := newRepo()
		priv := func(id, subject string) *Claim {
			c := mk(id, "pdks", "", "Ali 08:10 geldi "+id, "u1", base)
			c.SubjectID = subject
			c.Embedding = []float32{1, 0}
			c.EmbedModel = "m"
			return c
		}
		_ = r.Create(ctx, priv("p-ali", "u-ali"))
		_ = r.Create(ctx, priv("p-veli", "u-veli"))
		_ = r.Create(ctx, priv("p-nobody", ""))
		_ = r.Create(ctx, mk("f1", KindFirma, "Acme", "Acme firma", "u1", base))

		check := func(name string, f Filter, want ...string) {
			t.Helper()
			for _, op := range []string{"List", "Count", "Searchable"} {
				var got []string
				switch op {
				case "List":
					cs, _ := r.List(ctx, f, 0, 0)
					got = ids(cs)
				case "Count":
					n, _ := r.Count(ctx, f)
					if n != len(want) {
						t.Errorf("%s/Count=%d, beklenen %d", name, n, len(want))
					}
					continue
				case "Searchable":
					cs, _ := r.Searchable(ctx, f)
					got = ids(cs)
				}
				if len(got) != len(want) {
					t.Errorf("%s/%s: %v, beklenen %v", name, op, got, want)
					continue
				}
				seen := map[string]bool{}
				for _, g := range got {
					seen[g] = true
				}
				for _, w := range want {
					if !seen[w] {
						t.Errorf("%s/%s: %v, beklenen %v", name, op, got, want)
					}
				}
			}
		}
		own := []string{"pdks", "personel"}
		check("ilgili kişi", Filter{OwnOnly: own, ViewerID: "u-ali"}, "f1", "p-ali")
		check("başka üye", Filter{OwnOnly: own, ViewerID: "u-zeynep"}, "f1")
		check("kimliksiz (fail-closed)", Filter{OwnOnly: own}, "f1")
		check("OwnOnly yok = patron", Filter{}, "f1", "p-ali", "p-veli", "p-nobody")
		check("boş OwnOnly = süzme yok", Filter{OwnOnly: []string{}}, "f1", "p-ali", "p-veli", "p-nobody")
		// Kinds süzgeciyle birlikte: yetkisiz kişi pdks isteyince boş döner, hata değil.
		check("kind=pdks yetkisiz", Filter{Kinds: []string{"pdks"}, OwnOnly: own, ViewerID: "u-zeynep"})
		check("kind=pdks ilgili", Filter{Kinds: []string{"pdks"}, OwnOnly: own, ViewerID: "u-veli"}, "p-veli")
		// Alan sözleşmesi: Fields ve SubjectID gidiş-dönüşte korunur.
		got, _ := r.Get(ctx, "p-ali")
		if got == nil || got.SubjectID != "u-ali" {
			t.Errorf("subjectId korunmadı: %+v", got)
		}
	})

	queryContract(t, newRepo)
}
