package autogencase

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

	"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 TestFuzzyNetworkContainerServiceSuite struct {
	suite.Suite

	// can set by config.yaml
	orgID              string
	projectID          string
	provider           string
	clusterName        string
	clusterNamePrefix  string
	openAPIHost        string
	openAPIKey         string
	skipTearDown       bool
	env                string
	region             string
	dbAPITesterDSN     string
	dbAPIGeneratorDSN  string
	networkContainerID string

	customerAccountID string
	customerVpcID     string
	customerVpcCidr   string
	customerRegionID  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
	networkContainersToClean map[string]struct{}
	vpcpeeringToClean        map[string]struct{}
	placeHolderLookup        placeHolderLookup
	ratelimiter              ratelimit.Limiter
}

func TestFuzzyNetworkContainerService(t *testing.T) {
	testSuite := &TestFuzzyNetworkContainerServiceSuite{
		skipTearDown:             viper.GetBool("skip_tear_down"),
		orgID:                    viper.GetString("org_id"),
		projectID:                viper.GetString("prj_id"),
		provider:                 viper.GetString("cloud_provider"),
		clusterNamePrefix:        viper.GetString("cluster_name_prefix"),
		openAPIKey:               viper.GetString("api_key"),
		openAPIHost:              viper.GetString("host"),
		networkContainerID:       viper.GetString("network_container_id"),
		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),
		networkContainersToClean: make(map[string]struct{}),
		vpcpeeringToClean:        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"
		testSuite.customerAccountID = awsCustomerAccountID
		testSuite.customerRegionID = "aws" + "-" + awsCustomerRegion
		testSuite.customerVpcID = awsCustomerVpcID
		testSuite.customerVpcCidr = awsCustomerVpcCidr
	case "gcp":
		testSuite.region = "us-west1"
		testSuite.customerAccountID = gcpCustomerProject
		testSuite.customerRegionID = "gcp" + "-" + gcpCustomerRegion
		testSuite.customerVpcID = gcpCustomerNetwork
		testSuite.customerVpcCidr = gcpCustomerSubNetworkCidr
	case "azure":
		testSuite.region = "eastus2"
	}
	testSuite.clusterName = testSuite.clusterNamePrefix + randstr.GenWithCharset(4, randstr.AlphabetCharset)

	if testSuite.networkContainerID != "" {
		testSuite.placeHolderLookup["networkContainerId"] = testSuite.networkContainerID
	}
	regionID := testSuite.provider + "-" + testSuite.region
	testSuite.placeHolderLookup["regionId"] = regionID
	testSuite.placeHolderLookup["regionId_666"] = regionID
	testSuite.placeHolderLookup["tidbCloudRegionId"] = regionID
	testSuite.placeHolderLookup["tidbCloudRegionId_6666"] = regionID
	testSuite.placeHolderLookup["tidb.cloud/project"] = testSuite.projectID
	testSuite.placeHolderLookup["customerAccountId"] = testSuite.customerAccountID
	testSuite.placeHolderLookup["customerAccountId_666"] = testSuite.customerAccountID
	testSuite.placeHolderLookup["customerVpcCidr"] = testSuite.customerVpcCidr
	testSuite.placeHolderLookup["customerVpcCidr_666"] = testSuite.customerVpcCidr
	testSuite.placeHolderLookup["customerVpcId"] = testSuite.customerVpcID
	testSuite.placeHolderLookup["customerVpcId_666"] = testSuite.customerVpcID

	suite.Run(t, testSuite)
}

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

