// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package v2discovery

import (
	"context"
	"errors"
	"math"
	"math/rand"
	"net/http"
	"net/url"
	"reflect"
	"sort"
	"strconv"
	"testing"
	"time"

	"go.etcd.io/etcd/client/pkg/v3/types"
	"go.etcd.io/etcd/client/v2"

	"github.com/jonboulle/clockwork"
	"go.uber.org/zap"
)

const (
	maxRetryInTest = 3
)

func TestNewProxyFuncUnset(t *testing.T) {
	pf, err := newProxyFunc(zap.NewExample(), "")
	if pf != nil {
		t.Fatal("unexpected non-nil proxyFunc")
	}
	if err != nil {
		t.Fatalf("unexpected non-nil err: %v", err)
	}
}

func TestNewProxyFuncBad(t *testing.T) {
	tests := []string{
		"%%",
		"http://foo.com/%1",
	}
	for i, in := range tests {
		pf, err := newProxyFunc(zap.NewExample(), in)
		if pf != nil {
			t.Errorf("#%d: unexpected non-nil proxyFunc", i)
		}
		if err == nil {
			t.Errorf("#%d: unexpected nil err", i)
		}
	}
}

func TestNewProxyFunc(t *testing.T) {
	tests := map[string]string{
		"bar.com":              "http://bar.com",
		"http://disco.foo.bar": "http://disco.foo.bar",
	}
	for in, w := range tests {
		pf, err := newProxyFunc(zap.NewExample(), in)
		if pf == nil {
			t.Errorf("%s: unexpected nil proxyFunc", in)
			continue
		}
		if err != nil {
			t.Errorf("%s: unexpected non-nil err: %v", in, err)
			continue
		}
		g, err := pf(&http.Request{})
		if err != nil {
			t.Errorf("%s: unexpected non-nil err: %v", in, err)
		}
		if g.String() != w {
			t.Errorf("%s: proxyURL=%q, want %q", in, g, w)
		}

	}
}

func TestCheckCluster(t *testing.T) {
	cluster := "/prefix/1000"
	self := "/1000/1"

	tests := []struct {
		nodes []*client.Node
		index uint64
		werr  error
		wsize int
	}{
		{
			// self is in the size range
			[]*client.Node{
				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
				{Key: "/1000/_config/"},
				{Key: self, CreatedIndex: 2},
				{Key: "/1000/2", CreatedIndex: 3},
				{Key: "/1000/3", CreatedIndex: 4},
				{Key: "/1000/4", CreatedIndex: 5},
			},
			5,
			nil,
			3,
		},
		{
			// self is in the size range
			[]*client.Node{
				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
				{Key: "/1000/_config/"},
				{Key: "/1000/2", CreatedIndex: 2},
				{Key: "/1000/3", CreatedIndex: 3},
				{Key: self, CreatedIndex: 4},
				{Key: "/1000/4", CreatedIndex: 5},
			},
			5,
			nil,
			3,
		},
		{
			// self is out of the size range
			[]*client.Node{
				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
				{Key: "/1000/_config/"},
				{Key: "/1000/2", CreatedIndex: 2},
				{Key: "/1000/3", CreatedIndex: 3},
				{Key: "/1000/4", CreatedIndex: 4},
				{Key: self, CreatedIndex: 5},
			},
			5,
			ErrFullCluster,
			3,
		},
		{
			// self is not in the cluster
			[]*client.Node{
				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
				{Key: "/1000/_config/"},
				{Key: "/1000/2", CreatedIndex: 2},
				{Key: "/1000/3", CreatedIndex: 3},
			},
			3,
			nil,
			3,
		},
		{
			[]*client.Node{
				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
				{Key: "/1000/_config/"},
				{Key: "/1000/2", CreatedIndex: 2},
				{Key: "/1000/3", CreatedIndex: 3},
				{Key: "/1000/4", CreatedIndex: 4},
			},
			3,
			ErrFullCluster,
			3,
		},
		{
			// bad size key
			[]*client.Node{
				{Key: "/1000/_config/size", Value: "bad", CreatedIndex: 1},
			},
			0,
			ErrBadSizeKey,
			0,
		},
		{
			// no size key
			[]*client.Node{},
			0,
			ErrSizeNotFound,
			0,
		},
	}

	for i, tt := range tests {
		var rs []*client.Response
		if len(tt.nodes) > 0 {
			rs = append(rs, &client.Response{Node: tt.nodes[0], Index: tt.index})
			rs = append(rs, &client.Response{
				Node: &client.Node{
					Key:   cluster,
					Nodes: tt.nodes[1:],
				},
				Index: tt.index,
			})
		}
		c := &clientWithResp{rs: rs}
		dBase := newTestDiscovery(cluster, 1, c)

		cRetry := &clientWithRetry{failTimes: 3}
		cRetry.rs = rs
		fc := clockwork.NewFakeClock()
		dRetry := newTestDiscoveryWithClock(cluster, 1, cRetry, fc)

		for _, d := range []*discovery{dBase, dRetry} {
			go func() {
				for i := uint(1); i <= maxRetryInTest; i++ {
					fc.BlockUntil(1)
					fc.Advance(time.Second * (0x1 << i))
				}
			}()
			ns, size, index, err := d.checkCluster()
			if err != tt.werr {
				t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
			}
			if reflect.DeepEqual(ns, tt.nodes) {
				t.Errorf("#%d: nodes = %v, want %v", i, ns, tt.nodes)
			}
			if size != uint64(tt.wsize) {
				t.Errorf("#%d: size = %v, want %d", i, size, tt.wsize)
			}
			if index != tt.index {
				t.Errorf("#%d: index = %v, want %d", i, index, tt.index)
			}
		}
	}
}

