package chaos

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"

	"github.com/pingcap/test-infra/sdk/e2etest/framework"
	"github.com/pingcap/test-infra/sdk/resource"
)

var _ = Describe("Chaos", func() {
	Context("TiDBCluster.Chaos failure", func() {
		f := framework.NewDefaultFramework("testbed-chaos-failure")
		It("TiDBCluster' failure chaos", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterTiDBSelectorSpec: &resource.TiDBClusterTiDBSelectorSpec{
					Peer: &[]int{0}[0],
				},
			})
			framework.ExpectNoError(err)

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector:       selector,
				ExperimentSpec: &resource.FailureExperimentSpec{},
			})

			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})

		It("TiDBCluster' failure on pd leader", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReady(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					IsLeader: &[]bool{true, false}[0],
				},
			})
			framework.ExpectNoError(err)

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector:       selector,
				ExperimentSpec: &resource.FailureExperimentSpec{},
			})

			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})

		It("TiDBCluster failure on CDC Owner", func() {
			tc := f.CreateTiDBClusterWithTiCDC(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterCDCSelectorSpec: &resource.TiDBClusterCDCSelectorSpec{
					IsOwner: &[]bool{true, false}[0],
				},
			})
			framework.ExpectNoError(err)

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector:       selector,
				ExperimentSpec: &resource.FailureExperimentSpec{},
			})

			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})

		It("WorkloadNode failure should work", func() {
			w := f.CreateWorkloadNode(f.CreateTestbed())
			f.WaitForWorkloadNodeReady(w, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := w.ChaosSelect(resource.WorkloadNodeSelectorOpts{})
			framework.ExpectNoError(err)

			chaosId, err := w.ChaosClient().Create(resource.InjectChaosOptions{
				Selector:       selector,
				ExperimentSpec: &resource.FailureExperimentSpec{},
			})
			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())
			err = w.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})
	})

	Context("TiDBCluster.Chaos network partition", func() {
		f := framework.NewDefaultFramework("testbed-chaos-network-partition")

		It("TiDBCluster' partition on tikv with tidb/pd", func() {
			tc := f.CreateTiDBClusterWithTiflash(f.CreateTestbed())

			f.WaitForTiDBClusterReady(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					All: true,
				},
				TiDBClusterTiDBSelectorSpec: &resource.TiDBClusterTiDBSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)
			target, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					// All: true,
					StoreLabelSelector: &metav1.LabelSelector{
						MatchLabels: map[string]string{},
					},
				},
				TiDBClusterTiFlashSelectorSpec: &resource.TiDBClusterTiFlashSelectorSpec{
					StoreLabelSelector: &metav1.LabelSelector{
						MatchLabels: map[string]string{},
					},
				},
			})
			framework.ExpectNoError(err)
			Expect(len(target)).Should(Equal(2))

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector:       selector,
				Target:         &target,
				ExperimentSpec: &resource.NetworkPartitionSpec{},
			})

			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})
	})

	Context("TiDBCluster.Chaos network delay", func() {
		f := framework.NewDefaultFramework("testbed-chaos-network-delay")

		It("TiDBCluster' delay on tikv", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.NetworkLatencySpec{
					Latency:     "1000ms",
					Jitter:      "5ms",
					Correlation: "25",
				},
			})

			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			out, _, _, _ := tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.TiKV,
				Command: "ping -c 3 tc-pd-peer | tail -1| awk '{print $4}' | cut -d '/' -f 2",
				Peer:    0,
			})
			lat1, err := strconv.ParseFloat(out, 64)
			framework.ExpectNoError(err)
			Expect(lat1).Should(BeNumerically(">", 800))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})

		It("TiDBCluster' delay between tidb and tikv", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterTiDBSelectorSpec: &resource.TiDBClusterTiDBSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)
			target, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				Target:   &target,
				ExperimentSpec: &resource.NetworkLatencySpec{
					Latency:     "1000ms",
					Jitter:      "5ms",
					Correlation: "25",
				},
			})
			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			out, _, _, _ := tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.TiDB,
				Command: "ping -c 3 tc-tikv-peer | tail -1| awk '{print $4}' | cut -d '/' -f 2",
				Peer:    0,
			})
			lat1, err := strconv.ParseFloat(out, 64)
			framework.ExpectNoError(err)
			Expect(lat1).Should(BeNumerically(">", 800))

			out, _, _, _ = tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.TiDB,
				Command: "ping -c 3 tc-pd-peer | tail -1| awk '{print $4}' | cut -d '/' -f 2",
				Peer:    0,
			})
			lat2, err := strconv.ParseFloat(out, 64)
			framework.ExpectNoError(err)
			Expect(lat2).Should(BeNumerically("<", 100))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})
	})

	Context("TiDBCluster.Chaos network loss", func() {
		f := framework.NewDefaultFramework("testbed-chaos-network-loss")

		It("TiDBCluster' loss on tikv", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.NetworkLossSpec{
					Loss:        "100",
					Correlation: "25",
				},
			})

			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			_, out, _, _ := tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.TiKV,
				Command: "ping -c 1 tc-pd-peer",
				Peer:    0,
			})
			Expect(out).Should(ContainSubstring("ping: bad address"))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})

		It("TiDBCluster' loss on tikv to pd", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)

			target, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				Target:   &target,
				ExperimentSpec: &resource.NetworkLossSpec{
					Loss:        "100",
					Correlation: "25",
				},
			})
			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			// check only inject between tikv and pd
			out, _, _, _ := tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.TiKV,
				Command: "ping -c 1 tc-pd-peer | grep -o '100% packet loss'",
				Peer:    0,
			})
			Expect(out).Should(Equal("100% packet loss"))

			out, _, _, _ = tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.TiKV,
				Command: "ping -c 1 tc-tidb-peer | grep -o ' 0% packet loss'",
				Peer:    0,
			})
			Expect(strings.TrimSpace(out)).Should(Equal("0% packet loss"))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})

	})

	Context("TiDBCluster.Chaos IO delay", func() {
		f := framework.NewDefaultFramework("testbed-chaos-io-delay")

		It("TiDBCluster' IO delay on tikv should work", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterTiKVSelectorSpec: &resource.TiDBClusterTiKVSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)

			now := time.Now()
			framework.ExpectNoError(tc.WriteFileInContainer("tc-tikv-0", "tikv", strings.NewReader("hello"), "/var/lib/tikv/data/world.txt"))
			duration := time.Now().Sub(now)
			framework.Logf("duration: %s", duration.String())

			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.IODelaySpec{
					VolumePath: "/var/lib/tikv",
					Path:       "/var/lib/tikv/data/**/*",
					Delay:      "10000ms",
					Percent:    100,
				},
			})

			framework.ExpectNoError(err)
			Expect(chaosId).ShouldNot(BeNil())

			now = time.Now()
			framework.ExpectNoError(tc.WriteFileInContainer("tc-tikv-0", "tikv", strings.NewReader("world"), "/var/lib/tikv/data/hello.txt"))
			afterDuration := time.Now().Sub(now)
			framework.Logf("afterDuration: %s", afterDuration.String())
			Expect(afterDuration-duration-time.Millisecond*8000 >= 0).Should(Equal(true))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})
	})

	Context("TiDBCluster.Chaos time chaos", func() {
		f := framework.NewDefaultFramework("testbed-chaos-time-chaos")

		It("TiDBCluster' time chaos on pd should work", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)
			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.TimeChaosSpec{
					TimeOffset: "-12h",
				},
			})
			framework.ExpectNoError(err)
			time.Sleep(10 * time.Second)
			_, _, ec, err := tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.PD,
				Command: "grep 'system time jumps backward' /var/lib/pd/log/pd.log",
				Peer:    0,
			})
			framework.ExpectNoError(err)
			Expect(ec).Should(Equal(0))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})
	})

	Context("TiDBCluster.Chaos stress chaos", func() {
		f := framework.NewDefaultFramework("testbed-stress-chaos")

		It("TiDBCluster' cpu-stress on pd should work", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)
			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.StressChaosSpec{
					Stressors: &resource.Stressors{
						CPUStressor: &resource.CPUStressor{
							Stressor: resource.Stressor{Workers: 4},
							Load:     80,
						},
					},
				},
			})
			framework.ExpectNoError(err)

			stdout, _, _, err := tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.PD,
				Command: "ps",
				Peer:    0,
			})
			framework.ExpectNoError(err)
			Expect(strings.Contains(stdout, "stress-ng --cpu 4 --cpu-load 80")).Should(Equal(true))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})

		It("TiDBCluster' memory-stress on pd should work", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)
			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.StressChaosSpec{
					Stressors: &resource.Stressors{
						MemoryStressor: &resource.MemoryStressor{
							Stressor: resource.Stressor{Workers: 4},
							Size:     "4GB",
						},
					},
				},
			})
			framework.ExpectNoError(err)

			stdout, _, _, err := tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.PD,
				Command: "ps",
				Peer:    0,
			})
			framework.ExpectNoError(err)
			Expect(strings.Contains(stdout, "stress-ng --vm 4 --vm-keep --vm-bytes 4000000000")).Should(Equal(true))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})

		It("TiDBCluster' cpu-memory-stress on pd should work", func() {
			tc := f.CreateTiDBCluster(f.CreateTestbed())

			f.WaitForTiDBClusterReadyWithoutPing(tc, framework.TestContext.ResourceWaitReadyTimeout)

			selector, err := tc.ChaosSelect(resource.TiDBClusterSelectorOpts{
				TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)
			chaosId, err := tc.ChaosClient().Create(resource.InjectChaosOptions{
				Selector: selector,
				ExperimentSpec: &resource.StressChaosSpec{
					Stressors: &resource.Stressors{
						CPUStressor: &resource.CPUStressor{
							Stressor: resource.Stressor{Workers: 4},
							Load:     80,
						},
						MemoryStressor: &resource.MemoryStressor{
							Stressor: resource.Stressor{Workers: 4},
							Size:     "4GB",
						},
					},
				},
			})
			framework.ExpectNoError(err)

			stdout, _, _, err := tc.Exec(resource.TiDBClusterExecOptions{
				Target:  resource.PD,
				Command: "ps",
				Peer:    0,
			})
			framework.ExpectNoError(err)
			Expect(strings.Contains(stdout, "stress-ng --vm 4 --vm-keep --vm-bytes 4000000000 --cpu 4 --cpu-load 80")).Should(Equal(true))

			err = tc.ChaosClient().Delete(chaosId)
			framework.ExpectNoError(err)
		})
	})

	Context("DMCluster chaos select", func() {
		f := framework.NewDefaultFramework("testbed-dm-chaos")

		It("master select should works", func() {
			dmCluster := f.CreateDMCluster(f.CreateTestbed())
			f.WaitForDMClusterReady(dmCluster, framework.TestContext.ResourceWaitReadyTimeout)

			selectors, err := dmCluster.ChaosSelect(resource.DMClusterSelectorOpts{
				DMClusterMasterSelectorSpec: &resource.DMClusterMasterSelectorSpec{
					Peer: pointer.Int(0),
				},
			})
			framework.ExpectNoError(err)
			Expect(selectors[0].HumanReadableName()).Should(Equal(fmt.Sprintf("%s/%s-dm-master-0", dmCluster.Namespace(), dmCluster.Name())))

			selectors, err = dmCluster.ChaosSelect(resource.DMClusterSelectorOpts{
				DMClusterWorkerSelectorSpec: &resource.DMClusterWorkerSelectorSpec{
					All: true,
				},
			})
			framework.ExpectNoError(err)
			Expect(selectors[0].HumanReadableName()).Should(Equal(fmt.Sprintf("%s/%s-dm-worker-0", dmCluster.Namespace(), dmCluster.Name())))
		})
	})
})
