package knowledge

import (
	"context"
	"errors"
	"math"
	"testing"
)

// fakeEmb sabit vektör döndüren embedding sağlayıcısıdır.
type fakeEmb struct{ vec []float32 }

func (f fakeEmb) Embed(context.Context, string) ([]float32, error) { return f.vec, nil }

// vecRepo MemoryRepo'yu VectorSearcher ile genişletir; hangi yolun çalıştığını sayaçlarla gösterir.
type vecRepo struct {
	*MemoryRepo
	hits        []VectorHit
	extra       []Chunk
	vecErr      error
	vecCalls    int
	extraCalls  int
	scanCalls   int
	lastQuery   VectorQuery
	lastExtraFn ChunkFilter
}

func (r *vecRepo) VectorSearch(_ context.Context, q VectorQuery) ([]VectorHit, error) {
	r.vecCalls++
	r.lastQuery = q
	if r.vecErr != nil {
		return nil, r.vecErr
	}
	return r.hits, nil
}

func (r *vecRepo) UnembeddedChunks(_ context.Context, f ChunkFilter, _ string, _ int) ([]Chunk, error) {
	r.extraCalls++
	r.lastExtraFn = f
	return r.extra, nil
}

func (r *vecRepo) SearchableChunks(ctx context.Context, f ChunkFilter) ([]Chunk, error) {
	r.scanCalls++
	return r.MemoryRepo.SearchableChunks(ctx, f)
}

func newVecService(t *testing.T, r *vecRepo) *Service {
	t.Helper()
	s := NewService(r, nil, Options{})
	s.SetEmbedder(fakeEmb{vec: []float32{1, 0}}, "m")
	t.Cleanup(func() { _ = s.Close(context.Background()) })
	return s
}

func chunkOf(id, doc, text string) Chunk {
	return Chunk{ID: id, DocumentID: doc, UserID: "u1", Text: text, Title: "T"}
}

func near(a, b float64) bool { return math.Abs(a-b) < 1e-9 }

func TestSearchUsesVectorPathAndSkipsScan(t *testing.T) {
	r := &vecRepo{MemoryRepo: NewMemoryRepo(), hits: []VectorHit{
		{Chunk: chunkOf("c1", "d1", "abc xyz"), Similarity: 0.9},
	}}
	s := newVecService(t, r)

	hits, err := s.Search(context.Background(), SearchOptions{UserID: "u1", Query: "abc", Mode: ModeSemantic})
	if err != nil {
		t.Fatal(err)
	}
	if len(hits) != 1 || hits[0].ChunkID != "c1" || !near(hits[0].Score, 0.9) {
		t.Fatalf("beklenmeyen sonuç: %+v", hits)
	}
	if r.vecCalls != 1 || r.scanCalls != 0 {
		t.Fatalf("vektör yolu çalışmalı, tarama çalışmamalı: vec=%d scan=%d", r.vecCalls, r.scanCalls)
	}
	if r.lastQuery.Model != "m" || r.lastQuery.Filter.UserID != "u1" {
		t.Fatalf("model ve kullanıcı süzgece geçmeli: %+v", r.lastQuery)
	}
	if r.lastQuery.Limit < vectorMinPool {
		t.Fatalf("aday havuzu en az %d olmalı: %d", vectorMinPool, r.lastQuery.Limit)
	}
	if r.extraCalls != 0 {
		t.Fatal("semantic modda vektörsüz parça aranmamalı")
	}
}

func TestSearchHybridWeightsVectorAndKeyword(t *testing.T) {
	r := &vecRepo{MemoryRepo: NewMemoryRepo(), hits: []VectorHit{
		{Chunk: chunkOf("c1", "d1", "abc xyz"), Similarity: 0.5},
	}}
	s := newVecService(t, r)

	hits, err := s.Search(context.Background(), SearchOptions{UserID: "u1", Query: "abc"})
	if err != nil {
		t.Fatal(err)
	}
	// %80 × 0.5 + %20 × 1.0 (sorgu sözcüğü metinde var); "abc" 4 karakterden kısa: ifade bonusu yok.
	want := semanticWeight*0.5 + keywordWeight*1.0
	if len(hits) != 1 || !near(hits[0].Score, want) {
		t.Fatalf("hibrit skor %v bekleniyordu: %+v", want, hits)
	}
}

func TestSearchHybridFindsUnembeddedChunksByKeyword(t *testing.T) {
	r := &vecRepo{
		MemoryRepo: NewMemoryRepo(),
		hits:       []VectorHit{{Chunk: chunkOf("c1", "d1", "ilgisiz metin"), Similarity: 0.8}},
		extra:      []Chunk{chunkOf("c2", "d2", "abc anahtar kelime")},
	}
	s := newVecService(t, r)

	hits, err := s.Search(context.Background(), SearchOptions{UserID: "u1", Query: "abc"})
	if err != nil {
		t.Fatal(err)
	}
	got := map[string]float64{}
	for _, h := range hits {
		got[h.ChunkID] = h.Score
	}
	if !near(got["c2"], 1.0) {
		t.Fatalf("vektörsüz parça yalnızca anahtar kelimeyle (1.0) bulunmalı: %+v", hits)
	}
	if _, ok := got["c1"]; !ok {
		t.Fatalf("vektör sonucu da korunmalı: %+v", hits)
	}
	if r.extraCalls != 1 {
		t.Fatalf("hibritte vektörsüz parçalar bir kez sorgulanmalı: %d", r.extraCalls)
	}
}