func (su *TestFuzzyNetworkContainerServiceSuite) TearDownSuite() {
	if su.skipTearDown {
		return
	}
	ctx := context.Background()
	su.T().Logf("vpcpeering to clean:%v", su.vpcpeeringToClean)
	for id := range su.vpcpeeringToClean {
		_, resp, err := su.dedicatedAPICli.NetworkContainerServiceAPI.NetworkContainerServiceDeleteVpcPeering(ctx, id).
			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 NetworkContainerServiceDeleteVpcPeering failed, err:%v, clusterName:%v, x-debug-trace-id:%v",
					err, su.clusterName, resp.Header.Get("x-debug-trace-id"))
		}
	}
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_000_ListNetworkContainers() {
	operationID := ServiceNameNetworkContainerService + "_" + "ListNetworkContainers"
	su.runTestCases(operationID)
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_001_CreateNetworkContainer() {
	operationID := ServiceNameNetworkContainerService + "_" + "CreateNetworkContainer"
	su.runTestCases(operationID)
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_002_ListVpcPeerings() {
	operationID := ServiceNameNetworkContainerService + "_" + "ListVpcPeerings"
	su.runTestCases(operationID)
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_100_PrepareNetworkContainer() {
	if su.networkContainerID != "" {
		return
	}
	regionID := su.provider + "-" + su.region
	id := ""
	isActive := false
	ctx := context.Background()
	{
		req := su.dedicatedAPICli.NetworkContainerServiceAPI.NetworkContainerServiceListNetworkContainers(ctx).
			ProjectId(su.projectID).
			CloudProvider(su.provider)
		list, 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 NetworkContainerServiceListNetworkContainers failed, err:%v, project:%v, x-debug-trace-id:%v, resp_body:%v",
					err, su.projectID, resp.Header.Get("x-debug-trace-id"), body)
		}
		su.Require().NoError(err,
			"call NetworkContainerServiceListNetworkContainers failed, project:%v, x-debug-trace-id:%v",
			su.projectID, resp.Header.Get("x-debug-trace-id"))

		for _, v := range list.GetNetworkContainers() {
			if v.GetRegionId() == regionID {
				id = v.GetNetworkContainerId()
				isActive = v.GetState() == dedicatedapiclient.V1BETA1NETWORKCONTAINERSTATE_ACTIVE
				break
			}
		}
	}
	if isActive {
		su.networkContainerID = id
		su.placeHolderLookup["networkContainerId"] = id
		return
	}
	if id == "" {
		params := dedicatedapiclient.V1beta1NetworkContainer{
			Labels:       lo.ToPtr(map[string]string{"tidb.cloud/project": su.projectID}),
			RegionId:     fmt.Sprintf("%s-%s", su.provider, su.region),
			CidrNotation: lo.ToPtr("172.17.0.0/16"),
		}
		req := su.dedicatedAPICli.NetworkContainerServiceAPI.NetworkContainerServiceCreateNetworkContainer(ctx).
			NetworkContainer(params)
		nc, 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 NetworkContainerServiceCreateNetworkContainer failed, err:%v, x-debug-trace-id:%v, resp_body:%v",
					err, resp.Header.Get("x-debug-trace-id"), body)
		}
		su.Require().NoError(err,
			"call NetworkContainerServiceCreateNetworkContainer failed, x-debug-trace-id:%v",
			resp.Header.Get("x-debug-trace-id"))
		id = nc.GetNetworkContainerId()
		su.networkContainerID = id
		su.placeHolderLookup["networkContainerId"] = id
		su.T().Logf("create netowrkcontainer %v", id)
	}
	// create cluster and delete to active the networkcontainer
	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, x-debug-trace-id:%v, resp_body:%v",
			err, 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"))
	_, _, err = su.dedicatedAPICli.ClusterServiceAPI.ClusterServiceDeleteCluster(ctx, cluster.GetClusterId()).
		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 ClusterServiceDeleteCluster failed, err:%v, x-debug-trace-id:%v, resp_body:%v",
			err, resp.Header.Get("x-debug-trace-id"), body)
	}
	su.Require().NoErrorf(err,
		"call ClusterServiceDeleteCluster failed, clusterName:%v, x-debug-trace-id:%v",
		su.clusterName, resp.Header.Get("x-debug-trace-id"))
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_101_WaitNetworkContainerActive() {
	su.waitNetworkContainerActive(su.networkContainerID, 30*time.Second)
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_200_CreateVpcPeerings() {
	if !su.supportVpcPeering() {
		return
	}
	operationID := ServiceNameNetworkContainerService + "_" + "CreateVpcPeerings"
	su.runTestCases(operationID)
}

// clear the created vpcpeeing
func (su *TestFuzzyNetworkContainerServiceSuite) Test_300_ClearConflictVpcPeerings() {
	ctx := context.Background()
	req := su.dedicatedAPICli.NetworkContainerServiceAPI.NetworkContainerServiceListVpcPeerings(ctx)
	req.ProjectId(su.projectID)
	req.CloudProvider(su.provider)
	out, resp, err := req.Execute() //nolint:bodyclose // resp body is already closed in Execute()
	if err != nil {
		var body string
		var traceID string
		if resp != nil {
			bs, _ := io.ReadAll(resp.Body)
			body = string(bs)
			traceID = resp.Header.Get("x-debug-trace-id")
		}
		su.T().Logf("call NetworkContainerServiceListVpcPeerings failed, err:%v, x-debug-trace-id:%v, resp_body:%v",
			err, traceID, body)
	}
	su.Require().NoError(err)
	for i := range out.VpcPeerings {
		vpcpeering := out.VpcPeerings[i]
		if vpcpeering.GetCustomerVpcId() != su.customerVpcID {
			continue
		}
		id := vpcpeering.GetVpcPeeringId()
		_, resp, err := su.dedicatedAPICli.NetworkContainerServiceAPI.NetworkContainerServiceDeleteVpcPeering(ctx, id).
			Execute() //nolint:bodyclose // resp body is already closed in Execute()
		su.Require().NotNil(resp, "call NetworkContainerServiceDeleteVpcPeering failed, err: %s", err.Error())
		if resp.StatusCode == http.StatusNotFound {
			continue
		}
		if err != nil {
			su.T().
				Logf("call NetworkContainerServiceDeleteVpcPeering failed, err:%v, clusterName:%v, x-debug-trace-id:%v",
					err, su.clusterName, resp.Header.Get("x-debug-trace-id"))
		}
	}
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_301_PrepareVpcPeering() {
	if !su.supportVpcPeering() {
		return
	}
	ctx := context.Background()
	tidbcloudRegionID := su.provider + "-" + su.region
	params := dedicatedapiclient.NewDedicatedv1beta1VpcPeering(
		tidbcloudRegionID,
		su.customerAccountID,
		su.customerVpcID,
		su.customerVpcCidr,
	)
	params.SetLabels(map[string]string{
		"tidb.cloud/project": su.projectID,
	})
	params.SetCustomerRegionId(su.customerRegionID)
	cloudprovider, err := dedicatedapiclient.NewV1beta1RegionCloudProviderFromValue(su.provider)
	su.Require().NoError(err)
	params.SetTidbCloudCloudProvider(*cloudprovider)
	req := su.dedicatedAPICli.NetworkContainerServiceAPI.NetworkContainerServiceCreateVpcPeering(ctx).
		VpcPeering(*params)
	vpcpeering, 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)
		}
		payload, _ := json.Marshal(params)
		su.T().
			Logf("call NetworkContainerServiceCreateVpcPeering failed, err:%v, payload:%v, x-debug-trace-id:%v, resp_body:%v",
				err, string(payload), resp.Header.Get("x-debug-trace-id"), body)
	}
	su.Require().NoError(err,
		"call NetworkContainerServiceCreateVpcPeering failed, x-debug-trace-id:%v",
		resp.Header.Get("x-debug-trace-id"))
	id := vpcpeering.GetVpcPeeringId()
	su.placeHolderLookup["vpcPeeringId"] = id
	su.vpcpeeringToClean[id] = struct{}{}
	su.T().Logf("create vpcpeering %v", id)
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_400_GetNetworkContainer() {
	operationID := ServiceNameNetworkContainerService + "_" + "GetNetworkContainer"
	su.runTestCases(operationID)
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_401_GetVpcPeering() {
	if !su.supportVpcPeering() {
		return
	}
	operationID := ServiceNameNetworkContainerService + "_" + "GetVpcPeering"
	su.runTestCases(operationID)
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_402_DeleteVpcPeering() {
	if !su.supportVpcPeering() {
		return
	}
	operationID := ServiceNameNetworkContainerService + "_" + "DeleteVpcPeering"
	su.runTestCases(operationID)
}

func (su *TestFuzzyNetworkContainerServiceSuite) Test_403_DeleteNetworkContainer() {
	operationID := ServiceNameNetworkContainerService + "_" + "DeleteNetworkContainer"
	su.runTestCases(operationID)
}

func (su *TestFuzzyNetworkContainerServiceSuite) 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 *TestFuzzyNetworkContainerServiceSuite) 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 *TestFuzzyNetworkContainerServiceSuite) runTestCases(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 *TestFuzzyNetworkContainerServiceSuite) waitNetworkContainerActive(id string, interval time.Duration) {
	ctx := context.Background()
	// Timeout: 30 minutes.
	for range 30 {
		time.Sleep(interval)
		req := su.dedicatedAPICli.NetworkContainerServiceAPI.NetworkContainerServiceGetNetworkContainer(ctx, id)
		nc, 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 NetworkContainerServiceCreateVpcPeering failed, err:%v, x-debug-trace-id:%v, resp_body:%v",
					err, resp.Header.Get("x-debug-trace-id"), body)
		}
		su.Require().NoErrorf(err, "call NetworkContainerServiceGetNetworkContainer failed, id:%v", id)
		su.T().Logf("networkconainer %v, state: %v", nc.GetNetworkContainerId(), nc.GetState())
		if nc.GetState() == dedicatedapiclient.V1BETA1NETWORKCONTAINERSTATE_ACTIVE {
			break
		}
	}
}

func (su *TestFuzzyNetworkContainerServiceSuite) supportVpcPeering() bool {
	return su.provider != "azure"
}
