//go:build integration

package consuming

import (
	"bytes"
	"context"
	"errors"
	"sync/atomic"
	"testing"
	"time"

	"github.com/centrifugal/centrifugo/v6/internal/configtypes"
	"github.com/centrifugal/centrifugo/v6/internal/redisqueue"
	"github.com/centrifugal/centrifugo/v6/internal/redisshard"

	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/stretchr/testify/require"
)

// TestRedisStreamConsumer tests the Redis Stream consumer which is used to consume
// two different streams.
func TestRedisStreamConsumer(t *testing.T) {
	t.Parallel()
	done1 := make(chan struct{})
	done2 := make(chan struct{})

	dispatcher := &MockDispatcher{
		onDispatchCommand: func(ctx context.Context, method string, data []byte) error {
			if bytes.Equal(data, []byte("stream1")) {
				close(done1)
			} else if bytes.Equal(data, []byte("stream2")) {
				close(done2)
			}
			return nil
		},
	}

	streamName1 := "TEST_STREAM" + uuid.NewString()
	streamName2 := "TEST_STREAM" + uuid.NewString()

	cfg := RedisStreamConsumerConfig{
		Redis: configtypes.Redis{
			Address: []string{"localhost:6379"},
		},
		Streams:       []string{streamName1, streamName2},
		ConsumerGroup: uuid.NewString(),
		PayloadValue:  "payload",
		NumWorkers:    1,
	}

	consumer, err := NewRedisStreamConsumer(
		cfg, dispatcher, testCommon(prometheus.NewRegistry()))
	if err != nil {
		t.Fatalf("failed to create Redis Stream consumer: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	go func() {
		if err := consumer.Run(ctx); err != nil && err != context.Canceled {
			t.Errorf("consumer error: %v", err)
		}
	}()

	shards, err := redisshard.BuildRedisShards(cfg.Redis)
	require.NoError(t, err)
	require.Len(t, shards, 1)

	producer, err := redisqueue.NewProducer(shards[0], redisqueue.ProducerOptions{
		Stream: streamName1,
	})
	if err != nil {
		t.Fatalf("failed to create producer: %v", err)
	}
	err = producer.Enqueue(&redisqueue.Message{
		Values: map[string]string{
			"payload": "stream1",
		},
	})

	producer, err = redisqueue.NewProducer(shards[0], redisqueue.ProducerOptions{
		Stream: streamName2,
	})
	if err != nil {
		t.Fatalf("failed to create producer: %v", err)
	}
	err = producer.Enqueue(&redisqueue.Message{
		Values: map[string]string{
			"payload": "stream2",
		},
	})

	waitCh(t, done1, 5*time.Second, "timeout waiting for message processing")
	waitCh(t, done2, 5*time.Second, "timeout waiting for message processing")
}

// TestRedisStreamConsumer_ConcurrentConsumers tests that only one consumer processes
// the message when multiple consumers are running with the same consumer group.
func TestRedisStreamConsumer_ConcurrentConsumers(t *testing.T) {
	t.Parallel()
	done := make(chan string, 1)

	var receivedNum atomic.Int64

	dispatcher := func(name string) *MockDispatcher {
		return &MockDispatcher{
			onDispatchCommand: func(ctx context.Context, method string, data []byte) error {
				receivedNum.Add(1)
				close(done)
				return nil
			},
		}
	}

	streamName := "TEST_STREAM_" + uuid.NewString()
	consumerGroup := "group_" + uuid.NewString()

	cfg := RedisStreamConsumerConfig{
		Redis: configtypes.Redis{
			Address: []string{"localhost:6379"},
		},
		Streams:       []string{streamName},
		ConsumerGroup: consumerGroup,
		PayloadValue:  "payload",
		NumWorkers:    1,
	}

	// Build shards once to share producer
	shards, err := redisshard.BuildRedisShards(configtypes.Redis{
		Address: []string{"localhost:6379"},
	})
	require.NoError(t, err)

	// Start first consumer
	ctx1, cancel1 := context.WithCancel(context.Background())
	defer cancel1()
	consumer1, err := NewRedisStreamConsumer(cfg, dispatcher("consumer1"), testCommon(prometheus.NewRegistry()))
	require.NoError(t, err)
	go func() {
		if err := consumer1.Run(ctx1); err != nil && !errors.Is(err, context.Canceled) {
			t.Errorf("consumer1 error: %v", err)
		}
	}()

	// Start second consumer
	ctx2, cancel2 := context.WithCancel(context.Background())
	defer cancel2()
	consumer2, err := NewRedisStreamConsumer(cfg, dispatcher("consumer2"), testCommon(prometheus.NewRegistry()))
	require.NoError(t, err)
	go func() {
		if err := consumer2.Run(ctx2); err != nil && !errors.Is(err, context.Canceled) {
			t.Errorf("consumer2 error: %v", err)
		}
	}()

	// Send message.
	producer, err := redisqueue.NewProducer(shards[0], redisqueue.ProducerOptions{
		Stream: streamName,
	})
	require.NoError(t, err)
	err = producer.Enqueue(&redisqueue.Message{
		Values: map[string]string{
			"payload": "test",
		},
	})
	require.NoError(t, err)

	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("timeout waiting for message processing")
	}
	time.Sleep(500 * time.Millisecond) // prevent any second dispatch.
	require.Equal(t, int64(1), receivedNum.Load(), "only one consumer should process the message")
}
