package notify

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"strings"
	"testing"
	"time"
)

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

// fakeSink, gönderilen mesajları toplayan sahte yayıncıdır (WS değil; yalnızca
// "kaç kez anında iletildi" sorusunu yanıtlar).
type fakeSink struct{ delivered []Message }

func (f *fakeSink) Deliver(_ string, m Message) { f.delivered = append(f.delivered, m) }

func (f *fakeSink) count() int { return len(f.delivered) }

// fakeUsers, görünen ad çözümleyen sahte kullanıcı servisidir.
type fakeUsers struct{ names map[string]string }

func (f fakeUsers) DisplayName(_ context.Context, id string) (string, error) {
	if n, ok := f.names[id]; ok {
		return n, nil
	}
	return "", errors.New("kullanıcı yok")
}

// newTestService kurar; sınırlar testlerde küçük tutulur (hızlı olsun).
func newTestService(limits Limits, clock func() time.Time) (*Service, *MemoryRepository, *fakeSink) {
	repo := NewMemoryRepository()
	sink := &fakeSink{}
	s := NewService(Options{
		Repo:   repo,
		Users:  fakeUsers{names: map[string]string{"u1": "Ali", "u2": "Veli"}},
		Sink:   sink,
		Limits: limits,
		Logger: testLogger(),
		Now:    clock,
	})
	return s, repo, sink
}

// fixedClock, ileri alınabilen sabit saat döndürür (süre testleri için).
func fixedClock() (func() time.Time, *time.Time) {
	base := time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC)
	var cur time.Time = base
	return func() time.Time { return cur }, &cur
}

// TestNew_Dogrulama: metin sınırları, alıcı/gönderen zorunluluğu, kendine gönderim.
func TestNew_Dogrulama(t *testing.T) {
	if m, err := New(NewInput{FromUserID: "u1", ToUserID: "u2", Text: "Toplantı 15:00'te"}); err != nil {
		t.Fatalf("geçerli bildirim reddedildi: %v", err)
	} else if m.Kind != KindInfo {
		t.Fatalf("tür varsayılanı info olmalı, %q geldi", m.Kind)
	}

	cases := []struct {
		name string
		in   NewInput
		want string
	}{
		{"alici_yok", NewInput{FromUserID: "u1", Text: "merhaba"}, "toUserId"},
		{"gonderen_yok", NewInput{ToUserID: "u2", Text: "merhaba"}, "fromUserId"},
		{"metin_kisa", NewInput{FromUserID: "u1", ToUserID: "u2", Text: "ab"}, "text"},
		{"metin_uzun", NewInput{FromUserID: "u1", ToUserID: "u2", Text: strings.Repeat("a", MaxTextRunes+1)}, "text"},
		{"kendine", NewInput{FromUserID: "u1", ToUserID: "u1", Text: "merhaba"}, "kendine"},
		{"tur_bilinmez", NewInput{FromUserID: "u1", ToUserID: "u2", Text: "merhaba", Kind: Kind("spam")}, "kind"},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			_, err := New(c.in)
			if err == nil || !strings.Contains(err.Error(), c.want) {
				t.Fatalf("%q içeren hata bekleniyordu, %v", c.want, err)
			}
			if !errors.Is(err, ErrValidation) {
				t.Fatal("hata ErrValidation ile eşleşmeli (REST 400 üretecek)")
			}
		})
	}
}

// TestNew_SoruVeSecenek: soruda seçenekler korunur (tekrar elenir), bildirimde silinir.
func TestNew_SoruVeSecenek(t *testing.T) {
	now, _ := fixedClock()
	q, err := New(NewInput{FromUserID: "u1", ToUserID: "u2", Kind: KindQuestion,
		Text:    "Toplantı saat kaçta?",
		Options: []Option{{Text: "15:00"}, {Text: "15:00"}, {Text: "16:00"}},
		Now:     now()})
	if err != nil {
		t.Fatal(err)
	}
	if len(q.Options) != 2 {
		t.Fatalf("tekrar eden seçenek elenmeliydi, %d kaldı", len(q.Options))
	}
	if q.ExpiresAt == nil {
		t.Fatal("soru için süre dolma ZORUNLU (yanıtlanmazsa sonsuza dek beklemesin)")
	}

	i, err := New(NewInput{FromUserID: "u1", ToUserID: "u2", Kind: KindInfo,
		Text: "Toplantı 15:00", Options: []Option{{Text: "evet"}}, Now: now()})
	if err != nil {
		t.Fatal(err)
	}
	if len(i.Options) != 0 {
		t.Fatalf("düz bildirimde seçenek olmamalı, %d var", len(i.Options))
	}
	if i.ExpiresAt != nil {
		t.Fatal("düz bildirimin süresi dolmamalı")
	}
}

