package redis

import (
	"context"
	"reflect"
	"testing"
	"time"

	"github.com/go-kratos/kratos/pkg/container/pool"
	xtime "github.com/go-kratos/kratos/pkg/time"
)

func TestRedis(t *testing.T) {
	testSet(t, testPool)
	testSend(t, testPool)
	testGet(t, testPool)
	testErr(t, testPool)
	if err := testPool.Close(); err != nil {
		t.Errorf("redis: close error(%v)", err)
	}
	conn, err := NewConn(testConfig)
	if err != nil {
		t.Errorf("redis: new conn error(%v)", err)
	}
	if err := conn.Close(); err != nil {
		t.Errorf("redis: close error(%v)", err)
	}
}

func testSet(t *testing.T, p *Pool) {
	var (
		key   = "test"
		value = "test"
		conn  = p.Get(context.TODO())
	)
	defer conn.Close()
	if reply, err := conn.Do("set", key, value); err != nil {
		t.Errorf("redis: conn.Do(SET, %s, %s) error(%v)", key, value, err)
	} else {
		t.Logf("redis: set status: %s", reply)
	}
}

func testSend(t *testing.T, p *Pool) {
	var (
		key    = "test"
		value  = "test"
		expire = 1000
		conn   = p.Get(context.TODO())
	)
	defer conn.Close()
	if err := conn.Send("SET", key, value); err != nil {
		t.Errorf("redis: conn.Send(SET, %s, %s) error(%v)", key, value, err)
	}
	if err := conn.Send("EXPIRE", key, expire); err != nil {
		t.Errorf("redis: conn.Send(EXPIRE key(%s) expire(%d)) error(%v)", key, expire, err)
	}
	if err := conn.Flush(); err != nil {
		t.Errorf("redis: conn.Flush error(%v)", err)
	}
	for i := 0; i < 2; i++ {
		if _, err := conn.Receive(); err != nil {
			t.Errorf("redis: conn.Receive error(%v)", err)
			return
		}
	}
	t.Logf("redis: set value: %s", value)
}

func testGet(t *testing.T, p *Pool) {
	var (
		key  = "test"
		conn = p.Get(context.TODO())
	)
	defer conn.Close()
	if reply, err := conn.Do("GET", key); err != nil {
		t.Errorf("redis: conn.Do(GET, %s) error(%v)", key, err)
	} else {
		t.Logf("redis: get value: %s", reply)
	}
}

func testErr(t *testing.T, p *Pool) {
	conn := p.Get(context.TODO())
	if err := conn.Close(); err != nil {
		t.Errorf("redis: close error(%v)", err)
	}
	if err := conn.Err(); err == nil {
		t.Errorf("redis: err not nil")
	} else {
		t.Logf("redis: err: %v", err)
	}
}

func BenchmarkRedis(b *testing.B) {
	conf := &Config{
		Name:         "test",
		Proto:        "tcp",
		Addr:         testRedisAddr,
		DialTimeout:  xtime.Duration(time.Second),
		ReadTimeout:  xtime.Duration(time.Second),
		WriteTimeout: xtime.Duration(time.Second),
	}
	conf.Config = &pool.Config{
		Active:      10,
		Idle:        5,
		IdleTimeout: xtime.Duration(90 * time.Second),
	}
	benchmarkPool := NewPool(conf)

	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			conn := benchmarkPool.Get(context.TODO())
			if err := conn.Close(); err != nil {
				b.Errorf("redis: close error(%v)", err)
			}
		}
	})
	if err := benchmarkPool.Close(); err != nil {
		b.Errorf("redis: close error(%v)", err)
	}
}

