package server

import (
	"context"
	"errors"
	"testing"

	"github.com/samber/lo"
	"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"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	kubemocks "github.com/tidbcloud/cluster-service-ng/pkg/externalmocks/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	repoext_mocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repoext/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	autoscalingv2 "k8s.io/api/autoscaling/v2"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestResolveClusterKeyspace(t *testing.T) {
	t.Run("returns fetched keyspace when available", func(t *testing.T) {
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		keyspaceFetcher := repoext_mocks.NewKeyspaceFetcher(t)
		s := &serverImpl{sharedPoolRepo: sharedPoolRepo, keyspaceFetcher: keyspaceFetcher}

		keyspaceFetcher.On("GetKeyspace", mock.Anything, "cluster-1", "sp-1").Return("ks-1", nil)

		keyspace, err := s.resolveClusterKeyspace(context.Background(), "cluster-1", "sp-1")
		require.NoError(t, err)
		assert.Equal(t, "ks-1", keyspace)
	})

	t.Run("returns fetched keyspace when topology driven", func(t *testing.T) {
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		keyspaceFetcher := repoext_mocks.NewKeyspaceFetcher(t)
		s := &serverImpl{sharedPoolRepo: sharedPoolRepo, keyspaceFetcher: keyspaceFetcher}

		keyspaceFetcher.On("GetKeyspace", mock.Anything, "cluster-1", "sp-1").Return("ks-topology", nil)

		keyspace, err := s.resolveClusterKeyspace(context.Background(), "cluster-1", "sp-1")
		require.NoError(t, err)
		assert.Equal(t, "ks-topology", keyspace)
	})

	t.Run("returns fetched keyspace when topology driven with tidb worker", func(t *testing.T) {
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		keyspaceFetcher := repoext_mocks.NewKeyspaceFetcher(t)
		s := &serverImpl{sharedPoolRepo: sharedPoolRepo, keyspaceFetcher: keyspaceFetcher}

		keyspaceFetcher.On("GetKeyspace", mock.Anything, "cluster-1", "sp-1").Return("ks-topology-worker", nil)

		keyspace, err := s.resolveClusterKeyspace(context.Background(), "cluster-1", "sp-1")
		require.NoError(t, err)
		assert.Equal(t, "ks-topology-worker", keyspace)
	})

	t.Run("returns original keyspace fetch error when non-topology", func(t *testing.T) {
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		keyspaceFetcher := repoext_mocks.NewKeyspaceFetcher(t)
		s := &serverImpl{sharedPoolRepo: sharedPoolRepo, keyspaceFetcher: keyspaceFetcher}

		fetchErr := errors.New("not found")
		keyspaceFetcher.On("GetKeyspace", mock.Anything, "cluster-1", "sp-1").Return("", fetchErr)

		keyspace, err := s.resolveClusterKeyspace(context.Background(), "cluster-1", "sp-1")
		require.Error(t, err)
		assert.Empty(t, keyspace)
		require.ErrorIs(t, err, fetchErr)
	})
}

// Mock executor for testing
type MockExecutor struct {
	executor.RemoteExecutor
	mockTasks map[string]executor.ExecTaskIdStatus
}

func (m *MockExecutor) QueryRunnings(ctx context.Context, planName string, matchFilter executor.MatchFilter) (executor.ExecTaskIdStatus, error) {
	if m.mockTasks == nil {
		return executor.NewExecTaskIdStatus(), nil
	}

	result := executor.NewExecTaskIdStatus()
	if tasks, ok := m.mockTasks[planName]; ok {
		matchFunc := matchFilter.ToMatchFunc()
		for execId, task := range tasks {
			// Apply the match function to filter tasks
			if matchFunc != nil {
				match, err := matchFunc(task.ExecTask.Args)
				if err != nil {
					return nil, err
				}
				if !match {
					continue
				}
			}
			result[execId] = task
		}
	}
	return result, nil
}

