package api

import (
	"context"
	"net/http"
	"sort"
	"strings"
	"testing"
	"time"

	"fiko-ai/internal/claim"
)

// claims_access_test.go - Faz 8: tür bazlı görünürlük /api/claims üzerinde. Üye, ilgili kişi ve patron
// ayrı ayrı denenir; yetkisiz kişiye özel kaydın VAR OLDUĞU bile sızmamalıdır (404, "yasak" değil).

// seedPrivate özel/ekip kaydı ekler (yazar patron; ilgili kişi subject) ve create denetimini yazar.
func (e *claimsEnv) seedPrivate(t *testing.T, id, kind, text, subject, author string, st claim.Status, conflictsWith 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: author, AuthorName: "yazar",
		CreatedAt: now, UpdatedAt: now, Status: st, SubjectID: subject, ConflictsWith: conflictsWith,
		Embedding: []float32{1, 0, 0}, EmbedModel: "m",
	}
	if err := e.repo.Create(context.Background(), &c); err != nil {
		t.Fatal(err)
	}
	a := claim.AuditEntry{ID: "aud-" + id, ClaimID: id, Action: claim.ActionCreate, By: author, At: now, After: c.Snapshot()}
	if err := e.audit.Append(context.Background(), &a); err != nil {
		t.Fatal(err)
	}
}

func claimIDs(t *testing.T, raw []byte) []string {
	t.Helper()
	var out []string
	for _, c := range decode[claimListResponse](t, raw).Items {
		out = append(out, c.ID)
	}
	sort.Strings(out)
	return out
}

func equalIDs(got []string, want ...string) bool {
	sort.Strings(want)
	if len(got) != len(want) {
		return false
	}
	for i := range got {
		if got[i] != want[i] {
			return false
		}
	}
	return true
}

func (e *claimsEnv) accessFixture(t *testing.T) claimsActors {
	t.Helper()
	a := registerClaimActors(t, e.s)
	e.seedPrivate(t, "firma1", claim.KindFirma, "ACME Kalıp", "", a.patron.User.ID, claim.StatusActive, "")
	e.seedPrivate(t, "pdks1", "pdks", "Ali 08:10 geldi GIZLI-ALI", a.ali.User.ID, a.patron.User.ID, claim.StatusActive, "")
	e.seedPrivate(t, "pdks2", "pdks", "Veli 09:05 geldi GIZLI-VELI", a.veli.User.ID, a.patron.User.ID, claim.StatusActive, "")
	e.seedPrivate(t, "per1", "personel", "Ali maaş 50000 GIZLI-PERSONEL", "", a.patron.User.ID, claim.StatusActive, "")
	return a
}

func TestClaimsAPI_PrivateKindsListedOnlyToPatronAndSubject(t *testing.T) {
	e := claimsServer(t, true, true)
	a := e.accessFixture(t)

	cases := []struct {
		name  string
		token string
		want  []string
	}{
		{"üye (yalnız kendi kaydı + ekip)", a.veli.Token, []string{"firma1", "pdks2"}},
		{"ilgili kişi", a.ali.Token, []string{"firma1", "pdks1"}},
		{"patron", a.patron.Token, []string{"firma1", "pdks1", "pdks2", "per1"}},
	}
	for _, c := range cases {
		resp, raw := do(t, e.s, http.MethodGet, "/api/claims", nil, bearer(c.token))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("%s: %d %s", c.name, resp.StatusCode, raw)
		}
		if got := claimIDs(t, raw); !equalIDs(got, c.want...) {
			t.Errorf("%s: %v, beklenen %v", c.name, got, c.want)
		}
		if n := decode[claimListResponse](t, raw).Total; n != len(c.want) {
			t.Errorf("%s: toplam %d, beklenen %d (sayı sızıntısı)", c.name, n, len(c.want))
		}
	}

	// Süzgeçle yoklama özel kaydı ortaya çıkarmaz.
	for _, q := range []string{"?kind=personel", "?kind=pdks&status=aktif", "?q=GIZLI-PERSONEL", "?q=maaş", "?author=" + a.patron.User.ID + "&kind=personel"} {
		resp, raw := do(t, e.s, http.MethodGet, "/api/claims"+q, nil, bearer(a.veli.Token))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("%s: %d %s", q, resp.StatusCode, raw)
		}
		l := decode[claimListResponse](t, raw)
		for _, id := range claimIDs(t, raw) {
			if id == "pdks1" || id == "per1" {
				t.Errorf("üye %s ile özel kaydı gördü: %s", q, id)
			}
		}
		if l.Total != len(l.Items) {
			t.Errorf("%s: toplam %d / öğe %d", q, l.Total, len(l.Items))
		}
	}
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims", nil, nil); resp.StatusCode != http.StatusUnauthorized {
		t.Errorf("oturumsuz 401 olmalı: %d", resp.StatusCode)
	}
}

