package dedicatede2e_test

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

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

	"github.com/tidbcloud/observability/test/ft/common/checker"
	dedicatedapi "github.com/tidbcloud/observability/test/ft/common/dedicatedapi/api"
	"github.com/tidbcloud/observability/test/ft/common/dedicatedapi/api/client"
	"github.com/tidbcloud/observability/test/ft/utils/randstr"
	"github.com/tidbcloud/observability/test/ft/utils/sql"
)

const pageSize = 100

// TestDedicatedAwsCreateMinimalCluster verifies end-to-end cluster creation and cleanup
// by initializing a client, creating a cluster, checking its status, configuring access,
// running SQL test queries, and deleting the cluster.
func TestDedicatedAwsCreateMinimalCluster(t *testing.T) {
	testSuite := &TestDedicatedAwsCreateMinimalClusterSuite{
		env:    cfg.Env,
		host:   cfg.DedicatedHost,
		apiKey: cfg.DedicatedApiKey,

		orgID:             cfg.Tenant,
		prjID:             cfg.Project,
		clusterNamePrefix: fmt.Sprintf("o11y-v1beta1-e2e-%s-", cfg.Env),

		provider:   cfg.Provider,
		regionCode: cfg.Region,
		password:   randstr.GenWithCharset(16, randstr.AlphanumericCharset),
		port:       4000,
	}
	testSuite.clusterName = testSuite.clusterNamePrefix + randstr.GenWithCharset(4, randstr.AlphabetCharset)
	testSuite.cli = dedicatedapi.NewClient(&testSuite.Suite, testSuite.host, testSuite.apiKey)

	testSuite.tidbNodeSpec = &client.Dedicatedv1beta1NodeSpec{
		NodeSpecKey: lo.ToPtr("2C4G"),
	}
	testSuite.tikvNodeSpec = &client.Dedicatedv1beta1NodeSpec{
		NodeSpecKey:          lo.ToPtr("2C4G"),
		DefaultStorageSizeGi: lo.ToPtr(int32(10)),
	}

	suite.Run(t, testSuite)
}