func TestGetRunningDagIns(t *testing.T) {
	server := &serverImpl{
		dagExecutor: &MockExecutor{},
	}

	tests := []struct {
		name           string
		planNames      []string
		sharedPoolIds  []string
		tenantIds      []string
		tidbClusterIds []string
		tidbGroupIds   []string
		expectError    bool
	}{
		{
			name:           "empty plan names",
			planNames:      []string{},
			sharedPoolIds:  []string{},
			tenantIds:      []string{},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			expectError:    false,
		},
		{
			name:           "single plan name",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{},
			tenantIds:      []string{},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			expectError:    false,
		},
		{
			name:           "multiple plan names",
			planNames:      []string{"plan1", "plan2", "plan3"},
			sharedPoolIds:  []string{},
			tenantIds:      []string{},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			expectError:    false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			result, err := server.getRunningDagIns(ctx, tt.planNames, tt.sharedPoolIds, tt.tenantIds, tt.tidbClusterIds, tt.tidbGroupIds)

			if tt.expectError {
				if err == nil {
					t.Error("getRunningDagIns() expected error but got none")
				}
				return
			}

			if err != nil {
				t.Errorf("getRunningDagIns() unexpected error: %v", err)
				return
			}

			if result == nil {
				t.Error("getRunningDagIns() returned nil result")
			}
		})
	}
}

func TestCheckComponent(t *testing.T) {
	server := &serverImpl{}

	tests := []struct {
		name        string
		component   *entity.Component
		expectError bool
		errorCode   codes.Code
	}{
		{
			name: "valid component",
			component: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "v8.0.0",
				Configuration: &entity.ComponentConfiguration{
					ImageRepo: "tidb/tidb",
					ImageTag:  "v8.0.0",
				},
				NodeQuota: &apiv1.NodeQuota{
					NodeCount: 1,
					NodeSpec: &apiv1.NodeSpec{
						Cpu:      2,
						MemoryGi: 1,
					},
				},
			},
			expectError: false,
		},
		{
			name: "TiFlash learner (should be skipped)",
			component: &entity.Component{
				Id:            "tiflash-learner-1",
				ComponentType: apiv1.ComponentType_TIFLASH_LEARNER,
				Version:       "v8.0.0",
				Configuration: &entity.ComponentConfiguration{
					ImageRepo: "tidb/tiflash",
					ImageTag:  "v8.0.0",
				},
			},
			expectError: false, // Should be skipped, not error
		},
		{
			name: "missing image repo",
			component: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "v8.0.0",
				Configuration: &entity.ComponentConfiguration{
					ImageTag: "v8.0.0",
				},
			},
			expectError: true,
			errorCode:   codes.InvalidArgument,
		},
		{
			name: "missing image tag",
			component: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "v8.0.0",
				Configuration: &entity.ComponentConfiguration{
					ImageRepo: "tidb/tidb",
				},
			},
			expectError: true,
			errorCode:   codes.InvalidArgument,
		},
		{
			name: "unspecified component type",
			component: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED,
				Version:       "v8.0.0",
				Configuration: &entity.ComponentConfiguration{
					ImageRepo: "tidb/tidb",
					ImageTag:  "v8.0.0",
				},
			},
			expectError: true,
			errorCode:   codes.InvalidArgument,
		},
		{
			name: "empty component id",
			component: &entity.Component{
				Id:            "",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "v8.0.0",
				Configuration: &entity.ComponentConfiguration{
					ImageRepo: "tidb/tidb",
					ImageTag:  "v8.0.0",
				},
			},
			expectError: true,
			errorCode:   codes.InvalidArgument,
		},
		{
			name: "missing version",
			component: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "",
				Configuration: &entity.ComponentConfiguration{
					ImageRepo: "tidb/tidb",
					ImageTag:  "v8.0.0",
				},
			},
			expectError: true,
			errorCode:   codes.InvalidArgument,
		},
		{
			name:        "nil component",
			component:   nil,
			expectError: true,
			errorCode:   codes.InvalidArgument,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := server.checkComponent(tt.component)

			if tt.expectError {
				if err == nil {
					t.Error("checkComponent() expected error but got none")
					return
				}

				if tt.errorCode != codes.OK {
					st, ok := status.FromError(err)
					if !ok {
						t.Errorf("checkComponent() error is not a gRPC status: %v", err)
						return
					}
					if st.Code() != tt.errorCode {
						t.Errorf("checkComponent() error code = %v, want %v", st.Code(), tt.errorCode)
					}
				}
				return
			}

			if err != nil {
				t.Errorf("checkComponent() unexpected error: %v", err)
			}
		})
	}
}

