package action

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/fastflow/pkg/entity/run"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"go.uber.org/mock/gomock"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/regionserver/mock_regionserver"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cluster/mock_cluster"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/region/mock_region"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func TestCheckGKEHeterogeneousDeployReadyForGatekeeper(t *testing.T) {
	t.Run("ready", func(t *testing.T) {
		err := checkGKEHeterogeneousDeployReadyForGatekeeper(&regionaldedicatedv1.GKEHeterogeneousDeploy{
			Status: regionaldedicatedv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_READY_FOR_GATEKEEPER,
		})
		require.NoError(t, err)
	})

	t.Run("not ready", func(t *testing.T) {
		err := checkGKEHeterogeneousDeployReadyForGatekeeper(&regionaldedicatedv1.GKEHeterogeneousDeploy{
			Status: regionaldedicatedv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY,
		})
		require.Error(t, err)
		require.Equal(t, bizerr.ErrCodeUnknown, err.ErrCode())
		require.Contains(t, err.Error(), "not ready for gatekeeper")
	})

	t.Run("heterogeneous ready", func(t *testing.T) {
		err := checkGKEHeterogeneousDeployReadyForGatekeeper(&regionaldedicatedv1.GKEHeterogeneousDeploy{
			Status: regionaldedicatedv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_HETEROGENEOUS_READY,
		})
		require.NoError(t, err)
	})

	t.Run("empty response", func(t *testing.T) {
		err := checkGKEHeterogeneousDeployReadyForGatekeeper(nil)
		require.Error(t, err)
		require.Equal(t, bizerr.ErrCodeRegionalAPI, err.ErrCode())
		require.Contains(t, err.Error(), "empty gke heterogeneous deploy response")
	})
}

func TestCreateGKEHeterogeneousGatekeeperAssignActionEnsureExistingAssign(t *testing.T) {
	regionEn := &entity.Region{}
	target := &gkeGatekeeperTarget{
		region:        regionEn,
		clusterID:     "10365974781603411878",
		componentType: regionaldedicatedv1.ComponentType_TIDB,
		tidbGroupID:   "2061736176355643392",
		tidbGroupKind: regionaldedicatedv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
	}
	deploy := &regionaldedicatedv1.GKEHeterogeneousDeploy{
		Status:           regionaldedicatedv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_READY_FOR_GATEKEEPER,
		ComputeClassName: "cc-current",
	}

	t.Run("existing assign matches target", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousGatekeeperAssign(
			gomock.Any(), regionEn, gomock.Any(),
		).Return(&regionaldedicatedv1.GKEHeterogeneousGatekeeperAssign{
			Name:             "assign-current",
			ComponentType:    regionaldedicatedv1.ComponentType_TIDB,
			TidbGroupId:      "2061736176355643392",
			TidbGroupKind:    regionaldedicatedv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
			ComputeClassName: "cc-current",
		}, nil)

		a := &CreateGKEHeterogeneousGatekeeperAssignAction{regionalCli: mockCli}
		err := a.ensureGatekeeperAssign(context.Background(), target, deploy)
		require.NoError(t, err)
	})

	t.Run("existing assign compute class mismatch fails fast", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousGatekeeperAssign(
			gomock.Any(), regionEn, gomock.Any(),
		).Return(&regionaldedicatedv1.GKEHeterogeneousGatekeeperAssign{
			Name:             "assign-old",
			ComponentType:    regionaldedicatedv1.ComponentType_TIDB,
			TidbGroupId:      "2061736176355643392",
			TidbGroupKind:    regionaldedicatedv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
			ComputeClassName: "cc-old",
		}, nil)

		a := &CreateGKEHeterogeneousGatekeeperAssignAction{regionalCli: mockCli}
		err := a.ensureGatekeeperAssign(context.Background(), target, deploy)
		require.Error(t, err)
		require.Equal(t, bizerr.ErrCodeRegionalAPI, err.ErrCode())
		require.Contains(t, err.Error(), "compute class mismatch")
	})

	t.Run("existing assign component mismatch fails fast", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousGatekeeperAssign(
			gomock.Any(), regionEn, gomock.Any(),
		).Return(&regionaldedicatedv1.GKEHeterogeneousGatekeeperAssign{
			Name:             "assign-tikv",
			ComponentType:    regionaldedicatedv1.ComponentType_TIKV,
			TidbGroupId:      "2061736176355643392",
			TidbGroupKind:    regionaldedicatedv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
			ComputeClassName: "cc-current",
		}, nil)

		a := &CreateGKEHeterogeneousGatekeeperAssignAction{regionalCli: mockCli}
		err := a.ensureGatekeeperAssign(context.Background(), target, deploy)
		require.Error(t, err)
		require.Equal(t, bizerr.ErrCodeRegionalAPI, err.ErrCode())
		require.Contains(t, err.Error(), "component mismatch")
	})

	t.Run("existing assign tidb group mismatch fails fast", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousGatekeeperAssign(
			gomock.Any(), regionEn, gomock.Any(),
		).Return(&regionaldedicatedv1.GKEHeterogeneousGatekeeperAssign{
			Name:             "assign-old-group",
			ComponentType:    regionaldedicatedv1.ComponentType_TIDB,
			TidbGroupId:      "old-group",
			TidbGroupKind:    regionaldedicatedv1.TiDBGroupKind_TIDB_GROUP_KIND_EXTRA,
			ComputeClassName: "cc-current",
		}, nil)

		a := &CreateGKEHeterogeneousGatekeeperAssignAction{regionalCli: mockCli}
		err := a.ensureGatekeeperAssign(context.Background(), target, deploy)
		require.Error(t, err)
		require.Equal(t, bizerr.ErrCodeRegionalAPI, err.ErrCode())
		require.Contains(t, err.Error(), "tidb group mismatch")
	})

	t.Run("existing assign tidb group kind mismatch fails fast", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousGatekeeperAssign(
			gomock.Any(), regionEn, gomock.Any(),
		).Return(&regionaldedicatedv1.GKEHeterogeneousGatekeeperAssign{
			Name:             "assign-default-group",
			ComponentType:    regionaldedicatedv1.ComponentType_TIDB,
			TidbGroupId:      "2061736176355643392",
			TidbGroupKind:    regionaldedicatedv1.TiDBGroupKind_TIDB_GROUP_KIND_DEFAULT,
			ComputeClassName: "cc-current",
		}, nil)

		a := &CreateGKEHeterogeneousGatekeeperAssignAction{regionalCli: mockCli}
		err := a.ensureGatekeeperAssign(context.Background(), target, deploy)
		require.Error(t, err)
		require.Equal(t, bizerr.ErrCodeRegionalAPI, err.ErrCode())
		require.Contains(t, err.Error(), "tidb group kind mismatch")
	})
}

