package bffapi

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/samber/lo"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/suite"

	"github.com/tidbcloud/tidb-management-service/functional-test/pkg/bffapi"
	"github.com/tidbcloud/tidb-management-service/functional-test/pkg/bffapi/client"
	"github.com/tidbcloud/tidb-management-service/functional-test/pkg/coreportalapi"
	"github.com/tidbcloud/tidb-management-service/functional-test/pkg/coreportalapi/model"
	"github.com/tidbcloud/tidb-management-service/functional-test/pkg/tidbcloudlogin"
	"github.com/tidbcloud/tidb-management-service/utilities/randstr"
)

func TestClusterAdvanceModify(t *testing.T) {
	testSuite := &TestClusterAdvanceModifySuite{
		WebApiLoginContext: &tidbcloudlogin.WebApiLoginContext{
			Host:              viper.GetString("host"),
			Auth0Domain:       viper.GetString("auth0_domain"),
			Auth0ClientID:     viper.GetString("auth0_client_id"),
			Auth0ClientSecret: viper.GetString("auth0_client_secret"),
			UserEmail:         viper.GetString("user_email"),
			BearerToken:       viper.GetString("bearer_token"),
		},

		orgID:             viper.GetString("org_id"),
		prjID:             viper.GetString("prj_id"),
		clusterNamePrefix: viper.GetString("cluster_name_prefix"),

		provider: "aws",
		region:   "us-west-2",
		password: randstr.GenWithCharset(16, randstr.AlphabetCharset),
	}

	testSuite.clusterName = testSuite.clusterNamePrefix + randstr.GenWithCharset(4, randstr.AlphabetCharset)

	suite.Run(t, testSuite)
}

type TestClusterAdvanceModifySuite struct {
	suite.Suite

	*tidbcloudlogin.WebApiLoginContext

	orgID             string
	prjID             string
	clusterNamePrefix string

	provider    string
	region      string
	password    string
	clusterName string
	components  []*model.CentralClusterComponent

	bffApi    *bffapi.Cli
	portalCli *coreportalapi.Cli

	clusterID string
}

func (su *TestClusterAdvanceModifySuite) TearDownSuite() {
	if su.clusterID != "" {
		su.portalCli.EnsureDeleteCluster(su.orgID, su.prjID, su.clusterName)
	}
}

func (su *TestClusterAdvanceModifySuite) Test_01_LoginAndInitializeClient() {
	token, err := su.LoginAndGetToken(su.orgID)
	su.Require().NoError(err)

	su.portalCli, err = coreportalapi.NewClient(&su.Suite, su.Host, token)
	su.Require().NoError(err)
	su.Require().NotNil(su.portalCli)

	su.bffApi, err = bffapi.NewClient(&su.Suite, su.Host, token)
	su.Require().NoError(err)
	su.Require().NotNil(su.bffApi)
}

func (su *TestClusterAdvanceModifySuite) Test_02_ListClusters() {
	_, _, err := su.bffApi.ClusterServiceAPI.ClusterServiceListClusters(context.Background()).
		OrgId(su.orgID).
		ProjectId(su.prjID).
		Execute()
	su.Require().NoError(err)
}

func (su *TestClusterAdvanceModifySuite) Test_03_ListProvidersAndSelectAwsUsWest2() {
	providers := su.portalCli.ListProviders(su.orgID, su.prjID, false)

	p, ok := lo.Find(providers, func(p *model.CentralCloudProvider) bool {
		return p != nil && p.Name != nil && *p.Name == su.provider
	})
	su.Require().True(ok)
	su.Require().NotNil(p.Regions)
	_, ok = lo.Find(p.Regions, func(r *model.CentralRegion) bool {
		return r != nil && r.Name != nil && *r.Name == su.region
	})
	su.Require().True(ok)
}

func (su *TestClusterAdvanceModifySuite) Test_04_CheckProjectRegionCidr() {
	su.portalCli.CheckProjectRegionCidr(su.orgID, su.prjID, su.provider, su.region)
}

func (su *TestClusterAdvanceModifySuite) Test_05_GenerateClusterName() {
	_ = su.portalCli.GenerateClusterName(su.orgID, su.prjID)
}

