package api

// auth_flow_test.go üyelik sistemi AÇIKKEN gerçek giriş akışını sınar.
//
// Bu dosya, 2. dilimde kod okumasıyla bulunan hataların regresyon testidir:
//   - üyelik açıkken /api/health, /api/system, /api/models ve CORS OPTIONS token istememeli
//   - /api/auth/me oturumu çözebilmeli (önceden HER ZAMAN 401 dönüyordu)
//   - FIKO_API_KEY doluyken geçerli oturum token'ı yeterli olmalı
//   - bir üye kendi araç yetkisini yükseltememeli (yalnızca admin)
//   - ?token= yalnızca WebSocket yolunda kabul edilmeli (REST'te URL'deki token günlüğe sızar)

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

	"github.com/gofiber/fiber/v2"

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

// usersServer, üyeliği AÇIK bir sunucu kurar. authKey doluysa paylaşılan API anahtarı da tanımlıdır.
func usersServer(t *testing.T, authKey string) *Server {
	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})
	// BcryptCost 4 (en düşük): testler yavaşlamasın.
	users, err := user.NewService(user.NewMemoryStore(), user.ServiceOptions{BcryptCost: 4})
	if err != nil {
		t.Fatal(err)
	}
	s := New(context.Background(), Config{
		Env: "test", Version: "test",
		AuthKey:       authKey,
		UsersEnabled:  true,
		DefaultUserID: "local", DefaultPageSize: 2, MaxPageSize: 5,
	}, Deps{
		Conversations: svc, Hub: hub, Bus: bus, Users: users,
		Grants: tool.NewGrantStore(tool.DefaultPolicy()),
	}, log)
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		_ = s.Shutdown(ctx)
	})
	return s
}

// promptsServer, üyeliği AÇIK ve AYARLARI BAĞLI bir sunucu kurar (prompt uçları için).
//
// Neden ayrı? usersServer'da Deps.Settings yok; getPrompts/putPrompts ayar
// olmadan 503 döner ve yetki testi yapılamaz.
func promptsServer(t *testing.T) (*Server, *settings.Service) {
	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)
	}
	set := settings.New(settings.Values{Model: "m"}, log, bus)
	s := New(context.Background(), Config{
		Env: "test", Version: "test",
		UsersEnabled: true, DefaultUserID: "local",
		DefaultPageSize: 2, MaxPageSize: 5,
	}, Deps{
		Conversations: svc, Hub: hub, Bus: bus, Users: users,
		Grants:   tool.NewGrantStore(tool.DefaultPolicy()),
		Settings: set,
	}, log)
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		_ = s.Shutdown(ctx)
	})
	return s, set
}

type promptsBody struct {
	Parts []struct {
		Name     string `json:"name"`
		Title    string `json:"title"`
		Safe     bool   `json:"safe"`
		Editable bool   `json:"editable"`
		Default  string `json:"default"`
		Text     string `json:"text"`
		Enabled  bool   `json:"enabled"`
		Modified bool   `json:"modified"`
	} `json:"parts"`
	DisabledSafeParts []string `json:"disabledSafeParts"`
}

type authReply struct {
	Token string         `json:"token"`
	User  user.UserView  `json:"user"`
	Error map[string]any `json:"error"`
}

// registerUser, üye açar ve oturum bilgisini döner.
func registerUser(t *testing.T, s *Server, name string) authReply {
	t.Helper()
	resp, raw := do(t, s, http.MethodPost, "/api/auth/register",
		map[string]string{"username": name, "password": "parola-" + name + "-123"}, nil)
	if resp.StatusCode != 200 {
		t.Fatalf("%s kaydı: %d %s", name, resp.StatusCode, raw)
	}
	var out authReply
	if err := json.Unmarshal(raw, &out); err != nil {
		t.Fatal(err)
	}
	if out.Token == "" {
		t.Fatalf("%s için token dönmedi: %s", name, raw)
	}
	return out
}

func bearer(tok string) map[string]string { return map[string]string{"Authorization": "Bearer " + tok} }

