package events_test

import (
	"context"
	"sync"
	"testing"

	pushv1 "galaxy/backend/proto/push/v1"
	"galaxy/gateway/internal/events"
	"galaxy/gateway/internal/push"

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

type capturePublisher struct {
	mu     sync.Mutex
	events []push.Event
}

func (c *capturePublisher) Publish(event push.Event) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.events = append(c.events, event)
}

func (c *capturePublisher) snapshot() []push.Event {
	c.mu.Lock()
	defer c.mu.Unlock()
	out := make([]push.Event, len(c.events))
	copy(out, c.events)
	return out
}

type captureInvalidator struct {
	mu          sync.Mutex
	devices     []string
	users       []string
}

func (c *captureInvalidator) RevokeDeviceSession(id string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.devices = append(c.devices, id)
}

func (c *captureInvalidator) RevokeAllForUser(id string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.users = append(c.users, id)
}

func (c *captureInvalidator) snapshot() ([]string, []string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	d := append([]string(nil), c.devices...)
	u := append([]string(nil), c.users...)
	return d, u
}

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

	pub := &capturePublisher{}
	inv := &captureInvalidator{}
	disp := events.NewDispatcher(pub, inv, nil, nil)

	disp.Handle(context.Background(), &pushv1.PushEvent{
		Cursor: "00000000000000000001",
		Kind: &pushv1.PushEvent_ClientEvent{
			ClientEvent: &pushv1.ClientEvent{
				UserId:          "user-1",
				DeviceSessionId: "device-1",
				Kind:            "lobby.invite.received",
				Payload:         []byte(`{"x":1}`),
				EventId:         "route-1",
				RequestId:       "req-1",
				TraceId:         "trace-1",
			},
		},
	})

	got := pub.snapshot()
	require.Len(t, got, 1)
	assert.Equal(t, push.Event{
		UserID:          "user-1",
		DeviceSessionID: "device-1",
		EventType:       "lobby.invite.received",
		EventID:         "route-1",
		PayloadBytes:    []byte(`{"x":1}`),
		RequestID:       "req-1",
		TraceID:         "trace-1",
	}, got[0])

	devices, users := inv.snapshot()
	assert.Empty(t, devices)
	assert.Empty(t, users)
}

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

	pub := &capturePublisher{}
	disp := events.NewDispatcher(pub, &captureInvalidator{}, nil, nil)

	disp.Handle(context.Background(), &pushv1.PushEvent{
		Kind: &pushv1.PushEvent_ClientEvent{
			ClientEvent: &pushv1.ClientEvent{
				UserId: "user-1",
				Kind:   "lobby.invite.received",
			},
		},
	})

	assert.Empty(t, pub.snapshot())
}

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

	inv := &captureInvalidator{}
	disp := events.NewDispatcher(&capturePublisher{}, inv, nil, nil)

	disp.Handle(context.Background(), &pushv1.PushEvent{
		Kind: &pushv1.PushEvent_SessionInvalidation{
			SessionInvalidation: &pushv1.SessionInvalidation{
				UserId:          "user-1",
				DeviceSessionId: "device-1",
				Reason:          "auth.revoke_session",
			},
		},
	})

	devices, users := inv.snapshot()
	assert.Equal(t, []string{"device-1"}, devices)
	assert.Empty(t, users)
}

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

	inv := &captureInvalidator{}
	disp := events.NewDispatcher(&capturePublisher{}, inv, nil, nil)

	disp.Handle(context.Background(), &pushv1.PushEvent{
		Kind: &pushv1.PushEvent_SessionInvalidation{
			SessionInvalidation: &pushv1.SessionInvalidation{
				UserId: "user-1",
				Reason: "auth.revoke_all_for_user",
			},
		},
	})

	devices, users := inv.snapshot()
	assert.Empty(t, devices)
	assert.Equal(t, []string{"user-1"}, users)
}
