package claim

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

	"fiko-ai/internal/llm"
)

// ---- sahteler (read_ öneki: service_test.go'daki sahtelerle çakışmasın) ----

type readLLM struct {
	llm.Provider
	reply string
	err   error
	calls int
}

func (f *readLLM) Generate(ctx context.Context, r llm.Request) (llm.Result, error) {
	f.calls++
	if f.err != nil {
		return llm.Result{}, f.err
	}
	return llm.Result{Content: f.reply}, nil
}

type readEmb struct {
	vecs  map[string][]float32
	err   error
	calls int
}

func (e *readEmb) Embed(ctx context.Context, text string) ([]float32, error) {
	e.calls++
	if e.err != nil {
		return nil, e.err
	}
	if v, ok := e.vecs[text]; ok {
		return v, nil
	}
	return []float32{0, 0, 1}, nil
}

const readModel = "test-embed"

// Faz 8 test aktörleri: patron her şeyi, üye yalnızca ekip türlerini, "ilgili kişi" ayrıca kendi özel kaydını görür.
var (
	readPatron = Actor{ID: "u_patron", Name: "Fikri", Role: RolePatron}
	readUye    = Actor{ID: "u_uye", Name: "Veli", Role: RoleUye}
	readAli    = Actor{ID: "u_ali", Name: "Ali", Role: RoleUye}
)

func readClaim(id, kind, text string, st Status, vec []float32) Claim {
	return Claim{
		ID: id, Kind: kind, Text: text, Key: NormalizeKey(text), AuthorID: "u1", AuthorName: "Fikri",
		CreatedAt: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC),
		Status: st, Embedding: vec, EmbedModel: readModel,
	}
}

func readRepo(t *testing.T, cs ...Claim) *MemoryRepo {
	t.Helper()
	repo := NewMemoryRepo()
	for i := range cs {
		if err := repo.Create(context.Background(), &cs[i]); err != nil {
			t.Fatalf("Create %s: %v", cs[i].ID, err)
		}
	}
	return repo
}

func newReader(repo Repository, p llm.Provider, emb llm.Embedder) *Reader {
	return NewReader(repo, p, emb, ReadConfig{Model: "m", EmbedModel: readModel, TopK: 3, PageSize: 50})
}

// ---- ParseIntent ----

func TestParseIntent(t *testing.T) {
	cases := []struct {
		raw     string
		want    Intent
		wantErr bool
	}{
		{`{"type":"liste","kind":"firma"}`, Intent{Type: IntentList, Kind: "firma"}, false},
		{"```json\n{\"type\":\"Liste\",\"kind\":\"Kişi\"}\n```", Intent{Type: IntentList, Kind: "kisi"}, false},
		{`{"type":"liste","kind":"izin talebi"}`, Intent{Type: IntentList, Kind: "izin_talebi"}, false},
		{`{"type":"soru","kind":"firma"}`, Intent{Type: IntentQuestion}, false},
		{`{"type":"liste","kind":""}`, Intent{Type: IntentQuestion}, false},                // türsüz liste: aramaya düş
		{`{"type":"liste","kind":"1firma"}`, Intent{Type: IntentQuestion}, false},          // harfle başlamıyor
		{`{"type":"liste","kind":"../etc"}`, Intent{Type: IntentList, Kind: "etc"}, false}, // noktalama temizlenir, zararsız
		{`{"type":"liste","kind":"musteri"}`, Intent{Type: IntentList, Kind: "firma"}, false},
		{`{"type":"liste","kind":"Müşteriler"}`, Intent{Type: IntentList, Kind: "firma"}, false},
		{`{"type":"liste","kind":"firmalar"}`, Intent{Type: IntentList, Kind: "firma"}, false},
		{`{"type":"liste","kind":"çalışanlar"}`, Intent{Type: IntentList, Kind: "personel"}, false},
		{`{"type":"liste","kind":"pdks"}`, Intent{Type: IntentList, Kind: "pdks"}, false}, // bilinmeyen tür serbest etiket: olduğu gibi
		{`öyle olsun`, Intent{}, true},
		{`{"type":"başka"}`, Intent{}, true},
		{``, Intent{}, true},
	}
	for _, c := range cases {
		got, err := ParseIntent(c.raw)
		if (err != nil) != c.wantErr || got != c.want {
			t.Errorf("ParseIntent(%q) = %+v, %v; beklenen %+v hata=%v", c.raw, got, err, c.want, c.wantErr)
		}
	}
}

