package notify

import (
	"context"
	"errors"
	"strings"
	"testing"

	"fiko-ai/internal/tool"
)

// fakeRecipients, alıcı arama/çözümlemeyi taklit eder.
type fakeRecipients struct {
	all []UserRef
}

func (f fakeRecipients) Search(_ context.Context, q string, limit int) ([]UserRef, error) {
	q = strings.ToLower(q)
	out := make([]UserRef, 0, limit)
	for _, u := range f.all {
		if strings.Contains(strings.ToLower(u.DisplayName), q) ||
			strings.Contains(strings.ToLower(u.ID), q) {
			out = append(out, u)
			if len(out) >= limit {
				break
			}
		}
	}
	return out, nil
}

func (f fakeRecipients) Resolve(_ context.Context, ids []string) ([]UserRef, error) {
	byID := map[string]UserRef{}
	for _, u := range f.all {
		byID[u.ID] = u
	}
	out := make([]UserRef, 0, len(ids))
	for _, id := range ids {
		if u, ok := byID[id]; ok && !u.Disabled {
			out = append(out, u)
		}
	}
	return out, nil
}

// newToolService, sahte alıcı listesiyle tool seti kurar.
func newToolService(rec Recipients) (*ToolSet, *MemoryRepository) {
	repo := NewMemoryRepository()
	svc := NewService(Options{
		Repo:       repo,
		Users:      fakeUsers{names: map[string]string{"u1": "Ali", "u2": "Veli", "u3": "Ayşe"}},
		Recipients: rec,
		Limits:     DefaultLimits(),
		Logger:     testLogger(),
	})
	return NewToolSet(svc), repo
}

// toolWith, kullanıcı bağlamı taşıyan bir tool kaydı üretir.
func toolWith(name, userID string) *tool.Tool {
	return &tool.Tool{
		Name:   name,
		Config: map[string]any{"userId": userID, "conversationId": "conv_1"},
	}
}

// TestTools_KimlikYoksaCalismaz: GÜVENLİK — kimliksiz tool hiçbir şey gönderemez.
func TestTools_KimlikYoksaCalismaz(t *testing.T) {
	set, repo := newToolService(fakeRecipients{all: []UserRef{{ID: "u2", DisplayName: "Veli"}}})
	ctx := context.Background()

	handlers := []struct {
		name string
		h    tool.Handler
	}{
		{"notify.users", set.users},
		{"notify.send", set.send},
		{"notify.ask", set.ask},
		{"notify.answers", set.answers},
	}
	for _, tc := range handlers {
		t.Run(tc.name, func(t *testing.T) {
			_, err := tc.h(ctx, toolWith(tc.name, ""), map[string]any{
				"query": "Veli", "to": "u2", "text": "merhaba",
			})
			if err == nil {
				t.Fatal("kimlik yokken tool ÇALIŞMAMALI")
			}
		})
	}
	items, _, _ := repo.List(ctx, "u2", Filter{Box: BoxInbox})
	if len(items) != 0 {
		t.Fatalf("kimliksiz çağrı bir şey kaydetmemeli, %d kayıt var", len(items))
	}
}

// TestTools_GonderenKimligiSabit: model Config'i değiştirip başkası adına gönderemez.
func TestTools_GonderenKimligiSabit(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u2", DisplayName: "Veli"}}})
	ctx := context.Background()

	// Model girdiye kimlik alanları enjekte edebilir; HİÇBİRİ okunmaz.
	if _, err := set.send(ctx, toolWith("notify.send", "u1"), map[string]any{
		"to": "u2", "text": "merhaba",
		"fromUserId": "u3", "sender": "u3", "userId": "u3", "gonderen": "u3",
	}); err != nil {
		t.Fatal(err)
	}
	items, _, _ := set.svc.List(ctx, "u2", Filter{Box: BoxInbox})
	if len(items) != 1 {
		t.Fatalf("1 bildirim bekleniyordu, %d", len(items))
	}
	if items[0].FromUserID != "u1" {
		t.Fatalf("gönderen model girdisiyle DEĞİŞTİRİLEMEMEZ; %q geldi", items[0].FromUserID)
	}
	if items[0].FromName != "Ali" {
		t.Fatalf("gönderen adı çözülmeliydi, %q", items[0].FromName)
	}
}

