package ws

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"testing"
	"time"

	"fiko-ai/internal/agent"
	"fiko-ai/internal/notify"
	"fiko-ai/internal/protocol"
)

// notify_test.go: kullanıcılar arası bildirimin WS köprüsünü sınar.
// Kapsam: anlık iletim, bağlantı kurulunca geri yükleme, YETKİSİZ yanıtın
// reddi, zil sayacı tazeleme. Asıl amaç "başkasının sorusuna cevap verilemez"
// güvencesini uçtan uca kanıtlamaktır.

// notifyLog test log'unu çöpe atar.
func notifyLog() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }

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

func (n notifyUsersStub) DisplayName(_ context.Context, id string) (string, error) {
	return n.names[id], nil
}

func testNames() map[string]string { return map[string]string{"ali": "Ali", "veli": "Veli"} }

// bridgeOf, notify.Service'i ws.notifyBridge'e çevirir (main.go bağlantısının testi).
func bridgeOf(svc *notify.Service) notifyBridge {
	return notifyBridge{
		List: func(ctx context.Context, userID, box string) ([]notify.Message, error) {
			items, _, err := svc.List(ctx, userID, notify.Filter{Box: box, Limit: 50})
			return items, err
		},
		MarkRead: func(ctx context.Context, userID, id string) (notify.Message, error) {
			return svc.MarkRead(ctx, userID, id)
		},
		Answer: func(ctx context.Context, userID, qid, ans string) error {
			_, _, err := svc.Answer(ctx, userID, qid, ans)
			return err
		},
		Counts: func(ctx context.Context, userID string) (int, int) {
			return svc.Unread(ctx, userID), len(svc.PendingQuestions(userID))
		},
		DisplayName: func(_ context.Context, userID string) string {
			return testNames()[userID]
		},
	}
}

func newNotifyService() *notify.Service {
	return notify.NewService(notify.Options{
		Repo:   notify.NewMemoryRepository(),
		Users:  notifyUsersStub{names: testNames()},
		Limits: notify.DefaultLimits(),
		Logger: notifyLog(),
	})
}

// newNotifyHub, verilen servisle bağlı hub kurar. svc nil ise köprü YOK (kapalı akış).
func newNotifyHub(t *testing.T, svc *notify.Service) *Hub {
	t.Helper()
	log := notifyLog()
	hub := NewHub(agent.New(nil, nil, nil, nil, log, agent.Config{}), log, Options{})
	if svc != nil {
		hub.SetNotify(bridgeOf(svc))
	}
	return hub
}

// newServedHub, hub + notify.Sink bağlar ve BİR KULLANICI bağlantısı açar.
// Dönen yapı ikinci kullanıcıyı da bağlamak için yardımcı sunar.
type servedHub struct {
	hub  *Hub
	svc  *notify.Service
	log  *slog.Logger
	conn *fakeConn
}

func newServedHub(t *testing.T, userID string) *servedHub {
	t.Helper()
	svc := newNotifyService()
	log := notifyLog()
	hub := NewHub(agent.New(nil, nil, nil, nil, log, agent.Config{}), log, Options{})
	hub.SetNotify(bridgeOf(svc))
	// Sink: anlık bildirimler WS'e gitsin (main.go bağlantısının testi).
	svc.SetSink(hub)

	sh := &servedHub{hub: hub, svc: svc, log: log}
	sh.serve(t, userID)
	return sh
}

// serve, yeni bir kullanıcı bağlantısı açar (çoklu sekme testi için).
func (s *servedHub) serve(t *testing.T, userID string) *fakeConn {
	t.Helper()
	conn := newFakeConn()
	done := make(chan struct{})
	go func() { defer close(done); s.hub.Serve(context.Background(), conn, userID) }()
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		_ = s.hub.Close(ctx)
		select {
		case <-done:
		case <-time.After(3 * time.Second):
		}
	})
	s.conn = conn
	return conn
}

