package autogencase

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

	"github.com/avast/retry-go"
	"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"
)

type TestFuzzyMaintenanceServiceSuite struct {
	suite.Suite

	// can set by config.yaml
	orgID             string
	projectID         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
	ratelimiter             ratelimit.Limiter
}

func TestFuzzyMaintenanceService(t *testing.T) {
	testSuite := &TestFuzzyMaintenanceServiceSuite{
		skipTearDown:            viper.GetBool("skip_tear_down"),
		orgID:                   viper.GetString("org_id"),
		projectID:               viper.GetString("prj_id"),
		provider:                viper.GetString("cloud_provider"),
		openAPIKey:              viper.GetString("api_key"),
		openAPIHost:             viper.GetString("host"),
		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.placeHolderLookup["maintenanceWindowId"] = "project-" + testSuite.projectID
	testSuite.placeHolderLookup["maintenanceWindow.maintenanceWindowId"] = "project-" + testSuite.projectID
	testSuite.placeHolderLookup["regionId"] = testSuite.provider + "-" + testSuite.region
	suite.Run(t, testSuite)
}

func (su *TestFuzzyMaintenanceServiceSuite) 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, ServiceNameMaintenanceService)
		su.Require().NoError(err)
		for i := range records {
			record := &(records[i])
			group := record.OperationID
			su.casesGroupByOperationID[group] = append(su.casesGroupByOperationID[group], record)
		}
	}
}

func (su *TestFuzzyMaintenanceServiceSuite) TearDownSuite() {
	if su.skipTearDown {
		return
	}
	su.T().Logf("cluster to clean:%v", su.clustersToClean)
	ctx := context.Background()
	for clusterID := range su.clustersToClean {
		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(5*time.Second),
		)
		if retryErr != nil {
			su.T().Logf("call ClusterServiceDeleteCluster failed, x-debug-trace-id:%v, err:%v", traceID, retryErr)
		}
	}
}

func (su *TestFuzzyMaintenanceServiceSuite) Test_000_ListMaintenanceWindows() {
	operationID := ServiceNameMaintenanceService + "_" + "ListMaintenanceWindows"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyMaintenanceServiceSuite) Test_001_ListMaintenanceTasks() {
	operationID := ServiceNameMaintenanceService + "_" + "ListMaintenanceTasks"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyMaintenanceServiceSuite) Test_200_GetMaintenanceWindow() {
	operationID := ServiceNameMaintenanceService + "_" + "GetMaintenanceWindow"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyMaintenanceServiceSuite) Test_201_UpdateMaintenanceWindow() {
	operationID := ServiceNameMaintenanceService + "_" + "UpdateMaintenanceWindow"
	su.runTestCasses(operationID)
}

func (su *TestFuzzyMaintenanceServiceSuite) 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.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 *TestFuzzyMaintenanceServiceSuite) 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)
	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 *TestFuzzyMaintenanceServiceSuite) 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())
	// }
}
