package server

import (
	"context"
	"testing"

	"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/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	clustergatewaymocks "github.com/tidbcloud/cluster-service-ng/pkg/clustergateway/mocks"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	dataplanemgrmocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr/mocks"
	executormocks "github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor/mocks"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	repomnocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	autoscalingv2 "k8s.io/api/autoscaling/v2"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestGetRuConfig(t *testing.T) {
	tests := []struct {
		name          string
		id            string
		scope         entity.RuConfigScope
		setupMocks    func(*repomnocks.RuConfigRepo)
		expectedError string
		expectedCode  codes.Code
		validate      func(*testing.T, *entity.RuConfig, error)
	}{
		{
			name:  "successful get ru config",
			id:    "test-id",
			scope: apiv1.RuConfig_SHARED_POOL,
			setupMocks: func(ruConfigRepo *repomnocks.RuConfigRepo) {
				mockRuConfig := &entity.RuConfig{
					// RuConfig is just a type alias for apiv1.RuConfig
				}
				ruConfigRepo.On("GetRuConfig", mock.Anything, "test-id", apiv1.RuConfig_SHARED_POOL).Return(mockRuConfig, nil)
			},
			validate: func(t *testing.T, result *entity.RuConfig, err error) {
				t.Helper()
				require.NoError(t, err)
				require.NotNil(t, result)
			},
		},
		{
			name:          "unspecified scope",
			id:            "test-id",
			scope:         apiv1.RuConfig_SCOPE_UNSPECIFIED,
			setupMocks:    func(*repomnocks.RuConfigRepo) {},
			expectedError: "ru config scope is unspecified",
			expectedCode:  codes.InvalidArgument,
		},
		{
			name:  "repository error",
			id:    "test-id",
			scope: apiv1.RuConfig_SHARED_POOL,
			setupMocks: func(ruConfigRepo *repomnocks.RuConfigRepo) {
				ruConfigRepo.On("GetRuConfig", mock.Anything, "test-id", apiv1.RuConfig_SHARED_POOL).Return(nil, status.Error(codes.NotFound, "not found"))
			},
			expectedError: "failed to get ru config",
			expectedCode:  codes.Internal,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mocks
			ruConfigRepo := repomnocks.NewRuConfigRepo(t)
			dataplaneClient := dataplanemgrmocks.NewClient(t)
			dagExecutor := executormocks.NewRemoteExecutor(t)
			clusterGatewayClient := clustergatewaymocks.NewClient(t)

			// Setup mocks
			tt.setupMocks(ruConfigRepo)

			// Create server implementation
			server := &serverImpl{
				ruConfigRepo:      ruConfigRepo,
				dataplaneClient:   dataplaneClient,
				dagExecutor:       dagExecutor,
				transactioner:     &mockTransactioner{},
				clustergatewayCli: clusterGatewayClient,
			}

			// Execute test
			result, err := server.GetRuConfig(context.Background(), tt.id, tt.scope)

			// Validate results
			switch {
			case tt.expectedError != "":
				require.Error(t, err)
				require.Contains(t, err.Error(), tt.expectedError)
				if tt.expectedCode != codes.OK {
					require.Equal(t, tt.expectedCode, status.Code(err))
				}
				require.Nil(t, result)
			case tt.validate != nil:
				tt.validate(t, result, err)
			default:
				require.NoError(t, err)
				require.NotNil(t, result)
			}
		})
	}
}