func TestClaimsAPI_PrivateRecordIsIndistinguishableFromMissing(t *testing.T) {
	e := claimsServer(t, true, true)
	a := e.accessFixture(t)
	veli := bearer(a.veli.Token)

	type probe struct{ method, path string }
	for _, p := range []probe{
		{http.MethodGet, "/api/claims/"}, {http.MethodGet, "/api/claims/{id}/history"},
		{http.MethodPut, "/api/claims/"}, {http.MethodDelete, "/api/claims/"},
	} {
		call := func(id string) (int, string) {
			path := strings.Replace(p.path, "{id}", id, 1)
			if !strings.Contains(p.path, "{id}") {
				path += id
			}
			var body any
			if p.method == http.MethodPut {
				body = map[string]any{"text": "değiştirmeye çalışıyorum"}
			}
			resp, raw := do(t, e.s, p.method, path, body, veli)
			return resp.StatusCode, string(raw)
		}
		wantCode, wantBody := call("hic-yok-boyle-bir-kayit")
		if wantCode != http.StatusNotFound {
			t.Fatalf("%s %s: olmayan kayıt 404 olmalı: %d", p.method, p.path, wantCode)
		}
		for _, id := range []string{"pdks1", "per1"} {
			if code, body := call(id); code != wantCode || body != wantBody {
				t.Errorf("%s %s%s: yetkisiz yanıt (%d %s) olmayan kayıttan (%d %s) FARKLI → varlık sızıyor",
					p.method, p.path, id, code, body, wantCode, wantBody)
			}
		}
	}
	// Silme/değiştirme gerçekten yapılmadı.
	for _, id := range []string{"pdks1", "per1"} {
		if _, err := e.repo.Get(context.Background(), id); err != nil {
			t.Errorf("%s yetkisiz silmeyle kaybolmuş: %v", id, err)
		}
	}
	// İlgili kişi kendi kaydını görür; başkasınınkini ve ilgili kişisiz kaydı görmez.
	ali := bearer(a.ali.Token)
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/pdks1", nil, ali); resp.StatusCode != http.StatusOK {
		t.Errorf("ilgili kişi kendi kaydını görmeli: %d", resp.StatusCode)
	}
	for _, id := range []string{"pdks2", "per1"} {
		if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/"+id, nil, ali); resp.StatusCode != http.StatusNotFound {
			t.Errorf("ilgili kişi %s kaydını görmemeli: %d", id, resp.StatusCode)
		}
	}
	// Patron hepsini görür; yanıtta vektör yok.
	for _, id := range []string{"pdks1", "pdks2", "per1", "firma1"} {
		resp, raw := do(t, e.s, http.MethodGet, "/api/claims/"+id, nil, bearer(a.patron.Token))
		if resp.StatusCode != http.StatusOK || strings.Contains(strings.ToLower(string(raw)), "embedding") {
			t.Errorf("patron %s: %d %s", id, resp.StatusCode, raw)
		}
	}
}

func TestClaimsAPI_ConflictScreenAndHistoryRespectVisibility(t *testing.T) {
	e := claimsServer(t, true, true)
	a := e.accessFixture(t)
	e.seedPrivate(t, "cf-pub-a", claim.KindGeneral, "Vardiya 08:00", "", a.patron.User.ID, claim.StatusConflict, "")
	e.seedPrivate(t, "cf-pub-b", claim.KindGeneral, "Vardiya 09:00", "", a.patron.User.ID, claim.StatusConflict, "cf-pub-a")
	e.seedPrivate(t, "cf-pdks-a", "pdks", "Ali giriş 08:00 GIZLI-CF", a.ali.User.ID, a.patron.User.ID, claim.StatusConflict, "")
	e.seedPrivate(t, "cf-pdks-b", "pdks", "Ali giriş 09:00 GIZLI-CF", a.ali.User.ID, a.patron.User.ID, claim.StatusConflict, "cf-pdks-a")

	conflictIDs := func(token string) []string {
		t.Helper()
		resp, raw := do(t, e.s, http.MethodGet, "/api/claims/conflicts", nil, bearer(token))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("conflicts: %d %s", resp.StatusCode, raw)
		}
		var ids []string
		for _, g := range decode[struct {
			Items []struct {
				Claims []claim.Claim `json:"claims"`
			} `json:"groups"`
		}](t, raw).Items {
			for _, c := range g.Claims {
				ids = append(ids, c.ID)
			}
		}
		sort.Strings(ids)
		return ids
	}
	if got := conflictIDs(a.veli.Token); !equalIDs(got, "cf-pub-a", "cf-pub-b") {
		t.Errorf("üyenin çelişki ekranında özel kayıt var: %v", got)
	}
	if got := conflictIDs(a.ali.Token); !equalIDs(got, "cf-pub-a", "cf-pub-b", "cf-pdks-a", "cf-pdks-b") {
		t.Errorf("ilgili kişi kendi pdks çelişkisini görmeli: %v", got)
	}
	if got := conflictIDs(a.patron.Token); !equalIDs(got, "cf-pub-a", "cf-pub-b", "cf-pdks-a", "cf-pdks-b") {
		t.Errorf("patron hepsini görmeli: %v", got)
	}

	// Geçmiş: üye 404; patron okur ve bu erişim denetime düşer.
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/pdks1/history", nil, bearer(a.veli.Token)); resp.StatusCode != http.StatusNotFound {
		t.Errorf("üye özel kaydın geçmişini görmemeli (404): %d", resp.StatusCode)
	}
	if resp, raw := do(t, e.s, http.MethodGet, "/api/claims/pdks1/history", nil, bearer(a.patron.Token)); resp.StatusCode != http.StatusOK {
		t.Fatalf("patron geçmişi okumalı: %d %s", resp.StatusCode, raw)
	}
	views, _ := e.audit.ListByActor(context.Background(), a.patron.User.ID, 0, 0)
	seen := false
	for _, v := range views {
		if v.Action == claim.ActionView && v.ClaimID == "pdks1" {
			seen = true
		}
	}
	if !seen {
		t.Error("patronun pdks okuması denetime (view) yazılmadı")
	}
	// Üyenin yalnızca ekip verisine bakışı denetim üretmez.
	_, _ = do(t, e.s, http.MethodGet, "/api/claims/firma1", nil, bearer(a.veli.Token))
	if vs, _ := e.audit.ListByActor(context.Background(), a.veli.User.ID, 0, 0); len(vs) != 0 {
		for _, v := range vs {
			if v.ClaimID == "firma1" || v.ClaimID == "pdks1" || v.ClaimID == "per1" {
				t.Errorf("üye için beklenmeyen denetim kaydı: %+v", v)
			}
		}
	}
}