func TestComponentValidationEdgeCases(t *testing.T) {
	server := &serverImpl{}

	tests := []struct {
		name        string
		component   *entity.Component
		expectError bool
		description string
	}{
		{
			name: "component with empty configuration",
			component: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "v8.0.0",
				Configuration: &entity.ComponentConfiguration{},
			},
			expectError: true,
			description: "Component with empty configuration should fail validation",
		},
		{
			name: "component with nil configuration",
			component: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "v8.0.0",
				Configuration: nil,
			},
			expectError: true,
			description: "Component with nil configuration should fail validation",
		},
		{
			name: "component with whitespace-only id",
			component: &entity.Component{
				Id:            "   ",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "v8.0.0",
				Configuration: &entity.ComponentConfiguration{
					ImageRepo: "tidb/tidb",
					ImageTag:  "v8.0.0",
				},
			},
			expectError: true,
			description: "Component with whitespace-only id should fail validation",
		},
		{
			name: "component with whitespace-only version",
			component: &entity.Component{
				Id:            "tidb-1",
				ComponentType: apiv1.ComponentType_TIDB,
				Version:       "   ",
				Configuration: &entity.ComponentConfiguration{
					ImageRepo: "tidb/tidb",
					ImageTag:  "v8.0.0",
				},
			},
			expectError: true,
			description: "Component with whitespace-only version should fail validation",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := server.checkComponent(tt.component)

			if tt.expectError {
				if err == nil {
					t.Errorf("checkComponent() expected error but got none: %s", tt.description)
				}
				return
			}

			if err != nil {
				t.Errorf("checkComponent() unexpected error: %v - %s", err, tt.description)
			}
		})
	}
}

