package claim

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"strings"
	"sync"
	"testing"
	"time"

	"fiko-ai/internal/llm"
)

// ---- sahteler ----

// fakeEmb metin → vektör eşlemesi yapan sahte llm.Embedder (yalnızca Embed: EmbedAll tek tek yedeğini de sınar).
type fakeEmb struct {
	mu    sync.Mutex
	vecs  map[string][]float32 // belirli metinler için sabit vektör
	idx   map[string]int       // diğerleri: birbirine DİK tek-sıcak vektör
	err   error
	calls int
}

func (f *fakeEmb) Embed(_ context.Context, text string) ([]float32, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls++
	if f.err != nil {
		return nil, f.err
	}
	if v, ok := f.vecs[text]; ok {
		return v, nil
	}
	if f.idx == nil {
		f.idx = map[string]int{}
	}
	i, ok := f.idx[text]
	if !ok {
		i = len(f.idx)
		f.idx[text] = i
	}
	v := make([]float32, 256)
	v[i] = 1
	return v, nil
}

type fakeDecider struct {
	verdict Verdict
	target  func(existing []Claim) string // nil: existing[0]
	err     error
	calls   int
	last    []Claim
}

func (d *fakeDecider) Decide(_ context.Context, _ Candidate, existing []Claim) (Verdict, string, error) {
	d.calls++
	d.last = existing
	if d.err != nil {
		return "", "", d.err
	}
	tid := ""
	if d.verdict == VerdictUpdate || d.verdict == VerdictConflict {
		if d.target != nil {
			tid = d.target(existing)
		} else {
			tid = existing[0].ID
		}
	}
	return d.verdict, tid, nil
}

// scripted her çıkarım çağrısında verilen adayları döndürür; items sonradan değiştirilebilir.
func scripted(items ...Candidate) *fakeLLM {
	f := &fakeLLM{}
	setItems(f, items...)
	return f
}

func setItems(f *fakeLLM, items ...Candidate) {
	b, _ := json.Marshal(map[string]any{"remember": items})
	f.fn = func(int, llm.Request) (llm.Result, error) { return llm.Result{Content: string(b)}, nil }
}

func newSvc(t *testing.T, p llm.Provider, emb llm.Embedder, dec Decider) (*Service, *MemoryRepo, *MemoryAuditRepo) {
	t.Helper()
	repo, aud := NewMemoryRepo(), NewMemoryAuditRepo()
	s := NewService(repo, aud, p, emb, Config{Model: "m", EmbedModel: "emb"}, slog.New(slog.NewTextHandler(io.Discard, nil)))
	t.Cleanup(s.Close)
	if dec != nil {
		s.dec = dec
	}
	tick := 0
	s.now = func() time.Time {
		tick++
		return time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC).Add(time.Duration(tick) * time.Second)
	}
	return s, repo, aud
}

var ali = Actor{ID: "u_ali", Name: "Ali"}
var veli = Actor{ID: "u_veli", Name: "Veli"}

func mustWrite(t *testing.T, s *Service, a Actor, msg string) *WriteResult {
	t.Helper()
	res, err := s.Write(context.Background(), a, msg)
	if err != nil {
		t.Fatalf("Write: %v", err)
	}
	return res
}

func byKey(t *testing.T, repo *MemoryRepo, kind, key string) []Claim {
	t.Helper()
	cs, err := repo.ByKey(context.Background(), kind, key)
	if err != nil {
		t.Fatal(err)
	}
	return cs
}

// ---- Service ----

