package api

import (
	"context"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"testing"

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/event"
	"fiko-ai/internal/protocol"
	"fiko-ai/internal/skill"
)

func skillsServer(t *testing.T) *Server {
	t.Helper()
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	bus := event.NewMemoryBus(nil, 16)
	t.Cleanup(bus.Close)
	return New(context.Background(), Config{
		Env: "test", Version: "test", DefaultUserID: "local", DefaultPageSize: 50, MaxPageSize: 200,
	}, Deps{
		Conversations: conversation.NewService(conversation.NewMemoryRepo()),
		Bus:           bus,
		Skills:        skill.NewService(skill.NewMemoryRepo(), bus),
	}, log)
}

func activeSkillBody(name string, tags ...string) map[string]any {
	return map[string]any{
		"name":           name,
		"description":    "Offset plane oluşturur.",
		"status":         "active",
		"executorType":   "tool",
		"executorConfig": map[string]any{"tool": "catia.create_plane"},
		"tags":           tags,
	}
}

func mustCreateSkill(t *testing.T, s *Server, body map[string]any) skill.Skill {
	t.Helper()
	resp, raw := do(t, s, http.MethodPost, "/api/skills", body, nil)
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("POST /api/skills = %d, beklenen 201 (gövde %s)", resp.StatusCode, raw)
	}
	return decode[skillResponse](t, raw).Skill
}

func errBody(t *testing.T, raw []byte) apiError {
	t.Helper()
	return decode[errorBody](t, raw).Error
}

func TestSkillCRUDFlow(t *testing.T) {
	s := skillsServer(t)

	sk := mustCreateSkill(t, s, activeSkillBody("create_plane", "CATIA", "geometry"))
	if sk.ID == "" || sk.CreatedAt.IsZero() || sk.Status != skill.StatusActive {
		t.Fatalf("oluşturulan skill eksik: %+v", sk)
	}
	if strings.Join(sk.Tags, ",") != "catia,geometry" {
		t.Errorf("etiketler küçük harfe çevrilmeli: %v", sk.Tags)
	}

	resp, raw := do(t, s, http.MethodGet, "/api/skills/"+sk.ID, nil, nil)
	if resp.StatusCode != http.StatusOK || decode[skillResponse](t, raw).Skill.Name != "create_plane" {
		t.Fatalf("GET = %d %s", resp.StatusCode, raw)
	}

	upd := activeSkillBody("create_plane")
	upd["description"] = "güncel açıklama"
	resp, raw = do(t, s, http.MethodPut, "/api/skills/"+sk.ID, upd, nil)
	got := decode[skillResponse](t, raw).Skill
	if resp.StatusCode != http.StatusOK || got.Description != "güncel açıklama" || got.ID != sk.ID || !got.CreatedAt.Equal(sk.CreatedAt) {
		t.Fatalf("PUT = %d %s", resp.StatusCode, raw)
	}

	resp, raw = do(t, s, http.MethodPut, "/api/skills/"+sk.ID+"/status", map[string]any{"status": "disabled"}, nil)
	if resp.StatusCode != http.StatusOK || decode[skillResponse](t, raw).Skill.Status != skill.StatusDisabled {
		t.Fatalf("status = %d %s", resp.StatusCode, raw)
	}

	resp, raw = do(t, s, http.MethodPost, "/api/skills/"+sk.ID+"/duplicate", nil, nil)
	cp := decode[skillResponse](t, raw).Skill
	if resp.StatusCode != http.StatusCreated || cp.Name != "create_plane_copy" || cp.Status != skill.StatusDraft || cp.ID == sk.ID {
		t.Fatalf("duplicate = %d %s", resp.StatusCode, raw)
	}

	resp, _ = do(t, s, http.MethodDelete, "/api/skills/"+sk.ID, nil, nil)
	if resp.StatusCode != http.StatusNoContent {
		t.Fatalf("DELETE = %d, beklenen 204", resp.StatusCode)
	}
	resp, raw = do(t, s, http.MethodGet, "/api/skills/"+sk.ID, nil, nil)
	if resp.StatusCode != http.StatusNotFound || errBody(t, raw).Code != protocol.ErrCodeNotFound {
		t.Errorf("silinen skill: %d %s", resp.StatusCode, raw)
	}
}

