package autogencase

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/avast/retry-go"
	"github.com/icholy/digest"
	"github.com/samber/lo"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/suite"
	"go.uber.org/ratelimit"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"

	"github.com/tidbcloud/tidb-management-service/functional-test/cases/openapidedicated/autogencase/model"
	dedicatedapiclient "github.com/tidbcloud/tidb-management-service/functional-test/pkg/dedicatedapi/api/client"
	"github.com/tidbcloud/tidb-management-service/utilities/randstr"
)

type TestFuzzyTidbNodeGroupServiceSuite struct {
	suite.Suite

	// can set by config.yaml
	orgID             string
	projectID         string
	clusterID         string // maybe created during the e2e or perpare ahead
	clusterName       string
	clusterNamePrefix string
	provider          string
	openAPIHost       string
	openAPIKey        string
	skipTearDown      bool
	env               string
	region            string
	dbAPITesterDSN    string
	dbAPIGeneratorDSN string

	ghStartAt        time.Time
	ghRunID          string
	executionID      string
	testCaseSpecHash string

	// internal struct
	dedicatedAPICli         *dedicatedapiclient.APIClient
	resultRecordDAO         *model.ResultRecordDAO
	caseRecordDAO           *model.CaseRecordDAO
	casesGroupByOperationID map[string][]*model.CaseRecord // operation_id -> cases
	resultRecords           map[string][]*model.ResultRecord
	clustersToClean         map[string]struct{}
	placeHolderLookup       placeHolderLookup
	ratelimit               ratelimit.Limiter
}