const (
	defaultTiDBTier = "8C16G"
	defaultTiKVTier = "8C32G"
)

func (su *TestClusterAdvanceModifySuite) Test_06_QueryNodeProfiles() {
	profiles := su.portalCli.QueryNodeProfiles(su.orgID, su.prjID, su.provider, su.region, false)

	var (
		tidbProfile *model.CentralClusterComponent
		tikvProfile *model.CentralClusterComponent
	)

	// Select the node profile with the minimal CPU cores for TiDB and TiKV.
	for _, profile := range profiles {
		switch *profile.ComponentType {
		case model.CentralComponentTypeCOMPONENTTYPETIDB:
			for _, p := range profile.Profiles {
				if tidbProfile == nil {
					if strings.Contains(p.Name, defaultTiDBTier) {
						tidbProfile = &model.CentralClusterComponent{
							ComponentType:   lo.ToPtr(model.CentralComponentTypeCOMPONENTTYPETIDB),
							NodeProfileName: lo.ToPtr(p.Name),
							Replicas:        lo.ToPtr(int32(1)),
						}
					}
				}
			}
		case model.CentralComponentTypeCOMPONENTTYPETIKV:
			for _, p := range profile.Profiles {
				if tikvProfile == nil {
					if strings.Contains(p.Name, defaultTiKVTier) {
						tikvProfile = &model.CentralClusterComponent{
							ComponentType:   lo.ToPtr(model.CentralComponentTypeCOMPONENTTYPETIKV),
							NodeProfileName: lo.ToPtr(p.Name),
							Replicas:        lo.ToPtr(int32(3)),
							StorageSizeGi:   lo.ToPtr(p.MinStorageSizeGi),
						}
					}
				}
			}
		}
	}

	su.components = []*model.CentralClusterComponent{tidbProfile, tikvProfile}
}

func (su *TestClusterAdvanceModifySuite) Test_07_EstimateClusterPrice() {
	_ = su.portalCli.EstimateClusterPrice(su.orgID, su.prjID, su.provider, su.region, su.components)
}

func (su *TestClusterAdvanceModifySuite) Test_08_CreateCluster() {
	su.clusterID = *su.portalCli.CreateCluster(su.orgID, su.prjID, su.provider, su.region, su.clusterName, 4000, su.components, false, 0)
	su.Require().NotEmpty(su.clusterID)

	resp := su.portalCli.GetCluster(su.orgID, su.prjID, su.clusterID)
	su.Require().Equal(1, int(*resp.ClusterComponents.Tidb.Replicas))
}

func (su *TestClusterAdvanceModifySuite) Test_09_PollClusterStatus() {
	// wait for the cluster to be ready
	// timeout: 30 minutes (1800 seconds)
	for range 180 {
		resp, _, err := su.bffApi.ClusterServiceAPI.ClusterServiceGetCluster(context.Background(), su.clusterID).
			OrgId(su.orgID).
			ProjectId(su.prjID).
			Execute()
		if err != nil {
			su.T().Logf("failed to get cluster: %v", err)
			time.Sleep(10 * time.Second)
			continue
		}
		if resp.DedicatedCluster == nil {
			su.T().Logf("cluster is not ready yet")
			time.Sleep(10 * time.Second)
			continue
		}

		if lo.FromPtr(resp.DedicatedCluster.State) == client.COMMONV1BETA1CLUSTERSTATE_ACTIVE {
			break
		}
		su.T().Logf("cluster status: %s", resp.DedicatedCluster.GetState())
		time.Sleep(10 * time.Second)
	}
}

