package api

import (
	"context"
	"io"
	"log/slog"
	"net/http"
	"testing"

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/event"
	"fiko-ai/internal/protocol"
	"fiko-ai/internal/skill"
	"fiko-ai/internal/tool"
	"fiko-ai/internal/tool/builtin"
)

// toolServer tool sistemi bağlı bir sunucu kurar: Registry + Executor + GrantStore.
// grants verilmezse güvenli varsayılan (read+write+execute, delete YOK) kullanılır.
func toolServer(t *testing.T, grants *tool.GrantStore) (*Server, *tool.Registry) {
	t.Helper()
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	bus := event.NewMemoryBus(nil, 16)
	t.Cleanup(bus.Close)

	reg := tool.NewRegistry(tool.NewMemoryRepo(), bus)
	if err := builtin.Register(context.Background(), reg, builtin.Options{FSRoot: t.TempDir()}); err != nil {
		t.Fatalf("builtin.Register: %v", err)
	}
	if grants == nil {
		grants = tool.NewGrantStore(tool.DefaultPolicy())
	}
	exec := tool.NewExecutor(reg, grants, nil, bus, tool.ExecutorOptions{})

	skillSvc := skill.NewService(skill.NewMemoryRepo(), bus)
	// Skill'in executorConfig.tool bağı gerçek Registry'ye karşı doğrulanır ve
	// çalıştırma yeteneği Executor'a bağlanır (üretimde cmd/server'da yapılır).
	skillSvc.SetToolChecker(toolChecker{reg})
	skillSvc.SetToolExecutor(skillRunner{exec})

	s := New(context.Background(), Config{
		Env: "test", Version: "test", DefaultUserID: "local", DefaultPageSize: 50, MaxPageSize: 200,
	}, Deps{
		Conversations: conversation.NewService(conversation.NewMemoryRepo()),
		Bus:           bus,
		Skills:        skillSvc,
		Tools:         reg,
		ToolExec:      exec,
		Grants:        grants,
	}, log)
	return s, reg
}

// toolChecker Registry'yi skill.ToolChecker arayüzüne bağlar (üretimde cmd/server'da yapılır).
type toolChecker struct{ reg *tool.Registry }

func (c toolChecker) HasTool(ctx context.Context, name string) bool {
	_, err := c.reg.GetByName(ctx, name)
	return err == nil
}

// skillRunner Executor'u skill.ToolExecutor arayüzüne bağlar (üretimde cmd/server'da yapılır).
type skillRunner struct{ exec *tool.Executor }

func (r skillRunner) ExecuteTool(ctx context.Context, name string, args map[string]any) (skill.ToolRunResult, error) {
	res, err := r.exec.Execute(ctx, tool.Request{Ref: name, Input: args, Actor: "agent"})
	if err != nil {
		return skill.ToolRunResult{}, err
	}
	return skill.ToolRunResult{ToolName: res.ToolName, Output: res.Output, DurationMs: res.DurationMs}, nil
}

func TestToolListShowsPermissionState(t *testing.T) {
	s, _ := toolServer(t, nil)
	resp, raw := do(t, s, http.MethodGet, "/api/tools", nil, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("GET /api/tools = %d (%s)", resp.StatusCode, raw)
	}
	items := decode[toolsResponse](t, raw).Items
	if len(items) < 4 {
		t.Fatalf("builtin tool'lar listelenmedi: %d", len(items))
	}
	byName := map[string]toolView{}
	for _, it := range items {
		byName[it.Name] = it
	}
	echo, ok := byName["fiko.echo"]
	if !ok {
		t.Fatalf("fiko.echo yok: %v", byName)
	}
	if !echo.Executable || !echo.Allowed {
		t.Errorf("echo çalıştırılabilir ve varsayılan yetkili olmalı: %+v", echo)
	}
	// read artık varsayılan olarak VERİLİR (canlı hata gereği: şirket tool'ları aksi
	// hâlde modele hiç sunulmuyordu). Geri alınamaz işlem (fs.delete) KAPALI kalmalı.
	read := byName["fs.read_text"]
	if !read.Executable || !read.Allowed {
		t.Errorf("read tool'u varsayılan yetkili olmalı: %+v", read)
	}
	del := byName["fs.delete"]
	if !del.Executable || del.Allowed {
		t.Errorf("delete tool'u çalıştırılabilir ama varsayılan yetkisiz olmalı: %+v", del)
	}
	if len(del.Missing) == 0 || del.Missing[0] != "delete" {
		t.Errorf("delete tool'unda eksik 'delete' bildirilmeliydi: %v", del.Missing)
	}
}

