package action

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	dataplanemgrmocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/nodegroup"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	repomnocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func TestReactivateNodeInstanceGroupAction_Name(t *testing.T) {
	stateRepo := repomnocks.NewDataplaneNodeInstanceGroupStateRepo(t)
	lockRepo := repomnocks.NewDataplaneNodeInstanceGroupLockRepo(t)
	dataplaneClient := dataplanemgrmocks.NewClient(t)
	dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)

	action := NewReactivateNodeInstanceGroupAction(stateRepo, lockRepo, dataplaneClient)

	assert.Equal(t, ReactivateNodeInstanceGroupActionName, action.Name())
}

func TestReactivateNodeInstanceGroupAction_Invoke(t *testing.T) {
	tests := []struct {
		name          string
		setupMocks    func(*repomnocks.DataplaneNodeInstanceGroupStateRepo, *repomnocks.DataplaneNodeInstanceGroupLockRepo, *dataplanemgrmocks.Client)
		wantCode      codes.Code
		errorContains string
	}{
		{
			name: "removes deprecated taint from matching primary and fallback nodepools",
			setupMocks: func(stateRepo *repomnocks.DataplaneNodeInstanceGroupStateRepo, lockRepo *repomnocks.DataplaneNodeInstanceGroupLockRepo, dataplaneClient *dataplanemgrmocks.Client) {
				lock := &entity.DataplaneNodeInstanceGroupLock{}
				lock.FencingToken = 1
				lockRepo.On("TryAcquire", mock.Anything, "dp-1", "4C16G", "m5.xlarge", mock.AnythingOfType("string"), DefaultLockLeaseDuration).Return(lock, nil)
				lockRepo.On("Release", mock.Anything, "dp-1", "4C16G", "m5.xlarge", mock.AnythingOfType("string")).Return(nil)

				primaryLabels := map[string]string{
					nodegroup.LabelNodeSpec:  "4C16G",
					nodegroup.LabelPriority:  "1000",
					nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager,
					"custom":                 "preserved",
				}
				fallbackLabels := map[string]string{
					nodegroup.LabelNodeSpec:  "4C16G",
					nodegroup.LabelPriority:  "100",
					nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager,
				}
				dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)
				dataplaneClient.On("ListNodePools", mock.Anything, &dplanemgrv1.ListNodePoolsRequest{DataPlaneId: "dp-1"}).Return(&dplanemgrv1.ListNodePoolsResponse{
					NodePools: []*dplanemgrv1.NodePool{
						{
							Name:         "primary-m5-xlarge",
							InstanceType: "m5.xlarge",
							NodeType:     dplanemgrv1.NodeType_Dedicated,
							Labels:       primaryLabels,
							Taints: []*dplanemgrv1.NodePoolTaint{
								{Key: nodegroup.TaintKeyNodeGroupState, Value: nodegroup.TaintValueDeprecated, Effect: nodegroup.TaintEffectNoSchedule},
								{Key: "other", Value: "kept", Effect: nodegroup.TaintEffectNoSchedule},
							},
						},
						{
							Name:         "fallback-m5-2xlarge",
							InstanceType: "m5.2xlarge",
							NodeType:     dplanemgrv1.NodeType_NodeType_Unspecified,
							Labels:       fallbackLabels,
							Taints: []*dplanemgrv1.NodePoolTaint{
								{Key: nodegroup.TaintKeyNodeGroupState, Value: nodegroup.TaintValueDeprecated, Effect: nodegroup.TaintEffectNoSchedule},
							},
						},
						{
							Name:         "fallback-other",
							InstanceType: "m5.4xlarge",
							NodeType:     dplanemgrv1.NodeType_Dedicated,
							Labels:       fallbackLabels,
							Taints: []*dplanemgrv1.NodePoolTaint{
								{Key: nodegroup.TaintKeyNodeGroupState, Value: nodegroup.TaintValueDeprecated, Effect: nodegroup.TaintEffectNoSchedule},
							},
						},
					},
				}, nil)
				dataplaneClient.On("UpdateNodePool", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.UpdateNodePoolRequest) bool {
					taints := req.NodePool.GetTaints()
					return req.DataPlaneId == "dp-1" &&
						req.NodePoolId == "primary-m5-xlarge" &&
						assert.Equal(t, primaryLabels, req.NodePool.GetLabels()) &&
						len(taints) == 1 &&
						taints[0].GetKey() == "other" &&
						taints[0].GetValue() == "kept"
				})).Return(&dplanemgrv1.NodePool{}, nil).Once()
				dataplaneClient.On("UpdateNodePool", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.UpdateNodePoolRequest) bool {
					return req.DataPlaneId == "dp-1" &&
						req.NodePoolId == "fallback-m5-2xlarge" &&
						assert.Equal(t, fallbackLabels, req.NodePool.GetLabels()) &&
						len(req.NodePool.GetTaints()) == 0
				})).Return(&dplanemgrv1.NodePool{}, nil).Once()

				state := &entity.DataplaneNodeInstanceGroupState{}
				state.ID = "state-1"
				state.State = entity.NodeInstanceGroupStateDeprecated
				stateRepo.On("LoadOrCreate", mock.Anything, "dp-1", "4C16G", "m5.xlarge").Return(state, nil).Once()
				stateRepo.On("TransitionState", mock.Anything, "state-1", entity.NodeInstanceGroupStateDeprecated, entity.NodeInstanceGroupStateNormal).Return(nil).Once()
			},
			wantCode: codes.OK,
		},
		{
			name: "list nodepools failure returns internal",
			setupMocks: func(stateRepo *repomnocks.DataplaneNodeInstanceGroupStateRepo, lockRepo *repomnocks.DataplaneNodeInstanceGroupLockRepo, dataplaneClient *dataplanemgrmocks.Client) {
				lock := &entity.DataplaneNodeInstanceGroupLock{}
				lock.FencingToken = 1
				lockRepo.On("TryAcquire", mock.Anything, "dp-1", "4C16G", "m5.xlarge", mock.AnythingOfType("string"), DefaultLockLeaseDuration).Return(lock, nil)
				lockRepo.On("Release", mock.Anything, "dp-1", "4C16G", "m5.xlarge", mock.AnythingOfType("string")).Return(nil)
				dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)
				dataplaneClient.On("ListNodePools", mock.Anything, &dplanemgrv1.ListNodePoolsRequest{DataPlaneId: "dp-1"}).
					Return(nil, status.Errorf(codes.Internal, "dpm error"))
			},
			wantCode:      codes.Internal,
			errorContains: "failed to list nodepools",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			stateRepo := repomnocks.NewDataplaneNodeInstanceGroupStateRepo(t)
			lockRepo := repomnocks.NewDataplaneNodeInstanceGroupLockRepo(t)
			dataplaneClient := dataplanemgrmocks.NewClient(t)
			tt.setupMocks(stateRepo, lockRepo, dataplaneClient)

			action := NewReactivateNodeInstanceGroupAction(stateRepo, lockRepo, dataplaneClient)
			err := action.Invoke(actions.ActionArgs{
				workflow.DataplaneIdKey: actions.ActionArgString("dp-1"),
				workflow.NodeInstanceGroupKey: actions.ActionArgString(mustMarshalNodeInstanceGroup(&entity.NodeInstanceGroup{
					NodeSpec:            &apiv1.NodeSpec{Cpu: 4, MemoryGi: 16},
					PrimaryInstanceType: "m5.xlarge",
					FallbackInstances: []*apiv1.NodeInstanceGroup_FallbackInstance{
						{InstanceType: "m5.2xlarge", NodeSpec: &apiv1.NodeSpec{Cpu: 8, MemoryGi: 32}, Priority: 100},
					},
				})),
			})
			if tt.wantCode == codes.OK {
				require.NoError(t, err)
				return
			}
			require.Error(t, err)
			assert.Equal(t, tt.wantCode, status.Code(err))
			if tt.errorContains != "" {
				assert.Contains(t, err.Error(), tt.errorContains)
			}
		})
	}
}
