package tool

import (
	"context"
	"errors"
	"strings"
	"sync"
	"testing"
	"time"

	"fiko-ai/internal/audit"
)

// --- test yardımcıları ---

// fakeRecorder denetim kayıtlarını toplar (dosya yazmadan kayıt akışı sınanır).
type fakeRecorder struct {
	mu      sync.Mutex
	entries []audit.Entry
}

func (f *fakeRecorder) Record(e audit.Entry) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.entries = append(f.entries, e)
}

func (f *fakeRecorder) last() audit.Entry {
	f.mu.Lock()
	defer f.mu.Unlock()
	if len(f.entries) == 0 {
		return audit.Entry{}
	}
	return f.entries[len(f.entries)-1]
}

func (f *fakeRecorder) count() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return len(f.entries)
}

// testRegistry bellek içi depo kuran Registry'dir (bus yok → olay yayınlanmaz).
func testRegistry(t *testing.T) *Registry {
	t.Helper()
	return NewRegistry(NewMemoryRepo(), nil)
}

// boundTool tool'u kaydeder, handler'ı bağlar ve kaydı döndürür.
func boundTool(t *testing.T, reg *Registry, def Tool, h Handler) *Tool {
	t.Helper()
	if _, _, err := reg.EnsureBuiltin(context.Background(), def, h, true); err != nil {
		t.Fatalf("EnsureBuiltin: %v", err)
	}
	got, err := reg.GetByName(context.Background(), def.Name)
	if err != nil {
		t.Fatalf("GetByName: %v", err)
	}
	return got
}

func mustID(t *testing.T, reg *Registry, name string) string {
	t.Helper()
	x, err := reg.GetByName(context.Background(), name)
	if err != nil {
		t.Fatalf("GetByName(%s): %v", name, err)
	}
	return x.ID
}

func echoDef(name string) Tool {
	return Tool{
		Name:        name,
		Description: "Test tool",
		Permissions: []string{"execute"},
		Risk:        RiskLow,
		InputSchema: map[string]any{
			"type": "object",
			"properties": map[string]any{
				"message": map[string]any{"type": "string", "default": "varsayılan"},
			},
		},
		TimeoutMs: 1_000,
	}
}

// ---- Executor testleri ----

func TestExecuteSuccessAppliesDefaults(t *testing.T) {
	reg := testRegistry(t)
	var got map[string]any
	boundTool(t, reg, echoDef("t.echo"), func(_ context.Context, _ *Tool, in map[string]any) (map[string]any, error) {
		got = in
		return map[string]any{"ok": true}, nil
	})
	rec := &fakeRecorder{}
	ex := NewExecutor(reg, nil, rec, nil, ExecutorOptions{})

	res, err := ex.Execute(context.Background(), Request{Ref: "t.echo", Input: map[string]any{}})
	if err != nil {
		t.Fatal(err)
	}
	if got["message"] != "varsayılan" {
		t.Errorf("şema varsayılanı uygulanmadı: %v", got)
	}
	if res.Attempts != 1 || res.ToolName != "t.echo" {
		t.Errorf("sonuç beklenmedik: %+v", res)
	}
	if e := rec.last(); e.Action != "tool.execute" || e.Result != audit.ResultSuccess || e.Target != "t.echo" {
		t.Errorf("denetim kaydı beklenmedik: %+v", e)
	}
}

