package skill

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"strings"
	"testing"
	"time"

	"fiko-ai/internal/event"
)

// Bellek içi depo, Mongo ile AYNI sözleşmeden geçer.
func TestMemoryRepo_Contract(t *testing.T) {
	RepositoryContract(t, func() Repository { return NewMemoryRepo() })
}

// ---------------------------------------------------------------- Normalize / Validate

func validSkill() *Skill {
	s := contractSkill("x", "valid_skill")
	Normalize(s)
	return s
}

func hasField(errs []FieldError, field string) bool {
	for _, e := range errs {
		if e.Field == field {
			return true
		}
	}
	return false
}

func TestValidate_ValidSkillPasses(t *testing.T) {
	if errs := Validate(validSkill()); len(errs) != 0 {
		t.Fatalf("geçerli skill reddedildi: %v", errs)
	}
}

func TestValidate_DraftNeedsNoExecutor(t *testing.T) {
	s := &Skill{Name: "half_done", Description: "yarım iş"}
	Normalize(s)
	if s.Status != StatusDraft {
		t.Fatalf("varsayılan durum draft olmalı: %q", s.Status)
	}
	if errs := Validate(s); len(errs) != 0 {
		t.Fatalf("taslak executor'suz kaydedilebilmeli: %v", errs)
	}
}

func TestValidate_FieldErrors(t *testing.T) {
	cases := []struct {
		name  string
		field string
		mut   func(s *Skill)
	}{
		{"kötü ad", "name", func(s *Skill) { s.Name = "Bad Name" }},
		{"boş açıklama", "description", func(s *Skill) { s.Description = "" }},
		{"kötü sürüm", "version", func(s *Skill) { s.Version = "1.0" }},
		{"bilinmeyen durum", "status", func(s *Skill) { s.Status = "weird" }},
		{"tekrarlı parametre", "parameters[1].name", func(s *Skill) {
			s.Parameters = []Parameter{{Name: "a", Type: ParamString}, {Name: "a", Type: ParamString}}
		}},
		{"kötü parametre tipi", "parameters[0].type", func(s *Skill) {
			s.Parameters = []Parameter{{Name: "a", Type: "float"}}
		}},
		{"inputSchema object değil", "inputSchema", func(s *Skill) { s.InputSchema = map[string]any{"type": "array"} }},
		{"active + tool yok", "executorConfig", func(s *Skill) { s.ExecutorConfig = nil }},
		{"active + llm prompt yok", "executorConfig", func(s *Skill) {
			s.ExecutorType, s.ExecutorConfig = ExecutorLLM, map[string]any{"tool": "x"}
		}},
		{"bilinmeyen executor", "executorType", func(s *Skill) { s.ExecutorType = "shell" }},
		{"boş örnek", "examples[0].input", func(s *Skill) { s.Examples = []Example{{Input: "  "}} }},
		{"kötü izin", "permissions[0]", func(s *Skill) { s.Permissions = []string{"Bad Perm"} }},
		{"çok uzun etiket", "tags[0]", func(s *Skill) { s.Tags = []string{strings.Repeat("a", MaxTagRunes+1)} }},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			s := validSkill()
			c.mut(s)
			Normalize(s)
			if errs := Validate(s); !hasField(errs, c.field) {
				t.Errorf("%q alanı için hata bekleniyordu, gelen: %v", c.field, errs)
			}
		})
	}
}

func TestValidate_CollectsAllErrors(t *testing.T) {
	s := validSkill()
	s.Name, s.Description = "Bad Name", ""
	Normalize(s)
	errs := Validate(s)
	if !hasField(errs, "name") || !hasField(errs, "description") {
		t.Fatalf("tüm hatalar tek seferde dönmeli: %v", errs)
	}
}

func TestNormalize(t *testing.T) {
	s := &Skill{
		Name:        "  trimmed  ",
		Description: "  d  ",
		Tags:        []string{" Catia ", "CATIA", "geo", ""},
		Permissions: []string{" catia.write ", "catia.write"},
	}
	Normalize(s)
	if s.Name != "trimmed" || s.Description != "d" {
		t.Errorf("boşluklar kırpılmadı: %q %q", s.Name, s.Description)
	}
	if got := strings.Join(s.Tags, ","); got != "catia,geo" {
		t.Errorf("etiketler küçük harf + tekil olmalı: %q", got)
	}
	if got := strings.Join(s.Permissions, ","); got != "catia.write" {
		t.Errorf("izinler tekil olmalı: %q", got)
	}
	if s.Version != DefaultVersion || s.Status != StatusDraft || s.ExecutorType != ExecutorTool {
		t.Errorf("varsayılanlar uygulanmadı: %q %q %q", s.Version, s.Status, s.ExecutorType)
	}
	if s.Parameters == nil || s.Examples == nil {
		t.Error("listeler nil değil boş olmalı (API'de null yerine [])")
	}
}

func TestValidationError_MentionsFields(t *testing.T) {
	err := &ValidationError{Errors: []FieldError{{Field: "name", Message: "kötü"}}}
	if !strings.Contains(err.Error(), "name") {
		t.Errorf("hata iletisi alan adını içermeli: %q", err.Error())
	}
}

