package cloudconfig

import (
	"context"
	"time"

	gk "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	ccproto "github.com/tidbcloud/config-center/proto/config_center"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/pkg/infra/clients/configcentercli"
	ccsvcapi "github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
)

var _ = gk.Describe("[ClusterComponent]", func() {
	gk.Describe("normalizeSearchClusterComponentsParam", func() {
		gk.It("fills defaults and caps per_page to config center max", func() {
			param := normalizeSearchClusterComponentsParam(ccsvcapi.SearchClusterComponentsParam{
				PerPage: configCenterClusterComponentPageSize + 1,
			})

			Expect(param.RegionMode).To(Equal(ccsvcapi.RegionModeAuto))
			Expect(param.Page).To(Equal(defaultClusterComponentSearchPage))
			Expect(param.PerPage).To(Equal(configCenterClusterComponentPageSize))
		})
	})

	gk.Describe("clusterComponentVersionsFromConfigCenter", func() {
		gk.It("maps config center components and keeps the resolved logical region", func() {
			items := clusterComponentVersionsFromConfigCenter([]*ccproto.ClusterComponent{
				{
					ClusterType:       ccproto.ClusterType_LOGICAL_CLUSTER,
					ClusterId:         "123",
					ComponentName:     "tidb",
					Version:           "v8.5.0",
					CloudProvider:     "aws",
					NodeInstance:      "8C16G",
					OverwriteImageTag: "override",
					BaseImage:         "base",
					EffectiveImageTag: "effective",
					UpdateStrategy:    ccproto.UpdateStrategy_UPDATE_NEW,
				},
			}, clusterComponentSearchPlan{
				ClusterType:   ccent.ClusterTypeLogicalCluster,
				LogicalRegion: "us-west-2",
			})

			Expect(items).To(HaveLen(1))
			Expect(items[0]).To(Equal(&ccent.ClusterComponentVersion{
				ClusterType:       ccent.ClusterTypeLogicalCluster,
				ClusterID:         "123",
				ComponentName:     "tidb",
				Version:           "v8.5.0",
				CloudProvider:     "aws",
				NodeInstance:      "8C16G",
				OverwriteImageTag: "override",
				BaseImage:         "base",
				EffectiveImageTag: "effective",
				UpdateStrategy:    ccproto.UpdateStrategy_UPDATE_NEW.String(),
				LogicalRegion:     "us-west-2",
			}))
		})
	})

	gk.Describe("ListAvailableComponentVersions", func() {
		gk.It("uses requested cluster type for config-center versions", func() {
			ctrl := gomock.NewController(gk.GinkgoT())
			defer ctrl.Finish()

			cfgCenterAPI := configcentercli.NewMockConfigCenterServiceClient(ctrl)
			service := &svc{
				cfgCenter: &configcentercli.Client{ConfigAPI: cfgCenterAPI},
			}
			cfgCenterAPI.EXPECT().
				ListAvailableVersions(gomock.Any(), gomock.AssignableToTypeOf(&ccproto.ListAvailableVersionsRequest{})).
				DoAndReturn(func(_ context.Context, req *ccproto.ListAvailableVersionsRequest, _ ...grpc.CallOption) (*ccproto.ListAvailableVersionsResponse, error) {
					Expect(req.ClusterType).To(Equal(ccproto.ClusterType_LOGICAL_CLUSTER))
					Expect(req.Component).To(Equal("tidb"))
					return &ccproto.ListAvailableVersionsResponse{Versions: []string{"v8.5.0"}}, nil
				})

			versions, err := service.ListAvailableComponentVersions(context.Background(), ccent.ClusterTypeLogicalCluster, "tidb")

			Expect(err).ToNot(HaveOccurred())
			Expect(versions).To(Equal([]string{"v8.5.0"}))
		})
	})

	gk.Describe("searchClusterComponentsByPlan", func() {
		gk.It("passes empty component filters through to config center", func() {
			ctx := context.Background()
			ctrl := gomock.NewController(gk.GinkgoT())
			defer ctrl.Finish()

			cfgCenterAPI := configcentercli.NewMockConfigCenterServiceClient(ctrl)
			service := &svc{
				cfgCenter: &configcentercli.Client{ConfigAPI: cfgCenterAPI},
			}
			cfgCenterAPI.EXPECT().
				SearchClusterComponents(gomock.Any(), gomock.AssignableToTypeOf(&ccproto.SearchClusterComponentsRequest{})).
				DoAndReturn(func(reqCtx context.Context, req *ccproto.SearchClusterComponentsRequest, _ ...grpc.CallOption) (*ccproto.SearchClusterComponentsResponse, error) {
					deadline, ok := reqCtx.Deadline()
					Expect(ok).To(BeTrue())
					remaining := time.Until(deadline)
					Expect(remaining).To(BeNumerically("<=", configCenterSearchClusterComponentsTimeout))
					Expect(remaining).To(BeNumerically(">", configCenterSearchClusterComponentsTimeout-time.Second))
					Expect(req.ComponentName).To(Equal(""))
					Expect(req.ComponentVersion).To(Equal(""))
					Expect(req.ImageTag).To(Equal(""))
					return &ccproto.SearchClusterComponentsResponse{}, nil
				})

			_, err := service.searchClusterComponentsByPlan(ctx, clusterComponentSearchPlan{
				ClusterType: ccent.ClusterTypeLogicalCluster,
			}, ccsvcapi.SearchClusterComponentsParam{}, 1, 100)

			Expect(err).ToNot(HaveOccurred())
		})
	})

	gk.Describe("searchGlobalClusterComponents", func() {
		gk.It("requires component_name", func() {
			_, err := (&svc{}).searchGlobalClusterComponents(context.Background(), ccsvcapi.SearchClusterComponentsParam{
				ClusterType: ccent.ClusterTypeDedicatedTiDB,
			})

			Expect(err).To(MatchError(ContainSubstring("component_name is required for global search")))
		})
	})

	gk.Describe("paginateClusterComponentVersions", func() {
		gk.It("returns stable pages and empty slices past the end", func() {
			items := []*ccent.ClusterComponentVersion{
				{ClusterID: "1"},
				{ClusterID: "2"},
				{ClusterID: "3"},
			}

			Expect(paginateClusterComponentVersions(items, 2, 2)).To(Equal([]*ccent.ClusterComponentVersion{{ClusterID: "3"}}))
			Expect(paginateClusterComponentVersions(items, 3, 2)).To(BeEmpty())
		})

		gk.It("does not overflow when page is large", func() {
			items := []*ccent.ClusterComponentVersion{
				{ClusterID: "1"},
			}

			Expect(paginateClusterComponentVersions(items, 5_000_000, 500)).To(BeEmpty())
		})
	})

	gk.Describe("hasMoreClusterComponentPage", func() {
		gk.It("calculates with int64 operands", func() {
			Expect(hasMoreClusterComponentPage(5_000_000, 500, 100)).To(BeFalse())
			Expect(hasMoreClusterComponentPage(5_000_000, 500, 3_000_000_000)).To(BeTrue())
		})
	})

	gk.Describe("sortClusterComponentVersions", func() {
		gk.It("uses component fields as tie breakers for deterministic pagination", func() {
			items := []*ccent.ClusterComponentVersion{
				{ClusterType: ccent.ClusterTypeLogicalCluster, LogicalRegion: "us-west-2", ClusterID: "1", ComponentName: "tidb", Version: "v8.5.1"},
				{ClusterType: ccent.ClusterTypeLogicalCluster, LogicalRegion: "us-west-2", ClusterID: "1", ComponentName: "tidb", Version: "v8.5.0", EffectiveImageTag: "b"},
				{ClusterType: ccent.ClusterTypeLogicalCluster, LogicalRegion: "us-west-2", ClusterID: "1", ComponentName: "tidb", Version: "v8.5.0", EffectiveImageTag: "a"},
			}

			sortClusterComponentVersions(items)

			Expect(items).To(Equal([]*ccent.ClusterComponentVersion{
				{ClusterType: ccent.ClusterTypeLogicalCluster, LogicalRegion: "us-west-2", ClusterID: "1", ComponentName: "tidb", Version: "v8.5.0", EffectiveImageTag: "a"},
				{ClusterType: ccent.ClusterTypeLogicalCluster, LogicalRegion: "us-west-2", ClusterID: "1", ComponentName: "tidb", Version: "v8.5.0", EffectiveImageTag: "b"},
				{ClusterType: ccent.ClusterTypeLogicalCluster, LogicalRegion: "us-west-2", ClusterID: "1", ComponentName: "tidb", Version: "v8.5.1"},
			}))
		})
	})
})
