package agent

// prefetch_test.go: döviz kurunun MODELDEN BAĞIMSIZ getirildiğini doğrular.
//
// CANLI HATA (04.10.2026): "dolar kuru ne" denince model aracı çağırmıyor, kullanıcıya
// kuru söyleyemiyordu. Artık sunucu web.exchange_rate'i önceden çalıştırıp sonucu modele
// veriyor; en küçük model bile doğru cevabı kurabiliyor.

import (
	"context"
	"strings"
	"testing"

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

// rateSpec, döviz aracının modele sunulan halidir.
func rateSpec() []llm.ToolSpec {
	return []llm.ToolSpec{{Name: "web.exchange_rate", Description: "TCMB günlük kuru"}}
}

// rateOut, TCMB'den gelen gerçekçi yanıt biçimidir (02.10.2026 USD satış 49,0582).
func rateOut() map[string]any {
	return map[string]any{"date": "2026-10-02", "source": "TCMB",
		"rates": []any{map[string]any{"code": "USD", "selling": 49.0582, "buying": 48.5512}}}
}

// joinMsgs, modele giden mesajları tek metne bağlar (testte "model ne gördü?" sorusu).
func joinMsgs(msgs []llm.Message) string {
	var b strings.Builder
	for _, m := range msgs {
		b.WriteString(m.Content)
		b.WriteString("\n")
	}
	return b.String()
}

// Kullanıcı "dolar kuru ne" dediğinde araç model çağrısı BEKLENMEDEN çalışır ve
// sonucu modele geri beslenir.
func TestPrefetch_RunsExchangeRateWithoutModelCall(t *testing.T) {
	llmFake := &toolLLM{final: "Dolar 49,0582 TL."}
	runner := &fakeRunner{out: rateOut()}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{rateSpec()}, Config{})

	sendText(a, rec, conv, "dolar kuru ne")

	if runner.count() != 1 {
		t.Fatalf("kur aracı model çağrısından bağımsız çalışmalıydı: %d", runner.count())
	}
	if runner.calls[0] != "web.exchange_rate" {
		t.Errorf("yanlış araç çalıştı: %q", runner.calls[0])
	}
	if got := runner.args[0]["currency"]; got != "USD" {
		t.Errorf("mesajdan para birimi çıkarılmalı: %v", runner.args[0])
	}

	// Modelin gördüğü geçmişte GERÇEK veri olmalı (sadece "araç çalıştı" bilgisi değil).
	seen := joinMsgs(llmFake.msgs[0])
	if !strings.Contains(seen, "49.0582") {
		t.Errorf("modele kur değeri geri beslenmeliydi:\n%s", seen)
	}
	if !strings.Contains(seen, "UYDURMA") {
		t.Errorf("modele 'kur uydurma' uyarısı verilmeliydi:\n%s", seen)
	}
	// Modelin kendisi tool çağırmadı: tek tur yeter.
	if llmFake.turnCount() != 1 {
		t.Errorf("tek tur bekleniyordu, tur sayısı: %d", llmFake.turnCount())
	}
}

// İlgisiz mesajda ön çalıştırma YAPILMAZ (gereksiz HTTP + gecikme).
func TestPrefetch_NotTriggeredForUnrelatedMessage(t *testing.T) {
	llmFake := &toolLLM{final: "merhaba"}
	runner := &fakeRunner{out: rateOut()}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{rateSpec()}, Config{})

	sendText(a, rec, conv, "bugün hava nasıl")

	if runner.count() != 0 {
		t.Errorf("ilgisiz mesajda araç çalıştırılmamalı: %v", runner.calls)
	}
}

// Araç kullanıcıya SUNULMUYORSA çalıştırılmaz: izin yoktur, sızma olur.
func TestPrefetch_NotRunWhenToolUnavailable(t *testing.T) {
	llmFake := &toolLLM{final: "kur 49,0582"}
	runner := &fakeRunner{out: rateOut()}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{nil}, Config{})

	sendText(a, rec, conv, "dolar kuru ne")

	if runner.count() != 0 {
		t.Errorf("sunulmayan araç çalıştırılmamalı: %v", runner.calls)
	}
}

// Araç hata verirse tur BOZULMAZ: kullanıcı yine cevap alır.
func TestPrefetch_ErrorDoesNotBreakTurn(t *testing.T) {
	llmFake := &toolLLM{final: "Kura ulaşamadım."}
	runner := &fakeRunner{err: context.DeadlineExceeded}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{rateSpec()}, Config{})

	sendText(a, rec, conv, "dolar kuru ne")

	if runner.count() != 1 {
		t.Fatalf("araç denenmeliydi: %d", runner.count())
	}
	if llmFake.turnCount() != 1 || llmFake.final == "" {
		t.Error("hata turu bozmamalı, model yine cevap vermeli")
	}
	// Hata modele BESLENMEZ (kullanıcı asıl sorusundan kopmaz).
	if seen := joinMsgs(llmFake.msgs[0]); strings.Contains(seen, "deadline") || strings.Contains(seen, "HATASI") {
		t.Errorf("ön çalıştırma hatası modele sızmamalı:\n%s", seen)
	}
}