// serve, bağlantıyı userID adıyla ayağa kaldırır ve temizlik kaydeder.
func serve(t *testing.T, hub *Hub, userID string) *fakeConn {
	t.Helper()
	conn := newFakeConn()
	done := make(chan struct{})
	go func() { defer close(done); hub.Serve(context.Background(), conn, userID) }()
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		_ = hub.Close(ctx)
		select {
		case <-done:
		case <-time.After(3 * time.Second):
		}
	})
	return conn
}

func mustJSON(t *testing.T, v any) json.RawMessage {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatal(err)
	}
	return b
}

// sendInbound, istemci olayı gönderir.
func sendInbound(t *testing.T, conn *fakeConn, typ protocol.ClientType, payload any) {
	t.Helper()
	b, err := json.Marshal(protocol.Inbound{Type: typ, Payload: mustJSON(t, payload)})
	if err != nil {
		t.Fatal(err)
	}
	select {
	case conn.in <- b:
	case <-time.After(3 * time.Second):
		t.Fatal("istemci olayı gönderilemedi")
	}
}

// drainUntil, istenen türdeki ilk olayın payload'ını harita olarak döndürür.
// errPayload true ise ServerError beklenir (want yok sayılır).
func drainUntil(t *testing.T, conn *fakeConn, want protocol.ServerType, errPayload bool) map[string]any {
	t.Helper()
	deadline := time.After(5 * time.Second)
	for {
		select {
		case b := <-conn.writes:
			var o protocol.Outbound
			if json.Unmarshal(b, &o) != nil {
				continue
			}
			if errPayload && o.Type != protocol.ServerError {
				continue
			}
			if !errPayload && o.Type != want {
				continue
			}
			raw, _ := json.Marshal(o.Payload)
			var m map[string]any
			_ = json.Unmarshal(raw, &m)
			return m
		case <-deadline:
			if errPayload {
				t.Fatal("hata olayı gelmedi")
			}
			t.Fatalf("%s olayı gelmedi", want)
		}
	}
}

// TestNotify_BaglantiKuruluncaGeriYuklenir: bildirim anlıktır; kullanıcı
// çevrimdışıyken gönderilirse bağlantı kurulunca GERİ YÜKLENMELİDİR.
// Yoksa kalıcı kayıt var olsa bile kullanıcı onu hiç göremez.
func TestNotify_BaglantiKuruluncaGeriYuklenir(t *testing.T) {
	svc := newNotifyService()
	// Bağlantı KURULMADAN önce gönder (kullanıcı çevrimdışı).
	if _, err := svc.Send(context.Background(), notify.SendInput{
		FromUserID: "ali", ToUserID: "veli", Text: "toplantı 15:00"}); err != nil {
		t.Fatal(err)
	}
	hub := newNotifyHub(t, svc)
	conn := serve(t, hub, "veli")

	p := drainUntil(t, conn, protocol.ServerNotifyReceived, false)
	if p["text"] != "toplantı 15:00" {
		t.Fatalf("kaçırılmış bildirim gelmedi: %v", p)
	}
	if p["kind"] != "info" {
		t.Fatalf("tür 'info' olmalı: %v", p["kind"])
	}
	// Zil sayacı da bağlantıda gönderilir.
	u := drainUntil(t, conn, protocol.ServerNotifyUnread, false)
	if u["unread"].(float64) < 1 {
		t.Fatalf("okunmamış sayacı 0 geldi: %v", u)
	}
}