// ---- List ----

func TestListIsCompleteSortedAndCounted(t *testing.T) {
	var cs []Claim
	for i := 0; i < 140; i++ {
		cs = append(cs, readClaim(fmt.Sprintf("c%03d", i), KindFirma, fmt.Sprintf("Firma %03d Ltd.", i), StatusActive, nil))
	}
	cs = append(cs,
		readClaim("old", KindFirma, "Eski Firma", StatusUpdated, nil),
		readClaim("bad", KindFirma, "Geçersiz Firma", StatusInvalid, nil),
		readClaim("cf", KindFirma, "Çelişkili Firma", StatusConflict, nil),
		readClaim("kisi1", KindKisi, "Ali Veli", StatusActive, nil),
	)
	rd := newReader(readRepo(t, cs...), nil, nil) // PageSize 50: 3 sayfa

	res, err := rd.List(context.Background(), KindFirma, readPatron)
	if err != nil {
		t.Fatal(err)
	}
	if res.Total != 141 || len(res.Items) != 141 {
		t.Fatalf("toplam %d / öğe %d, beklenen 141 (140 aktif + 1 çelişkili)", res.Total, len(res.Items))
	}
	if res.Conflicts != 1 {
		t.Errorf("çelişkili sayısı %d, beklenen 1", res.Conflicts)
	}
	for _, bad := range []string{"Eski Firma", "Geçersiz Firma", "Ali Veli"} {
		if strings.Contains(res.Text, bad) {
			t.Errorf("listede olmaması gereken kayıt var: %q", bad)
		}
	}
	if !strings.Contains(res.Text, "Toplam: 141 firma") {
		t.Errorf("toplam satırı yok:\n%s", tail(res.Text, 300))
	}
	if !strings.Contains(res.Text, "Çelişkili Firma (yazan: Fikri, 2026-10-05) [çelişkili]") {
		t.Errorf("çelişkili kayıt işaretli değil")
	}
	if got := strings.Count(res.Text, "\n"); got < 141 {
		t.Errorf("satır sayısı %d, en az 141 bekleniyordu", got)
	}
	// Kararlı sıra: anahtar artan ("celiskili firma" < "firma 000 ltd").
	if !strings.HasPrefix(res.Text, "1. Çelişkili Firma") {
		t.Errorf("sıra anahtar artan değil: %q", clipRunes(res.Text, 60))
	}
}

func TestListEmptyAndDenied(t *testing.T) {
	ctx := context.Background()
	pdks := readClaim("p1", "pdks", "Ali 08:10 geldi", StatusActive, nil)
	pdks.SubjectID = "u_ali"
	rd := newReader(readRepo(t, pdks), nil, nil)

	empty, err := rd.List(ctx, KindFirma, readPatron)
	if err != nil || empty.Total != 0 || empty.Text != "Kayıtlı firma bulunamadı." {
		t.Fatalf("boş liste: %+v %v", empty, err)
	}
	// Yetkisiz: aynı "bulunamadı" metni, hiçbir şey ifşa edilmez.
	for name, who := range map[string]Actor{"üye": readUye, "kimliksiz": {}, "kimliksiz patron": {Role: RolePatron}} {
		denied, err := rd.List(ctx, "pdks", who)
		if err != nil {
			t.Fatal(err)
		}
		if !denied.Denied || denied.Total != 0 || len(denied.Items) != 0 || strings.Contains(denied.Text, "Ali") {
			t.Errorf("%s için yetkisiz liste sızdırdı: %+v", name, denied)
		}
	}
	for name, who := range map[string]Actor{"patron": readPatron, "ilgili kişi": readAli} {
		ok, _ := rd.List(ctx, "pdks", who)
		if ok.Total != 1 || ok.Denied {
			t.Errorf("%s kaydı görmeliydi: %+v", name, ok)
		}
	}
}

