package api

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

	"fiko-ai/internal/agent"
	"fiko-ai/internal/api/ws"
	"fiko-ai/internal/claim"
	"fiko-ai/internal/conversation"
	"fiko-ai/internal/event"
	"fiko-ai/internal/prompt"
	"fiko-ai/internal/settings"
	"fiko-ai/internal/tool"
	"fiko-ai/internal/user"
)

// claims_test.go — /api/claims (Faz 5). En önemli üç şey: (1) yalnızca yazar ya da patron
// düzenler/siler, (2) her işlem denetime yazılır ve silinen değer geçmişte kalır, (3) yetki yalnızca
// doğrulanmış hesaptan gelir (üyelik kapalıyken uçlar reddeder).

// testClaimDir hesap servisini claim.Directory'ye uyarlar (cmd/server.claimDirectory ile aynı eşleme).
type testClaimDir struct{ users *user.Service }

func (d testClaimDir) Lookup(ctx context.Context, id string) (*claim.Person, error) {
	u, err := d.users.ByID(ctx, id)
	if err != nil {
		if errors.Is(err, user.ErrNotFound) {
			return nil, nil
		}
		return nil, err
	}
	role := claim.RoleUye
	if u.Role == user.RoleAdmin {
		role = claim.RolePatron
	}
	return &claim.Person{ID: u.ID, Name: u.Username, Role: role, Disabled: u.Disabled}, nil
}

type claimsEnv struct {
	s     *Server
	repo  *claim.MemoryRepo
	audit *claim.MemoryAuditRepo
}

func claimsServer(t *testing.T, usersEnabled bool, withClaims bool) *claimsEnv {
	t.Helper()
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	svc := conversation.NewService(conversation.NewMemoryRepo())
	bus := event.NewMemoryBus(nil, 16)
	ag := agent.New(svc, nil, prompt.Default(), bus, log, agent.Config{})
	hub := ws.NewHub(ag, log, ws.Options{PingInterval: time.Hour, PongWait: 2 * time.Hour, WriteWait: time.Second})
	users, err := user.NewService(user.NewMemoryStore(), user.ServiceOptions{BcryptCost: 4})
	if err != nil {
		t.Fatal(err)
	}
	env := &claimsEnv{repo: claim.NewMemoryRepo(), audit: claim.NewMemoryAuditRepo()}
	deps := Deps{
		Conversations: svc, Hub: hub, Bus: bus,
		Grants:   tool.NewGrantStore(tool.DefaultPolicy()),
		Settings: settings.New(settings.Values{Model: "m"}, log, bus),
	}
	if usersEnabled {
		deps.Users = users
	}
	if withClaims {
		deps.Claims = claim.NewService(env.repo, env.audit, nil, nil, claim.Config{}, log)
		deps.ClaimIdentity = claim.NewIdentity(testClaimDir{users: users}, usersEnabled, "local")
		t.Cleanup(deps.Claims.Close)
	}
	env.s = New(context.Background(), Config{Env: "test", Version: "test", UsersEnabled: usersEnabled, DefaultUserID: "local"}, deps, log)
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		_ = env.s.Shutdown(ctx)
	})
	return env
}

func (e *claimsEnv) seed(t *testing.T, id, kind, text, authorID string) {
	t.Helper()
	now := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
	c := claim.Claim{
		ID: id, Kind: kind, Text: text, Key: claim.NormalizeKey(text), AuthorID: authorID, AuthorName: "yazar",
		CreatedAt: now, UpdatedAt: now, Status: claim.StatusActive, Embedding: []float32{1, 0, 0}, EmbedModel: "m",
	}
	if err := e.repo.Create(context.Background(), &c); err != nil {
		t.Fatal(err)
	}
}

type claimsActors struct{ patron, ali, veli authReply }

func registerClaimActors(t *testing.T, s *Server) claimsActors {
	t.Helper()
	return claimsActors{
		patron: registerUser(t, s, "patron"), // ilk üye admin → patron
		ali:    registerUser(t, s, "ali"),
		veli:   registerUser(t, s, "veli"),
	}
}