// TestService_SaatlikKota: sınır dolunca GÖNDERİLMEZ, depoya da YAZILMAZ.
func TestService_SaatlikKota(t *testing.T) {
	clock, cur := fixedClock()
	s, repo, sink := newTestService(
		Limits{MaxPerHour: 2, MaxPendingPerReceiver: 10, MaxRecipients: 5}, clock)
	ctx := context.Background()

	for i := 0; i < 2; i++ {
		if _, err := s.Send(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "mesaj"}); err != nil {
			t.Fatalf("%d. gönderim reddedildi: %v", i+1, err)
		}
	}
	if _, err := s.Send(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "mesaj"}); err == nil {
		t.Fatal("saatlik sınır aşılınca gönderim reddedilmeliydi")
	}
	// KRİTİK: reddedilen gönderim depoya da yazılmamalı.
	items, _, _ := repo.List(ctx, "u2", Filter{Box: BoxInbox})
	if len(items) != 2 {
		t.Fatalf("depoda 2 kayıt olmalı (reddedilen yazılmamalı), %d var", len(items))
	}
	if sink.count() != 2 {
		t.Fatalf("anında iletilen 2 olmalı, %d", sink.count())
	}
	// Süre dolunca tekrar izin verilmeli (sabit sayaç değil, zaman PENCERESİ).
	*cur = cur.Add(2 * time.Hour)
	if _, err := s.Send(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "mesaj"}); err != nil {
		t.Fatalf("saat dolduğunda gönderim tekrar açılmalı: %v", err)
	}
}

// TestService_AliciBekleyenSoruSiniri: alıcı sınırı dolunca yeni soru reddedilir,
// BAŞKA alıcı etkilenmez.
func TestService_AliciBekleyenSoruSiniri(t *testing.T) {
	clock, _ := fixedClock()
	s, _, _ := newTestService(
		Limits{MaxPerHour: 100, MaxPendingPerReceiver: 2, MaxRecipients: 5}, clock)
	ctx := context.Background()

	for i := 0; i < 2; i++ {
		if _, err := s.Ask(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "soru?"}); err != nil {
			t.Fatalf("%d. soru reddedildi: %v", i+1, err)
		}
	}
	if _, err := s.Ask(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "soru?"}); err == nil {
		t.Fatal("alıcının bekleyen soru sınırı dolunca yeni soru reddedilmeliydi")
	}
	if _, err := s.Ask(ctx, SendInput{FromUserID: "u1", ToUserID: "u3", Text: "soru?"}); err != nil {
		t.Fatalf("başka alıcı etkilenmemeliydi: %v", err)
	}
}

// TestService_AskTuruSabitler: model "bilgi" dese bile tür SORU olarak sabitlenir.
func TestService_AskTuruSabitler(t *testing.T) {
	clock, _ := fixedClock()
	s, _, _ := newTestService(DefaultLimits(), clock)
	ctx := context.Background()

	m, err := s.Ask(ctx, SendInput{FromUserID: "u1", ToUserID: "u2",
		Kind: KindInfo, Text: "bilgi mi soru mu?"})
	if err != nil {
		t.Fatal(err)
	}
	if m.Kind != KindQuestion {
		t.Fatalf("tür sunucuda sabitlenmeli, %q geldi", m.Kind)
	}
}

