package cluster

import (
	"context"
	"fmt"
	"testing"
	"time"

	. "github.com/onsi/gomega"
	"google.golang.org/genproto/protobuf/field_mask"
	grpcmetadata "google.golang.org/grpc/metadata"

	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/infra/repository/db"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/infra/repository/schema"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func TestUpdateServerlessCluster(t *testing.T) {
	g := NewWithT(t)

	service, err := initService()
	g.Expect(err).ShouldNot(HaveOccurred())
	dbProvider := db.NewProvider(nil)

	type testCase struct {
		name      string
		inputsFn  func() (context.Context, *openapiserverlessv1beta1.PartialUpdateClusterRequest)
		outputsFn func(*openapiserverlessv1beta1.Cluster, error)
	}

	cases := []testCase{
		{
			name: "Update display name should success",
			inputsFn: func() (context.Context, *openapiserverlessv1beta1.PartialUpdateClusterRequest) {
				// Create a cluster first
				ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.New(map[string]string{
					metadata.UserIDHeaderKey:           "1",
					metadata.OrgIDHeaderKey:            "1",
					metadata.DefaultProjectIDHeaderKey: "1",
					metadata.ProjectListHeaderKey:      "1,2,3",
					metadata.IsCheckPermissionKey:      "true",
				}))

				req := &openapiserverlessv1beta1.CreateClusterRequest{
					Cluster: &openapiserverlessv1beta1.Cluster{
						DisplayName: "update-test-1",
						Region: &openapicommonv1beta1.Region{
							Name: "regions/aws-us-east-1",
						},
						Labels: map[string]string{
							common.ProjectIDLabelKey: "1",
						},
					},
				}
				c, err := service.CreateCluster(ctx, req)
				g.Expect(err).ShouldNot(HaveOccurred())
				time.Sleep(time.Second)

				// Build update request
				return ctx, &openapiserverlessv1beta1.PartialUpdateClusterRequest{
					Cluster: &openapiserverlessv1beta1.PartialUpdateClusterRequest_PartialUpdateCluster{
						ClusterId:   c.ClusterId,
						DisplayName: "update-test-2",
					},
					UpdateMask: &field_mask.FieldMask{Paths: []string{"display_name"}},
				}
			},
			outputsFn: func(cluster *openapiserverlessv1beta1.Cluster, err error) {
				g.Expect(err).ShouldNot(HaveOccurred())
				g.Expect(cluster).ShouldNot(BeNil())
				g.Expect(cluster.ClusterId).ShouldNot(BeEmpty())
				g.Expect(cluster.Name).Should(Equal(fmt.Sprintf("clusters/%s", cluster.ClusterId)))
				g.Expect(cluster.DisplayName).Should(Equal("update-test-2"))
				g.Expect(cluster.Version).ShouldNot(BeEmpty())
				g.Expect(cluster.UserPrefix).ShouldNot(BeEmpty())

				// Check the cluster in database
				var record schema.Cluster

				// Find the soft deleted record
				err = dbProvider.ReadDB(t.Context()).
					Model(&schema.Cluster{}).
					Where("cluster_id = ?", cluster.ClusterId).
					Take(&record).
					Error
				g.Expect(err).ShouldNot(HaveOccurred())
				g.Expect(record).ShouldNot(BeNil())
				g.Expect(record.DisplayName).Should(Equal("update-test-2"))
				g.Expect(record.UpdatedAt).Should(BeTemporally(">", record.CreatedAt))
			},
		},
		{
			name: "disable cluster pubilc endpoint success",
			inputsFn: func() (context.Context, *openapiserverlessv1beta1.PartialUpdateClusterRequest) {
				// Create a cluster first
				ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.New(map[string]string{
					metadata.UserIDHeaderKey:           "1",
					metadata.OrgIDHeaderKey:            "1",
					metadata.DefaultProjectIDHeaderKey: "1",
					metadata.ProjectListHeaderKey:      "1,2,3",
					metadata.IsCheckPermissionKey:      "true",
				}))

				req := &openapiserverlessv1beta1.CreateClusterRequest{
					Cluster: &openapiserverlessv1beta1.Cluster{
						DisplayName: "update-test-3",
						Region: &openapicommonv1beta1.Region{
							Name: "regions/aws-us-east-1",
						},
						Labels: map[string]string{
							common.ProjectIDLabelKey: "1",
						},
					},
				}
				c, err := service.CreateCluster(ctx, req)
				g.Expect(err).ShouldNot(HaveOccurred())
				time.Sleep(time.Second)

				// Build update request
				return ctx, &openapiserverlessv1beta1.PartialUpdateClusterRequest{
					Cluster: &openapiserverlessv1beta1.PartialUpdateClusterRequest_PartialUpdateCluster{
						ClusterId: c.ClusterId,
						Endpoints: &openapiserverlessv1beta1.Cluster_Endpoints{
							Public: &openapiserverlessv1beta1.Cluster_Endpoints_Public{Disabled: true},
						},
					},
					UpdateMask: &field_mask.FieldMask{Paths: []string{"endpoints.public_endpoint.disabled"}},
				}
			},
			outputsFn: func(cluster *openapiserverlessv1beta1.Cluster, err error) {
				g.Expect(err).ShouldNot(HaveOccurred())
				g.Expect(cluster).ShouldNot(BeNil())
				g.Expect(cluster.ClusterId).ShouldNot(BeEmpty())
				g.Expect(cluster.Name).Should(Equal(fmt.Sprintf("clusters/%s", cluster.ClusterId)))
				g.Expect(cluster.DisplayName).Should(Equal("update-test-3"))
				g.Expect(cluster.Endpoints.Public.Disabled).Should(BeTrue())
				g.Expect(cluster.Version).ShouldNot(BeEmpty())
				g.Expect(cluster.UserPrefix).ShouldNot(BeEmpty())

				// Check the cluster in database
				var record schema.Cluster

				// Find the soft deleted record
				err = dbProvider.ReadDB(t.Context()).
					Model(&schema.Cluster{}).
					Where("cluster_id = ?", cluster.ClusterId).
					Take(&record).
					Error
				g.Expect(err).ShouldNot(HaveOccurred())
				g.Expect(record.UpdatedAt).Should(BeTemporally(">", record.CreatedAt))
			},
		},
		{
			name: "update labels success",
			inputsFn: func() (context.Context, *openapiserverlessv1beta1.PartialUpdateClusterRequest) {
				// Create a cluster first
				ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.New(map[string]string{
					metadata.UserIDHeaderKey:           "1",
					metadata.OrgIDHeaderKey:            "1",
					metadata.DefaultProjectIDHeaderKey: "1",
					metadata.ProjectListHeaderKey:      "1,2,3",
					metadata.IsCheckPermissionKey:      "true",
				}))

				req := &openapiserverlessv1beta1.CreateClusterRequest{
					Cluster: &openapiserverlessv1beta1.Cluster{
						DisplayName: "update-test-4",
						Region: &openapicommonv1beta1.Region{
							Name: "regions/aws-us-east-1",
						},
						Labels: map[string]string{
							common.ProjectIDLabelKey: "1",
						},
					},
				}
				c, err := service.CreateCluster(ctx, req)
				g.Expect(err).ShouldNot(HaveOccurred())

				// Build update request
				return ctx, &openapiserverlessv1beta1.PartialUpdateClusterRequest{
					Cluster: &openapiserverlessv1beta1.PartialUpdateClusterRequest_PartialUpdateCluster{
						ClusterId: c.ClusterId,
						Labels:    map[string]string{"tidbcloud/test": "test"},
					},
					UpdateMask: &field_mask.FieldMask{Paths: []string{"labels"}},
				}
			},
			outputsFn: func(cluster *openapiserverlessv1beta1.Cluster, err error) {
				g.Expect(err).ShouldNot(HaveOccurred())
				g.Expect(cluster).ShouldNot(BeNil())
				g.Expect(cluster.ClusterId).ShouldNot(BeEmpty())
				g.Expect(cluster.Name).Should(Equal(fmt.Sprintf("clusters/%s", cluster.ClusterId)))
				g.Expect(cluster.DisplayName).Should(Equal("update-test-4"))
				g.Expect(cluster.Labels["tidbcloud/test"]).Should(Equal("test"))
				g.Expect(cluster.Version).ShouldNot(BeEmpty())
				g.Expect(cluster.UserPrefix).ShouldNot(BeEmpty())
			},
		},
		{
			name: "update labels failed",
			inputsFn: func() (context.Context, *openapiserverlessv1beta1.PartialUpdateClusterRequest) {
				// Create a cluster first
				ctx := grpcmetadata.NewIncomingContext(t.Context(), grpcmetadata.New(map[string]string{
					metadata.UserIDHeaderKey:           "1",
					metadata.OrgIDHeaderKey:            "1",
					metadata.DefaultProjectIDHeaderKey: "1",
					metadata.ProjectListHeaderKey:      "1,2,3",
					metadata.IsCheckPermissionKey:      "true",
				}))

				req := &openapiserverlessv1beta1.CreateClusterRequest{
					Cluster: &openapiserverlessv1beta1.Cluster{
						DisplayName: "update-test-5",
						Region: &openapicommonv1beta1.Region{
							Name: "regions/aws-us-east-1",
						},
						Labels: map[string]string{
							common.ProjectIDLabelKey: "1",
						},
					},
				}
				c, err := service.CreateCluster(ctx, req)
				g.Expect(err).ShouldNot(HaveOccurred())

				// Build update request
				return ctx, &openapiserverlessv1beta1.PartialUpdateClusterRequest{
					Cluster: &openapiserverlessv1beta1.PartialUpdateClusterRequest_PartialUpdateCluster{
						ClusterId: c.ClusterId,
						Labels:    map[string]string{common.ProjectIDLabelKey: "5"},
					},
					UpdateMask: &field_mask.FieldMask{Paths: []string{"labels"}},
				}
			},
			outputsFn: func(cluster *openapiserverlessv1beta1.Cluster, err error) {
				g.Expect(err).Should(HaveOccurred())
				g.Expect(err.Error()).Should(ContainSubstring("label is not allowed to be modified"))
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			ctx, req := tc.inputsFn()
			cluster, err := service.UpdateCluster(ctx, req)
			tc.outputsFn(cluster, err)
		})
	}
}

