package main

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

	"golang.org/x/crypto/bcrypt"

	"fiko-ai/config"
	"fiko-ai/internal/agent"
	"fiko-ai/internal/claim"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/protocol"
	"fiko-ai/internal/user"
)

func newClaimTestService(t *testing.T, users ...user.User) *user.Service {
	t.Helper()
	store := user.NewMemoryStore()
	for i := range users {
		u := users[i]
		if err := store.Create(context.Background(), &u); err != nil {
			t.Fatalf("kullanıcı eklenemedi %q: %v", u.Username, err)
		}
	}
	svc, err := user.NewService(store, user.ServiceOptions{BcryptCost: bcrypt.MinCost})
	if err != nil {
		t.Fatalf("NewService: %v", err)
	}
	return svc
}

func claimCfg(enabled bool) config.Config {
	var c config.Config
	c.Users.Enabled = enabled
	c.Chat.DefaultUserID = "local"
	return c
}

// Rol eşlemesi ve görünen ad: admin → patron, diğer her şey → üye.
func TestClaimIdentity_RolEslemesi(t *testing.T) {
	svc := newClaimTestService(t,
		user.User{ID: "usr_fikri", Username: "fikri", DisplayName: "Fikri", Role: user.RoleAdmin},
		user.User{ID: "usr_ali", Username: "ali", Role: user.RoleUser},
	)
	id := newClaimIdentity(svc, claimCfg(true))
	ctx := context.Background()

	p, err := id.Actor(ctx, "usr_fikri")
	if err != nil || p.Role != claim.RolePatron || p.Name != "Fikri" {
		t.Fatalf("patron: %+v %v", p, err)
	}
	u, err := id.Actor(ctx, "usr_ali")
	if err != nil || u.Role != claim.RoleUye {
		t.Fatalf("üye: %+v %v", u, err)
	}
}

// Üyelik kapalıyken yazma kapalıdır: X-Fiko-User başlığıyla herkes herkes olabilir.
func TestClaimIdentity_UyelikKapaliYazmaKapali(t *testing.T) {
	svc := newClaimTestService(t, user.User{ID: "usr_fikri", Username: "fikri", Role: user.RoleAdmin})
	_, err := newClaimIdentity(svc, claimCfg(false)).Actor(context.Background(), "usr_fikri")
	if !errors.Is(err, claim.ErrAuthRequired) {
		t.Fatalf("beklenen ErrAuthRequired, gelen %v", err)
	}
}

// Hesap servisi hiç kurulamadıysa da yazma kapalı kalır (nil servis panik üretmez).
func TestClaimIdentity_ServisYoksaYazmaKapali(t *testing.T) {
	_, err := newClaimIdentity(nil, claimCfg(true)).Actor(context.Background(), "usr_fikri")
	if !errors.Is(err, claim.ErrAuthRequired) {
		t.Fatalf("beklenen ErrAuthRequired, gelen %v", err)
	}
}

// Paylaşılan API anahtarı DEFAULT_USER_ID ("local") ile gelir. Aynı adlı gerçek bir hesap olsa
// bile kişi sayılmaz; ayrıca olmayan hesap da reddedilir.
func TestClaimIdentity_PaylasilanAnahtarVeOlmayanHesap(t *testing.T) {
	svc := newClaimTestService(t, user.User{ID: "local", Username: "local", Role: user.RoleAdmin})
	id := newClaimIdentity(svc, claimCfg(true))
	for _, uid := range []string{"local", "hacker"} {
		if _, err := id.Actor(context.Background(), uid); !errors.Is(err, claim.ErrUnverified) {
			t.Fatalf("%s: beklenen ErrUnverified, gelen %v", uid, err)
		}
	}
}

// Devre dışı bırakılmış hesap yazamaz.
func TestClaimIdentity_DevreDisiHesap(t *testing.T) {
	svc := newClaimTestService(t, user.User{ID: "usr_off", Username: "kapali", Role: user.RoleUser, Disabled: true})
	if _, err := newClaimIdentity(svc, claimCfg(true)).Actor(context.Background(), "usr_off"); !errors.Is(err, claim.ErrUnverified) {
		t.Fatalf("beklenen ErrUnverified, gelen %v", err)
	}
}

// claimFakeLLM her çıkarım çağrısında aynı tek adayı döndüren sahte sağlayıcıdır.
type claimFakeLLM struct{}

