package agent

import (
	"strings"
	"testing"

	"fiko-ai/internal/llm"
)

var testNames = map[string]bool{"memory.ask": true, "memory.remember": true, "fiko.echo": true}

func TestParseTextToolCall(t *testing.T) {
	cases := []struct {
		name     string
		in       string
		wantOK   bool
		wantTool string
	}{
		{"düz biçim", `memory.ask {"question":"Emin misin?","content":"Deniz İzmir'de yaşıyor."}`, true, "memory.ask"},
		{"parantezli", `memory.remember({"content":"Kullanıcı Angular kullanıyor."})`, true, "memory.remember"},
		{"geçmiş oku biçimi", `→ fiko.echo({"text":"merhaba"})`, true, "fiko.echo"},
		{"json zarf", `{"name":"memory.remember","arguments":{"content":"Kullanıcı Python kullanıyor."}}`, true, "memory.remember"},
		{"json zarf parameters", `{"name":"fiko.echo","parameters":{"text":"x"}}`, true, "fiko.echo"},
		{"tool_call etiketi", "<tool_call>\n{\"name\":\"fiko.echo\",\"arguments\":{\"text\":\"x\"}}\n</tool_call>", true, "fiko.echo"},
		{"kod çiti", "```json\n{\"name\":\"fiko.echo\",\"arguments\":{\"text\":\"x\"}}\n```", true, "fiko.echo"},
		{"argüman JSON metni", `{"name":"fiko.echo","arguments":"{\"text\":\"x\"}"}`, true, "fiko.echo"},
		{"bilinmeyen tool", `sys.exec {"cmd":"dir"}`, false, ""},
		{"çağrı + serbest metin", `Tamam kaydediyorum. memory.remember {"content":"Kullanıcı Go kullanıyor."}`, false, ""},
		{"çağrı + sonra metin", `memory.remember {"content":"Kullanıcı Go kullanıyor."} Kaydettim.`, false, ""},
		{"bozuk json", `memory.ask {"question": }`, false, ""},
		{"sıradan cümle", `memory.ask nedir?`, false, ""},
		{"boş", `   `, false, ""},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			call, ok := parseTextToolCall(c.in, testNames)
			if ok != c.wantOK {
				t.Fatalf("ok=%v, beklenen %v (çağrı=%+v)", ok, c.wantOK, call)
			}
			if ok && call.Name != c.wantTool {
				t.Fatalf("tool=%q, beklenen %q", call.Name, c.wantTool)
			}
			if ok && call.Arguments == nil {
				t.Fatal("argümanlar nil olmamalı")
			}
		})
	}
}

func TestParseTextToolCallKeepsArguments(t *testing.T) {
	call, ok := parseTextToolCall(`memory.ask {"question":"Doğru mu?","content":"Deniz İzmir'de yaşıyor.","options":[{"text":"Evet","confirmed":true}]}`, testNames)
	if !ok {
		t.Fatal("çağrı bulunamadı")
	}
	if call.Arguments["question"] != "Doğru mu?" {
		t.Fatalf("question=%v", call.Arguments["question"])
	}
	opts, _ := call.Arguments["options"].([]any)
	if len(opts) != 1 {
		t.Fatalf("options=%v", call.Arguments["options"])
	}
}

// feed, parçaları sniffer'a verir ve kullanıcıya akan metni toplar.
func feed(t *testing.T, chunks []string, res llm.Result) (string, llm.Result, bool) {
	t.Helper()
	var shown strings.Builder
	sn := newCallSniffer(testNames, func(d string) error { shown.WriteString(d); return nil })
	for _, c := range chunks {
		if err := sn.Delta(c); err != nil {
			t.Fatal(err)
		}
	}
	res.Content = strings.Join(chunks, "")
	conv, err := sn.Finish(&res)
	if err != nil {
		t.Fatal(err)
	}
	return shown.String(), res, conv
}

func TestSnifferConvertsTextCallAndHidesIt(t *testing.T) {
	shown, res, conv := feed(t, []string{"memory", ".ask ", `{"question":"Emin misin?",`, `"content":"Deniz İzmir'de yaşıyor."}`}, llm.Result{})
	if !conv {
		t.Fatal("çağrıya çevrilmeliydi")
	}
	if shown != "" {
		t.Fatalf("ham çağrı metni kullanıcıya akmamalı: %q", shown)
	}
	if len(res.ToolCalls) != 1 || res.ToolCalls[0].Name != "memory.ask" || res.Content != "" {
		t.Fatalf("sonuç: %+v", res)
	}
}

func TestSnifferPassesNormalTextImmediately(t *testing.T) {
	var shown strings.Builder
	sn := newCallSniffer(testNames, func(d string) error { shown.WriteString(d); return nil })
	_ = sn.Delta("Merhaba")
	if shown.String() != "Merhaba" {
		t.Fatalf("sıradan metin ANINDA akmalı: %q", shown.String())
	}
	_ = sn.Delta(" nasılsın?")
	if shown.String() != "Merhaba nasılsın?" {
		t.Fatalf("akış: %q", shown.String())
	}
}

func TestSnifferLooksLikeToolNameButIsProse(t *testing.T) {
	// "memory" bir tool adının başı; ama devamı sıradan cümle -> metin eksiksiz akmalı.
	shown, res, conv := feed(t, []string{"memory", " kartı nedir? Açıklayayım."}, llm.Result{})
	if conv || len(res.ToolCalls) != 0 {
		t.Fatalf("çağrı değil: %+v", res)
	}
	if shown != "memory kartı nedir? Açıklayayım." {
		t.Fatalf("metin kayboldu/bozuldu: %q", shown)
	}
}

func TestSnifferHeldTextFlushedWhenNotACall(t *testing.T) {
	// Çağrı gibi başlıyor ama bozuk: metin kaybolmamalı.
	shown, res, conv := feed(t, []string{`memory.ask {"question": `}, llm.Result{})
	if conv || len(res.ToolCalls) != 0 {
		t.Fatal("bozuk çağrı çevrilmemeli")
	}
	if shown != `memory.ask {"question": ` {
		t.Fatalf("tutulan metin akıtılmalı: %q", shown)
	}
}

func TestSnifferKeepsRealToolCalls(t *testing.T) {
	// Sağlayıcı yapısal çağrı verdiyse (metin de varsa) metin akar, çağrı korunur.
	real := llm.Result{ToolCalls: []llm.ToolCall{{Name: "fiko.echo", Arguments: map[string]any{"text": "x"}}}}
	shown, res, conv := feed(t, []string{`{"not":"a call"}`}, real)
	if conv {
		t.Fatal("yapısal çağrı varken dönüşüm yapılmamalı")
	}
	if len(res.ToolCalls) != 1 || res.ToolCalls[0].Name != "fiko.echo" {
		t.Fatalf("yapısal çağrı bozuldu: %+v", res.ToolCalls)
	}
	if shown != `{"not":"a call"}` {
		t.Fatalf("metin akmalı: %q", shown)
	}
}

func TestSnifferNilOutput(t *testing.T) {
	sn := newCallSniffer(testNames, nil)
	if err := sn.Delta("Merhaba"); err != nil {
		t.Fatal(err)
	}
	var res llm.Result
	res.Content = "Merhaba"
	if _, err := sn.Finish(&res); err != nil {
		t.Fatal(err)
	}
}
