package autogencase

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"maps"
	"net/http"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/icholy/digest"
	"github.com/samber/lo"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/suite"
	"go.uber.org/ratelimit"
	"gopkg.in/yaml.v3"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	gormlogger "gorm.io/gorm/logger"

	"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"
)

// - use placeholderLookup to build all kinds of request
// - do the built Request
// - update placeholderLookup with the response
// - trace all created resource and clear them in TearDownSuite
type TestScenarioSuite struct {
	suite.Suite

	skipTearDown bool

	tenantID          string
	projectID         string
	host              string
	openAPIKey        string
	authRole          string
	dbAPITesterDSN    string
	dbAPIGeneratorDSN string
	generationID      string
	env               string
	cloudProvider     string
	region            string
	// for audit log
	azureAccountSASToken string
	azureBlobSASToken    string

	ghStartAt      time.Time
	ghRunID        string
	executionID    string
	readCaseFromFS bool
	saveResultToDB bool
	scenarioYAML   string

	scenarioContext         *model.ScenarioContext
	generationSteps         []*model.GenerationStep
	scenarioResultRecords   []*model.ScenarioResultRecord
	generationStepDAO       *model.GenerationStepDAO
	scenarioResultRecordDAO *model.ScenarioResultRecordDAO
	actionBuilder           *ActionBuilder
	ratelimiter             ratelimit.Limiter

	dedicatedAPICli *dedicatedapiclient.APIClient
	// provider client
	awsProfile string
	ec2Cli     *ec2.Client // aws client
	// get bearer token
	loginURL     string
	accountEmail string
	accountPwd   string
	accountToken string
	// for testing resource
	awsKeyId     string
	awsKeySecret string
	aliKeyId     string
	aliKeySecret string
}

func loadSecretScenarioPlaceholders() map[string]string {
	placeholders := make(map[string]string)
	if password := viper.GetString("mysql_source_password"); password != "" {
		placeholders["mysql_source_password"] = password
	}
	return placeholders
}

func generatedScenarioPlaceholders(clusterID, sharedPoolID, resourcePoolID string) map[string]string {
	placeholders := make(map[string]string)
	if clusterID != "" {
		placeholders["cluster_id"] = clusterID
	}
	if sharedPoolID != "" {
		placeholders["sharedpool_id"] = sharedPoolID
	}
	if resourcePoolID != "" {
		placeholders["resource_pool_id"] = resourcePoolID
	}
	return placeholders
}