func TestBuildNewClusterNilSafeForEndpointAndAuditMasks(t *testing.T) {
	g := NewWithT(t)

	serverlessCluster := &globalserverlessv1.ServerlessServiceCluster{
		Cluster: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{},
	}
	baseCluster := &entity.Cluster{ClusterId: 1, ProjectId: 1, OrganizationId: 1}

	_, _, err := buildNewCluster(
		serverlessCluster,
		&openapiserverlessv1beta1.PartialUpdateClusterRequest{
			Cluster: &openapiserverlessv1beta1.PartialUpdateClusterRequest_PartialUpdateCluster{
				ClusterId: "1",
				Endpoints: &openapiserverlessv1beta1.Cluster_Endpoints{
					Public: &openapiserverlessv1beta1.Cluster_Endpoints_Public{Disabled: true},
				},
			},
		},
		"endpoints.public.disabled",
		baseCluster,
	)
	g.Expect(err).ShouldNot(HaveOccurred())

	_, _, err = buildNewCluster(
		serverlessCluster,
		&openapiserverlessv1beta1.PartialUpdateClusterRequest{
			Cluster: &openapiserverlessv1beta1.PartialUpdateClusterRequest_PartialUpdateCluster{ClusterId: "1"},
		},
		"audit_log_config.enabled",
		baseCluster,
	)
	g.Expect(err).Should(HaveOccurred())
	g.Expect(err.Error()).Should(ContainSubstring("audit log config enabled is nil"))

	_, _, err = buildNewCluster(
		serverlessCluster,
		&openapiserverlessv1beta1.PartialUpdateClusterRequest{
			Cluster: &openapiserverlessv1beta1.PartialUpdateClusterRequest_PartialUpdateCluster{ClusterId: "1"},
		},
		"audit_log_config.unredacted",
		baseCluster,
	)
	g.Expect(err).Should(HaveOccurred())
	g.Expect(err.Error()).Should(ContainSubstring("audit log config unredacted is nil"))
}