func TestExecuteChainOrder(t *testing.T) {
	// Sıra doğrulanır: yetkisiz kullanıcı HİÇBİR ZAMAN handler'a ulaşmamalı,
	// kapalı tool çalıştırılmamalı, bilinmeyen tool bulunamadık demeli.
	reg := testRegistry(t)
	calls := 0
	boundTool(t, reg, echoDef("t.closed"), func(context.Context, *Tool, map[string]any) (map[string]any, error) {
		calls++
		return nil, nil
	})
	if _, err := reg.SetStatus(context.Background(), mustID(t, reg, "t.closed"), StatusDisabled); err != nil {
		t.Fatal(err)
	}
	def2 := echoDef("t.write")
	// delete isteyen tool: varsayılan politika geri alınamaz işlemi reddeder.
	// (write artık varsayılan olarak VERİLİR; canlı hata gereği şirket tool'ları
	//  aksi hâlde modele hiç sunulmuyordu. Reddedilmeli olan write DEĞİL, delete'tir.)
	def2.Permissions = []string{"delete"}
	boundTool(t, reg, def2, func(context.Context, *Tool, map[string]any) (map[string]any, error) {
		calls++
		return nil, nil
	})
	rec := &fakeRecorder{}
	ex := NewExecutor(reg, NewGrantStore(DefaultPolicy()), rec, nil, ExecutorOptions{})

	if _, err := ex.Execute(context.Background(), Request{Ref: "t.echo_missing"}); !errors.Is(err, ErrNotFound) {
		t.Errorf("bilinmeyen tool: %v, ErrNotFound bekleniyordu", err)
	}
	if _, err := ex.Execute(context.Background(), Request{Ref: "t.closed"}); !errors.Is(err, ErrDisabled) {
		t.Errorf("kapalı tool: %v, ErrDisabled bekleniyordu", err)
	}
	if _, err := ex.Execute(context.Background(), Request{Ref: "t.write"}); !errors.Is(err, ErrPermissionDenied) {
		t.Errorf("delete izni olmadan: %v, PermissionDenied bekleniyordu", err)
	}
	if calls != 0 {
		t.Errorf("hiçbir handler çalışmamalıydı, %d kez çalıştı", calls)
	}
	// Her reddedilen deneme denetim kaydına girmeli (denied/error ayrımı ile).
	if rec.count() != 3 {
		t.Errorf("3 denetim kaydı bekleniyordu, %d var: %+v", rec.count(), rec.entries)
	}
	if last := rec.last(); last.Result != audit.ResultDenied {
		t.Errorf("son kayıt 'denied' olmalıydı: %+v", last)
	}
}

func TestExecuteInputValidation(t *testing.T) {
	reg := testRegistry(t)
	boundTool(t, reg, echoDef("t.strict"), func(context.Context, *Tool, map[string]any) (map[string]any, error) {
		t.Error("geçersiz girdide handler çalışmamalı")
		return nil, nil
	})
	def := Tool{
		Name:        "t.strict",
		Description: "Test",
		Risk:        RiskLow,
		Permissions: []string{"execute"},
		InputSchema: map[string]any{
			"type":       "object",
			"properties": map[string]any{"offset": map[string]any{"type": "number"}},
			"required":   []any{"offset"},
		},
	}
	if _, err := reg.Update(context.Background(), mustID(t, reg, "t.strict"), def); err != nil {
		t.Fatal(err)
	}
	ex := NewExecutor(reg, nil, nil, nil, ExecutorOptions{})
	_, err := ex.Execute(context.Background(), Request{Ref: "t.strict", Input: map[string]any{"offset": "abc"}})
	var ie *InputError
	if !errors.As(err, &ie) {
		t.Fatalf("InputError bekleniyordu, %v", err)
	}
	if len(ie.Errors) == 0 || ie.Errors[0].Field != "offset" {
		t.Errorf("alan bazlı hata bekleniyordu: %+v", ie.Errors)
	}
}

func TestExecuteTimeout(t *testing.T) {
	reg := testRegistry(t)
	def := echoDef("t.slow")
	def.TimeoutMs = 50
	boundTool(t, reg, def, func(ctx context.Context, _ *Tool, _ map[string]any) (map[string]any, error) {
		select {
		case <-time.After(5 * time.Second):
			return nil, nil
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	})
	ex := NewExecutor(reg, nil, nil, nil, ExecutorOptions{})

	start := time.Now()
	_, err := ex.Execute(context.Background(), Request{Ref: "t.slow"})
	if !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("DeadlineExceeded bekleniyordu, %v", err)
	}
	if d := time.Since(start); d > 2*time.Second {
		t.Errorf("zaman aşımı uygulanmadı: %v", d)
	}

	// Çağıran iptali de aynı şekilde davranmalı.
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
	defer cancel()
	if _, err := ex.Execute(ctx, Request{Ref: "t.slow"}); !errors.Is(err, context.DeadlineExceeded) {
		t.Errorf("iptal: %v, DeadlineExceeded bekleniyordu", err)
	}
}

