package agent

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"strings"
	"sync"
	"testing"

	"fiko-ai/internal/conversation"
	"fiko-ai/internal/llm"
	"fiko-ai/internal/protocol"
)

// ---- tool döngüsü sahteleri ----

// toolLLM hem Provider hem ToolProvider'dır: sırayla tool çağrısı ve cevap döner.
type toolLLM struct {
	mu    sync.Mutex
	turns [][]llm.ToolCall // her turda dönen çağrılar
	calls int
	msgs  [][]llm.Message // her turda gönderilen mesajlar
	// models her turda gönderilen model adıdır (model seçimi regresyon testleri için).
	models []string
	specs  [][]llm.ToolSpec
	final  string
}

// sentModels her turda gönderilen model adlarını döndürür.
func (f *toolLLM) sentModels() []string {
	f.mu.Lock()
	defer f.mu.Unlock()
	return append([]string(nil), f.models...)
}

func (f *toolLLM) Name() string { return "tool-llm" }
func (f *toolLLM) Generate(context.Context, llm.Request) (llm.Result, error) {
	return llm.Result{}, errors.New("kullanılmıyor")
}
func (f *toolLLM) ListModels(context.Context) ([]llm.ModelInfo, error) { return nil, nil }

func (f *toolLLM) GenerateStream(ctx context.Context, req llm.Request, on llm.DeltaFunc) (llm.Result, error) {
	return f.GenerateStreamWithTools(ctx, req, nil, on)
}

func (f *toolLLM) GenerateStreamWithTools(_ context.Context, req llm.Request, tools []llm.ToolSpec, on llm.DeltaFunc) (llm.Result, error) {
	f.mu.Lock()
	i := f.calls
	f.calls++
	f.specs = append(f.specs, tools)
	f.msgs = append(f.msgs, req.Messages)
	f.models = append(f.models, req.Model)
	var calls []llm.ToolCall
	if i < len(f.turns) {
		calls = f.turns[i]
	}
	final := f.final
	f.mu.Unlock()

	if on != nil {
		_ = on(final)
	}
	return llm.Result{Model: "m", Content: final, ToolCalls: calls}, nil
}

func (f *toolLLM) turnCount() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.calls
}

// fakeRunner tool çalıştırma sonucunu kaydeder.
type fakeRunner struct {
	mu      sync.Mutex
	calls   []string
	args    []map[string]any
	userIDs []string
	out     map[string]any
	err     error
}

func (f *fakeRunner) Run(_ context.Context, name string, args map[string]any, userID string, progress func(string, int)) (ToolResult, error) {
	f.mu.Lock()
	f.calls = append(f.calls, name)
	f.args = append(f.args, args)
	f.userIDs = append(f.userIDs, userID)
	f.mu.Unlock()
	if progress != nil {
		progress("adım", 50)
	}
	if f.err != nil {
		return ToolResult{}, f.err
	}
	return ToolResult{Output: f.out, DurationMs: 3}, nil
}

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

// fakeCatalog sabit bir tool listesi verir.
type fakeCatalog struct{ specs []llm.ToolSpec }

func (c fakeCatalog) Available(context.Context, string) []llm.ToolSpec { return c.specs }

func echoSpec() []llm.ToolSpec {
	return []llm.ToolSpec{{Name: "fiko.echo", Description: "yankı", InputSchema: map[string]any{"type": "object"}}}
}

func setupTools(t *testing.T, f *toolLLM, runner *fakeRunner, catalog ToolCatalog, cfg Config) (*Agent, string, *recorder) {
	t.Helper()
	svc := conversation.NewService(conversation.NewMemoryRepo())
	c, err := svc.Create(context.Background(), "u")
	if err != nil {
		t.Fatal(err)
	}
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	a := New(svc, f, nil, nil, log, cfg)
	// TEK kayıtçı: tool olayları da artık HandleText'in emit'inden çıkar
	// (her turun emit'i kendi oturumundadır — agent.emitFor).
	rec := &recorder{}
	a.SetTools(runner, catalog)
	return a, c.ID, rec
}

func sendText(a *Agent, rec *recorder, conv, text string) {
	a.HandleText(context.Background(), input(conv, text), rec.emit)
}

