// Copyright 2022 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 clientv3

import (
	"crypto/tls"
	"encoding/json"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"go.etcd.io/etcd/client/pkg/v3/logutil"
	"go.etcd.io/etcd/client/pkg/v3/transport"
)

func TestNewClientConfig(t *testing.T) {
	cases := []struct {
		name         string
		spec         ConfigSpec
		expectedConf Config
	}{
		{
			name: "only has basic info",
			spec: ConfigSpec{
				Endpoints:        []string{"http://192.168.0.10:2379"},
				DialTimeout:      2 * time.Second,
				KeepAliveTime:    3 * time.Second,
				KeepAliveTimeout: 5 * time.Second,
			},
			expectedConf: Config{
				Endpoints:            []string{"http://192.168.0.10:2379"},
				DialTimeout:          2 * time.Second,
				DialKeepAliveTime:    3 * time.Second,
				DialKeepAliveTimeout: 5 * time.Second,
			},
		},
		{
			name: "auth enabled",
			spec: ConfigSpec{
				Endpoints:        []string{"http://192.168.0.12:2379"},
				DialTimeout:      1 * time.Second,
				KeepAliveTime:    4 * time.Second,
				KeepAliveTimeout: 6 * time.Second,
				Auth: &AuthConfig{
					Username: "test",
					Password: "changeme",
				},
			},
			expectedConf: Config{
				Endpoints:            []string{"http://192.168.0.12:2379"},
				DialTimeout:          1 * time.Second,
				DialKeepAliveTime:    4 * time.Second,
				DialKeepAliveTimeout: 6 * time.Second,
				Username:             "test",
				Password:             "changeme",
			},
		},
		{
			name: "JWT specified",
			spec: ConfigSpec{
				Endpoints:        []string{"http://192.168.0.12:2379"},
				DialTimeout:      1 * time.Second,
				KeepAliveTime:    4 * time.Second,
				KeepAliveTimeout: 6 * time.Second,
				Auth: &AuthConfig{
					Token: "test",
				},
			},
			expectedConf: Config{
				Endpoints:            []string{"http://192.168.0.12:2379"},
				DialTimeout:          1 * time.Second,
				DialKeepAliveTime:    4 * time.Second,
				DialKeepAliveTimeout: 6 * time.Second,
				Token:                "test",
			},
		},
		{
			name: "default secure transport",
			spec: ConfigSpec{
				Endpoints:        []string{"http://192.168.0.10:2379"},
				DialTimeout:      2 * time.Second,
				KeepAliveTime:    3 * time.Second,
				KeepAliveTimeout: 5 * time.Second,
				Secure: &SecureConfig{
					InsecureTransport: false,
				},
			},
			expectedConf: Config{
				Endpoints:            []string{"http://192.168.0.10:2379"},
				DialTimeout:          2 * time.Second,
				DialKeepAliveTime:    3 * time.Second,
				DialKeepAliveTimeout: 5 * time.Second,
				TLS:                  &tls.Config{},
			},
		},
		{
			name: "default secure transport and skip TLS verification",
			spec: ConfigSpec{
				Endpoints:        []string{"http://192.168.0.13:2379"},
				DialTimeout:      1 * time.Second,
				KeepAliveTime:    3 * time.Second,
				KeepAliveTimeout: 5 * time.Second,
				Secure: &SecureConfig{
					InsecureTransport:  false,
					InsecureSkipVerify: true,
				},
			},
			expectedConf: Config{
				Endpoints:            []string{"http://192.168.0.13:2379"},
				DialTimeout:          1 * time.Second,
				DialKeepAliveTime:    3 * time.Second,
				DialKeepAliveTimeout: 5 * time.Second,
				TLS: &tls.Config{
					InsecureSkipVerify: true,
				},
			},
		},
		{
			name: "insecure transport and skip TLS verification",
			spec: ConfigSpec{
				Endpoints:        []string{"http://192.168.0.13:2379"},
				DialTimeout:      1 * time.Second,
				KeepAliveTime:    3 * time.Second,
				KeepAliveTimeout: 5 * time.Second,
				Secure: &SecureConfig{
					InsecureTransport:  true,
					InsecureSkipVerify: true,
				},
			},
			expectedConf: Config{
				Endpoints:            []string{"http://192.168.0.13:2379"},
				DialTimeout:          1 * time.Second,
				DialKeepAliveTime:    3 * time.Second,
				DialKeepAliveTimeout: 5 * time.Second,
				TLS: &tls.Config{
					InsecureSkipVerify: true,
				},
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			lg, _ := logutil.CreateDefaultZapLogger(zap.InfoLevel)

			cfg, err := NewClientConfig(&tc.spec, lg)
			require.NoError(t, err)

			assert.Equal(t, tc.expectedConf, *cfg)
		})
	}
}

