package settings

import (
	"context"
	"io"
	"log/slog"
	"sync/atomic"
	"testing"
	"time"

	"fiko-ai/internal/event"
)

func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }

func TestNew_NormalizesUnsafeDefaults(t *testing.T) {
	// Bozuk başlangıç değerleri açılışta düzeltilmeli (sunucu bozuk ayarla çalışmamalı).
	s := New(Values{Temperature: 99, TopP: 5, MaxTokens: -10, ContextSize: -1}, testLogger(), nil)

	v := s.Get()
	if v.Temperature != MaxTemperature {
		t.Errorf("Temperature = %v, üst sınıra kırpılmalı", v.Temperature)
	}
	if v.TopP != 1 {
		t.Errorf("TopP = %v, 1'e kırpılmalı", v.TopP)
	}
	if v.MaxTokens != 0 || v.ContextSize != 0 {
		t.Errorf("negatif token değerleri 0 olmalı: max=%d ctx=%d", v.MaxTokens, v.ContextSize)
	}
}

func TestSet_PartialUpdatePreservesOthers(t *testing.T) {
	s := New(Values{Model: "m1", Temperature: 0.7, MaxTokens: 100}, testLogger(), nil)

	// Yalnızca model değişiyor: diğerleri korunmalı (partial update).
	got, err := s.Set(Values{Model: "m2"})
	if err != nil {
		t.Fatalf("Set: %v", err)
	}
	if got.Model != "m2" {
		t.Errorf("Model = %q, beklenen m2", got.Model)
	}
	if got.Temperature != 0.7 || got.MaxTokens != 100 {
		t.Errorf("korunması gerekenler değişti: %+v", got)
	}
}

func TestSet_RejectsOutOfRange(t *testing.T) {
	s := New(Values{Model: "m", Temperature: 0.5}, testLogger(), nil)

	cases := []struct {
		name  string
		patch Values
	}{
		{"cok_yuksek_temperature", Values{Temperature: 3}},
		{"cok_yuksek_topP", Values{TopP: 1.5}},
		{"cok_buyuk_maxtokens", Values{MaxTokens: MaxModelTokens + 1}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if _, err := s.Set(tc.patch); err == nil {
				t.Fatal("geçersiz değer reddedilmeliydi")
			}
		})
	}

	// Reddedilen değişiklik değerleri BOZMAMALI.
	v := s.Get()
	if v.Temperature != 0.5 {
		t.Errorf("reddedilen değişiklik uygulanmamalıydı, temperature=%v", v.Temperature)
	}
}

// LLMOptions temperature 0'ı "belirtilmemiş" saymalı: 0 geçerli bir değerdir
// (deterministik üretim) ve sağlayıcıya gönderilirse model anlamını yitirir.
func TestLLMOptions_ZeroTemperatureIsNotSent(t *testing.T) {
	s := New(Values{Model: "m", Temperature: 0}, testLogger(), nil)

	opts := s.LLMOptions()
	if opts.Temperature != nil {
		t.Errorf("temperature=0 gönderilmemeliydi, gönderilen: %v", *opts.Temperature)
	}
	if opts.TopP != nil {
		t.Error("topP 0 iken gönderilmemeliydi")
	}
}

func TestLLMOptions_CarriesValues(t *testing.T) {
	s := New(Values{}, testLogger(), nil)
	if _, err := s.Set(Values{Model: "m", Temperature: 0.8, MaxTokens: 512, ContextSize: 4096}); err != nil {
		t.Fatal(err)
	}

	opts := s.LLMOptions()
	if opts.Temperature == nil || *opts.Temperature != 0.8 {
		t.Errorf("Temperature = %v, beklenen 0.8", opts.Temperature)
	}
	if opts.MaxTokens != 512 || opts.ContextSize != 4096 {
		t.Errorf("token ayarları taşınmadı: %+v", opts)
	}
}

// topP ayarı sağlayıcıya GİTMELİ (eskiden LLMOptions TopP'yi hep nil bırakıyordu; UI'daki alan etkisizdi).
func TestLLMOptions_TopPIsForwarded(t *testing.T) {
	s := New(Values{}, testLogger(), nil)
	if _, err := s.Set(Values{Model: "m", TopP: 0.85}); err != nil {
		t.Fatal(err)
	}
	if got := s.LLMOptions().TopP; got == nil || *got != 0.85 {
		t.Errorf("TopP = %v, beklenen 0.85", got)
	}

	zero := 0.0
	if _, err := s.Update(Patch{TopP: &zero}); err != nil {
		t.Fatal(err)
	}
	if got := s.LLMOptions().TopP; got != nil {
		t.Errorf("topP 0'a dönünce gönderilmemeli, gelen %v", *got)
	}
}

// Ayarlar her üretimde yeniden okunmalı: Set sonrası LLMOptions yeni değeri dönmeli.
func TestLLMOptions_ReflectsLiveChanges(t *testing.T) {
	s := New(Values{Model: "m", Temperature: 0.2}, testLogger(), nil)
	if got := s.LLMOptions().Temperature; got == nil || *got != 0.2 {
		t.Fatalf("başlangıç: %v", got)
	}
	if _, err := s.Set(Values{Temperature: 1.4}); err != nil {
		t.Fatal(err)
	}
	if got := s.LLMOptions().Temperature; got == nil || *got != 1.4 {
		t.Errorf("ayar değişikliği yansımadı: %v", got)
	}
}

func TestSet_PublishesEvent(t *testing.T) {
	bus := event.NewMemoryBus(testLogger(), 8)
	defer bus.Close()

	got := make(chan event.Event, 1)
	bus.Subscribe(event.SettingsChanged, func(_ context.Context, e event.Event) { got <- e })

	s := New(Values{Model: "m"}, testLogger(), bus)
	if _, err := s.Set(Values{Model: "yeni"}); err != nil {
		t.Fatal(err)
	}

	select {
	case e := <-got:
		if e.Type != event.SettingsChanged {
			t.Errorf("olay türü = %v", e.Type)
		}
		if e.Data["model"] != "yeni" {
			t.Errorf("olay verisi = %v", e.Data)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("SettingsChanged yayınlanmadı")
	}
}

// Bus olmadan da çalışmalı (bus opsiyoneldir).
func TestSet_WithoutBus(t *testing.T) {
	s := New(Values{Model: "m"}, testLogger(), nil)
	if _, err := s.Set(Values{Temperature: 0.9}); err != nil {
		t.Fatalf("bus yokken Set çökmemeliydi: %v", err)
	}
}

// Eşzamanlı okuma/yazma güvenli olmalı (HTTP isteği + WS üretimi aynı anda).
func TestConcurrentGetSet(t *testing.T) {
	s := New(Values{Model: "m", Temperature: 0.5}, testLogger(), nil)
	var reads atomic.Int64
	done := make(chan struct{})

	go func() {
		defer close(done)
		for i := 0; i < 500; i++ {
			s.Get()
			s.LLMOptions()
			reads.Add(1)
		}
	}()
	for i := 0; i < 200; i++ {
		if _, err := s.Set(Values{Temperature: 0.1}); err != nil {
			t.Fatal(err)
		}
		if _, err := s.Set(Values{Temperature: 0.9}); err != nil {
			t.Fatal(err)
		}
	}
	<-done

	if reads.Load() != 500 {
		t.Errorf("okuma sayısı = %d", reads.Load())
	}
}