func TestGetRunningDagInsWithFilters(t *testing.T) {
	tests := []struct {
		name            string
		planNames       []string
		sharedPoolIds   []string
		tenantIds       []string
		tidbClusterIds  []string
		tidbGroupIds    []string
		mockTasks       map[string]executor.ExecTaskIdStatus
		expectedTaskIds []string
		expectError     bool
		description     string
	}{
		{
			name:           "filter by shared pool ids - matching task",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{"pool-123"},
			tenantIds:      []string{},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			mockTasks: map[string]executor.ExecTaskIdStatus{
				"test-plan": {
					"task-1": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.SharedPoolIdKey: actions.ActionArgString("pool-123"),
								},
							},
						},
					},
				},
			},
			expectedTaskIds: []string{"task-1"},
			expectError:     false,
			description:     "Should return task with matching shared pool ID",
		},
		{
			name:           "filter by shared pool ids - non-matching task",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{"pool-123"},
			tenantIds:      []string{},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			mockTasks: map[string]executor.ExecTaskIdStatus{
				"test-plan": {
					"task-1": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.SharedPoolIdKey: actions.ActionArgString("pool-456"),
								},
							},
						},
					},
				},
			},
			expectedTaskIds: []string{},
			expectError:     false,
			description:     "Should not return task with non-matching shared pool ID",
		},
		{
			name:           "filter by shared pool ids - task missing key",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{"pool-123"},
			tenantIds:      []string{},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			mockTasks: map[string]executor.ExecTaskIdStatus{
				"test-plan": {
					"task-1": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.TenantIdKey: actions.ActionArgString("tenant-123"),
								},
							},
						},
					},
				},
			},
			expectedTaskIds: []string{},
			expectError:     false,
			description:     "Should filter out task missing required key (independent filters)",
		},
		{
			name:           "no filter - all tasks returned",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{},
			tenantIds:      []string{},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			mockTasks: map[string]executor.ExecTaskIdStatus{
				"test-plan": {
					"task-1": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.SharedPoolIdKey: actions.ActionArgString("pool-123"),
								},
							},
						},
					},
					"task-2": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.TenantIdKey: actions.ActionArgString("tenant-456"),
								},
							},
						},
					},
				},
			},
			expectedTaskIds: []string{"task-1", "task-2"},
			expectError:     false,
			description:     "Should return all tasks when no filters specified",
		},
		{
			name:           "multiple filters - all match (AND logic)",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{"pool-123"},
			tenantIds:      []string{"tenant-123"},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			mockTasks: map[string]executor.ExecTaskIdStatus{
				"test-plan": {
					"task-1": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.SharedPoolIdKey: actions.ActionArgString("pool-123"),
									workflow.TenantIdKey:     actions.ActionArgString("tenant-123"),
								},
							},
						},
					},
				},
			},
			expectedTaskIds: []string{"task-1"},
			expectError:     false,
			description:     "Should return task when all filters match",
		},
		{
			name:           "multiple filters - partial match fails",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{"pool-123"},
			tenantIds:      []string{"tenant-123"},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{},
			mockTasks: map[string]executor.ExecTaskIdStatus{
				"test-plan": {
					"task-1": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.SharedPoolIdKey: actions.ActionArgString("pool-123"),
									workflow.TenantIdKey:     actions.ActionArgString("tenant-456"),
								},
							},
						},
					},
				},
			},
			expectedTaskIds: []string{},
			expectError:     false,
			description:     "Should filter out when only some filters match",
		},
		{
			name:           "independent filters - task has only tidb cluster id",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{},
			tenantIds:      []string{},
			tidbClusterIds: []string{"cluster-123"},
			tidbGroupIds:   []string{},
			mockTasks: map[string]executor.ExecTaskIdStatus{
				"test-plan": {
					"task-1": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.TiDBClusterIdKey: actions.ActionArgString("cluster-123"),
								},
							},
						},
					},
				},
			},
			expectedTaskIds: []string{"task-1"},
			expectError:     false,
			description:     "Filters are independent - should work even without SharedPoolIdKey",
		},
		{
			name:           "filter by tidb group ids - comma-separated match",
			planNames:      []string{"test-plan"},
			sharedPoolIds:  []string{},
			tenantIds:      []string{},
			tidbClusterIds: []string{},
			tidbGroupIds:   []string{"group-123"},
			mockTasks: map[string]executor.ExecTaskIdStatus{
				"test-plan": {
					"task-1": &executor.ExecTaskStatusEx{
						ExecTaskStatus: executor.ExecTaskStatus{
							ExecTask: &executor.ExecutorTask{
								Args: actions.ActionArgs{
									workflow.ComponentIdsKey: actions.ActionArgString("group-123,group-456"),
								},
							},
						},
					},
				},
			},
			expectedTaskIds: []string{"task-1"},
			expectError:     false,
			description:     "Should match when group ID is in comma-separated list",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			mockExecutor := &MockExecutor{
				mockTasks: tt.mockTasks,
			}
			server := &serverImpl{
				dagExecutor: mockExecutor,
			}

			result, err := server.getRunningDagIns(ctx, tt.planNames, tt.sharedPoolIds, tt.tenantIds, tt.tidbClusterIds, tt.tidbGroupIds)

			if tt.expectError {
				if err == nil {
					t.Errorf("getRunningDagIns() expected error but got none: %s", tt.description)
				}
				return
			}

			if err != nil {
				t.Errorf("getRunningDagIns() unexpected error: %v - %s", err, tt.description)
				return
			}

			if result == nil {
				t.Errorf("getRunningDagIns() returned nil result: %s", tt.description)
				return
			}

			// Check if the returned task IDs match expected
			if len(result) != len(tt.expectedTaskIds) {
				t.Errorf("getRunningDagIns() returned %d tasks, expected %d: %s\nGot: %v",
					len(result), len(tt.expectedTaskIds), tt.description, result)
				return
			}

			for _, expectedId := range tt.expectedTaskIds {
				if _, found := result[expectedId]; !found {
					t.Errorf("getRunningDagIns() missing expected task ID '%s': %s", expectedId, tt.description)
				}
			}
		})
	}
}

