package redis

import (
	"net"
	"testing"
	"time"

	"github.com/gomodule/redigo/redis"
	"github.com/rafaeljusto/redigomock"
	"github.com/stretchr/testify/require"

	"gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
	"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
)

func mockRedisServer(t *testing.T, connectReceived *bool) string {
	ln, err := net.Listen("tcp", "127.0.0.1:0")

	require.Nil(t, err)

	go func() {
		defer ln.Close()
		conn, err := ln.Accept()
		require.Nil(t, err)
		*connectReceived = true
		conn.Write([]byte("OK\n"))
	}()

	return ln.Addr().String()
}

// Setup a MockPool for Redis
//
// Returns a teardown-function and the mock-connection
func setupMockPool() (*redigomock.Conn, func()) {
	conn := redigomock.NewConn()
	cfg := &config.RedisConfig{URL: config.TomlURL{}}
	Configure(cfg, func(_ *config.RedisConfig, _ bool) func() (redis.Conn, error) {
		return func() (redis.Conn, error) {
			return conn, nil
		}
	})
	return conn, func() {
		pool = nil
	}
}

func TestDefaultDialFunc(t *testing.T) {
	testCases := []struct {
		scheme string
	}{
		{
			scheme: "tcp",
		},
		{
			scheme: "redis",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.scheme, func(t *testing.T) {
			connectReceived := false
			a := mockRedisServer(t, &connectReceived)

			parsedURL := helper.URLMustParse(tc.scheme + "://" + a)
			cfg := &config.RedisConfig{URL: config.TomlURL{URL: *parsedURL}}

			dialer := DefaultDialFunc(cfg, true)
			conn, err := dialer()

			require.Nil(t, err)
			conn.Receive()

			require.True(t, connectReceived)
		})
	}
}

func TestConfigureNoConfig(t *testing.T) {
	pool = nil
	Configure(nil, nil)
	require.Nil(t, pool, "Pool should be nil")
}

func TestConfigureMinimalConfig(t *testing.T) {
	cfg := &config.RedisConfig{URL: config.TomlURL{}, Password: ""}
	Configure(cfg, DefaultDialFunc)

	require.NotNil(t, pool, "Pool should not be nil")
	require.Equal(t, 1, pool.MaxIdle)
	require.Equal(t, 1, pool.MaxActive)
	require.Equal(t, 3*time.Minute, pool.IdleTimeout)

	pool = nil
}

func TestConfigureFullConfig(t *testing.T) {
	i, a := 4, 10
	r := config.TomlDuration{Duration: 3}
	cfg := &config.RedisConfig{
		URL:         config.TomlURL{},
		Password:    "",
		MaxIdle:     &i,
		MaxActive:   &a,
		ReadTimeout: &r,
	}
	Configure(cfg, DefaultDialFunc)

	require.NotNil(t, pool, "Pool should not be nil")
	require.Equal(t, i, pool.MaxIdle)
	require.Equal(t, a, pool.MaxActive)
	require.Equal(t, 3*time.Minute, pool.IdleTimeout)

	pool = nil
}

func TestGetConnFail(t *testing.T) {
	conn := Get()
	require.Nil(t, conn, "Expected `conn` to be nil")
}

func TestGetConnPass(t *testing.T) {
	_, teardown := setupMockPool()
	defer teardown()
	conn := Get()
	require.NotNil(t, conn, "Expected `conn` to be non-nil")
}

func TestGetStringPass(t *testing.T) {
	conn, teardown := setupMockPool()
	defer teardown()
	conn.Command("GET", "foobar").Expect("baz")
	str, err := GetString("foobar")

	require.NoError(t, err, "Expected `err` to be nil")
	var value string
	require.IsType(t, value, str, "Expected value to be a string")
	require.Equal(t, "baz", str, "Expected it to be equal")
}

func TestGetStringFail(t *testing.T) {
	_, err := GetString("foobar")
	require.Error(t, err, "Expected error when not connected to redis")
}

func TestSentinelConnNoSentinel(t *testing.T) {
	s := sentinelConn("", []config.TomlURL{})

	require.Nil(t, s, "Sentinel without urls should return nil")
}

func TestSentinelConnDialURL(t *testing.T) {
	testCases := []struct {
		scheme string
	}{
		{
			scheme: "tcp",
		},
		{
			scheme: "redis",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.scheme, func(t *testing.T) {
			connectReceived := false
			a := mockRedisServer(t, &connectReceived)

			addrs := []string{tc.scheme + "://" + a}
			var sentinelUrls []config.TomlURL

			for _, a := range addrs {
				parsedURL := helper.URLMustParse(a)
				sentinelUrls = append(sentinelUrls, config.TomlURL{URL: *parsedURL})
			}

			s := sentinelConn("foobar", sentinelUrls)
			require.Equal(t, len(addrs), len(s.Addrs))

			for i := range addrs {
				require.Equal(t, addrs[i], s.Addrs[i])
			}

			conn, err := s.Dial(s.Addrs[0])

			require.Nil(t, err)
			conn.Receive()

			require.True(t, connectReceived)
		})
	}
}

func TestSentinelConnTwoURLs(t *testing.T) {
	addrs := []string{"tcp://10.0.0.1:12345", "tcp://10.0.0.2:12345"}
	var sentinelUrls []config.TomlURL

	for _, a := range addrs {
		parsedURL := helper.URLMustParse(a)
		sentinelUrls = append(sentinelUrls, config.TomlURL{URL: *parsedURL})
	}

	s := sentinelConn("foobar", sentinelUrls)
	require.Equal(t, len(addrs), len(s.Addrs))

	for i := range addrs {
		require.Equal(t, addrs[i], s.Addrs[i])
	}
}

func TestDialOptionsBuildersPassword(t *testing.T) {
	dopts := dialOptionsBuilder(&config.RedisConfig{Password: "foo"}, false)
	require.Equal(t, 1, len(dopts))
}

func TestDialOptionsBuildersSetTimeouts(t *testing.T) {
	dopts := dialOptionsBuilder(nil, true)
	require.Equal(t, 2, len(dopts))
}

func TestDialOptionsBuildersSetTimeoutsConfig(t *testing.T) {
	cfg := &config.RedisConfig{
		ReadTimeout:  &config.TomlDuration{Duration: time.Second * time.Duration(15)},
		WriteTimeout: &config.TomlDuration{Duration: time.Second * time.Duration(15)},
	}
	dopts := dialOptionsBuilder(cfg, true)
	require.Equal(t, 2, len(dopts))
}

func TestDialOptionsBuildersSelectDB(t *testing.T) {
	db := 3
	dopts := dialOptionsBuilder(&config.RedisConfig{DB: &db}, false)
	require.Equal(t, 1, len(dopts))
}