func TestWaitNodes(t *testing.T) {
	all := []*client.Node{
		0: {Key: "/1000/1", CreatedIndex: 2},
		1: {Key: "/1000/2", CreatedIndex: 3},
		2: {Key: "/1000/3", CreatedIndex: 4},
	}

	tests := []struct {
		nodes []*client.Node
		rs    []*client.Response
	}{
		{
			all,
			[]*client.Response{},
		},
		{
			all[:1],
			[]*client.Response{
				{Node: &client.Node{Key: "/1000/2", CreatedIndex: 3}},
				{Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}},
			},
		},
		{
			all[:2],
			[]*client.Response{
				{Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}},
			},
		},
		{
			append(all, &client.Node{Key: "/1000/4", CreatedIndex: 5}),
			[]*client.Response{
				{Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}},
			},
		},
	}

	for i, tt := range tests {
		// Basic case
		c := &clientWithResp{rs: nil, w: &watcherWithResp{rs: tt.rs}}
		dBase := newTestDiscovery("1000", 1, c)

		// Retry case
		var retryScanResp []*client.Response
		if len(tt.nodes) > 0 {
			retryScanResp = append(retryScanResp, &client.Response{
				Node: &client.Node{
					Key:   "1000",
					Value: strconv.Itoa(3),
				},
			})
			retryScanResp = append(retryScanResp, &client.Response{
				Node: &client.Node{
					Nodes: tt.nodes,
				},
			})
		}
		cRetry := &clientWithResp{
			rs: retryScanResp,
			w:  &watcherWithRetry{rs: tt.rs, failTimes: 2},
		}
		fc := clockwork.NewFakeClock()
		dRetry := newTestDiscoveryWithClock("1000", 1, cRetry, fc)

		for _, d := range []*discovery{dBase, dRetry} {
			go func() {
				for i := uint(1); i <= maxRetryInTest; i++ {
					fc.BlockUntil(1)
					fc.Advance(time.Second * (0x1 << i))
				}
			}()
			g, err := d.waitNodes(tt.nodes, uint64(3), 0) // we do not care about index in this test
			if err != nil {
				t.Errorf("#%d: err = %v, want %v", i, err, nil)
			}
			if !reflect.DeepEqual(g, all) {
				t.Errorf("#%d: all = %v, want %v", i, g, all)
			}
		}
	}
}

