package api

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

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/memory"
	"fiko-ai/internal/protocol"
)

// memoryServer yalnızca kalıcı belleği bağlı bir API sunucusu kurar. Embedding BAĞLI DEĞİLDİR:
// arama anahtar kelimeyle çalışır (bellek, sağlayıcısız da ayakta kalmalı).
func memoryServer(t *testing.T) *Server {
	t.Helper()
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	svc := memory.NewService(memory.NewMemoryRepo(), nil, memory.Options{Logger: log})
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		_ = svc.Close(ctx)
	})
	return New(context.Background(), Config{
		Env: "test", Version: "test", DefaultUserID: "local", DefaultPageSize: 50, MaxPageSize: 200,
	}, Deps{
		Conversations: conversation.NewService(conversation.NewMemoryRepo()),
		Memory:        svc,
	}, log)
}

func mustCreateMemory(t *testing.T, s *Server, content string, headers map[string]string) memoryResponse {
	t.Helper()
	resp, raw := do(t, s, http.MethodPost, "/api/memories", map[string]any{"content": content}, headers)
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("POST /api/memories = %d, beklenen 201 (gövde %s)", resp.StatusCode, raw)
	}
	return decode[memoryResponse](t, raw)
}

// Bellek yapılandırılmamışsa (MEMORY_ENABLED=false) uçlar 503 döner, sunucu ayakta kalır.
func TestMemoryUnavailableIs503(t *testing.T) {
	s, _ := testServer(t, &fakeProvider{}) // Deps.Memory yok
	for _, tc := range []struct{ method, path string }{
		{http.MethodGet, "/api/memories"},
		{http.MethodPost, "/api/memories/search"},
		{http.MethodDelete, "/api/memories/mem_x"},
	} {
		resp, raw := do(t, s, tc.method, tc.path, nil, nil)
		if resp.StatusCode != http.StatusServiceUnavailable {
			t.Errorf("%s %s = %d, beklenen 503 (gövde %s)", tc.method, tc.path, resp.StatusCode, raw)
		}
	}
}

// Anı CRUD tam turu: oluştur → oku → listele → düzenle → ara → sil → 404.
func TestMemoryCRUDFlow(t *testing.T) {
	s := memoryServer(t)

	created := mustCreateMemory(t, s, "Kullanıcı Angular kullanıyor.", nil)
	m := created.Memory
	if created.Outcome != memory.Created || m.ID == "" || m.UserID != "local" {
		t.Fatalf("oluşturulan anı eksik: %+v", created)
	}
	if m.Origin != memory.OriginUser || m.Kind != memory.KindLongTerm || m.Importance != 0.8 {
		t.Errorf("elle eklenen anı varsayılanları: origin=%s kind=%s importance=%v", m.Origin, m.Kind, m.Importance)
	}

	resp, raw := do(t, s, http.MethodGet, "/api/memories/"+m.ID, nil, nil)
	if resp.StatusCode != http.StatusOK || decode[memoryResponse](t, raw).Memory.Content != "Kullanıcı Angular kullanıyor." {
		t.Fatalf("GET = %d %s", resp.StatusCode, raw)
	}
	if strings.Contains(string(raw), "embedding") || strings.Contains(string(raw), "\"hash\"") {
		t.Errorf("vektör/özet istemciye sızmamalı: %s", raw)
	}

	resp, raw = do(t, s, http.MethodGet, "/api/memories", nil, nil)
	list := decode[memoriesResponse](t, raw)
	if resp.StatusCode != http.StatusOK || list.Count != 1 || list.Total != 1 {
		t.Fatalf("liste = %d %s", resp.StatusCode, raw)
	}

	resp, raw = do(t, s, http.MethodPut, "/api/memories/"+m.ID,
		map[string]any{"content": "Kullanıcı React kullanıyor.", "tags": []string{"Frontend"}, "kind": "semantic"}, nil)
	upd := decode[memoryResponse](t, raw).Memory
	if resp.StatusCode != http.StatusOK || upd.Content != "Kullanıcı React kullanıyor." ||
		upd.Kind != memory.KindSemantic || len(upd.Tags) != 1 || upd.Tags[0] != "frontend" {
		t.Fatalf("PUT = %d %s", resp.StatusCode, raw)
	}

	resp, raw = do(t, s, http.MethodPost, "/api/memories/search", map[string]any{"query": "react"}, nil)
	found := decode[memorySearchResponse](t, raw)
	if resp.StatusCode != http.StatusOK || found.Count != 1 || found.Items[0].ID != m.ID || found.Items[0].Score <= 0 {
		t.Fatalf("search = %d %s", resp.StatusCode, raw)
	}

	resp, _ = do(t, s, http.MethodDelete, "/api/memories/"+m.ID, nil, nil)
	if resp.StatusCode != http.StatusNoContent {
		t.Errorf("DELETE = %d, beklenen 204", resp.StatusCode)
	}
	if resp, raw := do(t, s, http.MethodGet, "/api/memories/"+m.ID, nil, nil); resp.StatusCode != http.StatusNotFound {
		t.Errorf("silinen anı = %d, beklenen 404 (gövde %s)", resp.StatusCode, raw)
	}
}