// ---- Search ----

func TestSearchPermissionFilterAppliesBeforeSearch(t *testing.T) {
	near := []float32{1, 0, 0}
	secret := readClaim("secret", "pdks", "Ali geç kaldı", StatusActive, near) // en yakın ama özel
	secret.SubjectID = "u_ali"
	noSubject := readClaim("nosubj", "personel", "Ali maaş bilgisi", StatusActive, near) // ilgili kişi atanmamış
	rd := newReader(readRepo(t,
		secret, noSubject,
		readClaim("ok", KindFirma, "ACME müşteri", StatusActive, []float32{0.8, 0.6, 0}),
	), nil, &readEmb{vecs: map[string][]float32{"Ali ne zaman geldi?": near}})
	ctx := context.Background()
	q := "Ali ne zaman geldi?"

	ids := func(hs []Hit) string {
		var out []string
		for _, h := range hs {
			out = append(out, h.Claim.ID)
		}
		return strings.Join(out, ",")
	}
	for name, c := range map[string]struct {
		who  Actor
		want string
	}{
		"üye":         {readUye, "ok"},
		"kimliksiz":   {Actor{}, "ok"},
		"ilgili kişi": {readAli, "secret,ok"}, // yalnızca kendi kaydı; subjectId'siz personel kaydı görünmez
		"patron":      {readPatron, "nosubj,secret,ok"},
	} {
		hits, err := rd.Search(ctx, q, c.who)
		if err != nil {
			t.Fatal(err)
		}
		got := ids(hits)
		if name == "patron" || name == "ilgili kişi" {
			// Aynı skorlu kayıtların sırası depo sırasına bağlıdır; kümeyi karşılaştır.
			if !sameSet(got, c.want) {
				t.Errorf("%s: isabetler %q, beklenen küme %q", name, got, c.want)
			}
			continue
		}
		if got != c.want {
			t.Errorf("%s: isabetler %q, beklenen %q (özel kayıt aramaya girdi!)", name, got, c.want)
		}
	}
}

func sameSet(a, b string) bool {
	x, y := strings.Split(a, ","), strings.Split(b, ",")
	if len(x) != len(y) {
		return false
	}
	m := map[string]int{}
	for _, v := range x {
		m[v]++
	}
	for _, v := range y {
		m[v]--
	}
	for _, n := range m {
		if n != 0 {
			return false
		}
	}
	return true
}

func TestSearchTopKStatusOrderAndSkips(t *testing.T) {
	q := []float32{1, 0, 0}
	other := Claim{}
	other = readClaim("othermodel", KindGeneral, "başka model", StatusActive, q)
	other.EmbedModel = "baska-model"
	rd := newReader(readRepo(t,
		readClaim("conf", KindGeneral, "çelişkili yakın", StatusConflict, []float32{1, 0.05, 0}), // 2. en yakın
		readClaim("a1", KindGeneral, "aktif en yakın", StatusActive, []float32{1, 0.01, 0}),
		readClaim("a2", KindGeneral, "aktif orta", StatusActive, []float32{0.7, 0.7, 0}),
		readClaim("a3", KindGeneral, "aktif uzak", StatusActive, []float32{0, 1, 0}),
		readClaim("gone", KindGeneral, "eski", StatusUpdated, q),
		readClaim("bad", KindGeneral, "geçersiz", StatusInvalid, q),
		readClaim("novec", KindGeneral, "vektörsüz", StatusActive, nil),
		other,
	), nil, &readEmb{vecs: map[string][]float32{"soru": q}})

	hits, err := rd.Search(context.Background(), "soru", readPatron)
	if err != nil {
		t.Fatal(err)
	}
	var ids []string
	for _, h := range hits {
		ids = append(ids, h.Claim.ID)
		if len(h.Claim.Embedding) != 0 {
			t.Errorf("%s: vektör sonuçta kalmamalı", h.Claim.ID)
		}
	}
	// TopK=3 skora göre: a1, conf, a2; sonra aktif önce: a1, a2, conf.
	if strings.Join(ids, ",") != "a1,a2,conf" {
		t.Errorf("sıra %v, beklenen [a1 a2 conf]", ids)
	}
}

