package ollama

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

	"fiko-ai/internal/llm"
)

// retryTestClient, bekleme sürelerini testi yavaşlatmayacak kadar kısa tutar.
func retryTestClient(srvURL string) *Client {
	return New(srvURL, "m1", 5*time.Second).WithRetry(3, time.Millisecond)
}

// CANLI HATANIN KARSILIĞI: Ollama cloud proxy'si önce 502 verip sonra düzelir.
// Sohbet HATA balonuyla ölmemeli, cevap aynı turda gelmelidir.
func TestGenerateStream_Transient502ThenSuccess(t *testing.T) {
	var calls int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if atomic.AddInt32(&calls, 1) <= 2 {
			w.WriteHeader(http.StatusBadGateway)
			_, _ = w.Write([]byte(`{"error":"Post \"https://ollama.com:443/api/chat\": connection error: PROTOCOL_ERROR"}`))
			return
		}
		writeLine(w, `{"message":{"role":"assistant","content":"Mer"},"done":false}`)
		writeLine(w, `{"model":"gemma4:31b-cloud","message":{"role":"assistant","content":"haba"},"done":true,"done_reason":"stop"}`)
	}))
	defer srv.Close()

	// Cloud davranışı: Ollama cevapta soneki düşürür ("gemma4:31b"), ama biz
	// İSTENEN adı döndürmeliyiz (yoksa "başka model çalıştı" izlenimi doğar).
	req := userReq("selam")
	req.Model = "gemma4:31b-cloud"
	res, err := retryTestClient(srv.URL).Generate(context.Background(), req)
	if err != nil {
		t.Fatalf("geçici 502 sonrası sohbet ölmemeliydi: %v", err)
	}
	if res.Content != "Merhaba" {
		t.Fatalf("içerik = %q, beklenen %q", res.Content, "Merhaba")
	}
	if res.Model != "gemma4:31b-cloud" {
		t.Fatalf("model = %q, istenen ad korunmalı", res.Model)
	}
	if n := atomic.LoadInt32(&calls); n != 3 {
		t.Fatalf("deneme sayısı = %d, beklenen 3 (2 hata + 1 başarı)", n)
	}
}

// Kalıcı hatalar (404: model yok) TEKRARLANMAMALI: boşuna beklemek zaman kaybıdır.
func TestGenerateStream_PermanentErrorNotRetried(t *testing.T) {
	for _, code := range []int{http.StatusNotFound, http.StatusBadRequest, http.StatusUnauthorized} {
		var calls int32
		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			atomic.AddInt32(&calls, 1)
			w.WriteHeader(code)
			_, _ = w.Write([]byte(`{"error":"kalıcı hata"}`))
		}))

		_, err := retryTestClient(srv.URL).Generate(context.Background(), userReq("x"))
		var pe *llm.ProviderError
		if !errors.As(err, &pe) || pe.StatusCode != code {
			srv.Close()
			t.Fatalf("HTTP %d: beklenmeyen hata: %v", code, err)
		}
		if n := atomic.LoadInt32(&calls); n != 1 {
			srv.Close()
			t.Fatalf("HTTP %d: %d deneme yapıldı, kalıcı hata TEKRARLANMAMALI", code, n)
		}
		srv.Close()
	}
}

// Deneme sınırı: sunucu hep 502 derse maxAttempts kadar deneyip DURMALI (sonsuz döngü yok).
func TestGenerateStream_RetryLimitRespected(t *testing.T) {
	var calls int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&calls, 1)
		w.WriteHeader(http.StatusBadGateway)
		_, _ = w.Write([]byte(`{"error":"upstream yok"}`))
	}))
	defer srv.Close()

	_, err := retryTestClient(srv.URL).Generate(context.Background(), userReq("x"))
	var pe *llm.ProviderError
	if !errors.As(err, &pe) || pe.StatusCode != http.StatusBadGateway {
		t.Fatalf("son denemede 502 dönmeli: %v", err)
	}
	if n := atomic.LoadInt32(&calls); n != 3 {
		t.Fatalf("deneme sayısı = %d, beklenen 3 (tavan)", n)
	}
}

// maxAttempts=1 ise retry YOK: davranış eskisiyle aynı kalır.
func TestGenerateStream_RetryDisabled(t *testing.T) {
	var calls int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&calls, 1)
		w.WriteHeader(http.StatusServiceUnavailable)
	}))
	defer srv.Close()

	c := New(srv.URL, "m1", time.Second).WithRetry(1, time.Millisecond)
	if _, err := c.Generate(context.Background(), userReq("x")); err == nil {
		t.Fatal("hata bekleniyordu")
	}
	if n := atomic.LoadInt32(&calls); n != 1 {
		t.Fatalf("deneme sayısı = %d, retry kapalıyken 1 olmalı", n)
	}
}

