package ollama

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"fiko-ai/internal/llm"
)

// toolServer tools alanını doğrulayan sahte /api/chat sunucusu kurur.
// handler, gövdeyi okur ve verilen NDJSON satırlarını akışa yazar.
func toolServer(t *testing.T, lines []string, check func(toolChatRequest)) *httptest.Server {
	t.Helper()
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/chat" || r.Method != http.MethodPost {
			t.Errorf("beklenmeyen istek: %s %s", r.Method, r.URL.Path)
		}
		var body toolChatRequest
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Errorf("istek okunamadı: %v", err)
		}
		if check != nil {
			check(body)
		}
		for _, l := range lines {
			writeLine(w, l)
		}
	}))
	t.Cleanup(srv.Close)
	return srv
}

func spec() []llm.ToolSpec {
	return []llm.ToolSpec{{
		Name:        "fiko.echo",
		Description: "Girdiyi döndürür.",
		InputSchema: map[string]any{"type": "object", "properties": map[string]any{"message": map[string]any{"type": "string"}}},
	}}
}

func TestGenerateStreamWithTools_CollectsCall(t *testing.T) {
	srv := toolServer(t, []string{
		`{"message":{"role":"assistant","content":""},"done":false}`,
		`{"message":{"role":"assistant","tool_calls":[{"function":{"name":"fiko.echo","arguments":{"message":"merhaba"}}}]},"done":false}`,
		`{"model":"m1","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","prompt_eval_count":7,"eval_count":2}`,
	}, func(body toolChatRequest) {
		if len(body.Tools) != 1 || body.Tools[0].Function.Name != "fiko.echo" {
			t.Errorf("tools alanı yanlış: %+v", body.Tools)
		}
		if body.Tools[0].Type != "function" {
			t.Errorf("tools türü 'function' olmalı: %q", body.Tools[0].Type)
		}
		if body.Tools[0].Function.Parameters["type"] != "object" {
			t.Errorf("şema aktarılmamış: %+v", body.Tools[0].Function.Parameters)
		}
	})

	c := New(srv.URL, "m1", 5*time.Second)
	res, err := c.GenerateStreamWithTools(context.Background(), userReq("selam"), spec(), nil)
	if err != nil {
		t.Fatalf("hata: %v", err)
	}
	if len(res.ToolCalls) != 1 {
		t.Fatalf("tool çağrısı bekleniyordu: %+v", res.ToolCalls)
	}
	call := res.ToolCalls[0]
	if call.Name != "fiko.echo" || call.Arguments["message"] != "merhaba" {
		t.Errorf("çağrı hatalı: %+v", call)
	}
	if res.PromptTokens != 7 || res.Model != "m1" {
		t.Errorf("sonuç meta hatalı: %+v", res)
	}
}

func TestGenerateStreamWithTools_TextAndCall(t *testing.T) {
	// Model önce konuşup sonra tool isteyebilir: delta'lar akmalı, çağrı toplanmalı.
	srv := toolServer(t, []string{
		`{"message":{"role":"assistant","content":"Bakıyorum. "},"done":false}`,
		`{"message":{"role":"assistant","tool_calls":[{"function":{"name":"fiko.echo","arguments":{}}}]},"done":true}`,
	}, nil)

	c := New(srv.URL, "m1", 5*time.Second)
	var deltas []string
	res, err := c.GenerateStreamWithTools(context.Background(), userReq("x"), spec(), func(d string) error {
		deltas = append(deltas, d)
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}
	if res.Content != "Bakıyorum. " || len(deltas) != 1 {
		t.Errorf("metin akışı bozuk: %q %v", res.Content, deltas)
	}
	if len(res.ToolCalls) != 1 || res.ToolCalls[0].Name != "fiko.echo" {
		t.Errorf("çağrı yok: %+v", res.ToolCalls)
	}
}

func TestGenerateStreamWithTools_ArgumentsAsString(t *testing.T) {
	// Bazı Ollama sürümleri argümanları JSON METNİ olarak gönderir.
	srv := toolServer(t, []string{
		`{"message":{"role":"assistant","tool_calls":[{"function":{"name":"fiko.echo","arguments":"{\"message\":\"metin\"}"}}]},"done":true}`,
	}, nil)
	c := New(srv.URL, "m1", 5*time.Second)
	res, err := c.GenerateStreamWithTools(context.Background(), userReq("x"), spec(), nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(res.ToolCalls) != 1 || res.ToolCalls[0].Arguments["message"] != "metin" {
		t.Errorf("metin argüman çözülemedi: %+v", res.ToolCalls)
	}
}

func TestGenerateStreamWithTools_BadArguments(t *testing.T) {
	// Bozuk argüman SESSİZCE boş girdiye dönüşmez: hata verilir (LLM'e yanlış bağlam bindirmeyelim).
	srv := toolServer(t, []string{
		`{"message":{"role":"assistant","tool_calls":[{"function":{"name":"fiko.echo","arguments":"{bozuk"}}]},"done":true}`,
	}, nil)
	c := New(srv.URL, "m1", 5*time.Second)
	_, err := c.GenerateStreamWithTools(context.Background(), userReq("x"), spec(), nil)
	var ae *llm.ArgsError
	if !errors.As(err, &ae) {
		t.Fatalf("ArgsError bekleniyordu, %v", err)
	}
}

func TestGenerateStreamWithTools_NoCall(t *testing.T) {
	srv := toolServer(t, []string{`{"message":{"role":"assistant","content":"Sadece cevap."},"done":true}`}, nil)
	c := New(srv.URL, "m1", 5*time.Second)
	res, err := c.GenerateStreamWithTools(context.Background(), userReq("x"), spec(), nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(res.ToolCalls) != 0 || res.Content != "Sadece cevap." {
		t.Errorf("tool çağrısı olmamalıydı: %+v", res)
	}
}

func TestGenerateStreamWithTools_Guardrails(t *testing.T) {
	c := New("http://127.0.0.1:1", "", time.Second)
	if _, err := c.GenerateStreamWithTools(context.Background(), llm.Request{}, spec(), nil); !errors.Is(err, llm.ErrNoModel) {
		t.Errorf("model yok hatası: %v", err)
	}
	if _, err := c.GenerateStreamWithTools(context.Background(), llm.Request{Model: "m1"}, spec(), nil); !errors.Is(err, llm.ErrEmptyMessages) {
		t.Errorf("mesaj yok hatası: %v", err)
	}
}

func TestGenerateStreamWithTools_ProviderError(t *testing.T) {
	srv := toolServer(t, []string{`{"error":"model yok"}`}, nil)
	c := New(srv.URL, "m1", 5*time.Second)
	_, err := c.GenerateStreamWithTools(context.Background(), userReq("x"), spec(), nil)
	if err == nil || !strings.Contains(err.Error(), "model yok") {
		t.Errorf("sağlayıcı hatası bekleniyordu: %v", err)
	}
}

func TestGenerateStreamWithTools_Cancelled(t *testing.T) {
	srv := toolServer(t, []string{
		`{"message":{"role":"assistant","content":"bir"},"done":false}`,
		`{"message":{"role":"assistant","content":"iki"},"done":true}`,
	}, nil)
	c := New(srv.URL, "m1", 5*time.Second)
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	_, err := c.GenerateStreamWithTools(ctx, userReq("x"), spec(), nil)
	if !errors.Is(err, context.Canceled) {
		t.Errorf("iptal hatası bekleniyordu: %v", err)
	}
}

func TestGenerateStreamWithTools_TruncatedStream(t *testing.T) {
	// done=true gelmeden bağlantı kapanırsa hata verilir (sessizce "başarılı" sayılmaz).
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		writeLine(w, `{"message":{"role":"assistant","content":"yarım"},"done":false}`)
	}))
	defer srv.Close()
	c := New(srv.URL, "m1", 5*time.Second)
	_, err := c.GenerateStreamWithTools(context.Background(), userReq("x"), spec(), nil)
	if err == nil || !strings.Contains(err.Error(), "beklenmedik") {
		t.Errorf("kesik akış hatası bekleniyordu: %v", err)
	}
}

