package claim

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

// queryContract RepositoryContract'ın ikinci yarısıdır: sayı tutarlılığı, ByKey ve Searchable.
func queryContract(t *testing.T, newRepo func() Repository) {
	t.Helper()
	ctx := context.Background()
	base := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)

	t.Run("140-firma-paging-complete", func(t *testing.T) {
		// Liste soruları "hepsini, eksiksiz" ister (spec: okuma akışı). Sayfalama kayıt atlamamalı,
		// tekrar etmemeli ve Count ile tutmalı.
		r := newRepo()
		const total = 140
		for i := 0; i < total; i++ {
			name := fmt.Sprintf("Firma %03d", i)
			// Bazı kayıtlar aynı zamanı paylaşır: sıra yine de kararlı olmalı.
			at := base.Add(time.Duration(i/10) * time.Minute)
			err := r.Create(ctx, &Claim{
				ID: fmt.Sprintf("id%03d", i), Text: name + " bir firmadir", Kind: KindFirma, Key: NormalizeKey(name),
				AuthorID: "u1", AuthorName: "Fikri", CreatedAt: at, UpdatedAt: at, Status: StatusActive,
			})
			if err != nil {
				t.Fatal(err)
			}
		}
		f := Filter{Kinds: []string{KindFirma}, Statuses: []Status{StatusActive}}
		if n, err := r.Count(ctx, f); err != nil || n != total {
			t.Fatalf("Count=%d (%v), beklenen %d", n, err, total)
		}
		seen := map[string]bool{}
		for off := 0; off < total+50; off += 50 {
			page, err := r.List(ctx, f, 50, off)
			if err != nil {
				t.Fatal(err)
			}
			for _, c := range page {
				if seen[c.ID] {
					t.Fatalf("sayfalamada tekrar eden kayıt: %s", c.ID)
				}
				seen[c.ID] = true
			}
		}
		if len(seen) != total {
			t.Errorf("sayfalamayla %d kayıt görüldü, beklenen %d (kayıt atlandı)", len(seen), total)
		}
	})

	t.Run("bykey", func(t *testing.T) {
		r := newRepo()
		mk := func(id, kind, key string, st Status, at time.Time) *Claim {
			return &Claim{ID: id, Text: "metin " + id, Kind: kind, Key: key, AuthorID: "u1",
				CreatedAt: at, UpdatedAt: at, Status: st, Embedding: []float32{1}, EmbedModel: "m"}
		}
		_ = r.Create(ctx, mk("old", KindFirma, "acme", StatusUpdated, base))
		_ = r.Create(ctx, mk("new", KindFirma, "acme", StatusActive, base.Add(time.Hour)))
		_ = r.Create(ctx, mk("bad", KindFirma, "acme", StatusInvalid, base.Add(30*time.Minute)))
		_ = r.Create(ctx, mk("other-kind", KindKisi, "acme", StatusActive, base))
		_ = r.Create(ctx, mk("other-key", KindFirma, "beta", StatusActive, base))
		_ = r.Create(ctx, mk("nokey1", KindGeneral, "", StatusActive, base))
		_ = r.Create(ctx, mk("nokey2", KindGeneral, "", StatusActive, base))

		got, err := r.ByKey(ctx, KindFirma, "acme")
		if err != nil {
			t.Fatal(err)
		}
		if len(got) != 3 || got[0].ID != "new" || got[1].ID != "bad" || got[2].ID != "old" {
			t.Errorf("ByKey tüm durumları yeni önce döndürmeli: %+v", got)
		}
		for _, c := range got {
			if len(c.Embedding) != 0 {
				t.Error("ByKey vektör taşımamalı")
			}
		}
		if got, _ := r.ByKey(ctx, KindFirma, "yok"); got == nil || len(got) != 0 {
			t.Errorf("eşleşmeyen anahtar boş (nil değil) dilim dönmeli: %v", got)
		}
		if got, _ := r.ByKey(ctx, KindGeneral, ""); len(got) != 0 {
			t.Errorf("boş anahtar hiçbir şeyle eşleşmemeli: %+v", got)
		}
	})

	t.Run("searchable-carries-vectors-and-filters", func(t *testing.T) {
		r := newRepo()
		a := &Claim{ID: "a", Text: "Acme", Kind: KindFirma, Key: "acme", AuthorID: "u1", CreatedAt: base, UpdatedAt: base,
			Status: StatusActive, Embedding: []float32{1, 2, 3}, EmbedModel: "bge-m3"}
		b := &Claim{ID: "b", Text: "Ali maasi", Kind: "personel", Key: "ali", AuthorID: "u1", CreatedAt: base, UpdatedAt: base,
			Status: StatusActive, Embedding: []float32{3, 2, 1}, EmbedModel: "bge-m3"}
		c := &Claim{ID: "c", Text: "Vektorsuz", Kind: KindFirma, Key: "zeta", AuthorID: "u1", CreatedAt: base, UpdatedAt: base,
			Status: StatusActive}
		for _, x := range []*Claim{a, b, c} {
			if err := r.Create(ctx, x); err != nil {
				t.Fatal(err)
			}
		}
		all, err := r.Searchable(ctx, Filter{})
		if err != nil || len(all) != 3 {
			t.Fatalf("3 aday beklenirdi: %d %v", len(all), err)
		}
		if !all[0].HasEmbedding("bge-m3") || len(all[0].Embedding) != 3 || all[0].Embedding[0] != 1 {
			t.Errorf("Searchable vektörü taşımalı: %+v", all[0])
		}
		// Yetki süzgeci aramadan ÖNCE: izinli olmayan tür adaylara hiç girmez.
		only, _ := r.Searchable(ctx, Filter{Kinds: []string{KindFirma}, Statuses: []Status{StatusActive}})
		for _, x := range only {
			if x.Kind != KindFirma {
				t.Errorf("izinsiz tür aday listesine girdi: %+v", x)
			}
		}
		if len(only) != 2 {
			t.Errorf("2 firma adayı beklenirdi: %d", len(only))
		}
		// Vektör sonradan yazılınca adaylara yansır.
		if err := r.SetEmbedding(ctx, "c", []float32{7, 7, 7}, "bge-m3"); err != nil {
			t.Fatal(err)
		}
		got, _ := r.Searchable(ctx, Filter{Key: "zeta"})
		if len(got) != 1 || !got[0].HasEmbedding("bge-m3") {
			t.Errorf("SetEmbedding Searchable'a yansımalı: %+v", got)
		}
	})
}

