package builtin

// util_test.go yeni tool'ların (fs.list/search/stat, calc.eval, text.stats)
// davranışını ve GÜVENLİK sınırlarını sınar.
//
// En kritik olan TestCalcEvalRejectsCode'dır: calc.eval bir "ifade hesaplayıcı"
// gibi görünür ve insanı yanıltabilir; burada KOD ÇALIŞTIRILAMAZ olduğu sınanır.

import (
	"context"
	"fmt"
	"math"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"fiko-ai/internal/tool"
)

func TestListAndSearch(t *testing.T) {
	_, ex := testReg(t, Options{})

	run(t, ex, "fs.write_text", map[string]any{"path": "notlar/plan.txt", "content": "30 mm geriye"})
	run(t, ex, "fs.write_text", map[string]any{"path": "notlar/fiyat.txt", "content": "birim fiyat 25 TL"})

	// list: klasör içeriği (ad + tür)
	got := run(t, ex, "fs.list", map[string]any{"path": "notlar"})
	entries, _ := got["entries"].([]map[string]any)
	if len(entries) != 2 {
		t.Fatalf("2 dosya beklenir, %d geldi: %v", len(entries), got)
	}
	first := entries[0]
	if first["name"] != "fiyat.txt" || first["kind"] != "dosya" {
		t.Errorf("kayıt beklenenden farklı: %v", first)
	}
	if first["path"] != "notlar/fiyat.txt" {
		t.Errorf("yol köke göreli olmalı: %v", first["path"])
	}

	// search: ada göre (varsayılan)
	found := run(t, ex, "fs.search", map[string]any{"query": "fiyat"})
	matches, _ := found["matches"].([]map[string]any)
	if len(matches) != 1 {
		t.Fatalf("1 eşleşme beklenir: %v", found)
	}

	// search: içerik regex
	byContent := run(t, ex, "fs.search", map[string]any{
		"query":        `birim\s+fiyat`,
		"contentRegex": true,
	})
	m2, _ := byContent["matches"].([]map[string]any)
	if len(m2) != 1 {
		t.Fatalf("içerik araması 1 sonuç vermeli: %v", byContent)
	}
	row := m2[0]
	if ln, _ := toFloat(row["line"]); ln != 1 {
		t.Errorf("satır numarası 1 olmalı: %v", row)
	}
	if s, _ := row["match"].(string); !strings.Contains(s, "25 TL") {
		t.Errorf("eşleşen satır içeriği eksik: %v", row)
	}
}

func TestUtilToolsSandbox(t *testing.T) {
	root := t.TempDir()
	_, ex := testReg(t, Options{FSRoot: root})
	outside := filepath.Join(filepath.Dir(root), "gizli.txt")
	if err := os.WriteFile(outside, []byte("GİZLİ"), 0o600); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = os.Remove(outside) })

	// Hiçbir yeni tool kök dışına çıkamaz.
	for _, tc := range []struct {
		ref string
		in  map[string]any
	}{
		{"fs.list", map[string]any{"path": ".."}},
		{"fs.search", map[string]any{"query": "x", "path": "../.."}},
		{"fs.stat", map[string]any{"path": "../gizli.txt"}},
		{"fs.stat", map[string]any{"path": outside}},
	} {
		if _, err := ex.Execute(context.Background(), tool.Request{
			Ref: tc.ref, Input: tc.in, Actor: "user",
		}); err == nil {
			t.Errorf("%s kök dışı yolu reddetmeliydi: %v", tc.ref, tc.in)
		}
	}
	if _, err := os.Stat(outside); err != nil {
		t.Errorf("dış dosya etkilenmiş: %v", err)
	}
}

func TestStatReportsMissing(t *testing.T) {
	_, ex := testReg(t, Options{})
	// Var olmayan yol HATA değildir: exists=false döner.
	if got := run(t, ex, "fs.stat", map[string]any{"path": "yok.txt"}); got["exists"] != false {
		t.Errorf("olmayan dosya exists=false vermeli: %v", got)
	}
	run(t, ex, "fs.write_text", map[string]any{"path": "var.txt", "content": "abc"})
	if got := run(t, ex, "fs.stat", map[string]any{"path": "var.txt"}); got["exists"] != true {
		t.Errorf("stat: %v", got)
	} else if size, _ := toFloat(got["size"]); size != 3 {
		t.Errorf("stat boyutu 3 olmalı: %v", size)
	}
}
func TestCalcEval(t *testing.T) {
	_, ex := testReg(t, Options{})
	cases := []struct {
		expr string
		want float64
	}{
		{"2+3*4", 14},     // öncelik
		{"(2+3)*4", 20},   // parantez
		{"10/4", 2.5},     // bölme
		{"2^3^2", 512},    // sağa bağlı üstel
		{"-5+3", -2},      // tek eksi
		{"sqrt(16)", 4},   // fonksiyon
		{"min(3,1,2)", 1}, // çok argümanlı
		{"max(3,9,2)", 9},
		{"ceil(4.1)", 5},
		{"0.1+0.2", 0.3}, // kayan nokta artığı yuvarlanır
		{"round(2.5)", 3},
		{"10%3", 1}, // mod
	}
	for _, c := range cases {
		got := run(t, ex, "calc.eval", map[string]any{"expression": c.expr})
		if r, _ := toFloat(got["result"]); r != c.want {
			t.Errorf("%s = %v, beklenen %v", c.expr, got["result"], c.want)
		}
	}
	got := run(t, ex, "calc.eval", map[string]any{
		"expression": "adet*fiyat", "variables": map[string]any{"adet": float64(3), "fiyat": float64(25)},
	})
	if r, _ := toFloat(got["result"]); r != 75 {
		t.Errorf("değişkenli hesap: %v", got)
	}
}