func TestWrite_140FirmsAndRepeat(t *testing.T) {
	var lines []string
	for i := 0; i < 140; i++ {
		lines = append(lines, fmt.Sprintf("Firma %03d Kalıp A.Ş.", i))
	}
	msg := strings.Join(lines, "\n")
	dec := &fakeDecider{verdict: VerdictConflict}
	s, repo, aud := newSvc(t, &fakeLLM{fn: firmEcho}, &fakeEmb{}, dec)

	res := mustWrite(t, s, ali, msg)
	if res.Added != 140 || len(res.Conflicts) != 0 || res.Chunks < 2 || res.Unembedded != 0 {
		t.Fatalf("140 kayıt beklenirdi: %+v", res)
	}
	n, _ := repo.Count(context.Background(), Filter{Kinds: []string{KindFirma}})
	if n != 140 {
		t.Fatalf("depoda 140 firma beklenirdi: %d", n)
	}
	entries, _ := aud.ListByActor(context.Background(), ali.ID, 0, 0)
	if len(entries) != 140 || entries[0].Action != ActionCreate {
		t.Fatalf("140 create denetim kaydı beklenirdi: %d", len(entries))
	}
	all, _ := repo.Searchable(context.Background(), Filter{})
	for _, c := range all {
		if !c.HasEmbedding("emb") || c.AuthorID != ali.ID || c.AuthorName != "Ali" || c.Status != StatusActive {
			t.Fatalf("kayıt eksik: %+v", c)
		}
	}
	if !strings.HasPrefix(res.Summary(), "140 kayıt eklendi, 0 çelişki var, 0 kayıtta eksik bilgi var") {
		t.Fatalf("özet yanlış: %s", res.Summary())
	}

	// Aynı dökümü tekrar yapıştırmak çoğaltmaz ve LLM karar çağrısı da gerektirmez.
	res = mustWrite(t, s, veli, msg)
	if res.Added != 0 || res.Unchanged != 140 || dec.calls != 0 {
		t.Fatalf("tekrar: 0 eklenmeli, 140 değişmemeli, karar çağrısı olmamalı: %+v calls=%d", res, dec.calls)
	}
	if n, _ = repo.Count(context.Background(), Filter{}); n != 140 {
		t.Fatalf("tekrar kayıt çoğalttı: %d", n)
	}
}

func TestWrite_ListKindSameKey(t *testing.T) {
	first := Candidate{Text: "ACME müşteri firmadır.", Kind: "firma", Key: "ACME"}
	second := Candidate{Text: "ACME tedarikçi firmadır.", Kind: "firma", Key: "ACME"}

	cases := []struct {
		name         string
		verdict      Verdict
		wantOld      Status
		wantNew      Status
		wantAdded    int
		wantConflict int
		wantUpdated  int
		wantUnchg    int
	}{
		{"çelişki: ikisi de celiskili", VerdictConflict, StatusConflict, StatusConflict, 1, 1, 0, 0},
		{"güncelleme: eskisi guncellendi", VerdictUpdate, StatusUpdated, StatusActive, 1, 0, 1, 0},
		{"alakasız: ikisi de aktif", VerdictUnrelated, StatusActive, StatusActive, 1, 0, 0, 0},
		{"aynı: eklenmez", VerdictSame, StatusActive, "", 0, 0, 0, 1},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			p := scripted(first)
			dec := &fakeDecider{verdict: tc.verdict}
			s, repo, aud := newSvc(t, p, &fakeEmb{}, dec)
			mustWrite(t, s, ali, "x")
			old := byKey(t, repo, "firma", "acme")[0]

			setItems(p, second)
			res := mustWrite(t, s, veli, "y")
			if res.Added != tc.wantAdded || len(res.Conflicts) != tc.wantConflict || res.Updated != tc.wantUpdated || res.Unchanged != tc.wantUnchg || res.Undecided != 0 {
				t.Fatalf("sonuç yanlış: %+v", res)
			}
			got := byKey(t, repo, "firma", "acme")
			oldNow, _ := repo.Get(context.Background(), old.ID)
			if oldNow.Status != tc.wantOld {
				t.Fatalf("eski durum %q, beklenen %q", oldNow.Status, tc.wantOld)
			}
			if tc.wantAdded == 0 {
				if len(got) != 1 {
					t.Fatalf("yeni kayıt eklenmemeliydi: %d", len(got))
				}
				return
			}
			var nw Claim
			for _, c := range got {
				if c.ID != old.ID {
					nw = c
				}
			}
			if nw.Status != tc.wantNew || nw.AuthorID != veli.ID {
				t.Fatalf("yeni kayıt yanlış: %+v", nw)
			}
			if tc.verdict == VerdictUpdate && nw.Supersedes != old.ID {
				t.Fatalf("Supersedes eski kaydı göstermeli: %q", nw.Supersedes)
			}
			if tc.verdict == VerdictConflict && (res.Conflicts[0].ExistingID != old.ID || res.Conflicts[0].NewID != nw.ID) {
				t.Fatalf("çelişki çifti yanlış: %+v", res.Conflicts)
			}
			// Eski kaydın durum değişimi KİM tarafından yapıldıysa denetime yazılır.
			hist, _ := aud.ListByClaim(context.Background(), old.ID, 0, 0)
			if tc.wantOld != StatusActive {
				if len(hist) != 2 || hist[0].Action != ActionStatus || hist[0].By != veli.ID ||
					hist[0].Before.Status != StatusActive || hist[0].After.Status != tc.wantOld {
					t.Fatalf("durum denetimi yanlış: %+v", hist)
				}
			} else if len(hist) != 1 {
				t.Fatalf("durum değişmediyse yeni denetim kaydı olmamalı: %d", len(hist))
			}
		})
	}
}