func TestClaimsAPI_ListAndGetNeedSessionAndHideVector(t *testing.T) {
	e := claimsServer(t, true, true)
	a := registerClaimActors(t, e.s)
	e.seed(t, "c1", claim.KindFirma, "ACME Kalıp", a.ali.User.ID)
	e.seed(t, "c2", claim.KindKisi, "Ahmet Bey", a.veli.User.ID)

	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims", nil, nil); resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("oturumsuz liste 401 olmalı: %d", resp.StatusCode)
	}
	resp, raw := do(t, e.s, http.MethodGet, "/api/claims?kind=firma", nil, bearer(a.veli.Token))
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("liste: %d %s", resp.StatusCode, raw)
	}
	got := decode[claimListResponse](t, raw)
	if got.Total != 1 || len(got.Items) != 1 || got.Items[0].ID != "c1" {
		t.Fatalf("kind süzgeci yanlış: %+v", got)
	}
	if strings.Contains(strings.ToLower(string(raw)), "embedding") {
		t.Fatalf("vektör yanıta sızdı: %s", raw)
	}
	// Başkasının kaydını görüntüleme serbest (ekip ortağı).
	if resp, raw := do(t, e.s, http.MethodGet, "/api/claims/c1", nil, bearer(a.veli.Token)); resp.StatusCode != http.StatusOK ||
		decode[claim.Claim](t, raw).Text != "ACME Kalıp" {
		t.Fatalf("tek kayıt: %d %s", resp.StatusCode, raw)
	}
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/yok", nil, bearer(a.veli.Token)); resp.StatusCode != http.StatusNotFound {
		t.Errorf("olmayan kayıt 404 olmalı: %d", resp.StatusCode)
	}
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims?status=bozuk", nil, bearer(a.veli.Token)); resp.StatusCode != http.StatusBadRequest {
		t.Errorf("geçersiz status 400 olmalı: %d", resp.StatusCode)
	}
}

func TestClaimsAPI_EditOnlyAuthorOrPatronAndAudit(t *testing.T) {
	e := claimsServer(t, true, true)
	a := registerClaimActors(t, e.s)
	e.seed(t, "c1", claim.KindGeneral, "Vardiya 08:00", a.ali.User.ID)

	// Başka üye: 403, kayıt değişmez, denetim yazılmaz.
	resp, _ := do(t, e.s, http.MethodPut, "/api/claims/c1", map[string]any{"text": "Hack"}, bearer(a.veli.Token))
	if resp.StatusCode != http.StatusForbidden {
		t.Fatalf("başka üye 403 almalı: %d", resp.StatusCode)
	}
	if c, _ := e.repo.Get(context.Background(), "c1"); c.Text != "Vardiya 08:00" {
		t.Fatalf("yetkisiz düzenleme kaydı değiştirdi: %+v", c)
	}
	if es, _ := e.audit.ListByClaim(context.Background(), "c1", 0, 0); len(es) != 0 {
		t.Fatalf("reddedilen işlem denetim yazmamalı: %+v", es)
	}

	// Yazar düzenler: 200, denetimde önceki/sonraki değer.
	resp, raw := do(t, e.s, http.MethodPut, "/api/claims/c1", map[string]any{"text": "Vardiya 09:00"}, bearer(a.ali.Token))
	if resp.StatusCode != http.StatusOK || decode[claim.Claim](t, raw).Text != "Vardiya 09:00" {
		t.Fatalf("yazar düzenleyebilmeli: %d %s", resp.StatusCode, raw)
	}
	es, _ := e.audit.ListByClaim(context.Background(), "c1", 0, 0)
	if len(es) != 1 || es[0].Action != claim.ActionUpdate || es[0].By != a.ali.User.ID ||
		es[0].Before.Text != "Vardiya 08:00" || es[0].After.Text != "Vardiya 09:00" {
		t.Fatalf("denetim yanlış: %+v", es)
	}

	// Patron başkasının kaydını düzenler; yazar değişmez, düzenleyen denetimde görünür.
	resp, _ = do(t, e.s, http.MethodPut, "/api/claims/c1", map[string]any{"text": "Vardiya 10:00"}, bearer(a.patron.Token))
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("patron düzenleyebilmeli: %d", resp.StatusCode)
	}
	if c, _ := e.repo.Get(context.Background(), "c1"); c.AuthorID != a.ali.User.ID {
		t.Errorf("yazar değişmemeli: %s", c.AuthorID)
	}
	if es, _ := e.audit.ListByClaim(context.Background(), "c1", 0, 0); len(es) != 2 || es[0].By != a.patron.User.ID {
		t.Errorf("düzenleyen patron denetimde olmalı: %+v", es)
	}
}

