// Copyright 2020 PingCAP, Inc.
//
// 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 infosync

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path"
	"runtime"
	"testing"
	"time"

	"github.com/pingcap/failpoint"
	"github.com/pingcap/kvproto/pkg/keyspacepb"
	"github.com/pingcap/tidb/pkg/ddl/placement"
	"github.com/pingcap/tidb/pkg/ddl/util"
	"github.com/pingcap/tidb/pkg/keyspace"
	"github.com/pingcap/tidb/pkg/meta/model"
	pmodel "github.com/pingcap/tidb/pkg/parser/model"
	"github.com/pingcap/tidb/pkg/testkit/testsetup"
	util2 "github.com/pingcap/tidb/pkg/util"
	"github.com/stretchr/testify/require"
	"github.com/tikv/client-go/v2/tikv"
	"go.etcd.io/etcd/tests/v3/integration"
	"go.uber.org/goleak"
)

func TestMain(m *testing.M) {
	testsetup.SetupForCommonTest()
	opts := []goleak.Option{
		goleak.IgnoreTopFunction("github.com/golang/glog.(*fileSink).flushDaemon"),
		goleak.IgnoreTopFunction("github.com/bazelbuild/rules_go/go/tools/bzltestutil.RegisterTimeoutHandler.func1"),
		goleak.IgnoreTopFunction("github.com/lestrrat-go/httprc.runFetchWorker"),
		goleak.IgnoreTopFunction("go.etcd.io/etcd/client/pkg/v3/logutil.(*MergeLogger).outputLoop"),
		goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
	}
	goleak.VerifyTestMain(m, opts...)
}

func TestTopology(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("integration.NewClusterV3 will create file contains a colon which is not allowed on Windows")
	}
	integration.BeforeTestExternal(t)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	currentID := "test"

	cluster := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
	defer cluster.Terminate(t)

	client := cluster.RandClient()

	require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/domain/infosync/mockServerInfo", "return(true)"))
	defer func() {
		err := failpoint.Disable("github.com/pingcap/tidb/pkg/domain/infosync/mockServerInfo")
		require.NoError(t, err)
	}()

	info, err := GlobalInfoSyncerInit(ctx, currentID, func() uint64 { return 1 }, client, client, nil, nil, keyspace.CodecV1, false, nil)
	require.NoError(t, err)

	err = info.newTopologySessionAndStoreServerInfo(ctx, util2.NewSessionDefaultRetryCnt)
	require.NoError(t, err)

	topology, err := info.getTopologyFromEtcd(ctx)
	require.NoError(t, err)
	require.Equal(t, int64(1282967700), topology.StartTimestamp)

	v, ok := topology.Labels["foo"]
	require.True(t, ok)
	require.Equal(t, "bar", v)
	selfInfo := info.getLocalServerInfo()
	require.Equal(t, selfInfo.asTopologyInfo(), *topology)

	nonTTLKey := fmt.Sprintf("%s/%s:%v/info", TopologyInformationPath, selfInfo.IP, selfInfo.Port)
	ttlKey := fmt.Sprintf("%s/%s:%v/ttl", TopologyInformationPath, selfInfo.IP, selfInfo.Port)

	err = util.DeleteKeyFromEtcd(nonTTLKey, client, util2.NewSessionDefaultRetryCnt, time.Second)
	require.NoError(t, err)

	// Refresh and re-test if the key exists
	err = info.RestartTopology(ctx)
	require.NoError(t, err)

	topology, err = info.getTopologyFromEtcd(ctx)
	require.NoError(t, err)

	s, err := os.Executable()
	require.NoError(t, err)

	dir := path.Dir(s)
	require.Equal(t, dir, topology.DeployPath)
	require.Equal(t, int64(1282967700), topology.StartTimestamp)
	require.Equal(t, info.getLocalServerInfo().asTopologyInfo(), *topology)

	// check ttl key
	ttlExists, err := info.ttlKeyExists(ctx)
	require.NoError(t, err)
	require.True(t, ttlExists)

	err = util.DeleteKeyFromEtcd(ttlKey, client, util2.NewSessionDefaultRetryCnt, time.Second)
	require.NoError(t, err)

	err = info.updateTopologyAliveness(ctx)
	require.NoError(t, err)

	ttlExists, err = info.ttlKeyExists(ctx)
	require.NoError(t, err)
	require.True(t, ttlExists)
}

func (is *InfoSyncer) getTopologyFromEtcd(ctx context.Context) (*TopologyInfo, error) {
	info := is.getLocalServerInfo()
	key := fmt.Sprintf("%s/%s:%v/info", TopologyInformationPath, info.IP, info.Port)
	resp, err := is.etcdCli.Get(ctx, key)
	if err != nil {
		return nil, err
	}
	if len(resp.Kvs) == 0 {
		return nil, errors.New("not-exists")
	}
	if len(resp.Kvs) != 1 {
		return nil, errors.New("resp.Kvs error")
	}
	var ret TopologyInfo
	err = json.Unmarshal(resp.Kvs[0].Value, &ret)
	if err != nil {
		return nil, err
	}
	return &ret, nil
}

