package api

import (
	"context"
	"errors"
	"hash/fnv"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"testing"
	"unicode"

	"github.com/gofiber/fiber/v2"

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

// apiEmbedder deterministik bag-of-words sahte embedder'dır (gerçek Ollama gerekmez).
type apiEmbedder struct{ err error }

func (e apiEmbedder) Embed(_ context.Context, text string) ([]float32, error) {
	if e.err != nil {
		return nil, e.err
	}
	v := make([]float32, 256)
	for _, tok := range strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsDigit(r)
	}) {
		h := fnv.New32a()
		_, _ = h.Write([]byte(tok))
		v[h.Sum32()%256]++
	}
	return v, nil
}

// searchServer skill servisini isteğe bağlı embedder ile kurar.
func searchServer(t *testing.T, emb llm.Embedder) *Server {
	t.Helper()
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	bus := event.NewMemoryBus(nil, 16)
	t.Cleanup(bus.Close)
	svc := skill.NewService(skill.NewMemoryRepo(), bus)
	if emb != nil {
		svc.SetEmbedder(emb)
	}
	return New(context.Background(), Config{
		Env: "test", Version: "test", DefaultUserID: "local", DefaultPageSize: 50, MaxPageSize: 200,
	}, Deps{
		Conversations: conversation.NewService(conversation.NewMemoryRepo()),
		Bus:           bus,
		Skills:        svc,
	}, log)
}

func describedSkill(name, desc, status string) map[string]any {
	b := map[string]any{"name": name, "description": desc, "status": status}
	if status == "active" {
		b["executorType"] = "tool"
		b["executorConfig"] = map[string]any{"tool": "demo." + name}
	}
	return b
}

func seedAPISearch(t *testing.T, s *Server) {
	t.Helper()
	mustCreateSkill(t, s, describedSkill("create_plane", "Offset plane oluşturur.", "active"))
	mustCreateSkill(t, s, describedSkill("delete_file", "Diskteki bir dosyayı siler.", "active"))
	mustCreateSkill(t, s, describedSkill("draft_plane", "Taslak plane skill'i.", "draft"))
}

func TestSkillSearchEndpoint(t *testing.T) {
	s := searchServer(t, apiEmbedder{})
	seedAPISearch(t, s)

	resp, raw := do(t, s, http.MethodPost, "/api/skills/search",
		map[string]any{"query": "plane oluştur", "limit": 5}, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("POST /api/skills/search = %d %s", resp.StatusCode, raw)
	}
	out := decode[skillSearchResponse](t, raw)
	if out.Mode != skill.ModeHybrid {
		t.Errorf("varsayılan mod hybrid olmalı: %q", out.Mode)
	}
	if out.Count != len(out.Items) || out.Count == 0 {
		t.Fatalf("count tutarsız: %+v", out)
	}
	if out.Items[0].Skill.Name != "create_plane" {
		t.Fatalf("ilk sonuç create_plane olmalı: %+v", out.Items)
	}
	for _, it := range out.Items {
		if it.Skill.Name == "draft_plane" {
			t.Fatal("taslak skill aramada dönmemeli")
		}
		if it.Score <= 0 || it.Score > 1 {
			t.Fatalf("skor 0..1 olmalı: %v", it.Score)
		}
	}
	// Embedding API'de asla dönmemeli (büyük ve işe yaramaz).
	if strings.Contains(string(raw), "embedding") {
		t.Fatalf("yanıt embedding içermemeli: %s", raw)
	}
}

func TestSkillSearchKeywordWorksWithoutEmbedder(t *testing.T) {
	s := searchServer(t, nil)
	seedAPISearch(t, s)

	resp, raw := do(t, s, http.MethodPost, "/api/skills/search",
		map[string]any{"query": "dosyayı siler", "mode": "keyword"}, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("= %d %s", resp.StatusCode, raw)
	}
	out := decode[skillSearchResponse](t, raw)
	if out.Count != 1 || out.Items[0].Skill.Name != "delete_file" || out.Mode != skill.ModeKeyword {
		t.Fatalf("yalnızca delete_file beklenirdi: %+v", out)
	}
}