// TestCalcEvalRejectsCode: hesaplayıcı KOD ÇALIŞTIRMAZ.
func TestCalcEvalRejectsCode(t *testing.T) {
	_, ex := testReg(t, Options{})
	for _, expr := range []string{
		"__import__('os').system('dir')",
		"open('/etc/passwd')",
		"os.Exit(1)",
		"eval('1+1')",
		"sum(1,2)",
		"1+",
		"(1+2",
		"1/0",
		"sqrt(-1)",
		"",
	} {
		if _, err := ex.Execute(context.Background(), tool.Request{
			Ref: "calc.eval", Input: map[string]any{"expression": expr}, Actor: "user",
		}); err == nil {
			t.Errorf("reddedilmesi gereken ifade kabul edildi: %q", expr)
		}
	}
}

func TestTextStats(t *testing.T) {
	_, ex := testReg(t, Options{})
	got := run(t, ex, "text.stats", map[string]any{
		"text": "Merhaba dünya. Merhaba Fiko! Merhaba dünya.",
	})
	if w, _ := toFloat(got["words"]); w != 6 {
		t.Errorf("kelime sayısı 6 olmalı: %v", got)
	}
	if s, _ := toFloat(got["sentences"]); s != 3 {
		t.Errorf("cümle sayısı 3 olmalı: %v", got)
	}
	top, _ := got["topWords"].([]map[string]any)
	if len(top) == 0 || top[0]["word"] != "merhaba" {
		t.Errorf("en sık kelime 'merhaba' olmalı: %v", top)
	}
}

// TestNewToolsUseSafeUI: yanlış görünüm tool arayüzü bozar (bkz. builtin/ui.go).
func TestNewToolsUseSafeUI(t *testing.T) {
	want := map[string]tool.UI{
		"fs.list":   tool.UITable,
		"fs.search": tool.UITable,
		"fs.stat":   tool.UIForm,
		"calc.eval": tool.UIForm,
	}
	defs := append(fsDefinitions(Options{}), calcDefinitions(Options{})...)
	for _, d := range defs {
		if exp, ok := want[d.Tool.Name]; ok && d.Tool.UI != exp {
			t.Errorf("%s görünümü %q olmalı, %q", d.Tool.Name, exp, d.Tool.UI)
		}
		for _, perm := range d.Tool.Permissions {
			if perm != "read" && perm != "execute" {
				t.Errorf("%s güvenli olmayan izin istiyor: %s", d.Tool.Name, perm)
			}
		}
	}
}

// TestCalcEvalEdgeCases: bulunmus kenar durumlari kalici teste donustu.
// once "1e5" hata veriyordu (bilimsel gosterim yoktu); "1/3" ise 10 basamaga
// yuvarlanip 0.3333333333 kesiliyordu.
func TestCalcEvalEdgeCases(t *testing.T) {
	_, ex := testReg(t, Options{})
	cases := []struct {
		expr string
		want float64
	}{
		{"1e5", 100000}, // bilimsel gosterim
		{"2.5E-3", 0.0025},
		{"1/3", 1.0 / 3},   // hassasiyet kirpilmamali
		{"-0", 0},          // -0 yerine 0
		{"  2  +  3  ", 5}, // bosluklar
	}
	for _, c := range cases {
		got, _ := toFloat(run(t, ex, "calc.eval", map[string]any{"expression": c.expr})["result"])
		if math.Abs(got-c.want) > 1e-9 {
			t.Errorf("%q = %v, beklenen %v", c.expr, got, c.want)
		}
	}
}

// TestCalcEvalGuards: derinlik siniri ve uzun ifade guvenli sekilde islenir
// (yigilma/stack tasmasi olmamali).
func TestCalcEvalGuards(t *testing.T) {
	_, ex := testReg(t, Options{})
	deep := strings.Repeat("(", 5000) + "1" + strings.Repeat(")", 5000)
	if _, err := ex.Execute(context.Background(), tool.Request{
		Ref: "calc.eval", Input: map[string]any{"expression": deep}, Actor: "user",
	}); err == nil {
		t.Error("aşırı derin ifade reddedilmeliydi")
	}
	// Uzun ama duz ifade kabul edilir.
	long := strings.Repeat("1+", 2000) + "1"
	got, _ := toFloat(run(t, ex, "calc.eval", map[string]any{"expression": long})["result"])
	if got != 2001 {
		t.Errorf("uzun ifade = %v, beklenen 2001", got)
	}
}

// TestSearchReportsTruncation: limit asilince sonuc kesildigi ACIKCA bildirilir;
// kullanici eksik sonuclari bilmelidir (sessiz veri kaybi olmaz).
func TestSearchReportsTruncation(t *testing.T) {
	root := t.TempDir()
	_, ex := testReg(t, Options{FSRoot: root})
	for i := 0; i < 5; i++ {
		name := filepath.Join(root, fmt.Sprintf("f%d.txt", i))
		if err := os.WriteFile(name, []byte("x"), 0o600); err != nil {
			t.Fatal(err)
		}
	}
	got := run(t, ex, "fs.search", map[string]any{"query": ".txt", "limit": float64(2)})
	matches, _ := got["matches"].([]map[string]any)
	if len(matches) != 3 {
		t.Fatalf("2 sonuç + 1 uyarı satırı beklenir: %v", got)
	}
	last := matches[len(matches)-1]["name"].(string)
	if !strings.Contains(last, "sınırlandı") {
		t.Errorf("kesildiği uyarı satırı olmalı: %q", last)
	}
}