func TestClaimsAPI_EditValidationAndKeyConflict(t *testing.T) {
	e := claimsServer(t, true, true)
	a := registerClaimActors(t, e.s)
	e.seed(t, "c1", claim.KindFirma, "ACME", a.ali.User.ID)
	e.seed(t, "c2", claim.KindFirma, "Barış Metal", a.ali.User.ID)

	resp, raw := do(t, e.s, http.MethodPut, "/api/claims/c1", map[string]any{"text": "   "}, bearer(a.ali.Token))
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("boş metin 400 olmalı: %d %s", resp.StatusCode, raw)
	}
	body := decode[errorBody](t, raw)
	if body.Error.Code != "validation_error" || len(body.Error.Fields) == 0 || body.Error.Fields[0].Field != "text" {
		t.Errorf("alan hatası text için olmalı: %+v", body)
	}
	resp, _ = do(t, e.s, http.MethodPut, "/api/claims/c1", map[string]any{"text": "ACME", "key": "Barış Metal"}, bearer(a.ali.Token))
	if resp.StatusCode != http.StatusConflict {
		t.Errorf("aynı adlı kayıt varken 409 olmalı: %d", resp.StatusCode)
	}
	if resp, _ := do(t, e.s, http.MethodPut, "/api/claims/c1", "bozuk", bearer(a.ali.Token)); resp.StatusCode != http.StatusBadRequest {
		t.Errorf("bozuk gövde 400 olmalı: %d", resp.StatusCode)
	}
}

func TestClaimsAPI_DeleteRulesAndHistorySurvives(t *testing.T) {
	e := claimsServer(t, true, true)
	a := registerClaimActors(t, e.s)
	e.seed(t, "c1", claim.KindFirma, "ACME Kalıp", a.ali.User.ID)
	e.seed(t, "c2", claim.KindFirma, "Çelik Boya", a.ali.User.ID)

	if resp, _ := do(t, e.s, http.MethodDelete, "/api/claims/c1", nil, bearer(a.veli.Token)); resp.StatusCode != http.StatusForbidden {
		t.Fatalf("başka üye silememeli: %d", resp.StatusCode)
	}
	if _, err := e.repo.Get(context.Background(), "c1"); err != nil {
		t.Fatalf("yetkisiz silme kaydı kaldırdı: %v", err)
	}
	if resp, _ := do(t, e.s, http.MethodDelete, "/api/claims/c1", nil, bearer(a.ali.Token)); resp.StatusCode != http.StatusNoContent {
		t.Fatalf("yazar silebilmeli: %d", resp.StatusCode)
	}
	if resp, _ := do(t, e.s, http.MethodDelete, "/api/claims/c1", nil, bearer(a.ali.Token)); resp.StatusCode != http.StatusNotFound {
		t.Errorf("ikinci silme 404 olmalı: %d", resp.StatusCode)
	}
	if resp, _ := do(t, e.s, http.MethodDelete, "/api/claims/c2", nil, bearer(a.patron.Token)); resp.StatusCode != http.StatusNoContent {
		t.Fatalf("patron başkasının kaydını silebilmeli: %d", resp.StatusCode)
	}

	// Geçmiş kayıt silinse de durur; silinen değer orada yaşar.
	resp, raw := do(t, e.s, http.MethodGet, "/api/claims/c1/history", nil, bearer(a.patron.Token))
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("geçmiş: %d %s", resp.StatusCode, raw)
	}
	h := decode[claimHistoryResponse](t, raw)
	if len(h.Items) != 1 || h.Items[0].Action != claim.ActionDelete || h.Items[0].Before.Text != "ACME Kalıp" || h.Items[0].By != a.ali.User.ID {
		t.Fatalf("silme denetimde yok/yanlış: %+v", h.Items)
	}
	// Silme yazarı: aynı yazar görebilir; başka üye göremez.
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/c1/history", nil, bearer(a.ali.Token)); resp.StatusCode != http.StatusOK {
		t.Errorf("yazar silinen kaydın geçmişini görebilmeli: %d", resp.StatusCode)
	}
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/c1/history", nil, bearer(a.veli.Token)); resp.StatusCode != http.StatusForbidden {
		t.Errorf("başka üye geçmişi görmemeli: %d", resp.StatusCode)
	}
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/hiç-yok/history", nil, bearer(a.patron.Token)); resp.StatusCode != http.StatusNotFound {
		t.Errorf("geçmişi olmayan kayıt 404 olmalı: %d", resp.StatusCode)
	}
}

// Üyelik kapalıyken kimse doğrulanmış değildir: ortak kayıtlar görüntülenemez, değiştirilemez.
func TestClaimsAPI_MembershipOffRejectsEverything(t *testing.T) {
	e := claimsServer(t, false, true)
	e.seed(t, "c1", claim.KindFirma, "ACME", "local")

	for _, tc := range []struct{ m, p string }{
		{http.MethodGet, "/api/claims"}, {http.MethodGet, "/api/claims/c1"},
		{http.MethodPut, "/api/claims/c1"}, {http.MethodDelete, "/api/claims/c1"}, {http.MethodGet, "/api/claims/c1/history"},
	} {
		resp, raw := do(t, e.s, tc.m, tc.p, map[string]any{"text": "x"}, nil)
		if resp.StatusCode != http.StatusForbidden {
			t.Errorf("%s %s: üyelik kapalıyken 403 beklenirdi, %d %s", tc.m, tc.p, resp.StatusCode, raw)
		}
	}
	if _, err := e.repo.Get(context.Background(), "c1"); err != nil {
		t.Errorf("reddedilen istekler kaydı etkilememeli: %v", err)
	}
}