// EN KRİTİK GÜVENLİK KURALI: akış başladıktan sonra retry YAPILMAZ.
// Yoksa kullanıcıya aynı içerik iki kez akar ve sohbet bozulur.
func TestGenerateStream_NoRetryAfterStreamStarted(t *testing.T) {
	var calls int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&calls, 1)
		writeLine(w, `{"message":{"role":"assistant","content":"Parça"},"done":false}`)
		writeLine(w, `{"error":"akış ortası çöktü"}`) // akış BAŞLADIKTAN sonra hata
	}))
	defer srv.Close()

	res, err := retryTestClient(srv.URL).Generate(context.Background(), userReq("x"))
	if err == nil {
		t.Fatal("akış içi hata kullanıcıya yansımalı")
	}
	if n := atomic.LoadInt32(&calls); n != 1 {
		t.Fatalf("deneme sayısı = %d, akış başladıktan sonra tekrar DENENMEMELİ", n)
	}
	if res.Content != "Parça" {
		t.Fatalf("kısmi içerik kaybolmamalı: %q", res.Content)
	}
}

// Tool akışı da korunmalı: 502 sonrası tool_calls aynı turda gelmeli.
func TestGenerateStreamWithTools_Transient502ThenSuccess(t *testing.T) {
	var calls int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if atomic.AddInt32(&calls, 1) == 1 {
			w.WriteHeader(http.StatusBadGateway)
			_, _ = w.Write([]byte(`{"error":"upstream yok"}`))
			return
		}
		writeLine(w, `{"message":{"role":"assistant","tool_calls":[{"function":{"name":"fiko.now","arguments":{"x":1}}}]},"done":true,"done_reason":"stop"}`)
	}))
	defer srv.Close()

	c := retryTestClient(srv.URL)
	res, err := c.GenerateStreamWithTools(context.Background(), userReq("saat kaç"),
		[]llm.ToolSpec{{Name: "fiko.now", Description: "saat"}}, nil)
	if err != nil {
		t.Fatalf("geçici 502 sonrası tool akışı ölmemeliydi: %v", err)
	}
	if len(res.ToolCalls) != 1 || res.ToolCalls[0].Name != "fiko.now" {
		t.Fatalf("tool çağrısı kayboldu: %+v", res.ToolCalls)
	}
	if n := atomic.LoadInt32(&calls); n != 2 {
		t.Fatalf("deneme sayısı = %d, beklenen 2", n)
	}
}

// Her deneme gövdeyi TAMAMEN gönderebilmeli (retry sonrası boş gövde gönderilmez).
func TestGenerateStream_RetriedRequestKeepsBody(t *testing.T) {
	var mu struct {
		last   chatRequest
		filled int32
	}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var got chatRequest
		if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
			t.Errorf("gövde çözümlenemedi: %v", err)
		}
		if len(got.Messages) == 1 && got.Messages[0].Content == "selam" {
			atomic.AddInt32(&mu.filled, 1)
		}
		mu.last = got
		if atomic.LoadInt32(&mu.filled) < 2 {
			w.WriteHeader(http.StatusBadGateway)
			return
		}
		writeLine(w, `{"message":{"content":"tamam"},"done":true}`)
	}))
	defer srv.Close()

	res, err := retryTestClient(srv.URL).Generate(context.Background(), userReq("selam"))
	if err != nil {
		t.Fatalf("hata: %v", err)
	}
	if res.Content != "tamam" {
		t.Fatalf("içerik = %q", res.Content)
	}
	if mu.last.Model != "m1" {
		t.Fatalf("son denemede model bozuldu: %+v", mu.last)
	}
}

// REGRESYON: cloud modellerde Ollama cevap adından ":cloud" sonekini düşer
// ("gemma4:31b-cloud" isteğine "gemma4:31b" döner). Sonuç İSTENEN adla dönmeli:
// bellek öğrenme gibi sonraki çağrılar bu adı kullanıyor ve soneksiz ad
// Ollama'da 404 ("model 'gemma4:31b' not found") veriyordu.
func TestGenerateStreamWithTools_KeepsRequestedCloudModelName(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		writeLine(w, `{"model":"gemma4:31b","message":{"role":"assistant","content":"tamam"},"done":true,"done_reason":"stop"}`)
	}))
	defer srv.Close()

	c := New(srv.URL, "", time.Second).WithRetry(1, time.Millisecond)
	req := userReq("selam")
	req.Model = "gemma4:31b-cloud"
	res, err := c.GenerateStreamWithTools(context.Background(), req, nil, nil)
	if err != nil {
		t.Fatalf("hata: %v", err)
	}
	if res.Model != "gemma4:31b-cloud" {
		t.Fatalf("model = %q, istenen ad (sonekli) korunmalı; soneksiz ad 404'e yol açar", res.Model)
	}
}

// Kullanıcı bekleme sırasında vazgeçerse (iptal) retry DURMALI ve ctx.Err() dönmeli.
func TestGenerateStream_CancelDuringRetryBackoff(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadGateway)
	}))
	defer srv.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	c := New(srv.URL, "m1", time.Second).WithRetry(5, 2*time.Second) // uzun backoff
	go func() {
		time.Sleep(50 * time.Millisecond)
		cancel()
	}()

	if _, err := c.Generate(ctx, userReq("x")); err != context.Canceled {
		t.Fatalf("context.Canceled bekleniyordu, gelen: %v", err)
	}
}

