package ngoperation

import (
	"context"

	clusterng "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"go.uber.org/mock/gomock"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/mgmthttpcli"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
)

var _ = Describe("CreateTiKVGroup", func() {
	Describe("getParam", func() {
		It("leaves exclusive unset when the param is absent", func() {
			req, err := (&createTiKVGroup{}).getParam(context.Background(), buildCreateTiKVGroupInstance(baseCreateTiKVGroupParams()))

			Expect(err).NotTo(HaveOccurred())
			Expect(req.GetExclusive()).To(BeFalse())
			Expect(req.GetForce()).To(BeFalse())
		})

		It("sets force from force_create", func() {
			params := baseCreateTiKVGroupParams()
			params[opent.ParamForceCreate] = "true"

			req, err := (&createTiKVGroup{}).getParam(context.Background(), buildCreateTiKVGroupInstance(params))

			Expect(err).NotTo(HaveOccurred())
			Expect(req.GetForce()).To(BeTrue())
		})

		It("accepts explicit exclusive true", func() {
			params := baseCreateTiKVGroupParams()
			params[opent.ParamExclusive] = "true"

			req, err := (&createTiKVGroup{}).getParam(context.Background(), buildCreateTiKVGroupInstance(params))

			Expect(err).NotTo(HaveOccurred())
			Expect(req.GetExclusive()).To(BeTrue())
		})

		It("accepts explicit exclusive false", func() {
			params := baseCreateTiKVGroupParams()
			params[opent.ParamExclusive] = "false"

			req, err := (&createTiKVGroup{}).getParam(context.Background(), buildCreateTiKVGroupInstance(params))

			Expect(err).NotTo(HaveOccurred())
			Expect(req.GetExclusive()).To(BeFalse())
		})

		It("rejects invalid exclusive", func() {
			params := baseCreateTiKVGroupParams()
			params[opent.ParamExclusive] = "invalid"

			_, err := (&createTiKVGroup{}).getParam(context.Background(), buildCreateTiKVGroupInstance(params))

			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("invalid exclusive"))
		})
	})
})

