package ollama

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strconv"
	"sync/atomic"
	"testing"
	"time"

	"fiko-ai/internal/llm"
)

func TestEmbed_Success(t *testing.T) {
	var gotPath, gotModel, gotInput string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotPath = r.URL.Path
		var body struct {
			Model string `json:"model"`
			Input string `json:"input"`
		}
		_ = json.NewDecoder(r.Body).Decode(&body)
		gotModel, gotInput = body.Model, body.Input
		_, _ = w.Write([]byte(`{"model":"nomic-embed-text","embeddings":[[0.25,0.5,-1]]}`))
	}))
	defer srv.Close()

	c := New(srv.URL, "chat-model", time.Second).WithEmbedModel("  nomic-embed-text ")
	vec, err := c.Embed(context.Background(), "30 mm geriye plane koy")
	if err != nil {
		t.Fatal(err)
	}
	if gotPath != "/api/embed" || gotModel != "nomic-embed-text" || gotInput != "30 mm geriye plane koy" {
		t.Fatalf("istek yanlış: path=%q model=%q input=%q", gotPath, gotModel, gotInput)
	}
	if len(vec) != 3 || vec[0] != 0.25 || vec[2] != -1 {
		t.Fatalf("vektör yanlış: %v", vec)
	}
	if c.EmbedModel() != "nomic-embed-text" {
		t.Fatalf("model kırpılmalı: %q", c.EmbedModel())
	}
}

func TestEmbed_ValidationDoesNotHitNetwork(t *testing.T) {
	var hits int32
	srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { atomic.AddInt32(&hits, 1) }))
	defer srv.Close()

	if _, err := New(srv.URL, "m", time.Second).Embed(context.Background(), "x"); !errors.Is(err, llm.ErrNoEmbedModel) {
		t.Fatalf("model yokken ErrNoEmbedModel beklenirdi: %v", err)
	}
	c := New(srv.URL, "m", time.Second).WithEmbedModel("e")
	if _, err := c.Embed(context.Background(), "  \n "); !errors.Is(err, llm.ErrEmptyText) {
		t.Fatalf("boş metinde ErrEmptyText beklenirdi: %v", err)
	}
	if atomic.LoadInt32(&hits) != 0 {
		t.Fatal("doğrulama hatalarında ağ çağrısı yapılmamalı")
	}
}

func TestEmbed_ProviderErrors(t *testing.T) {
	cases := []struct {
		name       string
		status     int
		body       string
		wantStatus int
	}{
		{"HTTP hatası", http.StatusNotFound, `{"error":"model \"yok\" not found"}`, 404},
		{"200 içinde error alanı", http.StatusOK, `{"error":"bellek yetersiz"}`, 0},
		{"boş embeddings", http.StatusOK, `{"embeddings":[]}`, 0},
		{"boş vektör", http.StatusOK, `{"embeddings":[[]]}`, 0},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(tc.status)
				_, _ = w.Write([]byte(tc.body))
			}))
			defer srv.Close()

			_, err := New(srv.URL, "m", time.Second).WithEmbedModel("e").Embed(context.Background(), "merhaba")
			var pe *llm.ProviderError
			if !errors.As(err, &pe) || pe.StatusCode != tc.wantStatus {
				t.Fatalf("ProviderError(status=%d) beklenirdi: %v", tc.wantStatus, err)
			}
		})
	}
}

func TestEmbed_CancelReturnsContextError(t *testing.T) {
	release := make(chan struct{})
	srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { <-release }))
	defer srv.Close()
	defer close(release)

	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	_, err := New(srv.URL, "m", 0).WithEmbedModel("e").Embed(ctx, "merhaba")
	if !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("DeadlineExceeded beklenirdi: %v", err)
	}
}