// Aynı bilgi ikinci kez eklenmez: 200 + outcome=unchanged (201 değil).
func TestMemoryDuplicateIsNotAddedTwice(t *testing.T) {
	s := memoryServer(t)
	first := mustCreateMemory(t, s, "Kullanıcı Go ile backend yazıyor.", nil)

	// Büyük/küçük harf ve boşluk farkı aynı bilgidir.
	resp, raw := do(t, s, http.MethodPost, "/api/memories",
		map[string]any{"content": "  kullanıcı GO ile   backend yazıyor. "}, nil)
	again := decode[memoryResponse](t, raw)
	if resp.StatusCode != http.StatusOK || again.Outcome != memory.Unchanged || again.Memory.ID != first.Memory.ID {
		t.Fatalf("tekrar = %d %s", resp.StatusCode, raw)
	}
	_, raw = do(t, s, http.MethodGet, "/api/memories", nil, nil)
	if got := decode[memoriesResponse](t, raw); got.Total != 1 {
		t.Errorf("toplam %d, 1 bekleniyordu", got.Total)
	}
}

func TestMemoryValidationErrorsHaveFields(t *testing.T) {
	s := memoryServer(t)

	resp, raw := do(t, s, http.MethodPost, "/api/memories", map[string]any{"content": "ab"}, nil)
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("kısa içerik = %d, beklenen 400 (gövde %s)", resp.StatusCode, raw)
	}
	if e := errBody(t, raw); e.Code != protocol.ErrCodeValidation || len(e.Fields) == 0 || e.Fields[0].Field != "content" {
		t.Errorf("alan hatası bekleniyordu: %+v", e)
	}

	resp, raw = do(t, s, http.MethodPost, "/api/memories",
		map[string]any{"content": "Geçerli bir içerik.", "kind": "nonsense"}, nil)
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("geçersiz kind = %d, beklenen 400 (gövde %s)", resp.StatusCode, raw)
	}

	// Boş güncelleme anlamsızdır.
	m := mustCreateMemory(t, s, "Kullanıcı kahveyi sütsüz içiyor.", nil).Memory
	if resp, raw := do(t, s, http.MethodPut, "/api/memories/"+m.ID, map[string]any{}, nil); resp.StatusCode != http.StatusBadRequest {
		t.Errorf("boş PUT = %d, beklenen 400 (gövde %s)", resp.StatusCode, raw)
	}

	// Boş arama metni.
	if resp, raw := do(t, s, http.MethodPost, "/api/memories/search", map[string]any{"query": "  "}, nil); resp.StatusCode != http.StatusBadRequest {
		t.Errorf("boş arama = %d, beklenen 400 (gövde %s)", resp.StatusCode, raw)
	}
}

// Bir kullanıcı başkasının anısını göremez, değiştiremez, silemez; varlığı bile sızmaz (404).
func TestMemoryIsolationBetweenUsers(t *testing.T) {
	s := memoryServer(t)
	alice := map[string]string{"X-Fiko-User": "alice"}
	bob := map[string]string{"X-Fiko-User": "bob"}

	m := mustCreateMemory(t, s, "Alice Angular kullanıyor.", alice).Memory
	if m.UserID != "alice" {
		t.Fatalf("anı alice'e ait olmalı: %+v", m)
	}

	if resp, raw := do(t, s, http.MethodGet, "/api/memories/"+m.ID, nil, bob); resp.StatusCode != http.StatusNotFound {
		t.Errorf("bob GET = %d, beklenen 404 (gövde %s)", resp.StatusCode, raw)
	}
	if resp, _ := do(t, s, http.MethodPut, "/api/memories/"+m.ID, map[string]any{"content": "Bob ezmeye çalışıyor."}, bob); resp.StatusCode != http.StatusNotFound {
		t.Errorf("bob PUT = %d, beklenen 404", resp.StatusCode)
	}
	if resp, _ := do(t, s, http.MethodDelete, "/api/memories/"+m.ID, nil, bob); resp.StatusCode != http.StatusNotFound {
		t.Errorf("bob DELETE = %d, beklenen 404", resp.StatusCode)
	}
	_, raw := do(t, s, http.MethodGet, "/api/memories", nil, bob)
	if got := decode[memoriesResponse](t, raw); got.Total != 0 || got.Count != 0 {
		t.Errorf("bob alice'in anısını listede görmemeli: %+v", got)
	}
	_, raw = do(t, s, http.MethodPost, "/api/memories/search", map[string]any{"query": "angular"}, bob)
	if got := decode[memorySearchResponse](t, raw); got.Count != 0 {
		t.Errorf("bob aramada alice'in anısını görmemeli: %+v", got)
	}
	// Bob'un silme girişimi alice'in anısını etkilememiş olmalı.
	if resp, _ := do(t, s, http.MethodGet, "/api/memories/"+m.ID, nil, alice); resp.StatusCode != http.StatusOK {
		t.Errorf("alice'in anısı hâlâ duruyor olmalı, GET = %d", resp.StatusCode)
	}
}

