package webapidedicated

import (
	"fmt"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/samber/lo"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/suite"
	"github.com/tidbcloud/pkgv2/gormutil"
	"gorm.io/gorm"

	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
	"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 TestDedicatedClusterMetering(t *testing.T) {
	testSuite := &TestDedicatedClusterMeteringSuite{
		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)

	if strings.Contains(viper.GetString("host"), "prod") {
		t.Log("Skip dedicated cluster metering test in the prod environment")
		return
	}

	suite.Run(t, testSuite)
}

type TestDedicatedClusterMeteringSuite struct {
	suite.Suite

	*tidbcloudlogin.WebApiLoginContext

	orgID             string
	prjID             string
	clusterNamePrefix string

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

	portalCli *coreportalapi.Cli

	clusterID string

	dedicatedGlobalDB *gorm.DB
}

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

func (su *TestDedicatedClusterMeteringSuite) 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.dedicatedGlobalDB, err = gormutil.Connect(gormutil.Config{
		User:     viper.GetString("db_user"),
		Host:     viper.GetString("db_host"),
		Database: viper.GetString("db_database"),
		Password: viper.GetString("db_password"),
		Port:     3306,
	})
	su.Require().NoError(err)
	su.Require().NotNil(su.dedicatedGlobalDB)
}

func (su *TestDedicatedClusterMeteringSuite) Test_02_ListClusters() {
	_ = su.portalCli.ListClusters(su.orgID, su.prjID, 1, 100)
}

func (su *TestDedicatedClusterMeteringSuite) 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 *TestDedicatedClusterMeteringSuite) Test_04_CheckProjectRegionCidr() {
	su.portalCli.CheckProjectRegionCidr(su.orgID, su.prjID, su.provider, su.region)
}

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

func (su *TestDedicatedClusterMeteringSuite) 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, "2C") {
						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, "2C") {
						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 *TestDedicatedClusterMeteringSuite) Test_07_EstimateClusterPrice() {
	_ = su.portalCli.EstimateClusterPrice(su.orgID, su.prjID, su.provider, su.region, su.components)
}

func (su *TestDedicatedClusterMeteringSuite) 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 *TestDedicatedClusterMeteringSuite) Test_09_PollClusterStatus() {
	// timeout: 30 minutes (1800 seconds)
	for range 180 {
		status := su.portalCli.PollClusterStatus(su.orgID, su.prjID, su.clusterID)
		if *status == model.CentralClusterStatusNORMAL {
			break
		}
		su.T().Logf("cluster status: %s", *status)
		time.Sleep(10 * time.Second)
	}
}

func queryMeteringReportDataItem(
	billingReport *schema.BillingReportRecord,
	sku, componentType string,
) (*schema.MeteringReportDataItem, bool) {
	meteringReport, ok := lo.Find(billingReport.Data.Items, func(i *schema.MeteringReportDataItem) bool {
		if i.SKU != sku {
			return false
		}
		if len(i.Labels) == 0 {
			return false
		}
		componentLabel, exist := i.Labels["component"]
		if !exist {
			return false
		}
		if componentLabel == componentType {
			return true
		}

		return false
	})
	return meteringReport, ok
}

func (su *TestDedicatedClusterMeteringSuite) checkComponentMeteringReport(
	cluster *schema.Cluster,
	component *schema.ClusterComponent,
	meteringReport *schema.MeteringReportDataItem,
	clusterMetering *schema.ClusterMetering,
) {
	su.Require().Equal(clusterMetering.StartTS.Unix(), meteringReport.CostTimeStamp)
	su.Require().Equal(strconv.FormatUint(cluster.TenantId, 10), meteringReport.Labels["tenantID"])
	su.Require().Equal(strconv.FormatUint(cluster.ProjectId, 10), meteringReport.Labels["projectID"])
	su.Require().Equal(strconv.FormatUint(cluster.ID, 10), meteringReport.Labels["clusterID"])
	su.Require().
		Equal(strconv.FormatUint(uint64(component.ComponentTierUnit.InstanceType.CPUNum), 10), meteringReport.Labels["cpuNum"])
	su.Require().
		Equal(strconv.FormatUint(uint64(component.ComponentTierUnit.InstanceType.MemorySizeGi), 10), meteringReport.Labels["memoryGiB"])
	su.Require().
		Equal(strconv.FormatUint(uint64(component.ComponentTierUnit.DefaultIops), 10), meteringReport.Labels["iops"])
	su.Require().
		Equal(strconv.FormatUint(uint64(lo.FromPtr(component.ComponentTierUnit.DefaultThroughPut)), 10), meteringReport.Labels["throughput"])
	su.Require().Equal("false", meteringReport.Labels["devTier"])
	su.Require().NotEmpty(meteringReport.Labels["project"])
	su.Require().NotEmpty(meteringReport.Labels["poc"])
	su.Require().NotEmpty(meteringReport.Labels["crossAccount"])
}