var _ = Describe("DeleteTiKVGroup", func() {
	const (
		sharedPoolID  = "2002"
		tikvGroupID   = "tikv-dedicated"
		tenantRegion  = "us-west-2"
		cloudProvider = "aws"
	)

	baseInstance := func(params map[opent.ParamName]string) *opent.Instance {
		instance := &opent.Instance{
			Params: map[opent.ParamName]string{
				opent.ParamResourcePoolID: sharedPoolID,
				opent.ParamTiKVGroupID:    tikvGroupID,
			},
		}
		for k, v := range params {
			instance.Params[k] = v
		}
		return instance
	}

	resourcePool := func() *nextgenv1.ResourcePoolProfile {
		return &nextgenv1.ResourcePoolProfile{
			ResourcePool: &nextgenv1.ResourcePool{
				Id: 2002,
			},
			Region: &commonv1.Region{
				LogicalName:     tenantRegion,
				CloudProviderId: commonv1.CloudProvider_AWS,
			},
		}
	}

	components := func(mgmtHTTP mgmthttpcli.MgmtHTTPClient) *actions.Components {
		return &actions.Components{
			MgmtHTTP: mgmtHTTP,
			CoreSvc: &coresvccli.CoreSvcCli{
				Mgmt: &coresvccli.MgmtService{
					ResourcePool: &fakeResourcePoolServiceClient{resourcePool: resourcePool()},
				},
			},
		}
	}

	Describe("buildDeleteTiKVGroupParam", func() {
		It("builds the delete request", func() {
			req, err := buildDeleteTiKVGroupParam(baseInstance(nil))

			Expect(err).NotTo(HaveOccurred())
			Expect(req.GetSharedPoolId()).To(Equal(sharedPoolID))
			Expect(req.GetTikvGroupId()).To(Equal(tikvGroupID))
		})

		It("requires tikv_group_id", func() {
			instance := baseInstance(nil)
			delete(instance.Params, opent.ParamTiKVGroupID)

			_, err := buildDeleteTiKVGroupParam(instance)

			Expect(err).To(MatchError(ContainSubstring("tikv_group_id is required")))
		})
	})

	Describe("Start", func() {
		It("deletes a TiKV group through the shared pool regional server", func() {
			ctrl := gomock.NewController(GinkgoT())
			regional := mgmthttpcli.NewMockNextGenRegionServer(ctrl)
			regional.EXPECT().DeleteTiKVGroup(gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, req *mgmthttpcli.DeleteTiKVGroupRequest) error {
					Expect(req.GetSharedPoolId()).To(Equal(sharedPoolID))
					Expect(req.GetTikvGroupId()).To(Equal(tikvGroupID))
					return nil
				})

			mgmtHTTP := mgmthttpcli.NewMockMgmtHTTPClient(ctrl)
			mgmtHTTP.EXPECT().NextGenRegionServer(tenantRegion, cloudProvider).Return(regional)

			action := &deleteTiKVGroup{comps: components(mgmtHTTP)}
			result, err := action.Start(context.Background(), baseInstance(nil))

			Expect(err).NotTo(HaveOccurred())
			Expect(result.Result).To(Equal(opent.ActionStatusRunning))
			Expect(result.ActionOutput).To(ContainSubstring(tikvGroupID))
		})
	})

	Describe("Result", func() {
		It("keeps running while the TiKV group exists", func() {
			ctrl := gomock.NewController(GinkgoT())
			regional := mgmthttpcli.NewMockNextGenRegionServer(ctrl)
			regional.EXPECT().GetSharedPool(gomock.Any(), sharedPoolID).Return(&mgmthttpcli.SharedPool{
				ComponentGroups: []*mgmthttpcli.Component{
					{
						Id:            tikvGroupID,
						ComponentType: clusterng.ComponentType_TIKV.String(),
						Phase:         "DELETING",
					},
				},
			}, nil)

			mgmtHTTP := mgmthttpcli.NewMockMgmtHTTPClient(ctrl)
			mgmtHTTP.EXPECT().NextGenRegionServer(tenantRegion, cloudProvider).Return(regional)

			action := &deleteTiKVGroup{comps: components(mgmtHTTP)}
			result, err := action.Result(context.Background(), baseInstance(nil))

			Expect(err).NotTo(HaveOccurred())
			Expect(result.Result).To(Equal(opent.ActionStatusRunning))
			Expect(result.ActionOutput).To(ContainSubstring("DELETING"))
		})

		It("succeeds after the TiKV group disappears", func() {
			ctrl := gomock.NewController(GinkgoT())
			regional := mgmthttpcli.NewMockNextGenRegionServer(ctrl)
			regional.EXPECT().GetSharedPool(gomock.Any(), sharedPoolID).Return(&mgmthttpcli.SharedPool{
				ComponentGroups: []*mgmthttpcli.Component{
					{
						Id:            "default",
						ComponentType: clusterng.ComponentType_TIKV.String(),
						Phase:         "AVAILABLE",
					},
				},
			}, nil)

			mgmtHTTP := mgmthttpcli.NewMockMgmtHTTPClient(ctrl)
			mgmtHTTP.EXPECT().NextGenRegionServer(tenantRegion, cloudProvider).Return(regional)

			action := &deleteTiKVGroup{comps: components(mgmtHTTP)}
			result, err := action.Result(context.Background(), baseInstance(nil))

			Expect(err).NotTo(HaveOccurred())
			Expect(result.Result).To(Equal(opent.ActionStatusSucceeded))
		})
	})
})

func buildCreateTiKVGroupInstance(params map[opent.ParamName]string) *opent.Instance {
	return &opent.Instance{Params: params}
}

func baseCreateTiKVGroupParams() map[opent.ParamName]string {
	return map[opent.ParamName]string{
		opent.ParamResourcePoolID:                         "rp-1",
		opent.ParamTiKVGroupID:                            "tikv1",
		opent.ParamNodeCount:                              "3",
		opent.ParamCpuCores:                               "8",
		opent.ParamMemoryGi:                               "16",
		opent.ParamName("data_storage_type"):              "gp3",
		opent.ParamName("data_storage_size_gi"):           "100",
		opent.ParamName("data_storage_iops"):              "3000",
		opent.ParamName("data_storage_throughput_mbs"):    "125",
		opent.ParamName("raftlog_storage_type"):           "gp3",
		opent.ParamName("raftlog_storage_size_gi"):        "100",
		opent.ParamName("raftlog_storage_iops"):           "3000",
		opent.ParamName("raftlog_storage_throughput_mbs"): "125",
	}
}