func TestExecuteRetryOnlyWhenRetryable(t *testing.T) {
	reg := testRegistry(t)
	var mu sync.Mutex
	attempts := 0

	def := echoDef("t.flaky")
	def.Retry = Retry{MaxAttempts: 3}
	boundTool(t, reg, def, func(context.Context, *Tool, map[string]any) (map[string]any, error) {
		mu.Lock()
		defer mu.Unlock()
		attempts++
		if attempts < 4 {
			return nil, &FailedError{Tool: "t.flaky", Reason: "ağ koptu", err: Temporary(errors.New("timeout"))}
		}
		return map[string]any{"ok": true}, nil
	})
	ex := NewExecutor(reg, nil, nil, nil, ExecutorOptions{})

	res, err := ex.Execute(context.Background(), Request{Ref: "t.flaky"})
	if err != nil {
		t.Fatal(err)
	}
	if res.Attempts != 4 {
		t.Errorf("4 deneme bekleniyordu, %d oldu", res.Attempts)
	}

	mu.Lock()
	attempts = 0
	mu.Unlock()
	def2 := echoDef("t.permanent")
	def2.Retry = Retry{MaxAttempts: 3}
	boundTool(t, reg, def2, func(context.Context, *Tool, map[string]any) (map[string]any, error) {
		mu.Lock()
		defer mu.Unlock()
		attempts++
		return nil, &FailedError{Tool: "t.permanent", Reason: "dosya yok"}
	})
	if _, err := ex.Execute(context.Background(), Request{Ref: "t.permanent"}); err == nil {
		t.Fatal("hata bekleniyordu")
	}
	mu.Lock()
	defer mu.Unlock()
	if attempts != 1 {
		t.Errorf("kalıcı hata tekrar edilmemeliydi, %d kez denendi", attempts)
	}
}

func TestExecuteRecoversPanic(t *testing.T) {
	reg := testRegistry(t)
	boundTool(t, reg, echoDef("t.panic"), func(context.Context, *Tool, map[string]any) (map[string]any, error) {
		panic("kod hatası")
	})
	ex := NewExecutor(reg, nil, nil, nil, ExecutorOptions{})
	_, err := ex.Execute(context.Background(), Request{Ref: "t.panic"})
	var fe *FailedError
	if !errors.As(err, &fe) {
		t.Fatalf("panic hata olarak sarmalanmalıydı, %v", err)
	}
	if !strings.Contains(err.Error(), "panik") {
		t.Errorf("panik bilgisi kayboldu: %v", err)
	}
}

func TestExecuteDoesNotMutateCallerInput(t *testing.T) {
	reg := testRegistry(t)
	boundTool(t, reg, echoDef("t.iso"), func(_ context.Context, _ *Tool, in map[string]any) (map[string]any, error) {
		in["message"] = "MUTASYON"
		return map[string]any{"ok": 1}, nil
	})
	ex := NewExecutor(reg, nil, nil, nil, ExecutorOptions{})
	var steps []string
	in := map[string]any{"message": "orijinal"}
	if _, err := ex.Execute(context.Background(), Request{
		Ref: "t.iso", Input: in, Progress: func(s string, _ int) { steps = append(steps, s) },
	}); err != nil {
		t.Fatal(err)
	}
	if in["message"] != "orijinal" {
		t.Errorf("çağıranın girdisi mutasyona uğradı: %v", in)
	}
	if len(steps) == 0 {
		t.Error("ilerleme bildirimi bekleniyordu")
	}
}

