package cases

import (
	"fmt"
	"time"

	l4g "github.com/alecthomas/log4go"
	"github.com/avast/retry-go"
	"github.com/dailymotion/allure-go"
	opsmod "github.com/tidbcloud/qa-PlatformAPIAuto/models/ops"
	"github.com/tidbcloud/qa-PlatformAPIAuto/webScene/ops"
	"github.com/tidbcloud/qa-gourd/ProductService"
)

type ModifyClusterConfigTest struct {
	*ops.OpsTestBase

	opToModifyParam []string

	OriginConfig  *opsmod.CloudconfigConfiguration
	PreviewResult *opsmod.CloudconfigPreviewConfigurationChangeResp
	Change        *opsmod.GithubComTidbcloudDevopsEntityCloudconfigConfigurationChange
}

func RunModifyClusterConfigTest(w *ProductService.WebRequestInfo, urls map[string]ProductService.UrlInfo) {
	base := &ops.OpsTestBase{}
	base.Init(w, urls)
	t := &ModifyClusterConfigTest{
		opToModifyParam: []string{opAdd, opMod, opDel},
		OpsTestBase:     base,
	}
	t.Run()
}

func (t *ModifyClusterConfigTest) Run() {
	allure.Test(
		t.T,
		allure.Epic("TestConfigCenter"),
		allure.Feature("ModifyClusterConfig"),
		allure.Description("modify a cluster config"),
		allure.Action(func() {
			t.GetClusterConfigStep()

			t.PreviewConfigChange()

			t.CreateConfigChange()

			t.ApplyConfigChange()
		}),
	)
}

func (t *ModifyClusterConfigTest) GetClusterConfigStep() {
	allure.Step(
		allure.Description("get current cluster config"),
		allure.Action(func() {
			resp, err := t.API.GetClusterConfiguration(t.W.Cluster.ClusterId)
			if err != nil {
				allure.FailNow(err)
				return
			}
			t.OriginConfig = resp.Configuration
		}),
	)
}

func (t *ModifyClusterConfigTest) PreviewConfigChange() {
	allure.Step(
		allure.Description("preview config change"),
		allure.Action(func() {
			req := &opsmod.CloudconfigPreviewClusterConfigurationChangeReq{
				ClusterID:   &t.OriginConfig.TargetCluster.ClusterID,
				ClusterType: &t.OriginConfig.TargetCluster.ClusterType,
				Files:       t.buildChangeFileReq(),
			}

			resp, err := t.API.PreviewClusterConfiguration(req)
			if err != nil {
				allure.FailNow(err)
				return
			}
			t.PreviewResult = resp

			// check result
			if len(resp.Change.Scope.Files) == 0 {
				allure.FailNow(fmt.Errorf("scope of change is empty"))
				return
			}
			for _, file := range resp.Change.Scope.Files {
				err := checkChangedParams(file.Filename, file.ModifiedParameters, opsmod.CloudconfigParameterSourceCustom, t.opToModifyParam)
				if err != nil {
					allure.FailNow(err)
					return
				}

			}
			for _, file := range resp.Configuration.Files {
				customParams := extractCustomParamsFromAllParams(file.AllParameters)
				if err := checkParams(file.Filename, customParams, t.opToModifyParam); err != nil {
					allure.FailNow(err)
					return
				}
			}
		}),
	)
}

func (t *ModifyClusterConfigTest) CreateConfigChange() {
	allure.Step(
		allure.Description("create config change and check"),
		allure.Action(func() {
			req := &opsmod.CloudconfigCreateClusterConfigurationChangeReq{
				ClusterID:   &t.OriginConfig.TargetCluster.ClusterID,
				ClusterType: &t.OriginConfig.TargetCluster.ClusterType,
				ApplyPolicy: t.PreviewResult.ApplyPolicy,
				Reason:      t.MagicReason(),
				Files:       t.buildChangeFileReq(),
			}

			resp, err := t.API.CreateClusterConfigChange(req)
			if err != nil {
				allure.FailNow(err)
				return
			}
			t.Change = &opsmod.GithubComTidbcloudDevopsEntityCloudconfigConfigurationChange{ // init a change with id to sync it.
				ID: resp.ChangeID,
			}

			err = t.syncCurrentChange()
			if err != nil {
				allure.FailNow(err)
				return
			}

			// check result
			for _, file := range t.Change.Scope.Files {
				if err := checkChangedParams(file.Filename, file.ModifiedParameters, opsmod.CloudconfigParameterSourceCustom, t.opToModifyParam); err != nil {
					allure.FailNow(err)
					return
				}
			}

			// check chaeable
			configResp, err := t.API.GetClusterConfiguration(t.W.Cluster.ClusterId)
			if err != nil {
				allure.FailNow(err)
				return
			}
			if configResp.Changeable.Changeable {
				allure.FailNow(fmt.Errorf("config is still changeable after create a change"))
				return
			}
		}),
	)
}

func (t *ModifyClusterConfigTest) ApplyConfigChange() {
	allure.Step(
		allure.Description("apply config change"),
		allure.Action(func() {
			err := t.waitChangeToAppliable()
			if err != nil {
				allure.FailNow(err)
				return
			}

			err = t.API.ApplyConfigChange(t.Change.ID)
			if err != nil {
				allure.FailNow(err)
				return
			}

			// diff the current parameter
			resp, err := t.API.GetClusterConfiguration(t.W.Cluster.ClusterId)
			if err != nil {
				allure.FailNow(err)
				return
			}

			// check result
			for _, file := range resp.Configuration.Files {
				customParams := extractCustomParamsFromAllParams(file.AllParameters)
				if err := checkParams(file.Filename, customParams, t.opToModifyParam); err != nil {
					allure.FailNow(err)
					return
				}
			}
		}),
	)
}

func (t *ModifyClusterConfigTest) waitChangeToAppliable() error {
	err := retry.Do(
		func() error {
			err := t.syncCurrentChange()
			if err != nil {
				return err
			}
			if t.Change.Phase != opsmod.CloudconfigConfigurationChangePhaseWaitForApply {
				return fmt.Errorf("change(%d) phase is '%s' but not 'wait_for_apply'", t.Change.ID, t.Change.Phase)
			}
			return nil
		},
		retry.DelayType(retry.FixedDelay),
		retry.Delay(time.Second*10),
		retry.Attempts(18),
		retry.OnRetry(func(n uint, err error) {
			l4g.Info("#%d: %s\n", n, err)
		}),
	)

	return err
}

func (t *ModifyClusterConfigTest) syncCurrentChange() error {
	change, err := t.API.GetConfigChange(t.Change.ID)
	if err != nil {
		return err
	}
	t.Change = change
	return nil
}

func (t *ModifyClusterConfigTest) buildChangeFileReq() []*opsmod.CloudconfigChangeFileReq {
	result := make([]*opsmod.CloudconfigChangeFileReq, 0)

	for _, file := range t.OriginConfig.Files {
		fileChange := &opsmod.CloudconfigChangeFileReq{
			Filename:         file.Filename,
			ConfigType:       file.ConfigType,
			PreviousText:     file.Text,
			CustomParameters: nil,
		}

		customParams := extractCustomParamsFromAllParams(file.AllParameters)

		fileChange.CustomParameters = modifyParams(file.Filename, customParams, t.opToModifyParam)
		result = append(result, fileChange)
	}

	return result
}