func TestParseArguments(t *testing.T) {
	cases := []struct {
		name    string
		raw     string
		want    map[string]any
		wantErr bool
	}{
		{"boş", ``, map[string]any{}, false},
		{"null", `null`, map[string]any{}, false},
		{"nesne", `{"a":1}`, map[string]any{"a": 1.0}, false},
		{"metin", `"{\"b\":2}"`, map[string]any{"b": 2.0}, false},
		{"bozuk metin", `"bozuk"`, nil, true},
		{"dizi", `[1,2]`, nil, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := parseArguments(json.RawMessage(tc.raw))
			if tc.wantErr {
				if err == nil {
					t.Fatalf("hata bekleniyordu: %v", got)
				}
				return
			}
			if err != nil {
				t.Fatal(err)
			}
			if len(got) != len(tc.want) {
				t.Fatalf("got %v want %v", got, tc.want)
			}
			for k, v := range tc.want {
				if got[k] != v {
					t.Errorf("%s: got %v want %v", k, got[k], v)
				}
			}
		})
	}
}

// Aynı turda AYNI tool'un iki farklı çağrısı (ör. iki ayrı bilgi için iki memory.remember)
// ikisi de korunmalı; eskiden ikincisi birincinin üstüne yazılıp birincisi kayboluyordu.
// Birebir aynı çağrı (ad + argüman) ise tek sayılır.
func TestGenerateStreamWithTools_SameToolTwiceKeepsBoth(t *testing.T) {
	srv := toolServer(t, []string{
		`{"message":{"role":"assistant","tool_calls":[{"function":{"name":"fiko.echo","arguments":{"message":"Begüm 2014"}}}]},"done":false}`,
		`{"message":{"role":"assistant","tool_calls":[{"function":{"name":"fiko.echo","arguments":{"message":"Berkay 2018"}}}]},"done":false}`,
		`{"message":{"role":"assistant","tool_calls":[{"function":{"name":"fiko.echo","arguments":{"message":"Berkay 2018"}}}]},"done":false}`,
		`{"model":"m1","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop"}`,
	}, nil)

	c := New(srv.URL, "m1", 5*time.Second)
	res, err := c.GenerateStreamWithTools(context.Background(), userReq("selam"), spec(), nil)
	if err != nil {
		t.Fatalf("hata: %v", err)
	}
	if len(res.ToolCalls) != 2 {
		t.Fatalf("2 farklı çağrı bekleniyordu, gelen %d: %+v", len(res.ToolCalls), res.ToolCalls)
	}
	if res.ToolCalls[0].Arguments["message"] != "Begüm 2014" || res.ToolCalls[1].Arguments["message"] != "Berkay 2018" {
		t.Fatalf("sıra/içerik bozuk: %+v", res.ToolCalls)
	}
}