func (claimFakeLLM) Name() string { return "fake" }
func (claimFakeLLM) Generate(context.Context, llm.Request) (llm.Result, error) {
	return llm.Result{Content: `{"remember":[{"text":"ACME müşterimizdir","kind":"firma","key":"ACME"}]}`}, nil
}
func (f claimFakeLLM) GenerateStream(ctx context.Context, r llm.Request, _ llm.DeltaFunc) (llm.Result, error) {
	return f.Generate(ctx, r)
}
func (claimFakeLLM) ListModels(context.Context) ([]llm.ModelInfo, error) { return nil, nil }

func newCapturerFixture(t *testing.T, realAuth bool) (claimCapturer, claim.Repository) {
	t.Helper()
	svcUsers := newClaimTestService(t,
		user.User{ID: "usr_fikri", Username: "fikri", DisplayName: "Fikri", Role: user.RoleAdmin},
	)
	repo := claim.NewMemoryRepo()
	svc := claim.NewService(repo, claim.NewMemoryAuditRepo(), claimFakeLLM{}, nil, claim.Config{Model: "m"}, nil)
	t.Cleanup(svc.Close)
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	return claimCapturer{svc: svc, id: newClaimIdentity(svcUsers, claimCfg(realAuth)), log: log}, repo
}

func waitClaims(t *testing.T, repo claim.Repository, want int) int {
	t.Helper()
	deadline := time.Now().Add(3 * time.Second)
	for {
		n, err := repo.Count(context.Background(), claim.Filter{})
		if err != nil {
			t.Fatalf("Count: %v", err)
		}
		if n >= want || time.Now().After(deadline) {
			return n
		}
		time.Sleep(20 * time.Millisecond)
	}
}

// Doğrulanmış hesapla gelen mesaj gerçekten ortak depoya yazılır ve yazar bilgisi kayıtta durur.
func TestClaimCapturer_DogrulanmisHesapYazar(t *testing.T) {
	c, repo := newCapturerFixture(t, true)
	c.Capture(context.Background(), agent.ClaimRequest{UserID: "usr_fikri", ConversationID: "c1", Text: "ACME müşterimiz"})
	if n := waitClaims(t, repo, 1); n != 1 {
		t.Fatalf("1 kayıt beklenirdi, var=%d", n)
	}
	list, _ := repo.List(context.Background(), claim.Filter{}, 10, 0)
	if list[0].AuthorID != "usr_fikri" || list[0].AuthorName != "Fikri" {
		t.Fatalf("yazar yanlış: %+v", list[0])
	}
}

// Üyelik kapalıysa, doğrulanamayan kimlikle ya da paylaşılan anahtar kimliğiyle HİÇBİR ŞEY yazılmaz.
func TestClaimCapturer_KimliksizYazmaz(t *testing.T) {
	cases := []struct {
		name     string
		realAuth bool
		userID   string
	}{
		{"üyelik kapalı", false, "usr_fikri"},
		{"hesap yok", true, "hacker"},
		{"paylaşılan anahtar kimliği", true, "local"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			c, repo := newCapturerFixture(t, tc.realAuth)
			c.Capture(context.Background(), agent.ClaimRequest{UserID: tc.userID, Text: "ACME müşterimiz"})
			time.Sleep(150 * time.Millisecond) // yanlışlıkla başlayan bir yazmaya süre tanı
			if n, _ := repo.Count(context.Background(), claim.Filter{}); n != 0 {
				t.Fatalf("kimlik doğrulanamadı ama %d kayıt yazıldı", n)
			}
		})
	}
}

// Bayrak kapalıyken (varsayılan) kurulum hiçbir şey döndürmez.
func TestSetupClaims_BayrakKapaliNil(t *testing.T) {
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	cfg := claimCfg(true)
	if got := setupClaims(claim.NewMemoryRepo(), claim.NewMemoryAuditRepo(), claimFakeLLM{}, nil, nil, cfg, log, nil); got != nil {
		t.Fatal("FIKO_CLAIMS_WRITE=false iken kurulum nil olmalı")
	}
	cfg.Claims.Write = true
	got := setupClaims(claim.NewMemoryRepo(), claim.NewMemoryAuditRepo(), claimFakeLLM{}, nil, nil, cfg, log, nil)
	if got == nil {
		t.Fatal("bayrak açıkken kurulum dolu olmalı")
	}
	closeClaims(got)
}