func TestCreateSelf(t *testing.T) {
	rs := []*client.Response{{Node: &client.Node{Key: "1000/1", CreatedIndex: 2}}}

	w := &watcherWithResp{rs: rs}
	errw := &watcherWithErr{err: errors.New("watch err")}

	c := &clientWithResp{rs: rs, w: w}
	errc := &clientWithErr{err: errors.New("create err"), w: w}
	errdupc := &clientWithErr{err: client.Error{Code: client.ErrorCodeNodeExist}}
	errwc := &clientWithResp{rs: rs, w: errw}

	tests := []struct {
		c    client.KeysAPI
		werr error
	}{
		// no error
		{c, nil},
		// client.create returns an error
		{errc, errc.err},
		// watcher.next returns an error
		{errwc, errw.err},
		// parse key exist error to duplicate ID error
		{errdupc, ErrDuplicateID},
	}

	for i, tt := range tests {
		d := newTestDiscovery("1000", 1, tt.c)
		if err := d.createSelf(""); err != tt.werr {
			t.Errorf("#%d: err = %v, want %v", i, err, nil)
		}
	}
}

func TestNodesToCluster(t *testing.T) {
	tests := []struct {
		nodes    []*client.Node
		size     uint64
		wcluster string
		werr     error
	}{
		{
			[]*client.Node{
				0: {Key: "/1000/1", Value: "1=http://1.1.1.1:2380", CreatedIndex: 1},
				1: {Key: "/1000/2", Value: "2=http://2.2.2.2:2380", CreatedIndex: 2},
				2: {Key: "/1000/3", Value: "3=http://3.3.3.3:2380", CreatedIndex: 3},
			},
			3,
			"1=http://1.1.1.1:2380,2=http://2.2.2.2:2380,3=http://3.3.3.3:2380",
			nil,
		},
		{
			[]*client.Node{
				0: {Key: "/1000/1", Value: "1=http://1.1.1.1:2380", CreatedIndex: 1},
				1: {Key: "/1000/2", Value: "2=http://2.2.2.2:2380", CreatedIndex: 2},
				2: {Key: "/1000/3", Value: "2=http://3.3.3.3:2380", CreatedIndex: 3},
			},
			3,
			"1=http://1.1.1.1:2380,2=http://2.2.2.2:2380,2=http://3.3.3.3:2380",
			ErrDuplicateName,
		},
		{
			[]*client.Node{
				0: {Key: "/1000/1", Value: "1=1.1.1.1:2380", CreatedIndex: 1},
				1: {Key: "/1000/2", Value: "2=http://2.2.2.2:2380", CreatedIndex: 2},
				2: {Key: "/1000/3", Value: "2=http://3.3.3.3:2380", CreatedIndex: 3},
			},
			3,
			"1=1.1.1.1:2380,2=http://2.2.2.2:2380,2=http://3.3.3.3:2380",
			ErrInvalidURL,
		},
	}

	for i, tt := range tests {
		cluster, err := nodesToCluster(tt.nodes, tt.size)
		if err != tt.werr {
			t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
		}
		if !reflect.DeepEqual(cluster, tt.wcluster) {
			t.Errorf("#%d: cluster = %v, want %v", i, cluster, tt.wcluster)
		}
	}
}

func TestSortableNodes(t *testing.T) {
	ns := []*client.Node{
		0: {CreatedIndex: 5},
		1: {CreatedIndex: 1},
		2: {CreatedIndex: 3},
		3: {CreatedIndex: 4},
	}
	// add some randomness
	for i := 0; i < 10000; i++ {
		ns = append(ns, &client.Node{CreatedIndex: uint64(rand.Int31())})
	}
	sns := sortableNodes{ns}
	sort.Sort(sns)
	var cis []int
	for _, n := range sns.Nodes {
		cis = append(cis, int(n.CreatedIndex))
	}
	if !sort.IntsAreSorted(cis) {
		t.Errorf("isSorted = %v, want %v", sort.IntsAreSorted(cis), true)
	}
	cis = make([]int, 0)
	for _, n := range ns {
		cis = append(cis, int(n.CreatedIndex))
	}
	if !sort.IntsAreSorted(cis) {
		t.Errorf("isSorted = %v, want %v", sort.IntsAreSorted(cis), true)
	}
}