func TestComponentLabelForHPA(t *testing.T) {
	tests := []struct {
		name           string
		componentType  apiv1.ComponentType
		expectedLabel  string
		expectedHasHPA bool
	}{
		{
			name:           "TIDB supports HPA",
			componentType:  apiv1.ComponentType_TIDB,
			expectedLabel:  "tidb",
			expectedHasHPA: true,
		},
		{
			name:           "TIDB_WORKER supports HPA",
			componentType:  apiv1.ComponentType_TIDB_WORKER,
			expectedLabel:  "worker-tidb",
			expectedHasHPA: true,
		},
		{
			name:           "TIKV_WORKER supports HPA",
			componentType:  apiv1.ComponentType_TIKV_WORKER,
			expectedLabel:  entity.TiKVWorkerType,
			expectedHasHPA: true,
		},
		{
			name:           "COPROCESSOR_WORKER supports HPA",
			componentType:  apiv1.ComponentType_COPROCESSOR_WORKER,
			expectedLabel:  entity.CoprocessorWorkerType,
			expectedHasHPA: true,
		},
		{
			name:           "TIKV does not support HPA",
			componentType:  apiv1.ComponentType_TIKV,
			expectedLabel:  "",
			expectedHasHPA: false,
		},
		{
			name:           "PD does not support HPA",
			componentType:  apiv1.ComponentType_PD,
			expectedLabel:  "",
			expectedHasHPA: false,
		},
		{
			name:           "TIFLASH_WRITE does not support HPA",
			componentType:  apiv1.ComponentType_TIFLASH_WRITE,
			expectedLabel:  "",
			expectedHasHPA: false,
		},
		{
			name:           "TIFLASH_COMPUTE does not support HPA",
			componentType:  apiv1.ComponentType_TIFLASH_COMPUTE,
			expectedLabel:  "",
			expectedHasHPA: false,
		},
		{
			name:           "PD_TSO does not support HPA",
			componentType:  apiv1.ComponentType_PD_TSO,
			expectedLabel:  "",
			expectedHasHPA: false,
		},
		{
			name:           "TICDC does not support HPA",
			componentType:  apiv1.ComponentType_TICDC,
			expectedLabel:  "",
			expectedHasHPA: false,
		},
		{
			name:           "TIPROXY supports HPA",
			componentType:  apiv1.ComponentType_TIPROXY,
			expectedLabel:  "tiproxy",
			expectedHasHPA: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			label := componentLabelForHPA(tt.componentType)
			hasHPA := lo.Contains(entity.EnableHAPComponent, tt.componentType)
			assert.Equal(t, tt.expectedLabel, label, "label should match")
			assert.Equal(t, tt.expectedHasHPA, hasHPA, "hasHPA should match")
		})
	}
}