func (is *InfoSyncer) ttlKeyExists(ctx context.Context) (bool, error) {
	info := is.getLocalServerInfo()
	key := fmt.Sprintf("%s/%s:%v/ttl", TopologyInformationPath, info.IP, info.Port)
	resp, err := is.etcdCli.Get(ctx, key)
	if err != nil {
		return false, err
	}
	if len(resp.Kvs) >= 2 {
		return false, errors.New("too many arguments in resp.Kvs")
	}
	return len(resp.Kvs) == 1, nil
}

func TestPutBundlesRetry(t *testing.T) {
	_, err := GlobalInfoSyncerInit(context.TODO(), "test", func() uint64 { return 1 }, nil, nil, nil, nil, keyspace.CodecV1, false, nil)
	require.NoError(t, err)

	bundle, err := placement.NewBundleFromOptions(&model.PlacementSettings{PrimaryRegion: "r1", Regions: "r1,r2"})
	require.NoError(t, err)
	bundle = bundle.Reset(placement.RuleIndexTable, []int64{1024})

	t.Run("serviceErrorShouldNotRetry", func(t *testing.T) {
		require.NoError(t, PutRuleBundles(context.TODO(), []*placement.Bundle{{ID: bundle.ID}}))
		require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/domain/infosync/putRuleBundlesError", "1*return(true)"))
		defer func() {
			require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/pkg/domain/infosync/putRuleBundlesError"))
		}()

		err := PutRuleBundlesWithRetry(context.TODO(), []*placement.Bundle{bundle}, 3, time.Millisecond)
		require.Error(t, err)
		require.Equal(t, "[domain:8243]mock service error", err.Error())

		got, err := GetRuleBundle(context.TODO(), bundle.ID)
		require.NoError(t, err)
		require.True(t, got.IsEmpty())
	})

	t.Run("nonServiceErrorShouldRetry", func(t *testing.T) {
		require.NoError(t, PutRuleBundles(context.TODO(), []*placement.Bundle{{ID: bundle.ID}}))
		require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/domain/infosync/putRuleBundlesError", "3*return(false)"))
		defer func() {
			require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/pkg/domain/infosync/putRuleBundlesError"))
		}()

		err := PutRuleBundlesWithRetry(context.TODO(), []*placement.Bundle{bundle}, 3, time.Millisecond)
		require.NoError(t, err)

		got, err := GetRuleBundle(context.TODO(), bundle.ID)
		require.NoError(t, err)

		gotJSON, err := json.Marshal(got)
		require.NoError(t, err)

		expectJSON, err := json.Marshal(bundle)
		require.NoError(t, err)

		require.Equal(t, expectJSON, gotJSON)
	})

	t.Run("nonServiceErrorRetryAndFail", func(t *testing.T) {
		require.NoError(t, PutRuleBundles(context.TODO(), []*placement.Bundle{{ID: bundle.ID}}))
		require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/domain/infosync/putRuleBundlesError", "4*return(false)"))
		defer func() {
			require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/pkg/domain/infosync/putRuleBundlesError"))
		}()

		err := PutRuleBundlesWithRetry(context.TODO(), []*placement.Bundle{bundle}, 3, time.Millisecond)
		require.Error(t, err)
		require.Equal(t, "mock other error", err.Error())

		got, err := GetRuleBundle(context.TODO(), bundle.ID)
		require.NoError(t, err)
		require.True(t, got.IsEmpty())
	})
}