func TestBuildNewClusterRejectsPGStarterNetworkUpdates(t *testing.T) {
	g := NewWithT(t)

	pgServerlessCluster := &globalserverlessv1.ServerlessServiceCluster{
		EngineType: openapicommonv1beta1.EngineType_POSTGRESQL_COMPATIBLE.String(),
		Cluster:    &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{},
	}
	pgBaseCluster := &entity.Cluster{
		ClusterId:      1,
		ProjectId:      1,
		OrganizationId: 1,
		ServicePlan:    openapicommonv1beta1.ServicePlan_Starter,
		EngineType:     openapicommonv1beta1.EngineType_POSTGRESQL_COMPATIBLE,
	}

	for _, mask := range []string{"endpoints.public.disabled", "endpoints.public_endpoint.disabled", "endpoints.public.authorized_networks"} {
		_, _, err := buildNewCluster(
			pgServerlessCluster,
			&openapiserverlessv1beta1.PartialUpdateClusterRequest{
				Cluster: &openapiserverlessv1beta1.PartialUpdateClusterRequest_PartialUpdateCluster{
					ClusterId: "1",
					Endpoints: &openapiserverlessv1beta1.Cluster_Endpoints{
						Public: &openapiserverlessv1beta1.Cluster_Endpoints_Public{Disabled: true},
					},
				},
			},
			mask,
			pgBaseCluster,
		)
		g.Expect(err).Should(HaveOccurred())
		g.Expect(err.Error()).Should(ContainSubstring("not supported for pg starter clusters"))
	}
}
