package claim

// Faz 8: tür bazlı görünürlük testleri. Üç rol ayrı ayrı denenir:
//   - üye        : yalnızca ekip türlerini görür
//   - ilgili kişi: ekip türleri + kendi (subjectId) özel kayıtları
//   - patron     : her şey
// ve kimliksiz çağrı (fail-closed) hiçbir özel kaydı görmez. Kontrol edilen yüzeyler: sohbet prompt'u,
// liste, arama, kayıt geçmişi ve çelişki ekranı. Yetkisiz kişiye kaydın VAR OLDUĞU bile sızmamalıdır.

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"
	"testing"
)

const (
	secretPDKS    = "Ali 08:10 geldi GIZLI-PDKS"
	secretPersonl = "Ali maaş 50000 GIZLI-PERSONEL"
	secretVeli    = "Veli 09:05 geldi GIZLI-VELI"
)

var (
	accAli   = mgrOwner  // u-ali: pdks1'in ilgili kişisi (üye rolü)
	accVeli  = mgrOther  // u-veli: ilgili kişisi olduğu kayıt dışında hiçbir özel kaydı görmez
	accBoss  = mgrPatron // u-patron
	accNobdy = Actor{}   // kimliksiz
)

// seedAccess ekip + özel kayıtları tohumlar. Özel kayıtların yazarı patrondur; her kayda create denetimi de
// konur (History denetim kayıtlarından çalışır).
func (e *mgrEnv) seedAccess(t *testing.T) {
	t.Helper()
	add := func(id, kind, text, subject string, st Status, conflictsWith string) {
		c := readClaim(id, kind, text, st, []float32{1, 0, 0})
		c.AuthorID, c.AuthorName = accBoss.ID, accBoss.Name
		c.SubjectID, c.ConflictsWith = subject, conflictsWith
		if err := e.repo.Create(context.Background(), &c); err != nil {
			t.Fatal(err)
		}
		a := AuditEntry{ID: "aud-seed-" + id, ClaimID: id, Action: ActionCreate, By: accBoss.ID, ByName: accBoss.Name,
			At: c.CreatedAt, After: c.Snapshot()}
		if err := e.audit.Append(context.Background(), &a); err != nil {
			t.Fatal(err)
		}
	}
	add("firma1", KindFirma, "ACME Kalıp A.Ş.", "", StatusActive, "")
	add("pdks1", "pdks", secretPDKS, "u-ali", StatusActive, "")
	add("pdks2", "pdks", secretVeli, "u-veli", StatusActive, "")
	add("per1", "personel", secretPersonl, "", StatusActive, "") // ilgili kişi atanmamış: yalnızca patron
	// Çelişki kümeleri: biri ekip bilgisi, biri Ali'nin özel kaydı.
	add("cf-pub-a", KindGeneral, "Vardiya 08:00", "", StatusConflict, "")
	add("cf-pub-b", KindGeneral, "Vardiya 09:00", "", StatusConflict, "cf-pub-a")
	add("cf-pdks-a", "pdks", "Ali giriş 08:00 GIZLI-CF", "u-ali", StatusConflict, "")
	add("cf-pdks-b", "pdks", "Ali giriş 09:00 GIZLI-CF", "u-ali", StatusConflict, "cf-pdks-a")
}

func accIDSet(cs []Claim) []string {
	out := make([]string, len(cs))
	for i, c := range cs {
		out[i] = c.ID
	}
	sort.Strings(out)
	return out
}

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

var (
	publicIDs = []string{"cf-pub-a", "cf-pub-b", "firma1"}
	aliIDs    = []string{"cf-pdks-a", "cf-pdks-b", "pdks1"}
)

func accConcat(parts ...[]string) []string {
	var out []string
	for _, p := range parts {
		out = append(out, p...)
	}
	return out
}

// ---- Actor.Scope / CanSee tutarlılığı ----