var _ = Describe("CreateTiKVGroup", func() {
	defaultTiKVGroup := func() *mgmthttpcli.Component {
		return &mgmthttpcli.Component{
			Id:            "default",
			ComponentType: clusterng.ComponentType_TIKV.String(),
			NodeQuota: &mgmthttpcli.NodeQuota{
				NodeCount: 3,
				NodeSpec: &mgmthttpcli.NodeSpec{
					Cpu:      8,
					MemoryGi: 16,
				},
			},
			Storages: []*mgmthttpcli.Component_Storage{
				{
					Name:          clusterng.Component_Storage_DATA.String(),
					Type:          "gp3",
					SizeGi:        1024,
					Iops:          3000,
					ThroughputMbs: 125,
				},
				{
					Name:          clusterng.Component_Storage_RAFTLOG.String(),
					Type:          "io2",
					SizeGi:        512,
					Iops:          6000,
					ThroughputMbs: 250,
				},
			},
		}
	}

	Describe("findDefaultTiKVGroup", func() {
		It("uses PD component id as the default component id", func() {
			defaultGroup := defaultTiKVGroup()
			sharedPool := &mgmthttpcli.SharedPool{
				ComponentGroups: []*mgmthttpcli.Component{
					{
						Id:            "default",
						ComponentType: clusterng.ComponentType_PD.String(),
					},
					{
						Id:            "other",
						ComponentType: clusterng.ComponentType_TIKV.String(),
					},
					defaultGroup,
				},
			}

			got, err := findDefaultTiKVGroup(sharedPool)

			Expect(err).NotTo(HaveOccurred())
			Expect(got).To(Equal(defaultGroup))
		})
	})

	Describe("buildCreateTiKVGroupRequest", func() {
		It("fills missing spec and storage params from the default TiKV group", func() {
			instance := &opent.Instance{
				Params: map[opent.ParamName]string{
					opent.ParamNodeCount: "5",
					opent.ParamCpuCores:  "12",
				},
			}

			req, err := buildCreateTiKVGroupRequest("123", "newgroup", instance, defaultTiKVGroup())

			Expect(err).NotTo(HaveOccurred())
			Expect(req.GetSharedPoolId()).To(Equal("123"))
			Expect(req.GetTikvGroupId()).To(Equal("newgroup"))
			Expect(req.GetNodeQuota().GetNodeCount()).To(Equal(uint32(5)))
			Expect(req.GetNodeQuota().GetNodeSpec().GetCpu()).To(Equal(uint32(12)))
			Expect(req.GetNodeQuota().GetNodeSpec().GetMemoryGi()).To(Equal(uint32(16)))
			Expect(req.GetStorages()).To(HaveLen(2))
			Expect(req.GetStorages()[0].GetName()).To(Equal(clusterng.Component_Storage_DATA))
			Expect(req.GetStorages()[0].GetType()).To(Equal("gp3"))
			Expect(req.GetStorages()[0].GetSizeGi()).To(Equal(uint32(1024)))
			Expect(req.GetStorages()[0].GetIops()).To(Equal(uint32(3000)))
			Expect(req.GetStorages()[0].GetThroughputMbs()).To(Equal(uint32(125)))
			Expect(req.GetStorages()[1].GetName()).To(Equal(clusterng.Component_Storage_RAFTLOG))
			Expect(req.GetStorages()[1].GetType()).To(Equal("io2"))
			Expect(req.GetStorages()[1].GetSizeGi()).To(Equal(uint32(512)))
			Expect(req.GetStorages()[1].GetIops()).To(Equal(uint32(6000)))
			Expect(req.GetStorages()[1].GetThroughputMbs()).To(Equal(uint32(250)))
		})

		It("requires node_count even when a default TiKV group exists", func() {
			instance := &opent.Instance{
				Params: map[opent.ParamName]string{},
			}

			_, err := buildCreateTiKVGroupRequest("123", "newgroup", instance, defaultTiKVGroup())

			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("node_count"))
		})
	})
})