// TestTools_HataSebebiModeleUlasir notify hatalarının SEBEBİP modele taşındığını
// kilitler.
//
// CANLI HATA (05.10.2026): ekranda "notify.send → tool hata döndürdü" görünüyordu.
// Executor yalnızca *tool.FailedError içindeki Reason'ı modele iletir; düz hata
// geldiğinde "tool hata döndürdü" diye GENEL mesaj döner. Bu paket fmt.Errorf
// döndürdüğü için gerçek sebep ("kendine bildirim gönderilemez") modele hiç
// ulaşmıyor, model da aynı hatayı tekrarlıyordu.
//
// Düzeltme: failTool artık *tool.FailedError döndürüyor (builtin.Fail ile aynı).
func TestTools_HataSebebiModeleUlasir(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u1", DisplayName: "Ali"}}})
	_, err := set.send(context.Background(), toolWith("notify.send", "u1"),
		map[string]any{"to": "u1", "text": "kendime not"})
	if err == nil {
		t.Fatal("kendine gönderim hata vermeli")
	}
	// Executor'ın aradığı tip: FailedError değilse sebep kaybolur.
	var fe *tool.FailedError
	if !errors.As(err, &fe) {
		t.Fatalf("hata *tool.FailedError olmalı, %T: %v", err, err)
	}
	if fe.Reason == "" {
		t.Fatal("FailedError.Reason boş: model sebebi göremez")
	}
	// Sebep modelin anlayacağı, YÖNLENDİRİCİ olmalı.
	for _, want := range []string{"kendine", "başka"} {
		if !strings.Contains(strings.ToLower(fe.Reason), want) {
			t.Errorf("sebep %q içermeli: %q", want, fe.Reason)
		}
	}
	// Uydurma alıcı seçmemesi de söylenmeli (tek kullanıcılı sistemde kritik).
	if !strings.Contains(fe.Reason, "uydurma") {
		t.Errorf("sebep uydurma alıcı uyarısı içermeli: %q", fe.Reason)
	}
}

// TestTools_TumHatalarFailedError döndürür: failTool her yerde aynı tipi üretmeli.
func TestTools_TumHatalarFailedError(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u2", DisplayName: "Veli"}}})
	tl := toolWith("notify.users", "u1")
	cases := []struct {
		name string
		run  func() error
	}{
		{"users_girdi_yok", func() error { _, e := set.users(context.Background(), tl, map[string]any{}); return e }},
		{"send_gonderen_yok", func() error {
			_, e := set.send(context.Background(), toolWith("notify.send", ""), map[string]any{"to": "u2", "text": "x"})
			return e
		}},
	}
	for _, tc := range cases {
		err := tc.run()
		if err == nil {
			t.Errorf("%s: hata bekleniyordu", tc.name)
			continue
		}
		var fe *tool.FailedError
		if !errors.As(err, &fe) || fe.Reason == "" {
			t.Errorf("%s: *tool.FailedError + Reason bekleniyordu, %T: %v", tc.name, err, err)
		}
	}
}

// TestTools_KendineBildirimGonderilmez: model kendine yazmayı denerse elenir.
//
// Beklenen davranış: HATA (sunucu "kendine gönderilemez" der). Sessizce
// elemek yerine hata vermek daha dürüst: model "gönderdim" deyip yalan söylemesin.
func TestTools_KendineBildirimGonderilmez(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u1", DisplayName: "Ali"}}})
	_, err := set.send(context.Background(), toolWith("notify.send", "u1"),
		map[string]any{"to": "u1", "text": "kendime not"})
	if err == nil || !strings.Contains(err.Error(), "kendine") {
		t.Fatalf("kendine gönderim hata vermeli, %v", err)
	}
}

// TestTools_CokluGonderimKendiniEler: toplu listede kendisi varsa SESSİZCE elenir
// (tek alıcı hatası vermek yerine; çünkü elenen varsa diğerleri gönderilmelidir).
func TestTools_CokluGonderimKendiniEler(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{
		{ID: "u2", DisplayName: "Veli"}, {ID: "u3", DisplayName: "Ayşe"}}})
	out, err := set.send(context.Background(), toolWith("notify.send", "u1"),
		map[string]any{"toMany": []any{"u1", "u2", "u3"}, "text": "toplantı 15:00"})
	if err != nil {
		t.Fatal(err)
	}
	if c, _ := out["count"].(int); c != 2 {
		t.Fatalf("kendisi elenmeli, 2 alıcıya gitmeli; count=%v", out["count"])
	}
}

// TestTools_AliciYoksaNetHata: model uydurma kimlik yazmaya çalışırsa yönlendirilir.
func TestTools_AliciYoksaNetHata(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u2", DisplayName: "Veli"}}})
	_, err := set.send(context.Background(), toolWith("notify.send", "u1"),
		map[string]any{"text": "merhaba"})
	if err == nil || !strings.Contains(err.Error(), "notify.users") {
		t.Fatalf("hata 'notify.users ile ara' yönlendirmesi içermeli, %v", err)
	}
}

// TestTools_UsersSadeceKimlikVerir: modele e-posta/rol sızmaz.
func TestTools_UsersSadeceKimlikVerir(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u2", DisplayName: "Veli"}}})
	out, err := set.users(context.Background(), toolWith("notify.users", "u1"),
		map[string]any{"query": "Veli"})
	if err != nil {
		t.Fatal(err)
	}
	users, _ := out["users"].([]map[string]any)
	if len(users) != 1 {
		t.Fatalf("1 kullanıcı bekleniyordu, %d", len(users))
	}
	if len(users[0]) != 2 {
		t.Fatalf("yalnızca id + displayName olmalı, %d alan: %v", len(users[0]), users[0])
	}
}

