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 TestDeprecateNodeInstanceGroupAction_Name(t *testing.T) {
	stateRepo := repomnocks.NewDataplaneNodeInstanceGroupStateRepo(t)
	lockRepo := repomnocks.NewDataplaneNodeInstanceGroupLockRepo(t)
	dataplaneClient := dataplanemgrmocks.NewClient(t)
	dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)

	action := NewDeprecateNodeInstanceGroupAction(stateRepo, lockRepo, dataplaneClient)

	expected := DeprecateNodeInstanceGroupActionName
	assert.Equal(t, expected, action.Name())
}

func TestDeprecateNodeInstanceGroupAction_Invoke(t *testing.T) {
	tests := []struct {
		name          string
		args          actions.ActionArgs
		setupMocks    func(*repomnocks.DataplaneNodeInstanceGroupStateRepo, *repomnocks.DataplaneNodeInstanceGroupLockRepo, *dataplanemgrmocks.Client)
		wantCode      codes.Code
		errorContains string
	}{
		{
			name: "success",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey: actions.ActionArgString("dataplane-abc"),
				workflow.NodeInstanceGroupKey: actions.ActionArgString(mustMarshalNodeInstanceGroup(&entity.NodeInstanceGroup{
					NodeSpec:            &apiv1.NodeSpec{Cpu: 4, MemoryGi: 16},
					PrimaryInstanceType: "m5.xlarge",
				})),
			},
			setupMocks: func(stateRepo *repomnocks.DataplaneNodeInstanceGroupStateRepo, lockRepo *repomnocks.DataplaneNodeInstanceGroupLockRepo, dataplaneClient *dataplanemgrmocks.Client) {
				lock := &entity.DataplaneNodeInstanceGroupLock{}
				lock.FencingToken = 1

				lockRepo.On("TryAcquire", mock.Anything, "dataplane-abc", "4C16G", "m5.xlarge", mock.AnythingOfType("string"), DefaultLockLeaseDuration).Return(lock, nil)
				lockRepo.On("Release", mock.Anything, "dataplane-abc", "4C16G", "m5.xlarge", mock.AnythingOfType("string")).Return(nil)

				dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)
				dataplaneClient.On("ListNodePools", mock.Anything, &dplanemgrv1.ListNodePoolsRequest{DataPlaneId: "dataplane-abc"}).Return(&dplanemgrv1.ListNodePoolsResponse{}, nil)

				state := &entity.DataplaneNodeInstanceGroupState{}
				state.State = entity.NodeInstanceGroupStateNormal
				state.ID = "state-123"
				stateRepo.On("LoadOrCreate", mock.Anything, "dataplane-abc", "4C16G", "m5.xlarge").Return(state, nil)
				stateRepo.On("TransitionState", mock.Anything, state.ID, entity.NodeInstanceGroupStateNormal, entity.NodeInstanceGroupStateDeprecated).Return(nil)
			},
			wantCode: codes.OK,
		},
		{
			name: "only taints nodepools matching primary instance type, not other groups",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey: actions.ActionArgString("dataplane-abc"),
				workflow.NodeInstanceGroupKey: actions.ActionArgString(mustMarshalNodeInstanceGroup(&entity.NodeInstanceGroup{
					NodeSpec:            &apiv1.NodeSpec{Cpu: 8, MemoryGi: 16},
					PrimaryInstanceType: "m5.2xlarge",
					FallbackInstances: []*apiv1.NodeInstanceGroup_FallbackInstance{
						{InstanceType: "m5.2xlarge", NodeSpec: &apiv1.NodeSpec{Cpu: 8, MemoryGi: 16}, Priority: 100},
					},
				})),
			},
			setupMocks: func(stateRepo *repomnocks.DataplaneNodeInstanceGroupStateRepo, lockRepo *repomnocks.DataplaneNodeInstanceGroupLockRepo, dataplaneClient *dataplanemgrmocks.Client) {
				lock := &entity.DataplaneNodeInstanceGroupLock{}
				lock.FencingToken = 1
				lockRepo.On("TryAcquire", mock.Anything, "dataplane-abc", "8C16G", "m5.2xlarge", mock.AnythingOfType("string"), DefaultLockLeaseDuration).Return(lock, nil)
				lockRepo.On("Release", mock.Anything, "dataplane-abc", "8C16G", "m5.2xlarge", mock.AnythingOfType("string")).Return(nil)

				dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)
				// Two nodepools share the same node_spec "8C16G" but belong to different groups.
				dataplaneClient.On("ListNodePools", mock.Anything, &dplanemgrv1.ListNodePoolsRequest{DataPlaneId: "dataplane-abc"}).Return(&dplanemgrv1.ListNodePoolsResponse{
					NodePools: []*dplanemgrv1.NodePool{
						{
							Name:         "np-m5-2xlarge",
							InstanceType: "m5.2xlarge",
							Labels:       map[string]string{nodegroup.LabelNodeSpec: "8C16G", nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager},
						},
						{
							Name:         "np-r5-2xlarge",
							InstanceType: "r5.2xlarge", // belongs to a different group
							Labels:       map[string]string{nodegroup.LabelNodeSpec: "8C16G", nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager},
						},
					},
				}, nil)
				// Only np-m5-2xlarge should be tainted (matches primary instance type).
				deprecatedTaint := nodegroup.DeprecatedTaint()
				dataplaneClient.On("UpdateNodePool", mock.Anything, &dplanemgrv1.UpdateNodePoolRequest{
					DataPlaneId: "dataplane-abc",
					NodePoolId:  "np-m5-2xlarge",
					NodePool: &dplanemgrv1.NodePool{
						Labels: map[string]string{nodegroup.LabelNodeSpec: "8C16G", nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager},
						Taints: []*dplanemgrv1.NodePoolTaint{
							{Key: deprecatedTaint.Key, Value: deprecatedTaint.Value, Effect: deprecatedTaint.Effect},
						},
					},
				}).Return(&dplanemgrv1.NodePool{}, nil)

				state := &entity.DataplaneNodeInstanceGroupState{}
				state.State = entity.NodeInstanceGroupStateNormal
				state.ID = "state-456"
				stateRepo.On("LoadOrCreate", mock.Anything, "dataplane-abc", "8C16G", "m5.2xlarge").Return(state, nil)
				stateRepo.On("TransitionState", mock.Anything, state.ID, entity.NodeInstanceGroupStateNormal, entity.NodeInstanceGroupStateDeprecated).Return(nil)
			},
			wantCode: codes.OK,
		},
		{
			name: "skips fallback nodepool with same instance type as primary group",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey: actions.ActionArgString("dataplane-abc"),
				workflow.NodeInstanceGroupKey: actions.ActionArgString(mustMarshalNodeInstanceGroup(&entity.NodeInstanceGroup{
					NodeSpec:            &apiv1.NodeSpec{Cpu: 8, MemoryGi: 16},
					PrimaryInstanceType: "m5.2xlarge",
				})),
			},
			setupMocks: func(stateRepo *repomnocks.DataplaneNodeInstanceGroupStateRepo, lockRepo *repomnocks.DataplaneNodeInstanceGroupLockRepo, dataplaneClient *dataplanemgrmocks.Client) {
				lock := &entity.DataplaneNodeInstanceGroupLock{}
				lock.FencingToken = 1
				lockRepo.On("TryAcquire", mock.Anything, "dataplane-abc", "8C16G", "m5.2xlarge", mock.AnythingOfType("string"), DefaultLockLeaseDuration).Return(lock, nil)
				lockRepo.On("Release", mock.Anything, "dataplane-abc", "8C16G", "m5.2xlarge", mock.AnythingOfType("string")).Return(nil)

				dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)
				dataplaneClient.On("ListNodePools", mock.Anything, &dplanemgrv1.ListNodePoolsRequest{DataPlaneId: "dataplane-abc"}).Return(&dplanemgrv1.ListNodePoolsResponse{
					NodePools: []*dplanemgrv1.NodePool{
						{
							Name:         "primary-m5-2xlarge",
							InstanceType: "m5.2xlarge",
							Labels: map[string]string{
								nodegroup.LabelNodeSpec:  "8C16G",
								nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager,
								nodegroup.LabelPriority:  "1000",
							},
						},
						{
							Name:         "fallback-other-primary-m5-2xlarge",
							InstanceType: "m5.2xlarge",
							Labels: map[string]string{
								nodegroup.LabelNodeSpec:  "8C16G",
								nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager,
								nodegroup.LabelPriority:  "100",
							},
						},
					},
				}, nil)
				deprecatedTaint := nodegroup.DeprecatedTaint()
				dataplaneClient.On("UpdateNodePool", mock.Anything, &dplanemgrv1.UpdateNodePoolRequest{
					DataPlaneId: "dataplane-abc",
					NodePoolId:  "primary-m5-2xlarge",
					NodePool: &dplanemgrv1.NodePool{
						Labels: map[string]string{
							nodegroup.LabelNodeSpec:  "8C16G",
							nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager,
							nodegroup.LabelPriority:  "1000",
						},
						Taints: []*dplanemgrv1.NodePoolTaint{
							{Key: deprecatedTaint.Key, Value: deprecatedTaint.Value, Effect: deprecatedTaint.Effect},
						},
					},
				}).Return(&dplanemgrv1.NodePool{}, nil)

				state := &entity.DataplaneNodeInstanceGroupState{}
				state.State = entity.NodeInstanceGroupStateNormal
				state.ID = "state-789"
				stateRepo.On("LoadOrCreate", mock.Anything, "dataplane-abc", "8C16G", "m5.2xlarge").Return(state, nil)
				stateRepo.On("TransitionState", mock.Anything, state.ID, entity.NodeInstanceGroupStateNormal, entity.NodeInstanceGroupStateDeprecated).Return(nil)
			},
			wantCode: codes.OK,
		},
		{
			name: "empty primary instance preserves spec only deprecation",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey: actions.ActionArgString("dataplane-abc"),
				workflow.NodeInstanceGroupKey: actions.ActionArgString(mustMarshalNodeInstanceGroup(&entity.NodeInstanceGroup{
					NodeSpec: &apiv1.NodeSpec{Cpu: 8, MemoryGi: 16},
				})),
			},
			setupMocks: func(stateRepo *repomnocks.DataplaneNodeInstanceGroupStateRepo, lockRepo *repomnocks.DataplaneNodeInstanceGroupLockRepo, dataplaneClient *dataplanemgrmocks.Client) {
				lock := &entity.DataplaneNodeInstanceGroupLock{}
				lock.FencingToken = 1
				lockRepo.On("TryAcquire", mock.Anything, "dataplane-abc", "8C16G", "", mock.AnythingOfType("string"), DefaultLockLeaseDuration).Return(lock, nil)
				lockRepo.On("Release", mock.Anything, "dataplane-abc", "8C16G", "", mock.AnythingOfType("string")).Return(nil)

				dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)
				dataplaneClient.On("ListNodePools", mock.Anything, &dplanemgrv1.ListNodePoolsRequest{DataPlaneId: "dataplane-abc"}).Return(&dplanemgrv1.ListNodePoolsResponse{
					NodePools: []*dplanemgrv1.NodePool{
						{
							Name:         "legacy-m5-2xlarge",
							InstanceType: "m5.2xlarge",
							Labels: map[string]string{
								nodegroup.LabelNodeSpec:  "8C16G",
								nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager,
							},
						},
						{
							Name:         "other-spec",
							InstanceType: "m5.4xlarge",
							Labels: map[string]string{
								nodegroup.LabelNodeSpec:  "16C32G",
								nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager,
							},
						},
					},
				}, nil)
				deprecatedTaint := nodegroup.DeprecatedTaint()
				dataplaneClient.On("UpdateNodePool", mock.Anything, &dplanemgrv1.UpdateNodePoolRequest{
					DataPlaneId: "dataplane-abc",
					NodePoolId:  "legacy-m5-2xlarge",
					NodePool: &dplanemgrv1.NodePool{
						Labels: map[string]string{
							nodegroup.LabelNodeSpec:  "8C16G",
							nodegroup.LabelManagedBy: nodegroup.ManagedByInstanceTypeManager,
						},
						Taints: []*dplanemgrv1.NodePoolTaint{
							{Key: deprecatedTaint.Key, Value: deprecatedTaint.Value, Effect: deprecatedTaint.Effect},
						},
					},
				}).Return(&dplanemgrv1.NodePool{}, nil)

				state := &entity.DataplaneNodeInstanceGroupState{}
				state.State = entity.NodeInstanceGroupStateNormal
				state.ID = "state-legacy"
				stateRepo.On("LoadOrCreate", mock.Anything, "dataplane-abc", "8C16G", "").Return(state, nil)
				stateRepo.On("TransitionState", mock.Anything, state.ID, entity.NodeInstanceGroupStateNormal, entity.NodeInstanceGroupStateDeprecated).Return(nil)
			},
			wantCode: codes.OK,
		},
		{
			name: "lock held by another owner",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey: actions.ActionArgString("dataplane-abc"),
				workflow.NodeInstanceGroupKey: actions.ActionArgString(mustMarshalNodeInstanceGroup(&entity.NodeInstanceGroup{
					// Use a valid NodeSpec that produces "0C0G" which is technically valid format
					// The action should handle this and try to acquire lock, but we mock failure
					NodeSpec:            &apiv1.NodeSpec{Cpu: 0, MemoryGi: 0},
					PrimaryInstanceType: "m5.xlarge",
				})),
			},
			setupMocks: func(stateRepo *repomnocks.DataplaneNodeInstanceGroupStateRepo, lockRepo *repomnocks.DataplaneNodeInstanceGroupLockRepo, dataplaneClient *dataplanemgrmocks.Client) {
				dataplaneClient.On("GetDataPlaneServiceClient").Return(dataplaneClient)
				// The spec "0C0G" is technically valid format, so lock acquisition is attempted
				lockRepo.On("TryAcquire", mock.Anything, "dataplane-abc", "0C0G", "m5.xlarge", mock.AnythingOfType("string"), DefaultLockLeaseDuration).Return(nil, nil)
			},
			wantCode:      codes.Aborted,
			errorContains: "lock is held by another owner",
		},
	}

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

			if tt.setupMocks != nil {
				tt.setupMocks(stateRepo, lockRepo, dataplaneClient)
			}

			action := NewDeprecateNodeInstanceGroupAction(stateRepo, lockRepo, dataplaneClient)
			err := action.Invoke(tt.args)
			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)
			}
		})
	}
}
