package cluster

import (
	"math/rand/v2"
	"strconv"
	"testing"
	"time"

	"github.com/onsi/gomega"
	"github.com/samber/lo"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	clusterdedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"go.uber.org/mock/gomock"
	"google.golang.org/protobuf/types/known/timestamppb"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/account"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/account/mock_account"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/billing"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/billing/mock_billing"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/clusterconfig/mock_clusterconfig"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/clustermetric/mock_clustermetric"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/regionserver/mock_regionserver"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/remoteencryptor/mock_remoteencryptor"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
)

func TestBuildModifyProgress(t *testing.T) {
	g := gomega.NewWithT(t)

	tests := []struct {
		name       string
		components []*entity.ClusterComponent
		nodeInfos  []*regionaldedicatedv1.ComponentNodeInfo
		expected   func(result []*globaldedicatedv1.Cluster_ClusterComponent)
	}{
		{
			name: "only one component without node info no modify progress",
			components: []*entity.ClusterComponent{
				{
					Id:                 1,
					ClusterId:          1,
					ComponentType:      globaldedicatedv1.ComponentType_TIDB,
					SpuName:            "2C4G",
					Count:              lo.ToPtr(uint32(10)),
					Storages:           nil,
					TidbGroupId:        1,
					IsDefaultTidbGroup: true,
					InstanceType:       "t2.micro",
					CpuNum:             2,
					MemorySizeGi:       4,
					InstanceCpuArch:    globaldedicatedv1.InstanceMu_AMD64,
				},
			},
			nodeInfos: []*regionaldedicatedv1.ComponentNodeInfo{},
			expected: func(result []*globaldedicatedv1.Cluster_ClusterComponent) {
				g.Expect(len(result)).To(gomega.Equal(1))
				g.Expect(result[0].ComponentType).To(gomega.Equal(globaldedicatedv1.ComponentType_TIDB))
				g.Expect(result[0].ModifyProgress).To(gomega.BeNil())
			},
		},
		{
			name: "scale up component with modify progress",
			components: []*entity.ClusterComponent{
				{
					Id:                 1,
					ClusterId:          1,
					ComponentType:      globaldedicatedv1.ComponentType_TIDB,
					SpuName:            "2C4G",
					Count:              lo.ToPtr(uint32(3)),
					Storages:           nil,
					TidbGroupId:        1,
					IsDefaultTidbGroup: true,
					InstanceType:       "t2.mdium",
					CpuNum:             4,
					MemorySizeGi:       8,
					InstanceCpuArch:    globaldedicatedv1.InstanceMu_AMD64,
				},
			},
			nodeInfos: []*regionaldedicatedv1.ComponentNodeInfo{
				{
					ComponentType: regionaldedicatedv1.ComponentType_TIDB,
					Nodes: []*regionaldedicatedv1.ComponentNodeInfo_Node{
						{
							InstanceType:      "t2.micro",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     0,
							StorageIops:       0,
							StorageThroughput: 0,
							AvailabilityZone:  "us-west-1a",
						},
						{
							InstanceType:      "t2.micro",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     0,
							StorageIops:       0,
							StorageThroughput: 0,
							AvailabilityZone:  "us-west-1b",
						},
						{
							InstanceType:      "t2.micro",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     0,
							StorageIops:       0,
							StorageThroughput: 0,
							AvailabilityZone:  "us-west-1c",
						},
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     0,
							StorageIops:       0,
							StorageThroughput: 0,
							AvailabilityZone:  "us-west-1a",
						},
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_CREATING,
							StorageSizeGi:     0,
							StorageIops:       0,
							StorageThroughput: 0,
							AvailabilityZone:  "us-west-1a",
						},
					},
					TidbGroupId: "1",
				},
			},
			expected: func(result []*globaldedicatedv1.Cluster_ClusterComponent) {
				g.Expect(len(result)).To(gomega.Equal(1))
				g.Expect(result[0].ComponentType).To(gomega.Equal(globaldedicatedv1.ComponentType_TIDB))
				g.Expect(result[0].ModifyProgress).NotTo(gomega.BeNil())
				g.Expect(result[0].ModifyProgress.UptodateQuantity).To(gomega.Equal(int32(1)))
				g.Expect(result[0].ModifyProgress.RemainingDeletionQuantity).To(gomega.Equal(int32(0)))
			},
		},
		{
			name: "scale in component with modify progress",
			components: []*entity.ClusterComponent{
				{
					Id:                 1,
					ClusterId:          1,
					ComponentType:      globaldedicatedv1.ComponentType_TIDB,
					SpuName:            "2C4G",
					Count:              lo.ToPtr(uint32(1)),
					Storages:           nil,
					TidbGroupId:        1,
					IsDefaultTidbGroup: true,
					InstanceType:       "t2.mdium",
					CpuNum:             2,
					MemorySizeGi:       4,
					InstanceCpuArch:    globaldedicatedv1.InstanceMu_AMD64,
				},
				{
					Id:            2,
					ClusterId:     1,
					ComponentType: globaldedicatedv1.ComponentType_TIKV,
					SpuName:       "2C4G",
					Count:         lo.ToPtr(uint32(3)),
					Storages: map[string]*globaldedicatedv1.Cluster_ClusterComponent_Storage{
						globaldedicatedv1.Sku_DATA.String(): {
							SizeGi:          100,
							Iops:            1000,
							ThroughputMibps: 1000,
							VolumeType:      lo.ToPtr(globaldedicatedv1.StorageMu_IO2),
						},
					},
					TidbGroupId:        1,
					IsDefaultTidbGroup: true,
					InstanceType:       "t2.mdium",
					CpuNum:             4,
					MemorySizeGi:       8,
					InstanceCpuArch:    globaldedicatedv1.InstanceMu_AMD64,
				},
			},
			nodeInfos: []*regionaldedicatedv1.ComponentNodeInfo{
				{
					ComponentType: regionaldedicatedv1.ComponentType_TIDB,
					Nodes: []*regionaldedicatedv1.ComponentNodeInfo_Node{
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     0,
							StorageIops:       0,
							StorageThroughput: 0,
							AvailabilityZone:  "us-west-1a",
						},
					},
					TidbGroupId: "1",
				},
				{
					ComponentType: regionaldedicatedv1.ComponentType_TIKV,
					Nodes: []*regionaldedicatedv1.ComponentNodeInfo_Node{
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     100,
							StorageIops:       1000,
							StorageThroughput: 1000,
							VolumeType:        "io2",
						},
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     100,
							StorageIops:       1000,
							StorageThroughput: 1000,
							VolumeType:        "io2",
						},
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     100,
							StorageIops:       1000,
							StorageThroughput: 1000,
							VolumeType:        "io2",
						},
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     100,
							StorageIops:       1000,
							StorageThroughput: 0,
							VolumeType:        "io2",
						},
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     100,
							StorageIops:       1000,
							StorageThroughput: 0,
							VolumeType:        "io2",
						},
						{
							InstanceType:      "t2.mdium",
							State:             regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL,
							StorageSizeGi:     100,
							StorageIops:       1000,
							StorageThroughput: 0,
							VolumeType:        "io2",
						},
					},
					TidbGroupId: "",
				},
			},
			expected: func(result []*globaldedicatedv1.Cluster_ClusterComponent) {
				g.Expect(len(result)).To(gomega.Equal(2))
				for _, r := range result {
					switch r.ComponentType {
					case globaldedicatedv1.ComponentType_TIDB:
						g.Expect(r.ModifyProgress).To(gomega.BeNil())
					case globaldedicatedv1.ComponentType_TIKV:
						g.Expect(r.ModifyProgress).NotTo(gomega.BeNil())
						g.Expect(r.ModifyProgress.UptodateQuantity).To(gomega.Equal(int32(3)))
						g.Expect(r.ModifyProgress.RemainingDeletionQuantity).To(gomega.Equal(int32(3)))
					default:
						t.Error("unexpected component type")
					}
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := buildComponents(t.Context(), globaldedicatedv1.Cluster_SCALING, tt.components, tt.nodeInfos, 1, 1)
			tt.expected(got)
		})
	}
}

