package tikvrel

import (
	"context"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"strconv"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	. "github.com/onsi/gomega"
	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"

	"github.com/pingcap/endless/pkg/chaos"
	. "github.com/pingcap/endless/pkg/matcher"
	. "github.com/pingcap/endless/pkg/util"
	"github.com/pingcap/endless/testcase/oltp/oltppkg"
	"github.com/pingcap/log"
	"github.com/pingcap/test-infra/sdk/resource"
	_ "github.com/pingcap/test-infra/sdk/resource/impl/k8s"
)

func OLTP_NODE_REL_BASELINE(
	ctx context.Context,
	tc resource.TiDBCluster,
	benchToolset resource.WorkloadNode,
	pr *io.PipeReader,
	caseCmdParms oltppkg.CaseCmdParms,
) {
	type testParams struct {
		compType      resource.TiDBCompType
		isLeader      bool
		faultTpye     string
		faultDuration time.Duration
	}

	testCasesParams := []testParams{
		// pod failure
		testParams{compType: resource.PD, isLeader: true, faultTpye: "restart"},  // 重启 pd leader
		testParams{compType: resource.PD, isLeader: false, faultTpye: "restart"}, // 重启一个 pd follower
		testParams{compType: resource.TiDB, faultTpye: "restart"},
		testParams{compType: resource.TiKV, faultTpye: "restart"},
		testParams{compType: resource.TiKV, faultTpye: "failure", faultDuration: 10 * time.Minute},
		testParams{compType: resource.TiDB, faultTpye: "failure", faultDuration: 10 * time.Minute},
		testParams{compType: resource.TiKV, faultTpye: "failure", faultDuration: 60 * time.Minute},
		testParams{compType: resource.PD, isLeader: false, faultTpye: "failure-all-followers", faultDuration: 10 * time.Minute}, //down 两个 followers

		// network-loss
		testParams{compType: resource.PD, isLeader: true, faultTpye: "network-loss-loss:100", faultDuration: 10 * time.Minute},
		testParams{compType: resource.PD, isLeader: false, faultTpye: "network-loss-loss:100", faultDuration: 10 * time.Minute},
		testParams{compType: resource.TiDB, faultTpye: "network-loss-loss:100", faultDuration: 10 * time.Minute},
		testParams{compType: resource.TiKV, faultTpye: "network-loss-loss:100", faultDuration: 10 * time.Minute},
		testParams{compType: resource.TiKV, faultTpye: "network-loss-loss:100", faultDuration: 60 * time.Minute},

		// network-delay
		testParams{compType: resource.PD, isLeader: true, faultTpye: "network-delay-latency:2ms", faultDuration: 10 * time.Minute},
		testParams{compType: resource.PD, isLeader: true, faultTpye: "network-delay-latency:50ms", faultDuration: 2 * time.Minute},
		testParams{compType: resource.TiDB, faultTpye: "network-delay-latency:2ms", faultDuration: 10 * time.Minute},
		testParams{compType: resource.TiDB, faultTpye: "network-delay-latency:50ms", faultDuration: 2 * time.Minute},
		testParams{compType: resource.TiKV, faultTpye: "network-delay-latency:2ms", faultDuration: 10 * time.Minute},
		testParams{compType: resource.TiKV, faultTpye: "network-delay-latency:50ms", faultDuration: 2 * time.Minute},
	}
	warmUpTime := 30 * time.Minute
	recoveryTime := 30 * time.Minute
	waitForRaftLogCatchUp := 5 * time.Hour
	waitForBalance := 5 * time.Hour

	oltppkg.LogAndBy("Waiting for raft log lag to be less than a certain value before test.")
	oltppkg.WaitForRaftLogLagLessThan(ctx, tc, waitForRaftLogCatchUp, 2*time.Minute, 1000)

	oltppkg.LogAndBy("Waiting for balance before test.")
	oltppkg.WaitForBalance(ctx, tc, waitForBalance, 5*time.Minute, 0.1)

	for _, testCase := range testCasesParams {
		var instanceIdList []int
		var falutInstanceId int
		var randomPDFollowerId int
		var pdFollowersIdList []int
		var c chaos.Chaos
		var node resource.TiDBComponentNode
		comp := testCase.compType
		instanceIdList = oltppkg.GetInstanceIdList(tc, comp)
		instanceNum := len(instanceIdList)
		if comp.String() == "pd" {
			pdMembers := oltppkg.GetPdMembersName(tc)
			pdLeader := oltppkg.GetPdLeaderName(tc)
			pdLeaderIndex, _ := strconv.Atoi(strings.TrimPrefix(pdLeader, "tc-pd-"))
			for _, pdMember := range pdMembers {
				pdMemberIndex, _ := strconv.Atoi(strings.TrimPrefix(pdMember, "tc-pd-"))
				if pdMemberIndex != pdLeaderIndex {
					pdFollowersIdList = append(pdFollowersIdList, pdMemberIndex)
				}
			}
			pdLeaderId, _ := strconv.Atoi(strings.TrimPrefix(pdLeader, "tc-pd-"))
			randomPDFollowerId = rand.Intn(len(pdFollowersIdList))
			if testCase.isLeader {
				node = Try(tc.GetNodes(comp)).([]resource.TiDBComponentNode)[pdLeaderId]
			} else {
				node = Try(tc.GetNodes(comp)).([]resource.TiDBComponentNode)[randomPDFollowerId]
			}
			log.Info("faultDetailsInfo",
				zap.String("instance-type", comp.String()),
				zap.String("fault-type", testCase.faultTpye),
				zap.Bool("isLeader", testCase.isLeader),
				zap.Duration("faultDuration", testCase.faultDuration),
				zap.Strings("pdMembers: ", pdMembers),
				zap.String("pdLeader: ", pdLeader),
				zap.Ints("pdFollowersIdList: ", pdFollowersIdList),
				zap.Int("randomPDFollowerId: ", randomPDFollowerId))
		} else {
			falutInstanceId = oltppkg.GetRandInt(0, instanceNum)
			node = Try(tc.GetNodes(comp)).([]resource.TiDBComponentNode)[falutInstanceId]
			log.Info("faultDetailsInfo",
				zap.String("instance-type", comp.String()),
				zap.String("fault-type", testCase.faultTpye),
				zap.Int("fault-Instance-Id", falutInstanceId),
				zap.Duration("faultDuration", testCase.faultDuration))
		}

		if strings.Contains(testCase.faultTpye, "failure") {
			c.ExperimentSpec = &resource.FailureExperimentSpec{}
		} else if strings.Contains(testCase.faultTpye, "network-loss") {
			c.ExperimentSpec = &resource.NetworkLossSpec{
				Loss:        strings.TrimPrefix(testCase.faultTpye, "network-loss-loss:"),
				Correlation: "25",
			}
		} else if strings.Contains(testCase.faultTpye, "network-delay") {
			c.ExperimentSpec = &resource.NetworkLatencySpec{
				Latency:     strings.TrimPrefix(testCase.faultTpye, "network-delay-latency:"),
				Correlation: "25",
			}
		} else if strings.Contains(testCase.faultTpye, "IODelay") {
			c.ExperimentSpec = &resource.IODelaySpec{
				VolumePath: "/var/lib/tikv",
				Path:       "/var/lib/tikv/data/**/*",
				Delay:      strings.TrimPrefix(testCase.faultTpye, "IODelay-delay:"), //base delay: 40ms
				Percent:    100,
			}
		} else if strings.Contains(testCase.faultTpye, "restart") {
			log.Info("will test restart, do not need chaos")
		} else {
			Expect(errors.New("invalid faultType")).Should(Succeed())
		}

		if comp.String() == "pd" {
			if testCase.isLeader {
				c = Try(oltppkg.InitChaos(tc, comp, testCase.faultTpye, c.ExperimentSpec, &testCase.isLeader)).(chaos.Chaos)
			} else {
				if testCase.faultTpye == "failure-all-followers" {
					c = Try(oltppkg.InitChaos(tc, comp, testCase.faultTpye, c.ExperimentSpec, &testCase.isLeader)).(chaos.Chaos)
				} else {
					rand.Seed(time.Now().UnixNano())
					c = Try(oltppkg.InitChaos(tc, comp, testCase.faultTpye, c.ExperimentSpec, &pdFollowersIdList[randomPDFollowerId])).(chaos.Chaos)
				}
			}
		} else {
			c = Try(oltppkg.InitChaos(tc, comp, testCase.faultTpye, c.ExperimentSpec, &instanceIdList[falutInstanceId])).(chaos.Chaos)
		}

		maxRunTime := warmUpTime + testCase.faultDuration + recoveryTime + waitForRaftLogCatchUp + waitForBalance
		ctxAll, cancel := context.WithTimeout(ctx, maxRunTime)
		defer cancel()
		g, gctx := errgroup.WithContext(ctxAll)
		g.Go(WithGinkgoRecover(func() {
			err := oltppkg.WorkLoadErrLogCheck(gctx, pr, maxRunTime, false, []string{})
			Expect(err).Should(Succeed())
		}))
		oltppkg.WaitFor(gctx, "Warm Up before inject fault", warmUpTime)
		log.Info("faultInfo",
			zap.String("fault-type", testCase.faultTpye),
			zap.Duration("faultDuration", testCase.faultDuration))
		g.Go(WithGinkgoRecover(func() {
			if testCase.faultTpye == "restart" {
				log.Info(fmt.Sprintf("will restart %s_%d ", comp.String(), falutInstanceId))
				Try(node.Restart())
			} else {
				err := oltppkg.InjectFaultByChaos(&c, gctx, testCase.faultDuration)
				Expect(err).Should(Succeed())
			}
			if comp.String() == "tidb" {
				oltppkg.WaitFor(gctx, "Wait for restart workload.", 60*time.Second)
				oltppkg.StopWorkload(benchToolset, "goTpc")
				oltppkg.StopWorkload(benchToolset, "sysBench")
			}

			oltppkg.LogAndBy("Waiting for raft log lag to be less than a certain value after recover fault.")
			oltppkg.WaitForRaftLogLagLessThan(gctx, tc, waitForRaftLogCatchUp, 2*time.Minute, 1000)

			oltppkg.LogAndBy("Waiting for balance.")
			oltppkg.WaitForBalance(gctx, tc, waitForBalance, 5*time.Minute, 0.1)

			oltppkg.WaitFor(gctx, "Recovery observation period", recoveryTime)
			cancel()
		}))
		Expect(g.Wait()).Should(BeNil())
	}
}