func TestToolExecuteAndPermissionGate(t *testing.T) {
	s, _ := toolServer(t, nil)

	// Varsayılan yetki: echo çalışır.
	id := toolID(t, s, "fiko.echo")
	resp, raw := do(t, s, http.MethodPost, "/api/tools/"+id+"/execute", map[string]any{
		"input": map[string]any{"message": "merhaba"},
	}, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("execute = %d (%s)", resp.StatusCode, raw)
	}
	out := decode[tool.Result](t, raw).Output
	if out["message"] != "merhaba" {
		t.Errorf("çıktı: %v", out)
	}

	// delete izni varsayılan YOKTUR → 403 permission_denied.
	// (read artık varsayılan verildiği için onunla yetki kapısı sınanamaz.)
	delID := toolID(t, s, "fs.delete")
	resp, raw = do(t, s, http.MethodPost, "/api/tools/"+delID+"/execute", map[string]any{
		"input": map[string]any{"path": "a.txt"},
	}, nil)
	if resp.StatusCode != http.StatusForbidden || errBody(t, raw).Code != protocol.ErrCodePermissionDenied {
		t.Fatalf("yetkisiz çalıştırma: %d %s", resp.StatusCode, raw)
	}

	// İzin verilince çalışır (dosya yok → tool hatası, ama 403 DEĞİL).
	// NOT 1: PUT tüm izin kümesini DEĞİŞTİRİR; execute'i de vermek gerekir
	// (her tool çalıştırıldığında execute ister — bkz. tool.Required).
	// NOT 2: fs.delete HIGH risk'tir; izin (delete) tek başına yetmez, risk kapısı da
	// açılmalıdır (allowHighRisk). İkisi ayrı kapılardır (bkz. Policy.Authorize).
	resp, raw = do(t, s, http.MethodPut, "/api/permissions", map[string]any{
		"grants": []string{"execute", "read", "delete"}, "allowHighRisk": true,
	}, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("PUT /api/permissions = %d (%s)", resp.StatusCode, raw)
	}
	resp, raw = do(t, s, http.MethodPost, "/api/tools/"+delID+"/execute", map[string]any{
		"input": map[string]any{"path": "a.txt"},
	}, nil)
	if resp.StatusCode == http.StatusForbidden {
		t.Errorf("izin verildiği hâlde reddedildi: %s", raw)
	}

	// Yalnızca "read" verilirse execute eksikliği bildirilir (sessiz davranış yok).
	resp, _ = do(t, s, http.MethodPut, "/api/permissions", map[string]any{
		"grants": []string{"read"},
	}, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatal("izin güncellenemedi")
	}
	resp, raw = do(t, s, http.MethodPost, "/api/tools/"+delID+"/execute", map[string]any{
		"input": map[string]any{"path": "a.txt"},
	}, nil)
	if resp.StatusCode != http.StatusForbidden {
		t.Errorf("execute izni olmadan çalışmamalıydı: %d %s", resp.StatusCode, raw)
	}
}

func TestToolInputValidationIs400WithFields(t *testing.T) {
	s, _ := toolServer(t, nil)
	id := toolID(t, s, "fiko.now")
	resp, raw := do(t, s, http.MethodPost, "/api/tools/"+id+"/execute", map[string]any{
		"input": map[string]any{"timezone": 5}, // string olmalı
	}, nil)
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("geçersiz girdi: %d %s", resp.StatusCode, raw)
	}
	eb := errBody(t, raw)
	if eb.Code != protocol.ErrCodeValidation || len(eb.Fields) == 0 || eb.Fields[0].Field != "timezone" {
		t.Errorf("alan bazlı hata bekleniyordu: %+v", eb)
	}
}