func TestRetryFailure(t *testing.T) {
	nRetries = maxRetryInTest
	defer func() { nRetries = math.MaxUint32 }()

	cluster := "1000"
	c := &clientWithRetry{failTimes: 4}
	fc := clockwork.NewFakeClock()
	d := newTestDiscoveryWithClock(cluster, 1, c, fc)
	go func() {
		for i := uint(1); i <= maxRetryInTest; i++ {
			fc.BlockUntil(1)
			fc.Advance(time.Second * (0x1 << i))
		}
	}()
	if _, _, _, err := d.checkCluster(); err != ErrTooManyRetries {
		t.Errorf("err = %v, want %v", err, ErrTooManyRetries)
	}
}

type clientWithResp struct {
	rs []*client.Response
	w  client.Watcher
	client.KeysAPI
}

func (c *clientWithResp) Create(ctx context.Context, key string, value string) (*client.Response, error) {
	if len(c.rs) == 0 {
		return &client.Response{}, nil
	}
	r := c.rs[0]
	c.rs = c.rs[1:]
	return r, nil
}

func (c *clientWithResp) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
	if len(c.rs) == 0 {
		return &client.Response{}, &client.Error{Code: client.ErrorCodeKeyNotFound}
	}
	r := c.rs[0]
	c.rs = append(c.rs[1:], r)
	return r, nil
}

func (c *clientWithResp) Watcher(key string, opts *client.WatcherOptions) client.Watcher {
	return c.w
}

type clientWithErr struct {
	err error
	w   client.Watcher
	client.KeysAPI
}

func (c *clientWithErr) Create(ctx context.Context, key string, value string) (*client.Response, error) {
	return &client.Response{}, c.err
}

func (c *clientWithErr) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
	return &client.Response{}, c.err
}

func (c *clientWithErr) Watcher(key string, opts *client.WatcherOptions) client.Watcher {
	return c.w
}

type watcherWithResp struct {
	client.KeysAPI
	rs []*client.Response
}

func (w *watcherWithResp) Next(context.Context) (*client.Response, error) {
	if len(w.rs) == 0 {
		return &client.Response{}, nil
	}
	r := w.rs[0]
	w.rs = w.rs[1:]
	return r, nil
}

type watcherWithErr struct {
	err error
}

func (w *watcherWithErr) Next(context.Context) (*client.Response, error) {
	return &client.Response{}, w.err
}

// clientWithRetry will timeout all requests up to failTimes
type clientWithRetry struct {
	clientWithResp
	failCount int
	failTimes int
}

func (c *clientWithRetry) Create(ctx context.Context, key string, value string) (*client.Response, error) {
	if c.failCount < c.failTimes {
		c.failCount++
		return nil, &client.ClusterError{Errors: []error{context.DeadlineExceeded}}
	}
	return c.clientWithResp.Create(ctx, key, value)
}

func (c *clientWithRetry) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
	if c.failCount < c.failTimes {
		c.failCount++
		return nil, &client.ClusterError{Errors: []error{context.DeadlineExceeded}}
	}
	return c.clientWithResp.Get(ctx, key, opts)
}

// watcherWithRetry will timeout all requests up to failTimes
type watcherWithRetry struct {
	rs        []*client.Response
	failCount int
	failTimes int
}

func (w *watcherWithRetry) Next(context.Context) (*client.Response, error) {
	if w.failCount < w.failTimes {
		w.failCount++
		return nil, &client.ClusterError{Errors: []error{context.DeadlineExceeded}}
	}
	if len(w.rs) == 0 {
		return &client.Response{}, nil
	}
	r := w.rs[0]
	w.rs = w.rs[1:]
	return r, nil
}

func newTestDiscovery(cluster string, id types.ID, c client.KeysAPI) *discovery {
	return &discovery{
		lg:      zap.NewExample(),
		cluster: cluster,
		id:      id,
		c:       c,
		url:     &url.URL{Scheme: "http", Host: "test.com"},
	}
}

func newTestDiscoveryWithClock(cluster string, id types.ID, c client.KeysAPI, clock clockwork.Clock) *discovery {
	return &discovery{
		lg:      zap.NewExample(),
		cluster: cluster,
		id:      id,
		c:       c,
		url:     &url.URL{Scheme: "http", Host: "test.com"},
		clock:   clock,
	}
}
