package server

import (
	"context"
	"testing"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	"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/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/cert"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	dataplanemgrmocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr/mocks"
	repomnocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestForceUpdateGatewayIdentity_BasicValidation(t *testing.T) {
	tests := []struct {
		name            string
		tidbClusterID   string
		gatewayIdentity string
		expectedError   string
		expectedCode    codes.Code
	}{
		{
			name:            "empty cluster id",
			tidbClusterID:   "",
			gatewayIdentity: "valid-gateway",
			expectedError:   "tidb cluster id is required",
			expectedCode:    codes.InvalidArgument,
		},
		{
			name:            "empty gateway identity",
			tidbClusterID:   "cluster-id",
			gatewayIdentity: "",
			expectedError:   "gateway identity is required",
			expectedCode:    codes.InvalidArgument,
		},
		{
			name:            "invalid gateway identity",
			tidbClusterID:   "cluster-id",
			gatewayIdentity: "Invalid_Gateway",
			expectedError:   "failed to validate gateway identity",
			expectedCode:    codes.InvalidArgument,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			s := &serverImpl{}

			err := s.ForceUpdateGatewayIdentity(context.Background(), tt.tidbClusterID, tt.gatewayIdentity)

			require.Error(t, err)
			assert.Contains(t, err.Error(), tt.expectedError)
			assert.Equal(t, tt.expectedCode, status.Code(err))
		})
	}
}