func TestScopeAndCanSeeAgree(t *testing.T) {
	claims := []Claim{
		{ID: "t", Kind: KindFirma},
		{ID: "own", Kind: "pdks", SubjectID: "u-ali"},
		{ID: "other", Kind: "pdks", SubjectID: "u-veli"},
		{ID: "nosubj", Kind: "personel"},
		{ID: "free", Kind: "serbest_etiket"}, // şemasız tür: ekip görünürlüğü
	}
	actors := map[string]Actor{
		"patron": accBoss, "üye": accVeli, "ilgili": accAli, "kimliksiz": accNobdy,
		"kimliksiz patron": {Role: RolePatron}, "rolsüz kimlik": {ID: "u-ali"}, "boşluklu kimlik": {ID: "  ", Role: RoleUye},
	}
	want := map[string][]string{
		"patron":           {"free", "nosubj", "other", "own", "t"},
		"üye":              {"free", "other", "t"}, // u-veli kendi kaydı "other"ı görür
		"ilgili":           {"free", "own", "t"},
		"kimliksiz":        {"free", "t"},
		"kimliksiz patron": {"free", "t"},
		"rolsüz kimlik":    {"free", "own", "t"},
		"boşluklu kimlik":  {"free", "t"},
	}
	for name, a := range actors {
		var got []string
		for _, c := range claims {
			c := c
			byFilter := matches(a.Scope(Filter{}), c)
			if byFilter != a.CanSee(&c) {
				t.Errorf("%s/%s: depo süzgeci (%v) ile CanSee (%v) ayrıştı", name, c.ID, byFilter, a.CanSee(&c))
			}
			if byFilter {
				got = append(got, c.ID)
			}
		}
		sort.Strings(got)
		if !accSameIDs(got, want[name]...) {
			t.Errorf("%s: görünen %v, beklenen %v", name, got, want[name])
		}
	}
	if (Actor{}).CanSee(nil) {
		t.Error("nil kayıt görünmemeli")
	}
}

// ---- Service: Get / List / History / Conflicts ----

func TestServiceGetHidesPrivateRecordsWithoutRevealingExistence(t *testing.T) {
	e := newMgr(t)
	e.seedAccess(t)
	ctx := context.Background()

	missing := func(a Actor, id string) {
		t.Helper()
		c, err := e.svc.Get(ctx, a, id)
		if c != nil || !errors.Is(err, ErrNotFound) {
			t.Errorf("%s %s: ErrNotFound bekleniyordu (kayıt VAR ama görünmemeli): %v %v", a.Name, id, c, err)
		}
	}
	visible := func(a Actor, id string) {
		t.Helper()
		if c, err := e.svc.Get(ctx, a, id); err != nil || c == nil || c.ID != id || len(c.Embedding) != 0 {
			t.Errorf("%s %s: görmeliydi (vektörsüz): %v %v", a.Name, id, c, err)
		}
	}

	// Üye: yalnızca ekip türleri.
	visible(accVeli, "firma1")
	missing(accVeli, "pdks1")
	missing(accVeli, "per1")
	missing(accVeli, "yok-boyle-bir-kayit")
	// Hata, hiç var olmayan kayıtla BİREBİR aynı olmalı.
	_, errPriv := e.svc.Get(ctx, accVeli, "pdks1")
	_, errNone := e.svc.Get(ctx, accVeli, "yok-boyle-bir-kayit")
	if errPriv != errNone {
		t.Errorf("yetkisiz ve olmayan kayıt hatası farklı: %v / %v", errPriv, errNone)
	}
	// İlgili kişi: yalnızca KENDİ kaydı; başkasınınki ve ilgili kişisiz kayıt görünmez.
	visible(accAli, "pdks1")
	missing(accAli, "pdks2")
	missing(accAli, "per1")
	// Patron: hepsi.
	for _, id := range []string{"pdks1", "pdks2", "per1", "firma1"} {
		visible(accBoss, id)
	}
	// Kimliksiz: reddedilir (ErrNoActor) ve hiçbir şey okunmaz.
	if _, err := e.svc.Get(ctx, accNobdy, "firma1"); !errors.Is(err, ErrNoActor) {
		t.Errorf("kimliksiz Get ErrNoActor olmalı: %v", err)
	}
}