func TestScenario(t *testing.T) {
	testSuite := &TestScenarioSuite{
		env:               viper.GetString("env"),
		skipTearDown:      viper.GetBool("skip_tear_down"),
		scenarioYAML:      viper.GetString("scenario_yaml"),
		readCaseFromFS:    viper.GetBool("read_case_from_fs"),
		saveResultToDB:    viper.GetBool("save_result_to_db"),
		tenantID:          viper.GetString("tenant_id"),
		projectID:         viper.GetString("project_id"),
		cloudProvider:     viper.GetString("cloud_provider"),
		region:            viper.GetString("region"),
		openAPIKey:        viper.GetString("api_key"),
		authRole:          viper.GetString("auth_role"),
		host:              viper.GetString("host"),
		dbAPITesterDSN:    viper.GetString("db_api_tester_dsn"),
		dbAPIGeneratorDSN: viper.GetString("db_api_generator_dsn"),
		generationID:      viper.GetString("generation_id"),
		executionID:       viper.GetString("execution_id"),
		ghRunID:           os.Getenv("GITHUB_RUN_ID"),
		awsProfile:        viper.GetString("aws_profile"),
		// for audit log
		azureAccountSASToken: viper.GetString("azure_account_sas_token"),
		azureBlobSASToken:    viper.GetString("azure_blob_sas_token"),
		// for generate bearer token
		loginURL:     viper.GetString("login_url"),
		accountEmail: viper.GetString("account_email"),
		accountPwd:   viper.GetString("account_pwd"),
		accountToken: viper.GetString("account_token"),

		generationSteps:       make([]*model.GenerationStep, 0),
		scenarioResultRecords: make([]*model.ScenarioResultRecord, 0),
		ratelimiter:           ratelimit.New(100, ratelimit.Per(time.Minute)),
		// for testing resource
		awsKeyId:     os.Getenv("IAM_AWS_KEY_ID"),
		awsKeySecret: os.Getenv("IAM_AWS_KEY_SECRET"),
		aliKeyId:     os.Getenv("IAM_ALI_KEY_ID"),
		aliKeySecret: os.Getenv("IAM_ALI_KEY_SECRET"),
	}
	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"
	// }
	if testSuite.region == "" {
		switch testSuite.cloudProvider {
		case "aws":
			testSuite.region = "us-west-2"
		case "gcp":
			testSuite.region = "us-west1"
		case "azure":
			testSuite.region = "japaneast"
		case "alicloud":
			testSuite.region = "ap-southeast-1"
		}
	}
	token := testSuite.accountToken
	if token == "" {
		// get bearerToken via browser
		webclient := NewTiDBCloudWebsite(testSuite.loginURL)
		token, _, _ = webclient.Token(t.Context(), testSuite.accountEmail, testSuite.accountPwd)
	}
	// set placeholderLookup
	testSuite.actionBuilder = NewActionBuilder(testSuite.env, testSuite.host, token)
	testSuite.actionBuilder.scenarioPlaceholder["open_api_key"] = testSuite.openAPIKey
	// set auth role
	// fmt.Printf("auth role is: %s", testSuite.authRole)
	if testSuite.authRole != "" {
		authRolePlaceholder := make(map[string]string)
		err := json.Unmarshal([]byte(testSuite.authRole), &authRolePlaceholder)
		if err != nil {
			fmt.Println("Error parsing authRole:", err)
			return
		}
		maps.Copy(testSuite.actionBuilder.scenarioPlaceholder, authRolePlaceholder)
	}
	maps.Copy(testSuite.actionBuilder.scenarioPlaceholder, loadSecretScenarioPlaceholders())

	testSuite.actionBuilder.scenarioPlaceholder["project_id"] = testSuite.projectID
	testSuite.actionBuilder.scenarioPlaceholder["org_id"] = testSuite.tenantID
	testSuite.actionBuilder.scenarioPlaceholder["tenant_id"] = testSuite.tenantID
	testSuite.actionBuilder.scenarioPlaceholder["region"] = testSuite.region
	testSuite.actionBuilder.scenarioPlaceholder["region_id"] = testSuite.cloudProvider + "-" + testSuite.region
	testSuite.actionBuilder.scenarioPlaceholder["cloud_provider"] = testSuite.cloudProvider
	testSuite.actionBuilder.scenarioPlaceholder["iam_aws_key_id"] = testSuite.awsKeyId
	testSuite.actionBuilder.scenarioPlaceholder["iam_aws_key_secret"] = testSuite.awsKeySecret
	testSuite.actionBuilder.scenarioPlaceholder["iam_ali_key_id"] = testSuite.aliKeyId
	testSuite.actionBuilder.scenarioPlaceholder["iam_ali_key_secret"] = testSuite.aliKeySecret
	apikey := testSuite.actionBuilder.scenarioPlaceholder["target_apikey"]
	testSuite.actionBuilder.scenarioPlaceholder["target_apikey_public"], _, _ = strings.Cut(apikey, ":")
	if testSuite.azureAccountSASToken != "" {
		testSuite.actionBuilder.scenarioPlaceholder["azure_account_sas_token"] = testSuite.azureAccountSASToken
	}
	if testSuite.azureBlobSASToken != "" {
		testSuite.actionBuilder.scenarioPlaceholder["azure_blob_sas_token"] = testSuite.azureBlobSASToken
	}
	maps.Copy(testSuite.actionBuilder.scenarioPlaceholder, generatedScenarioPlaceholders(
		viper.GetString("gen_cluster_id"),
		viper.GetString("gen_sharedpool_id"),
		viper.GetString("gen_resource_pool_id"),
	))
	suite.Run(t, testSuite)
}