func TestForceUpdateGatewayIdentity(t *testing.T) {
	tests := []struct {
		name            string
		tidbClusterID   string
		gatewayIdentity string
		tidbCluster     *entity.TiDBCluster
		tidbGroups      []*entity.TiDBGroup
		lockErr         error
		saveErr         error
		expectList      bool
		listCallCount   int
		expectedError   string
		expectedCode    codes.Code
		expectSave      bool
		setupCertUpdate func(t *testing.T, sharedPoolRepo *repomnocks.SharedPoolRepo, dataplaneManager *dataplanemocks.Manager, dataplaneClient *dataplanemgrmocks.Client)
	}{
		{
			name:            "cluster not found",
			tidbClusterID:   "cluster-id",
			gatewayIdentity: "new-gateway",
			expectedError:   "not found",
			expectedCode:    codes.NotFound,
		},
		{
			name:            "lock failed",
			tidbClusterID:   "cluster-id",
			gatewayIdentity: "new-gateway",
			tidbCluster: &entity.TiDBCluster{
				Id:           "cluster-id",
				SharedPoolId: "shared-pool-id",
				Phase:        apiv1.Cluster_AVAILABLE,
			},
			lockErr:       status.Error(codes.Internal, "lock failed"),
			expectedError: "lock failed",
			expectedCode:  codes.Internal,
		},
		{
			name:            "cluster phase is not available",
			tidbClusterID:   "cluster-id",
			gatewayIdentity: "new-gateway",
			tidbCluster: &entity.TiDBCluster{
				Id:           "cluster-id",
				SharedPoolId: "shared-pool-id",
				Phase:        apiv1.Cluster_MODIFYING,
			},
			expectedError: "phase must be AVAILABLE",
			expectedCode:  codes.FailedPrecondition,
		},
		{
			name:            "cluster has tidb groups",
			tidbClusterID:   "cluster-id",
			gatewayIdentity: "new-gateway",
			tidbCluster: &entity.TiDBCluster{
				Id:           "cluster-id",
				SharedPoolId: "shared-pool-id",
				Phase:        apiv1.Cluster_AVAILABLE,
			},
			tidbGroups: []*entity.TiDBGroup{
				{
					TiDBGroup: apiv1.TiDBGroup{
						Id:    "group-id",
						Phase: apiv1.TiDBGroup_AVAILABLE,
					},
				},
			},
			expectList:    true,
			listCallCount: 2,
			expectedError: "must not have any tidb groups",
			expectedCode:  codes.FailedPrecondition,
		},
		{
			name:            "save failed",
			tidbClusterID:   "cluster-id",
			gatewayIdentity: "new-gateway",
			tidbCluster: &entity.TiDBCluster{
				Id:           "cluster-id",
				SharedPoolId: "shared-pool-id",
				Phase:        apiv1.Cluster_AVAILABLE,
			},
			expectList:    true,
			listCallCount: 2,
			saveErr:       status.Error(codes.Internal, "save failed"),
			expectedError: "failed to save tidb cluster",
			expectedCode:  codes.Internal,
			expectSave:    true,
		},
		{
			name:            "success",
			tidbClusterID:   "cluster-id",
			gatewayIdentity: "new-gateway",
			tidbCluster: &entity.TiDBCluster{
				Id:              "cluster-id",
				SharedPoolId:    "shared-pool-id",
				TenantId:        "tenant-id",
				GatewayIdentity: "old-gateway",
				Phase:           apiv1.Cluster_AVAILABLE,
			},
			expectList:    true,
			listCallCount: 2,
			expectedCode:  codes.OK,
			expectSave:    true,
			setupCertUpdate: func(t *testing.T, sharedPoolRepo *repomnocks.SharedPoolRepo, dataplaneManager *dataplanemocks.Manager, dataplaneClient *dataplanemgrmocks.Client) {
				t.Helper()

				scheme := runtime.NewScheme()
				require.NoError(t, certmanagerv1.AddToScheme(scheme))

				sharedPool := &entity.SharedPool{
					SharedPool: apiv1.SharedPool{
						Id:            "shared-pool-id",
						DataplaneName: "test-dataplane",
					},
				}
				oldCert := cert.TiProxyServerCerts("cluster-id", sharedPool.GetDataPlaneNamespace(), "old-gateway", "example.com", false)
				kubeclient := fake.NewClientBuilder().
					WithScheme(scheme).
					WithObjects(oldCert).
					Build()

				sharedPoolRepo.On("Load", mock.Anything, "shared-pool-id", false).
					Return(sharedPool, nil).
					Once()
				dataplaneManager.On("GetClient", "test-dataplane").
					Return(kubeclient, nil).
					Once()
				dataplaneClient.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "test-dataplane"
				})).
					Return(&dplanemgrv1.DataPlane{Domain: "example.com"}, nil).
					Once()

				t.Cleanup(func() {
					expectedCert := cert.TiProxyServerCerts("cluster-id", sharedPool.GetDataPlaneNamespace(), "new-gateway", "example.com", false)
					actualCert := &certmanagerv1.Certificate{}
					require.NoError(t, kubeclient.Get(context.Background(), client.ObjectKeyFromObject(expectedCert), actualCert))
					assert.Equal(t, expectedCert.Spec, actualCert.Spec)
				})
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			tidbClusterRepo := repomnocks.NewTiDBClusterRepo(t)
			sharedPoolRepo := repomnocks.NewSharedPoolRepo(t)
			tidbGroupRepo := repomnocks.NewTiDBGroupRepo(t)
			lockRepo := repomnocks.NewLockRepo(t)
			dataplaneManager := dataplanemocks.NewManager(t)
			dataplaneClient := dataplanemgrmocks.NewClient(t)

			server := &serverImpl{
				tidbClusterRepo:  tidbClusterRepo,
				sharedPoolRepo:   sharedPoolRepo,
				tidbGroupRepo:    tidbGroupRepo,
				lockRepo:         lockRepo,
				dataplaneManager: dataplaneManager,
				dataplaneClient:  dataplaneClient,
				dagExecutor:      &MockExecutor{},
				transactioner:    &mockTransactioner{},
				bizCfg:           &config.BizConfig{},
			}

			if tt.tidbCluster == nil {
				tidbClusterRepo.On("Load", mock.Anything, mock.Anything, false).
					Return(nil, status.Error(codes.NotFound, "not found")).
					Once()
			} else {
				tidbClusterRepo.On("Load", mock.Anything, mock.Anything, false).
					Return(tt.tidbCluster, nil)
				lockRepo.On("Lock", mock.Anything, mock.Anything).
					Return(func() error { return nil }, tt.lockErr)

				if tt.expectList {
					tidbGroupRepo.On("List", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, false).
						Return(tt.tidbGroups, nil).
						Times(tt.listCallCount)
				}
			}

			if tt.expectSave {
				if tt.setupCertUpdate == nil {
					sharedPoolRepo.On("Load", mock.Anything, "shared-pool-id", false).
						Return(&entity.SharedPool{}, nil).
						Once()
				}
				tidbClusterRepo.On("Save", mock.Anything, mock.MatchedBy(func(cluster *entity.TiDBCluster) bool {
					return cluster.GetGatewayIdentity() == tt.gatewayIdentity
				})).Return(tt.saveErr)
			}
			if tt.setupCertUpdate != nil {
				tt.setupCertUpdate(t, sharedPoolRepo, dataplaneManager, dataplaneClient)
			}

			err := server.ForceUpdateGatewayIdentity(context.Background(), tt.tidbClusterID, tt.gatewayIdentity)

			if tt.expectedCode == codes.OK {
				require.NoError(t, err)
				return
			}

			require.Error(t, err)
			assert.Contains(t, err.Error(), tt.expectedError)
			assert.Equal(t, tt.expectedCode, status.Code(err))
		})
	}
}
