package eventbus

import (
	"sync"
	"testing"
	"time"
)

func TestPublish_DeliversToSubscriber(t *testing.T) {
	t.Parallel()
	b := New()
	ch, unsub := b.Subscribe(4)
	defer unsub()

	b.Publish(Event{Kind: "test.event", UserID: "u1", Data: map[string]any{"x": 1}})

	select {
	case got := <-ch:
		if got.Kind != "test.event" || got.UserID != "u1" {
			t.Fatalf("unexpected event: %+v", got)
		}
		if got.Data["x"] != 1 {
			t.Fatalf("payload mismatch: %+v", got.Data)
		}
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for event")
	}
}

func TestPublish_FanOutToMultipleSubscribers(t *testing.T) {
	t.Parallel()
	b := New()
	ch1, unsub1 := b.Subscribe(4)
	ch2, unsub2 := b.Subscribe(4)
	defer unsub1()
	defer unsub2()

	b.Publish(Event{Kind: "broadcast"})

	for i, ch := range []<-chan Event{ch1, ch2} {
		select {
		case got := <-ch:
			if got.Kind != "broadcast" {
				t.Fatalf("ch%d: kind=%q", i, got.Kind)
			}
		case <-time.After(time.Second):
			t.Fatalf("ch%d: timed out", i)
		}
	}
}

func TestPublish_DropsWhenSubscriberBufferFull(t *testing.T) {
	t.Parallel()
	b := New()
	_, unsub := b.Subscribe(2) // tiny buffer
	defer unsub()

	// Without draining the channel, push more events than the buffer holds.
	// Drops are silent; the assertion is that Publish does not block.
	done := make(chan struct{})
	go func() {
		for i := 0; i < 100; i++ {
			b.Publish(Event{Kind: "flood"})
		}
		close(done)
	}()

	select {
	case <-done:
		// Good — Publish returned without blocking on the laggard.
	case <-time.After(2 * time.Second):
		t.Fatal("Publish blocked on a slow subscriber")
	}
}

func TestUnsubscribe_RemovesFromBusAndClosesChannel(t *testing.T) {
	t.Parallel()
	b := New()
	ch, unsub := b.Subscribe(4)

	if got := b.SubscriberCount(); got != 1 {
		t.Fatalf("after Subscribe: SubscriberCount=%d, want 1", got)
	}

	unsub()

	if got := b.SubscriberCount(); got != 0 {
		t.Fatalf("after unsub: SubscriberCount=%d, want 0", got)
	}

	// Channel is closed: receive returns zero value with ok=false.
	if _, ok := <-ch; ok {
		t.Fatal("expected channel to be closed after unsubscribe")
	}
}

func TestUnsubscribe_Idempotent(t *testing.T) {
	t.Parallel()
	b := New()
	_, unsub := b.Subscribe(4)
	unsub()
	unsub() // must not panic / double-close
	if got := b.SubscriberCount(); got != 0 {
		t.Fatalf("SubscriberCount=%d, want 0", got)
	}
}

func TestPublish_ConcurrentWritersAndSubscribers(t *testing.T) {
	t.Parallel()
	b := New()

	const (
		subs       = 8
		perWriter  = 50
		writers    = 4
		bufferSize = 100
	)

	var subWG sync.WaitGroup
	subWG.Add(subs)
	stops := make([]func(), 0, subs)
	for i := 0; i < subs; i++ {
		ch, unsub := b.Subscribe(bufferSize)
		stops = append(stops, unsub)
		go func() {
			defer subWG.Done()
			for range ch {
				// Drain until channel closes.
			}
		}()
	}

	var pubWG sync.WaitGroup
	pubWG.Add(writers)
	for w := 0; w < writers; w++ {
		go func() {
			defer pubWG.Done()
			for i := 0; i < perWriter; i++ {
				b.Publish(Event{Kind: "concurrent"})
			}
		}()
	}
	pubWG.Wait()

	// Tear down subscribers — channels close, goroutines exit.
	for _, stop := range stops {
		stop()
	}
	subWG.Wait()
}