// AuditRepositoryContract her AuditRepository uygulamasının sağlaması gereken davranışlardır.
func AuditRepositoryContract(t *testing.T, newRepo func() AuditRepository) {
	t.Helper()
	ctx := context.Background()
	base := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)

	mk := func(id, claimID string, action Action, by string, at time.Time) *AuditEntry {
		return &AuditEntry{ID: id, ClaimID: claimID, Action: action, By: by, ByName: "Kisi " + by, At: at}
	}
	ids := func(es []AuditEntry) []string {
		out := make([]string, len(es))
		for i, e := range es {
			out[i] = e.ID
		}
		return out
	}

	t.Run("append-and-list-by-claim", func(t *testing.T) {
		r := newRepo()
		before := &Claim{ID: "c1", Text: "Eski metin", Kind: KindFirma, Key: "acme", AuthorID: "u1", Status: StatusActive,
			CreatedAt: base, UpdatedAt: base, Embedding: []float32{1, 2, 3}, EmbedModel: "m"}
		after := *before
		after.Text = "Yeni metin"
		after.Status = StatusUpdated

		e1 := mk("a1", "c1", ActionCreate, "u1", base)
		e1.After = before
		e2 := mk("a2", "c1", ActionUpdate, "u2", base.Add(time.Minute))
		e2.Before, e2.After = before, &after
		e3 := mk("a3", "c2", ActionDelete, "u2", base.Add(2*time.Minute))
		e3.Before = &after
		for _, e := range []*AuditEntry{e1, e2, e3} {
			if err := r.Append(ctx, e); err != nil {
				t.Fatal(err)
			}
		}
		got, err := r.ListByClaim(ctx, "c1", 0, 0)
		if err != nil || len(got) != 2 || got[0].ID != "a2" || got[1].ID != "a1" {
			t.Fatalf("c1 geçmişi yeni önce 2 kayıt olmalı: %v %v", ids(got), err)
		}
		if got[0].Before == nil || got[0].Before.Text != "Eski metin" || got[0].After == nil || got[0].After.Text != "Yeni metin" ||
			got[0].After.Status != StatusUpdated || got[0].By != "u2" || got[0].ByName != "Kisi u2" || !got[0].At.Equal(base.Add(time.Minute)) {
			t.Errorf("önceki/sonraki değer ve kim/ne zaman yazılmalı: %+v", got[0])
		}
		if got[1].Before != nil {
			t.Errorf("create kaydında Before boş kalmalı: %+v", got[1].Before)
		}
		// Vektör geçmişe taşınmaz (depo, çağıran unutsa bile atar).
		for _, e := range got {
			for _, snap := range []*Claim{e.Before, e.After} {
				if snap != nil && (len(snap.Embedding) != 0 || snap.EmbedModel != "") {
					t.Errorf("denetim kaydı vektör taşımamalı: %+v", snap)
				}
			}
		}
		if len(before.Embedding) != 3 {
			t.Error("depo çağıranın kaydını değiştirmemeli")
		}
		// Silinen kaydın geçmişi okunabilir (claim deposuyla ilişkisi yok).
		if del, _ := r.ListByClaim(ctx, "c2", 0, 0); len(del) != 1 || del[0].Before == nil || del[0].Before.Text != "Yeni metin" {
			t.Errorf("silme kaydı önceki değeri taşımalı: %+v", del)
		}
		if none, _ := r.ListByClaim(ctx, "yok", 0, 0); none == nil || len(none) != 0 {
			t.Errorf("geçmişi olmayan kayıt boş (nil değil) dilim dönmeli: %v", none)
		}
	})

	t.Run("paging-order-and-actor", func(t *testing.T) {
		r := newRepo()
		for i := 0; i < 5; i++ {
			by := "u1"
			if i%2 == 1 {
				by = "u2"
			}
			e := mk(fmt.Sprintf("e%d", i), "c1", ActionView, by, base.Add(time.Duration(i)*time.Minute))
			if err := r.Append(ctx, e); err != nil {
				t.Fatal(err)
			}
		}
		// Aynı anda iki kayıt: eşitlikte ID sırası.
		_ = r.Append(ctx, mk("tie-b", "c9", ActionView, "u3", base))
		_ = r.Append(ctx, mk("tie-a", "c9", ActionView, "u3", base))

		page, _ := r.ListByClaim(ctx, "c1", 2, 1)
		if len(page) != 2 || page[0].ID != "e3" || page[1].ID != "e2" {
			t.Errorf("sayfalama bozuk: %v", ids(page))
		}
		if beyond, _ := r.ListByClaim(ctx, "c1", 10, 99); beyond == nil || len(beyond) != 0 {
			t.Errorf("aşan offset boş (nil değil) dilim olmalı: %v", beyond)
		}
		actor, _ := r.ListByActor(ctx, "u2", 0, 0)
		if len(actor) != 2 || actor[0].ID != "e3" || actor[1].ID != "e1" {
			t.Errorf("ListByActor yeni önce yalnız u2'nin işlemleri olmalı: %v", ids(actor))
		}
		tie, _ := r.ListByActor(ctx, "u3", 0, 0)
		if len(tie) != 2 || tie[0].ID != "tie-a" || tie[1].ID != "tie-b" {
			t.Errorf("eşit zamanda ID sırası beklenirdi: %v", ids(tie))
		}
	})

	t.Run("validation-and-duplicate", func(t *testing.T) {
		r := newRepo()
		bad := map[string]*AuditEntry{
			"kimliksiz":    mk("", "c1", ActionCreate, "u1", base),
			"kayitsiz":     mk("x1", "", ActionCreate, "u1", base),
			"kullanicisiz": mk("x2", "c1", ActionCreate, "", base),
			"bilinmeyen":   mk("x3", "c1", Action("sihir"), "u1", base),
			"zamansiz":     mk("x4", "c1", ActionCreate, "u1", time.Time{}),
		}
		for name, e := range bad {
			err := r.Append(ctx, e)
			var ve *ValidationError
			if !errors.As(err, &ve) {
				t.Errorf("%s: ValidationError beklenirdi: %v", name, err)
			}
		}
		if err := r.Append(ctx, nil); err == nil {
			t.Error("nil kayıt reddedilmeli")
		}
		if got, _ := r.ListByClaim(ctx, "c1", 0, 0); len(got) != 0 {
			t.Errorf("geçersiz kayıtlar yazılmamalı: %v", ids(got))
		}
		if err := r.Append(ctx, mk("dup", "c1", ActionCreate, "u1", base)); err != nil {
			t.Fatal(err)
		}
		if err := r.Append(ctx, mk("dup", "c1", ActionDelete, "u1", base.Add(time.Hour))); !errors.Is(err, ErrDuplicate) {
			t.Errorf("aynı kimlik ErrDuplicate olmalı (geçmiş ezilemez): %v", err)
		}
		if got, _ := r.ListByClaim(ctx, "c1", 0, 0); len(got) != 1 || got[0].Action != ActionCreate {
			t.Errorf("ilk kayıt korunmalı: %+v", got)
		}
	})
}