// Üyelik AÇIKKEN açık uçlar token İSTEMEMELİ: ön yüz /api/system'e bakıp giriş gerekip
// gerekmediğini öğrenir; burası 400 verirse giriş ekranı hiç açılamaz.
func TestUsersEnabledPublicEndpointsStayOpen(t *testing.T) {
	s := usersServer(t, "")
	for _, p := range []string{"/api/health", "/api/system", "/api/models"} {
		resp, raw := do(t, s, http.MethodGet, p, nil, nil)
		if resp.StatusCode == 400 || resp.StatusCode == 401 {
			t.Errorf("%s token istememeli, %d geldi: %s", p, resp.StatusCode, raw)
		}
	}
	// CORS ön kontrolü (OPTIONS) kimlik isteyemez: tarayıcı başlık gönderemez.
	req := httptest.NewRequest(http.MethodOptions, "/api/conversations", nil)
	req.Header.Set("Origin", "http://localhost:4200")
	req.Header.Set("Access-Control-Request-Method", "GET")
	resp, err := s.App().Test(req, 5000)
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode == 400 || resp.StatusCode == 401 {
		t.Errorf("OPTIONS ön kontrolü kimlik istememeli, %d geldi", resp.StatusCode)
	}
}

// /api/system üyeliğin açık olduğunu bildirmeli (ön yüz kayıt/giriş sekmelerini buna göre açar).
func TestSystemReportsUsersEnabled(t *testing.T) {
	s := usersServer(t, "")
	_, raw := do(t, s, http.MethodGet, "/api/system", nil, nil)
	var out struct {
		Auth struct {
			Users bool `json:"users"`
		} `json:"auth"`
	}
	if err := json.Unmarshal(raw, &out); err != nil {
		t.Fatal(err)
	}
	if !out.Auth.Users {
		t.Errorf("auth.users true olmalı: %s", raw)
	}
}

// Korumalı uç token'sız 401, geçersiz token 401, geçerli token 200 vermeli.
func TestProtectedEndpointNeedsSession(t *testing.T) {
	s := usersServer(t, "")
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations", nil, nil); resp.StatusCode != 401 {
		t.Errorf("token yokken 401 beklenir, %d geldi", resp.StatusCode)
	}
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations", nil, bearer("uydurma-token")); resp.StatusCode != 401 {
		t.Errorf("geçersiz token'da 401 beklenir, %d geldi", resp.StatusCode)
	}
	tok := registerUser(t, s, "ali").Token
	if resp, raw := do(t, s, http.MethodGet, "/api/conversations", nil, bearer(tok)); resp.StatusCode != 200 {
		t.Errorf("geçerli token'da 200 beklenir, %d geldi: %s", resp.StatusCode, raw)
	}
}

// /api/auth/me oturumu çözmeli. Önceki hâlinde isAuthPath erken döndüğü için HER ZAMAN 401'di.
func TestMeResolvesSession(t *testing.T) {
	s := usersServer(t, "")
	reg := registerUser(t, s, "ali")

	resp, raw := do(t, s, http.MethodGet, "/api/auth/me", nil, bearer(reg.Token))
	if resp.StatusCode != 200 {
		t.Fatalf("/me geçerli token'la 200 vermeli, %d: %s", resp.StatusCode, raw)
	}
	if !strings.Contains(string(raw), `"username":"ali"`) {
		t.Errorf("/me kullanıcı adını dönmeli: %s", raw)
	}
	if strings.Contains(string(raw), "passwordHash") || strings.Contains(string(raw), "parola-ali") {
		t.Errorf("/me parola bilgisi SIZDIRMAMALI: %s", raw)
	}
	if resp, _ := do(t, s, http.MethodGet, "/api/auth/me", nil, nil); resp.StatusCode != 401 {
		t.Errorf("/me token'sız 401 vermeli, %d geldi", resp.StatusCode)
	}
}