func TestSkillValidationErrorCarriesFields(t *testing.T) {
	s := skillsServer(t)
	resp, raw := do(t, s, http.MethodPost, "/api/skills", map[string]any{"name": "Bad Name"}, nil)
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("durum = %d, beklenen 400 (%s)", resp.StatusCode, raw)
	}
	e := errBody(t, raw)
	if e.Code != protocol.ErrCodeValidation {
		t.Errorf("kod = %q, beklenen %q", e.Code, protocol.ErrCodeValidation)
	}
	fields := map[string]bool{}
	for _, f := range e.Fields {
		fields[f.Field] = true
	}
	if !fields["name"] || !fields["description"] {
		t.Errorf("tüm alan hataları dönmeli (name, description): %v", e.Fields)
	}
}

func TestSkillDuplicateNameIsConflict(t *testing.T) {
	s := skillsServer(t)
	mustCreateSkill(t, s, activeSkillBody("same_name"))
	resp, raw := do(t, s, http.MethodPost, "/api/skills", activeSkillBody("same_name"), nil)
	if resp.StatusCode != http.StatusConflict || errBody(t, raw).Code != protocol.ErrCodeConflict {
		t.Fatalf("%d %s, beklenen 409 conflict", resp.StatusCode, raw)
	}
}

func TestSkillBadBodyAndUnknownID(t *testing.T) {
	s := skillsServer(t)

	resp, raw := do(t, s, http.MethodPost, "/api/skills", nil, nil) // gövde yok
	if resp.StatusCode != http.StatusBadRequest || errBody(t, raw).Code != protocol.ErrCodeBadRequest {
		t.Errorf("gövdesiz POST: %d %s", resp.StatusCode, raw)
	}
	for _, c := range []struct{ method, path string }{
		{http.MethodGet, "/api/skills/yok"},
		{http.MethodDelete, "/api/skills/yok"},
		{http.MethodPost, "/api/skills/yok/duplicate"},
	} {
		resp, raw := do(t, s, c.method, c.path, nil, nil)
		if resp.StatusCode != http.StatusNotFound || errBody(t, raw).Code != protocol.ErrCodeNotFound {
			t.Errorf("%s %s: %d %s, beklenen 404", c.method, c.path, resp.StatusCode, raw)
		}
	}
	resp, _ = do(t, s, http.MethodPut, "/api/skills/yok", activeSkillBody("x_y"), nil)
	if resp.StatusCode != http.StatusNotFound {
		t.Errorf("PUT bilinmeyen id: %d, beklenen 404", resp.StatusCode)
	}
}

func TestSkillSetStatusRules(t *testing.T) {
	s := skillsServer(t)
	draft := mustCreateSkill(t, s, map[string]any{"name": "no_executor", "description": "taslak"})

	// Boş status sessizce draft'a dönmemeli.
	resp, raw := do(t, s, http.MethodPut, "/api/skills/"+draft.ID+"/status", map[string]any{}, nil)
	if resp.StatusCode != http.StatusBadRequest || errBody(t, raw).Code != protocol.ErrCodeBadRequest {
		t.Errorf("boş status: %d %s", resp.StatusCode, raw)
	}
	// Executor'suz taslak active yapılamaz: kurallar yeniden denetlenir.
	resp, raw = do(t, s, http.MethodPut, "/api/skills/"+draft.ID+"/status", map[string]any{"status": "active"}, nil)
	e := errBody(t, raw)
	if resp.StatusCode != http.StatusBadRequest || e.Code != protocol.ErrCodeValidation || len(e.Fields) == 0 || e.Fields[0].Field != "executorConfig" {
		t.Errorf("executor'suz active: %d %s", resp.StatusCode, raw)
	}
}