func TestServiceListFiltersBeforeQueryAndCounts(t *testing.T) {
	e := newMgr(t)
	e.seedAccess(t)
	ctx := context.Background()

	list := func(a Actor, f Filter) ([]string, int) {
		t.Helper()
		items, total, err := e.svc.List(ctx, a, f, 200, 0)
		if err != nil {
			t.Fatalf("%s List: %v", a.Name, err)
		}
		for _, c := range items {
			if len(c.Embedding) != 0 {
				t.Errorf("%s: listede vektör var", c.ID)
			}
		}
		return accIDSet(items), total
	}
	all := accConcat(publicIDs, aliIDs, []string{"pdks2", "per1"})

	if ids, total := list(accVeli, Filter{}); !accSameIDs(ids, accConcat(publicIDs, []string{"pdks2"})...) || total != len(ids) {
		t.Errorf("üye: %v (toplam %d)", ids, total)
	}
	if ids, total := list(accAli, Filter{}); !accSameIDs(ids, accConcat(publicIDs, aliIDs)...) || total != len(ids) {
		t.Errorf("ilgili kişi: %v (toplam %d)", ids, total)
	}
	if ids, total := list(accBoss, Filter{}); !accSameIDs(ids, all...) || total != len(all) {
		t.Errorf("patron: %v (toplam %d)", ids, total)
	}
	// Sayfa sınırı bile özel kaydı ele vermez: toplam yalnızca görünenleri sayar.
	if _, total, _ := e.svc.List(ctx, accVeli, Filter{}, 1, 0); total != len(publicIDs)+1 {
		t.Errorf("üye toplamı özel kayıtları saydı (sayı sızıntısı): %d", total)
	}

	// Süzgeçle YOKLAMA (kind / metin / durum / yazar) özel kaydı ortaya çıkarmaz.
	probes := map[string]Filter{
		"kind":    {Kinds: []string{"pdks", "personel"}},
		"metin":   {Query: "GIZLI"},
		"maaş":    {Query: "maaş"},
		"durum":   {Kinds: []string{"pdks"}, Statuses: []Status{StatusConflict}},
		"yazar":   {AuthorID: accBoss.ID, Kinds: []string{"personel"}},
		"anahtar": {Key: NormalizeKey(secretPersonl)},
	}
	for name, f := range probes {
		for _, who := range []Actor{accVeli} {
			ids, total := list(who, f)
			for _, id := range ids {
				if id == "pdks1" || id == "per1" || id == "cf-pdks-a" || id == "cf-pdks-b" {
					t.Errorf("%s yoklaması (%s) özel kaydı döndürdü: %s", name, who.Name, id)
				}
			}
			if total != len(ids) {
				t.Errorf("%s yoklaması: toplam %d, öğe %d (sayı sızıntısı)", name, total, len(ids))
			}
		}
	}
	// "Yalnızca özel tür" isteyen üye sıfır görür; hata ya da ipucu yok.
	if ids, total := list(accVeli, Filter{Kinds: []string{"personel"}}); len(ids) != 0 || total != 0 {
		t.Errorf("üye personel listesi boş olmalı: %v %d", ids, total)
	}
	if _, _, err := e.svc.List(ctx, accNobdy, Filter{}, 0, 0); !errors.Is(err, ErrNoActor) {
		t.Errorf("kimliksiz List ErrNoActor olmalı: %v", err)
	}
}

func TestServiceHistoryDoesNotLeakPrivateClaims(t *testing.T) {
	e := newMgr(t)
	e.seedAccess(t)
	ctx := context.Background()

	for _, id := range []string{"pdks1", "per1"} {
		h, err := e.svc.History(ctx, accVeli, id, 0, 0)
		if h != nil || !errors.Is(err, ErrNotFound) { // ErrForbidden DEĞİL: "var ama yasak" demek varlığı ele verir
			t.Errorf("üye History(%s): ErrNotFound bekleniyordu: %v %v", id, h, err)
		}
	}
	if _, err := e.svc.History(ctx, accVeli, "hic-yok", 0, 0); !errors.Is(err, ErrNotFound) {
		t.Errorf("olmayan kayıt ErrNotFound olmalı: %v", err)
	}
	// Başkasının kaydı: ilgili kişi için de yok.
	if _, err := e.svc.History(ctx, accAli, "pdks2", 0, 0); !errors.Is(err, ErrNotFound) {
		t.Errorf("Ali, Veli'nin kaydının geçmişini görmemeli: %v", err)
	}
	// Patron geçmişi okur ve bu erişim denetime yazılır.
	h, err := e.svc.History(ctx, accBoss, "pdks1", 0, 0)
	if err != nil || len(h) == 0 {
		t.Fatalf("patron geçmişi okuyabilmeli: %v %v", h, err)
	}
	if !viewLogged(t, e, accBoss.ID, "pdks1") {
		t.Error("patronun pdks geçmişi okuması denetime yazılmadı")
	}
}