// Tutarlı tetikleme: "500 dolar kaç tl" → currency + amount.
func TestExchangeRateArgs(t *testing.T) {
	cases := []struct {
		msg      string
		currency string
		amount   any
	}{
		{"dolar kuru ne", "USD", nil},
		{"Dolar KURU kaç", "USD", nil},
		{"euro ne kadar", "EUR", nil},
		{"500 dolar kaç tl", "USD", float64(500)},
		{"1.500 euro kaç", "EUR", float64(1500)},
		{"sterlin kuru", "GBP", nil},
	}
	for _, tc := range cases {
		got := exchangeRateArgs(tc.msg)
		if got == nil {
			t.Errorf("exchangeRateArgs(%q) = nil", tc.msg)
			continue
		}
		if got["currency"] != tc.currency {
			t.Errorf("exchangeRateArgs(%q) currency = %v, beklenen %v", tc.msg, got["currency"], tc.currency)
		}
		if tc.amount == nil {
			if _, ok := got["amount"]; ok {
				t.Errorf("exchangeRateArgs(%q) tutar olmadığı hâlde amount üretildi: %v", tc.msg, got)
			}
		} else if got["amount"] != tc.amount {
			t.Errorf("exchangeRateArgs(%q) amount = %v, beklenen %v", tc.msg, got["amount"], tc.amount)
		}
	}
	if got := exchangeRateArgs("bugün hava nasıl"); got != nil {
		t.Errorf("döviz sözcüğü yokken girdi üretilmemeli: %v", got)
	}
}

// Ön çalıştırma, model sonradan aynı aracı çağırırsa TEKRAR çalıştırmaz.
//
// Beklenen: GERÇEK çalıştırma sayısı 1. Ön çalıştırmada bir kez çalışır, modelin
// çağrısı aynı girdiyle geldiği için önbellekten döner (HTTP tekrarı olmaz).
func TestPrefetch_ModelCallReusesPrefetchedResult(t *testing.T) {
	llmFake := &toolLLM{
		turns: [][]llm.ToolCall{{{Name: "web.exchange_rate", Arguments: map[string]any{"currency": "USD"}}}},
		final: "Dolar 49,0582 TL.",
	}
	runner := &fakeRunner{out: rateOut()}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{rateSpec()}, Config{})

	sendText(a, rec, conv, "dolar kuru ne")

	if runner.count() != 1 {
		t.Errorf("GERÇEK çalıştırma sayısı = %d; ön çalıştırma + önbellek => 1 olmalı", runner.count())
	}
	if llmFake.turnCount() != 2 {
		t.Errorf("model turu = %d; ön çalıştırma sonrası model çağrısı bir tur harcamalı", llmFake.turnCount())
	}
	// Model ikinci turda sonucu ÖNBELLEKTEN alır: gerçek veri yine elinde olmalı.
	if seen := joinMsgs(llmFake.msgs[1]); !strings.Contains(seen, "49.0582") {
		t.Errorf("model ikinci turda kur değerini görmeliydi:\n%s", seen)
	}
}

// Kayıt ve olaylar: ön çalıştırma da "ne çalıştı?" sorusuna görünür olmalı.
func TestPrefetch_EmitsToolEvents(t *testing.T) {
	llmFake := &toolLLM{final: "49,0582"}
	runner := &fakeRunner{out: rateOut()}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{rateSpec()}, Config{})

	sendText(a, rec, conv, "dolar kuru ne")

	var start, result protocol.Outbound
	for _, o := range rec.all() {
		switch o.Type {
		case protocol.ServerToolStart:
			start = o
		case protocol.ServerToolResult:
			result = o
		}
	}
	if start.Type == "" {
		t.Error("ön çalıştırma için tool.start olayı gönderilmeli (arayüz rozeti)")
	}
	if result.Type == "" {
		t.Error("ön çalıştırma için tool.result olayı gönderilmeli")
	}
}

// Kalıcı kayıt: "hangi araçlar çalıştı?" sorusu ön çalıştırmayı da görmeli.
func TestPrefetch_RecordedAsToolRun(t *testing.T) {
	llmFake := &toolLLM{final: "49,0582"}
	runner := &fakeRunner{out: rateOut()}
	a, conv, rec := setupTools(t, llmFake, runner, fakeCatalog{rateSpec()}, Config{})

	sendText(a, rec, conv, "dolar kuru ne")

	var found bool
	svc := a.svc
	msgs, err := svc.Messages(context.Background(), "u", conv, 10)
	if err != nil {
		t.Fatalf("mesajlar okunamadı: %v", err)
	}
	for _, m := range msgs {
		if m.Role == "assistant" {
			for _, r := range m.ToolRuns {
				if r.Name == "web.exchange_rate" {
					found = true
				}
			}
		}
	}
	if !found {
		t.Error("ön çalıştırma kalıcı tool kaydına girmeli")
	}
}