func TestSetRuConfig(t *testing.T) {
	tests := []struct {
		name          string
		id            string
		scope         entity.RuConfigScope
		ruConfig      *entity.RuConfig
		setupMocks    func(*repomnocks.RuConfigRepo)
		expectedError string
		expectedCode  codes.Code
		validate      func(*testing.T, *entity.RuConfig, error)
	}{
		{
			name:     "successful set ru config",
			id:       "test-id",
			scope:    apiv1.RuConfig_SHARED_POOL,
			ruConfig: &entity.RuConfig{
				// RuConfig is just a type alias
			},
			setupMocks: func(ruConfigRepo *repomnocks.RuConfigRepo) {
				ruConfigRepo.On("SetRuConfig", mock.Anything, "test-id", apiv1.RuConfig_SHARED_POOL, mock.Anything).Return(nil)
			},
			validate: func(t *testing.T, result *entity.RuConfig, err error) {
				t.Helper()
				require.NoError(t, err)
				require.NotNil(t, result)
			},
		},
		{
			name:          "unspecified scope",
			id:            "test-id",
			scope:         apiv1.RuConfig_SCOPE_UNSPECIFIED,
			ruConfig:      &entity.RuConfig{},
			setupMocks:    func(*repomnocks.RuConfigRepo) {},
			expectedError: "ru config scope is unspecified",
			expectedCode:  codes.InvalidArgument,
		},
		{
			name:     "repository error",
			id:       "test-id",
			scope:    apiv1.RuConfig_SHARED_POOL,
			ruConfig: &entity.RuConfig{
				// RuConfig is just a type alias
			},
			setupMocks: func(ruConfigRepo *repomnocks.RuConfigRepo) {
				ruConfigRepo.On("SetRuConfig", mock.Anything, "test-id", apiv1.RuConfig_SHARED_POOL, mock.Anything).Return(status.Error(codes.Internal, "repository error"))
			},
			expectedError: "failed to set ru config",
			expectedCode:  codes.Internal,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mocks
			ruConfigRepo := repomnocks.NewRuConfigRepo(t)
			dataplaneClient := dataplanemgrmocks.NewClient(t)
			dagExecutor := executormocks.NewRemoteExecutor(t)
			clusterGatewayClient := clustergatewaymocks.NewClient(t)

			// Setup mocks
			tt.setupMocks(ruConfigRepo)

			// Create server implementation
			server := &serverImpl{
				ruConfigRepo:      ruConfigRepo,
				sharedPoolRepo:    availableSharedPoolRepoForMutationTest(),
				dataplaneClient:   dataplaneClient,
				dagExecutor:       dagExecutor,
				transactioner:     &mockTransactioner{},
				clustergatewayCli: clusterGatewayClient,
			}

			// Execute test
			result, err := server.SetRuConfig(context.Background(), tt.id, tt.scope, tt.ruConfig)

			// Validate results
			switch {
			case tt.expectedError != "":
				require.Error(t, err)
				require.Contains(t, err.Error(), tt.expectedError)
				if tt.expectedCode != codes.OK {
					require.Equal(t, tt.expectedCode, status.Code(err))
				}
				require.Nil(t, result)
			case tt.validate != nil:
				tt.validate(t, result, err)
			default:
				require.NoError(t, err)
				require.NotNil(t, result)
			}
		})
	}
}