func TestSearchMinScoreDropsIrrelevantHits(t *testing.T) {
	q := []float32{1, 0, 0}
	repo := readRepo(t,
		readClaim("near", KindGeneral, "yakın", StatusActive, []float32{1, 0.1, 0}),     // ~0.995
		readClaim("far", KindGeneral, "uzak", StatusActive, []float32{0.2, 1, 0}),       // ~0.196
		readClaim("edge", KindGeneral, "sınırda", StatusActive, []float32{0.6, 0.8, 0}), // 0.6
	)
	emb := &readEmb{vecs: map[string][]float32{"soru": q}}
	off := NewReader(repo, nil, emb, ReadConfig{EmbedModel: readModel, TopK: 5})
	if hits, _ := off.Search(context.Background(), "soru", readPatron); len(hits) != 3 {
		t.Fatalf("MinScore=0 iken hepsi gelmeliydi: %d", len(hits))
	}
	on := NewReader(repo, nil, emb, ReadConfig{EmbedModel: readModel, TopK: 5, MinScore: 0.5})
	hits, _ := on.Search(context.Background(), "soru", readPatron)
	if len(hits) != 2 || hits[0].Claim.ID != "near" || hits[1].Claim.ID != "edge" {
		t.Fatalf("MinScore=0.5: [near edge] bekleniyordu, geldi %+v", hits)
	}
	// Hepsi eşik altındaysa prompt'a hiçbir şey eklenmez.
	strict := NewReader(repo, nil, emb, ReadConfig{EmbedModel: readModel, MinScore: 0.999})
	none, _ := strict.Search(context.Background(), "soru", readPatron)
	if KnownInfo(none) != "" {
		t.Errorf("eşik altı isabetler prompt'a girmemeli")
	}
}

func TestSearchEmbedDisabledOrEmptyQuestion(t *testing.T) {
	rd := newReader(readRepo(t, readClaim("a", KindGeneral, "x", StatusActive, []float32{1, 0, 0})), nil, nil)
	if hits, err := rd.Search(context.Background(), "soru", readPatron); err != nil || hits != nil {
		t.Errorf("embedding kapalıyken boş bekleniyordu: %v %v", hits, err)
	}
	rd2 := newReader(readRepo(t), nil, &readEmb{})
	if hits, err := rd2.Search(context.Background(), "   ", readPatron); err != nil || hits != nil {
		t.Errorf("boş soruda boş bekleniyordu: %v %v", hits, err)
	}
}

// ---- KnownInfo ----

func TestKnownInfo(t *testing.T) {
	if KnownInfo(nil) != "" {
		t.Error("isabet yokken prompt'a hiçbir şey eklenmemeli")
	}
	plain := KnownInfo([]Hit{{Claim: readClaim("a", KindFirma, "ACME müşteri", StatusActive, nil)}})
	if !strings.Contains(plain, "[firma] ACME müşteri (yazan: Fikri, 2026-10-05, durum: aktif)") || strings.Contains(plain, "ÇELİŞKİLİ") {
		t.Errorf("düz blok yanlış:\n%s", plain)
	}
	withConflict := KnownInfo([]Hit{
		{Claim: readClaim("a", KindGeneral, "Vardiya 08:00", StatusActive, nil)},
		{Claim: readClaim("b", KindGeneral, "Vardiya 09:00", StatusConflict, nil)},
	})
	if !strings.Contains(withConflict, "ÇELİŞKİLİ kayıtlar var") || !strings.Contains(withConflict, "suçlama") {
		t.Errorf("çelişki yönergesi yok:\n%s", withConflict)
	}
}

