// 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package upgrade

import (
	"context"
	"fmt"
	"testing"

	"github.com/pingcap/check"
	"github.com/pingcap/tiflow/dm/common"
	clientv3 "go.etcd.io/etcd/client/v3"
	"go.etcd.io/etcd/tests/v3/integration"
)

var (
	mockCluster   *integration.ClusterV3
	bigTxnCluster *integration.ClusterV3
	etcdTestCli   *clientv3.Client
	bigTxnTestCli *clientv3.Client
)

func TestUpgrade(t *testing.T) {
	integration.BeforeTestExternal(t)

	suite.(*testForEtcd).testT = t
	suiteForBigTxn.(*testForBigTxn).testT = t

	check.TestingT(t)
}

func clearTestData(c *check.C) {
	clearVersion := clientv3.OpDelete(common.ClusterVersionKey)
	_, err := etcdTestCli.Txn(context.Background()).Then(clearVersion).Commit()
	c.Assert(err, check.IsNil)
}

type testForEtcd struct {
	testT *testing.T
}

type testForBigTxn struct {
	testT *testing.T
}

var (
	suite          = check.SerialSuites(&testForEtcd{})
	suiteForBigTxn = check.SerialSuites(&testForBigTxn{})
)

func (t *testForEtcd) SetUpSuite(c *check.C) {
	mockCluster = integration.NewClusterV3(t.testT, &integration.ClusterConfig{Size: 1})
	etcdTestCli = mockCluster.RandClient()
}

func (t *testForEtcd) TearDownSuite(c *check.C) {
	mockCluster.Terminate(t.testT)
}

func (t *testForBigTxn) SetUpSuite(c *check.C) {
	bigTxnCluster = integration.NewClusterV3(t.testT, &integration.ClusterConfig{Size: 1, MaxTxnOps: 2048})
	bigTxnTestCli = bigTxnCluster.RandClient()
}

func (t *testForBigTxn) TearDownSuite(c *check.C) {
	bigTxnCluster.Terminate(t.testT)
}

func (t *testForEtcd) TestTryUpgrade(c *check.C) {
	defer clearTestData(c)

	// mock upgrade functions.
	oldUpgrades := upgrades
	defer func() {
		upgrades = oldUpgrades
	}()
	mockVerNo := uint64(0)
	upgrades = []func(cli *clientv3.Client, uctx Context) error{
		func(cli *clientv3.Client, uctx Context) error {
			mockVerNo = currentInternalNo + 1
			return nil
		},
	}

	// no previous version exist, new cluster.
	ver, rev1, err := GetVersion(etcdTestCli)
	c.Assert(err, check.IsNil)
	c.Assert(rev1, check.Greater, int64(0))
	c.Assert(ver.NotSet(), check.IsTrue)

	// try to upgrade, run actual upgrade functions
	c.Assert(TryUpgrade(etcdTestCli, newUpgradeContext()), check.IsNil)
	ver, rev2, err := GetVersion(etcdTestCli)
	c.Assert(err, check.IsNil)
	c.Assert(rev2, check.Greater, rev1)
	c.Assert(ver, check.DeepEquals, CurrentVersion)
	c.Assert(mockVerNo, check.Equals, uint64(5))

	// try to upgrade again, do nothing because the version is the same.
	mockVerNo = 0
	c.Assert(TryUpgrade(etcdTestCli, newUpgradeContext()), check.IsNil)
	ver, rev3, err := GetVersion(etcdTestCli)
	c.Assert(err, check.IsNil)
	c.Assert(rev3, check.Equals, rev2)
	c.Assert(ver, check.DeepEquals, CurrentVersion)
	c.Assert(mockVerNo, check.Equals, uint64(0))

	// mock a newer current version.
	oldCurrentVer := CurrentVersion
	defer func() {
		CurrentVersion = oldCurrentVer
	}()
	newerVer := NewVersion(currentInternalNo+1, "mock-current-ver")
	CurrentVersion = newerVer

	// try to upgrade, to a newer version, upgrade operations applied.
	c.Assert(TryUpgrade(etcdTestCli, newUpgradeContext()), check.IsNil)
	ver, rev4, err := GetVersion(etcdTestCli)
	c.Assert(err, check.IsNil)
	c.Assert(rev4, check.Greater, rev3)
	c.Assert(ver, check.DeepEquals, newerVer)
	c.Assert(mockVerNo, check.Equals, currentInternalNo+1)

	// try to upgrade, to an older version, do nothing.
	mockVerNo = 0
	CurrentVersion = oldCurrentVer
	c.Assert(TryUpgrade(etcdTestCli, newUpgradeContext()), check.IsNil)
	ver, rev5, err := GetVersion(etcdTestCli)
	c.Assert(err, check.IsNil)
	c.Assert(rev5, check.Equals, rev4)
	c.Assert(ver, check.DeepEquals, newerVer) // not changed.
	c.Assert(mockVerNo, check.Equals, uint64(0))
}

func (t *testForEtcd) TestUpgradeToVer3(c *check.C) {
	ctx := context.Background()
	source := "source-1"
	oldKey := common.UpstreamConfigKeyAdapterV1.Encode(source)
	oldVal := "test"

	_, err := etcdTestCli.Put(ctx, oldKey, oldVal)
	c.Assert(err, check.IsNil)
	c.Assert(upgradeToVer3(ctx, etcdTestCli), check.IsNil)

	newKey := common.UpstreamConfigKeyAdapter.Encode(source)
	resp, err := etcdTestCli.Get(ctx, newKey)
	c.Assert(err, check.IsNil)
	c.Assert(resp.Kvs, check.HasLen, 1)
	c.Assert(string(resp.Kvs[0].Value), check.Equals, oldVal)

	// test won't overwrite new value
	newVal := "test2"
	_, err = etcdTestCli.Put(ctx, newKey, newVal)
	c.Assert(err, check.IsNil)
	c.Assert(upgradeToVer3(ctx, etcdTestCli), check.IsNil)
	resp, err = etcdTestCli.Get(ctx, newKey)
	c.Assert(err, check.IsNil)
	c.Assert(resp.Kvs, check.HasLen, 1)
	c.Assert(string(resp.Kvs[0].Value), check.Equals, newVal)

	for i := 0; i < 500; i++ {
		key := common.UpstreamConfigKeyAdapterV1.Encode(fmt.Sprintf("%s-%d", source, i))
		val := fmt.Sprintf("%s-%d", oldVal, i)
		_, err := etcdTestCli.Put(ctx, key, val)
		c.Assert(err, check.IsNil)
	}
	c.Assert(upgradeToVer3(ctx, etcdTestCli), check.ErrorMatches, ".*too many operations in txn request.*")
}

func (t *testForBigTxn) TestUpgradeToVer3(c *check.C) {
	source := "source-1"
	oldVal := "test"
	ctx := context.Background()

	for i := 0; i < 1000; i++ {
		key := common.UpstreamConfigKeyAdapterV1.Encode(fmt.Sprintf("%s-%d", source, i))
		val := fmt.Sprintf("%s-%d", oldVal, i)
		_, err := bigTxnTestCli.Put(ctx, key, val)
		c.Assert(err, check.IsNil)
	}
	c.Assert(upgradeToVer3(ctx, bigTxnTestCli), check.IsNil)
}
