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

const (
	overwriteImageTag = "v8.0.0"
)

type UpdateClusterImageTest struct {
	*ops.OpsTestBase

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

func RunUpdateClusterImageTest(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 *UpdateClusterImageTest) Run() {
	allure.Test(
		t.T,
		allure.Epic("TestConfigCenter"),
		allure.Feature("UpdateClusterImageTest"),
		allure.Description("modify component's images of a cluster"),
		allure.Action(func() {
			t.GetClusterConfigStep()

			t.PreviewConfigChange()

			t.CreateConfigChange()

			t.ApplyConfigChange()
		}),
	)
}

func (t *UpdateClusterImageTest) 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 *UpdateClusterImageTest) PreviewConfigChange() {
	allure.Step(
		allure.Description("preview config change"),
		allure.Action(func() {
			req := &opsmod.CloudconfigPreviewClusterConfigurationChangeReq{
				ClusterID:   &t.OriginConfig.TargetCluster.ClusterID,
				ClusterType: &t.OriginConfig.TargetCluster.ClusterType,
				Components:  t.buildChangeComponentReq(),
			}

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

			// check result
			if len(resp.Change.Scope.Components) == 0 {
				allure.FailNow(fmt.Errorf("scope of change is empty"))
				return
			}
			for _, compChg := range resp.Change.Scope.Components {
				err := t.checkChangedComponent(compChg)
				if err != nil {
					allure.FailNow(fmt.Errorf("check component change failed: %s", err))
					return
				}
			}
			if len(resp.Configuration.Components) == 0 {
				allure.FailNow(fmt.Errorf("scope of change is empty"))
				return
			}
			for _, comp := range resp.Configuration.Components {
				err := t.checkComponent(comp)
				if err != nil {
					allure.FailNow(fmt.Errorf("check component failed: %s", err))
					return
				}
			}
		}),
	)
}

func (t *UpdateClusterImageTest) 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: opsmod.CloudconfigParameterApplyPolicyRollingRestart,
				Reason:      t.MagicReason(),
				Components:  t.buildChangeComponentReq(),
			}

			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
			if len(t.Change.Scope.Components) == 0 {
				allure.FailNow(fmt.Errorf("scope of change is empty"))
				return
			}
			for _, compChg := range t.Change.Scope.Components {
				err := t.checkChangedComponent(compChg)
				if err != nil {
					allure.FailNow(fmt.Errorf("check component change failed: %s", err))
					return
				}
			}
		}),
	)
}

func (t *UpdateClusterImageTest) 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
			}

			// TODO: check result
		}),
	)
}

func (t *UpdateClusterImageTest) 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 *UpdateClusterImageTest) buildChangeComponentReq() []*opsmod.CloudconfigChangeComponentReq {
	result := make([]*opsmod.CloudconfigChangeComponentReq, 0)

	for _, comp := range t.OriginConfig.Components {
		compChange := &opsmod.CloudconfigChangeComponentReq{
			Component: comp.Name,
			OverwriteImageTag: &opsmod.CloudconfigChangeImageTagReq{
				ToImageTag: overwriteImageTag,
			},
		}

		result = append(result, compChange)
	}

	return nil
}

func (t *UpdateClusterImageTest) checkChangedComponent(compChg *opsmod.GithubComTidbcloudDevopsEntityCloudconfigConfigurationComponentChange) error {
	if compChg.ModifiedImageTag != nil {
		return fmt.Errorf("'modified_image_tag' is nil")
	}
	if compChg.ModifiedImageTag.To != overwriteImageTag {
		return fmt.Errorf("'modified_image_tag.to' is not %s", overwriteImageTag)
	}
	if compChg.ModifiedOverwirteImageTag != nil {
		return fmt.Errorf("'modified_overwirte_image_tag' is not nil")
	}
	if compChg.ModifiedOverwirteImageTag.From != "" {
		return fmt.Errorf("'modified_overwirte_image_tag.from' is not empty")
	}
	if compChg.ModifiedOverwirteImageTag.To != overwriteImageTag {
		return fmt.Errorf("'modified_overwirte_image_tag.to' is not %s", overwriteImageTag)
	}
	return nil
}

func (t *UpdateClusterImageTest) checkComponent(comp opsmod.CloudconfigComponent) error {
	if comp.EffectiveImageTag != overwriteImageTag {
		return fmt.Errorf("'effective_image_tag' is not %s", overwriteImageTag)
	}
	if comp.OverwriteImageTag != overwriteImageTag {
		return fmt.Errorf("'overwrite_image_tag' is not %s", overwriteImageTag)
	}
	return nil
}

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