package push

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestHubDeliversSessionTargetedEvent(t *testing.T) {
	t.Parallel()

	hub := NewHub(4)
	target, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-1",
	})
	require.NoError(t, err)
	otherSession, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-2",
	})
	require.NoError(t, err)
	unrelatedUser, err := hub.Register(StreamBinding{
		UserID:          "user-999",
		DeviceSessionID: "device-session-3",
	})
	require.NoError(t, err)

	hub.Publish(Event{
		UserID:          "user-123",
		DeviceSessionID: "device-session-1",
		EventType:       "fleet.updated",
		EventID:         "event-1",
		PayloadBytes:    []byte("payload-1"),
	})

	assertEvent(t, target.Events(), Event{
		UserID:          "user-123",
		DeviceSessionID: "device-session-1",
		EventType:       "fleet.updated",
		EventID:         "event-1",
		PayloadBytes:    []byte("payload-1"),
	})
	assertNoEvent(t, otherSession.Events())
	assertNoEvent(t, unrelatedUser.Events())
}

func TestHubDeliversUserTargetedEventToAllUserSessions(t *testing.T) {
	t.Parallel()

	hub := NewHub(4)
	first, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-1",
	})
	require.NoError(t, err)
	second, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-2",
	})
	require.NoError(t, err)
	unrelated, err := hub.Register(StreamBinding{
		UserID:          "user-999",
		DeviceSessionID: "device-session-3",
	})
	require.NoError(t, err)

	hub.Publish(Event{
		UserID:       "user-123",
		EventType:    "fleet.updated",
		EventID:      "event-1",
		PayloadBytes: []byte("payload-1"),
		RequestID:    "request-1",
		TraceID:      "trace-1",
	})

	want := Event{
		UserID:       "user-123",
		EventType:    "fleet.updated",
		EventID:      "event-1",
		PayloadBytes: []byte("payload-1"),
		RequestID:    "request-1",
		TraceID:      "trace-1",
	}
	assertEvent(t, first.Events(), want)
	assertEvent(t, second.Events(), want)
	assertNoEvent(t, unrelated.Events())
}

func TestSubscriptionCloseUnregistersStream(t *testing.T) {
	t.Parallel()

	hub := NewHub(4)
	subscription, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-1",
	})
	require.NoError(t, err)

	subscription.Close()

	select {
	case <-subscription.Done():
	case <-time.After(time.Second):
		require.FailNow(t, "subscription did not close")
	}

	hub.Publish(Event{
		UserID:       "user-123",
		EventType:    "fleet.updated",
		EventID:      "event-1",
		PayloadBytes: []byte("payload-1"),
	})

	assertNoEvent(t, subscription.Events())
	assert.NoError(t, subscription.Err())
}

func TestHubOverflowClosesOnlySlowSubscription(t *testing.T) {
	t.Parallel()

	hub := NewHub(1)
	slow, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-1",
	})
	require.NoError(t, err)
	fast, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-2",
	})
	require.NoError(t, err)

	hub.Publish(Event{
		UserID:       "user-123",
		EventType:    "fleet.updated",
		EventID:      "event-1",
		PayloadBytes: []byte("payload-1"),
	})
	assertEvent(t, fast.Events(), Event{
		UserID:       "user-123",
		EventType:    "fleet.updated",
		EventID:      "event-1",
		PayloadBytes: []byte("payload-1"),
	})

	hub.Publish(Event{
		UserID:       "user-123",
		EventType:    "fleet.updated",
		EventID:      "event-2",
		PayloadBytes: []byte("payload-2"),
	})

	select {
	case <-slow.Done():
	case <-time.After(time.Second):
		require.FailNow(t, "slow subscription did not close after overflow")
	}

	assert.ErrorIs(t, slow.Err(), ErrSubscriptionOverflow)
	assertEvent(t, fast.Events(), Event{
		UserID:       "user-123",
		EventType:    "fleet.updated",
		EventID:      "event-2",
		PayloadBytes: []byte("payload-2"),
	})
}

func TestHubRevokeDeviceSessionClosesOnlyMatchingSubscriptions(t *testing.T) {
	t.Parallel()

	hub := NewHub(4)
	targetOne, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-1",
	})
	require.NoError(t, err)
	targetTwo, err := hub.Register(StreamBinding{
		UserID:          "user-456",
		DeviceSessionID: "device-session-1",
	})
	require.NoError(t, err)
	otherSession, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-2",
	})
	require.NoError(t, err)

	hub.RevokeDeviceSession("device-session-1")

	select {
	case <-targetOne.Done():
	case <-time.After(time.Second):
		require.FailNow(t, "first matching subscription did not close after revoke")
	}

	select {
	case <-targetTwo.Done():
	case <-time.After(time.Second):
		require.FailNow(t, "second matching subscription did not close after revoke")
	}

	assert.ErrorIs(t, targetOne.Err(), ErrSubscriptionRevoked)
	assert.ErrorIs(t, targetTwo.Err(), ErrSubscriptionRevoked)

	select {
	case <-otherSession.Done():
		require.FailNow(t, "unrelated session subscription closed after revoke")
	case <-time.After(50 * time.Millisecond):
	}

	hub.Publish(Event{
		UserID:          "user-123",
		DeviceSessionID: "device-session-2",
		EventType:       "fleet.updated",
		EventID:         "event-1",
		PayloadBytes:    []byte("payload-1"),
	})

	assertEvent(t, otherSession.Events(), Event{
		UserID:          "user-123",
		DeviceSessionID: "device-session-2",
		EventType:       "fleet.updated",
		EventID:         "event-1",
		PayloadBytes:    []byte("payload-1"),
	})
}

func TestHubRevokeDeviceSessionIgnoresUnknownOrEmptySession(t *testing.T) {
	t.Parallel()

	hub := NewHub(4)
	subscription, err := hub.Register(StreamBinding{
		UserID:          "user-123",
		DeviceSessionID: "device-session-1",
	})
	require.NoError(t, err)

	hub.RevokeDeviceSession("")
	hub.RevokeDeviceSession("missing-session")

	select {
	case <-subscription.Done():
		require.FailNow(t, "subscription closed for empty or unknown session revoke")
	case <-time.After(50 * time.Millisecond):
	}
}

func assertEvent(t *testing.T, eventCh <-chan Event, want Event) {
	t.Helper()

	select {
	case got := <-eventCh:
		assert.Equal(t, want, got)
	case <-time.After(time.Second):
		require.FailNow(t, "event was not delivered")
	}
}

func assertNoEvent(t *testing.T, eventCh <-chan Event) {
	t.Helper()

	select {
	case got := <-eventCh:
		require.FailNowf(t, "unexpected event delivered", "%+v", got)
	case <-time.After(50 * time.Millisecond):
	}
}