func TestPollUntilReadyCheckReadyStability(t *testing.T) {
	regionEn := &entity.Region{}
	clusterID := "10365974781603411878"
	componentType := regionaldedicatedv1.ComponentType_TIDB
	tidbGroupID := "2061736176355643392"

	readyDeploy := &regionaldedicatedv1.GKEHeterogeneousDeploy{
		Status: regionaldedicatedv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_READY_FOR_GATEKEEPER,
	}
	notReadyDeploy := &regionaldedicatedv1.GKEHeterogeneousDeploy{
		Status: regionaldedicatedv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY,
	}

	t.Run("ready remains ready", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousDeploy(
			gomock.Any(), regionEn,
			gomock.Any(),
		).Return(readyDeploy, nil)

		a := &PollUntilGKEHeterogeneousDeployReadyForGatekeeperAction{
			regionalCli:         mockCli,
			readyStabilityDelay: time.Nanosecond,
		}
		err := a.checkReadyStability(context.Background(), regionEn, clusterID, componentType, tidbGroupID)
		require.NoError(t, err)
	})

	t.Run("ready reverts to not ready", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousDeploy(
			gomock.Any(), regionEn,
			gomock.Any(),
		).Return(notReadyDeploy, nil)

		a := &PollUntilGKEHeterogeneousDeployReadyForGatekeeperAction{
			regionalCli:         mockCli,
			readyStabilityDelay: time.Nanosecond,
		}
		err := a.checkReadyStability(context.Background(), regionEn, clusterID, componentType, tidbGroupID)
		require.Error(t, err)
		require.Contains(t, err.Error(), "not ready for gatekeeper")
	})

	t.Run("regional api error on recheck", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousDeploy(
			gomock.Any(), regionEn,
			gomock.Any(),
		).Return(nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "regional server unavailable"))

		a := &PollUntilGKEHeterogeneousDeployReadyForGatekeeperAction{
			regionalCli:         mockCli,
			readyStabilityDelay: time.Nanosecond,
		}
		err := a.checkReadyStability(context.Background(), regionEn, clusterID, componentType, tidbGroupID)
		require.Error(t, err)
		require.Contains(t, err.Error(), "regional server unavailable")
	})

	t.Run("context canceled during wait", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()
		mockCli := mock_regionserver.NewMockClient(ctrl)

		ctx, cancel := context.WithCancel(context.Background())
		cancel()

		a := &PollUntilGKEHeterogeneousDeployReadyForGatekeeperAction{
			regionalCli:         mockCli,
			readyStabilityDelay: time.Minute,
		}
		err := a.checkReadyStability(ctx, regionEn, clusterID, componentType, tidbGroupID)
		require.ErrorIs(t, err, context.Canceled)
	})
}