func TestNewClientConfigWithSecureCfg(t *testing.T) {
	tls, err := transport.SelfCert(zap.NewNop(), t.TempDir(), []string{"localhost"}, 1)
	require.NoError(t, err)

	scfg := &SecureConfig{
		Cert:   tls.CertFile,
		Key:    tls.KeyFile,
		Cacert: tls.TrustedCAFile,
	}

	cfg, err := NewClientConfig(&ConfigSpec{
		Endpoints:        []string{"http://192.168.0.13:2379"},
		DialTimeout:      2 * time.Second,
		KeepAliveTime:    3 * time.Second,
		KeepAliveTimeout: 5 * time.Second,
		Secure:           scfg,
	}, nil)
	require.NoErrorf(t, err, "Unexpected result client config")
	if cfg == nil || cfg.TLS == nil {
		t.Fatalf("Unexpected result client config: %v", err)
	}
}

func TestConfigSpecClone(t *testing.T) {
	cfgSpec := &ConfigSpec{
		Endpoints:        []string{"ep1", "ep2", "ep3"},
		RequestTimeout:   10 * time.Second,
		DialTimeout:      2 * time.Second,
		KeepAliveTime:    5 * time.Second,
		KeepAliveTimeout: 2 * time.Second,

		Secure: &SecureConfig{
			Cert:               "path/2/cert",
			Key:                "path/2/key",
			Cacert:             "path/2/cacert",
			InsecureTransport:  true,
			InsecureSkipVerify: false,
		},

		Auth: &AuthConfig{
			Username: "foo",
			Password: "changeme",
		},
	}

	testCases := []struct {
		name          string
		cs            *ConfigSpec
		newEp         []string
		newSecure     *SecureConfig
		newAuth       *AuthConfig
		expectedEqual bool
	}{
		{
			name:          "normal case",
			cs:            cfgSpec,
			expectedEqual: true,
		},
		{
			name:          "point to a new slice of endpoint, but with the same data",
			cs:            cfgSpec,
			newEp:         []string{"ep1", "ep2", "ep3"},
			expectedEqual: true,
		},
		{
			name:          "update endpoint",
			cs:            cfgSpec,
			newEp:         []string{"ep1", "newep2", "ep3"},
			expectedEqual: false,
		},
		{
			name: "point to a new secureConfig, but with the same data",
			cs:   cfgSpec,
			newSecure: &SecureConfig{
				Cert:               "path/2/cert",
				Key:                "path/2/key",
				Cacert:             "path/2/cacert",
				InsecureTransport:  true,
				InsecureSkipVerify: false,
			},
			expectedEqual: true,
		},
		{
			name: "update key in secureConfig",
			cs:   cfgSpec,
			newSecure: &SecureConfig{
				Cert:               "path/2/cert",
				Key:                "newPath/2/key",
				Cacert:             "path/2/cacert",
				InsecureTransport:  true,
				InsecureSkipVerify: false,
			},
			expectedEqual: false,
		},
		{
			name: "update bool values in secureConfig",
			cs:   cfgSpec,
			newSecure: &SecureConfig{
				Cert:               "path/2/cert",
				Key:                "path/2/key",
				Cacert:             "path/2/cacert",
				InsecureTransport:  false,
				InsecureSkipVerify: true,
			},
			expectedEqual: false,
		},
		{
			name: "point to a new authConfig, but with the same data",
			cs:   cfgSpec,
			newAuth: &AuthConfig{
				Username: "foo",
				Password: "changeme",
			},
			expectedEqual: true,
		},
		{
			name: "update authConfig",
			cs:   cfgSpec,
			newAuth: &AuthConfig{
				Username: "newUser",
				Password: "newPassword",
			},
			expectedEqual: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			dataBeforeTest, err := json.Marshal(tc.cs)
			require.NoError(t, err)

			clonedCfgSpec := tc.cs.Clone()
			if len(tc.newEp) > 0 {
				clonedCfgSpec.Endpoints = tc.newEp
			}
			if tc.newSecure != nil {
				clonedCfgSpec.Secure = tc.newSecure
			}
			if tc.newAuth != nil {
				clonedCfgSpec.Auth = tc.newAuth
			}

			actualEqual := reflect.DeepEqual(tc.cs, clonedCfgSpec)
			require.Equal(t, tc.expectedEqual, actualEqual)

			// double-check the original ConfigSpec isn't updated
			dataAfterTest, err := json.Marshal(tc.cs)
			require.NoError(t, err)
			require.True(t, reflect.DeepEqual(dataBeforeTest, dataAfterTest))
		})
	}
}
