package tool

import (
	"context"
	"encoding/json"
	"errors"
	"slices"
	"testing"
	"time"
)

// RepositoryContract her Repository uygulamasının sağlaması gereken davranışı sınar.
// Üretim dosyasında durur (conversation/skill ile aynı gerekçe): _test.go içindeyken
// dış paketler (mongostore) çağıramaz. newRepo her alt test için TEMİZ bir depo vermelidir.
func RepositoryContract(t *testing.T, newRepo func() Repository) {
	t.Helper()
	ctx := context.Background()

	t.Run("create_get_roundtrip", func(t *testing.T) {
		r := newRepo()
		want := contractTool("tool_1", "catia.create_plane")
		if err := r.Create(ctx, want); err != nil {
			t.Fatal(err)
		}
		got, err := r.Get(ctx, "tool_1")
		if err != nil {
			t.Fatal(err)
		}
		sameTool(t, want, got)
		byName, err := r.GetByName(ctx, "catia.create_plane")
		if err != nil {
			t.Fatal(err)
		}
		sameTool(t, want, byName)
	})

	t.Run("missing_is_not_found", func(t *testing.T) {
		r := newRepo()
		if _, err := r.Get(ctx, "yok"); !errors.Is(err, ErrNotFound) {
			t.Errorf("Get: %v, ErrNotFound bekleniyordu", err)
		}
		if _, err := r.GetByName(ctx, "yok"); !errors.Is(err, ErrNotFound) {
			t.Errorf("GetByName: %v, ErrNotFound bekleniyordu", err)
		}
		if err := r.Update(ctx, contractTool("yok", "x_y")); !errors.Is(err, ErrNotFound) {
			t.Errorf("Update: %v, ErrNotFound bekleniyordu", err)
		}
		if err := r.Delete(ctx, "yok"); !errors.Is(err, ErrNotFound) {
			t.Errorf("Delete: %v, ErrNotFound bekleniyordu", err)
		}
	})

	t.Run("duplicates_rejected", func(t *testing.T) {
		r := newRepo()
		if err := r.Create(ctx, contractTool("tool_1", "same_name")); err != nil {
			t.Fatal(err)
		}
		if err := r.Create(ctx, contractTool("tool_2", "same_name")); !errors.Is(err, ErrDuplicateName) {
			t.Errorf("aynı ad: %v, ErrDuplicateName bekleniyordu", err)
		}
		if err := r.Create(ctx, contractTool("tool_1", "other_name")); err == nil {
			t.Error("aynı kimlikle ikinci kayıt eklenmemeliydi")
		}
		if _, err := r.GetByName(ctx, "other_name"); !errors.Is(err, ErrNotFound) {
			t.Errorf("reddedilen tool depoda kalmış: %v", err)
		}
	})

	t.Run("update_replaces_everything", func(t *testing.T) {
		r := newRepo()
		if err := r.Create(ctx, contractTool("tool_1", "old_name")); err != nil {
			t.Fatal(err)
		}
		upd := contractTool("tool_1", "new_name")
		upd.Description = "güncel açıklama"
		upd.Status = StatusDisabled
		upd.Risk = RiskCritical
		upd.Permissions = []string{"system"}
		upd.Retry = Retry{MaxAttempts: 2, BackoffMs: 50}
		if err := r.Update(ctx, upd); err != nil {
			t.Fatal(err)
		}
		got, err := r.Get(ctx, "tool_1")
		if err != nil {
			t.Fatal(err)
		}
		sameTool(t, upd, got)
		if _, err := r.GetByName(ctx, "old_name"); !errors.Is(err, ErrNotFound) {
			t.Errorf("eski ad hâlâ çözülüyor: %v", err)
		}
	})

	t.Run("update_name_conflict_and_self", func(t *testing.T) {
		r := newRepo()
		a, b := contractTool("tool_a", "alpha"), contractTool("tool_b", "beta")
		for _, x := range []*Tool{a, b} {
			if err := r.Create(ctx, x); err != nil {
				t.Fatal(err)
			}
		}
		b2 := contractTool("tool_b", "alpha")
		if err := r.Update(ctx, b2); !errors.Is(err, ErrDuplicateName) {
			t.Errorf("başkasının adı: %v, ErrDuplicateName bekleniyordu", err)
		}
		a2 := contractTool("tool_a", "alpha") // kendi adıyla güncelleme çakışma değildir
		if err := r.Update(ctx, a2); err != nil {
			t.Errorf("kendi adıyla güncelleme: %v", err)
		}
	})

	t.Run("delete_removes_name_index", func(t *testing.T) {
		r := newRepo()
		if err := r.Create(ctx, contractTool("tool_1", "silinecek")); err != nil {
			t.Fatal(err)
		}
		if err := r.Delete(ctx, "tool_1"); err != nil {
			t.Fatal(err)
		}
		if _, err := r.GetByName(ctx, "silinecek"); !errors.Is(err, ErrNotFound) {
			t.Errorf("silinen tool adla çözülüyor: %v", err)
		}
		// Aynı ad yeniden kullanılabilmeli: benzersizlik kaydı da silinmeli.
		if err := r.Create(ctx, contractTool("tool_2", "silinecek")); err != nil {
			t.Errorf("ad yeniden kullanılamadı: %v", err)
		}
	})

	t.Run("list_filters_and_paging", func(t *testing.T) {
		r := newRepo()
		seed := []struct{ id, name, tag, desc string }{
			{"t1", "alpha", "catia", "plane oluşturur"},
			{"t2", "beta", "catia", "plane siler"},
			{"t3", "mid", "fs", "dosya okur"},
			{"t4", "zeta", "fs", "PLAne ölçer"},
		}
		for _, s := range seed {
			x := contractTool(s.id, s.name)
			x.Description = s.desc
			x.Tags = []string{s.tag}
			x.Risk = RiskLow
			if s.name == "zeta" {
				x.Title = "Plane Ölçer" // başlıkta arama yalnızca bu kayıtta eşleşmeli
			}
			if s.id == "t3" {
				x.Status = StatusDisabled
			}
			if err := r.Create(ctx, x); err != nil {
				t.Fatal(err)
			}
		}
		names := func(f ListFilter, limit, offset int) []string {
			got, err := r.List(ctx, f, limit, offset)
			if err != nil {
				t.Fatal(err)
			}
			out := make([]string, 0, len(got))
			for _, x := range got {
				out = append(out, x.Name)
			}
			return out
		}
		check := func(label string, got, want []string) {
			t.Helper()
			if !slices.Equal(got, want) {
				t.Errorf("%s: %v, beklenen %v", label, got, want)
			}
		}
		check("ada göre sıralı", names(ListFilter{}, 0, 0), []string{"alpha", "beta", "mid", "zeta"})
		check("durum", names(ListFilter{Status: StatusDisabled}, 0, 0), []string{"mid"})
		check("tür", names(ListFilter{Kind: KindBuiltin}, 0, 0), []string{"alpha", "beta", "mid", "zeta"})
		check("risk", names(ListFilter{Risk: RiskCritical}, 0, 0), []string{})
		check("etiket", names(ListFilter{Tag: "fs"}, 0, 0), []string{"mid", "zeta"})
		check("sorgu açıklamada, büyük/küçük harf duyarsız", names(ListFilter{Query: "plane"}, 0, 0), []string{"alpha", "beta", "zeta"})
		check("sorgu adda", names(ListFilter{Query: "ZET"}, 0, 0), []string{"zeta"})
		// Başlıkta arama: yalnızca "Ölçer" sözcüğü zeta'nın başlığında geçiyor
		// (açıklamalarında "plane" var ama "ölçer" yok).
		check("sorgu başlıkta", names(ListFilter{Query: "ÖLÇER"}, 0, 0), []string{"zeta"})
		check("birleşik filtre", names(ListFilter{Status: StatusEnabled, Tag: "catia", Query: "plane"}, 0, 0), []string{"alpha", "beta"})
		check("sayfalama", names(ListFilter{}, 2, 1), []string{"beta", "mid"})
		check("offset sonu aşıyor", names(ListFilter{}, 2, 10), []string{})
		// Regex özel karakterleri düz metin olmalı: "a.c" hiçbir kaydı eşleştirmemeli.
		check("regex özel karakterleri düz metindir", names(ListFilter{Query: "a.c"}, 0, 0), []string{})
	})

	t.Run("no_aliasing", func(t *testing.T) {
		r := newRepo()
		x := contractTool("tool_1", "alias_check")
		if err := r.Create(ctx, x); err != nil {
			t.Fatal(err)
		}
		x.Tags[0] = "DEGISTI"
		x.InputSchema["properties"].(map[string]any)["offset"] = "bozuk"
		got, err := r.Get(ctx, "tool_1")
		if err != nil {
			t.Fatal(err)
		}
		if got.Tags[0] == "DEGISTI" || got.InputSchema["properties"].(map[string]any)["offset"] == "bozuk" {
			t.Error("depo, çağıranın verisiyle bellek paylaşıyor")
		}
	})
}