func TestFuzzyTidbNodeGroupService(t *testing.T) {
	testSuite := &TestFuzzyTidbNodeGroupServiceSuite{
		skipTearDown:            viper.GetBool("skip_tear_down"),
		orgID:                   viper.GetString("org_id"),
		projectID:               viper.GetString("prj_id"),
		clusterID:               viper.GetString("cluster_id"),
		provider:                viper.GetString("cloud_provider"),
		openAPIKey:              viper.GetString("api_key"),
		openAPIHost:             viper.GetString("host"),
		clusterNamePrefix:       viper.GetString("cluster_name_prefix"),
		dbAPITesterDSN:          viper.GetString("db_api_tester_dsn"),
		dbAPIGeneratorDSN:       viper.GetString("db_api_generator_dsn"),
		executionID:             viper.GetString("execution_id"),
		ghRunID:                 os.Getenv("GITHUB_RUN_ID"),
		casesGroupByOperationID: make(map[string][]*model.CaseRecord),
		resultRecords:           make(map[string][]*model.ResultRecord),
		clustersToClean:         make(map[string]struct{}),
		placeHolderLookup:       make(placeHolderLookup),
		ratelimit:               ratelimit.New(100, ratelimit.Per(time.Minute)),
	}
	testSuite.ghStartAt = time.Now()
	switch {
	case strings.Contains(testSuite.openAPIHost, "dev") || strings.Contains(testSuite.openAPIHost, "nightly"):
		testSuite.env = "dev"
	case strings.Contains(testSuite.openAPIHost, "staging"):
		testSuite.env = "staging"
	case strings.Contains(testSuite.openAPIHost, "prod"):
		testSuite.env = "prod"
	}
	switch testSuite.provider {
	case "aws":
		testSuite.region = "us-west-2"
	case "gcp":
		testSuite.region = "us-west1"
	case "azure":
		testSuite.region = "eastus2"
	}
	testSuite.clusterName = testSuite.clusterNamePrefix + randstr.GenWithCharset(4, randstr.AlphabetCharset)
	testSuite.placeHolderLookup["tidb.cloud/project"] = testSuite.projectID
	testSuite.placeHolderLookup["clusterId"] = testSuite.clusterID
	testSuite.placeHolderLookup["cluster.clusterId"] = testSuite.clusterID
	testSuite.placeHolderLookup["regionId"] = testSuite.provider + "-" + testSuite.region
	testSuite.placeHolderLookup["componentType"] = "TIDB"
	testSuite.placeHolderLookup["nodeSpecKey"] = "2C8G"
	testSuite.placeHolderLookup["instanceId"] = "tidb-0"
	suite.Run(t, testSuite)
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) SetupSuite() {
	{
		cfg := dedicatedapiclient.NewConfiguration()
		cfg.Debug = false
		// Set the server URL. Notice the base path is not included in the host.
		cfg.Servers = dedicatedapiclient.ServerConfigurations{
			{
				URL: fmt.Sprintf("https://%s/v1beta1", su.openAPIHost),
			},
		}
		client := dedicatedapiclient.NewAPIClient(cfg)
		// Set the digest auth.
		username, password, _ := strings.Cut(su.openAPIKey, ":")
		cfg.HTTPClient.Transport = &digest.Transport{
			Username: username,
			Password: password,
		}

		su.dedicatedAPICli = client
		su.Require().NotNil(su.dedicatedAPICli)
	}
	{
		db, err := gorm.Open(mysql.Open(su.dbAPITesterDSN), &gorm.Config{})
		su.Require().NoError(err)
		err = db.AutoMigrate(&model.ResultRecord{})
		su.Require().NoError(err)
		dao, err := model.NewResultRecordDAO(db)
		su.Require().NoError(err)
		su.resultRecordDAO = dao
		su.Require().NotNil(su.resultRecordDAO)
	}
	{
		db, err := gorm.Open(mysql.Open(su.dbAPIGeneratorDSN), &gorm.Config{})
		su.Require().NoError(err)
		dao, err := model.NewCaseRecordDAO(db)
		su.Require().NoError(err)
		su.caseRecordDAO = dao
		su.Require().NotNil(su.resultRecordDAO)
	}
	{
		ctx := context.Background()
		records, err := su.caseRecordDAO.ListCaseRecord(ctx, ServiceNameTidbNodeGroupService)
		su.Require().NoError(err)
		for i := range records {
			record := &(records[i])
			group := record.OperationID
			su.casesGroupByOperationID[group] = append(su.casesGroupByOperationID[group], record)
		}
	}
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) TearDownSuite() {
	if su.skipTearDown {
		return
	}
	su.T().Logf("cluster to clean:%v", su.clustersToClean)
	ctx := context.Background()
	for clusterID := range su.clustersToClean {
		_, resp, err := su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceDeleteCluster(ctx, clusterID).
			Execute() //nolint:bodyclose // resp body is already closed in Execute()
		if resp != nil && resp.StatusCode == http.StatusNotFound {
			continue
		}
		if err != nil {
			su.T().Logf("call ClusterServiceDeleteCluster failed, err:%v, clusterName:%v, x-debug-trace-id:%v",
				err, su.clusterName, resp.Header.Get("x-debug-trace-id"))
		}
	}
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_100_PrepareCluster() {
	if !su.supportTidbNodeGroup() {
		return
	}
	ctx := context.Background()
	tidbNodeSpec := "8C16G"
	tikvNodeSpec := "8C32G"
	if su.provider == "azure" {
		tidbNodeSpec = "8C32G"
	}
	if su.clusterID == "" {
		spec := dedicatedapiclient.TidbCloudOpenApidedicatedv1beta1Cluster{
			DisplayName: su.clusterName,
			RegionId:    su.provider + "-" + su.region,
			Labels:      lo.ToPtr(map[string]string{"tidb.cloud/project": su.projectID}),
			TidbNodeSetting: dedicatedapiclient.V1beta1ClusterTidbNodeSetting{
				NodeSpecKey: lo.ToPtr(tidbNodeSpec),
				TidbNodeGroups: []dedicatedapiclient.Dedicatedv1beta1TidbNodeGroup{
					{
						NodeCount: 1,
					},
				},
			},
			TikvNodeSetting: dedicatedapiclient.V1beta1ClusterStorageNodeSetting{
				NodeCount:     3,
				NodeSpecKey:   tikvNodeSpec,
				StorageSizeGi: 200,
				StorageType:   lo.ToPtr(dedicatedapiclient.CLUSTERSTORAGENODESETTINGSTORAGETYPE_STANDARD),
			},
			Port:         4000,
			RootPassword: lo.ToPtr("31415926"),
		}
		// bs, _ := json.Marshal(spec)
		// su.T().Log(string(bs))
		req := su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceCreateCluster(ctx).Cluster(spec)
		cluster, resp, err := req.Execute() //nolint:bodyclose // resp body is already closed in Execute()
		if err != nil {
			var body string
			if resp != nil {
				bs, _ := io.ReadAll(resp.Body)
				body = string(bs)
			}
			su.T().
				Logf("call ClusterServiceCreateCluster failed, err:%v, clusterName:%v, x-debug-trace-id:%v, resp_body:%v",
					err, su.clusterName, resp.Header.Get("x-debug-trace-id"), body)
		}
		su.Require().NoErrorf(err,
			"call ClusterServiceCreateCluster failed, clusterName:%v, x-debug-trace-id:%v",
			su.clusterName, resp.Header.Get("x-debug-trace-id"))
		su.Require().NotNil(cluster.ClusterId)
		su.clusterID = cluster.GetClusterId()
		su.clustersToClean[su.clusterID] = struct{}{}
		su.placeHolderLookup["clusterId"] = su.clusterID
		su.placeHolderLookup["tidbNodeGroup.clusterId"] = su.clusterID
		su.T().Logf("create cluster %v", su.clusterID)
	}
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_101_WaitClusterAvailable() {
	if !su.supportTidbNodeGroup() {
		return
	}
	su.waitClusterAvailable(su.clusterID, 30*time.Second)
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_102_PrepareTidbGroup() {
	if !su.supportTidbNodeGroup() {
		return
	}
	ctx := context.Background()
	req := su.dedicatedAPICli.TidbNodeGroupServiceAPI.TidbNodeGroupServiceCreateTidbNodeGroup(ctx, su.clusterID).
		TidbNodeGroup(dedicatedapiclient.TidbNodeGroupServiceCreateTidbNodeGroupRequest{
			NodeCount:   1,
			DisplayName: lo.ToPtr("e2e-fuzzy-1"),
		})
	tidbnodegroup, resp, err := req.Execute() //nolint:bodyclose // resp body is already closed in Execute()
	if err != nil {
		var body string
		if resp != nil {
			bs, _ := io.ReadAll(resp.Body)
			body = string(bs)
		}
		su.T().
			Logf("call TidbNodeGroupServiceCreateTidbNodeGroup failed, err:%v, clusterID:%v, x-debug-trace-id:%v, resp_body:%v",
				err, su.clusterID, resp.Header.Get("x-debug-trace-id"), body)
	}
	su.Require().NoErrorf(err, "call TidbNodeGroupServiceCreateTidbNodeGroup failed, clusterID:%v", su.clusterID)
	su.placeHolderLookup["publicEndpointSetting.tidbNodeGroupId"] = tidbnodegroup.GetTidbNodeGroupId()
	su.placeHolderLookup["tidbNodeGroup.tidbNodeGroupId"] = tidbnodegroup.GetTidbNodeGroupId()
	su.placeHolderLookup["tidbNodeGroupId"] = tidbnodegroup.GetTidbNodeGroupId()
	su.waitClusterAvailable(su.clusterID, 30*time.Second)
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_200_ListTidbNodeGroups() {
	if !su.supportTidbNodeGroup() {
		return
	}
	operationID := ServiceNameTidbNodeGroupService + "_" + "ListTidbNodeGroups"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_201_CreateTidbNodeGroup() {
	if !su.supportTidbNodeGroup() {
		return
	}
	operationID := ServiceNameTidbNodeGroupService + "_" + "CreateTidbNodeGroup"
	testcases, ok := su.casesGroupByOperationID[operationID]
	if !ok {
		su.T().Logf("operation %s has not case", operationID)
		return
	}
	for _, tc := range testcases {
		req, resp, dur := su.doRequest(tc) //nolint:bodyclose // resp body is already closed in doRequest()
		if resp.StatusCode == http.StatusOK {
			su.waitClusterAvailable(su.clusterID, time.Minute)
		}
		result, err := buildResultRecord(tc, req, resp, dur)
		su.Require().NoError(err)
		su.resultRecords[operationID] = append(su.resultRecords[operationID], result)
	}
	records := su.resultRecords[operationID]
	for _, record := range records {
		su.T().Log(record.String())
	}
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_202_GetTidbNodeGroup() {
	if !su.supportTidbNodeGroup() {
		return
	}
	operationID := ServiceNameTidbNodeGroupService + "_" + "GetTidbNodeGroup"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_203_UpdateTidbNodeGroup() {
	if !su.supportTidbNodeGroup() {
		return
	}
	operationID := ServiceNameTidbNodeGroupService + "_" + "UpdateTidbNodeGroup"
	testcases, ok := su.casesGroupByOperationID[operationID]
	if !ok {
		su.T().Logf("operation %s has not case", operationID)
		return
	}
	for _, tc := range testcases {
		req, resp, dur := su.doRequest(tc) //nolint:bodyclose // resp body is already closed in doRequest()
		if resp.StatusCode == http.StatusOK {
			su.waitClusterAvailable(su.clusterID, time.Minute)
		}
		result, err := buildResultRecord(tc, req, resp, dur)
		su.Require().NoError(err)
		su.resultRecords[operationID] = append(su.resultRecords[operationID], result)
	}
	records := su.resultRecords[operationID]
	for _, record := range records {
		su.T().Log(record.String())
	}
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_204_GetPublicEndpointSetting() {
	if !su.supportTidbNodeGroup() {
		return
	}
	operationID := ServiceNameTidbNodeGroupService + "_" + "GetPublicEndpointSetting"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_205_UpdatePublicEndpointSetting() {
	if !su.supportTidbNodeGroup() {
		return
	}
	operationID := ServiceNameTidbNodeGroupService + "_" + "UpdatePublicEndpointSetting"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_205_DeleteTidbNodeGroup() {
	if !su.supportTidbNodeGroup() {
		return
	}
	operationID := ServiceNameTidbNodeGroupService + "_" + "DeleteTidbNodeGroup"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) Test_999_Summary() {
	if !su.supportTidbNodeGroup() {
		su.T().Logf("TidbNodeGroupService is not supported on %v", su.provider)
		return
	}
	ctx := context.Background()
	totalFialed := 0
	finishAt := time.Now()

	for _, results := range su.resultRecords {
		for i := range results {
			result := results[i]
			result.WorkflowFinishAt = finishAt
			result.TestCaseSpecHash = su.testCaseSpecHash
			result.Env = su.env
			result.CloudProvider = su.provider
			result.WorkflowStartAt = su.ghStartAt
			result.RunID = su.ghRunID
			err := su.resultRecordDAO.CreateResultRecord(ctx, result)
			su.Require().NoError(err)
		}
	}
	for operationID, results := range su.resultRecords {
		failed := 0
		for _, result := range results {
			if !result.Expected {
				failed++
			}
		}
		total := len(su.casesGroupByOperationID[operationID])
		su.T().Logf("operation %s has %v out of %v cases failed", operationID, failed, total)
		totalFialed += failed
	}
	su.Require().Zerof(totalFialed, "%v cases failed", totalFialed)
}

// internal func

func (su *TestFuzzyTidbNodeGroupServiceSuite) waitClusterAvailable(clusterID string, interval time.Duration) {
	ctx := context.Background()
	req := su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceGetCluster(ctx, clusterID)
	var cluster *dedicatedapiclient.TidbCloudOpenApidedicatedv1beta1Cluster
	var traceID string
	// Timeout: 30 minutes.
	for range 30 {
		time.Sleep(interval)
		retryErr := retry.Do(
			func() error {
				out, resp, err := req.Execute() //nolint:bodyclose // resp body is already closed in Execute()
				if err != nil {
					var body string
					if resp != nil {
						traceID = resp.Header.Get("x-debug-trace-id")
						bs, _ := io.ReadAll(resp.Body)
						body = string(bs)
					}
					su.T().
						Logf("call ClusterServiceGetCluster failed, err:%v, clusterId:%v, x-debug-trace-id:%v, resp_body:%v",
							err, clusterID, traceID, body)
					return err
				}
				cluster = out
				return nil
			},
			retry.Attempts(3),
			retry.DelayType(retry.FixedDelay),
			retry.Delay(5*time.Second),
		)
		su.Require().
			NoErrorf(retryErr, "call ClusterServiceGetCluster failed, clusterId:%v, x-debug-trace-id:%v", clusterID, traceID)
		su.Require().NotNil(cluster)
		su.T().Logf("cluster %v, state: %v", su.clusterID, cluster.GetState())
		if cluster.GetState() == dedicatedapiclient.COMMONV1BETA1CLUSTERSTATE_ACTIVE {
			break
		}
	}
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) doRequest(
	tc *model.CaseRecord,
) (*http.Request, *http.Response, time.Duration) {
	req, err := buildRequest(tc, su.placeHolderLookup, su.openAPIHost)
	su.Require().NoError(err, "build request failed, case id %v", tc.CaseID)
	cli, err := buildHttpClientWithDigestAuth(tc, su.openAPIKey)
	su.Require().NoErrorf(err, "build http client failed, case id %v", tc.CaseID)
	_ = su.ratelimit.Take()
	start := time.Now()
	resp, err := cli.Do(req)
	dur := time.Since(start)
	su.Require().NoError(err)
	bs, err := io.ReadAll(resp.Body)
	_ = resp.Body.Close()
	su.Require().NoError(err)
	resp.Body = ReaderWrapper{bytes.NewReader(bs)}
	return req, resp, dur
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) runTestCasses(operationID string) {
	testcases, ok := su.casesGroupByOperationID[operationID]
	if !ok {
		su.T().Logf("operation %s has not case", operationID)
		return
	}
	for _, tc := range testcases {
		req, resp, dur := su.doRequest(tc) //nolint:bodyclose // resp body is already closed in doRequest()
		result, err := buildResultRecord(tc, req, resp, dur)
		su.Require().NoError(err)
		su.resultRecords[operationID] = append(su.resultRecords[operationID], result)
	}
	// records := su.resultRecords[operationID]
	// for _, record := range records {
	// 	su.T().Log(record.String())
	// }
}

func (su *TestFuzzyTidbNodeGroupServiceSuite) supportTidbNodeGroup() bool {
	return su.provider == "aws"
}
