package ngoperation

import (
	"context"
	"strconv"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"go.uber.org/mock/gomock"

	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/infra/eventbus"
	"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("ScaleNextGenResourcePool", func() {
	const sharedPoolID = "2055088224153374720"

	baseInstance := func(componentType string) *opent.Instance {
		return &opent.Instance{
			Applicant: "operator@example.com",
			Params: map[opent.ParamName]string{
				opent.ParamResourcePoolID: sharedPoolID,
				opent.ParamComponentType:  componentType,
				opent.ParamNodeCount:      "3",
			},
		}
	}

	resourcePool := func(ownerType nextgenv1.ResourcePool_OwnerType) *nextgenv1.ResourcePoolProfile {
		resourcePoolID, err := strconv.ParseUint(sharedPoolID, 10, 64)
		Expect(err).NotTo(HaveOccurred())
		return &nextgenv1.ResourcePoolProfile{
			ResourcePool: &nextgenv1.ResourcePool{
				Id:        resourcePoolID,
				OwnerType: ownerType,
			},
			Region: &commonv1.Region{
				LogicalName:     "us-east-1",
				CloudProviderId: commonv1.CloudProvider_AWS,
			},
		}
	}

	sharedPool := func(components ...*mgmthttpcli.Component) *mgmthttpcli.SharedPool {
		return &mgmthttpcli.SharedPool{
			Id:              sharedPoolID,
			Phase:           "AVAILABLE",
			ComponentGroups: components,
		}
	}

	componentWithID := func(componentID, componentType string, nodeCount uint32) *mgmthttpcli.Component {
		return &mgmthttpcli.Component{
			Id:            componentID,
			ComponentType: componentType,
			Phase:         "AVAILABLE",
			NodeQuota: &mgmthttpcli.NodeQuota{
				NodeCount: nodeCount,
				NodeSpec:  &mgmthttpcli.NodeSpec{Cpu: 4, MemoryGi: 8},
			},
		}
	}

	component := func(componentType string, nodeCount uint32) *mgmthttpcli.Component {
		return componentWithID("default", componentType, nodeCount)
	}

	newAction := func(ctrl *gomock.Controller, regional mgmthttpcli.NextGenRegionServer, rp *nextgenv1.ResourcePoolProfile) *scaleResourcePool {
		mgmt := mgmthttpcli.NewMockMgmtHTTPClient(ctrl)
		mgmt.EXPECT().NextGenRegionServer("us-east-1", "aws").Return(regional).AnyTimes()
		return &scaleResourcePool{comps: &actions.Components{
			MgmtHTTP: mgmt,
			CoreSvc: &coresvccli.CoreSvcCli{
				Mgmt: &coresvccli.MgmtService{
					ResourcePool: &fakeResourcePoolServiceClient{resourcePool: rp},
				},
			},
			EventBus: eventbus.NewMockEventBus(),
		}}
	}

	Describe("TIPROXY shared-pool guardrail", func() {
		It("allows TIPROXY for shared resource pools and calls the existing shared-pool scale API", func() {
			ctrl := gomock.NewController(GinkgoT())
			regional := &fakeScaleResourcePoolRegionServer{
				sharedPool: sharedPool(component(resourcePoolTiProxyComponentType, 2)),
			}
			action := newAction(ctrl, regional, resourcePool(nextgenv1.ResourcePool_SHARED))
			instance := baseInstance(resourcePoolTiProxyComponentType)

			err := action.PreCheck(context.Background(), instance)
			Expect(err).NotTo(HaveOccurred())
			Expect(instance.Params[opent.ParamScaleDirection]).To(Equal("up"))

			result, err := action.Start(context.Background(), instance)
			Expect(err).NotTo(HaveOccurred())
			Expect(result.Result).To(Equal(opent.ActionStatusRunning))
			Expect(regional.scaleSharedPoolID).To(Equal(sharedPoolID))
			Expect(regional.scaleReq).NotTo(BeNil())
			Expect(regional.scaleReq.Component.Id).To(BeEmpty())
			Expect(regional.scaleReq.Component.ComponentType).To(Equal(resourcePoolTiProxyComponentType))
			Expect(regional.scaleReq.Component.NodeQuota.NodeCount).To(Equal(uint32(3)))
		})

		It("uses component_id to target one component when multiple components share a type", func() {
			const targetComponentID = "tikv-b"

			ctrl := gomock.NewController(GinkgoT())
			regional := &fakeScaleResourcePoolRegionServer{
				sharedPool: sharedPool(
					componentWithID("tikv-a", "TIKV", 6),
					componentWithID(targetComponentID, "TIKV", 2),
				),
			}
			action := newAction(ctrl, regional, resourcePool(nextgenv1.ResourcePool_ORGANIZATION))
			instance := baseInstance("TIKV")
			instance.Params[opent.ParamComponentID] = targetComponentID

			err := action.PreCheck(context.Background(), instance)
			Expect(err).NotTo(HaveOccurred())
			Expect(instance.Params[opent.ParamScaleDirection]).To(Equal("up"))

			result, err := action.Start(context.Background(), instance)
			Expect(err).NotTo(HaveOccurred())
			Expect(result.Result).To(Equal(opent.ActionStatusRunning))
			Expect(regional.scaleSharedPoolID).To(Equal(sharedPoolID))
			Expect(regional.scaleReq).NotTo(BeNil())
			Expect(regional.scaleReq.Component.Id).To(Equal(targetComponentID))
			Expect(regional.scaleReq.Component.ComponentType).To(Equal("TIKV"))
			Expect(regional.scaleReq.Component.NodeQuota.NodeCount).To(Equal(uint32(3)))

			result, err = action.Result(context.Background(), instance)
			Expect(err).NotTo(HaveOccurred())
			Expect(result.Result).To(Equal(opent.ActionStatusSucceeded))
		})

		It("rejects a missing component_id during precheck", func() {
			ctrl := gomock.NewController(GinkgoT())
			regional := &fakeScaleResourcePoolRegionServer{
				sharedPool: sharedPool(componentWithID("tikv-a", "TIKV", 6)),
			}
			action := newAction(ctrl, regional, resourcePool(nextgenv1.ResourcePool_ORGANIZATION))
			instance := baseInstance("TIKV")
			instance.Params[opent.ParamComponentID] = "tikv-b"

			err := action.PreCheck(context.Background(), instance)
			Expect(err).To(MatchError("component tikv-b of type TIKV is not found in shared pool"))
		})

		It("rejects TIPROXY for organization-owned resource pools", func() {
			ctrl := gomock.NewController(GinkgoT())
			regional := &fakeScaleResourcePoolRegionServer{}
			action := newAction(ctrl, regional, resourcePool(nextgenv1.ResourcePool_ORGANIZATION))
			instance := baseInstance(resourcePoolTiProxyComponentType)

			err := action.PreCheck(context.Background(), instance)
			Expect(err).To(MatchError(ContainSubstring("TIPROXY can only be scaled at pool level for shared resource pools")))

			result, err := action.Start(context.Background(), instance)
			Expect(err).NotTo(HaveOccurred())
			Expect(result.Result).To(Equal(opent.ActionStatusFailed))
			Expect(result.ActionOutput).To(ContainSubstring("TIPROXY can only be scaled at pool level for shared resource pools"))
			Expect(regional.getSharedPoolCalls).To(Equal(0))
			Expect(regional.scaleReq).To(BeNil())
		})

		It("rejects TIPROXY when the resource pool client returns no profile", func() {
			ctrl := gomock.NewController(GinkgoT())
			regional := &fakeScaleResourcePoolRegionServer{}
			action := newAction(ctrl, regional, nil)
			instance := baseInstance(resourcePoolTiProxyComponentType)

			result, err := action.Start(context.Background(), instance)
			Expect(err).NotTo(HaveOccurred())
			Expect(result.Result).To(Equal(opent.ActionStatusFailed))
			Expect(result.ActionOutput).To(ContainSubstring("resource pool " + sharedPoolID + " not found"))
			Expect(regional.getSharedPoolCalls).To(Equal(0))
			Expect(regional.scaleReq).To(BeNil())
		})

		It("returns a clear error for a nil resource pool profile", func() {
			err := validateScaleResourcePoolTarget(resourcePoolTiProxyComponentType, sharedPoolID, nil)
			Expect(err).To(MatchError(ContainSubstring("resource pool " + sharedPoolID + " is not found")))
		})

		It("keeps non-TIPROXY resource-pool scale behavior unchanged", func() {
			ctrl := gomock.NewController(GinkgoT())
			regional := &fakeScaleResourcePoolRegionServer{
				sharedPool: sharedPool(component("TIKV", 6)),
			}
			action := newAction(ctrl, regional, resourcePool(nextgenv1.ResourcePool_ORGANIZATION))
			instance := baseInstance("TIKV")

			err := action.PreCheck(context.Background(), instance)
			Expect(err).NotTo(HaveOccurred())
			Expect(instance.Params[opent.ParamScaleDirection]).To(Equal("down"))
		})
	})
})

type fakeScaleResourcePoolRegionServer struct {
	mgmthttpcli.NextGenRegionServer

	sharedPool         *mgmthttpcli.SharedPool
	getSharedPoolCalls int
	scaleSharedPoolID  string
	scaleReq           *mgmthttpcli.ScaleSharedPoolRequest
}

func (f *fakeScaleResourcePoolRegionServer) GetSharedPool(context.Context, string) (*mgmthttpcli.SharedPool, error) {
	f.getSharedPoolCalls++
	return f.sharedPool, nil
}

func (f *fakeScaleResourcePoolRegionServer) ScaleSharedPool(_ context.Context, sharedPoolID string, req *mgmthttpcli.ScaleSharedPoolRequest) (*mgmthttpcli.SharedPool, error) {
	f.scaleSharedPoolID = sharedPoolID
	f.scaleReq = req
	return f.sharedPool, nil
}