// contractTool sözleşme testleri için dolu bir tool üretir. Zaman damgaları milisaniyeye
// kırpılır (MongoDB milisaniye hassasiyetinde saklar).
func contractTool(id, name string) *Tool {
	now := time.Now().UTC().Truncate(time.Millisecond)
	return &Tool{
		ID:          id,
		Name:        name,
		Title:       "Test tool",
		Description: "Sözleşme testi için örnek tool.",
		Version:     "1.0.0",
		Status:      StatusEnabled,
		Kind:        KindBuiltin,
		InputSchema: map[string]any{
			"type":       "object",
			"properties": map[string]any{"offset": map[string]any{"type": "number"}},
			"required":   []any{"offset"},
		},
		OutputSchema: map[string]any{"type": "object"},
		Permissions:  []string{"execute", "read"},
		Risk:         RiskLow,
		TimeoutMs:    15_000,
		Retry:        Retry{MaxAttempts: 1, BackoffMs: 10},
		Config:       map[string]any{"root": "data"},
		Tags:         []string{"test"},
		Builtin:      true,
		CreatedAt:    now,
		UpdatedAt:    now,
	}
}

// sameTool iki tool'u JSON üzerinden karşılaştırır: bellek içi ve Mongo'nun iç temsilleri
// (ör. []any / bson.A) farklı olsa da anlamsal eşitlik doğrulanır.
func sameTool(t *testing.T, want, got *Tool) {
	t.Helper()
	w, err1 := json.Marshal(want)
	g, err2 := json.Marshal(got)
	if err1 != nil || err2 != nil {
		t.Fatalf("JSON üretilemedi: %v %v", err1, err2)
	}
	if string(w) != string(g) {
		t.Errorf("tool farklı:\n beklenen: %s\n gelen:    %s", w, g)
	}
}
