package skill

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.RepositoryContract gibi): _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 := contractSkill("sk_1", "create_plane")
		if err := r.Create(ctx, want); err != nil {
			t.Fatal(err)
		}
		got, err := r.Get(ctx, "sk_1")
		if err != nil {
			t.Fatal(err)
		}
		sameSkill(t, want, got)

		byName, err := r.GetByName(ctx, "create_plane")
		if err != nil {
			t.Fatal(err)
		}
		sameSkill(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, contractSkill("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, contractSkill("sk_1", "same_name")); err != nil {
			t.Fatal(err)
		}
		if err := r.Create(ctx, contractSkill("sk_2", "same_name")); !errors.Is(err, ErrDuplicateName) {
			t.Errorf("aynı ad: %v, ErrDuplicateName bekleniyordu", err)
		}
		if err := r.Create(ctx, contractSkill("sk_1", "other_name")); err == nil {
			t.Error("aynı kimlikle ikinci kayıt eklenmemeliydi")
		}
		// Reddedilen kayıt sızmamalı.
		if _, err := r.GetByName(ctx, "other_name"); !errors.Is(err, ErrNotFound) {
			t.Errorf("reddedilen skill depoda kalmış: %v", err)
		}
	})

	t.Run("update_replaces_everything", func(t *testing.T) {
		r := newRepo()
		orig := contractSkill("sk_1", "old_name")
		if err := r.Create(ctx, orig); err != nil {
			t.Fatal(err)
		}
		upd := contractSkill("sk_1", "new_name")
		upd.Description = "güncel açıklama"
		upd.Tags = []string{"yeni"}
		upd.ExecutorConfig = map[string]any{"tool": "catia.other"}
		upd.Examples = []Example{}
		upd.Embedding = nil
		if err := r.Update(ctx, upd); err != nil {
			t.Fatal(err)
		}
		got, err := r.Get(ctx, "sk_1")
		if err != nil {
			t.Fatal(err)
		}
		sameSkill(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 := contractSkill("sk_a", "alpha"), contractSkill("sk_b", "beta")
		for _, s := range []*Skill{a, b} {
			if err := r.Create(ctx, s); err != nil {
				t.Fatal(err)
			}
		}
		b2 := contractSkill("sk_b", "alpha")
		if err := r.Update(ctx, b2); !errors.Is(err, ErrDuplicateName) {
			t.Errorf("başkasının adı: %v, ErrDuplicateName bekleniyordu", err)
		}
		// Kendi adıyla güncelleme çakışma değildir.
		a2 := contractSkill("sk_a", "alpha")
		a2.Description = "değişti"
		if err := r.Update(ctx, a2); err != nil {
			t.Errorf("kendi adıyla güncelleme: %v", err)
		}
	})

	t.Run("delete_frees_name", func(t *testing.T) {
		r := newRepo()
		if err := r.Create(ctx, contractSkill("sk_1", "reuse_me")); err != nil {
			t.Fatal(err)
		}
		if err := r.Delete(ctx, "sk_1"); err != nil {
			t.Fatal(err)
		}
		if _, err := r.Get(ctx, "sk_1"); !errors.Is(err, ErrNotFound) {
			t.Errorf("silinen skill hâlâ okunuyor: %v", err)
		}
		if err := r.Create(ctx, contractSkill("sk_2", "reuse_me")); err != nil {
			t.Errorf("silinen adın yeniden kullanımı: %v", err)
		}
	})

	t.Run("list_sorted_filtered_paged", func(t *testing.T) {
		r := newRepo()
		if got, err := r.List(ctx, ListFilter{}, 0, 0); err != nil || got == nil || len(got) != 0 {
			t.Fatalf("boş depoda List: %v, %v (nil olmayan boş dilim bekleniyordu)", got, err)
		}

		mk := func(id, name, desc string, st Status, tags ...string) {
			s := contractSkill(id, name)
			s.Description, s.Status, s.Tags = desc, st, tags
			if err := r.Create(ctx, s); err != nil {
				t.Fatal(err)
			}
		}
		mk("1", "zeta", "Kesit alır", StatusActive, "catia")
		mk("2", "alpha", "Offset PLANE oluşturur", StatusActive, "catia", "geometry")
		mk("3", "mid", "Rapor yazar", StatusDraft, "docs")
		mk("4", "beta", "Plane siler", StatusDisabled, "geometry")

		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 _, s := range got {
				out = append(out, s.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: StatusActive}, 0, 0), []string{"alpha", "zeta"})
		check("etiket", names(ListFilter{Tag: "geometry"}, 0, 0), []string{"alpha", "beta"})
		check("sorgu açıklamada, büyük/küçük harf duyarsız", names(ListFilter{Query: "plane"}, 0, 0), []string{"alpha", "beta"})
		check("sorgu adda", names(ListFilter{Query: "ZET"}, 0, 0), []string{"zeta"})
		check("birleşik filtre", names(ListFilter{Status: StatusActive, Tag: "catia", Query: "plane"}, 0, 0), []string{"alpha"})
		check("sayfalama", names(ListFilter{}, 2, 1), []string{"beta", "mid"})
		check("offset sonu aşıyor", names(ListFilter{}, 2, 10), []string{})
		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()
		s := contractSkill("sk_1", "alias_check")
		if err := r.Create(ctx, s); err != nil {
			t.Fatal(err)
		}
		s.Tags[0] = "DEGISTI" // çağıranın elindeki kopyayı bozmak depoyu etkilememeli
		s.ExecutorConfig["tool"] = "bozuk"
		got, err := r.Get(ctx, "sk_1")
		if err != nil {
			t.Fatal(err)
		}
		if got.Tags[0] == "DEGISTI" || got.ExecutorConfig["tool"] == "bozuk" {
			t.Error("depo, çağıranın verisiyle bellek paylaşıyor")
		}
	})
}

// contractSkill sözleşme testleri için dolu bir skill üretir. Zaman damgaları milisaniyeye kırpılır
// (MongoDB milisaniye hassasiyetinde saklar); sayılar float64'tür (BSON'da tip kaybı olmasın).
func contractSkill(id, name string) *Skill {
	now := time.Now().UTC().Truncate(time.Millisecond)
	return &Skill{
		ID:          id,
		Name:        name,
		Description: "Seçilen eksen sistemi üzerinden offset plane oluşturur.",
		Version:     "1.0.0",
		Status:      StatusActive,
		Parameters: []Parameter{
			{Name: "offset", Type: ParamNumber, Description: "mm cinsinden uzaklık", Required: true},
			{Name: "direction", Type: ParamNumber},
		},
		InputSchema: map[string]any{
			"type":       "object",
			"properties": map[string]any{"offset": map[string]any{"type": "number"}},
			"required":   []any{"offset"},
		},
		ExecutorType:   ExecutorTool,
		ExecutorConfig: map[string]any{"tool": "catia.create_plane"},
		Examples:       []Example{{Input: "30 mm geriye plane koy", Arguments: map[string]any{"offset": 30.0}}},
		Permissions:    []string{"catia.write"},
		Tags:           []string{"catia", "geometry"},
		Embedding:      []float32{0.25, -0.5, 0.125},
		CreatedAt:      now,
		UpdatedAt:      now,
	}
}

// sameSkill iki skill'i 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. Embedding json'da yoktur, ayrıca denetlenir.
func sameSkill(t *testing.T, want, got *Skill) {
	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("skill farklı:\n beklenen: %s\n gelen:    %s", w, g)
	}
	if !slices.Equal(want.Embedding, got.Embedding) {
		t.Errorf("embedding farklı: %v != %v", want.Embedding, got.Embedding)
	}
}