// TestService_AnswerKatilim: YALNIZCA sorunun alıcısı yanıtlar.
func TestService_AnswerKatilim(t *testing.T) {
	clock, _ := fixedClock()
	s, _, sink := newTestService(DefaultLimits(), clock)
	ctx := context.Background()

	q, err := s.Ask(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "Onaylıyor musun?"})
	if err != nil {
		t.Fatal(err)
	}

	// Üçüncü taraf yanıtlayamaz.
	if _, _, err := s.Answer(ctx, "u3", q.ID, "evet"); !errors.Is(err, ErrForbidden) {
		t.Fatalf("başkasının sorusu yanıtlanmamalı, %v", err)
	}
	// GÖNDEREN de kendi sorusunu yanıtlayamaz (aksi halde kendini onaylardı).
	if _, _, err := s.Answer(ctx, "u1", q.ID, "evet"); !errors.Is(err, ErrForbidden) {
		t.Fatalf("gönderen kendi sorusunu yanıtlamamalı, %v", err)
	}
	if sink.count() != 1 {
		t.Fatalf("reddedilen yanıtlar alıcıya ITERILMEMELI, %d iletildi", sink.count())
	}

	// Doğru alıcı cevaplayabilir.
	_, reply, err := s.Answer(ctx, "u2", q.ID, "evet, onaylıyorum")
	if err != nil {
		t.Fatal(err)
	}
	if reply.AnswerText != "evet, onaylıyorum" {
		t.Fatalf("yanıt metni taşınmadı: %q", reply.AnswerText)
	}
	if reply.ToUserID != "u1" {
		t.Fatalf("yanıt SORANA gitmeli, %q'ya gitti", reply.ToUserID)
	}
	if reply.QuestionID != q.ID {
		t.Fatal("yanıt soruyu bağlayan kimliği taşımalı")
	}
	// Yanıtlayanın adı ÇÖZÜLMÜŞ olmalı: arayüz gönderen olarak bunu gösterir.
	// Ad yazılmazsa kart "Bilinmeyen" der (gerçek tarayıcı testinde görüldü).
	if reply.FromName == "" || reply.FromName == "Bilinmeyen" {
		t.Fatalf("yanıtlayanın adı çözülmeli, %q", reply.FromName)
	}
	// İkinci yanıt reddedilmeli.
	if _, _, err := s.Answer(ctx, "u2", q.ID, "bir daha"); err == nil {
		t.Fatal("yanıtlanmış soru tekrar yanıtlanamamalı")
	}
}

// TestService_SureDolmusSoru: süresi dolmuş soru "bulunamadı" olur.
func TestService_SureDolmusSoru(t *testing.T) {
	clock, cur := fixedClock()
	s, _, _ := newTestService(DefaultLimits(), clock)
	ctx := context.Background()

	q, err := s.Ask(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "Yanıtlar mısın?"})
	if err != nil {
		t.Fatal(err)
	}
	*cur = cur.Add(QuestionTTL + time.Hour)
	if _, _, err := s.Answer(ctx, "u2", q.ID, "evet"); !errors.Is(err, ErrNotFound) {
		t.Fatalf("süresi dolmuş soru bulunamadı olmalı, %v", err)
	}
}

// TestService_ToplukGonderim: alıcı sayısı sınırlıdır.
func TestService_ToplukGonderim(t *testing.T) {
	clock, _ := fixedClock()
	s, _, _ := newTestService(
		Limits{MaxPerHour: 100, MaxPendingPerReceiver: 10, MaxRecipients: 2}, clock)
	ctx := context.Background()

	if _, err := s.SendToMany(ctx, SendInput{FromUserID: "u1", Text: "duyuru"},
		[]string{"a", "b", "c"}); err == nil {
		t.Fatal("alıcı sınırı aşılınca reddedilmeliydi")
	}
	res, err := s.SendToMany(ctx, SendInput{FromUserID: "u1", Text: "duyuru"}, []string{"a", "b"})
	if err != nil {
		t.Fatal(err)
	}
	if len(res.Sent) != 2 {
		t.Fatalf("2 alıcıya gitmeliydi, %d gitti", len(res.Sent))
	}
}