func TestServiceConflictsScreenIsScopedByVisibility(t *testing.T) {
	e := newMgr(t)
	e.seedAccess(t)
	ctx := context.Background()

	ids := func(a Actor) []string {
		t.Helper()
		gs, err := e.svc.Conflicts(ctx, a)
		if err != nil {
			t.Fatalf("%s Conflicts: %v", a.Name, err)
		}
		var out []string
		for _, g := range gs {
			out = append(out, accIDSet(g.Claims)...)
		}
		sort.Strings(out)
		return out
	}
	if got := ids(accVeli); !accSameIDs(got, "cf-pub-a", "cf-pub-b") {
		t.Errorf("üyenin çelişki ekranında özel kayıt var: %v", got)
	}
	if got := ids(accAli); !accSameIDs(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 := ids(accBoss); !accSameIDs(got, "cf-pub-a", "cf-pub-b", "cf-pdks-a", "cf-pdks-b") {
		t.Errorf("patron hepsini görmeli: %v", got)
	}
	if _, err := e.svc.Conflicts(ctx, accNobdy); !errors.Is(err, ErrNoActor) {
		t.Errorf("kimliksiz Conflicts ErrNoActor olmalı: %v", err)
	}
	// Özel çelişkiye bakış denetlenir; ekip çelişkisine bakış denetlenmez.
	if !viewLogged(t, e, accAli.ID, "cf-pdks-a") {
		t.Error("ilgili kişinin pdks çelişkisini görmesi denetime yazılmadı")
	}
	if viewLogged(t, e, accVeli.ID, "cf-pub-a") || viewLogged(t, e, accAli.ID, "cf-pub-a") {
		t.Error("ekip verisi okumaları denetime yazılmamalı")
	}
}

// viewLogged actor'ın claimID için ActionView denetim kaydı var mı?
func viewLogged(t *testing.T, e *mgrEnv, actorID, claimID string) bool {
	t.Helper()
	es, err := e.audit.ListByActor(context.Background(), actorID, 0, 0)
	if err != nil {
		t.Fatal(err)
	}
	for _, x := range es {
		if x.Action == ActionView && x.ClaimID == claimID {
			return true
		}
	}
	return false
}

func TestPrivateReadsAreAuditedPerActor(t *testing.T) {
	e := newMgr(t)
	e.seedAccess(t)
	ctx := context.Background()

	_, _ = e.svc.Get(ctx, accAli, "pdks1")
	_, _, _ = e.svc.List(ctx, accBoss, Filter{Kinds: []string{"personel"}}, 0, 0)
	_, _ = e.svc.Get(ctx, accVeli, "pdks1")            // reddedildi
	_, _, _ = e.svc.List(ctx, accVeli, Filter{}, 0, 0) // yalnızca ekip + kendi kaydı
	_, _ = e.svc.Get(ctx, accVeli, "firma1")           // ekip verisi

	if !viewLogged(t, e, accAli.ID, "pdks1") || !viewLogged(t, e, accBoss.ID, "per1") {
		t.Error("yetkili özel okumalar denetime yazılmalı")
	}
	if viewLogged(t, e, accVeli.ID, "pdks1") {
		t.Error("reddedilen erişim 'görüldü' diye yazılmamalı")
	}
	if !viewLogged(t, e, accVeli.ID, "pdks2") {
		t.Error("üyenin KENDİ özel kaydını listelemesi denetlenmeli")
	}
	if viewLogged(t, e, accVeli.ID, "firma1") {
		t.Error("ekip verisi okuması denetime yazılmamalı")
	}
	es, _ := e.audit.ListByActor(ctx, accAli.ID, 0, 0)
	for _, x := range es {
		if x.Action == ActionView && (x.By != accAli.ID || x.ByName != accAli.Name || x.At.IsZero()) {
			t.Errorf("view kaydı eksik (kim/ne zaman): %+v", x)
		}
		if x.Action == ActionView && (x.Before != nil || x.After != nil) {
			t.Errorf("view kaydı kişisel veri anlık görüntüsü taşımamalı: %+v", x)
		}
	}
}

// viewFailAudit görüntüleme kayıtlarını yazamayan denetim deposudur.
type viewFailAudit struct{ AuditRepository }

func (a viewFailAudit) Append(ctx context.Context, e *AuditEntry) error {
	if e.Action == ActionView {
		return errors.New("denetim deposu kapalı")
	}
	return a.AuditRepository.Append(ctx, e)
}

func TestPrivateDataIsWithheldWhenAccessCannotBeAudited(t *testing.T) {
	e := newMgr(t)
	e.seedAccess(t)
	ctx := context.Background()
	svc := NewService(e.repo, viewFailAudit{e.audit}, nil, e.emb, Config{EmbedModel: readModel}, nil)
	t.Cleanup(svc.Close)

	if c, err := svc.Get(ctx, accAli, "pdks1"); c != nil || err == nil || errors.Is(err, ErrNotFound) {
		t.Errorf("denetim yazılamıyorsa kişisel veri DÖNMEMELİ (ve hata gerçek hata olmalı): %v %v", c, err)
	}
	if items, _, err := svc.List(ctx, accBoss, Filter{Kinds: []string{"pdks"}}, 0, 0); err == nil || len(items) != 0 {
		t.Errorf("List denetimsiz özel veri döndürdü: %d kayıt, hata %v", len(items), err)
	}
	if gs, err := svc.Conflicts(ctx, accAli); err == nil || len(gs) != 0 {
		t.Errorf("Conflicts denetimsiz özel veri döndürdü: %v %v", gs, err)
	}
	if h, err := svc.History(ctx, accBoss, "pdks1", 0, 0); err == nil || len(h) != 0 {
		t.Errorf("History denetimsiz özel veri döndürdü: %v %v", h, err)
	}
	// Ekip verisi denetim gerektirmez: çalışmaya devam eder.
	if c, err := svc.Get(ctx, accVeli, "firma1"); err != nil || c == nil {
		t.Errorf("ekip verisi denetim arızasından etkilenmemeli: %v", err)
	}
}

// ---- Reader: sohbet prompt'u ve liste ----

func TestReaderNeverPutsInvisibleRecordsInPromptListOrHits(t *testing.T) {
	near := []float32{1, 0, 0}
	mk := func(id, kind, text, subject string) Claim {
		c := readClaim(id, kind, text, StatusActive, near)
		c.SubjectID = subject
		return c
	}
	const q = "Ali ne zaman geldi?"
	repo := readRepo(t,
		mk("pdks1", "pdks", secretPDKS, "u_ali"),
		mk("per1", "personel", secretPersonl, ""), // ilgili kişisiz
		mk("firma1", KindFirma, "ACME Kalıp müşteri", ""),
	)
	audit := NewMemoryAuditRepo()
	emb := &readEmb{vecs: map[string][]float32{q: near}}
	prep := func(a Actor, reply string) *Prepared {
		t.Helper()
		rd := NewReader(repo, &readLLM{reply: reply}, emb,
			ReadConfig{Model: "m", EmbedModel: readModel, TopK: 5, PageSize: 50}).WithAudit(audit)
		p, err := rd.Prepare(context.Background(), q, a)
		if err != nil {
			t.Fatalf("%s Prepare: %v", a.Name, err)
		}
		return p
	}
	rows := []struct {
		name                   string
		who                    Actor
		seesPDKS, seesPersonel bool
	}{
		{"üye", readUye, false, false},
		{"kimliksiz", Actor{}, false, false},
		{"kimliksiz patron", Actor{Role: RolePatron}, false, false},
		{"ilgili kişi", readAli, true, false},
		{"patron", readPatron, true, true},
	}
	for _, r := range rows {
		// Bilgi sorusu: prompt'taki "bilinen bilgiler".
		p := prep(r.who, `{"type":"soru"}`)
		if got := strings.Contains(p.Prompt, "GIZLI-PDKS"); got != r.seesPDKS {
			t.Errorf("%s: pdks prompt'ta=%v, beklenen %v\n%s", r.name, got, r.seesPDKS, p.Prompt)
		}
		if got := strings.Contains(p.Prompt, "GIZLI-PERSONEL"); got != r.seesPersonel {
			t.Errorf("%s: personel prompt'ta=%v, beklenen %v", r.name, got, r.seesPersonel)
		}
		if !strings.Contains(p.Prompt, "ACME Kalıp") {
			t.Errorf("%s: ekip bilgisi prompt'ta olmalı", r.name)
		}
		for _, h := range p.Hits {
			if !r.who.CanSee(&h.Claim) {
				t.Errorf("%s: görme yetkisi olmayan isabet: %s", r.name, h.Claim.ID)
			}
		}
		// Liste sorusu: sunucunun ürettiği liste.
		for kind, secret := range map[string]string{"pdks": "GIZLI-PDKS", "personel": "GIZLI-PERSONEL"} {
			l := prep(r.who, fmt.Sprintf(`{"type":"liste","kind":%q}`, kind))
			sees := (kind == "pdks" && r.seesPDKS) || (kind == "personel" && r.seesPersonel)
			if got := strings.Contains(l.Append+l.Prompt, secret); got != sees {
				t.Errorf("%s: %s listesinde sır=%v, beklenen %v", r.name, kind, got, sees)
			}
			if sees != (l.List.Total > 0) || l.List.Denied == sees {
				t.Errorf("%s/%s: Total=%d Denied=%v (görme=%v)", r.name, kind, l.List.Total, l.List.Denied, sees)
			}
			if !sees && l.Append != fmt.Sprintf("Kayıtlı %s bulunamadı.", kind) {
				t.Errorf("%s/%s: yetkisiz liste ipucu verdi: %q", r.name, kind, l.Append)
			}
		}
	}

	// Denetim: yalnızca GÖRÜLEN özel kayıtlar, görenin kimliğiyle yazılmış.
	for who, wantIDs := range map[string][]string{
		readUye.ID:    nil,
		readAli.ID:    {"pdks1"},
		readPatron.ID: {"pdks1", "per1"},
	} {
		es, _ := audit.ListByActor(context.Background(), who, 0, 0)
		seen := map[string]bool{}
		for _, x := range es {
			if x.Action != ActionView {
				t.Errorf("%s: beklenmeyen işlem %s", who, x.Action)
			}
			seen[x.ClaimID] = true
		}
		var got []string
		for id := range seen {
			got = append(got, id)
		}
		if !accSameIDs(got, wantIDs...) {
			t.Errorf("%s denetlenen kayıtlar %v, beklenen %v", who, got, wantIDs)
		}
	}
	if es, _ := audit.ListByActor(context.Background(), "", 0, 0); len(es) != 0 {
		t.Errorf("kimliksiz çağrılar denetim kaydı üretmemeli: %d", len(es))
	}
}

func TestReaderFailsClosedWhenAccessCannotBeAudited(t *testing.T) {
	near := []float32{1, 0, 0}
	c := readClaim("pdks1", "pdks", secretPDKS, StatusActive, near)
	c.SubjectID = "u_ali"
	repo := readRepo(t, c)
	rd := NewReader(repo, &readLLM{reply: `{"type":"soru"}`}, &readEmb{vecs: map[string][]float32{"soru": near}},
		ReadConfig{Model: "m", EmbedModel: readModel}).WithAudit(viewFailAudit{NewMemoryAuditRepo()})

	p, err := rd.Prepare(context.Background(), "soru", readAli)
	if err == nil && p != nil && strings.Contains(p.Prompt+p.Append, "GIZLI-PDKS") {
		t.Fatal("denetim yazılamadığı halde kişisel veri prompt'a girdi")
	}
	if err == nil {
		t.Error("denetim yazılamayınca hata dönmeli (çağıran sohbeti bilgisiz sürdürür)")
	}
}