func TestPollUntilGKEHeterogeneousDeployReadyForGatekeeperAction_Run(t *testing.T) {
	cluster := &entity.Cluster{
		Cluster: &globaldedicatedv1.Cluster{
			ClusterId:       10365974781603411878,
			OrganizationId:  60205,
			ProjectId:       3199728,
			CloudProviderId: globalcommonv1.CloudProvider_GCP,
			RegionId:        globalcommonv1.Region_GCP_US_WEST1,
			State:           globaldedicatedv1.Cluster_AVAILABLE,
		},
	}
	regionEn := &entity.Region{
		Id:    globalcommonv1.Region_GCP_US_WEST1,
		State: globalcommonv1.Region_ACTIVE,
	}

	readyDeploy := &regionaldedicatedv1.GKEHeterogeneousDeploy{
		Status: regionaldedicatedv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_READY_FOR_GATEKEEPER,
	}
	notReadyDeploy := &regionaldedicatedv1.GKEHeterogeneousDeploy{
		Status: regionaldedicatedv1.GKEHeterogeneousDeployStatus_GKE_HETEROGENEOUS_DEPLOY_STATUS_INTENT_ENABLED_NOT_READY,
	}

	applyReqJSON := `{"tenantId":"60205","projectId":"3199728","clusterId":"10365974781603411878","componentType":"TIKV","currentInstanceType":"n2-standard-8","alternativeInstanceType":"n2-standard-16"}`

	newExecContext := func() *run.DefExecuteContext {
		return run.NewDefExecuteContext(
			context.Background(),
			nil,
			nil,
			func(key string) (string, bool) { return "", false },
			nil,
		)
	}

	t.Run("first call not ready returns error with single GET", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		clusterRepo := mock_cluster.NewMockRepo(ctrl)
		clusterRepo.EXPECT().Get(gomock.Any(), uint64(10365974781603411878), gomock.Any()).Return(cluster, nil).Times(2)

		regionRepo := mock_region.NewMockRepo(ctrl)
		regionRepo.EXPECT().SelectRegionById(gomock.Any(), globalcommonv1.Region_GCP_US_WEST1, uint64(60205)).Return(regionEn, nil)

		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousDeploy(gomock.Any(), regionEn, gomock.Any()).Return(notReadyDeploy, nil).Times(1)

		a := &PollUntilGKEHeterogeneousDeployReadyForGatekeeperAction{
			clusterRepo:         clusterRepo,
			regionRepo:          regionRepo,
			regionalCli:         mockCli,
			readyStabilityDelay: time.Nanosecond,
		}

		err := a.Run(newExecContext(), &gkeHeterogeneousDeployActionParam{RequestBody: applyReqJSON})
		require.Error(t, err)
		require.Contains(t, err.Error(), "not ready for gatekeeper")
	})

	t.Run("ready reverts to not ready returns error with two GETs", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		clusterRepo := mock_cluster.NewMockRepo(ctrl)
		clusterRepo.EXPECT().Get(gomock.Any(), uint64(10365974781603411878), gomock.Any()).Return(cluster, nil).Times(2)

		regionRepo := mock_region.NewMockRepo(ctrl)
		regionRepo.EXPECT().SelectRegionById(gomock.Any(), globalcommonv1.Region_GCP_US_WEST1, uint64(60205)).Return(regionEn, nil)

		mockCli := mock_regionserver.NewMockClient(ctrl)
		gomock.InOrder(
			mockCli.EXPECT().GetGKEHeterogeneousDeploy(gomock.Any(), regionEn, gomock.Any()).Return(readyDeploy, nil),
			mockCli.EXPECT().GetGKEHeterogeneousDeploy(gomock.Any(), regionEn, gomock.Any()).Return(notReadyDeploy, nil),
		)

		a := &PollUntilGKEHeterogeneousDeployReadyForGatekeeperAction{
			clusterRepo:         clusterRepo,
			regionRepo:          regionRepo,
			regionalCli:         mockCli,
			readyStabilityDelay: time.Nanosecond,
		}

		err := a.Run(newExecContext(), &gkeHeterogeneousDeployActionParam{RequestBody: applyReqJSON})
		require.Error(t, err)
		require.Contains(t, err.Error(), "not ready for gatekeeper")
	})

	t.Run("ready remains ready returns nil with two GETs", func(t *testing.T) {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		clusterRepo := mock_cluster.NewMockRepo(ctrl)
		clusterRepo.EXPECT().Get(gomock.Any(), uint64(10365974781603411878), gomock.Any()).Return(cluster, nil).Times(2)

		regionRepo := mock_region.NewMockRepo(ctrl)
		regionRepo.EXPECT().SelectRegionById(gomock.Any(), globalcommonv1.Region_GCP_US_WEST1, uint64(60205)).Return(regionEn, nil)

		mockCli := mock_regionserver.NewMockClient(ctrl)
		mockCli.EXPECT().GetGKEHeterogeneousDeploy(gomock.Any(), regionEn, gomock.Any()).Return(readyDeploy, nil).Times(2)

		a := &PollUntilGKEHeterogeneousDeployReadyForGatekeeperAction{
			clusterRepo:         clusterRepo,
			regionRepo:          regionRepo,
			regionalCli:         mockCli,
			readyStabilityDelay: time.Nanosecond,
		}

		err := a.Run(newExecContext(), &gkeHeterogeneousDeployActionParam{RequestBody: applyReqJSON})
		require.NoError(t, err)
	})
}