func TestClaimsAPI_NotConfiguredIs503(t *testing.T) {
	e := claimsServer(t, true, false)
	a := registerUser(t, e.s, "patron")
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims", nil, bearer(a.Token)); resp.StatusCode != http.StatusServiceUnavailable {
		t.Fatalf("servis yokken 503 olmalı: %d", resp.StatusCode)
	}
}

// ---- Faz 6: çelişki netleştirme ----

// seedConflict çelişkili iki kaydı koyar: a (yazar ali) ve a ile çelişen b (yazar veli).
func (e *claimsEnv) seedConflict(t *testing.T, a claimsActors) {
	t.Helper()
	now := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
	mk := func(id, text, author, with string, n int) {
		c := claim.Claim{
			ID: id, Kind: claim.KindGeneral, Text: text, Key: claim.NormalizeKey(text), AuthorID: author, AuthorName: "yazar",
			CreatedAt: now.Add(time.Duration(n) * time.Minute), UpdatedAt: now, Status: claim.StatusConflict,
			ConflictsWith: with, Embedding: []float32{1, 0, 0}, EmbedModel: "m",
		}
		if err := e.repo.Create(context.Background(), &c); err != nil {
			t.Fatal(err)
		}
	}
	mk("ca", "Kalıp B-12 teslimi cuma", a.ali.User.ID, "", 1)
	mk("cb", "Kalıp B-12 teslimi pazartesi", a.veli.User.ID, "ca", 2)
}

func (e *claimsEnv) statusOf(t *testing.T, id string) claim.Status {
	t.Helper()
	c, err := e.repo.Get(context.Background(), id)
	if err != nil {
		t.Fatalf("Get %s: %v", id, err)
	}
	return c.Status
}

func TestClaimsAPI_ConflictsVisibleToMembersDecisionPatronOnly(t *testing.T) {
	e := claimsServer(t, true, true)
	a := registerClaimActors(t, e.s)
	e.seedConflict(t, a)

	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/conflicts", nil, nil); resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("oturumsuz 401 olmalı: %d", resp.StatusCode)
	}
	// Üye çelişkileri görür (ekip bilgisi) — /conflicts, /:id olarak yorumlanmamalı.
	resp, raw := do(t, e.s, http.MethodGet, "/api/claims/conflicts", nil, bearer(a.veli.Token))
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("üye çelişki listesini görebilmeli: %d %s", resp.StatusCode, raw)
	}
	got := decode[claimConflictsResponse](t, raw)
	if got.Total != 1 || len(got.Groups) != 1 || len(got.Groups[0].Claims) != 2 ||
		got.Groups[0].Claims[0].ID != "ca" || got.Groups[0].Claims[1].ID != "cb" {
		t.Fatalf("tek küme, en eskiden yeniye iki kayıt beklenirdi: %+v", got)
	}
	if strings.Contains(strings.ToLower(string(raw)), "embedding") {
		t.Fatalf("vektör yanıta sızdı: %s", raw)
	}

	// Üye karar veremez: yazarı bile olsa 403, hiçbir şey değişmez, denetim yazılmaz.
	for _, who := range []authReply{a.veli, a.ali} {
		for _, path := range []string{"/api/claims/cb/resolve", "/api/claims/cb/invalidate", "/api/claims/cb/restore"} {
			resp, _ := do(t, e.s, http.MethodPost, path, map[string]any{"decision": "keep"}, bearer(who.Token))
			if resp.StatusCode != http.StatusForbidden {
				t.Fatalf("%s üye için 403 olmalı: %d", path, resp.StatusCode)
			}
		}
	}
	if e.statusOf(t, "ca") != claim.StatusConflict || e.statusOf(t, "cb") != claim.StatusConflict {
		t.Fatal("reddedilen işlemler durum değiştirmemeli")
	}
	if es, _ := e.audit.ListByClaim(context.Background(), "ca", 0, 0); len(es) != 0 {
		t.Fatalf("reddedilen işlem denetim yazmamalı: %+v", es)
	}
}