func (su *TestDedicatedClusterMeteringSuite) checkMeteringReport(
	cluster *schema.Cluster,
	components []*schema.ClusterComponent,
	billingReport *schema.BillingReportRecord,
	clusterMetering *schema.ClusterMetering,
) {
	tidbComponent, ok := lo.Find(components, func(c *schema.ClusterComponent) bool {
		return c.ComponentType == "tidb"
	})
	su.Require().True(ok)
	su.Require().NotNil(tidbComponent)

	tikvComponent, ok := lo.Find(components, func(c *schema.ClusterComponent) bool {
		return c.ComponentType == "tikv"
	})
	su.Require().True(ok)
	su.Require().NotNil(tikvComponent)

	tidbNodeSku := fmt.Sprintf(
		"node:aws:us-west-2:DEPLOY_TYPE_DEDICATED:%s",
		tidbComponent.ComponentTierUnit.InstanceType.InstanceType,
	)
	tidbNodeMeteringReport, ok := queryMeteringReportDataItem(billingReport, tidbNodeSku, "COMPONENT_TYPE_TIDB")
	su.Require().True(ok)
	su.Require().NotNil(tidbNodeMeteringReport)
	su.Require().Equal(int64(1), tidbNodeMeteringReport.Cost)
	su.checkComponentMeteringReport(cluster, tidbComponent, tidbNodeMeteringReport, clusterMetering)

	tikvNodeSku := fmt.Sprintf(
		"node:aws:us-west-2:DEPLOY_TYPE_DEDICATED:%s",
		tikvComponent.ComponentTierUnit.InstanceType.InstanceType,
	)
	tikvNodeMeteringReport, ok := queryMeteringReportDataItem(billingReport, tikvNodeSku, "COMPONENT_TYPE_TIKV")
	su.Require().True(ok)
	su.Require().NotNil(tikvNodeMeteringReport)
	su.Require().Equal(int64(3), tikvNodeMeteringReport.Cost)
	su.checkComponentMeteringReport(cluster, tikvComponent, tikvNodeMeteringReport, clusterMetering)

	// FIXME(@liuzhi): Preload `Cluster.Storages`.
	// tikvDiskStorageSku := "disk-storage:aws:us-west-2:gp3"
	// tikvDiskStorageMeteringReport, ok := queryMeteringReportDataItem(billingReport, tikvDiskStorageSku, "COMPONENT_TYPE_TIKV")
	// su.Require().True(ok)
	// su.Require().NotNil(tikvDiskStorageMeteringReport)
	// su.Require().Equal(int64(*tikvComponent.StorageSizeGi)*3, tikvDiskStorageMeteringReport.Cost)
	// su.checkComponentMeteringReport(cluster, tikvComponent, tikvNodeMeteringReport, clusterMetering)

	tikvDiskIopsSku := "disk-iops:aws:us-west-2:gp3"
	tikvDiskIopsMeteringReport, ok := queryMeteringReportDataItem(billingReport, tikvDiskIopsSku, "COMPONENT_TYPE_TIKV")
	su.Require().True(ok)
	su.Require().NotNil(tikvDiskIopsMeteringReport)
	su.Require().Equal(int64(3), tikvDiskIopsMeteringReport.Cost)
	su.checkComponentMeteringReport(cluster, tikvComponent, tikvNodeMeteringReport, clusterMetering)

	tikvDiskThroughputSku := "disk-throughput:aws:us-west-2:gp3"
	tikvDiskThroughputMeteringReport, ok := queryMeteringReportDataItem(
		billingReport,
		tikvDiskThroughputSku,
		"COMPONENT_TYPE_TIKV",
	)
	su.Require().True(ok)
	su.Require().NotNil(tikvDiskThroughputMeteringReport)
	su.Require().Equal(int64(3), tikvDiskThroughputMeteringReport.Cost)
	su.checkComponentMeteringReport(cluster, tikvComponent, tikvNodeMeteringReport, clusterMetering)
}

func (su *TestDedicatedClusterMeteringSuite) Test_10_CheckClusterMetering() {
	// wait 2 minutes to generate cluster metering
	time.Sleep(2 * time.Minute)

	clusterID := su.clusterID

	var cluster schema.Cluster
	err := su.dedicatedGlobalDB.Model(&schema.Cluster{}).
		Where("`id` = ?", clusterID).
		First(&cluster).Error
	su.Require().NoError(err)

	// FIXME(@liuzhi): Preload `Cluster.Storages` field.
	var components []*schema.ClusterComponent
	err = su.dedicatedGlobalDB.Model(&schema.ClusterComponent{}).
		Preload("ComponentTierUnit").
		Preload("ComponentTierUnit.InstanceType").
		Preload("ComponentTierUnit.StorageTemplate").
		Where("`cluster_id` = ?", clusterID).
		Find(&components).Error
	su.Require().NoError(err)
	su.Require().Len(components, 3)

	var clusterMetering schema.ClusterMetering
	err = su.dedicatedGlobalDB.Model(&schema.ClusterMetering{}).
		Where("`cluster_id` = ?", clusterID).
		Order("id DESC").
		First(&clusterMetering).Error
	su.Require().NoError(err)
	su.Require().Equal(clusterMetering.TenantID, cluster.TenantId)
	su.Require().Equal(clusterMetering.ProjectID, cluster.ProjectId)
	su.Require().Equal(time.Minute, clusterMetering.EndTS.Sub(clusterMetering.StartTS))

	var billingReport schema.BillingReportRecord
	err = su.dedicatedGlobalDB.Model(&schema.BillingReportRecord{}).
		Where("`cluster_metering_id` = ?", clusterMetering.ID).
		First(&billingReport).Error
	su.Require().NoError(err)

	su.Require().Equal(billingReport.TenantID, cluster.TenantId)
	su.Require().Len(billingReport.Data.Items, 5)
	su.checkMeteringReport(&cluster, components, &billingReport, &clusterMetering)
}