// ---------------------------------------------------------------- Service (Registry)

func newTestService(t *testing.T) (*Service, *MemoryRepo, event.Bus) {
	t.Helper()
	repo := NewMemoryRepo()
	bus := event.NewMemoryBus(slog.New(slog.NewTextHandler(io.Discard, nil)), 16)
	t.Cleanup(bus.Close)
	return NewService(repo, bus), repo, bus
}

func activeInput(name string) Skill {
	return Skill{
		Name:           name,
		Description:    "Offset plane oluşturur.",
		Status:         StatusActive,
		ExecutorType:   ExecutorTool,
		ExecutorConfig: map[string]any{"tool": "catia.create_plane"},
	}
}

// waitEvent verilen türde bir olayı bekler.
func waitEvent(t *testing.T, ch <-chan event.Event) event.Event {
	t.Helper()
	select {
	case e := <-ch:
		return e
	case <-time.After(2 * time.Second):
		t.Fatal("olay zamanında gelmedi")
		return event.Event{}
	}
}

func subscribe(bus event.Bus, typ event.Type) <-chan event.Event {
	ch := make(chan event.Event, 4)
	bus.Subscribe(typ, func(_ context.Context, e event.Event) { ch <- e })
	return ch
}

func TestService_RegisterOwnsServerFields(t *testing.T) {
	ctx := context.Background()
	svc, repo, bus := newTestService(t)
	created := subscribe(bus, event.SkillCreated)

	in := activeInput("create_plane")
	in.ID = "kotu_niyetli"
	in.Embedding = []float32{1, 2, 3}
	in.CreatedAt = time.Unix(1, 0)

	got, err := svc.Register(ctx, in)
	if err != nil {
		t.Fatal(err)
	}
	if got.ID == "" || got.ID == "kotu_niyetli" {
		t.Errorf("kimlik sunucuya ait olmalı: %q", got.ID)
	}
	if got.Embedding != nil {
		t.Error("girdideki embedding yok sayılmalı")
	}
	if got.CreatedAt.Equal(time.Unix(1, 0)) || got.CreatedAt.IsZero() || !got.CreatedAt.Equal(got.UpdatedAt) {
		t.Errorf("zaman damgaları sunucuya ait olmalı: %v %v", got.CreatedAt, got.UpdatedAt)
	}
	if _, err := repo.Get(ctx, got.ID); err != nil {
		t.Errorf("kayıt depoda yok: %v", err)
	}
	e := waitEvent(t, created)
	if e.Data["name"] != "create_plane" || e.Data["id"] != got.ID {
		t.Errorf("SkillCreated verisi yanlış: %v", e.Data)
	}
}

func TestService_RegisterRejectsInvalidAndDuplicate(t *testing.T) {
	ctx := context.Background()
	svc, _, _ := newTestService(t)

	var ve *ValidationError
	if _, err := svc.Register(ctx, Skill{Name: "Bad Name"}); !errors.As(err, &ve) {
		t.Fatalf("*ValidationError bekleniyordu: %v", err)
	}
	if _, err := svc.Register(ctx, activeInput("same_name")); err != nil {
		t.Fatal(err)
	}
	if _, err := svc.Register(ctx, activeInput("same_name")); !errors.Is(err, ErrDuplicateName) {
		t.Errorf("ErrDuplicateName bekleniyordu: %v", err)
	}
}

func TestService_UpdateKeepsIdentityAndEmbedding(t *testing.T) {
	ctx := context.Background()
	svc, repo, bus := newTestService(t)
	updated := subscribe(bus, event.SkillUpdated)

	sk, err := svc.Register(ctx, activeInput("first_name"))
	if err != nil {
		t.Fatal(err)
	}
	stored, _ := repo.Get(ctx, sk.ID)
	stored.Embedding = []float32{0.5, 0.25}
	if err := repo.Update(ctx, stored); err != nil {
		t.Fatal(err)
	}

	in := activeInput("second_name")
	in.ID, in.Embedding = "baska", nil
	later := sk.UpdatedAt.Add(time.Hour)
	svc.now = func() time.Time { return later }

	got, err := svc.Update(ctx, sk.ID, in)
	if err != nil {
		t.Fatal(err)
	}
	if got.ID != sk.ID || !got.CreatedAt.Equal(sk.CreatedAt) {
		t.Errorf("kimlik/oluşturma zamanı korunmalı: %q %v", got.ID, got.CreatedAt)
	}
	if len(got.Embedding) != 2 {
		t.Errorf("embedding korunmalı: %v", got.Embedding)
	}
	if !got.UpdatedAt.Equal(later) || got.Name != "second_name" {
		t.Errorf("güncelleme uygulanmadı: %v %q", got.UpdatedAt, got.Name)
	}
	waitEvent(t, updated)

	if _, err := svc.Update(ctx, "yok", in); !errors.Is(err, ErrNotFound) {
		t.Errorf("ErrNotFound bekleniyordu: %v", err)
	}
}