// check all required fields are not empty
func (su *TestScenarioSuite) validate() error {
	if su.scenarioYAML == "" {
		return errors.New(
			"Use config field `scenario_yaml` or env variable `OPEN_API_DEDICATED_SCENARIO_YAML` to set scenarioYAML",
		)
	}
	if su.projectID == "" {
		return errors.New(
			"Use config field `project_id` or env variable `OPEN_API_DEDICATED_PROJECI_ID` to set projectID",
		)
	}
	if su.cloudProvider == "" {
		return errors.New(
			"Use config field `cloud_provider` or env variable `OPEN_API_DEDICATED_CLOUD_PROVIDER` to set cloudProvider",
		)
	}
	if su.region == "" {
		return errors.New("Use config field `region` or env variable `OPEN_API_DEDICATED_REGION` to set region")
	}
	if !strings.Contains(su.host, "regional-api") {
		if su.openAPIKey == "" {
			return errors.New("Use config field `api_key` or env variable `OPEN_API_DEDICATED_API_KEY` to set openAPIKey")
		}
		if su.dbAPITesterDSN == "" {
			return errors.New(
				"Use config field `db_api_tester_dsn` or env variable `OPEN_API_DEDICATED_DB_API_TESTER_DSN` to set dbAPITesterDSN",
			)
		}
		if su.dbAPIGeneratorDSN == "" {
			return errors.New(
				"Use config field `db_api_generator_dsn` or env variable `OPEN_API_DEDICATED_DB_API_GENERATOR_DSN` to set dbAPIGeneratorDSN",
			)
		}
	}
	return nil
}

func (su *TestScenarioSuite) SetupSuite() {
	// check all field and print friendly hints for user.
	su.Require().NoError(su.validate())
	{
		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.host),
			},
		}
		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{
			Logger: gormlogger.New(
				log.New(os.Stdout, "\r\n", log.LstdFlags),
				gormlogger.Config{
					SlowThreshold: time.Second,
					LogLevel:      gormlogger.Error,
				}),
		})
		su.Require().NoError(err)
		db.Logger.LogMode(gormlogger.Error)
		err = db.AutoMigrate(&model.ScenarioResultRecord{})
		su.Require().NoError(err)
		dao, err := model.NewScenarioResultRecordDAO(db)
		su.Require().NoError(err)
		su.scenarioResultRecordDAO = dao
		su.Require().NotNil(su.scenarioResultRecordDAO)
	}
	{
		db, err := gorm.Open(mysql.Open(su.dbAPIGeneratorDSN), &gorm.Config{})
		su.Require().NoError(err)
		dao, err := model.NewGenerationStepDAO(db)
		su.Require().NoError(err)
		su.generationStepDAO = dao
		su.Require().NotNil(su.generationStepDAO)
	}
	if !su.readCaseFromFS {
		// read step from db
		ctx := context.Background()
		steps, err := su.generationStepDAO.ListGenerationStep(ctx, su.generationID)
		su.Require().NoError(err)
		for i := range steps {
			step := &(steps[i])
			su.generationSteps = append(su.generationSteps, step)
		}
	} else {
		var scenariofiles model.ScenarioFile
		bs, err := testdata.LoadYAML(su.scenarioYAML)
		su.Require().NoError(err)
		err = yaml.Unmarshal(bs, &scenariofiles)
		su.Require().NoError(err)
		su.scenarioContext = &scenariofiles.Scenario.Context
		scenariocases := scenariofiles.Steps
		for i := range scenariocases {
			step := &scenariocases[i]
			su.generationSteps = append(su.generationSteps, step)
		}
	}

	// make sure all step is parseable
	for _, step := range su.generationSteps {
		switch step.RequestType {
		case model.RequestTypeHTTP:
			_, err := step.ParseHTTPRequestSpec()
			su.Require().NoErrorf(err, "case_id:%s, name:%s, ParseHTTPRequestSpec fialed", step.CaseID, step.Name)
			_, err = step.ParseHTTPResponseExpect()
			su.Require().NoErrorf(err, "case_id:%s, name:%s, ParseHTTPResponseExpect fialed", step.CaseID, step.Name)
			_, err = step.ParseSaveSpec()
			su.Require().NoErrorf(err, "case_id:%s, name:%s, ParseSaveSpec fialed", step.CaseID, step.Name)
		case model.RequestTypeCMDAWS, model.RequestTypeCMDGCP, model.RequestTypeCMDAzure, model.RequestTypeCMDMysqlsh, model.RequestTypeCMDALIYUN:
			_, err := step.ParseCommandSpec()
			su.Require().NoErrorf(err, "case_id:%s, name:%s, ParseCommandSpec fialed", step.CaseID, step.Name)
			_, err = step.ParseCommandExpect()
			su.Require().NoErrorf(err, "case_id:%s, name:%s, ParseCommandExpect fialed", step.CaseID, step.Name)
			_, err = step.ParseSaveSpec()
			su.Require().NoErrorf(err, "case_id:%s, name:%s, ParseSaveSpec fialed", step.CaseID, step.Name)
		}
	}

	switch su.cloudProvider {
	case "aws":
		options := make([]func(*config.LoadOptions) error, 0)
		if su.awsProfile != "" {
			options = append(options, config.WithSharedConfigProfile(su.awsProfile))
		}
		awsCfg, err := config.LoadDefaultConfig(context.Background(), options...)
		su.Require().NoError(err, "load default aws config failed")
		su.ec2Cli = ec2.NewFromConfig(awsCfg)
		su.Require().NotNil(su.ec2Cli)
	case "gcp":
	case "azure":
	case "alicloud":
	}
}