// TestDedicatedAwsCreateMinimalClusterSuite run `Suite.TestXxx()` in alphabetical order, sequentially.
//
// Caution: `Test_10_GetCluster()` runs before `Test_2_GetCluster()`. You should use 00-99 serial number.
type TestDedicatedAwsCreateMinimalClusterSuite struct {
	suite.Suite

	env    string
	host   string
	apiKey string

	orgID             string
	prjID             string
	clusterNamePrefix string

	provider    string
	regionCode  string
	password    string
	clusterName string
	port        int32

	cli *dedicatedapi.Cli

	tidbNodeSpec *client.Dedicatedv1beta1NodeSpec
	tikvNodeSpec *client.Dedicatedv1beta1NodeSpec
	clusterId    string
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_01_EnsureNetworkContainer() {
	// List network containers (CIDRs)
	ctx := context.Background()
	req := su.cli.NetworkContainerServiceAPI.NetworkContainerServiceListNetworkContainers(ctx).
		ProjectId(su.prjID).
		CloudProvider(su.provider).
		PageSize(pageSize)
	resp, _, err := req.Execute()
	su.Require().NoError(err)

	isProjectAndCloudProviderValidForAll := lo.Reduce(resp.GetNetworkContainers(), func(agg bool, item client.V1beta1NetworkContainer, _ int) bool {
		pid, ok := item.GetLabels()["tidb.cloud/project"]
		return agg &&
			ok && pid == su.prjID && // Check result's project ID.
			string(item.GetCloudProvider()) == su.provider // Check result's cloud provider.
	}, true)
	su.Require().True(isProjectAndCloudProviderValidForAll)

	// TODO(@yifan) : Create a network container if not exists.
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_02_ListClusters() {
	// List clusters under the project.
	ctx := context.Background()
	req := su.cli.ClusterServiceAPI.ClusterServiceListClusters(ctx).
		ProjectId(su.prjID).
		PageSize(pageSize)
	resp, _, err := req.Execute()
	su.Require().NoError(err)

	isProjectValidForAll := lo.Reduce(resp.Clusters, func(agg bool, item client.TidbCloudOpenApidedicatedv1beta1Cluster, _ int) bool {
		pid, ok := item.GetLabels()["tidb.cloud/project"]
		return agg &&
			ok && pid == su.prjID // Check result's project ID.
	}, true)
	su.Require().True(isProjectValidForAll)
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_03_ListCloudProvidersRegions() {
	ctx := context.Background()

	// List cloud providers.
	cloudProviderReq := su.cli.RegionServiceAPI.RegionServiceShowCloudProviders(ctx).
		ProjectId(su.prjID)
	cloudProviderResp, _, err := cloudProviderReq.Execute()
	su.Require().NoError(err)
	su.Require().Contains(cloudProviderResp.CloudProviders, client.V1beta1RegionCloudProvider(su.provider))

	// List regions.
	regionReq := su.cli.RegionServiceAPI.RegionServiceListRegions(ctx).
		CloudProvider(su.provider).
		ProjectId(su.prjID).
		PageSize(pageSize)
	regionResp, _, err := regionReq.Execute()
	su.Require().NoError(err)
	su.Require().True(lo.ContainsBy(regionResp.Regions, func(item client.Commonv1beta1Region) bool {
		return item.GetRegionId() == fmt.Sprintf("%s-%s", su.provider, su.regionCode)
	}))
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_04_ListNodeSpecs() {
	ctx := context.Background()

	req := su.cli.RegionServiceAPI.RegionServiceListNodeSpecs(ctx, fmt.Sprintf("%s-%s", su.provider, su.regionCode)).
		ProjectId(su.prjID).
		PageSize(pageSize)
	resp, _, err := req.Execute()
	su.Require().NoError(err)

	isRegionValidForAll := lo.Reduce(resp.NodeSpecs, func(agg bool, item client.Dedicatedv1beta1NodeSpec, _ int) bool {
		return agg &&
			item.GetRegionId() == fmt.Sprintf("%s-%s", su.provider, su.regionCode)
	}, true)
	su.Require().True(isRegionValidForAll)

	// Find the minimal TiDB node spec.
	tidbNodeSpecs := lo.Filter(resp.NodeSpecs, func(item client.Dedicatedv1beta1NodeSpec, _ int) bool {
		return item.GetComponentType() == client.DEDICATEDV1BETA1COMPONENTTYPE_TIDB
	})
	su.tidbNodeSpec = lo.ToPtr(lo.MinBy(tidbNodeSpecs, func(a client.Dedicatedv1beta1NodeSpec, b client.Dedicatedv1beta1NodeSpec) bool {
		return a.GetVCpu() < b.GetVCpu()
	}))

	// Find the minimal TiKV node spec.
	tikvNodeSpecs := lo.Filter(resp.NodeSpecs, func(item client.Dedicatedv1beta1NodeSpec, _ int) bool {
		return item.GetComponentType() == client.DEDICATEDV1BETA1COMPONENTTYPE_TIKV
	})
	su.tikvNodeSpec = lo.ToPtr(lo.MinBy(tikvNodeSpecs, func(a client.Dedicatedv1beta1NodeSpec, b client.Dedicatedv1beta1NodeSpec) bool {
		return a.GetVCpu() < b.GetVCpu()
	}))

	su.Require().Equal(su.tidbNodeSpec.VCpu, su.tikvNodeSpec.VCpu)
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_05_CreateCluster() {
	ctx := context.Background()

	req := su.cli.ClusterServiceAPI.ClusterServiceCreateCluster(ctx).
		Cluster(client.TidbCloudOpenApidedicatedv1beta1Cluster{
			DisplayName: su.clusterName,
			RegionId:    fmt.Sprintf("%s-%s", su.provider, su.regionCode),
			Labels:      lo.ToPtr(map[string]string{"tidb.cloud/project": su.prjID}),
			TidbNodeSetting: client.V1beta1ClusterTidbNodeSetting{
				NodeSpecKey: su.tidbNodeSpec.GetNodeSpecKey(),
				TidbNodeGroups: []client.Dedicatedv1beta1TidbNodeGroup{
					{
						NodeCount: 1,
					},
				},
			},
			TikvNodeSetting: client.V1beta1ClusterStorageNodeSetting{
				NodeCount:     3,
				NodeSpecKey:   su.tikvNodeSpec.GetNodeSpecKey(),
				StorageSizeGi: su.tikvNodeSpec.GetDefaultStorageSizeGi(),
				StorageType:   client.CLUSTERSTORAGENODESETTINGSTORAGETYPE_BASIC,
			},
			Port:         su.port,
			RootPassword: lo.ToPtr(su.password),
		})

	resp, _, err := req.Execute()
	su.Require().NoError(err)
	su.Require().Equal(su.clusterName, resp.GetDisplayName())
	su.Require().Equal(fmt.Sprintf("%s-%s", su.provider, su.regionCode), resp.GetRegionId())
	su.Require().Equal(client.COMMONV1BETA1CLUSTERSTATE_CREATING, resp.GetState())

	su.clusterId = resp.GetClusterId()
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_06_PollClusterUntilActive() {
	ctx := context.Background()

	// List clusters to find the cluster.
	req := su.cli.ClusterServiceAPI.ClusterServiceListClusters(ctx).
		ProjectId(su.prjID).
		PageSize(pageSize)
	resp, _, err := req.Execute()
	su.Require().NoError(err)
	su.Require().True(lo.ContainsBy(resp.Clusters, func(item client.TidbCloudOpenApidedicatedv1beta1Cluster) bool {
		return item.GetClusterId() == su.clusterId
	}))

	// Poll the cluster using GET method until it becomes active.
	// Timeout: 30 minutes (1800 seconds).
	for i := 0; i < 180; i++ {
		req := su.cli.ClusterServiceAPI.ClusterServiceGetCluster(ctx, su.clusterId)
		resp, _, err := req.Execute()
		su.Require().NoError(err)

		switch resp.GetState() {
		case client.COMMONV1BETA1CLUSTERSTATE_ACTIVE:
			return
		case client.COMMONV1BETA1CLUSTERSTATE_CREATING:
			su.T().Logf("Cluster state is CREATING")
		default:
			su.Fail("Cluster state is not ACTIVE or CREATING")
		}
		time.Sleep(10 * time.Second)
	}
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_07_SetPublicNetworkAccess() {
	ctx := context.Background()

	// Get the cluster first.
	getClusterReq := su.cli.ClusterServiceAPI.ClusterServiceGetCluster(ctx, su.clusterId)
	cluster, _, err := getClusterReq.Execute()
	su.Require().NoError(err)
	su.Require().Len(cluster.TidbNodeSetting.GetTidbNodeGroups(), 1)

	group := cluster.TidbNodeSetting.GetTidbNodeGroups()[0]

	nullableTrue := client.NullableBool{}
	nullableTrue.Set(lo.ToPtr(true))

	updatePublicAccessReq := su.cli.TidbNodeGroupServiceAPI.TidbNodeGroupServiceUpdatePublicEndpointSetting(ctx, su.clusterId, group.GetTidbNodeGroupId()).
		PublicEndpointSetting(client.TidbNodeGroupServiceUpdatePublicEndpointSettingRequest{
			Enabled: nullableTrue,
			IpAccessList: []client.PublicEndpointSettingIpAccessList{
				{
					CidrNotation: "0.0.0.0/0",
					Description:  lo.ToPtr("allow access from anywhere."),
				},
			},
		})
	_, _, err = updatePublicAccessReq.Execute()
	su.Require().NoError(err)
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_08_ExecuteSql() {
	ctx := context.Background()

	for n := 0; n < 30; n++ {
		time.Sleep(10 * time.Second)

		// List TiDB groups first.
		listGroupsReq := su.cli.TidbNodeGroupServiceAPI.TidbNodeGroupServiceListTidbNodeGroups(ctx, su.clusterId).
			PageSize(pageSize)
		listGroupsResp, _, err := listGroupsReq.Execute()
		su.Require().NoError(err)
		su.Require().Len(listGroupsResp.GetTidbNodeGroups(), 1)

		pubEndpoint, ok := lo.Find(listGroupsResp.GetTidbNodeGroups()[0].GetEndpoints(), func(item client.Dedicatedv1beta1TidbNodeGroupEndpoint) bool {
			return item.GetConnectionType() == client.ENDPOINTCONNECTIONTYPE_PUBLIC
		})
		su.Require().True(ok)

		// Skip if the public endpoint is not ready.
		if pubEndpoint.GetHost() == "" {
			continue
		}

		err = sql.ExecuteSqlWithUrl(ctx, pubEndpoint.GetHost(), "", su.password, "skip-verify", su.port)

		switch {
		case err == nil:
			// Success.
			return
		case strings.Contains(err.Error(), "connection refused"),
			strings.Contains(err.Error(), "no such host"),
			strings.Contains(err.Error(), "i/o timeout"):
			// Retry if the connection is refused.
			// Dns propagation may take some time.
		default:
			su.Fail(err.Error())
		}
	}
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_09_CheckO11yData() {
	ctx := context.Background()
	for n := 0; n < 10; n++ {
		ok, err := checker.CheckAll(ctx, checker.ClusterInfo{
			Provider:  su.provider,
			Env:       su.env,
			Region:    su.regionCode,
			TenantID:  su.orgID,
			ProjectID: su.prjID,
			ClusterID: su.clusterId,
		}, checker.TimeRange{}, false, func(dt checker.DataType) error {
			su.T().Logf("round %d: %s not found", n, dt)
			return nil
		})
		su.Require().NoError(err)
		if ok {
			return
		}
		time.Sleep(time.Minute)
	}
	su.Fail("o11y data check failed")
}

func (su *TestDedicatedAwsCreateMinimalClusterSuite) Test_10_DeleteCluster() {
	if su.clusterId != "" {
		ctx := context.Background()
		req := su.cli.ClusterServiceAPI.ClusterServiceDeleteCluster(ctx, su.clusterId)
		_, _, err := req.Execute()
		su.Require().NoError(err)
	}
}