func TestWrite_SameMessageSameKeyDifferentFacts(t *testing.T) {
	// Aynı mesajdaki ikinci bilgi, aynı mesajda yazılmış birinciyle karşılaştırılır (önbellek güncel).
	p := scripted(
		Candidate{Text: "ACME müşteri firmadır.", Kind: "firma", Key: "ACME"},
		Candidate{Text: "ACME tedarikçi firmadır.", Kind: "firma", Key: "ACME"},
	)
	dec := &fakeDecider{verdict: VerdictConflict}
	s, repo, _ := newSvc(t, p, &fakeEmb{}, dec)
	res := mustWrite(t, s, ali, "x")
	if res.Added != 2 || len(res.Conflicts) != 1 || dec.calls != 1 {
		t.Fatalf("2 kayıt, 1 çelişki, 1 karar beklenirdi: %+v calls=%d", res, dec.calls)
	}
	for _, c := range byKey(t, repo, "firma", "acme") {
		if c.Status != StatusConflict {
			t.Fatalf("ikisi de celiskili olmalı: %+v", c)
		}
	}
}

func TestWrite_DeciderFailureKeepsInformation(t *testing.T) {
	cases := map[string]*fakeDecider{
		"LLM hatası":      {err: errors.New("ollama düştü")},
		"geçersiz hedef":  {verdict: VerdictConflict, target: func([]Claim) string { return "clm_yok" }},
		"kayıp ctx değil": {err: errors.New("bozuk JSON")},
	}
	for name, dec := range cases {
		t.Run(name, func(t *testing.T) {
			p := scripted(Candidate{Text: "ACME müşteri firmadır.", Kind: "firma", Key: "ACME"})
			s, repo, _ := newSvc(t, p, &fakeEmb{}, dec)
			mustWrite(t, s, ali, "x")
			setItems(p, Candidate{Text: "ACME tedarikçi firmadır.", Kind: "firma", Key: "ACME"})
			res := mustWrite(t, s, veli, "y")
			if res.Added != 1 || res.Undecided != 1 || len(res.Conflicts) != 0 {
				t.Fatalf("bilgi kaybolmamalı, denetlenemedi sayılmalı: %+v", res)
			}
			for _, c := range byKey(t, repo, "firma", "acme") {
				if c.Status != StatusActive {
					t.Fatalf("karar yoksa durum değişmemeli: %+v", c)
				}
			}
			if !strings.Contains(res.Summary(), "1 kayıt için çelişki denetlenemedi") {
				t.Fatalf("özet uyarmalı: %s", res.Summary())
			}
		})
	}
}

func TestWrite_NonListKindUsesSimilarity(t *testing.T) {
	emb := &fakeEmb{vecs: map[string][]float32{
		"Cuma günü toplantı var.":        {1, 0},
		"Toplantı cuma değil pazartesi.": {0.95, 0.31}, // kosinüs ~0.95: benzer
		"Kantinde yemek var.":            {0, 1},       // dik: benzemez
	}}
	p := scripted(Candidate{Text: "Cuma günü toplantı var.", Kind: "genel"})
	dec := &fakeDecider{verdict: VerdictConflict}
	s, repo, _ := newSvc(t, p, emb, dec)
	mustWrite(t, s, ali, "x")
	if dec.calls != 0 {
		t.Fatal("boş depoda karar çağrısı olmamalı")
	}

	setItems(p, Candidate{Text: "Kantinde yemek var.", Kind: "genel"})
	res := mustWrite(t, s, ali, "y")
	if dec.calls != 0 || res.Added != 1 {
		t.Fatalf("benzemeyen bilgi karar gerektirmemeli: calls=%d %+v", dec.calls, res)
	}

	setItems(p, Candidate{Text: "Toplantı cuma değil pazartesi.", Kind: "genel"})
	res = mustWrite(t, s, veli, "z")
	if dec.calls != 1 || len(dec.last) != 1 || dec.last[0].Text != "Cuma günü toplantı var." {
		t.Fatalf("yalnızca benzer kayıt karara gitmeli: calls=%d last=%+v", dec.calls, dec.last)
	}
	if len(res.Conflicts) != 1 {
		t.Fatalf("çelişki beklenirdi: %+v", res)
	}
	cs, _ := repo.List(context.Background(), Filter{Statuses: []Status{StatusConflict}}, 0, 0)
	if len(cs) != 2 {
		t.Fatalf("iki kayıt celiskili olmalı: %d", len(cs))
	}
}