// Çıkış token'ı geçersiz kılmalı.
func TestLogoutInvalidatesToken(t *testing.T) {
	s := usersServer(t, "")
	tok := registerUser(t, s, "ali").Token
	if resp, _ := do(t, s, http.MethodPost, "/api/auth/logout", nil, bearer(tok)); resp.StatusCode != 200 {
		t.Fatalf("logout 200 vermeli, %d geldi", resp.StatusCode)
	}
	if resp, _ := do(t, s, http.MethodGet, "/api/auth/me", nil, bearer(tok)); resp.StatusCode != 401 {
		t.Errorf("çıkıştan sonra token geçersiz olmalı, %d geldi", resp.StatusCode)
	}
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations", nil, bearer(tok)); resp.StatusCode != 401 {
		t.Errorf("çıkıştan sonra korumalı uç 401 vermeli, %d geldi", resp.StatusCode)
	}
}

// Yanlış parola ve olmayan kullanıcı AYNI yanıtı vermeli (kullanıcı adı sızmasın).
func TestLoginFailureIsGeneric(t *testing.T) {
	s := usersServer(t, "")
	registerUser(t, s, "ali")
	r1, b1 := do(t, s, http.MethodPost, "/api/auth/login", map[string]string{"username": "ali", "password": "yanlis-parola"}, nil)
	r2, b2 := do(t, s, http.MethodPost, "/api/auth/login", map[string]string{"username": "yok-boyle-biri", "password": "yanlis-parola"}, nil)
	if r1.StatusCode != 401 || r2.StatusCode != 401 {
		t.Fatalf("ikisi de 401 olmalı: %d / %d", r1.StatusCode, r2.StatusCode)
	}
	if string(b1) != string(b2) {
		t.Errorf("hata gövdeleri AYNI olmalı:\n%s\n%s", b1, b2)
	}
}

// Üyelik AÇIKKEN X-Fiko-User taklidi işe yaramamalı; iki üye birbirinin verisini görmemeli.
func TestTwoUsersIsolationAndNoHeaderImpersonation(t *testing.T) {
	s := usersServer(t, "")
	ali := registerUser(t, s, "ali")
	veli := registerUser(t, s, "veli")

	// Token'sız, yalnızca başlıkla: reddedilmeli.
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations", nil, map[string]string{UserHeader: "ali"}); resp.StatusCode != 401 {
		t.Errorf("başlıkla taklit 401 vermeli, %d geldi", resp.StatusCode)
	}

	resp, raw := do(t, s, http.MethodPost, "/api/conversations", map[string]string{"title": "ali'nin gizli"}, bearer(ali.Token))
	if resp.StatusCode != 201 && resp.StatusCode != 200 {
		t.Fatalf("konuşma açılamadı: %d %s", resp.StatusCode, raw)
	}
	var created struct {
		Conversation struct {
			ID string `json:"id"`
		} `json:"conversation"`
	}
	if err := json.Unmarshal(raw, &created); err != nil || created.Conversation.ID == "" {
		t.Fatalf("konuşma kimliği okunamadı: %s", raw)
	}
	id := created.Conversation.ID

	// Veli, Ali'nin konuşmasını göremez.
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations/"+id, nil, bearer(veli.Token)); resp.StatusCode != 404 {
		t.Errorf("başka üyenin konuşması 404 vermeli, %d geldi", resp.StatusCode)
	}
	// Veli, Ali'nin adını başlığa yazsa bile token'ı Veli'nindir → hâlâ göremez.
	h := bearer(veli.Token)
	h[UserHeader] = "ali"
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations/"+id, nil, h); resp.StatusCode != 404 {
		t.Errorf("X-Fiko-User token'ı EZMEMELİ, %d geldi", resp.StatusCode)
	}
	// Ali kendi konuşmasını görür.
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations/"+id, nil, bearer(ali.Token)); resp.StatusCode != 200 {
		t.Errorf("sahibi görebilmeli, %d geldi", resp.StatusCode)
	}
}