// TestNotify_SoruGeriYuklenirVeCevaplanir: İKİ kullanıcı, tam akış.
// Ali sorar (Veli bağlı) → Veli cevaplar → Ali'nin bağlantısında notify.answer gelir
// ve cevaplayanın ADI çözülür ("Veli sana yanıt verdi").
func TestNotify_SoruGeriYuklenirVeCevaplanir(t *testing.T) {
	sh := newServedHub(t, "ali") // önce ALİ bağlanır (soran)
	aliConn := sh.conn
	q, err := sh.svc.Ask(context.Background(), notify.SendInput{
		FromUserID: "ali", ToUserID: "veli", Text: "toplantı 15:00'te mi?",
		Options: []notify.Option{{Text: "Evet"}, {Text: "Hayır"}}})
	if err != nil {
		t.Fatal(err)
	}
	// Veli bağlanır → bildirimi ANINDA alır (Sink).
	veliConn := sh.serve(t, "veli")
	p := drainUntil(t, veliConn, protocol.ServerNotifyReceived, false)
	if p["kind"] != "question" || p["questionId"] != q.ID {
		t.Fatalf("soru kimliği gelmeliydi: %v", p)
	}
	if opts, ok := p["options"].([]any); !ok || len(opts) != 2 {
		t.Fatalf("seçenekler gelmeliydi: %v", p["options"])
	}
	if p["pending"] != true {
		t.Fatalf("soru 'pending' olmalı: %v", p)
	}

	// Veli cevaplar → notify.answer ALİ'NİN bağlantısına düşer.
	if _, _, err := sh.svc.Answer(context.Background(), "veli", q.ID, "Evet"); err != nil {
		t.Fatal(err)
	}
	a := drainUntil(t, aliConn, protocol.ServerNotifyAnswered, false)
	if a["answer"] != "Evet" {
		t.Fatalf("yanıt içeriği taşınmadı: %v", a)
	}
	if a["fromName"] != "Veli" {
		t.Fatalf("cevaplayanın adı çözülmeliydi, %v geldi", a["fromName"])
	}
	if a["question"] != "toplantı 15:00'te mi?" {
		t.Fatalf("soru metni yanıtla birlikte gitmeliydi: %v", a)
	}
	if a["questionId"] != q.ID {
		t.Fatalf("yanıt soruyla eşleşmeliydi: %v", a)
	}
}

// TestNotify_AnlikBildirimAliciyaDuser: bildirim, çevrimiçi alıcıya ANINDA gider
// (replay beklemeden) ve zil sayacı tazelenir.
func TestNotify_AnlikBildirimAliciyaDuser(t *testing.T) {
	sh := newServedHub(t, "veli")
	if _, err := sh.svc.Send(context.Background(), notify.SendInput{
		FromUserID: "ali", ToUserID: "veli", Text: "acil toplantı"}); err != nil {
		t.Fatal(err)
	}
	p := drainUntil(t, sh.conn, protocol.ServerNotifyReceived, false)
	if p["text"] != "acil toplanti" && p["text"] != "acil toplantı" {
		t.Fatalf("bildirim içeriği bozuk: %v", p)
	}
	// Zil sayacı da anında tazelenir.
	u := drainUntil(t, sh.conn, protocol.ServerNotifyUnread, false)
	if u["unread"].(float64) < 1 {
		t.Fatalf("zil sayacı tazelenmedi: %v", u)
	}
}

// TestNotify_YetkisizYanitReddi: yanıtlanmış soru TEKRAR yanıtlanamaz.
func TestNotify_YetkisizYanitReddi(t *testing.T) {
	svc := newNotifyService()
	q, err := svc.Ask(context.Background(), notify.SendInput{
		FromUserID: "ali", ToUserID: "veli", Text: "toplantı 15:00'te mi?"})
	if err != nil {
		t.Fatal(err)
	}
	hub := newNotifyHub(t, svc)
	conn := serve(t, hub, "veli")

	// İlk yanıt başarılı → güncel sayaç gelir.
	sendInbound(t, conn, protocol.ClientNotifyAnswer,
		protocol.NotifyAnswerInput{QuestionID: q.ID, Answer: "Evet"})
	drainUntil(t, conn, protocol.ServerNotifyUnread, false)

	// İkinci yanıt reddedilmeli.
	sendInbound(t, conn, protocol.ClientNotifyAnswer,
		protocol.NotifyAnswerInput{QuestionID: q.ID, Answer: "tekrar"})
	drainUntil(t, conn, protocol.ServerError, true)
}