func (su *TestScenarioSuite) TearDownSuite() {
	if su.skipTearDown {
		return
	}
	su.clearClusters()
	// TODO: check and clear created resource on cloud provider
	switch su.cloudProvider {
	case "aws":
		su.clearAWSRejectedVpcEndpoints()
	case "gcp":
		// TODO: clear customer resource
	case "azure":
		// TODO: clear customer resource
	case "alicloud":
		// TODO: clear customer resource
	}
}

func (su *TestScenarioSuite) clearClusters() {
	ctx := context.Background()
	clusterIDs := make([]string, 0)

	for k, v := range su.actionBuilder.scenarioPlaceholder {
		if strings.HasPrefix(k, "cluster") {
			clusterIDs = append(clusterIDs, v)
		}
	}
	su.T().Logf("DEBUG: clusters to clear:%v", clusterIDs)
	for _, clusterID := range clusterIDs {
		_, resp, err := su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceGetCluster(ctx, clusterID).
			Execute() //nolint:bodyclose // resp body is already closed in Execute()
		if resp != nil && resp.StatusCode == http.StatusNotFound {
			continue
		}
		if err != nil {
			header := make(http.Header)
			if resp != nil {
				header = resp.Header
			}
			su.T().Logf("WARN: get cluster %s failed, resp_header:%v, err:%s", clusterID, header, err.Error())
		}
		_, _, err = su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceDeleteCluster(ctx, clusterID).
			Execute() //nolint:bodyclose // resp body is already closed in Execute()
		if err != nil {
			header := make(http.Header)
			if resp != nil {
				header = resp.Header
			}
			su.T().Logf("WARN: delete cluster %s failed, resp_header:%v, err:%s", clusterID, header, err.Error())
		}
	}

	// fallback cleanup: delete cluster by v1beta2 API
	username, password, _ := strings.Cut(su.openAPIKey, ":")
	httpClient := &http.Client{
		Timeout:   30 * time.Second,
		Transport: &digest.Transport{Username: username, Password: password},
	}
	for _, clusterID := range clusterIDs {
		url := fmt.Sprintf("https://%s/v1beta2/tidbs/%s", su.host, clusterID)
		req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, http.NoBody)
		if err != nil {
			su.T().Logf("WARN: build v1beta2 delete request failed, cluster:%s, err:%s", clusterID, err.Error())
			continue
		}
		resp, err := httpClient.Do(req)
		if err != nil {
			su.T().Logf("WARN: v1beta2 delete cluster %s failed, err:%s", clusterID, err.Error())
			continue
		}
		if _, err = io.Copy(io.Discard, resp.Body); err != nil {
			su.T().Logf("WARN: failed to drain response body for cluster %s: %v", clusterID, err)
		}
		_ = resp.Body.Close()
		if (resp.StatusCode < 200 || resp.StatusCode >= 300) && resp.StatusCode != http.StatusNotFound {
			su.T().Logf("WARN: v1beta2 delete cluster %s failed, status:%d, resp_header:%v", clusterID, resp.StatusCode, resp.Header)
		}
	}
}