func TestClaimsAPI_PatronResolvesInvalidatesAndRestores(t *testing.T) {
	e := claimsServer(t, true, true)
	a := registerClaimActors(t, e.s)
	e.seedConflict(t, a)
	pt := bearer(a.patron.Token)

	if resp, _ := do(t, e.s, http.MethodPost, "/api/claims/cb/resolve", map[string]any{"decision": "belki"}, pt); resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("geçersiz karar 400 olmalı: %d", resp.StatusCode)
	}
	if resp, _ := do(t, e.s, http.MethodPost, "/api/claims/yok/resolve", map[string]any{"decision": "keep"}, pt); resp.StatusCode != http.StatusNotFound {
		t.Fatalf("olmayan kayıt 404 olmalı: %d", resp.StatusCode)
	}

	resp, raw := do(t, e.s, http.MethodPost, "/api/claims/cb/resolve", map[string]any{"decision": "keep"}, pt)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("patron karar verebilmeli: %d %s", resp.StatusCode, raw)
	}
	if res := decode[claim.ResolveResult](t, raw); len(res.Changes) != 2 {
		t.Fatalf("iki durum değişikliği beklenirdi: %+v", res)
	}
	if e.statusOf(t, "cb") != claim.StatusActive || e.statusOf(t, "ca") != claim.StatusInvalid {
		t.Fatalf("cb aktif, ca geçersiz olmalı: cb=%s ca=%s", e.statusOf(t, "cb"), e.statusOf(t, "ca"))
	}
	es, _ := e.audit.ListByClaim(context.Background(), "ca", 0, 0)
	if len(es) != 1 || es[0].By != a.patron.User.ID || es[0].Before.Status != claim.StatusConflict || es[0].After.Status != claim.StatusInvalid {
		t.Fatalf("karar denetime yazılmalı (kim, önce, sonra): %+v", es)
	}
	// Çözülmüş kayıt için ikinci karar: 409, kümeler boş.
	if resp, _ := do(t, e.s, http.MethodPost, "/api/claims/cb/resolve", map[string]any{"decision": "keep"}, pt); resp.StatusCode != http.StatusConflict {
		t.Fatalf("çelişkili olmayan kayıt 409 olmalı: %d", resp.StatusCode)
	}
	_, raw = do(t, e.s, http.MethodGet, "/api/claims/conflicts", nil, pt)
	if got := decode[claimConflictsResponse](t, raw); got.Total != 0 || len(got.Groups) != 0 {
		t.Fatalf("karar sonrası bekleyen küme kalmamalı: %+v", got)
	}

	// Geri al: ca tekrar aktif; ikinci kez geri almak 409.
	resp, raw = do(t, e.s, http.MethodPost, "/api/claims/ca/restore", nil, pt)
	if resp.StatusCode != http.StatusOK || decode[claim.Claim](t, raw).Status != claim.StatusActive {
		t.Fatalf("restore: %d %s", resp.StatusCode, raw)
	}
	if resp, _ := do(t, e.s, http.MethodPost, "/api/claims/ca/restore", nil, pt); resp.StatusCode != http.StatusConflict {
		t.Fatalf("geçersiz olmayanı geri almak 409 olmalı: %d", resp.StatusCode)
	}
	// Doğrudan geçersiz işaretle.
	resp, raw = do(t, e.s, http.MethodPost, "/api/claims/cb/invalidate", nil, pt)
	if resp.StatusCode != http.StatusOK || decode[claim.Claim](t, raw).Status != claim.StatusInvalid {
		t.Fatalf("invalidate: %d %s", resp.StatusCode, raw)
	}
}

func TestClaimsAPI_ConflictEndpointsMembershipOffAndNotConfigured(t *testing.T) {
	e := claimsServer(t, false, true)
	for _, c := range []struct{ method, path string }{
		{http.MethodGet, "/api/claims/conflicts"},
		{http.MethodPost, "/api/claims/x/resolve"},
		{http.MethodPost, "/api/claims/x/invalidate"},
		{http.MethodPost, "/api/claims/x/restore"},
	} {
		if resp, _ := do(t, e.s, c.method, c.path, map[string]any{"decision": "keep"}, nil); resp.StatusCode != http.StatusForbidden {
			t.Errorf("%s %s: üyelik kapalıyken 403 beklenirdi: %d", c.method, c.path, resp.StatusCode)
		}
	}
	e = claimsServer(t, true, false)
	tok := registerUser(t, e.s, "patron")
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/conflicts", nil, bearer(tok.Token)); resp.StatusCode != http.StatusServiceUnavailable {
		t.Errorf("servis yoksa 503 beklenirdi: %d", resp.StatusCode)
	}
}