func TestCheckHPAScaleDownDisabled(t *testing.T) {
	tests := []struct {
		name                  string
		componentType         apiv1.ComponentType
		setupDataplaneManager func(*dataplanemocks.Manager, *kubemocks.Client)
		setupHPA              func(*kubemocks.Client)
		expectedDisabled      bool
		expectedError         bool
	}{
		{
			name:             "TiKV - no HPA support - returns true",
			componentType:    apiv1.ComponentType_TIKV,
			setupHPA:         nil,
			expectedDisabled: true,
			expectedError:    false,
		},
		{
			name:             "PD - no HPA support - returns true",
			componentType:    apiv1.ComponentType_PD,
			setupHPA:         nil,
			expectedDisabled: true,
			expectedError:    false,
		},
		{
			name:          "TIDB - HPA not found - returns true",
			componentType: apiv1.ComponentType_TIDB,
			setupHPA: func(kubeclient *kubemocks.Client) {
				kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
					Run(func(args mock.Arguments) {
						hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
						hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{}
					}).
					Return(nil)
			},
			expectedDisabled: true,
			expectedError:    false,
		},
		{
			name:          "TIDB - HPA scaleDown disabled - returns true",
			componentType: apiv1.ComponentType_TIDB,
			setupHPA: func(kubeclient *kubemocks.Client) {
				kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
					Run(func(args mock.Arguments) {
						hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
						selectPolicy := autoscalingv2.DisabledPolicySelect
						hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{
							{
								Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
									Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
										ScaleDown: &autoscalingv2.HPAScalingRules{
											SelectPolicy: &selectPolicy,
										},
									},
								},
							},
						}
					}).
					Return(nil)
			},
			expectedDisabled: true,
			expectedError:    false,
		},
		{
			name:          "TIDB - HPA scaleDown enabled - returns false",
			componentType: apiv1.ComponentType_TIDB,
			setupHPA: func(kubeclient *kubemocks.Client) {
				kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
					Run(func(args mock.Arguments) {
						hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
						selectPolicy := autoscalingv2.MaxChangePolicySelect
						hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{
							{
								Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
									Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
										ScaleDown: &autoscalingv2.HPAScalingRules{
											SelectPolicy: &selectPolicy,
										},
									},
								},
							},
						}
					}).
					Return(nil)
			},
			expectedDisabled: false,
			expectedError:    false,
		},
		{
			name:          "TIDB_WORKER - HPA scaleDown enabled - returns false",
			componentType: apiv1.ComponentType_TIDB_WORKER,
			setupHPA: func(kubeclient *kubemocks.Client) {
				kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
					Run(func(args mock.Arguments) {
						hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
						selectPolicy := autoscalingv2.MaxChangePolicySelect
						hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{
							{
								Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
									Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
										ScaleDown: &autoscalingv2.HPAScalingRules{
											SelectPolicy: &selectPolicy,
										},
									},
								},
							},
						}
					}).
					Return(nil)
			},
			expectedDisabled: false,
			expectedError:    false,
		},
		{
			name:          "TIDB - HPA behavior nil - returns false",
			componentType: apiv1.ComponentType_TIDB,
			setupHPA: func(kubeclient *kubemocks.Client) {
				kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
					Run(func(args mock.Arguments) {
						hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
						hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{
							{
								Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
									Behavior: nil,
								},
							},
						}
					}).
					Return(nil)
			},
			expectedDisabled: false,
			expectedError:    false,
		},
		{
			name:          "TIDB - HPA scaleDown nil - returns false",
			componentType: apiv1.ComponentType_TIDB,
			setupHPA: func(kubeclient *kubemocks.Client) {
				kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
					Run(func(args mock.Arguments) {
						hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
						hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{
							{
								Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
									Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
										ScaleDown: nil,
									},
								},
							},
						}
					}).
					Return(nil)
			},
			expectedDisabled: false,
			expectedError:    false,
		},
		{
			name:          "TIDB - HPA selectPolicy nil - returns false",
			componentType: apiv1.ComponentType_TIDB,
			setupHPA: func(kubeclient *kubemocks.Client) {
				kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
					Run(func(args mock.Arguments) {
						hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
						hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{
							{
								Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
									Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
										ScaleDown: &autoscalingv2.HPAScalingRules{
											SelectPolicy: nil,
										},
									},
								},
							},
						}
					}).
					Return(nil)
			},
			expectedDisabled: false,
			expectedError:    false,
		},
		{
			name:          "TIDB - GetClient error - returns error",
			componentType: apiv1.ComponentType_TIDB,
			setupDataplaneManager: func(dm *dataplanemocks.Manager, _ *kubemocks.Client) {
				dm.On("GetClient", mock.AnythingOfType("string")).Return(nil, errors.New("connection failed"))
			},
			setupHPA:         nil,
			expectedDisabled: false,
			expectedError:    true,
		},
		{
			name:          "TIDB - List error - returns error",
			componentType: apiv1.ComponentType_TIDB,
			setupHPA: func(kubeclient *kubemocks.Client) {
				kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
					Return(errors.New("list failed"))
			},
			expectedDisabled: false,
			expectedError:    true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			kubeclient := kubemocks.NewClient(t)
			dataplaneManager := dataplanemocks.NewManager(t)

			if tt.setupDataplaneManager != nil {
				tt.setupDataplaneManager(dataplaneManager, kubeclient)
			} else {
				dataplaneManager.On("GetClient", mock.AnythingOfType("string")).Maybe().Return(kubeclient, nil)
			}

			if tt.setupHPA != nil {
				tt.setupHPA(kubeclient)
			}

			server := &serverImpl{
				dataplaneManager: dataplaneManager,
			}

			sharedPool := &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					Id:            "test-sharedpool-id",
					DataplaneName: "test-dataplane",
				},
			}

			disabled, err := server.checkHPAScaleDownDisabled(
				context.Background(),
				sharedPool,
				tt.componentType,
				"test-group-id",
			)

			if tt.expectedError {
				require.Error(t, err, "expected error but got none")
			} else {
				require.NoError(t, err, "unexpected error: %v", err)
				assert.Equal(t, tt.expectedDisabled, disabled, "disabled flag should match")
			}

			dataplaneManager.AssertExpectations(t)
			kubeclient.AssertExpectations(t)
		})
	}
}