// TestExecuteInjectsUserContext, çalıştırma bağlamının tool Config'ine yazıldığını kanıtlar.
//
// Canlı regresyon: bellek tool'ları (memory.remember/update/recall/ask) model tarafından
// çağrılıyordu ve tool.start OLAYLARI GÖRÜLÜYORDU, ama hiçbiri kullanıcının belleğine
// ulaşamıyordu. Sebep: handler imzası (ctx, tool, input) olduğu halde kullanıcı
// kimliği yalnızca Request.UserID'de taşınıyordu; executor bunu tool'a taşımıyordu.
// Köprü olmadan HİÇBİR tool "kimin belleği?" sorusunu cevaplayamaz.
func TestExecuteInjectsUserContext(t *testing.T) {
	reg := testRegistry(t)

	var seenUser, seenConv string
	var configWasNil bool
	boundTool(t, reg, echoDef("t.ctx"), func(_ context.Context, tl *Tool, _ map[string]any) (map[string]any, error) {
		configWasNil = tl.Config == nil
		seenUser, _ = tl.Config[ConfigUserID].(string)
		seenConv, _ = tl.Config[ConfigConversationID].(string)
		return map[string]any{"ok": true}, nil
	})

	ex := NewExecutor(reg, nil, nil, nil, ExecutorOptions{})
	_, err := ex.Execute(context.Background(), Request{
		Ref:            "t.ctx",
		UserID:         "u-42",
		ConversationID: "c-7",
		Input:          map[string]any{},
	})
	if err != nil {
		t.Fatalf("Execute: %v", err)
	}
	if configWasNil {
		t.Fatal("handler Config'i nil gördü: bağlam yazılamaz")
	}
	if seenUser != "u-42" {
		t.Errorf("kullanıcı kimliği aktarılmadı: %q", seenUser)
	}
	if seenConv != "c-7" {
		t.Errorf("sohbet kimliği aktarılmadı: %q", seenConv)
	}
}

// TestExecuteDoesNotLeakContextIntoRegistry, köprünün kayıt defterini kirletmediğini
// kanıtlar: bağlam ÇALIŞTIRMA kopyasına yazılır, kalıcı tool tanımına DEĞİL.
// Aksi hâlde "local" kullanıcısının kimliği kayıtta kalır ve sonraki her çalıştırma
// yanlış kullanıcının belleğini okur (çok kullanıcılı sessiz veri sızıntısı).
func TestExecuteDoesNotLeakContextIntoRegistry(t *testing.T) {
	reg := testRegistry(t)
	boundTool(t, reg, echoDef("t.leak"), func(context.Context, *Tool, map[string]any) (map[string]any, error) {
		return map[string]any{"ok": true}, nil
	})

	ex := NewExecutor(reg, nil, nil, nil, ExecutorOptions{})
	if _, err := ex.Execute(context.Background(), Request{
		Ref: "t.leak", UserID: "u-42", Input: map[string]any{},
	}); err != nil {
		t.Fatalf("Execute: %v", err)
	}

	stored, err := reg.GetByName(context.Background(), "t.leak")
	if err != nil {
		t.Fatalf("GetByName: %v", err)
	}
	if _, leaked := stored.Config[ConfigUserID]; leaked {
		t.Errorf("kullanıcı kimliği kayıt defterine sızdı: %v", stored.Config)
	}
}

// TestExecuteInjectsIntoToolWithoutConfig, Config tanımı olmayan bir tool'un da bağlam
// alabildiğini kanıtlar (nil haritaya yazmak paniklerdi).
func TestExecuteInjectsIntoToolWithoutConfig(t *testing.T) {
	reg := testRegistry(t)
	def := Tool{
		Name:        "t.noconfig",
		Description: "Config tanımı yok",
		Permissions: []string{"execute"},
		Risk:        RiskLow,
		TimeoutMs:   1_000,
	}

	var seen string
	boundTool(t, reg, def, func(_ context.Context, tl *Tool, _ map[string]any) (map[string]any, error) {
		seen, _ = tl.Config[ConfigUserID].(string)
		return map[string]any{}, nil
	})

	ex := NewExecutor(reg, nil, nil, nil, ExecutorOptions{})
	if _, err := ex.Execute(context.Background(), Request{
		Ref: "t.noconfig", UserID: "u-9", Input: map[string]any{},
	}); err != nil {
		t.Fatalf("Execute: %v", err)
	}
	if seen != "u-9" {
		t.Error("Config tanımı yokken bağlam aktarılmadı (nil harita paniği)")
	}
}