func TestToolLoop_ExecutesThenAnswers(t *testing.T) {
	llmFake := &toolLLM{
		turns: [][]llm.ToolCall{{{Name: "fiko.echo", Arguments: map[string]any{"message": "merhaba"}}}},
		final: "Tool çalıştı, cevabım hazır.",
	}
	runner := &fakeRunner{out: map[string]any{"message": "merhaba"}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{echoSpec()}, Config{})

	sendText(a, rec, conv, "selam")

	if llmFake.turnCount() != 2 {
		t.Fatalf("iki tur bekleniyordu: %d", llmFake.turnCount())
	}
	if runner.count() != 1 {
		t.Fatalf("tool bir kez çalışmalıydı: %d", runner.count())
	}
	if runner.args[0]["message"] != "merhaba" {
		t.Errorf("argüman aktarılmamış: %v", runner.args[0])
	}
	second := ""
	for _, m := range llmFake.msgs[1] {
		second += m.Content
	}
	if !strings.Contains(second, "fiko.echo") || !strings.Contains(second, "merhaba") {
		t.Errorf("tool sonucu geçmişe eklenmemiş: %v", second)
	}
	// Olay sırası: tool.start → tool.result → text.final
	types := rec.types()
	startIdx, resultIdx, finalIdx := -1, -1, -1
	for i, ty := range types {
		switch ty {
		case protocol.ServerToolStart:
			if startIdx < 0 {
				startIdx = i
			}
		case protocol.ServerToolResult:
			if resultIdx < 0 {
				resultIdx = i
			}
		case protocol.ServerAssistantTextFinal:
			finalIdx = i
		}
	}
	if startIdx < 0 || resultIdx < 0 || finalIdx < 0 {
		t.Fatalf("tool olayları eksik: %v", types)
	}
	if !(startIdx < resultIdx && resultIdx < finalIdx) {
		t.Errorf("olay sırası yanlış: %v", types)
	}
	p, _ := rec.find(protocol.ServerToolResult)
	payload, ok := p.Payload.(protocol.ToolResultPayload)
	if !ok || payload.Status != "success" || payload.Name != "fiko.echo" {
		t.Errorf("tool sonucu payload hatalı: %+v", p.Payload)
	}
}

// REGRESYON: tool döngüsü de ayardaki modeli kullanmalı ve HER TURDA AYNI modeli
// kullanmalıdır. Model üretimin başında BİR KEZ çözülür; ayar araya girerse bile
// turdan tura model değişmemelidir (aksi hâlde "ilk tur farklı model, ikinci tur
// başka model" gibi tutarsız ve pahalı davranış oluşur).
func TestToolLoop_UsesModelFromSettingsOnEveryTurn(t *testing.T) {
	llmFake := &toolLLM{
		turns: [][]llm.ToolCall{{{Name: "fiko.echo", Arguments: map[string]any{"message": "m"}}}},
		final: "bitti",
	}
	runner := &fakeRunner{out: map[string]any{"message": "m"}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{echoSpec()},
		Config{ModelFunc: func() string { return "qwen3:14b" }})

	sendText(a, rec, conv, "selam")

	models := llmFake.sentModels()
	if len(models) != 2 {
		t.Fatalf("iki tur bekleniyordu: %d", len(models))
	}
	for i, m := range models {
		if m != "qwen3:14b" {
			t.Errorf("%d. turda ayar modeli kullanılmalıydı, gönderilen: %q", i+1, m)
		}
	}
}

func TestToolLoop_NoToolsKeepsSingleTurn(t *testing.T) {
	// Tool BAĞLI DEĞİLKEN davranış değişmez: tek tur, tool olayı yok.
	llmFake := &toolLLM{final: "merhaba"}
	svc := conversation.NewService(conversation.NewMemoryRepo())
	c, _ := svc.Create(context.Background(), "u")
	a := New(svc, llmFake, nil, nil, slog.New(slog.NewTextHandler(io.Discard, nil)), Config{})
	rec := runText(a, c.ID, "selam")

	if llmFake.turnCount() != 1 {
		t.Errorf("tek tur bekleniyordu: %d", llmFake.turnCount())
	}
	for _, ty := range rec.types() {
		if ty == protocol.ServerToolStart || ty == protocol.ServerToolResult {
			t.Errorf("tool olayı üretilmemeliydi: %v", rec.types())
		}
	}
}

func TestToolLoop_EmptyCatalogIsSingleTurn(t *testing.T) {
	// Katalog boş (tool yok / hepsi kapalı): sunulan tanım yok, döngüye girilmez.
	llmFake := &toolLLM{turns: [][]llm.ToolCall{{{Name: "yok", Arguments: map[string]any{}}}}}
	runner := &fakeRunner{}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{}, Config{})
	sendText(a, rec, conv, "selam")

	if llmFake.turnCount() != 1 || runner.count() != 0 {
		t.Errorf("boş katalogda tek tur bekleniyordu: tur=%d tool=%d", llmFake.turnCount(), runner.count())
	}
}