func TestGetHPAForComponentUsesGroupLabelForTiProxy(t *testing.T) {
	kubeclient := kubemocks.NewClient(t)
	groupID := "cluster-a"
	expectedSelector := map[string]string{
		schemav1.LabelComponentKey: "tiproxy",
		schemav1.LabelPingcapGroupKey: entity.GetComponentGroupName(&entity.Component{
			Id:            groupID,
			ComponentType: apiv1.ComponentType_TIPROXY,
		}),
	}

	kubeclient.On("List",
		mock.Anything,
		mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}),
		mock.MatchedBy(func(opts []client.ListOption) bool {
			listOpts := &client.ListOptions{}
			for _, opt := range opts {
				opt.ApplyToList(listOpts)
			}
			if listOpts.Namespace != "tidbtest-sharedpool-id" || listOpts.LabelSelector == nil {
				return false
			}
			reqs, selectable := listOpts.LabelSelector.Requirements()
			if !selectable || len(reqs) != len(expectedSelector) {
				return false
			}
			for key, value := range expectedSelector {
				actualValue, found := listOpts.LabelSelector.RequiresExactMatch(key)
				if !found || actualValue != value {
					return false
				}
			}
			return true
		}),
	).Run(func(args mock.Arguments) {
		hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
		hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{{}}
	}).Return(nil).Once()

	server := &serverImpl{}
	hpa, err := server.getHPAForComponent(
		context.Background(),
		kubeclient,
		"tidbtest-sharedpool-id",
		apiv1.ComponentType_TIPROXY,
		groupID,
	)
	require.NoError(t, err)
	require.NotNil(t, hpa)
	kubeclient.AssertExpectations(t)
}

func TestGetHPAForComponentRequiresGroupIDForTiProxy(t *testing.T) {
	server := &serverImpl{}
	hpa, err := server.getHPAForComponent(
		context.Background(),
		kubemocks.NewClient(t),
		"tidbtest-sharedpool-id",
		apiv1.ComponentType_TIPROXY,
		"",
	)
	require.Nil(t, hpa)
	require.Error(t, err)
	require.Equal(t, codes.InvalidArgument, status.Code(err))
}

func TestCheckManualScaleHPAGate(t *testing.T) {
	t.Run("force skips HPA gate for scale out", func(t *testing.T) {
		dataplaneManager := dataplanemocks.NewManager(t)
		server := &serverImpl{
			dataplaneManager: dataplaneManager,
		}
		sharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id:            "test-sharedpool-id",
				DataplaneName: "test-dataplane",
			},
		}

		err := server.checkManualScaleHPAGate(
			context.Background(),
			sharedPool,
			apiv1.ComponentType_TIDB,
			"test-group-id",
			2,
			3,
			true,
		)

		require.NoError(t, err)
		dataplaneManager.AssertNotCalled(t, "GetClient", mock.Anything)
	})

	t.Run("scale in skips HPA gate", func(t *testing.T) {
		dataplaneManager := dataplanemocks.NewManager(t)
		server := &serverImpl{
			dataplaneManager: dataplaneManager,
		}
		sharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id:            "test-sharedpool-id",
				DataplaneName: "test-dataplane",
			},
		}

		err := server.checkManualScaleHPAGate(
			context.Background(),
			sharedPool,
			apiv1.ComponentType_TIDB,
			"test-group-id",
			3,
			2,
			false,
		)

		require.NoError(t, err)
		dataplaneManager.AssertNotCalled(t, "GetClient", mock.Anything)
	})

	t.Run("scale out is blocked when HPA scaleDown is enabled", func(t *testing.T) {
		kubeclient := kubemocks.NewClient(t)
		dataplaneManager := dataplanemocks.NewManager(t)
		dataplaneManager.On("GetClient", mock.AnythingOfType("string")).Return(kubeclient, nil)

		kubeclient.On("List", mock.Anything, mock.IsType(&autoscalingv2.HorizontalPodAutoscalerList{}), mock.Anything).
			Run(func(args mock.Arguments) {
				hpaList := args.Get(1).(*autoscalingv2.HorizontalPodAutoscalerList)
				selectPolicy := autoscalingv2.MaxChangePolicySelect
				hpaList.Items = []autoscalingv2.HorizontalPodAutoscaler{
					{
						Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
							Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
								ScaleDown: &autoscalingv2.HPAScalingRules{
									SelectPolicy: &selectPolicy,
								},
							},
						},
					},
				}
			}).
			Return(nil)

		server := &serverImpl{
			dataplaneManager: dataplaneManager,
		}
		sharedPool := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id:            "test-sharedpool-id",
				DataplaneName: "test-dataplane",
			},
		}

		err := server.checkManualScaleHPAGate(
			context.Background(),
			sharedPool,
			apiv1.ComponentType_TIDB,
			"test-group-id",
			2,
			3,
			false,
		)

		require.Error(t, err)
		assert.Equal(t, codes.FailedPrecondition, status.Code(err))
		assert.Contains(t, err.Error(), "cannot scale out")
	})
}