func TestToolCRUDRestrictions(t *testing.T) {
	s, _ := toolServer(t, nil)

	// Yeni (custom) tool oluşturulur — ama çalıştırılamaz (handler yok).
	resp, raw := do(t, s, http.MethodPost, "/api/tools", map[string]any{
		"name": "catia.create_plane", "description": "Plane oluşturur",
		"permissions": []string{"execute"}, "risk": "low",
	}, nil)
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("POST /api/tools = %d (%s)", resp.StatusCode, raw)
	}
	custom := decode[toolResponse](t, raw).Tool
	if custom.Executable {
		t.Error("handler'ı olmayan tool 'executable' görünmemeli")
	}
	if custom.ID == "" || custom.Builtin {
		t.Errorf("sunucu alanları: %+v", custom)
	}

	// Aynı ad → 409.
	resp, _ = do(t, s, http.MethodPost, "/api/tools", map[string]any{
		"name": "catia.create_plane", "description": "tekrar",
	}, nil)
	if resp.StatusCode != http.StatusConflict {
		t.Errorf("ad çakışması 409 olmalıydı, %d", resp.StatusCode)
	}

	// Yürütülemeyen tool → 501 not_implemented.
	resp, raw = do(t, s, http.MethodPost, "/api/tools/"+custom.ID+"/execute", map[string]any{}, nil)
	if resp.StatusCode != http.StatusNotImplemented || errBody(t, raw).Code != protocol.ErrCodeNotImplemented {
		t.Errorf("yürütülemeyen tool: %d %s", resp.StatusCode, raw)
	}

	// Geçersiz tanım → 400 validation_error (alan hatalarıyla).
	resp, raw = do(t, s, http.MethodPost, "/api/tools", map[string]any{
		"name": "Hatali Ad", "description": "", "risk": "yok",
	}, nil)
	eb := errBody(t, raw)
	if resp.StatusCode != http.StatusBadRequest || eb.Code != protocol.ErrCodeValidation || len(eb.Fields) < 2 {
		t.Errorf("geçersiz tanım: %d %+v", resp.StatusCode, eb)
	}

	// Silme çalışır, builtin silinemez (409).
	resp, _ = do(t, s, http.MethodDelete, "/api/tools/"+custom.ID, nil, nil)
	if resp.StatusCode != http.StatusNoContent {
		t.Errorf("custom silinmeliydi, %d", resp.StatusCode)
	}
	builtinID := toolID(t, s, "fiko.echo")
	resp, raw = do(t, s, http.MethodDelete, "/api/tools/"+builtinID, nil, nil)
	if resp.StatusCode != http.StatusConflict {
		t.Errorf("builtin silinememeliydi: %d %s", resp.StatusCode, raw)
	}

	// Kapatılan tool çalıştırılamaz (409 conflict).
	resp, raw = do(t, s, http.MethodPut, "/api/tools/"+builtinID+"/status", map[string]any{"status": "disabled"}, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("kapatma: %d %s", resp.StatusCode, raw)
	}
	resp, raw = do(t, s, http.MethodPost, "/api/tools/"+builtinID+"/execute", map[string]any{}, nil)
	if resp.StatusCode != http.StatusConflict {
		t.Errorf("kapalı tool çalıştırılmamalıydı: %d %s", resp.StatusCode, raw)
	}

	// Canlı doğrulama uçsu 200 döner.
	resp, raw = do(t, s, http.MethodPost, "/api/tools/validate", map[string]any{
		"name": "catia.create_plane", "description": "geçerli",
	}, nil)
	if resp.StatusCode != http.StatusOK || !decode[toolValidateResponse](t, raw).Valid {
		t.Errorf("validate: %d %s", resp.StatusCode, raw)
	}
}

func TestToolsUnavailableWhenNotWired(t *testing.T) {
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	s := New(context.Background(), Config{Env: "test", Version: "test"}, Deps{
		Conversations: conversation.NewService(conversation.NewMemoryRepo()),
	}, log)
	resp, _ := do(t, s, http.MethodGet, "/api/tools", nil, nil)
	if resp.StatusCode != http.StatusServiceUnavailable {
		t.Errorf("bağlanmamış tool sistemi 503 vermeliydi, %d", resp.StatusCode)
	}
}