// TestTools_UsersBosSonucUyarir: kimse bulunamazsa model uydurmasın diye uyarı var.
func TestTools_UsersBosSonucUyarir(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u2", DisplayName: "Veli"}}})
	out, err := set.users(context.Background(), toolWith("notify.users", "u1"),
		map[string]any{"query": "YokBiri"})
	if err != nil {
		t.Fatal(err)
	}
	if msg, _ := out["message"].(string); !strings.Contains(msg, "UYDURMA") {
		t.Fatalf("boş sonuçta uyarı olmalı, %v", out)
	}
}

// TestTools_AskAsenkronBildirilir: model "cevap geldi" sanmasın.
func TestTools_AskAsenkronBildirilir(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u2", DisplayName: "Veli"}}})
	out, err := set.ask(context.Background(), toolWith("notify.ask", "u1"), map[string]any{
		"to": "u2", "text": "Toplantı 15:00'te mi?", "options": []any{"Evet", "Hayır"}})
	if err != nil {
		t.Fatal(err)
	}
	if out["questionId"] == nil {
		t.Fatal("soru kimliği dönmeli")
	}
	if msg, _ := out["message"].(string); !strings.Contains(msg, "Tur bitiyor") {
		t.Fatalf("asenkron davranış açıkça bildirilmeli, %q", msg)
	}
}

// TestTools_AnswersSoruyuBirlikteDoner: model "evet"in neye ait olduğunu bilsin.
func TestTools_AnswersSoruyuBirlikteDoner(t *testing.T) {
	set, _ := newToolService(fakeRecipients{all: []UserRef{{ID: "u2", DisplayName: "Veli"}}})
	ctx := context.Background()

	out, err := set.ask(ctx, toolWith("notify.ask", "u1"),
		map[string]any{"to": "u2", "text": "Toplantı 15:00'te mi?"})
	if err != nil {
		t.Fatal(err)
	}
	qid, _ := out["questionId"].(string)
	if _, _, err := set.svc.Answer(ctx, "u2", qid, "Evet, 15:00"); err != nil {
		t.Fatal(err)
	}
	// Ali (soran) yanıtları okur.
	ans, err := set.answers(ctx, toolWith("notify.answers", "u1"), map[string]any{})
	if err != nil {
		t.Fatal(err)
	}
	rows, _ := ans["results"].([]map[string]any)
	if len(rows) != 1 {
		t.Fatalf("1 yanıt bekleniyordu, %d", len(rows))
	}
	if rows[0]["answer"] != "Evet, 15:00" {
		t.Fatalf("yanıt metni taşınmadı: %v", rows[0])
	}
	if !strings.Contains(rows[0]["question"].(string), "15:00") {
		t.Fatalf("soru metni de dönmeli: %v", rows[0])
	}
	if rows[0]["questionId"] != qid {
		t.Fatal("soru kimliği eşleşmeli")
	}
}

// TestTools_DefinitionsKosulluSunum: alıcı arayüzü yoksa notify.users SUNULMAZ.
func TestTools_DefinitionsKosulluSunum(t *testing.T) {
	set, _ := newToolService(fakeRecipients{})
	if !hasTool(set, "notify.users") {
		t.Fatal("alıcı arayüzü varken notify.users sunulmalı")
	}
	// Arayüz YOK.
	svc := NewService(Options{Repo: NewMemoryRepository(), Limits: DefaultLimits(),
		Logger: testLogger()})
	if hasTool(NewToolSet(svc), "notify.users") {
		t.Fatal("alıcı arayüzü yokken notify.users sunulMAMALI")
	}
	// Servis YOK: hiç tool yok.
	if NewToolSet(nil).Definitions() != nil {
		t.Fatal("servis yokken tool tanımı üretilmemeli")
	}
}

func hasTool(set *ToolSet, name string) bool {
	for _, d := range set.Definitions() {
		if d.Tool.Name == name {
			return true
		}
	}
	return false
}

// TestInputStrings: düz metin ve dizi biçimleri kabul edilir.
func TestInputStrings(t *testing.T) {
	if got := inputStrings(map[string]any{"to": "a, b ,c"}, "to"); len(got) != 3 {
		t.Fatalf("virgüllü metin 3 alıcı vermeli, %d", len(got))
	}
	if got := inputStrings(map[string]any{"to": []any{"a", "b", ""}}, "to"); len(got) != 2 {
		t.Fatalf("dizide boşlar atlanmalı, %d", len(got))
	}
	if got := inputStrings(map[string]any{}, "to"); len(got) != 0 {
		t.Fatalf("eksik alan boş dönmeli, %d", len(got))
	}
}

// TestOptionsOf: düz metin ve nesne biçimleri kabul edilir.
func TestOptionsOf(t *testing.T) {
	got := optionsOf([]any{"Evet", "Hayır"})
	if len(got) != 2 || got[0].Text != "Evet" {
		t.Fatalf("düz metin seçenekler okunmalı: %+v", got)
	}
	objs := optionsOf([]any{map[string]any{"text": "Evet", "value": "onay"}})
	if len(objs) != 1 || objs[0].Value != "onay" {
		t.Fatalf("nesne seçeneği okunmalı: %+v", objs)
	}
	if optionsOf(nil) != nil {
		t.Fatal("eksik seçenek nil dönmeli")
	}
}