func TestSkillValidateEndpoint(t *testing.T) {
	s := skillsServer(t)
	existing := mustCreateSkill(t, s, activeSkillBody("taken_name"))

	// Geçerli girdi: 200, valid=true ve errors null DEĞİL boş dizi.
	resp, raw := do(t, s, http.MethodPost, "/api/skills/validate", map[string]any{"name": "fresh_one", "description": "d"}, nil)
	if resp.StatusCode != http.StatusOK || !decode[skillValidateResponse](t, raw).Valid || !strings.Contains(string(raw), `"errors":[]`) {
		t.Errorf("geçerli: %d %s", resp.StatusCode, raw)
	}
	// Geçersizlik hata değil sonuçtur: yine 200.
	resp, raw = do(t, s, http.MethodPost, "/api/skills/validate", map[string]any{"name": "Bad Name"}, nil)
	if v := decode[skillValidateResponse](t, raw); resp.StatusCode != http.StatusOK || v.Valid || len(v.Errors) < 2 {
		t.Errorf("geçersiz: %d %s", resp.StatusCode, raw)
	}
	// Yeni skill için ad çakışması bildirilir; güncellemede (id verilince) kendi adı sorun değildir.
	body := map[string]any{"name": "taken_name", "description": "d"}
	_, raw = do(t, s, http.MethodPost, "/api/skills/validate", body, nil)
	if decode[skillValidateResponse](t, raw).Valid {
		t.Error("alınmış ad geçerli sayıldı")
	}
	_, raw = do(t, s, http.MethodPost, "/api/skills/validate?id="+existing.ID, body, nil)
	if !decode[skillValidateResponse](t, raw).Valid {
		t.Errorf("kendi adı çakışma sayılmamalı: %s", raw)
	}
}

func TestSkillListFiltersAndPaging(t *testing.T) {
	s := skillsServer(t)
	mustCreateSkill(t, s, activeSkillBody("zeta", "catia"))
	mustCreateSkill(t, s, activeSkillBody("alpha", "catia", "geometry"))
	mustCreateSkill(t, s, map[string]any{"name": "mid", "description": "Rapor yazar", "tags": []string{"docs"}})

	names := func(query string) []string {
		t.Helper()
		resp, raw := do(t, s, http.MethodGet, "/api/skills"+query, nil, nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("%s = %d %s", query, resp.StatusCode, raw)
		}
		r := decode[skillsResponse](t, raw)
		out := make([]string, 0, len(r.Items))
		for _, sk := range r.Items {
			out = append(out, sk.Name)
		}
		if r.Count != len(out) {
			t.Errorf("count = %d, öğe sayısı %d", r.Count, len(out))
		}
		return out
	}
	eq := func(label string, got []string, want ...string) {
		t.Helper()
		if strings.Join(got, ",") != strings.Join(want, ",") {
			t.Errorf("%s: %v, beklenen %v", label, got, want)
		}
	}

	eq("hepsi, ada göre", names(""), "alpha", "mid", "zeta")
	eq("durum", names("?status=draft"), "mid")
	eq("etiket (büyük harf sorgu küçültülür)", names("?tag=CATIA"), "alpha", "zeta")
	eq("sorgu", names("?q=rapor"), "mid")
	eq("sayfalama", names("?limit=1&offset=1"), "mid")

	resp, raw := do(t, s, http.MethodGet, "/api/skills?status=weird", nil, nil)
	if resp.StatusCode != http.StatusBadRequest || errBody(t, raw).Code != protocol.ErrCodeBadRequest || strings.Contains(string(raw), `"items"`) {
		t.Errorf("geçersiz status: %d %s", resp.StatusCode, raw)
	}
}

func TestSkillsDisabledWhenNotConfigured(t *testing.T) {
	s, _ := testServer(t, &fakeProvider{}) // Skills verilmedi
	resp, raw := do(t, s, http.MethodGet, "/api/skills", nil, nil)
	if resp.StatusCode != http.StatusServiceUnavailable || errBody(t, raw).Code != protocol.ErrCodeInternal {
		t.Errorf("%d %s, beklenen 503", resp.StatusCode, raw)
	}
}

// Regresyon: geçersiz limit/offset'te hata gövdesi normal yanıtla EZİLMEMELİ
// (eskiden parseLimit fail() sonucunu — yani nil'i — döndürüyor, handler devam ediyordu).
func TestPagingErrorBodyIsError(t *testing.T) {
	s := skillsServer(t)
	for _, path := range []string{
		"/api/skills?limit=abc", "/api/skills?offset=-1",
		"/api/conversations?limit=abc", "/api/conversations?offset=xyz",
		"/api/conversations/c1/messages?limit=-3",
	} {
		resp, raw := do(t, s, http.MethodGet, path, nil, nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Errorf("%s: durum %d, beklenen 400", path, resp.StatusCode)
			continue
		}
		if e := errBody(t, raw); e.Code != protocol.ErrCodeBadRequest || strings.Contains(string(raw), `"items"`) {
			t.Errorf("%s: gövde hata olmalı, geldi: %s", path, raw)
		}
	}
}