func (su *TestClusterAdvanceModifySuite) Test_10_CreateMultiTiDBGroups() {
	resp, _, err := su.bffApi.ClusterServiceAPI.ClusterServiceAdvancedModifyDedicatedCluster(context.Background(), su.clusterID).
		OrgId(su.orgID).
		ProjectId(su.prjID).
		Cluster(client.ClusterServiceAdvancedModifyDedicatedClusterRequest{
			TikvNodeSetting: &client.V1beta1UpdateClusterRequestStorageNodeSetting{
				NodeSpecKey: lo.ToPtr(defaultTiKVTier),
			},
			TidbNodeSetting: &client.V1beta1AdvancedModifyDedicatedClusterRequestTidbNodeSetting{
				CreateTidbNodeGroups: []client.AdvancedModifyDedicatedClusterRequestTidbNodeGroupCreate{
					{
						DisplayName: "test-0",
						NodeCount:   2,
					},
					{
						DisplayName: "test-1",
						NodeCount:   2,
					},
					{
						DisplayName: "test-2",
						NodeCount:   2,
					},
				},
			},
		}).
		Execute()

	su.Require().NoError(err)
	su.Require().Equal(client.CLUSTERCLUSTERTYPE_DEDICATED, resp.GetClusterType())
	su.Require().NotNil(resp.DedicatedCluster)
	su.Require().Len(resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups, 4)
	su.Require().Equal(defaultTiDBTier, resp.DedicatedCluster.TidbNodeSetting.GetNodeSpecKey())

	actualTiDBCount := lo.SumBy(
		resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups,
		func(item client.Dedicatedv1beta1TidbNodeGroup) int32 {
			return item.NodeCount
		},
	)
	su.Require().Equal(int32(7), actualTiDBCount)
}

func (su *TestClusterAdvanceModifySuite) Test_11_PollClusterStatus() {
	// timeout: 30 minutes (1800 seconds)
	for range 180 {
		resp, _, err := su.bffApi.ClusterServiceAPI.ClusterServiceGetCluster(context.Background(), su.clusterID).
			OrgId(su.orgID).
			ProjectId(su.prjID).
			Execute()
		if err != nil {
			su.T().Logf("failed to get cluster: %v", err)
			time.Sleep(10 * time.Second)
			continue
		}
		if resp.DedicatedCluster == nil {
			su.T().Logf("cluster is not ready yet")
			time.Sleep(10 * time.Second)
			continue
		}

		su.Require().Len(resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups, 4)
		su.Require().Equal(defaultTiDBTier, resp.DedicatedCluster.TidbNodeSetting.GetNodeSpecKey())
		actualTiDBCount := lo.SumBy(
			resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups,
			func(item client.Dedicatedv1beta1TidbNodeGroup) int32 {
				return item.NodeCount
			},
		)
		su.Require().Equal(int32(7), actualTiDBCount)

		if lo.FromPtr(resp.DedicatedCluster.State) == client.COMMONV1BETA1CLUSTERSTATE_ACTIVE {
			break
		}
		su.T().Logf("cluster status: %v", *resp)
		time.Sleep(10 * time.Second)
	}
}

func (su *TestClusterAdvanceModifySuite) Test_12_ModifyTiDBNodeGroups() {
	resp, _, err := su.bffApi.ClusterServiceAPI.ClusterServiceGetCluster(context.Background(), su.clusterID).
		OrgId(su.orgID).
		ProjectId(su.prjID).
		Execute()
	su.Require().NoError(err)

	defaultTidbGroup, ok := lo.Find(
		resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups,
		func(item client.Dedicatedv1beta1TidbNodeGroup) bool {
			return item.GetIsDefaultGroup()
		},
	)
	su.Require().True(ok)

	excludeDefaultTiDBGroups := lo.Filter(
		resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups,
		func(item client.Dedicatedv1beta1TidbNodeGroup, index int) bool {
			return !item.GetIsDefaultGroup()
		},
	)

	updateResp, _, err := su.bffApi.ClusterServiceAPI.ClusterServiceAdvancedModifyDedicatedCluster(context.Background(), su.clusterID).
		OrgId(su.orgID).
		ProjectId(su.prjID).
		Cluster(client.ClusterServiceAdvancedModifyDedicatedClusterRequest{
			TikvNodeSetting: &client.V1beta1UpdateClusterRequestStorageNodeSetting{
				NodeSpecKey: lo.ToPtr(defaultTiKVTier),
			},
			TidbNodeSetting: &client.V1beta1AdvancedModifyDedicatedClusterRequestTidbNodeSetting{
				CreateTidbNodeGroups: []client.AdvancedModifyDedicatedClusterRequestTidbNodeGroupCreate{
					{
						DisplayName: "test-3",
						NodeCount:   1,
					},
				},
				DeleteTidbNodeGroupIds: []string{
					excludeDefaultTiDBGroups[0].GetTidbNodeGroupId(),
					excludeDefaultTiDBGroups[1].GetTidbNodeGroupId(),
				},
				UpdateTidbNodeGroups: []client.AdvancedModifyDedicatedClusterRequestTidbNodeGroupUpdate{
					{
						TidbNodeGroupId: defaultTidbGroup.GetTidbNodeGroupId(),
						NodeCount:       lo.ToPtr(int32(2)),
					},
				},
				NodeSpecKey: lo.ToPtr("8C16G"),
			},
		}).
		Execute()
	su.Require().NoError(err)
	su.Require().Equal(client.CLUSTERCLUSTERTYPE_DEDICATED, updateResp.GetClusterType())
	su.Require().NotNil(updateResp.DedicatedCluster)
	su.Require().Len(updateResp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups, 3)
	su.Require().Equal("8C16G", updateResp.DedicatedCluster.TidbNodeSetting.GetNodeSpecKey())
	actualTiDBCount := lo.SumBy(
		updateResp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups,
		func(item client.Dedicatedv1beta1TidbNodeGroup) int32 {
			return item.NodeCount
		},
	)
	su.Require().Equal(int32(5), actualTiDBCount)
}