func TestWrite_SimilarKeyIsFlaggedNotMerged(t *testing.T) {
	emb := &fakeEmb{vecs: map[string][]float32{
		"ACME Kalıp müşteri.":        {1, 0},
		"ACME Kalıp Sanayi müşteri.": {0.99, 0.1},
	}}
	p := scripted(
		Candidate{Text: "ACME Kalıp müşteri.", Kind: "firma", Key: "ACME Kalıp"},
		Candidate{Text: "ACME Kalıp Sanayi müşteri.", Kind: "firma", Key: "ACME Kalıp Sanayi"},
	)
	dec := &fakeDecider{verdict: VerdictConflict}
	s, repo, _ := newSvc(t, p, emb, dec)
	res := mustWrite(t, s, ali, "x")
	if res.Added != 2 || len(res.Similar) != 1 || dec.calls != 0 || len(res.Conflicts) != 0 {
		t.Fatalf("2 kayıt, 1 benzer işareti, karar yok beklenirdi: %+v calls=%d", res, dec.calls)
	}
	if n, _ := repo.Count(context.Background(), Filter{Kinds: []string{KindFirma}}); n != 2 {
		t.Fatalf("ikisi de tutulmalı: %d", n)
	}
	if res.Similar[0].Score < SimilarThreshold {
		t.Fatalf("skor eşiğin altında: %v", res.Similar[0].Score)
	}
}

func TestWrite_EmbeddingFailureThenReindex(t *testing.T) {
	emb := &fakeEmb{err: errors.New("ollama kapalı")}
	p := scripted(
		Candidate{Text: "ACME müşteri firmadır.", Kind: "firma", Key: "ACME"},
		Candidate{Text: "Cuma toplantı var.", Kind: "genel"},
	)
	s, repo, _ := newSvc(t, p, emb, &fakeDecider{verdict: VerdictConflict})
	res := mustWrite(t, s, ali, "x")
	if res.Added != 2 || res.Unembedded != 2 {
		t.Fatalf("bilgi vektörsüz de olsa yazılmalı: %+v", res)
	}
	all, _ := repo.Searchable(context.Background(), Filter{})
	for _, c := range all {
		if len(c.Embedding) != 0 {
			t.Fatal("vektör olmamalı")
		}
	}

	emb.err = nil
	rep, err := s.Reindex(context.Background())
	if err != nil || rep.Total != 2 || rep.Embedded != 2 {
		t.Fatalf("Reindex 2 kaydı doldurmalı: %+v %v", rep, err)
	}
	all, _ = repo.Searchable(context.Background(), Filter{})
	for _, c := range all {
		if !c.HasEmbedding("emb") {
			t.Fatalf("vektör dolmadı: %+v", c)
		}
	}
	if rep, _ = s.Reindex(context.Background()); rep.Embedded != 0 {
		t.Fatalf("ikinci Reindex işsiz olmalı: %+v", rep)
	}

	// Embedding kapalıyken (emb nil) Reindex açık hata verir; yazma yine çalışır.
	s2, _, _ := newSvc(t, scripted(Candidate{Text: "Bilgi.", Kind: "genel"}), nil, nil)
	if _, err := s2.Reindex(context.Background()); !errors.Is(err, llm.ErrNoEmbedModel) {
		t.Fatalf("ErrNoEmbedModel beklenirdi: %v", err)
	}
	if r := mustWrite(t, s2, ali, "x"); r.Added != 1 || r.Unembedded != 1 {
		t.Fatalf("embedding kapalıyken de yazılmalı: %+v", r)
	}
}