func TestSearchVectorPathSkipsDuplicateOfSupplement(t *testing.T) {
	c := chunkOf("c1", "d1", "abc xyz")
	r := &vecRepo{MemoryRepo: NewMemoryRepo(),
		hits:  []VectorHit{{Chunk: c, Similarity: 0.6}},
		extra: []Chunk{c}, // aynı parça iki kaynaktan gelirse tek kez sayılır
	}
	s := newVecService(t, r)
	hits, err := s.Search(context.Background(), SearchOptions{UserID: "u1", Query: "abc"})
	if err != nil {
		t.Fatal(err)
	}
	if len(hits) != 1 {
		t.Fatalf("aynı parça iki kez dönmemeli: %+v", hits)
	}
}

func TestSearchVectorMinScoreFilters(t *testing.T) {
	r := &vecRepo{MemoryRepo: NewMemoryRepo(), hits: []VectorHit{
		{Chunk: chunkOf("c1", "d1", "abc"), Similarity: 0.2},
		{Chunk: chunkOf("c2", "d2", "abc"), Similarity: 0.9},
	}}
	s := newVecService(t, r)
	hits, err := s.Search(context.Background(), SearchOptions{UserID: "u1", Query: "abc", Mode: ModeSemantic, MinScore: 0.5})
	if err != nil {
		t.Fatal(err)
	}
	if len(hits) != 1 || hits[0].ChunkID != "c2" {
		t.Fatalf("yalnızca eşiği geçen sonuç kalmalı: %+v", hits)
	}
}

func TestSearchFallsBackToScanWhenVectorUnavailable(t *testing.T) {
	for name, vecErr := range map[string]error{
		"hazır değil": ErrVectorUnavailable,
		"gerçek hata": errors.New("mongot çöktü"),
		"sarmalanmış": errors.Join(ErrVectorUnavailable, errors.New("ayrıntı")),
	} {
		t.Run(name, func(t *testing.T) {
			mem := NewMemoryRepo()
			c := chunkOf("c1", "d1", "abc xyz")
			c.Embedding, c.EmbedModel = []float32{1, 0}, "m"
			if err := mem.AddChunks(context.Background(), []Chunk{c}); err != nil {
				t.Fatal(err)
			}
			r := &vecRepo{MemoryRepo: mem, vecErr: vecErr}
			s := newVecService(t, r)

			hits, err := s.Search(context.Background(), SearchOptions{UserID: "u1", Query: "abc", Mode: ModeSemantic})
			if err != nil {
				t.Fatalf("düşüş HATA olmamalı: %v", err)
			}
			if r.scanCalls != 1 || len(hits) != 1 || !near(hits[0].Score, 1.0) {
				t.Fatalf("Go tarafı tarama sonucu bekleniyordu: scan=%d hits=%+v", r.scanCalls, hits)
			}
		})
	}
}

func TestSearchKeywordModeNeverTouchesVectorPath(t *testing.T) {
	mem := NewMemoryRepo()
	if err := mem.AddChunks(context.Background(), []Chunk{chunkOf("c1", "d1", "abc xyz")}); err != nil {
		t.Fatal(err)
	}
	r := &vecRepo{MemoryRepo: mem}
	s := newVecService(t, r)
	hits, err := s.Search(context.Background(), SearchOptions{UserID: "u1", Query: "abc", Mode: ModeKeyword})
	if err != nil || len(hits) != 1 {
		t.Fatalf("anahtar kelime sonucu bekleniyordu: %v %+v", err, hits)
	}
	if r.vecCalls != 0 {
		t.Fatalf("keyword modunda vektör arama çağrılmamalı: %d", r.vecCalls)
	}
}

func TestSearchForwardsFilterToVectorQuery(t *testing.T) {
	r := &vecRepo{MemoryRepo: NewMemoryRepo()}
	s := newVecService(t, r)
	_, err := s.Search(context.Background(), SearchOptions{
		UserID: "u1", Query: "abc", ProjectID: "p1", DocumentIDs: []string{"d1"}, Tags: []string{" Etiket "},
	})
	if err != nil {
		t.Fatal(err)
	}
	f := r.lastQuery.Filter
	if f.UserID != "u1" || f.ProjectID != "p1" || len(f.DocumentIDs) != 1 || len(f.Tags) != 1 || f.Tags[0] != "etiket" {
		t.Fatalf("süzgeç eksik/yanlış iletildi: %+v", f)
	}
}
