package tidbrel

import (
	"context"
	"errors"
	"io"
	"math/rand"
	"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/matcher/metrics"
	. "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"
)

type TiDBRelParams struct {
	WarmUpTime    time.Duration
	FaultType     string
	FaultDuration time.Duration
	RecoveryTime  time.Duration
}

func TiDBFaultTest(
	ctx context.Context,
	tc resource.TiDBCluster,
	benchToolset resource.WorkloadNode,
	pr *io.PipeReader,
	tidbRelParams TiDBRelParams,
) {
	oltppkg.LogAndBy("one tidb fault testing")
	instanceType := resource.TiDB
	instanceIdList := oltppkg.GetInstanceIdList(tc, instanceType)
	instanceId, _ := oltppkg.GetRandInstance(tc, instanceType)
	tidbFaultTypes := []string{"failure", "network-loss-100", "network-delay"}
	if tidbRelParams.FaultType == "random" {
		rand.Seed(time.Now().UnixNano())
		tidbRelParams.FaultType = tidbFaultTypes[rand.Intn(len(tidbFaultTypes))]
	}
	var c chaos.Chaos
	switch tidbRelParams.FaultType {
	case "failure":
		c.ExperimentSpec = &resource.FailureExperimentSpec{}
	case "network-loss-100":
		c.ExperimentSpec = &resource.NetworkLossSpec{
			Loss:        "100",
			Correlation: "25",
		}
	case "network-loss-1":
		c.ExperimentSpec = &resource.NetworkLossSpec{
			Loss:        "1",
			Correlation: "25",
		}
	case "network-delay":
		c.ExperimentSpec = &resource.NetworkLatencySpec{
			Latency:     oltppkg.GetRandIntStr(100, 500) + "ms",
			Correlation: "25",
		}
	case "io-delay":
		c.ExperimentSpec = &resource.IODelaySpec{
			VolumePath: "/var/lib/tidb",
			Path:       "/var/lib/tidb/log/**",
			Delay:      "300s",
			Percent:    100,
		}
	case "network-partition":
		c = chaos.Chaos{
			Name:           tidbRelParams.FaultType,
			Tc:             tc,
			TargetTc:       tc,
			ExperimentSpec: &resource.NetworkPartitionSpec{},
			SelectorOpts: resource.TiDBClusterSelectorOpts{
				TiDBClusterTiDBSelectorSpec: &resource.TiDBClusterTiDBSelectorSpec{
					Peer: &instanceIdList[instanceId],
				},
			},
			TargetSelectorOpts: &resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					All: true,
				},
			},
		}
	default:
		err := errors.New("invalid faultType")
		Expect(err).Should(Succeed())
	}

	if tidbRelParams.FaultType != "network-partition" {
		c = Try(oltppkg.InitChaos(tc, instanceType, tidbRelParams.FaultType, c.ExperimentSpec, &instanceIdList[instanceId])).(chaos.Chaos)
	}

	//Error 8027: https://github.com/pingcap/tidb/issues/30258
	workloadCheckParams := oltppkg.WorkloadCheckParameters{
		IsCheck:   true,
		IgnoreErr: []string{"8022", "1105", "1205", "1213", "9007", "failed"},
	}
	qpsJitterCheckParams := oltppkg.QpsJitterCheckParameters{
		IsCheck: false,
	}
	durJitterCheckParams := oltppkg.DurJitterCheckParameters{
		IsCheck: false,
	}
	var allComponentRestartCheck bool
	var tidbRestartCheck bool

	if tidbRelParams.FaultType == "failure" {
		allComponentRestartCheck = false
		tidbRestartCheck = false
	} else {
		allComponentRestartCheck = true
		tidbRestartCheck = true
	}
	restartCheckParms := oltppkg.RestartCheckParameters{
		AllComponentRestartCheck: allComponentRestartCheck,
		TiKVRestartCheck:         true,
		PDRestartCheck:           true,
		TiDBRestartCheck:         tidbRestartCheck,
	}

	runTime := tidbRelParams.WarmUpTime + tidbRelParams.FaultDuration + tidbRelParams.RecoveryTime
	recoveryCheckOffset := tidbRelParams.WarmUpTime + tidbRelParams.FaultDuration
	recoveryCheckOffsetStr := strings.Split(recoveryCheckOffset.String(), "m")[0] + "m"

	ctxAll, cancel := context.WithTimeout(ctx, runTime)
	defer cancel()
	g, ctxg := errgroup.WithContext(ctxAll)

	oltppkg.FunCommonCheck(ctxg, tc, g, runTime, pr, qpsJitterCheckParams, durJitterCheckParams, restartCheckParms, workloadCheckParams)

	oltppkg.WaitFor(ctxg, "Warm Up before inject fault", tidbRelParams.WarmUpTime)

	g.Go(WithGinkgoRecover(func() {
		err := oltppkg.InjectFaultByChaos(&c, ctxg, tidbRelParams.FaultDuration)
		Expect(err).Should(Succeed())
		oltppkg.WaitFor(ctxg, "Wait 20s to restart workload.", 60*time.Second)
		oltppkg.StopWorkload(benchToolset, "goTpc")
		oltppkg.StopWorkload(benchToolset, "sysBench")
		oltppkg.WaitFor(ctxg, "Wait 2min to warm up workload.", 2*time.Minute)
		log.Info("recoveryCheckOffset", zap.String("recoveryCheckOffset", recoveryCheckOffsetStr))
		ConsistentlyWithMetrics(tc, tidbRelParams.RecoveryTime).
			ShouldNot(QPSDropMoreThanOffset(0.2, recoveryCheckOffsetStr).For(60 * time.Second).WithCtx(ctxg))
	}))
	Expect(g.Wait()).Should(BeNil())
}