func TestInsufficientResourceField(t *testing.T) {
	ctrl := gomock.NewController(t, gomock.WithOverridableExpectations())

	encryptor := mock_remoteencryptor.NewMockClient(ctrl)
	encryptor.EXPECT().EncryptBase64(gomock.Any(), gomock.Any()).Return("encrypted", nil).AnyTimes()
	encryptor.EXPECT().DecryptBase64(gomock.Any(), gomock.Any()).Return("password", nil).AnyTimes()

	accountCli := mock_account.NewMockClient(ctrl)
	accountCli.EXPECT().GetProject(gomock.Any(), uint64(ProjectID)).Return(&account.ProjectInfo{
		ID:             ProjectID,
		TenantID:       TenantID,
		Name:           "test",
		AwsCmekEnabled: false,
	}, nil).AnyTimes()
	accountCli.EXPECT().GetTenant(gomock.Any(), uint64(TenantID)).Return(&account.TenantInfo{
		ID:   TenantID,
		Name: "test",
		Plan: account.TenantPlanPOC,
	}, nil).AnyTimes()

	regionalCli := mock_regionserver.NewMockClient(ctrl)
	regionalCli.EXPECT().ListVpcs(gomock.Any(), gomock.Any(), &regionaldedicatedv1.ListVpcsRequest{
		TenantId:  strconv.FormatUint(TenantID, 10),
		ProjectId: strconv.FormatUint(ProjectID, 10),
		Statuses: []regionaldedicatedv1.Vpc_Status{
			regionaldedicatedv1.Vpc_ACTIVE,
			regionaldedicatedv1.Vpc_CREATING,
			regionaldedicatedv1.Vpc_FAILED,
		},
	}).Return(&regionaldedicatedv1.ListVpcsResponse{
		Vpcs: []*regionaldedicatedv1.Vpc{
			{
				VpcId:      "vpc-xxxxxxxxxxxxxxxxx",
				TenantIds:  []string{strconv.FormatUint(TenantID, 10)},
				ProjectIds: []string{strconv.FormatUint(ProjectID, 10)},
				CidrBlock:  "0.0.0.0/0",
			},
		},
	}, nil).AnyTimes()

	metricCli := mock_clustermetric.NewMockClient(ctrl)

	billingCli := mock_billing.NewMockClient(ctrl)
	billingCli.EXPECT().
		BillingActionCheck(gomock.Any(), billing.Action_Create, uint64(TenantID), gomock.Any()).
		Return(&billing.ActionCheckResult{Pass: true}, nil).
		AnyTimes()

	clusterConfigCli := mock_clusterconfig.NewMockClient(ctrl)
	clusterConfigCli.EXPECT().GetClusterVersion(gomock.Any(), gomock.Any()).Return(ClusterVersion, nil).AnyTimes()
	clusterConfigCli.EXPECT().
		ListComponentConfigurations(gomock.Any(), gomock.Any()).
		Return([]*regionaldedicatedv1.ComponentConfiguration{}, nil).
		AnyTimes()

	svc := newTestService(encryptor, accountCli, regionalCli, metricCli, billingCli, clusterConfigCli, true)

	t.Run("insufficient resource field should present", func(t *testing.T) {
		req := &globaldedicatedv1.CreateClusterRequest{
			Cluster: &globaldedicatedv1.Cluster{
				ClusterId:       uint64(rand.Int64()),
				OrganizationId:  TenantID,
				ProjectId:       ProjectID,
				CloudProviderId: globalcommonv1.CloudProvider_AWS,
				RegionId:        globalcommonv1.Region_AWS_US_WEST_2,
				Port:            4000,
				ClusterComponents: []*globaldedicatedv1.Cluster_ClusterComponent{
					{
						ComponentType: globaldedicatedv1.ComponentType_TIDB,
						SpuName:       "8c8g",
						Count:         lo.ToPtr(uint32(1)),
					},
					{
						ComponentType: globaldedicatedv1.ComponentType_TIKV,
						SpuName:       "8c8g",
						Count:         lo.ToPtr(uint32(3)),
						Storages: map[string]*globaldedicatedv1.Cluster_ClusterComponent_Storage{
							globaldedicatedv1.Sku_DATA.String(): {
								VolumeType: lo.ToPtr(globaldedicatedv1.StorageMu_GP3),
								SizeGi:     555,
							},
							globaldedicatedv1.Sku_RAFT_LOG.String(): {
								VolumeType: lo.ToPtr(globaldedicatedv1.StorageMu_VOLUME_TYPE_UNSPECIFIED),
							},
						},
					},
					{
						ComponentType: globaldedicatedv1.ComponentType_TIFLASH,
						SpuName:       "8c8g",
						Count:         lo.ToPtr(uint32(1)),
						Storages: map[string]*globaldedicatedv1.Cluster_ClusterComponent_Storage{
							globaldedicatedv1.Sku_DATA.String(): {
								VolumeType:      lo.ToPtr(globaldedicatedv1.StorageMu_GP3),
								SizeGi:          555,
								Iops:            555,
								ThroughputMibps: 555,
							},
							globaldedicatedv1.Sku_RAFT_LOG.String(): {
								VolumeType: lo.ToPtr(globaldedicatedv1.StorageMu_VOLUME_TYPE_UNSPECIFIED),
							},
						},
					},
				},
			},
		}
		createdCluster, err := svc.Create(t.Context(), req)
		require.NoError(t, err)

		svc.cfg.BizConfig.InsufficientResourceTimeout = "10s"
		regionalCli.EXPECT().
			ListClusters(gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&clusterdedicatedv1.ListClustersResponse{
				Clusters: []*clusterdedicatedv1.Cluster{
					{
						Id: strconv.FormatUint(createdCluster.GetClusterId(), 10),
						Conditions: []*clusterdedicatedv1.ClusterCondition{
							{
								ClusterConditionType: clusterdedicatedv1.ClusterCondition_ClusterNodeReady,
								ConditionStatus:      clusterdedicatedv1.ClusterCondition_False,
								LastTransitionTime:   timestamppb.New(time.Now().Add(-time.Hour)),
							},
						},
					},
				},
			}, nil).
			AnyTimes()

		gotCluster, err := svc.Get(
			t.Context(),
			&globaldedicatedv1.GetClusterRequest{ClusterId: createdCluster.GetClusterId()},
		)
		require.NoError(t, err)

		assert.True(t, gotCluster.GetInsufficientVmResource())
	})
}