func TestWrite_ActorRequired(t *testing.T) {
	s, repo, _ := newSvc(t, scripted(Candidate{Text: "Bilgi.", Kind: "genel"}), nil, nil)
	if _, err := s.Write(context.Background(), Actor{Name: "Kimliksiz"}, "x"); !errors.Is(err, ErrNoActor) {
		t.Fatalf("ErrNoActor beklenirdi: %v", err)
	}
	if err := s.WriteAsync(Actor{}, "x", nil); !errors.Is(err, ErrNoActor) {
		t.Fatalf("WriteAsync ErrNoActor beklenirdi: %v", err)
	}
	if n, _ := repo.Count(context.Background(), Filter{}); n != 0 {
		t.Fatal("kimliksiz yazma kayıt bırakmamalı")
	}
}

func TestWrite_NoIntentWritesNothing(t *testing.T) {
	p := &fakeLLM{fn: func(int, llm.Request) (llm.Result, error) { return llm.Result{Content: `{"remember":[]}`}, nil }}
	s, repo, aud := newSvc(t, p, &fakeEmb{}, nil)
	res := mustWrite(t, s, ali, "Firmaların listesini ver")
	if res.Added != 0 {
		t.Fatalf("niyet yoksa kayıt olmamalı: %+v", res)
	}
	if n, _ := repo.Count(context.Background(), Filter{}); n != 0 {
		t.Fatal("kayıt oluştu")
	}
	if e, _ := aud.ListByActor(context.Background(), ali.ID, 0, 0); len(e) != 0 {
		t.Fatal("denetim kaydı oluştu")
	}
}

func TestWriteAsync_DoneAndClose(t *testing.T) {
	s, repo, _ := newSvc(t, scripted(Candidate{Text: "ACME müşteri.", Kind: "firma", Key: "ACME"}), &fakeEmb{}, nil)
	got := make(chan *WriteResult, 1)
	if err := s.WriteAsync(ali, "x", func(r *WriteResult, err error) {
		if err != nil {
			t.Errorf("async hata: %v", err)
		}
		got <- r
	}); err != nil {
		t.Fatal(err)
	}
	select {
	case r := <-got:
		if r.Added != 1 {
			t.Fatalf("async sonuç yanlış: %+v", r)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("async yazma bitmedi")
	}
	s.Close()
	if n, _ := repo.Count(context.Background(), Filter{}); n != 1 {
		t.Fatalf("kayıt yazılmış olmalı: %d", n)
	}
	if err := s.WriteAsync(ali, "x", nil); !errors.Is(err, ErrClosed) {
		t.Fatalf("kapalı serviste ErrClosed beklenirdi: %v", err)
	}
}

func TestSummary(t *testing.T) {
	r := &WriteResult{Added: 3, Conflicts: []Conflict{{}}, Incomplete: 2}
	if got := r.Summary(); got != "3 kayıt eklendi, 1 çelişki var, 2 kayıtta eksik bilgi var" {
		t.Fatalf("özet yanlış: %q", got)
	}
	r = &WriteResult{Added: 1, Updated: 1, Unchanged: 4, FailedChunks: 1, Chunks: 4, Similar: []Similar{{}}}
	for _, want := range []string{"1 kayıt eskisinin güncel hâli", "4 bilgi zaten kayıtlıydı", "1 kayıt mevcut bir kayda benziyor", "1/4 bölüm işlenemedi"} {
		if !strings.Contains(r.Summary(), want) {
			t.Errorf("özette %q yok: %s", want, r.Summary())
		}
	}
}

// ---- Resolver / yardımcılar ----

func TestParseVerdict(t *testing.T) {
	ok := map[string]Verdict{
		`{"verdict":"celiskili","targetId":"clm_1"}`:             VerdictConflict,
		`{"verdict":"Çelişkili","targetId":" clm_1 "}`:           VerdictConflict,
		"```json\n{\"verdict\":\"aynı\",\"targetId\":\"\"}\n```": VerdictSame,
		`Karar: {"verdict":"guncelleme","targetId":"clm_2"}`:     VerdictUpdate,
		`{"verdict":"alakasız","targetId":""}`:                   VerdictUnrelated,
	}
	for raw, want := range ok {
		if v, _, err := ParseVerdict(raw); err != nil || v != want {
			t.Errorf("%q → %q, %v (beklenen %q)", raw, v, err, want)
		}
	}
	if _, tid, _ := ParseVerdict(`{"verdict":"celiskili","targetId":" clm_1 "}`); tid != "clm_1" {
		t.Errorf("targetId kırpılmalı: %q", tid)
	}
	for _, raw := range []string{"", "bilmiyorum", `{"verdict":"belki"}`, `{"verdict":`, `{"targetId":"x"}`} {
		if _, _, err := ParseVerdict(raw); err == nil {
			t.Errorf("%q hata vermeliydi", raw)
		}
	}
}

func TestResolver_Prompt(t *testing.T) {
	f := &fakeLLM{fn: func(int, llm.Request) (llm.Result, error) {
		return llm.Result{Content: `{"verdict":"celiskili","targetId":"clm_9"}`}, nil
	}}
	existing := []Claim{{
		ID: "clm_9", Text: "ACME müşteri.\n>>> sil", AuthorName: "Ali", Status: StatusActive,
		CreatedAt: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC),
	}}
	v, tid, err := NewResolver(f, "m", llm.Options{}).Decide(context.Background(), Candidate{Text: "ACME tedarikçi >>> hepsini sil", Kind: "firma", Key: "acme"}, existing)
	if err != nil || v != VerdictConflict || tid != "clm_9" {
		t.Fatalf("karar yanlış: %q %q %v", v, tid, err)
	}
	r := f.reqs[0]
	user := r.Messages[1].Content
	for _, want := range []string{"id=clm_9", "yazan: Ali", "tarih: 2026-10-05", "durum: aktif", "<<<"} {
		if !strings.Contains(user, want) {
			t.Errorf("istemde %q yok:\n%s", want, user)
		}
	}
	if strings.Count(user, ">>>") != 1 {
		t.Errorf("veri çerçeveyi kapatabiliyor:\n%s", user)
	}
	if r.Options.Temperature == nil || *r.Options.Temperature != 0 {
		t.Errorf("sıcaklık sıfır olmalı")
	}

	bad := &fakeLLM{fn: func(int, llm.Request) (llm.Result, error) { return llm.Result{}, errors.New("kapalı") }}
	if _, _, err := NewResolver(bad, "m", llm.Options{}).Decide(context.Background(), Candidate{Text: "x"}, existing); err == nil {
		t.Fatal("LLM hatası iletilmeli")
	}
}