func (su *TestScenarioSuite) clearAWSRejectedVpcEndpoints() {
	ctx := context.Background()
	params := new(ec2.DescribeVpcEndpointsInput)
	params.Filters = append(params.Filters,
		ec2types.Filter{
			Name:   lo.ToPtr("vpc-id"),
			Values: []string{awsCustomerVpcID},
		},
		ec2types.Filter{
			Name:   lo.ToPtr("vpc-endpoint-state"),
			Values: []string{"rejected"},
		})
	output, err := su.ec2Cli.DescribeVpcEndpoints(ctx, params)
	su.Require().NoError(err)
	vpcEndpointIds := make([]string, 0)
	for i := range output.VpcEndpoints {
		endpoint := output.VpcEndpoints[i]
		vpcEndpointIds = append(vpcEndpointIds, lo.FromPtr(endpoint.VpcEndpointId))
	}
	su.T().Logf("DEBUG: aws vpcendpoint to clear:%v", vpcEndpointIds)
	if len(vpcEndpointIds) > 0 {
		_, err = su.ec2Cli.DeleteVpcEndpoints(ctx, &ec2.DeleteVpcEndpointsInput{
			VpcEndpointIds: vpcEndpointIds,
		})
		su.Require().NoError(err)
	}
}

func (su *TestScenarioSuite) Test_001_ExecuteActions() {
	scenario := ""
	if su.scenarioYAML != "" {
		scenario = su.scenarioYAML
	}
	for i := range su.generationSteps {
		step := su.generationSteps[i]
		// su.actionBuilder.scenarioPlaceholder["curr_step_start_ts"] = strconv.FormatInt(time.Now().Unix(), 10)
		su.T().Logf("DEBUG: do step, case:%v, operation:%v, order:%v, request_type:%v, saved_placeholder:%v",
			step.CaseID, step.OperationID, step.Order, step.RequestType, su.actionBuilder.scenarioPlaceholder)
		action, err := su.actionBuilder.BuildAction(step, su.scenarioContext)
		su.Require().NoError(err, "build action failed, case:%v", step.CaseID)
		err = action.Do()
		if !step.ContinueOnUnexpected {
			su.Require().
				NoError(err, "do action failed, scenario:%v, case:%v, order:%v", scenario, step.CaseID, step.Order)
		} else {
			su.T().Logf("ERROR: do action failed, scenario:%v, case:%v, order:%v, err:%v", scenario, step.CaseID, step.Order, err)
		}
		// update placeholder
		action.FillPlaceholder(su.actionBuilder.scenarioPlaceholder)
		result, err := action.BuildResult(step)
		su.Require().NoError(err, "build result failed, case:%v", step.CaseID)
		su.scenarioResultRecords = append(su.scenarioResultRecords, result)
	}
}

func (su *TestScenarioSuite) Test_999_Summary() {
	ctx := context.Background()
	finishAt := time.Now()
	failed := 0
	total := len(su.scenarioResultRecords)
	for i := range su.scenarioResultRecords {
		record := su.scenarioResultRecords[i]
		record.WorkflowFinishAt = finishAt
		record.Env = su.env
		record.CloudProvider = su.cloudProvider
		record.WorkflowStartAt = su.ghStartAt
		record.RunID = su.ghRunID
		record.ExecutionID = su.executionID
	}
	for _, record := range su.scenarioResultRecords {
		if !record.Expected {
			su.T().Logf("WARN: case:%v do not match expect, result:%v", record.CaseID, record.Result)
			failed++
		}
	}
	if su.saveResultToDB {
		for _, result := range su.scenarioResultRecords {
			err := su.scenarioResultRecordDAO.CreateScenarioResultRecord(ctx, result)
			su.Require().NoError(err)
		}
	}
	su.T().Logf("DEBUG: generation %s has %v out of %v cases failed", su.generationID, failed, total)
}