// TestService_AdCozulemezseBildirimYineGider: kullanıcı servisi çökse bile mesaj gider.
func TestService_AdCozulemezseBildirimYineGider(t *testing.T) {
	clock, _ := fixedClock()
	// Users YOK: ad hiç çözülemeyecek.
	s := NewService(Options{Repo: NewMemoryRepository(), Limits: DefaultLimits(),
		Logger: testLogger(), Now: clock})
	m, err := s.Send(context.Background(),
		SendInput{FromUserID: "u1", ToUserID: "u2", Text: "merhaba"})
	if err != nil {
		t.Fatalf("ad çözülemezse bildirim YINE gönderilmeli: %v", err)
	}
	if m.FromName != "" {
		t.Fatalf("ad boş olmalı (kimseye yanlış ad gösterilmez), %q", m.FromName)
	}
}

// TestDepo_KutuAyrimi: bana gelenler ile benim gönderdiklerim AYRI listelenir.
func TestDepo_KutuAyrimi(t *testing.T) {
	clock, _ := fixedClock()
	s, _, _ := newTestService(DefaultLimits(), clock)
	ctx := context.Background()

	q, err := s.Ask(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "Onaylıyor musun?"})
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err := s.Answer(ctx, "u2", q.ID, "evet"); err != nil {
		t.Fatal(err)
	}
	inbox2, _, _ := s.List(ctx, "u2", Filter{Box: BoxInbox})
	sent1, _, _ := s.List(ctx, "u1", Filter{Box: BoxSent})
	if len(inbox2) != 1 {
		t.Fatalf("u2 inbox'ta 1 kayıt olmalı, %d", len(inbox2))
	}
	if len(sent1) != 2 {
		t.Fatalf("u1 sent kutusunda soru + yanıt olmalı, %d", len(sent1))
	}
	var sawAnswer bool
	for _, m := range sent1 {
		if m.Kind == KindAnswer {
			sawAnswer = true
		}
	}
	if !sawAnswer {
		t.Fatal("soranın kutusunda yanıt görünmeli")
	}
}

// TestDepo_OkunduVeZil: okunmayan sayılır, okunan sayılmaz; gönderen işaretleyemez.
func TestDepo_OkunduVeZil(t *testing.T) {
	clock, _ := fixedClock()
	s, _, _ := newTestService(DefaultLimits(), clock)
	ctx := context.Background()

	if _, err := s.Send(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "birinci"}); err != nil {
		t.Fatal(err)
	}
	if _, err := s.Send(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "ikinci"}); err != nil {
		t.Fatal(err)
	}
	if got := s.Unread(ctx, "u2"); got != 2 {
		t.Fatalf("okunmamış 2 olmalı, %d", got)
	}
	items, _, _ := s.List(ctx, "u2", Filter{Box: BoxInbox})
	if _, err := s.MarkRead(ctx, "u2", items[0].ID); err != nil {
		t.Fatal(err)
	}
	if got := s.Unread(ctx, "u2"); got != 1 {
		t.Fatalf("okunan sayılmamalı, 1 kalmalı; %d", got)
	}
	if _, err := s.MarkRead(ctx, "u1", items[0].ID); !errors.Is(err, ErrForbidden) {
		t.Fatalf("gönderen okundu işaretleyememeli, %v", err)
	}
}

// TestDepo_SilmeKatilimi: başkasının bildirimi silinemez, alıcı silebilir.
func TestDepo_SilmeKatilimi(t *testing.T) {
	clock, _ := fixedClock()
	s, _, _ := newTestService(DefaultLimits(), clock)
	ctx := context.Background()

	m, err := s.Send(ctx, SendInput{FromUserID: "u1", ToUserID: "u2", Text: "silme testi"})
	if err != nil {
		t.Fatal(err)
	}
	if err := s.Delete(ctx, "u3", m.ID); !errors.Is(err, ErrForbidden) {
		t.Fatalf("üçüncü taraf silememeli, %v", err)
	}
	if err := s.Delete(ctx, "u2", m.ID); err != nil {
		t.Fatalf("alıcı silebilmeli: %v", err)
	}
}