func TestPermissionEndpoints(t *testing.T) {
	s, _ := toolServer(t, nil)

	resp, raw := do(t, s, http.MethodGet, "/api/permissions", nil, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("GET /api/permissions = %d (%s)", resp.StatusCode, raw)
	}
	got := decode[permissionsResponse](t, raw)
	if got.Scope != "default" || len(got.Policy.Grants) != 3 {
		t.Errorf("varsayılan politika read+write+execute olmalı (delete yok): %+v", got)
	}
	if len(got.Known) != 5 {
		t.Errorf("known izinler eksik: %v", got.Known)
	}

	// Bilinmeyen izin reddedilir.
	resp, _ = do(t, s, http.MethodPut, "/api/permissions", map[string]any{
		"grants": []string{"sudo"},
	}, nil)
	if resp.StatusCode != http.StatusBadRequest {
		t.Errorf("bilinmeyen izin reddedilmeliydi: %d", resp.StatusCode)
	}

	// İzin verilir ve kapsam "user" olur.
	resp, raw = do(t, s, http.MethodPut, "/api/permissions", map[string]any{
		"grants": []string{"execute", "delete"}, "allowHighRisk": true,
	}, nil)
	got = decode[permissionsResponse](t, raw)
	if got.Scope != "user" || len(got.Policy.Grants) != 2 || !got.Policy.AllowHighRisk {
		t.Errorf("politika uygulanmadı: %+v", got)
	}

	// İzin yalnızca BU kullanıcıya uygulanır; başka kimlik sınırlı kalır.
	resp, raw = do(t, s, http.MethodGet, "/api/tools", nil, map[string]string{"X-Fiko-User": "diger"})
	if resp.StatusCode != http.StatusOK {
		t.Fatal(string(raw))
	}
	for _, it := range decode[toolsResponse](t, raw).Items {
		if it.Name == "fs.delete" && it.Allowed {
			t.Error("başka kullanıcıya izin sızmamalı")
		}
	}
}

func TestExecuteSkillThroughTool(t *testing.T) {
	s, _ := toolServer(t, nil)
	sk := mustCreateSkill(t, s, map[string]any{
		"name":           "echo_test",
		"description":    "Tool üzerinden çalışır.",
		"status":         "active",
		"executorType":   "tool",
		"executorConfig": map[string]any{"tool": "fiko.echo"},
	})

	resp, raw := do(t, s, http.MethodPost, "/api/skills/"+sk.ID+"/execute",
		map[string]any{"arguments": map[string]any{"message": "çalıştı"}}, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("skill.execute = %d (%s)", resp.StatusCode, raw)
	}
	res := decode[skill.ExecutionResult](t, raw)
	if res.Tool != "fiko.echo" || res.Output["message"] != "çalıştı" {
		t.Errorf("sonuç: %+v", res)
	}
}

func TestSkillWithUnknownToolRejected(t *testing.T) {
	s, _ := toolServer(t, nil)
	resp, raw := do(t, s, http.MethodPost, "/api/skills", map[string]any{
		"name":           "olmayan_tool",
		"description":    "x",
		"status":         "active",
		"executorType":   "tool",
		"executorConfig": map[string]any{"tool": "catia.yok"},
	}, nil)
	eb := errBody(t, raw)
	if resp.StatusCode != http.StatusBadRequest || eb.Code != protocol.ErrCodeValidation {
		t.Fatalf("bilinmeyen tool reddedilmeliydi: %d %s", resp.StatusCode, raw)
	}
	if len(eb.Fields) == 0 || eb.Fields[0].Field != "executorConfig" {
		t.Errorf("executorConfig alanı işaretlenmeliydi: %+v", eb)
	}
}

// toolID adıyla tool kimliğini bulur.
func toolID(t *testing.T, s *Server, name string) string {
	t.Helper()
	resp, raw := do(t, s, http.MethodGet, "/api/tools", nil, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("GET /api/tools = %d (%s)", resp.StatusCode, raw)
	}
	for _, it := range decode[toolsResponse](t, raw).Items {
		if it.Name == name {
			return it.ID
		}
	}
	t.Fatalf("tool bulunamadı: %s", name)
	return ""
}
