package autogencase

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

	"github.com/icholy/digest"
	"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 TestFuzzyRegionServiceSuite 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 TestFuzzyRegionService(t *testing.T) {
	testSuite := &TestFuzzyRegionServiceSuite{
		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, 0),
		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 *TestFuzzyRegionServiceSuite) 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, ServiceNameRegionService)
		su.Require().NoError(err)
		for i := range records {
			record := &(records[i])
			group := record.OperationID
			su.casesGroupByOperationID[group] = append(su.casesGroupByOperationID[group], record)
		}
	}
}

func (su *TestFuzzyRegionServiceSuite) TearDownSuite() {
	if su.skipTearDown {
		return
	}
	su.T().Logf("cluster to clean:%v", su.clustersToClean)
	ctx := context.Background()
	for clusterID := range su.clustersToClean {
		_, _, err := su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceDeleteCluster(ctx, clusterID).
			Execute() //nolint:bodyclose // resp body is already closed in Execute()
		su.Require().NoError(err)
	}
}

func (su *TestFuzzyRegionServiceSuite) Test_200_ShowCloudProvider() {
	operationID := ServiceNameRegionService + "_" + "ShowCloudProviders"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyRegionServiceSuite) Test_201_ListRegions() {
	operationID := ServiceNameRegionService + "_" + "ListRegions"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyRegionServiceSuite) Test_202_GetRegion() {
	operationID := ServiceNameRegionService + "_" + "GetRegion"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyRegionServiceSuite) Test_203_ListNodeSpecs() {
	operationID := ServiceNameRegionService + "_" + "ListNodeSpecs"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyRegionServiceSuite) Test_204_GetNodeSpec() {
	operationID := ServiceNameRegionService + "_" + "GetNodeSpec"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyRegionServiceSuite) Test_999_Summary() {
	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
			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 *TestFuzzyRegionServiceSuite) 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 *TestFuzzyRegionServiceSuite) 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())
	// }
}