func TestService_SetStatusRevalidates(t *testing.T) {
	ctx := context.Background()
	svc, _, _ := newTestService(t)

	draft, err := svc.Register(ctx, Skill{Name: "no_executor", Description: "taslak"})
	if err != nil {
		t.Fatal(err)
	}
	var ve *ValidationError
	if _, err := svc.SetStatus(ctx, draft.ID, StatusActive); !errors.As(err, &ve) || !hasField(ve.Errors, "executorConfig") {
		t.Fatalf("executor'suz taslak active yapılamamalı: %v", err)
	}

	ok, err := svc.Register(ctx, activeInput("has_executor"))
	if err != nil {
		t.Fatal(err)
	}
	off, err := svc.SetStatus(ctx, ok.ID, StatusDisabled)
	if err != nil || off.Status != StatusDisabled {
		t.Fatalf("devre dışı bırakma: %v %v", off, err)
	}
	if _, err := svc.SetStatus(ctx, "yok", StatusActive); !errors.Is(err, ErrNotFound) {
		t.Errorf("ErrNotFound bekleniyordu: %v", err)
	}
}

func TestService_DuplicateNamesAndTruncation(t *testing.T) {
	ctx := context.Background()
	svc, _, _ := newTestService(t)

	src, err := svc.Register(ctx, activeInput("base_skill"))
	if err != nil {
		t.Fatal(err)
	}
	c1, err := svc.Duplicate(ctx, src.ID)
	if err != nil {
		t.Fatal(err)
	}
	c2, err := svc.Duplicate(ctx, src.ID)
	if err != nil {
		t.Fatal(err)
	}
	if c1.Name != "base_skill_copy" || c2.Name != "base_skill_copy2" {
		t.Errorf("kopya adları: %q %q", c1.Name, c2.Name)
	}
	if c1.Status != StatusDraft || c1.ID == src.ID || c1.Embedding != nil {
		t.Errorf("kopya taslak, yeni kimlikli, embedding'siz olmalı: %+v", c1)
	}

	long, err := svc.Register(ctx, activeInput(strings.Repeat("a", 64)))
	if err != nil {
		t.Fatal(err)
	}
	cp, err := svc.Duplicate(ctx, long.ID)
	if err != nil {
		t.Fatalf("64 karakterlik adın kopyası: %v", err)
	}
	if len(cp.Name) > 64 || !strings.HasSuffix(cp.Name, "_copy") {
		t.Errorf("kopya adı 64'e kırpılmalı ve _copy ile bitmeli: %q (%d)", cp.Name, len(cp.Name))
	}

	if _, err := svc.Duplicate(ctx, "yok"); !errors.Is(err, ErrNotFound) {
		t.Errorf("ErrNotFound bekleniyordu: %v", err)
	}
}

func TestService_Delete(t *testing.T) {
	ctx := context.Background()
	svc, _, bus := newTestService(t)
	deleted := subscribe(bus, event.SkillDeleted)

	sk, err := svc.Register(ctx, activeInput("to_delete"))
	if err != nil {
		t.Fatal(err)
	}
	if err := svc.Delete(ctx, sk.ID); err != nil {
		t.Fatal(err)
	}
	if e := waitEvent(t, deleted); e.Data["name"] != "to_delete" {
		t.Errorf("SkillDeleted verisi: %v", e.Data)
	}
	if _, err := svc.Get(ctx, sk.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("silinen skill okunuyor: %v", err)
	}
	if err := svc.Delete(ctx, sk.ID); !errors.Is(err, ErrNotFound) {
		t.Errorf("ikinci silme ErrNotFound olmalı: %v", err)
	}
}

func TestService_ValidateDryRun(t *testing.T) {
	ctx := context.Background()
	svc, _, _ := newTestService(t)

	taken, err := svc.Register(ctx, activeInput("taken_name"))
	if err != nil {
		t.Fatal(err)
	}

	errs, err := svc.Validate(ctx, "", Skill{Name: "taken_name", Description: "d"})
	if err != nil || !hasField(errs, "name") {
		t.Errorf("yeni skill için ad çakışması bildirilmeli: %v %v", errs, err)
	}
	// Güncelleme: kendi adı çakışma sayılmaz.
	errs, err = svc.Validate(ctx, taken.ID, Skill{Name: "taken_name", Description: "d"})
	if err != nil || hasField(errs, "name") {
		t.Errorf("kendi adı çakışma sayılmamalı: %v %v", errs, err)
	}
	errs, err = svc.Validate(ctx, "", Skill{Name: "fresh_one", Description: "d"})
	if err != nil || errs == nil || len(errs) != 0 {
		t.Errorf("geçerli girdi: nil olmayan boş dilim bekleniyordu: %#v %v", errs, err)
	}
}

func TestService_NilBusIsFine(t *testing.T) {
	svc := NewService(NewMemoryRepo(), nil)
	if _, err := svc.Register(context.Background(), activeInput("no_bus")); err != nil {
		t.Fatal(err)
	}
}