func TestToolLoop_ErrorDoesNotBreakLoop(t *testing.T) {
	// Tool hatası döngüyü KIRMAMALI: model hatayı görüp ikinci turda yanıtlar.
	llmFake := &toolLLM{
		turns: [][]llm.ToolCall{{{Name: "fiko.echo", Arguments: map[string]any{}}}},
		final: "Tool çalışamadı, kusura bakma.",
	}
	runner := &fakeRunner{err: errors.New("yetkiniz yok")}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{echoSpec()}, Config{})

	sendText(a, rec, conv, "selam")

	if llmFake.turnCount() != 2 {
		t.Errorf("hata sonrası ikinci tur olmalıydı: %d", llmFake.turnCount())
	}
	p, _ := rec.find(protocol.ServerToolResult)
	payload := p.Payload.(protocol.ToolResultPayload)
	if payload.Status != "error" || !strings.Contains(payload.Error, "yetkiniz yok") {
		t.Errorf("hata tool.result'a yansımalıydı: %+v", payload)
	}
	joined := ""
	for _, m := range llmFake.msgs[1] {
		joined += m.Content
	}
	if !strings.Contains(joined, "HATASI") {
		t.Errorf("modele hata geri beslenmeliydi: %v", joined)
	}
}

func TestToolLoop_StopsAtMaxIterations(t *testing.T) {
	// Model HER TURDA aynı tool'ı aynı girdiyle istiyor: sonsuz döngüye girmemeli.
	turns := make([][]llm.ToolCall, 0, 6)
	for i := 0; i < 6; i++ {
		turns = append(turns, []llm.ToolCall{{Name: "fiko.echo", Arguments: map[string]any{"message": "aynı"}}})
	}
	llmFake := &toolLLM{turns: turns, final: "hâlâ çalışıyorum"}
	runner := &fakeRunner{out: map[string]any{"ok": true}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{echoSpec()}, Config{MaxIterations: 3})

	sendText(a, rec, conv, "çözdür")

	if llmFake.turnCount() != 3 {
		t.Errorf("3 tur bekleniyordu: %d", llmFake.turnCount())
	}
	fin, _ := rec.find(protocol.ServerAssistantTextFinal)
	if !strings.Contains(fin.Payload.(protocol.TextFinalPayload).Content, "döngü durduruldu") {
		t.Errorf("kullanıcıya döngü sınırı bildirilmeliydi: %+v", fin.Payload)
	}
}

func TestToolLoop_IdenticalCallIsNotReExecuted(t *testing.T) {
	// AYNI araç + AYNI girdi ikinci kez istenirse GERÇEKTEN çalıştırılmaz.
	// Gerekçe: aynı yazma/silme işlemini iki kez yapmak yan etkidir; ayrıca döngüyü uzatır.
	turns := make([][]llm.ToolCall, 0, 3)
	for i := 0; i < 3; i++ {
		turns = append(turns, []llm.ToolCall{{Name: "fiko.echo", Arguments: map[string]any{"message": "aynı"}}})
	}
	llmFake := &toolLLM{turns: turns, final: "tamam"}
	runner := &fakeRunner{out: map[string]any{"ok": true}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{echoSpec()}, Config{})

	sendText(a, rec, conv, "tekrar et")

	if runner.count() != 1 {
		t.Errorf("araç bir kez çalışmalıydı, %d kez çalıştı", runner.count())
	}
	if llmFake.turnCount() != 4 {
		// 3 tool turu + son turda (çağrısız) kapanış.
		t.Errorf("4 tur bekleniyordu: %d", llmFake.turnCount())
	}
	// Her çağrı için istemciye tool.result gider (bekleyen rozet kalmamalı).
	results := 0
	for _, e := range rec.all() {
		if e.Type == protocol.ServerToolResult {
			results++
		}
	}
	if results != 3 {
		t.Errorf("her çağrı için tool.result bekleniyordu: %d", results)
	}
	// Modele "zaten çalıştı" bilgisi verilmiş olmalı.
	last := ""
	for _, m := range llmFake.msgs[len(llmFake.msgs)-1] {
		last += m.Content
	}
	if !strings.Contains(last, "TEKRAR ÇAĞRI") {
		t.Errorf("tekrar modele bildirilmeliydi: %v", last)
	}
}