func TestSetRuConfig_SharedPoolTiProxyScaleInFlagSyncsHPA(t *testing.T) {
	ctx := context.Background()
	sharedPoolID := "sp-sync"
	namespace := "tidb" + sharedPoolID
	minReplicas := int32(1)

	tests := []struct {
		name       string
		flag       bool
		prevFlag   bool
		prevFound  bool
		initial    autoscalingv2.ScalingPolicySelect
		wantPolicy autoscalingv2.ScalingPolicySelect
	}{
		{
			name:       "flag false disables existing TiProxy scale-in when previous row is missing",
			flag:       false,
			prevFound:  false,
			initial:    autoscalingv2.MinChangePolicySelect,
			wantPolicy: autoscalingv2.DisabledPolicySelect,
		},
		{
			name:       "flag true enables existing TiProxy scale-in",
			flag:       true,
			prevFlag:   false,
			prevFound:  true,
			initial:    autoscalingv2.DisabledPolicySelect,
			wantPolicy: autoscalingv2.MinChangePolicySelect,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := runtime.NewScheme()
			require.NoError(t, autoscalingv2.AddToScheme(scheme))
			hpa := &autoscalingv2.HorizontalPodAutoscaler{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "tiproxygroup-db-cluster-1",
					Namespace: namespace,
					Labels: map[string]string{
						schemav1.LabelComponentKey: "tiproxy",
					},
				},
				Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
					MinReplicas: &minReplicas,
					MaxReplicas: 3,
					Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
						ScaleDown: &autoscalingv2.HPAScalingRules{
							SelectPolicy: &tt.initial,
						},
					},
				},
			}
			kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(hpa).Build()

			ruConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
				FeatureFlags: &entity.RuConfigFeatureFlags{
					EnableAutoscaleTiproxyScaleIn: &tt.flag,
				},
			}}
			sharedPool := &entity.SharedPool{SharedPool: apiv1.SharedPool{
				Id:            sharedPoolID,
				Phase:         apiv1.SharedPool_AVAILABLE,
				DataplaneName: "dp-1",
			}}
			prevRuConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
				FeatureFlags: &entity.RuConfigFeatureFlags{
					EnableAutoscaleTiproxyScaleIn: &tt.prevFlag,
				},
			}}
			ruConfigRepo := repomnocks.NewRuConfigRepo(t)
			ruConfigRepo.On("GetRuConfigWithFound", mock.Anything, sharedPoolID, apiv1.RuConfig_SHARED_POOL).
				Return(prevRuConfig, tt.prevFound, nil).Once()
			ruConfigRepo.On("SetRuConfig", mock.Anything, sharedPoolID, apiv1.RuConfig_SHARED_POOL, ruConfig).
				Return(nil).Once()
			sharedPoolRepo := repomnocks.NewSharedPoolRepo(t)
			sharedPoolRepo.On("Load", mock.Anything, sharedPoolID, false).
				Return(sharedPool, nil).Twice()
			dataplaneManager := dataplanemocks.NewManager(t)
			dataplaneManager.On("GetClient", "dp-1").Return(kubeClient, nil).Once()

			s := &serverImpl{
				ruConfigRepo:     ruConfigRepo,
				sharedPoolRepo:   sharedPoolRepo,
				dataplaneManager: dataplaneManager,
			}

			got, err := s.SetRuConfig(ctx, sharedPoolID, apiv1.RuConfig_SHARED_POOL, ruConfig)
			require.NoError(t, err)
			require.Same(t, ruConfig, got)

			updated := &autoscalingv2.HorizontalPodAutoscaler{}
			require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{
				Namespace: namespace,
				Name:      "tiproxygroup-db-cluster-1",
			}, updated))
			require.NotNil(t, updated.Spec.Behavior)
			require.NotNil(t, updated.Spec.Behavior.ScaleDown)
			require.NotNil(t, updated.Spec.Behavior.ScaleDown.SelectPolicy)
			require.Equal(t, tt.wantPolicy, *updated.Spec.Behavior.ScaleDown.SelectPolicy)
		})
	}
}

