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"
	"github.com/tidwall/gjson"
	"go.uber.org/ratelimit"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"

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

type TestFuzzyClusterServiceSuite 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

	// 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
	ratelimiter             ratelimit.Limiter
}

func TestFuzzyClusterService(t *testing.T) {
	testSuite := &TestFuzzyClusterServiceSuite{
		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"),
		region:                  viper.GetString("region"),
		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),
		ratelimiter:             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 *TestFuzzyClusterServiceSuite) 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.caseRecordDAO)
	}
	{
		ctx := context.Background()
		records, err := su.caseRecordDAO.ListCaseRecord(ctx, ServiceNameClusterService)
		su.Require().NoError(err)
		for i := range records {
			record := &(records[i])
			group := record.OperationID
			su.casesGroupByOperationID[group] = append(su.casesGroupByOperationID[group], record)
		}
	}
}

func (su *TestFuzzyClusterServiceSuite) TearDownSuite() {
	if su.skipTearDown {
		return
	}
	su.T().Logf("cluster to delete:%v", su.clustersToClean)
	ctx := context.Background()
	for clusterID := range su.clustersToClean {
		_, resp, err := su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceGetCluster(ctx, clusterID).
			Execute() //nolint:bodyclose // resp body is already closed in Execute()
		su.Require().NotNil(resp, "call ClusterServiceGetCluster failed, err: %s", err.Error())
		if resp.StatusCode == http.StatusNotFound {
			continue
		}
		req := su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceDeleteCluster(ctx, clusterID)
		var traceID string
		retryErr := retry.Do(
			func() error {
				_, resp, err := req.Execute() //nolint:bodyclose // resp body is already closed in Execute()
				if err != nil {
					if resp != nil {
						traceID = resp.Header.Get("x-debug-trace-id")
					}
					return err
				}
				return nil
			},
			retry.Attempts(3),
			retry.DelayType(retry.FixedDelay),
			retry.Delay(30*time.Second),
		)
		if retryErr != nil {
			su.T().Logf("call ClusterServiceDeleteCluster failed, err:%v, x-debug-trace-id:%v", retryErr, traceID)
		}
		su.Require().NoError(retryErr)
	}
}

func (su *TestFuzzyClusterServiceSuite) Test_000_ListClusters() {
	operationID := ServiceNameClusterService + "_" + "ListClusters"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyClusterServiceSuite) Test_001_ShowNodeQuota() {
	operationID := ServiceNameClusterService + "_" + "ShowNodeQuota"
	su.runTestCasses(operationID)
}

// CreateCluster case has addtional logic to save the created cluster for resource clear.
func (su *TestFuzzyClusterServiceSuite) Test_002_CreateCluster() {
	operationID := ServiceNameClusterService + "_" + "CreateCluster"
	testcases, ok := su.casesGroupByOperationID[operationID]
	if !ok {
		su.T().Logf("operation %s has not case", operationID)
		return
	}
	createdClusters := make(map[string]struct{})
	for _, tc := range testcases {
		req, resp, dur := su.doRequest(tc) //nolint:bodyclose // resp body is already closed in doRequest()
		if resp.StatusCode == http.StatusOK && req.Method == http.MethodPost {
			data, err := io.ReadAll(resp.Body)
			su.Require().NoError(err)
			rseeker, ok := resp.Body.(io.ReadSeeker)
			if ok {
				_, _ = rseeker.Seek(0, 0)
			}
			value := gjson.GetBytes(data, "clusterId")
			if clusterID := value.Str; clusterID != "" {
				createdClusters[clusterID] = struct{}{}
			}
		}
		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())
	}
	// clear the created cluster to save credits
	ctx := context.Background()
	for clusterID := range createdClusters {
		_ = su.ratelimiter.Take()
		_, _, err := su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceDeleteCluster(ctx, clusterID).
			Execute() //nolint:bodyclose // resp body is already closed in Execute()
		if err != nil {
			su.T().Logf("ClusterServiceDeleteCluster failed, err:%v", err)
		}
	}
}

func (su *TestFuzzyClusterServiceSuite) Test_100_PrepareCluster() {
	ctx := context.Background()
	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("2C8G"),
				TidbNodeGroups: []dedicatedapiclient.Dedicatedv1beta1TidbNodeGroup{
					{
						NodeCount: 1,
					},
				},
			},
			TikvNodeSetting: dedicatedapiclient.V1beta1ClusterStorageNodeSetting{
				NodeCount:     3,
				NodeSpecKey:   "2C8G",
				StorageSizeGi: 200,
				StorageType:   lo.ToPtr(dedicatedapiclient.CLUSTERSTORAGENODESETTINGSTORAGETYPE_STANDARD),
			},
			Port:         4000,
			RootPassword: lo.ToPtr("31415926"),
		}
		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["cluster.clusterId"] = su.clusterID
		su.T().Logf("create cluster %v", su.clusterID)
	}
}

func (su *TestFuzzyClusterServiceSuite) Test_101_WaitClusterAvailable() {
	su.waitClusterAvailable(su.clusterID, 30*time.Second)
}

func (su *TestFuzzyClusterServiceSuite) Test_200_GetCluster() {
	operationID := ServiceNameClusterService + "_" + "GetCluster"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyClusterServiceSuite) Test_201_ListNodeInstances() {
	operationID := ServiceNameClusterService + "_" + "ListNodeInstances"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyClusterServiceSuite) Test_202_GetNodeInstance() {
	operationID := ServiceNameClusterService + "_" + "GetNodeInstance"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyClusterServiceSuite) Test_203_UpdateCluster() {
	operationID := ServiceNameClusterService + "_" + "UpdateCluster"
	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 *TestFuzzyClusterServiceSuite) Test_204_ResetRootPassword() {
	operationID := ServiceNameClusterService + "_" + "ResetRootPassword"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyClusterServiceSuite) Test_205_PauseCluster() {
	operationID := ServiceNameClusterService + "_" + "PauseCluster"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyClusterServiceSuite) Test_206_ResumeCluster() {
	operationID := ServiceNameClusterService + "_" + "ResumeCluster"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyClusterServiceSuite) Test_207_DeleteCluster() {
	operationID := ServiceNameClusterService + "_" + "DeleteCluster"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyClusterServiceSuite) Test_999_Summary() {
	ctx := context.Background()
	finishAt := time.Now()
	totalFialed := 0

	for _, results := range su.resultRecords {
		for i := range results {
			result := results[i]
			result.WorkflowFinishAt = finishAt
			result.Env = su.env
			result.CloudProvider = su.provider
			result.WorkflowStartAt = su.ghStartAt
			result.RunID = su.ghRunID
			result.ExecutionID = su.executionID
			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 *TestFuzzyClusterServiceSuite) 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(30*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 *TestFuzzyClusterServiceSuite) 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.ratelimiter.Take()
	start := time.Now()
	resp, err := cli.Do(req)
	su.Require().NoError(err)
	dur := time.Since(start)
	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 *TestFuzzyClusterServiceSuite) 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())
	}
}