// FIKO_API_KEY doluyken geçerli oturum token'ı YETERLİ olmalı (önceden X-Fiko-Auth de isteniyordu).
func TestSessionTokenSufficientWhenAPIKeySet(t *testing.T) {
	s := usersServer(t, "gizli-anahtar")
	tok := registerUser(t, s, "ali").Token

	if resp, raw := do(t, s, http.MethodGet, "/api/conversations", nil, bearer(tok)); resp.StatusCode != 200 {
		t.Errorf("API anahtarı + geçerli token: 200 beklenir, %d geldi: %s", resp.StatusCode, raw)
	}
	// Ne token ne anahtar: reddedilmeli.
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations", nil, nil); resp.StatusCode != 401 {
		t.Errorf("kimlik yokken 401 beklenir, %d geldi", resp.StatusCode)
	}
	// Geçersiz token + doğru anahtar: geçersiz token sessizce yutulmamalı.
	h := bearer("uydurma")
	h[AuthHeader] = "gizli-anahtar"
	if resp, _ := do(t, s, http.MethodGet, "/api/conversations", nil, h); resp.StatusCode != 401 {
		t.Errorf("geçersiz token reddedilmeli, %d geldi", resp.StatusCode)
	}
}

// İlk hesap yönetici olur (kurulum): sonrakiler normal üyedir.
func TestFirstAccountIsAdmin(t *testing.T) {
	s := usersServer(t, "")
	if r := registerUser(t, s, "sahip"); r.User.Role != user.RoleAdmin {
		t.Errorf("ilk hesap admin olmalı, %q geldi", r.User.Role)
	}
	if r := registerUser(t, s, "misafir"); r.User.Role != user.RoleUser {
		t.Errorf("ikinci hesap normal üye olmalı, %q geldi", r.User.Role)
	}
}