// Bağlantı hiç KURULAMAZSA (Ollama kapalı) tekrar denenmeli: istek sunucuya ulaşmadı.
func TestGenerateStream_DialFailureRetried(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
	dead := srv.URL
	srv.Close() // port kapandı: bağlantı reddedilecek

	c := New(dead, "m1", time.Second).WithRetry(2, time.Millisecond)
	start := time.Now()
	if _, err := c.Generate(context.Background(), userReq("x")); err == nil {
		t.Fatal("kapalı sunucuya karşı hata bekleniyordu")
	}
	if d := time.Since(start); d > 2*time.Second {
		t.Fatalf("bekleme beklenmedik uzun: %v", d)
	}
}

func TestRetryDelay_BackoffGrowsAndIsCapped(t *testing.T) {
	c := New("http://x", "m", time.Second).WithRetry(5, 100*time.Millisecond)
	if got := c.retryDelay(1, nil); got != 100*time.Millisecond {
		t.Errorf("1. deneme = %v, beklenen 100ms", got)
	}
	if got := c.retryDelay(2, nil); got != 200*time.Millisecond {
		t.Errorf("2. deneme = %v, beklenen 200ms", got)
	}
	if got := c.retryDelay(9, nil); got != maxRetryDelay {
		t.Errorf("tavan aşılmamalı: %v", got)
	}
	// Retry-After sunucudan geliyorsa o UYGULANMALI.
	resp := &http.Response{Header: http.Header{"Retry-After": []string{"2"}}}
	if got := c.retryDelay(1, resp); got != 2*time.Second {
		t.Errorf("Retry-After yok sayıldı: %v", got)
	}
	// Ama tavan yine de geçerli.
	resp2 := &http.Response{Header: http.Header{"Retry-After": []string{"600"}}}
	if got := c.retryDelay(1, resp2); got != maxRetryDelay {
		t.Errorf("Retry-After tavanı aştı: %v", got)
	}
}

func TestRetryableStatus(t *testing.T) {
	for _, code := range []int{408, 429, 500, 502, 503, 504} {
		if !retryableStatus(code) {
			t.Errorf("HTTP %d geçici sayılmalı", code)
		}
	}
	for _, code := range []int{400, 401, 403, 404, 405, 413, 422} {
		if retryableStatus(code) {
			t.Errorf("HTTP %d KALICI (tekrar edilmemeli)", code)
		}
	}
}

func TestParseRetryAfter(t *testing.T) {
	now := time.Now()
	if _, ok := parseRetryAfter("", now); ok {
		t.Error("boş değer için false bekleniyordu")
	}
	if d, ok := parseRetryAfter("5", now); !ok || d != 5*time.Second {
		t.Errorf("saniye biçimi: %v %v", d, ok)
	}
	if _, ok := parseRetryAfter("-1", now); ok {
		t.Error("negatif değer kabul edilmemeli")
	}
	// Geçmişteki tarih: bekleme 0, ama çözümlenmiş sayılır.
	if d, ok := parseRetryAfter(now.Add(-time.Hour).UTC().Format(http.TimeFormat), now); !ok || d != 0 {
		t.Errorf("geçmiş tarih: %v %v", d, ok)
	}
	if _, ok := parseRetryAfter("yarın", now); ok {
		t.Error("anlaşılmayan değer için false bekleniyordu")
	}
}

// ListModels de transient hataya dayanıklı olmalı: liste boş kalırsa ayar ekranı bozulur.
func TestListModels_Transient502ThenSuccess(t *testing.T) {
	var calls int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if atomic.AddInt32(&calls, 1) == 1 {
			w.WriteHeader(http.StatusBadGateway)
			_, _ = w.Write([]byte(`{"error":"upstream yok"}`))
			return
		}
		_, _ = w.Write([]byte(`{"models":[{"name":"qwen2.5:7b","size":10,"modified_at":"2026-09-01T10:00:00Z"}]}`))
	}))
	defer srv.Close()

	ms, err := retryTestClient(srv.URL).ListModels(context.Background())
	if err != nil {
		t.Fatalf("model listesi 502 sonrası alınmalıydı: %v", err)
	}
	if len(ms) != 1 || ms[0].Name != "qwen2.5:7b" {
		t.Fatalf("model listesi: %+v", ms)
	}
}

// Embed de aynı korumaya sahip olmalı (skill/bilgi araması arka planda çalışır).
func TestEmbed_Transient502ThenSuccess(t *testing.T) {
	var calls int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if atomic.AddInt32(&calls, 1) == 1 {
			w.WriteHeader(http.StatusBadGateway)
			return
		}
		_, _ = w.Write([]byte(`{"embeddings":[[0.1,0.2,0.3]]}`))
	}))
	defer srv.Close()

	vec, err := New(srv.URL, "m", time.Second).WithEmbedModel("qwen3-embedding").
		WithRetry(3, time.Millisecond).Embed(context.Background(), "merhaba")
	if err != nil {
		t.Fatalf("embed 502 sonrası çalışmalıydı: %v", err)
	}
	if len(vec) != 3 || vec[0] != 0.1 {
		t.Fatalf("vektör = %v", vec)
	}
}