// batchServer her girdi için [index-tabanlı değer] döndüren sahte /api/embed sunucusudur: vektörün ilk elemanı
// metnin sayısal değeridir ("m7" -> 7), böylece sıra korunumu doğrulanabilir.
func batchServer(t *testing.T, sizes *[]int) *httptest.Server {
	t.Helper()
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var body struct {
			Input []string `json:"input"`
		}
		_ = json.NewDecoder(r.Body).Decode(&body)
		*sizes = append(*sizes, len(body.Input))
		vecs := make([][]float32, 0, len(body.Input))
		for _, s := range body.Input {
			var n int
			for _, ch := range s[1:] {
				n = n*10 + int(ch-'0')
			}
			vecs = append(vecs, []float32{float32(n), 1})
		}
		_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": vecs})
	}))
}

func TestEmbedBatch_OrderAndChunking(t *testing.T) {
	var sizes []int
	srv := batchServer(t, &sizes)
	defer srv.Close()

	texts := make([]string, 140)
	for i := range texts {
		texts[i] = "m" + strconv.Itoa(i)
	}
	c := New(srv.URL, "m", time.Second).WithEmbedModel("e")
	vecs, err := c.EmbedBatch(context.Background(), texts)
	if err != nil {
		t.Fatal(err)
	}
	if len(vecs) != 140 {
		t.Fatalf("140 vektör beklenirdi: %d", len(vecs))
	}
	for i, v := range vecs {
		if int(v[0]) != i {
			t.Fatalf("sıra bozuldu: %d. vektör %v", i, v)
		}
	}
	// 140 = 32+32+32+32+12
	if len(sizes) != 5 || sizes[0] != maxEmbedBatch || sizes[4] != 12 {
		t.Fatalf("istekler 32'lik parçalara bölünmeli: %v", sizes)
	}
}

func TestEmbedBatch_EmptyTextDoesNotHitNetwork(t *testing.T) {
	var hits int32
	srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { atomic.AddInt32(&hits, 1) }))
	defer srv.Close()

	c := New(srv.URL, "m", time.Second).WithEmbedModel("e")
	if _, err := c.EmbedBatch(context.Background(), []string{"a", " \n", "c"}); !errors.Is(err, llm.ErrEmptyText) {
		t.Fatalf("boş metinde ErrEmptyText beklenirdi: %v", err)
	}
	if _, err := New(srv.URL, "m", time.Second).EmbedBatch(context.Background(), []string{"a"}); !errors.Is(err, llm.ErrNoEmbedModel) {
		t.Fatalf("model yokken ErrNoEmbedModel beklenirdi: %v", err)
	}
	if atomic.LoadInt32(&hits) != 0 {
		t.Fatal("doğrulama hatalarında ağ çağrısı yapılmamalı")
	}
}

func TestEmbedBatch_CountMismatchIsError(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"embeddings":[[1],[2]]}`)) // 3 istendi, 2 geldi
	}))
	defer srv.Close()

	_, err := New(srv.URL, "m", time.Second).WithEmbedModel("e").EmbedBatch(context.Background(), []string{"a", "b", "c"})
	var pe *llm.ProviderError
	if !errors.As(err, &pe) {
		t.Fatalf("vektör sayısı uyuşmazlığı ProviderError olmalı: %v", err)
	}
}

type singleOnly struct{ calls int }

func (s *singleOnly) Embed(_ context.Context, text string) ([]float32, error) {
	s.calls++
	return []float32{float32(len(text))}, nil
}

func TestEmbedAll_FallsBackToSingle(t *testing.T) {
	s := &singleOnly{}
	vecs, err := llm.EmbedAll(context.Background(), s, []string{"a", "bb", "ccc"})
	if err != nil || len(vecs) != 3 || vecs[2][0] != 3 || s.calls != 3 {
		t.Fatalf("tek tek yedek yolu yanlış: vecs=%v calls=%d err=%v", vecs, s.calls, err)
	}
}

func TestEmbedAll_PrefersBatch(t *testing.T) {
	var sizes []int
	srv := batchServer(t, &sizes)
	defer srv.Close()

	c := New(srv.URL, "m", time.Second).WithEmbedModel("e")
	if _, err := llm.EmbedAll(context.Background(), c, []string{"m1", "m2", "m3"}); err != nil {
		t.Fatal(err)
	}
	if len(sizes) != 1 || sizes[0] != 3 {
		t.Fatalf("toplu destek varsa tek istek atılmalı: %v", sizes)
	}
}