func TestTiFlashManager(t *testing.T) {
	ctx := context.Background()
	_, err := GlobalInfoSyncerInit(ctx, "test", func() uint64 { return 1 }, nil, nil, nil, nil, keyspace.CodecV1, false, nil)
	tiflash := NewMockTiFlash()
	SetMockTiFlash(tiflash)

	require.NoError(t, err)

	// SetTiFlashPlacementRule/GetTiFlashGroupRules
	rule := MakeNewRule(1, 2, []string{"a"})
	require.NoError(t, SetTiFlashPlacementRule(ctx, rule))
	rules, err := GetTiFlashGroupRules(ctx, "tiflash")
	require.NoError(t, err)
	require.Equal(t, 1, len(rules))
	require.Equal(t, "table-1-r", rules[0].ID)
	require.Equal(t, 2, rules[0].Count)
	require.Equal(t, []string{"a"}, rules[0].LocationLabels)
	require.Equal(t, false, rules[0].Override, false)
	require.Equal(t, placement.RuleIndexTiFlash, rules[0].Index)

	// GetTiFlashStoresStat
	stats, err := GetTiFlashStoresStat(ctx)
	require.NoError(t, err)
	require.Equal(t, 1, stats.Count)

	// DeleteTiFlashPlacementRules
	require.NoError(t, DeleteTiFlashPlacementRules(ctx, []int64{1}))
	rules, err = GetTiFlashGroupRules(ctx, "tiflash")
	require.NoError(t, err)
	require.Equal(t, 0, len(rules))

	// ConfigureTiFlashPDForTable
	require.Nil(t, ConfigureTiFlashPDForTable(1, 2, &[]string{"a"}))
	rules, err = GetTiFlashGroupRules(ctx, "tiflash")
	require.NoError(t, err)
	require.Equal(t, 1, len(rules))

	// ConfigureTiFlashPDForPartitions
	ConfigureTiFlashPDForPartitions(true, &[]model.PartitionDefinition{
		{
			ID:       2,
			Name:     pmodel.NewCIStr("p1"),
			LessThan: []string{},
		},
		{
			ID:       3,
			Name:     pmodel.NewCIStr("p2"),
			LessThan: []string{},
		},
	}, 3, &[]string{}, 100)
	rules, err = GetTiFlashGroupRules(ctx, "tiflash")
	require.NoError(t, err)
	// Have table a and partitions p1, p2
	require.Equal(t, 3, len(rules))
	z, ok := tiflash.SyncStatus[2]
	require.Equal(t, true, ok)
	require.Equal(t, true, z.Accel)
	z, ok = tiflash.SyncStatus[3]
	require.Equal(t, true, ok)
	require.Equal(t, true, z.Accel)

	CloseTiFlashManager(ctx)
}

func TestInfoSyncerMarshal(t *testing.T) {
	info := &ServerInfo{
		StaticServerInfo: StaticServerInfo{
			ServerVersionInfo: ServerVersionInfo{
				Version: "8.8.8",
				GitHash: "123456",
			},
			ID:             "tidb1",
			IP:             "127.0.0.1",
			Port:           4000,
			StatusPort:     10080,
			Lease:          "1s",
			StartTimestamp: 10000,
			ServerIDGetter: func() uint64 { return 0 },
			JSONServerID:   1,
		},
		DynamicServerInfo: DynamicServerInfo{
			Labels: map[string]string{"zone": "ap-northeast-1a"},
		},
	}
	data, err := json.Marshal(info)
	require.NoError(t, err)
	require.Equal(t, data, []byte(`{"version":"8.8.8","git_hash":"123456",`+
		`"ddl_id":"tidb1","ip":"127.0.0.1","listening_port":4000,"status_port":10080,"lease":"1s","start_timestamp":10000,`+
		`"server_id":1,"labels":{"zone":"ap-northeast-1a"}}`))
	var decodeInfo *ServerInfo
	err = json.Unmarshal(data, &decodeInfo)
	require.NoError(t, err)
	require.Nil(t, decodeInfo.ServerIDGetter)
	require.Equal(t, info.Version, decodeInfo.Version)
	require.Equal(t, info.GitHash, decodeInfo.GitHash)
	require.Equal(t, info.ID, decodeInfo.ID)
	require.Equal(t, info.IP, decodeInfo.IP)
	require.Equal(t, info.Port, decodeInfo.Port)
	require.Equal(t, info.StatusPort, decodeInfo.StatusPort)
	require.Equal(t, info.Lease, decodeInfo.Lease)
	require.Equal(t, info.StartTimestamp, decodeInfo.StartTimestamp)
	require.Equal(t, info.JSONServerID, decodeInfo.JSONServerID)
	require.Equal(t, info.Labels, decodeInfo.Labels)
}

func TestSetKeyspaceMinStartTsPath(t *testing.T) {
	keyspaceMeta := keyspacepb.KeyspaceMeta{}
	keyspaceMeta.Id = 2
	keyspaceMeta.Name = "test_ks_name2"
	codec, err := tikv.NewCodecV2(tikv.ModeTxn, &keyspaceMeta)
	require.NoError(t, err)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	currentServerID := "testServerID"

	infoSyncer, err := GlobalInfoSyncerInit(ctx, currentServerID, func() uint64 { return 1 }, nil, nil, nil, nil, codec, false, nil)
	require.NoError(t, err)

	hostName, err := os.Hostname()
	require.NotEqual(t, 0, len(hostName))
	require.NoError(t, err)

	expectMinStartTSPath := fmt.Sprintf("%s/%s_%d_%s", ServerMinStartTSPath, hostName, codec.GetKeyspaceID(), currentServerID)
	require.Equal(t, expectMinStartTSPath, infoSyncer.minStartTSPath)
}