var testRedisCommands = []struct {
	args     []interface{}
	expected interface{}
}{
	{
		[]interface{}{"PING"},
		"PONG",
	},
	{
		[]interface{}{"SET", "foo", "bar"},
		"OK",
	},
	{
		[]interface{}{"GET", "foo"},
		[]byte("bar"),
	},
	{
		[]interface{}{"GET", "nokey"},
		nil,
	},
	{
		[]interface{}{"MGET", "nokey", "foo"},
		[]interface{}{nil, []byte("bar")},
	},
	{
		[]interface{}{"INCR", "mycounter"},
		int64(1),
	},
	{
		[]interface{}{"LPUSH", "mylist", "foo"},
		int64(1),
	},
	{
		[]interface{}{"LPUSH", "mylist", "bar"},
		int64(2),
	},
	{
		[]interface{}{"LRANGE", "mylist", 0, -1},
		[]interface{}{[]byte("bar"), []byte("foo")},
	},
}

func TestNewRedis(t *testing.T) {
	type args struct {
		c       *Config
		options []DialOption
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			"new_redis",
			args{
				testConfig,
				make([]DialOption, 0),
			},
			false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := NewRedis(tt.args.c, tt.args.options...)
			if r == nil {
				t.Errorf("NewRedis() error, got nil")
				return
			}
			err := r.Close()
			if err != nil {
				t.Errorf("Close() error %v", err)
			}
		})
	}
}

func TestRedis_Do(t *testing.T) {
	r := NewRedis(testConfig)
	r.Do(context.TODO(), "FLUSHDB")

	for _, cmd := range testRedisCommands {
		actual, err := r.Do(context.TODO(), cmd.args[0].(string), cmd.args[1:]...)
		if err != nil {
			t.Errorf("Do(%v) returned error %v", cmd.args, err)
			continue
		}
		if !reflect.DeepEqual(actual, cmd.expected) {
			t.Errorf("Do(%v) = %v, want %v", cmd.args, actual, cmd.expected)
		}
	}
	err := r.Close()
	if err != nil {
		t.Errorf("Close() error %v", err)
	}
}

func TestRedis_Conn(t *testing.T) {
	type args struct {
		ctx context.Context
	}
	tests := []struct {
		name    string
		p       *Redis
		args    args
		wantErr bool
		g       int
		c       int
	}{
		{
			"Close",
			NewRedis(&Config{
				Config: &pool.Config{
					Active: 1,
					Idle:   1,
				},
				Name:         "test_get",
				Proto:        "tcp",
				Addr:         testRedisAddr,
				DialTimeout:  xtime.Duration(time.Second),
				ReadTimeout:  xtime.Duration(time.Second),
				WriteTimeout: xtime.Duration(time.Second),
			}),
			args{context.TODO()},
			false,
			3,
			3,
		},
		{
			"CloseExceededPoolSize",
			NewRedis(&Config{
				Config: &pool.Config{
					Active: 1,
					Idle:   1,
				},
				Name:         "test_get_out",
				Proto:        "tcp",
				Addr:         testRedisAddr,
				DialTimeout:  xtime.Duration(time.Second),
				ReadTimeout:  xtime.Duration(time.Second),
				WriteTimeout: xtime.Duration(time.Second),
			}),
			args{context.TODO()},
			true,
			5,
			3,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			for i := 1; i <= tt.g; i++ {
				got := tt.p.Conn(tt.args.ctx)
				if err := got.Close(); err != nil {
					if !tt.wantErr {
						t.Error(err)
					}
				}
				if i <= tt.c {
					if err := got.Close(); err != nil {
						t.Error(err)
					}
				}
			}
		})
	}
}

func BenchmarkRedisDoPing(b *testing.B) {
	r := NewRedis(testConfig)
	defer r.Close()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		if _, err := r.Do(context.Background(), "PING"); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkRedisDoSET(b *testing.B) {
	r := NewRedis(testConfig)
	defer r.Close()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		if _, err := r.Do(context.Background(), "SET", "a", "b"); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkRedisDoGET(b *testing.B) {
	r := NewRedis(testConfig)
	defer r.Close()
	r.Do(context.Background(), "SET", "a", "b")
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		if _, err := r.Do(context.Background(), "GET", "b"); err != nil {
			b.Fatal(err)
		}
	}
}