func TestToolLoop_SameToolDifferentArgsRunsAgain(t *testing.T) {
	// FARKLI girdi = farklı iş: tekrar koruması bunu ENGELLEMEMELİ.
	llmFake := &toolLLM{
		turns: [][]llm.ToolCall{
			{{Name: "fiko.echo", Arguments: map[string]any{"message": "bir"}}},
			{{Name: "fiko.echo", Arguments: map[string]any{"message": "iki"}}},
		},
		final: "ikisi de bitti",
	}
	runner := &fakeRunner{out: map[string]any{"ok": true}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{echoSpec()}, Config{})

	sendText(a, rec, conv, "iki kez çağır")

	if runner.count() != 2 {
		t.Errorf("farklı girdiler iki kez çalışmalıydı: %d", runner.count())
	}
}

func TestToolLoop_MaxIterationsIsCapped(t *testing.T) {
	// Yapılandırmada aşırı büyük değer verilse bile üst sınır uygulanır.
	llmFake := &toolLLM{}
	a, _, _ := setupTools(t, llmFake, &fakeRunner{}, fakeCatalog{echoSpec()}, Config{MaxIterations: 1000})
	if a.cfg.MaxIterations != MaxIterationsCap {
		t.Errorf("üst sınır uygulanmadı: %d", a.cfg.MaxIterations)
	}
}

func TestToolLoop_UsesUserIdentityAndProgress(t *testing.T) {
	llmFake := &toolLLM{turns: [][]llm.ToolCall{{{Name: "fiko.echo", Arguments: map[string]any{}}}}, final: "ok"}
	runner := &fakeRunner{out: map[string]any{"x": 1}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{echoSpec()}, Config{})

	sendText(a, rec, conv, "selam")

	if runner.userIDs[0] != "u" {
		t.Errorf("tool kullanıcı kimliğiyle çağrılmalıydı: %q", runner.userIDs[0])
	}
	found := false
	for _, e := range rec.all() {
		if e.Type == protocol.ServerToolProgress {
			found = true
		}
	}
	if !found {
		t.Error("tool.progress olayı yayınlanmadı")
	}
}

func TestToolLoop_CancelStopsBeforeTool(t *testing.T) {
	// Tool çağrısı geldikten sonra iptal edilirse ikinci tur yapılmaz.
	// Tool olayları da HandleText'in emit'inden çıktığı için iptal buraya bağlanabilir.
	llmFake := &toolLLM{turns: [][]llm.ToolCall{{{Name: "fiko.echo", Arguments: map[string]any{}}}}, final: "cevap"}
	runner := &fakeRunner{out: map[string]any{"ok": true}}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{echoSpec()}, Config{})

	a.HandleText(context.Background(), input(conv, "selam"), func(o protocol.Outbound) {
		rec.emit(o)
		if o.Type == protocol.ServerToolStart {
			a.Cancel(conv, "user") // araç çalışırken kullanıcı iptal etti
		}
	})

	if llmFake.turnCount() != 1 {
		t.Errorf("iptalden sonra ikinci tur yapılmamalıydı: %d", llmFake.turnCount())
	}
}

// ---- Faz 5b: kalıcı execution tracking ----

func savedMessages(t *testing.T, svc *conversation.Service, convID string) []conversation.Message {
	t.Helper()
	msgs, err := svc.Messages(context.Background(), "u", convID, 50)
	if err != nil {
		t.Fatal(err)
	}
	return msgs
}

func TestToolRun_PersistedWithAssistantMessage(t *testing.T) {
	// Yeniden bağlanan kullanıcı "bu cevapta ne çalıştı?" sorusunu sorabilmeli.
	llmFake := &toolLLM{
		turns: [][]llm.ToolCall{{{Name: "fiko.echo", Arguments: map[string]any{"message": "merhaba"}}}},
		final: "Cevap.",
	}
	runner := &fakeRunner{out: map[string]any{"message": "merhaba"}}
	svc := conversation.NewService(conversation.NewMemoryRepo())
	c, _ := svc.Create(context.Background(), "u")
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	a := New(svc, llmFake, nil, nil, log, Config{})
	a.SetTools(runner, fakeCatalog{echoSpec()})

	a.HandleText(context.Background(), input(c.ID, "selam"), func(protocol.Outbound) {})

	msgs := savedMessages(t, svc, c.ID)
	if len(msgs) != 2 {
		t.Fatalf("2 mesaj bekleniyordu: %d", len(msgs))
	}
	assistant := msgs[len(msgs)-1]
	if len(assistant.ToolRuns) != 1 {
		t.Fatalf("tool kaydı saklanmadı: %+v", assistant.ToolRuns)
	}
	run := assistant.ToolRuns[0]
	if run.Name != "fiko.echo" || run.Status != "success" {
		t.Errorf("kayıt hatalı: %+v", run)
	}
	if run.CallID == "" || run.Input["message"] != "merhaba" || run.Output["message"] != "merhaba" {
		t.Errorf("girdi/çıktı kaydedilmedi: %+v", run)
	}
	if run.DurationMs != 3 {
		t.Errorf("süre kaydedilmedi: %d", run.DurationMs)
	}
}