// Üye kendi araç yetkisini yükseltememeli; yönetici başkasının yetkisini ayarlayabilmeli.
func TestPermissionsAdminOnly(t *testing.T) {
	s := usersServer(t, "")
	admin := registerUser(t, s, "sahip")
	member := registerUser(t, s, "misafir")

	grant := map[string]any{"grants": []string{"read", "write", "execute", "delete", "system"}, "allowHighRisk": true}

	// Üye kendine yetki VEREMEZ. Yönetici ucu üyeye GÖRÜNMEZ (404), 403 değil.
	if resp, _ := do(t, s, http.MethodPut, "/api/permissions", grant, bearer(member.Token)); resp.StatusCode != 404 {
		t.Errorf("üyenin kendi yetkisini yükseltmesi 404 vermeli, %d geldi", resp.StatusCode)
	}
	if resp, _ := do(t, s, http.MethodGet, "/api/admin/users", nil, bearer(member.Token)); resp.StatusCode != 404 {
		t.Errorf("üyeye admin listesi görünmemeli, %d geldi", resp.StatusCode)
	}
	if resp, _ := do(t, s, http.MethodPut, "/api/admin/users/"+member.User.ID+"/permissions", grant, bearer(member.Token)); resp.StatusCode != 404 {
		t.Errorf("üye başkasının yetkisini ayarlayamamalı, %d geldi", resp.StatusCode)
	}

	// Üye kendi mevcut yetkisini OKUYABİLİR ve varsayılan read+write+execute'dur (delete YOK).
	resp, raw := do(t, s, http.MethodGet, "/api/permissions", nil, bearer(member.Token))
	if resp.StatusCode != 200 {
		t.Fatalf("üye kendi yetkisini okuyabilmeli, %d: %s", resp.StatusCode, raw)
	}
	var perm struct {
		Policy struct {
			Grants []string `json:"grants"`
		} `json:"policy"`
		Admin bool `json:"admin"`
	}
	if err := json.Unmarshal(raw, &perm); err != nil {
		t.Fatal(err)
	}
	// NOT: yanıttaki "known" listesi tüm izin adlarını içerir; bu yüzden yalnızca politikaya bakılır.
	if len(perm.Policy.Grants) != 3 {
		t.Errorf("yeni üye varsayılanı read+write+execute olmalı (delete yok): %v", perm.Policy.Grants)
	}
	if perm.Admin {
		t.Errorf("üye yönetici görünmemeli: %s", raw)
	}

	// Yönetici üyeler listesini görür; parola özeti sızmaz.
	resp, raw = do(t, s, http.MethodGet, "/api/admin/users", nil, bearer(admin.Token))
	if resp.StatusCode != 200 {
		t.Fatalf("admin listesi 200 vermeli, %d: %s", resp.StatusCode, raw)
	}
	if !strings.Contains(string(raw), `"username":"misafir"`) {
		t.Errorf("liste üyeyi içermeli: %s", raw)
	}
	if strings.Contains(string(raw), "passwordHash") {
		t.Errorf("liste parola özeti SIZDIRMAMALI: %s", raw)
	}

	// Yönetici üyeye read+execute verir; üye bunu kendi okumasında görür.
	resp, raw = do(t, s, http.MethodPut, "/api/admin/users/"+member.User.ID+"/permissions",
		map[string]any{"grants": []string{"read", "execute"}}, bearer(admin.Token))
	if resp.StatusCode != 200 {
		t.Fatalf("admin yetki ayarı 200 vermeli, %d: %s", resp.StatusCode, raw)
	}
	_, raw = do(t, s, http.MethodGet, "/api/permissions", nil, bearer(member.Token))
	if err := json.Unmarshal(raw, &perm); err != nil {
		t.Fatal(err)
	}
	if len(perm.Policy.Grants) != 2 || perm.Policy.Grants[0] != "read" {
		t.Errorf("üyenin yeni yetkisi read+execute olmalı: %v", perm.Policy.Grants)
	}
	// Admin yetkisi YALNIZCA o üyeye yazılır; yöneticinin kendi politikası değişmez.
	_, raw = do(t, s, http.MethodGet, "/api/permissions", nil, bearer(admin.Token))
	if !strings.Contains(string(raw), `"scope":"default"`) {
		t.Errorf("yöneticinin kendi politikası DEĞİŞMEMELİ (scope default kalmalı): %s", raw)
	}

	// Olmayan üye: 404.
	if resp, _ := do(t, s, http.MethodPut, "/api/admin/users/usr_yok/permissions", grant, bearer(admin.Token)); resp.StatusCode != 404 {
		t.Errorf("olmayan üye 404 vermeli, %d geldi", resp.StatusCode)
	}
	// Bozuk izin adı: 400.
	if resp, _ := do(t, s, http.MethodPut, "/api/admin/users/"+member.User.ID+"/permissions",
		map[string]any{"grants": []string{"uydurma"}}, bearer(admin.Token)); resp.StatusCode != 400 {
		t.Errorf("bilinmeyen izin 400 vermeli, %d geldi", resp.StatusCode)
	}
}

// ?token= YALNIZCA /ws yolunda kabul edilir: tarayıcı WebSocket'i başlık gönderemez, ama
// REST'te URL'ye token koymak erişim günlüklerine sızmasına yol açar.
func TestTokenQueryOnlyOnWebSocketPath(t *testing.T) {
	app := fiber.New()
	app.Get("/ws", func(c *fiber.Ctx) error { return c.SendString(bearerToken(c)) })
	app.Get("/api/x", func(c *fiber.Ctx) error { return c.SendString(bearerToken(c)) })

	get := func(path string) string {
		resp, err := app.Test(httptest.NewRequest(http.MethodGet, path, nil), 5000)
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()
		b, _ := io.ReadAll(resp.Body)
		return string(b)
	}
	if got := get("/ws?token=abc123"); got != "abc123" {
		t.Errorf("/ws?token= kabul edilmeli, %q geldi", got)
	}
	if got := get("/api/x?token=abc123"); got != "" {
		t.Errorf("REST yolunda ?token= kabul EDİLMEMELİ, %q geldi", got)
	}
}