func TestSkillSearchValidation(t *testing.T) {
	s := searchServer(t, nil)

	resp, raw := do(t, s, http.MethodPost, "/api/skills/search", map[string]any{"query": "  "}, nil)
	eb := errBody(t, raw)
	if resp.StatusCode != http.StatusBadRequest || eb.Code != protocol.ErrCodeValidation || len(eb.Fields) == 0 || eb.Fields[0].Field != "query" {
		t.Fatalf("boş sorgu: %d %+v", resp.StatusCode, eb)
	}

	resp, raw = do(t, s, http.MethodPost, "/api/skills/search", map[string]any{"query": "x", "mode": "fuzzy"}, nil)
	if resp.StatusCode != http.StatusBadRequest || errBody(t, raw).Code != protocol.ErrCodeValidation {
		t.Fatalf("geçersiz mod: %d %s", resp.StatusCode, raw)
	}

	resp, raw = do(t, s, http.MethodPost, "/api/skills/search", "bu bir nesne değil", nil)
	if resp.StatusCode != http.StatusBadRequest || errBody(t, raw).Code != protocol.ErrCodeBadRequest {
		t.Fatalf("bozuk gövde: %d %s", resp.StatusCode, raw)
	}
}

func TestSkillSearchEmbeddingErrors(t *testing.T) {
	// Embedder yok + semantic → 503.
	s := searchServer(t, nil)
	resp, raw := do(t, s, http.MethodPost, "/api/skills/search", map[string]any{"query": "plane", "mode": "semantic"}, nil)
	if resp.StatusCode != http.StatusServiceUnavailable || errBody(t, raw).Code != protocol.ErrCodeLLM {
		t.Fatalf("embedder yok: %d %s", resp.StatusCode, raw)
	}

	// Sağlayıcı hatası + semantic → 502 (sessizce düşmez); hybrid ise 200'e düşer.
	s = searchServer(t, apiEmbedder{err: &llm.ProviderError{Provider: "ollama", StatusCode: 404, Message: "model yok"}})
	resp, raw = do(t, s, http.MethodPost, "/api/skills/search", map[string]any{"query": "plane", "mode": "semantic"}, nil)
	if resp.StatusCode != http.StatusBadGateway || errBody(t, raw).Code != protocol.ErrCodeLLM {
		t.Fatalf("sağlayıcı hatası: %d %s", resp.StatusCode, raw)
	}
	resp, raw = do(t, s, http.MethodPost, "/api/skills/search", map[string]any{"query": "plane"}, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("hybrid sağlayıcı hatasında anahtar kelimeye düşmeli: %d %s", resp.StatusCode, raw)
	}
}

func TestSkillReindexEndpoint(t *testing.T) {
	s := searchServer(t, nil) // önce embedder'sız: vektörsüz kayıtlar
	seedAPISearch(t, s)

	// Embedder yokken → 503.
	resp, raw := do(t, s, http.MethodPost, "/api/skills/reindex", nil, nil)
	if resp.StatusCode != http.StatusServiceUnavailable {
		t.Fatalf("embedder yok: %d %s", resp.StatusCode, raw)
	}

	s.deps.Skills.SetEmbedder(apiEmbedder{})

	resp, raw = do(t, s, http.MethodPost, "/api/skills/reindex", nil, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("reindex = %d %s", resp.StatusCode, raw)
	}
	if rep := decode[skill.ReindexReport](t, raw); rep.Total != 3 || rep.Embedded != 3 || rep.Failed != 0 {
		t.Fatalf("eksikler doldurulmalı: %+v", rep)
	}

	_, raw = do(t, s, http.MethodPost, "/api/skills/reindex", nil, nil)
	if rep := decode[skill.ReindexReport](t, raw); rep.Embedded != 0 || rep.Skipped != 3 {
		t.Fatalf("ikinci çalıştırma işsiz kalmalı: %+v", rep)
	}

	_, raw = do(t, s, http.MethodPost, "/api/skills/reindex?force=true", nil, nil)
	if rep := decode[skill.ReindexReport](t, raw); rep.Embedded != 3 {
		t.Fatalf("force hepsini yeniden üretmeli: %+v", rep)
	}

	resp, raw = do(t, s, http.MethodPost, "/api/skills/reindex?force=belki", nil, nil)
	if resp.StatusCode != http.StatusBadRequest || errBody(t, raw).Code != protocol.ErrCodeBadRequest {
		t.Fatalf("geçersiz force: %d %s", resp.StatusCode, raw)
	}
}

func TestFailEmbeddingMapsSentinels(t *testing.T) {
	// failEmbedding'in bilinmeyen hatayı yutmadığını (failDomain'e bıraktığını) doğrular.
	s := searchServer(t, nil)
	s.app.Get("/__boom", func(c *fiber.Ctx) error { return failEmbedding(c, errors.New("beklenmeyen")) })
	resp, _ := do(t, s, http.MethodGet, "/__boom", nil, nil)
	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("bilinmeyen hata 500 olmalı: %d", resp.StatusCode)
	}
}