func TestToolRun_ErrorIsPersisted(t *testing.T) {
	// Hatalı çalıştırma da kayda geçer: "izin yok" bilgisi kaybolmamalı.
	llmFake := &toolLLM{
		turns: [][]llm.ToolCall{{{Name: "fs.delete", Arguments: map[string]any{}}}},
		final: "Silinemedi.",
	}
	runner := &fakeRunner{err: errors.New("yetkiniz yok")}
	svc := conversation.NewService(conversation.NewMemoryRepo())
	c, _ := svc.Create(context.Background(), "u")
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	a := New(svc, llmFake, nil, nil, log, Config{})
	a.SetTools(runner, fakeCatalog{echoSpec()})

	a.HandleText(context.Background(), input(c.ID, "sil"), func(protocol.Outbound) {})

	msgs := savedMessages(t, svc, c.ID)
	assistant := msgs[len(msgs)-1]
	if len(assistant.ToolRuns) != 1 || assistant.ToolRuns[0].Status != "error" {
		t.Fatalf("hata kaydı bekleniyordu: %+v", assistant.ToolRuns)
	}
	if !strings.Contains(assistant.ToolRuns[0].Error, "yetkiniz yok") {
		t.Errorf("hata metni kaydedilmedi: %+v", assistant.ToolRuns[0])
	}
}

func TestToolRun_NoToolNoRecord(t *testing.T) {
	// Tool çalışmadıysa alan boş kalır (eski mesajların şeması bozulmaz).
	llmFake := &toolLLM{final: "Sadece metin."}
	svc := conversation.NewService(conversation.NewMemoryRepo())
	c, _ := svc.Create(context.Background(), "u")
	log := slog.New(slog.NewTextHandler(io.Discard, nil))
	a := New(svc, llmFake, nil, nil, log, Config{})
	a.SetTools(&fakeRunner{}, fakeCatalog{echoSpec()})

	a.HandleText(context.Background(), input(c.ID, "merhaba"), func(protocol.Outbound) {})

	msgs := savedMessages(t, svc, c.ID)
	if got := msgs[len(msgs)-1].ToolRuns; len(got) != 0 {
		t.Errorf("tool çalışmadıysa kayıt olmamalı: %+v", got)
	}
}

func TestToolPreview_TruncatesBigOutput(t *testing.T) {
	// Dosya içeriği gibi büyük çıktı mesajı şişirmemeli.
	big := map[string]any{"content": strings.Repeat("x", ToolPreviewMax*2)}
	got := ToolPreview(big, ToolPreviewMax)
	if got["_truncated"] != true {
		t.Fatalf("büyük çıktı kısaltılmadı: %+v", got)
	}
	raw, err := json.Marshal(got)
	if err != nil {
		t.Fatal(err)
	}
	if len(raw) > ToolPreviewMax+200 {
		t.Errorf("kısaltma yetersiz: %d bayt", len(raw))
	}

	small := map[string]any{"ok": true}
	if got := ToolPreview(small, ToolPreviewMax); got["_truncated"] != nil {
		t.Errorf("küçük çıktı bozulmamalıydı: %+v", got)
	}
	if ToolPreview(nil, ToolPreviewMax) != nil {
		t.Error("boş girdi nil dönmeli")
	}
}

func TestRunState_CapsRecords(t *testing.T) {
	// Aşırı sayıda kayıt sınırı aşmamalı (istemci/monitör koruması).
	var st runState
	for i := 0; i < MaxToolRunsPerTurn+10; i++ {
		st.add(ToolRun{Name: "t", Status: "success"})
	}
	if len(st.runs) != MaxToolRunsPerTurn {
		t.Errorf("kayıt sınırı uygulanmadı: %d", len(st.runs))
	}
}
