package replay

import (
	"context"
	"errors"
	"net"
	"testing"
	"time"

	"galaxy/gateway/internal/config"

	"github.com/alicebob/miniredis/v2"
	"github.com/redis/go-redis/v9"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func newRedisClient(t *testing.T, addr string) *redis.Client {
	t.Helper()

	client := redis.NewClient(&redis.Options{
		Addr:            addr,
		Protocol:        2,
		DisableIdentity: true,
	})
	t.Cleanup(func() {
		assert.NoError(t, client.Close())
	})

	return client
}

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

	server := miniredis.RunT(t)
	client := newRedisClient(t, server.Addr())

	validCfg := config.ReplayRedisConfig{
		KeyPrefix:      "gateway:replay:",
		ReserveTimeout: 250 * time.Millisecond,
	}

	tests := []struct {
		name    string
		client  *redis.Client
		cfg     config.ReplayRedisConfig
		wantErr string
	}{
		{name: "valid config", client: client, cfg: validCfg},
		{name: "nil client", client: nil, cfg: validCfg, wantErr: "nil redis client"},
		{
			name:    "empty replay key prefix",
			client:  client,
			cfg:     config.ReplayRedisConfig{ReserveTimeout: 250 * time.Millisecond},
			wantErr: "replay key prefix must not be empty",
		},
		{
			name:    "non-positive reserve timeout",
			client:  client,
			cfg:     config.ReplayRedisConfig{KeyPrefix: "gateway:replay:"},
			wantErr: "reserve timeout must be positive",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			store, err := NewRedisStore(tt.client, tt.cfg)
			if tt.wantErr != "" {
				require.Error(t, err)
				require.ErrorContains(t, err, tt.wantErr)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, store)
		})
	}
}

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

	tests := []struct {
		name            string
		replayCfg       config.ReplayRedisConfig
		deviceSessionID string
		requestID       string
		ttl             time.Duration
		secondReserve   func(*testing.T, Store)
		wantErrIs       error
		wantErrText     string
	}{
		{
			name:            "first reservation succeeds",
			deviceSessionID: "device-session-123",
			requestID:       "request-123",
			ttl:             5 * time.Second,
		},
		{
			name:            "duplicate reservation is rejected",
			deviceSessionID: "device-session-123",
			requestID:       "request-123",
			ttl:             5 * time.Second,
			secondReserve: func(t *testing.T, store Store) {
				t.Helper()
				err := store.Reserve(context.Background(), "device-session-123", "request-123", 5*time.Second)
				require.ErrorIs(t, err, ErrDuplicate)
			},
		},
		{
			name:            "same request id in distinct sessions does not collide",
			deviceSessionID: "device-session-123",
			requestID:       "request-123",
			ttl:             5 * time.Second,
			secondReserve: func(t *testing.T, store Store) {
				t.Helper()
				require.NoError(t, store.Reserve(context.Background(), "device-session-456", "request-123", 5*time.Second))
			},
		},
		{
			name:        "empty device session id",
			requestID:   "request-123",
			ttl:         5 * time.Second,
			wantErrText: "empty device session id",
		},
		{
			name:            "empty request id",
			deviceSessionID: "device-session-123",
			ttl:             5 * time.Second,
			wantErrText:     "empty request id",
		},
		{
			name:            "non-positive ttl",
			deviceSessionID: "device-session-123",
			requestID:       "request-123",
			wantErrText:     "ttl must be positive",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			server := miniredis.RunT(t)
			store := newTestRedisStore(t, server, tt.replayCfg)

			err := store.Reserve(context.Background(), tt.deviceSessionID, tt.requestID, tt.ttl)
			if tt.wantErrIs != nil || tt.wantErrText != "" {
				require.Error(t, err)
				if tt.wantErrIs != nil {
					require.ErrorIs(t, err, tt.wantErrIs)
				}
				if tt.wantErrText != "" {
					require.ErrorContains(t, err, tt.wantErrText)
				}
				return
			}

			require.NoError(t, err)
			if tt.secondReserve != nil {
				tt.secondReserve(t, store)
			}
		})
	}
}

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

	client := newRedisClient(t, unusedTCPAddr(t))
	store, err := NewRedisStore(client, config.ReplayRedisConfig{
		KeyPrefix:      "gateway:replay:",
		ReserveTimeout: 100 * time.Millisecond,
	})
	require.NoError(t, err)

	err = store.Reserve(context.Background(), "device-session-123", "request-123", 5*time.Second)
	require.Error(t, err)
	assert.False(t, errors.Is(err, ErrDuplicate))
	assert.ErrorContains(t, err, "reserve replay request in redis")
}

func newTestRedisStore(t *testing.T, server *miniredis.Miniredis, replayCfg config.ReplayRedisConfig) *RedisStore {
	t.Helper()

	if replayCfg.KeyPrefix == "" {
		replayCfg.KeyPrefix = "gateway:replay:"
	}
	if replayCfg.ReserveTimeout == 0 {
		replayCfg.ReserveTimeout = 250 * time.Millisecond
	}

	store, err := NewRedisStore(newRedisClient(t, server.Addr()), replayCfg)
	require.NoError(t, err)

	return store
}

func unusedTCPAddr(t *testing.T) string {
	t.Helper()

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)

	addr := listener.Addr().String()
	require.NoError(t, listener.Close())

	return addr
}