// Yazma bitince özet, doğrulanmış kullanıcının bağlantısına (sohbet kimliğiyle) iletilir.
func TestClaimCapturer_OzetKullaniciyaIletilir(t *testing.T) {
	c, _ := newCapturerFixture(t, true)
	type sent struct {
		user, conv string
		p          protocol.ClaimSummaryPayload
	}
	got := make(chan sent, 1)
	c.emit = func(u, conv string, p protocol.ClaimSummaryPayload) { got <- sent{u, conv, p} }

	c.Capture(context.Background(), agent.ClaimRequest{UserID: "usr_fikri", ConversationID: "c1", Text: "ACME müşterimiz"})
	select {
	case s := <-got:
		if s.user != "usr_fikri" || s.conv != "c1" {
			t.Fatalf("özet yanlış kişiye/sohbete gitti: %+v", s)
		}
		if s.p.Added != 1 || s.p.Conflicts != 0 || s.p.Incomplete != 0 {
			t.Fatalf("sayılar yanlış: %+v", s.p)
		}
		if !strings.Contains(s.p.Text, "1 kayıt eklendi") {
			t.Fatalf("özet metni beklenen cümleyi içermiyor: %q", s.p.Text)
		}
	case <-time.After(3 * time.Second):
		t.Fatal("özet iletilmedi")
	}
}

// Kimlik doğrulanamayan tur hiçbir şey yazmaz ve özet de ÜRETMEZ (kimse bilgilendirilmez).
func TestClaimCapturer_KimliksizOzetGondermez(t *testing.T) {
	c, _ := newCapturerFixture(t, true)
	called := make(chan struct{}, 1)
	c.emit = func(string, string, protocol.ClaimSummaryPayload) { called <- struct{}{} }

	c.Capture(context.Background(), agent.ClaimRequest{UserID: "hacker", Text: "ACME müşterimiz"})
	select {
	case <-called:
		t.Fatal("kimliksiz turda özet gönderilmemeli")
	case <-time.After(200 * time.Millisecond):
	}
}

// Özet yalnızca gösterilecek bir şey varsa üretilir; sıradan turlar sessiz kalır.
func TestClaimSummaryOf(t *testing.T) {
	if _, ok := claimSummaryOf(nil); ok {
		t.Fatal("nil sonuç için özet olmamalı")
	}
	if _, ok := claimSummaryOf(&claim.WriteResult{}); ok {
		t.Fatal("hiçbir şey olmadıysa özet olmamalı")
	}
	if _, ok := claimSummaryOf(&claim.WriteResult{Unchanged: 5, Duplicates: 2}); ok {
		t.Fatal("yalnızca 'zaten kayıtlıydı' özet nedeni değildir")
	}
	p, ok := claimSummaryOf(&claim.WriteResult{Added: 140, Updated: 3, Incomplete: 2, Conflicts: make([]claim.Conflict, 4)})
	if !ok || p.Added != 140 || p.Updated != 3 || p.Conflicts != 4 || p.Incomplete != 2 {
		t.Fatalf("sayılar yanlış taşındı: ok=%v %+v", ok, p)
	}
	if !strings.Contains(p.Text, "140 kayıt eklendi") || !strings.Contains(p.Text, "4 çelişki var") ||
		!strings.Contains(p.Text, "2 kayıtta eksik bilgi var") {
		t.Fatalf("özet cümlesi spec biçiminde değil: %q", p.Text)
	}
	if p, ok := claimSummaryOf(&claim.WriteResult{FailedChunks: 1, Chunks: 3}); !ok || p.FailedChunks != 1 {
		t.Fatalf("işlenemeyen parça tek başına özet nedenidir: ok=%v %+v", ok, p)
	}
}

func TestClaimSummaryOf_ConflictNoteIsNeutralAndPointsToPatron(t *testing.T) {
	p, ok := claimSummaryOf(&claim.WriteResult{Added: 2, Conflicts: []claim.Conflict{{NewID: "n", ExistingID: "e"}}})
	if !ok || p.Conflicts != 1 {
		t.Fatalf("çelişki özet nedenidir: ok=%v %+v", ok, p)
	}
	if !strings.Contains(p.Text, "2 kayıt eklendi, 1 çelişki var") ||
		!strings.Contains(p.Text, "hangisinin geçerli olduğuna patron karar verecek") {
		t.Fatalf("özet çelişki notunu taşımalı: %q", p.Text)
	}
	// Çelişki yoksa not eklenmez.
	if p, _ := claimSummaryOf(&claim.WriteResult{Added: 2}); strings.Contains(p.Text, "patron") {
		t.Fatalf("çelişkisiz özette patron notu olmamalı: %q", p.Text)
	}
}