// ---- Prepare ----

func TestPrepareListSkipsEmbeddingAndAppendsServerList(t *testing.T) {
	emb := &readEmb{}
	p := &readLLM{reply: `{"type":"liste","kind":"firma"}`}
	rd := newReader(readRepo(t, readClaim("f", KindFirma, "ACME", StatusActive, []float32{1, 0, 0})), p, emb)

	got, err := rd.Prepare(context.Background(), "firmaların listesini ver", readPatron)
	if err != nil {
		t.Fatal(err)
	}
	if got.Intent.Type != IntentList || got.List == nil || got.List.Total != 1 {
		t.Fatalf("liste hazırlanmadı: %+v", got)
	}
	if emb.calls != 0 {
		t.Errorf("liste sorusunda embedding çağrıldı (%d)", emb.calls)
	}
	if got.Append != got.List.Text || !strings.Contains(got.Prompt, "yalnızca tek bir kısa giriş cümlesi") {
		t.Errorf("Append/Prompt yanlış: %+v", got)
	}
}

func TestPrepareQuestionUsesKnownInfo(t *testing.T) {
	q := []float32{1, 0, 0}
	p := &readLLM{reply: `{"type":"soru"}`}
	rd := newReader(readRepo(t, readClaim("f", KindFirma, "ACME telefonu 0224 111", StatusActive, q)), p,
		&readEmb{vecs: map[string][]float32{"ACME telefonu?": q}})

	got, err := rd.Prepare(context.Background(), "ACME telefonu?", readPatron)
	if err != nil || got.Degraded || len(got.Hits) != 1 || !strings.Contains(got.Prompt, "0224 111") || got.Append != "" {
		t.Fatalf("bilgi sorusu hazırlığı yanlış: %+v %v", got, err)
	}
}

func TestPrepareDegradesInsteadOfFailing(t *testing.T) {
	q := []float32{1, 0, 0}
	repo := readRepo(t, readClaim("f", KindFirma, "ACME", StatusActive, q))

	// Ayırıcı bozuk → bilgi sorusu varsayılır, arama yine çalışır.
	rd := newReader(repo, &readLLM{reply: "bilmem"}, &readEmb{vecs: map[string][]float32{"ACME?": q}})
	got, err := rd.Prepare(context.Background(), "ACME?", readPatron)
	if err != nil || got.Intent.Type != IntentQuestion || !got.Degraded || len(got.Hits) != 1 {
		t.Errorf("ayırıcı hatasında düşmedi: %+v %v", got, err)
	}

	// Embedding bozuk → sohbet sürer, hafıza yok.
	rd = newReader(repo, &readLLM{reply: `{"type":"soru"}`}, &readEmb{err: errors.New("ollama kapalı")})
	got, err = rd.Prepare(context.Background(), "ACME?", readPatron)
	if err != nil || !got.Degraded || len(got.Hits) != 0 || got.Prompt != "" {
		t.Errorf("embedding hatasında düşmedi: %+v %v", got, err)
	}

	// İptal gerçek hata sayılır.
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	rd = newReader(repo, &readLLM{err: ctx.Err()}, &readEmb{})
	if _, err := rd.Prepare(ctx, "ACME?", readPatron); !errors.Is(err, context.Canceled) {
		t.Errorf("iptal hata olarak dönmedi: %v", err)
	}
}

func tail(s string, n int) string {
	r := []rune(s)
	if len(r) <= n {
		return s
	}
	return string(r[len(r)-n:])
}
