package chaos_test

import (
	"context"
	"fmt"
	"time"

	"github.com/golang/mock/gomock"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/pingcap/test-infra/sdk/resource"

	"github.com/pingcap/endless/pkg/chaos"
	mockc "github.com/pingcap/endless/pkg/chaos/generated"
	mockm "github.com/pingcap/endless/pkg/matcher/metrics/generated"
	. "github.com/pingcap/endless/pkg/util"
)

var _ = Context("Chaos", func() {
	var MockTc *mockm.MockTiDBCluster
	var MockCc *mockc.MockChaosClient
	ChaosCreateSelectorsNames := make(chan []string, 10)
	Schedule := "@every 100ms"
	ContextTimeout := 200 * time.Millisecond

	BeforeEach(func() {
		MockCc = mockc.NewMockChaosClient(GomockController)
		MockCc.EXPECT().Create(gomock.Any()).DoAndReturn(
			func(inject resource.InjectChaosOptions) (resource.ChaosIdentifier, error) {
				var selectorsNames []string
				for _, selector := range inject.Selector {
					selectorsNames = append(selectorsNames, selector.HumanReadableName())
				}
				ChaosCreateSelectorsNames <- selectorsNames

				return MockHumanReadableName{MockName: "MockChaosId"}, nil
			}).AnyTimes()
		MockCc.EXPECT().Delete(gomock.Any()).DoAndReturn(func(arg0 resource.ChaosIdentifier) error {
			fmt.Printf("chaos %s deleted.\n", arg0)
			return nil
		}).AnyTimes()

		MockTc = mockm.NewMockTiDBCluster(GomockController)
		MockTc.EXPECT().ChaosClient().Return(MockCc).AnyTimes()
		MockTc.EXPECT().ChaosSelect(gomock.Any()).Return(mockChaosSelector("Chaos1", "Chaos2", "Chaos3"), nil).AnyTimes()
	})

	It("should select all when RunPeriodically w/o SelectorsRetainPolicy", Repeat(2, func() {
		c := chaos.Chaos{
			Name:           "TestChaos1",
			Tc:             MockTc,
			ExperimentSpec: &resource.FailureExperimentSpec{},
		}
		ctx, cancel := context.WithTimeout(context.TODO(), ContextTimeout)
		defer cancel()
		err := c.RunPeriodically(ctx, Schedule, 100*time.Millisecond)
		Expect(err).Should(BeNil())
		Eventually(ChaosCreateSelectorsNames).Should(Receive(ConsistOf([]string{"Chaos1", "Chaos3", "Chaos2"})))
	}))
	It("should select all when RunPeriodically with RetainAll", Repeat(2, func() {
		ctx, cancel := context.WithTimeout(context.TODO(), ContextTimeout)
		defer cancel()
		err := mockChaos("RetainAll", chaos.RetainAll, MockTc).RunPeriodically(ctx, Schedule, 100*time.Millisecond)
		Expect(err).Should(BeNil())
		Eventually(ChaosCreateSelectorsNames).Should(Receive(ConsistOf([]string{"Chaos1", "Chaos2", "Chaos3"})))
	}))

	It("should select one instance randomly when RunPeriodically with RetainRandomly(1)", Repeat(2, func() {
		ctx, cancel := context.WithTimeout(context.TODO(), ContextTimeout)
		defer cancel()
		err := mockChaos("RetainRandomly(1)", chaos.RetainRandomly(1), MockTc).RunPeriodically(ctx, Schedule, 100*time.Millisecond)
		Expect(err).Should(BeNil())
		Eventually(ChaosCreateSelectorsNames).Should(Receive(
			Or(ConsistOf("Chaos1"),
				ConsistOf("Chaos2"),
				ConsistOf("Chaos3"))))
	}))
	It("should select two instance randomly when RunPeriodically with RetainRandomly(2)", Repeat(2, func() {
		ctx, cancel := context.WithTimeout(context.TODO(), ContextTimeout)
		defer cancel()
		err := mockChaos("RetainRandomly(2)", chaos.RetainRandomly(2), MockTc).RunPeriodically(ctx, Schedule, 100*time.Millisecond)
		Expect(err).Should(BeNil())
		Eventually(ChaosCreateSelectorsNames).Should(Receive(
			SatisfyAll(HaveLen(2),
				Or(ConsistOf([]string{"Chaos1", "Chaos2"}),
					ConsistOf([]string{"Chaos1", "Chaos3"}),
					ConsistOf([]string{"Chaos2", "Chaos3"})))))
	}))
	It("should select all instance randomly when RunPeriodically with RetainRandomly(4)", Repeat(2, func() {
		ctx, cancel := context.WithTimeout(context.TODO(), ContextTimeout)
		defer cancel()
		err := mockChaos("RetainRandomly(4)", chaos.RetainRandomly(4), MockTc).RunPeriodically(ctx, Schedule, 100*time.Millisecond)
		Expect(err).Should(BeNil())
		Eventually(ChaosCreateSelectorsNames).Should(Receive(ConsistOf([]string{"Chaos1", "Chaos2", "Chaos3"})))
	}))
})

func mockChaos(id string, retainPolicy chaos.RetainPolicy, mockTc resource.TiDBCluster) *chaos.Chaos {
	return &chaos.Chaos{
		Name:                  "TestChaos" + id,
		Tc:                    mockTc,
		ExperimentSpec:        &resource.FailureExperimentSpec{},
		SelectorsRetainPolicy: retainPolicy,
	}
}

func mockChaosSelector(names ...string) []resource.ChaosSelector {
	var selectors []resource.ChaosSelector
	for _, name := range names {
		selectors = append(selectors, MockHumanReadableName{MockName: name})
	}
	return selectors
}

type MockHumanReadableName struct {
	MockName string
}

func (m MockHumanReadableName) HumanReadableName() string { return m.MockName }