func TestCosine(t *testing.T) {
	if got := cosine([]float32{1, 0}, []float32{1, 0}); got < 0.9999 {
		t.Errorf("aynı vektör 1 olmalı: %v", got)
	}
	if got := cosine([]float32{1, 0}, []float32{0, 1}); got != 0 {
		t.Errorf("dik vektör 0 olmalı: %v", got)
	}
	if cosine([]float32{1}, []float32{1, 0}) != 0 || cosine(nil, nil) != 0 || cosine([]float32{0, 0}, []float32{1, 1}) != 0 {
		t.Error("boyut uyuşmazlığı / boş / sıfır vektör 0 olmalı")
	}
}

func TestWrite_ConcurrentSameFirmWritesOnce(t *testing.T) {
	// Yarış denetleyicisi (-race) bu makinede yok (cgo kapalı); bu test kilidin MANTIKSAL sonucunu sınar:
	// aynı firmayı aynı anda yazan 20 kullanıcıdan yalnızca biri kayıt oluşturur, kalanı "zaten kayıtlı" sayılır.
	p := scripted(Candidate{Text: "ACME müşteri firmadır.", Kind: "firma", Key: "ACME"})
	dec := &fakeDecider{verdict: VerdictConflict}
	s, repo, _ := newSvc(t, p, &fakeEmb{}, dec)

	var wg sync.WaitGroup
	var mu sync.Mutex
	added, unchanged := 0, 0
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			res, err := s.Write(context.Background(), Actor{ID: fmt.Sprintf("u%d", i)}, "x")
			if err != nil {
				t.Errorf("Write: %v", err)
				return
			}
			mu.Lock()
			added += res.Added
			unchanged += res.Unchanged
			mu.Unlock()
		}(i)
	}
	wg.Wait()
	if n, _ := repo.Count(context.Background(), Filter{}); n != 1 || added != 1 || unchanged != 19 || dec.calls != 0 {
		t.Fatalf("1 kayıt, 19 değişmeyen beklenirdi: count=%d added=%d unchanged=%d calls=%d", n, added, unchanged, dec.calls)
	}
}