// TestNotify_BaslasininSorusunuCevaplayamaz: GÜVENLİK — soru VELİ'ye sorulmuştur,
// bağlantı ise ALİ'nindir. Ali bu soruyu yanıtlayamaz (WS katmanındaki izolasyon).
func TestNotify_BaslasininSorusunuCevaplayamaz(t *testing.T) {
	svc := newNotifyService()
	q, err := svc.Ask(context.Background(), notify.SendInput{
		FromUserID: "ali", ToUserID: "veli", Text: "Ali'ye özel soru"})
	if err != nil {
		t.Fatal(err)
	}
	hub := newNotifyHub(t, svc)
	conn := serve(t, hub, "ali") // bağlantı sahibi ALİ, sorunun alıcısı DEĞİL

	sendInbound(t, conn, protocol.ClientNotifyAnswer,
		protocol.NotifyAnswerInput{QuestionID: q.ID, Answer: "ele gecirildi"})
	drainUntil(t, conn, protocol.ServerError, true)

	// Soru HÂLÂ yanıtsız olmalı (denetim gerçekten çalıştı).
	items, _, _ := svc.List(context.Background(), "veli",
		notify.Filter{Box: notify.BoxInbox, Kind: notify.KindQuestion, PendingOnly: true})
	if len(items) != 1 {
		t.Fatalf("yetkisiz yanıt soruyu kaldırmış olmamalı, %d bekleyen soru var", len(items))
	}
}

// TestNotify_BosYanitReddi: boş yanıt reddedilir.
func TestNotify_BosYanitReddi(t *testing.T) {
	hub := newNotifyHub(t, newNotifyService())
	conn := serve(t, hub, "veli")
	sendInbound(t, conn, protocol.ClientNotifyAnswer,
		protocol.NotifyAnswerInput{QuestionID: "q1", Answer: "   "})
	drainUntil(t, conn, protocol.ServerError, true)
}

// TestNotify_AkisKapali: SetNotify çağrılmamışsa notify.answer hata döner.
func TestNotify_AkisKapali(t *testing.T) {
	hub := newNotifyHub(t, nil) // köprü YOK
	conn := serve(t, hub, "veli")
	sendInbound(t, conn, protocol.ClientNotifyAnswer,
		protocol.NotifyAnswerInput{QuestionID: "x", Answer: "evet"})
	drainUntil(t, conn, protocol.ServerError, true)
}

// TestNotify_EksikKopruKabulEdilmez: yarım köprü bildirimleri sessizce kaybettirirdi.
func TestNotify_EksikKopruKabulEdilmez(t *testing.T) {
	log := notifyLog()
	hub := NewHub(agent.New(nil, nil, nil, nil, log, agent.Config{}), log, Options{})

	hub.SetNotify(notifyBridge{
		List: func(context.Context, string, string) ([]notify.Message, error) { return nil, nil },
	})
	if hub.notify != nil {
		t.Fatal("eksik köprü kabul edilmemeli")
	}
	hub.SetNotify(bridgeOf(newNotifyService()))
	if hub.notify == nil {
		t.Fatal("tam köprü kabul edilmeli")
	}
	hub.SetNotify(notifyBridge{})
	if hub.notify != nil {
		t.Fatal("boş köprü kabul edilmemeli")
	}
}

// TestNotify_KopruHatasiPanikYapmaz: list/answer hata döndürse oturum çökmez.
func TestNotify_KopruHatasiPanikYapmaz(t *testing.T) {
	log := notifyLog()
	hub := NewHub(agent.New(nil, nil, nil, nil, log, agent.Config{}), log, Options{})
	hub.SetNotify(notifyBridge{
		List: func(context.Context, string, string) ([]notify.Message, error) {
			return nil, errors.New("depo patladı") // replay bunu loglar
		},
		Answer: func(context.Context, string, string, string) error {
			return errors.New("yazma hatası")
		},
		Counts: func(context.Context, string) (int, int) { return 0, 0 },
	})
	conn := serve(t, hub, "veli")

	sendInbound(t, conn, protocol.ClientNotifyAnswer,
		protocol.NotifyAnswerInput{QuestionID: "q1", Answer: "evet"})
	drainUntil(t, conn, protocol.ServerError, true)
}