func TestClaimsAPI_OnlyPatronAssignsSubject(t *testing.T) {
	e := claimsServer(t, true, true)
	a := e.accessFixture(t)
	e.s.deps.Claims.SetDirectory(testClaimDir{users: e.s.deps.Users})

	put := func(token, id string, body map[string]any) (int, []byte) {
		resp, raw := do(t, e.s, http.MethodPut, "/api/claims/"+id, body, bearer(token))
		return resp.StatusCode, raw
	}
	subjectOf := func(id string) string {
		c, err := e.repo.Get(context.Background(), id)
		if err != nil {
			t.Fatal(err)
		}
		return c.SubjectID
	}

	// Patron ilgili kişi atar: Ali artık "per1"i de görür.
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/per1", nil, bearer(a.ali.Token)); resp.StatusCode != http.StatusNotFound {
		t.Fatalf("atamadan önce görünmemeli: %d", resp.StatusCode)
	}
	if code, raw := put(a.patron.Token, "per1", map[string]any{"subjectId": a.ali.User.ID}); code != http.StatusOK {
		t.Fatalf("patron atamalı: %d %s", code, raw)
	}
	if subjectOf("per1") != a.ali.User.ID {
		t.Fatal("subjectId yazılmadı")
	}
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/per1", nil, bearer(a.ali.Token)); resp.StatusCode != http.StatusOK {
		t.Errorf("atamadan sonra ilgili kişi görmeli: %d", resp.StatusCode)
	}
	// Üye (kaydı görse bile) atayamaz: pdks3'ün yazarı ve ilgili kişisi Ali.
	e.seedPrivate(t, "pdks3", "pdks", "Ali 07:55 geldi", a.ali.User.ID, a.ali.User.ID, claim.StatusActive, "")
	if code, _ := put(a.ali.Token, "pdks3", map[string]any{"subjectId": a.veli.User.ID}); code != http.StatusForbidden {
		t.Errorf("üye ilgili kişiyi değiştirememeli (403): %d", code)
	}
	if subjectOf("pdks3") != a.ali.User.ID {
		t.Error("yetkisiz atama subjectId'yi değiştirdi")
	}
	// Geçersiz atamalar: olmayan hesap, ekip türü.
	if code, _ := put(a.patron.Token, "per1", map[string]any{"subjectId": "olmayan-hesap"}); code != http.StatusBadRequest {
		t.Errorf("olmayan hesaba atama 400 olmalı: %d", code)
	}
	if code, _ := put(a.patron.Token, "firma1", map[string]any{"subjectId": a.ali.User.ID}); code != http.StatusBadRequest {
		t.Errorf("ekip türüne ilgili kişi atanamaz (400): %d", code)
	}
	// Bağ kaldırılınca kayıt yalnızca patrona görünür (belirsizlikte güvenli varsayılan: görme).
	if code, raw := put(a.patron.Token, "per1", map[string]any{"subjectId": ""}); code != http.StatusOK {
		t.Fatalf("bağ kaldırılamadı: %d %s", code, raw)
	}
	if resp, _ := do(t, e.s, http.MethodGet, "/api/claims/per1", nil, bearer(a.ali.Token)); resp.StatusCode != http.StatusNotFound {
		t.Errorf("bağ kalkınca ilgili kişi artık görmemeli: %d", resp.StatusCode)
	}
}