// "Beni unut": geri alınamaz olduğu için açık onay ister, yalnızca isteyen kullanıcının anılarını siler.
func TestMemoryDeleteAllRequiresConfirm(t *testing.T) {
	s := memoryServer(t)
	alice := map[string]string{"X-Fiko-User": "alice"}
	bob := map[string]string{"X-Fiko-User": "bob"}
	mustCreateMemory(t, s, "Alice Angular kullanıyor.", alice)
	mustCreateMemory(t, s, "Alice kahveyi sütsüz içiyor.", alice)
	mustCreateMemory(t, s, "Bob Go kullanıyor.", bob)

	if resp, raw := do(t, s, http.MethodDelete, "/api/memories", nil, alice); resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("onaysız toplu silme = %d, beklenen 400 (gövde %s)", resp.StatusCode, raw)
	}
	if _, raw := do(t, s, http.MethodGet, "/api/memories", nil, alice); decode[memoriesResponse](t, raw).Total != 2 {
		t.Fatal("onaysız istek hiçbir şeyi silmemeli")
	}

	resp, raw := do(t, s, http.MethodDelete, "/api/memories?confirm=true", nil, alice)
	var out struct {
		Deleted int `json:"deleted"`
	}
	decodeInto(t, raw, &out)
	if resp.StatusCode != http.StatusOK || out.Deleted != 2 {
		t.Fatalf("toplu silme = %d %s", resp.StatusCode, raw)
	}
	if _, raw := do(t, s, http.MethodGet, "/api/memories", nil, alice); decode[memoriesResponse](t, raw).Total != 0 {
		t.Error("alice'in anıları silinmeliydi")
	}
	if _, raw := do(t, s, http.MethodGet, "/api/memories", nil, bob); decode[memoriesResponse](t, raw).Total != 1 {
		t.Error("bob'un anısı etkilenmemeli")
	}
}

func TestMemoryListPagingAndFilters(t *testing.T) {
	s := memoryServer(t)
	for _, c := range []string{"Birinci anı kaydı.", "İkinci anı kaydı.", "Üçüncü anı kaydı."} {
		mustCreateMemory(t, s, c, nil)
	}
	_, raw := do(t, s, http.MethodGet, "/api/memories?limit=2", nil, nil)
	page := decode[memoriesResponse](t, raw)
	if page.Count != 2 || page.Total != 3 || page.Limit != 2 {
		t.Errorf("sayfalama: %+v", page)
	}
	_, raw = do(t, s, http.MethodGet, "/api/memories?q=ikinci", nil, nil)
	if got := decode[memoriesResponse](t, raw); got.Count != 1 || got.Total != 3 {
		t.Errorf("q süzgeci: %+v", got)
	}
	if resp, raw := do(t, s, http.MethodGet, "/api/memories?kind=nonsense", nil, nil); resp.StatusCode != http.StatusBadRequest {
		t.Errorf("geçersiz kind süzgeci = %d, beklenen 400 (gövde %s)", resp.StatusCode, raw)
	}
}

// Embedding yokken yeniden indeksleme 503'tür (sunucu çökmez, nedeni söyler).
func TestMemoryReindexWithoutEmbedderIs503(t *testing.T) {
	s := memoryServer(t)
	resp, raw := do(t, s, http.MethodPost, "/api/memories/reindex", nil, nil)
	if resp.StatusCode != http.StatusServiceUnavailable {
		t.Fatalf("reindex = %d, beklenen 503 (gövde %s)", resp.StatusCode, raw)
	}
	if resp, _ := do(t, s, http.MethodPost, "/api/memories/reindex?force=maybe", nil, nil); resp.StatusCode != http.StatusBadRequest {
		t.Errorf("geçersiz force = %d, beklenen 400", resp.StatusCode)
	}
}
