package knowledge

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

// RepositoryContract bir Repository uygulamasının sağlaması gereken davranışı doğrular.
// Hem bellek içi hem MongoDB deposu AYNI testten geçer: davranış farkı depo seçimine bağlı
// sessiz bir hata olurdu. Üretim dosyasındadır (test dosyasında olsaydı mongostore erişemezdi).
//
// newRepo her çağrıda TEMİZ bir depo döndürmelidir.
func RepositoryContract(t *testing.T, newRepo func() Repository) {
	t.Helper()
	ctx := context.Background()
	base := time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC)

	doc := func(id, user, hash string, minute int) Document {
		return Document{
			ID: id, UserID: user, Title: "Belge " + id, Hash: hash,
			Tags: []string{"genel"}, Status: StatusIndexing, ChunkCount: 3,
			CreatedAt: base.Add(time.Duration(minute) * time.Minute),
			UpdatedAt: base.Add(time.Duration(minute) * time.Minute),
		}
	}
	chunk := func(id, docID, user string, idx int) Chunk {
		return Chunk{
			ID: id, DocumentID: docID, UserID: user, Index: idx,
			Text: fmt.Sprintf("parça %d", idx), Title: "Belge " + docID,
			Tags: []string{"genel"}, CreatedAt: base,
		}
	}

	t.Run("belge gidiş-dönüş ve kopya yalıtımı", func(t *testing.T) {
		r := newRepo()
		d := doc("d1", "u1", "h1", 0)
		d.ProjectID, d.Source, d.Filename, d.ContentType = "p1", "elle", "a.md", "text/markdown"
		if err := r.CreateDocument(ctx, &d); err != nil {
			t.Fatal(err)
		}
		got, err := r.GetDocument(ctx, "d1")
		if err != nil {
			t.Fatal(err)
		}
		if got.Title != d.Title || got.Hash != "h1" || got.ProjectID != "p1" || got.Source != "elle" ||
			got.Filename != "a.md" || got.ContentType != "text/markdown" ||
			got.Status != StatusIndexing || got.ChunkCount != 3 || len(got.Tags) != 1 || got.Tags[0] != "genel" ||
			!got.CreatedAt.Equal(d.CreatedAt) {
			t.Fatalf("alanlar korunmadı: %+v", got)
		}
		got.Tags[0] = "bozuldu"
		again, _ := r.GetDocument(ctx, "d1")
		if again.Tags[0] != "genel" {
			t.Fatal("dışarı verilen kayıt depoyla paylaşılıyor")
		}
	})

	t.Run("olmayan belge ErrNotFound", func(t *testing.T) {
		r := newRepo()
		if _, err := r.GetDocument(ctx, "yok"); !errors.Is(err, ErrNotFound) {
			t.Fatalf("Get: %v", err)
		}
		if _, err := r.GetDocumentByHash(ctx, "u1", "yok"); !errors.Is(err, ErrNotFound) {
			t.Fatalf("GetByHash: %v", err)
		}
		d := doc("yok", "u1", "h", 0)
		if err := r.UpdateDocument(ctx, &d); !errors.Is(err, ErrNotFound) {
			t.Fatalf("Update: %v", err)
		}
		if err := r.DeleteDocument(ctx, "yok"); !errors.Is(err, ErrNotFound) {
			t.Fatalf("Delete: %v", err)
		}
	})

	t.Run("aynı içerik yalnızca kullanıcı içinde benzersiz", func(t *testing.T) {
		r := newRepo()
		a := doc("d1", "u1", "H", 0)
		if err := r.CreateDocument(ctx, &a); err != nil {
			t.Fatal(err)
		}
		dupHash := doc("d2", "u1", "H", 1)
		if err := r.CreateDocument(ctx, &dupHash); !errors.Is(err, ErrDuplicate) {
			t.Fatalf("aynı hash: %v", err)
		}
		dupID := doc("d1", "u1", "baska", 1)
		if err := r.CreateDocument(ctx, &dupID); !errors.Is(err, ErrDuplicate) {
			t.Fatalf("aynı kimlik: %v", err)
		}
		other := doc("d3", "u2", "H", 2) // başka kullanıcı aynı içeriği yükleyebilir
		if err := r.CreateDocument(ctx, &other); err != nil {
			t.Fatalf("başka kullanıcı: %v", err)
		}
		got, err := r.GetDocumentByHash(ctx, "u1", "H")
		if err != nil || got.ID != "d1" {
			t.Fatalf("GetByHash: %v %+v", err, got)
		}
	})

	t.Run("güncelleme kaydı tamamen değiştirir", func(t *testing.T) {
		r := newRepo()
		d := doc("d1", "u1", "h", 0)
		_ = r.CreateDocument(ctx, &d)
		d.Status, d.EmbeddedCount, d.Tags = StatusReady, 3, []string{"x", "y"}
		if err := r.UpdateDocument(ctx, &d); err != nil {
			t.Fatal(err)
		}
		got, _ := r.GetDocument(ctx, "d1")
		if got.Status != StatusReady || got.EmbeddedCount != 3 || len(got.Tags) != 2 {
			t.Fatalf("güncelleme yansımadı: %+v", got)
		}
	})

	t.Run("liste süzgeçleri, sıra, sayfalama ve yalıtım", func(t *testing.T) {
		r := newRepo()
		mk := func(id, user, hash string, minute int, mod func(*Document)) {
			d := doc(id, user, hash, minute)
			if mod != nil {
				mod(&d)
			}
			if err := r.CreateDocument(ctx, &d); err != nil {
				t.Fatal(err)
			}
		}
		mk("d1", "u1", "h1", 1, func(d *Document) { d.Title = "Rapor Ocak"; d.ProjectID = "p1"; d.Tags = []string{"rapor"} })
		mk("d2", "u1", "h2", 2, func(d *Document) {
			d.Title = "Rapor Şubat"
			d.ProjectID = "p2"
			d.Tags = []string{"rapor", "mali"}
			d.Status = StatusReady
		})
		mk("d3", "u1", "h3", 3, func(d *Document) { d.Title = "Notlar"; d.Source = "https://example.com/Kilavuz" })
		mk("d4", "u1", "h4", 4, nil)
		mk("d5", "u2", "h5", 5, nil)

		ids := func(f DocumentFilter, limit, offset int) []string {
			t.Helper()
			ds, err := r.ListDocuments(ctx, f, limit, offset)
			if err != nil {
				t.Fatal(err)
			}
			if ds == nil {
				t.Fatal("sonuç nil olmamalı")
			}
			out := make([]string, 0, len(ds))
			for _, d := range ds {
				out = append(out, d.ID)
			}
			return out
		}
		eq := func(name string, got []string, want ...string) {
			t.Helper()
			if fmt.Sprint(got) != fmt.Sprint(want) {
				t.Fatalf("%s: got %v, want %v", name, got, want)
			}
		}

		eq("kullanıcı + en yeni önce", ids(DocumentFilter{UserID: "u1"}, 0, 0), "d4", "d3", "d2", "d1")
		eq("başka kullanıcı", ids(DocumentFilter{UserID: "u2"}, 0, 0), "d5")
		eq("boş kullanıcı HİÇBİR ŞEY görmez", ids(DocumentFilter{}, 0, 0))
		eq("proje", ids(DocumentFilter{UserID: "u1", ProjectID: "p2"}, 0, 0), "d2")
		eq("durum", ids(DocumentFilter{UserID: "u1", Status: StatusReady}, 0, 0), "d2")
		eq("etiket", ids(DocumentFilter{UserID: "u1", Tag: "rapor"}, 0, 0), "d2", "d1")
		eq("başlıkta arama (büyük/küçük duyarsız)", ids(DocumentFilter{UserID: "u1", Query: "rapor"}, 0, 0), "d2", "d1")
		eq("kaynakta arama", ids(DocumentFilter{UserID: "u1", Query: "kilavuz"}, 0, 0), "d3")
		eq("düz metin (regex değil)", ids(DocumentFilter{UserID: "u1", Query: "a.c("}, 0, 0))
		eq("limit", ids(DocumentFilter{UserID: "u1"}, 2, 0), "d4", "d3")
		eq("offset", ids(DocumentFilter{UserID: "u1"}, 2, 2), "d2", "d1")
		eq("offset taşması", ids(DocumentFilter{UserID: "u1"}, 2, 10))

		n, err := r.CountDocuments(ctx, DocumentFilter{UserID: "u1", Tag: "rapor"})
		if err != nil || n != 2 {
			t.Fatalf("Count: %d %v", n, err)
		}
		if n, _ := r.CountDocuments(ctx, DocumentFilter{}); n != 0 {
			t.Fatalf("boş kullanıcı sayımı %d, 0 beklenirdi", n)
		}
	})

	t.Run("parçalar: sıra, sayfalama, embedding", func(t *testing.T) {
		r := newRepo()
		cs := []Chunk{chunk("c2", "d1", "u1", 2), chunk("c0", "d1", "u1", 0), chunk("c1", "d1", "u1", 1)}
		if err := r.AddChunks(ctx, cs); err != nil {
			t.Fatal(err)
		}
		if err := r.AddChunks(ctx, []Chunk{chunk("c0", "d1", "u1", 9)}); !errors.Is(err, ErrDuplicate) {
			t.Fatalf("aynı kimlik: %v", err)
		}
		got, err := r.ChunksOfDocument(ctx, "d1", 0, 0)
		if err != nil || len(got) != 3 || got[0].ID != "c0" || got[1].ID != "c1" || got[2].ID != "c2" {
			t.Fatalf("sıra yanlış: %v %+v", err, got)
		}
		page, _ := r.ChunksOfDocument(ctx, "d1", 1, 1)
		if len(page) != 1 || page[0].ID != "c1" {
			t.Fatalf("sayfa yanlış: %+v", page)
		}
		if empty, _ := r.ChunksOfDocument(ctx, "yok", 0, 0); empty == nil || len(empty) != 0 {
			t.Fatalf("olmayan belge: %v", empty)
		}

		if got[0].HasEmbedding("") {
			t.Fatal("henüz embedding yok")
		}
		if err := r.SetChunkEmbedding(ctx, "c1", []float32{0.5, -1, 2}, "model-a"); err != nil {
			t.Fatal(err)
		}
		again, _ := r.ChunksOfDocument(ctx, "d1", 0, 0)
		c1 := again[1]
		if !c1.HasEmbedding("model-a") || c1.HasEmbedding("model-b") ||
			len(c1.Embedding) != 3 || c1.Embedding[1] != -1 || c1.EmbedModel != "model-a" {
			t.Fatalf("embedding yazılmadı: %+v", c1)
		}
		if err := r.SetChunkEmbedding(ctx, "yok", []float32{1}, "m"); !errors.Is(err, ErrNotFound) {
			t.Fatalf("olmayan parça: %v", err)
		}
	})

	t.Run("arama adayları: kullanıcı, proje, belge, etiket süzgeci", func(t *testing.T) {
		r := newRepo()
		mk := func(id, docID, user, project string, tags ...string) Chunk {
			c := chunk(id, docID, user, 0)
			c.ProjectID, c.Tags = project, tags
			c.Embedding, c.EmbedModel = []float32{1, 0}, "m"
			return c
		}
		_ = r.AddChunks(ctx, []Chunk{
			mk("a1", "d1", "u1", "p1", "x"),
			mk("a2", "d2", "u1", "p2", "y"),
			mk("a3", "d2", "u1", "p2", "x", "y"),
			mk("b1", "d9", "u2", "p1", "x"),
		})
		ids := func(f ChunkFilter) string {
			t.Helper()
			cs, err := r.SearchableChunks(ctx, f)
			if err != nil {
				t.Fatal(err)
			}
			set := map[string]bool{}
			for _, c := range cs {
				set[c.ID] = true
			}
			s := ""
			for _, id := range []string{"a1", "a2", "a3", "b1"} {
				if set[id] {
					s += id + " "
				}
			}
			return s
		}
		check := func(name string, f ChunkFilter, want string) {
			t.Helper()
			if got := ids(f); got != want {
				t.Fatalf("%s: got %q, want %q", name, got, want)
			}
		}
		check("kullanıcı", ChunkFilter{UserID: "u1"}, "a1 a2 a3 ")
		check("boş kullanıcı HİÇBİR ŞEY görmez", ChunkFilter{}, "")
		check("proje", ChunkFilter{UserID: "u1", ProjectID: "p2"}, "a2 a3 ")
		check("belge", ChunkFilter{UserID: "u1", DocumentIDs: []string{"d1"}}, "a1 ")
		check("başkasının belgesi", ChunkFilter{UserID: "u1", DocumentIDs: []string{"d9"}}, "")
		check("etiket (herhangi biri)", ChunkFilter{UserID: "u1", Tags: []string{"y", "zzz"}}, "a2 a3 ")
		cs, _ := r.SearchableChunks(ctx, ChunkFilter{UserID: "u1", DocumentIDs: []string{"d1"}})
		if len(cs) != 1 || len(cs[0].Embedding) != 2 || cs[0].EmbedModel != "m" {
			t.Fatalf("adaylar embedding taşımalı: %+v", cs)
		}
	})

	t.Run("belge silinince parçaları da gider", func(t *testing.T) {
		r := newRepo()
		d1, d2 := doc("d1", "u1", "h1", 0), doc("d2", "u1", "h2", 1)
		_ = r.CreateDocument(ctx, &d1)
		_ = r.CreateDocument(ctx, &d2)
		_ = r.AddChunks(ctx, []Chunk{chunk("c1", "d1", "u1", 0), chunk("c2", "d1", "u1", 1), chunk("c3", "d2", "u1", 0)})

		if err := r.DeleteDocument(ctx, "d1"); err != nil {
			t.Fatal(err)
		}
		if _, err := r.GetDocument(ctx, "d1"); !errors.Is(err, ErrNotFound) {
			t.Fatalf("belge silinmeliydi: %v", err)
		}
		if cs, _ := r.ChunksOfDocument(ctx, "d1", 0, 0); len(cs) != 0 {
			t.Fatalf("parçalar silinmeliydi: %+v", cs)
		}
		all, _ := r.SearchableChunks(ctx, ChunkFilter{UserID: "u1"})
		if len(all) != 1 || all[0].ID != "c3" {
			t.Fatalf("başka belgenin parçası korunmalı: %+v", all)
		}
		if err := r.DeleteChunks(ctx, "d2"); err != nil {
			t.Fatal(err)
		}
		if err := r.DeleteChunks(ctx, "d2"); err != nil { // idempotent
			t.Fatalf("ikinci DeleteChunks: %v", err)
		}
		if err := r.SetChunkEmbedding(ctx, "c3", []float32{1}, "m"); !errors.Is(err, ErrNotFound) {
			t.Fatalf("silinmiş parçaya yazma: %v", err)
		}
	})

	t.Run("bekleyen belgeler kullanıcıdan bağımsız, en eski önce", func(t *testing.T) {
		r := newRepo()
		a, b, c := doc("d1", "u1", "h1", 2), doc("d2", "u2", "h2", 1), doc("d3", "u1", "h3", 3)
		c.Status = StatusReady
		for _, d := range []*Document{&a, &b, &c} {
			if err := r.CreateDocument(ctx, d); err != nil {
				t.Fatal(err)
			}
		}
		got, err := r.PendingDocuments(ctx)
		if err != nil || got == nil || len(got) != 2 || got[0].ID != "d2" || got[1].ID != "d1" {
			t.Fatalf("PendingDocuments: %v %+v", err, got)
		}
	})

	t.Run("eşzamanlı aynı içerik: yalnızca biri kazanır", func(t *testing.T) {
		r := newRepo()
		const n = 8
		errs := make([]error, n)
		var wg sync.WaitGroup
		for i := 0; i < n; i++ {
			wg.Add(1)
			go func(i int) {
				defer wg.Done()
				d := doc(fmt.Sprintf("d%d", i), "u1", "AYNI", i)
				errs[i] = r.CreateDocument(ctx, &d)
			}(i)
		}
		wg.Wait()
		ok := 0
		for _, err := range errs {
			switch {
			case err == nil:
				ok++
			case !errors.Is(err, ErrDuplicate):
				t.Fatalf("beklenmeyen hata: %v", err)
			}
		}
		if ok != 1 {
			t.Fatalf("%d kayıt başarılı, 1 beklenirdi", ok)
		}
	})
}