func (su *TestClusterAdvanceModifySuite) Test_13_PollClusterStatus() {
	// timeout: 30 minutes (1800 seconds)
	for range 180 {
		resp, _, err := su.bffApi.ClusterServiceAPI.ClusterServiceGetCluster(context.Background(), su.clusterID).
			OrgId(su.orgID).
			ProjectId(su.prjID).
			Execute()
		if err != nil {
			su.T().Logf("failed to get cluster: %v", err)
			time.Sleep(10 * time.Second)
			continue
		}
		if resp.DedicatedCluster == nil {
			su.T().Logf("cluster is not ready yet")
			time.Sleep(10 * time.Second)
			continue
		}

		su.Require().Len(resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups, 3)
		su.Require().Equal("8C16G", resp.DedicatedCluster.TidbNodeSetting.GetNodeSpecKey())
		actualTiDBCount := lo.SumBy(
			resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups,
			func(item client.Dedicatedv1beta1TidbNodeGroup) int32 {
				return item.NodeCount
			},
		)
		su.Require().Equal(int32(5), actualTiDBCount)

		if lo.FromPtr(resp.DedicatedCluster.State) == client.COMMONV1BETA1CLUSTERSTATE_ACTIVE {
			break
		}
		su.T().Logf("cluster status: %v", *resp)
		time.Sleep(10 * time.Second)
	}
}

func (su *TestClusterAdvanceModifySuite) Test_14_CreateManyTiDBGroups() {
	groups := make([]client.AdvancedModifyDedicatedClusterRequestTidbNodeGroupCreate, 0)
	for i := range 10 {
		groups = append(groups, client.AdvancedModifyDedicatedClusterRequestTidbNodeGroupCreate{
			DisplayName: fmt.Sprintf("test-group-number-%d", i),
			NodeCount:   1,
		})
	}
	resp, _, err := su.bffApi.ClusterServiceAPI.ClusterServiceAdvancedModifyDedicatedCluster(context.Background(), su.clusterID).
		OrgId(su.orgID).
		ProjectId(su.prjID).
		Cluster(client.ClusterServiceAdvancedModifyDedicatedClusterRequest{
			TikvNodeSetting: &client.V1beta1UpdateClusterRequestStorageNodeSetting{
				NodeSpecKey: lo.ToPtr(defaultTiKVTier),
			},
			TidbNodeSetting: &client.V1beta1AdvancedModifyDedicatedClusterRequestTidbNodeSetting{
				CreateTidbNodeGroups: groups,
			},
		}).
		Execute()
	su.Require().NoError(err)

	su.Require().Equal(client.CLUSTERCLUSTERTYPE_DEDICATED, resp.GetClusterType())
	su.Require().NotNil(resp.DedicatedCluster)
	su.Require().Len(resp.DedicatedCluster.TidbNodeSetting.TidbNodeGroups, 13)
}
