package push

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	pushv1 "galaxy/backend/proto/push/v1"

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

func newTestService(t *testing.T) *Service {
	t.Helper()
	svc, err := NewService(ServiceConfig{
		FreshnessWindow: time.Minute,
		RingCapacity:    16,
		PerConnBuffer:   8,
	}, nil, nil)
	require.NoError(t, err)
	return svc
}

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

	svc := newTestService(t)
	t.Cleanup(svc.Close)

	userID := uuid.New()
	devID := uuid.New()
	payload := map[string]any{"game_id": "g1", "n": 7.0}
	require.NoError(t, svc.PublishClientEvent(context.Background(), userID, &devID, JSONEvent{EventKind: "lobby.invite.received", Payload: payload},"route-1", "req-1", "trace-1"))

	events, stale := svc.ring.since(0, time.Now())
	require.False(t, stale)
	require.Len(t, events, 1)

	ev := events[0]
	assert.Equal(t, formatCursor(1), ev.Cursor)
	ce := ev.GetClientEvent()
	require.NotNil(t, ce)
	assert.Equal(t, userID.String(), ce.UserId)
	assert.Equal(t, devID.String(), ce.DeviceSessionId)
	assert.Equal(t, "lobby.invite.received", ce.Kind)
	assert.Equal(t, "route-1", ce.EventId)
	assert.Equal(t, "req-1", ce.RequestId)
	assert.Equal(t, "trace-1", ce.TraceId)

	var got map[string]any
	require.NoError(t, json.Unmarshal(ce.Payload, &got))
	assert.Equal(t, "g1", got["game_id"])
	assert.EqualValues(t, 7.0, got["n"])
}

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

	svc := newTestService(t)
	t.Cleanup(svc.Close)

	userID := uuid.New()
	require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, JSONEvent{EventKind: "x"},"", "", ""))

	events, _ := svc.ring.since(0, time.Now())
	require.Len(t, events, 1)
	assert.Empty(t, events[0].GetClientEvent().DeviceSessionId)
}

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

	svc := newTestService(t)
	t.Cleanup(svc.Close)

	require.Error(t, svc.PublishClientEvent(context.Background(), uuid.Nil, nil, JSONEvent{EventKind: "k"},"", "", ""))
	require.Error(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, JSONEvent{EventKind: "  "},"", "", ""))
}

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

	svc := newTestService(t)
	t.Cleanup(svc.Close)

	userID := uuid.New()
	devID := uuid.New()
	svc.PublishSessionInvalidation(context.Background(), devID, userID, "auth.revoke_session")

	events, _ := svc.ring.since(0, time.Now())
	require.Len(t, events, 1)
	si := events[0].GetSessionInvalidation()
	require.NotNil(t, si)
	assert.Equal(t, userID.String(), si.UserId)
	assert.Equal(t, devID.String(), si.DeviceSessionId)
	assert.Equal(t, "auth.revoke_session", si.Reason)
}

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

	svc := newTestService(t)
	t.Cleanup(svc.Close)

	userID := uuid.New()
	svc.PublishSessionInvalidation(context.Background(), uuid.Nil, userID, "auth.revoke_all_for_user")

	events, _ := svc.ring.since(0, time.Now())
	require.Len(t, events, 1)
	si := events[0].GetSessionInvalidation()
	assert.Empty(t, si.DeviceSessionId)
	assert.Equal(t, userID.String(), si.UserId)
}

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

	svc := newTestService(t)
	t.Cleanup(svc.Close)

	userID := uuid.New()
	for range 5 {
		require.NoError(t, svc.PublishClientEvent(context.Background(), userID, nil, JSONEvent{EventKind: "k"},"", "", ""))
	}
	events, _ := svc.ring.since(0, time.Now())
	require.Len(t, events, 5)
	for i, ev := range events {
		assert.Equal(t, formatCursor(uint64(i+1)), ev.Cursor)
	}
}

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

	svc := newTestService(t)
	svc.Close()
	require.NoError(t, svc.PublishClientEvent(context.Background(), uuid.New(), nil, JSONEvent{EventKind: "k"},"", "", ""))
	events, _ := svc.ring.since(0, time.Now())
	assert.Empty(t, events)
}

// Compile-time interface checks: Service must satisfy the publisher
// contracts that internal/auth and internal/notification import.
var (
	_ pushClientEventPublisher        = (*Service)(nil)
	_ pushSessionInvalidationEmitter  = (*Service)(nil)
)

type pushClientEventPublisher interface {
	PublishClientEvent(ctx context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, event Event, eventID, requestID, traceID string) error
}

type pushSessionInvalidationEmitter interface {
	PublishSessionInvalidation(ctx context.Context, deviceSessionID, userID uuid.UUID, reason string)
}

// Make sure the publisher satisfies pushv1.PushServer at the type level.
var _ pushv1.PushServer = (*Service)(nil)