func TestSetRuConfig_TiProxyScaleInFlagDoesNotPersistWhenHPASyncFails(t *testing.T) {
	ctx := context.Background()
	sharedPoolID := "sp-sync-fail"
	enabled := true
	prev := false
	ruConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableAutoscaleTiproxyScaleIn: &enabled,
		},
	}}
	prevRuConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableAutoscaleTiproxyScaleIn: &prev,
		},
	}}
	sharedPool := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id:            sharedPoolID,
		Phase:         apiv1.SharedPool_AVAILABLE,
		DataplaneName: "dp-1",
	}}

	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.On("GetRuConfigWithFound", mock.Anything, sharedPoolID, apiv1.RuConfig_SHARED_POOL).
		Return(prevRuConfig, true, nil).Once()
	sharedPoolRepo := repomnocks.NewSharedPoolRepo(t)
	sharedPoolRepo.On("Load", mock.Anything, sharedPoolID, false).
		Return(sharedPool, nil).Twice()
	dataplaneManager := dataplanemocks.NewManager(t)
	dataplaneManager.On("GetClient", "dp-1").
		Return(nil, status.Error(codes.Internal, "dataplane unavailable")).Once()

	s := &serverImpl{
		ruConfigRepo:     ruConfigRepo,
		sharedPoolRepo:   sharedPoolRepo,
		dataplaneManager: dataplaneManager,
	}

	got, err := s.SetRuConfig(ctx, sharedPoolID, apiv1.RuConfig_SHARED_POOL, ruConfig)
	require.Error(t, err)
	require.Equal(t, codes.Internal, status.Code(err))
	require.Contains(t, err.Error(), "failed to get remote client for TiProxy scale-in sync")
	require.Nil(t, got)
	ruConfigRepo.AssertNotCalled(t, "SetRuConfig", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
}

func TestSetRuConfig_TiProxyScaleInFlagDoesNotSyncWhenUnchanged(t *testing.T) {
	ctx := context.Background()
	sharedPoolID := "sp-unchanged"
	enabled := true
	ruConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableAutoscaleTiproxyScaleIn: &enabled,
		},
	}}
	prevRuConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableAutoscaleTiproxyScaleIn: &enabled,
		},
	}}

	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.On("GetRuConfigWithFound", mock.Anything, sharedPoolID, apiv1.RuConfig_SHARED_POOL).
		Return(prevRuConfig, true, nil).Once()
	ruConfigRepo.On("SetRuConfig", mock.Anything, sharedPoolID, apiv1.RuConfig_SHARED_POOL, ruConfig).
		Return(nil).Once()

	s := &serverImpl{
		ruConfigRepo:   ruConfigRepo,
		sharedPoolRepo: availableSharedPoolRepoForMutationTest(),
	}

	got, err := s.SetRuConfig(ctx, sharedPoolID, apiv1.RuConfig_SHARED_POOL, ruConfig)
	require.NoError(t, err)
	require.Same(t, ruConfig, got)
}

func TestSetRuConfig_TiProxyScaleInFlagDoesNotSyncHPAInEnforceSingleTenant(t *testing.T) {
	ctx := context.Background()
	sharedPoolID := "sp-enforce"
	enabled := true
	ruConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableAutoscaleTiproxyScaleIn: &enabled,
		},
	}}
	sharedPool := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id:    sharedPoolID,
		Phase: apiv1.SharedPool_AVAILABLE,
		Provider: &apiv1.Provider{Tags: map[string]string{
			schemav1.LabelUserTenantKey: "tenant-1",
		}},
	}}

	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.On("GetRuConfigWithFound", mock.Anything, sharedPoolID, apiv1.RuConfig_SHARED_POOL).
		Return(&entity.RuConfig{}, false, nil).Once()
	ruConfigRepo.On("SetRuConfig", mock.Anything, sharedPoolID, apiv1.RuConfig_SHARED_POOL, ruConfig).
		Return(nil).Once()
	sharedPoolRepo := repomnocks.NewSharedPoolRepo(t)
	sharedPoolRepo.On("Load", mock.Anything, sharedPoolID, false).
		Return(sharedPool, nil).Twice()

	s := &serverImpl{
		ruConfigRepo:   ruConfigRepo,
		sharedPoolRepo: sharedPoolRepo,
		bizCfg: &config.BizConfig{AutoscalePolicy: config.AutoscalePolicyConfig{
			Mode:            config.AutoscalePolicyModeEnforce,
			AllowAllTenants: true,
		}},
	}

	got, err := s.SetRuConfig(ctx, sharedPoolID, apiv1.RuConfig_SHARED_POOL, ruConfig)
	require.NoError(t, err)
	require.Same(t, ruConfig, got)
}
