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/config"
	"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/runtime/actions"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	mysqlschema "github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
)

func TestCreateDataplaneActionStart(t *testing.T) {
	tests := []struct {
		name        string
		args        actions.ActionArgs
		bizConfig   *config.BizConfig
		plan        string
		setupMocks  func(*dataplanemgrmocks.Client)
		validate    func(*testing.T, *dplanemgrv1.CreateDataPlaneRequest)
		expectError bool
	}{
		{
			name: "create dataplane with pca",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey:           actions.ActionArgString("dp-1"),
				workflow.DataplaneAccountIdKey:    actions.ActionArgString("account-1"),
				workflow.DataplaneCidrKey:         actions.ActionArgString("10.0.0.0/16"),
				workflow.DataplaneServiceCidrKey:  actions.ActionArgString("10.96.0.0/16"),
				workflow.DataplanePCAKey:          actions.ActionArgString(mustMarshalPCA(t, &entity.SharedPoolPCA{CaIdentifier: "arn:aws:acm-pca"})),
				workflow.EnableCapacityPlannerKey: actions.ActionArgString("true"),
				workflow.TenantIdKey:              actions.ActionArgString("tenant-1"),
				utils.TraceIdKey:                  actions.ActionArgString(""),
			},
			bizConfig: &config.BizConfig{AzureResourceGroupName: "rg-1"},
			plan:      entity.PremiumServicePlan,
			setupMocks: func(client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "dp-1"
				})).Return(nil, status.Error(codes.NotFound, "not found"))
				client.On("CreateDataPlane", mock.Anything, mock.Anything).Return(&dplanemgrv1.DataPlane{}, nil)
			},
			validate: func(t *testing.T, createdReq *dplanemgrv1.CreateDataPlaneRequest) {
				t.Helper()
				require.NotNil(t, createdReq)
				assert.Equal(t, "dp-1", createdReq.GetDataPlaneId())
				assert.Equal(t, "account-1", createdReq.GetDataPlane().GetAccountId())
				assert.Equal(t, "10.0.0.0/16", createdReq.GetDataPlane().GetVpc().GetCidr())
				assert.Equal(t, "10.96.0.0/16", createdReq.GetDataPlane().GetKubernetes().GetServiceCidr())
				assert.Equal(t, "rg-1", createdReq.GetDataPlane().GetResourceGroupName())
				assert.Equal(t, "tenant-1", createdReq.GetDataPlane().GetTags()[schemav1.LabelUserTenantKey])
				assert.Equal(t, entity.PremiumServicePlan, createdReq.GetDataPlane().GetTags()[schemav1.LabelServiceTypeKey])
				assert.True(t, createdReq.GetEnableCapacityPlanner())
			},
		},
		{
			name: "create dataplane with byoc service type",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey:          actions.ActionArgString("dp-byoc"),
				workflow.DataplaneAccountIdKey:   actions.ActionArgString("account-1"),
				workflow.DataplaneCidrKey:        actions.ActionArgString("10.0.0.0/16"),
				workflow.DataplaneServiceCidrKey: actions.ActionArgString("10.96.0.0/16"),
				workflow.TenantIdKey:             actions.ActionArgString("tenant-1"),
				utils.TraceIdKey:                 actions.ActionArgString(""),
			},
			plan: "byoc",
			setupMocks: func(client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "dp-byoc"
				})).Return(nil, status.Error(codes.NotFound, "not found"))
				client.On("CreateDataPlane", mock.Anything, mock.Anything).Return(&dplanemgrv1.DataPlane{}, nil)
			},
			validate: func(t *testing.T, createdReq *dplanemgrv1.CreateDataPlaneRequest) {
				t.Helper()
				require.NotNil(t, createdReq)
				assert.Equal(t, "byoc", createdReq.GetDataPlane().GetTags()[schemav1.LabelServiceTypeKey])
			},
		},
		{
			name: "create dataplane applies shared pool custom tags with highest priority",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey:          actions.ActionArgString("dp-custom-tags"),
				workflow.DataplaneAccountIdKey:   actions.ActionArgString("account-1"),
				workflow.DataplaneCidrKey:        actions.ActionArgString("10.0.0.0/16"),
				workflow.DataplaneServiceCidrKey: actions.ActionArgString("10.96.0.0/16"),
				workflow.TenantIdKey:             actions.ActionArgString("tenant-1"),
				workflow.SharedPoolKey: actions.ActionArgString(mustMarshalSharedPool(t, &entity.SharedPool{
					SharedPool: apiv1.SharedPool{
						Id: "sp-1",
						CloudResourceTags: map[string]string{
							"servicetype":   "custom-service-type",
							"cost-center":   "cc-1",
							"business-unit": "serverless",
						},
					},
				})),
				utils.TraceIdKey: actions.ActionArgString(""),
			},
			plan: entity.PremiumServicePlan,
			setupMocks: func(client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "dp-custom-tags"
				})).Return(nil, status.Error(codes.NotFound, "not found"))
				client.On("CreateDataPlane", mock.Anything, mock.Anything).Return(&dplanemgrv1.DataPlane{}, nil)
			},
			validate: func(t *testing.T, createdReq *dplanemgrv1.CreateDataPlaneRequest) {
				t.Helper()
				require.NotNil(t, createdReq)
				tags := createdReq.GetDataPlane().GetTags()
				assert.Equal(t, "tenant-1", tags[schemav1.LabelUserTenantKey])
				assert.Equal(t, "custom-service-type", tags[schemav1.LabelServiceTypeKey])
				assert.Equal(t, "cc-1", tags["tags.tidbcloud.com/cost-center"])
				assert.Equal(t, "serverless", tags["tags.tidbcloud.com/business-unit"])
			},
		},
		{
			name: "create dataplane ignores empty shared pool snapshot",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey:          actions.ActionArgString("dp-empty-shared-pool"),
				workflow.DataplaneAccountIdKey:   actions.ActionArgString("account-1"),
				workflow.DataplaneCidrKey:        actions.ActionArgString("10.0.0.0/16"),
				workflow.DataplaneServiceCidrKey: actions.ActionArgString("10.96.0.0/16"),
				workflow.TenantIdKey:             actions.ActionArgString("tenant-1"),
				workflow.SharedPoolKey:           actions.ActionArgString(""),
				utils.TraceIdKey:                 actions.ActionArgString(""),
			},
			plan: entity.PremiumServicePlan,
			setupMocks: func(client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "dp-empty-shared-pool"
				})).Return(nil, status.Error(codes.NotFound, "not found"))
				client.On("CreateDataPlane", mock.Anything, mock.Anything).Return(&dplanemgrv1.DataPlane{}, nil)
			},
			validate: func(t *testing.T, createdReq *dplanemgrv1.CreateDataPlaneRequest) {
				t.Helper()
				require.NotNil(t, createdReq)
				tags := createdReq.GetDataPlane().GetTags()
				assert.Equal(t, "tenant-1", tags[schemav1.LabelUserTenantKey])
				assert.Equal(t, entity.PremiumServicePlan, tags[schemav1.LabelServiceTypeKey])
			},
		},
		{
			name: "create dataplane with premium reserved service type for topology driven",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey:          actions.ActionArgString("dp-reserved"),
				workflow.DataplaneAccountIdKey:   actions.ActionArgString("account-1"),
				workflow.DataplaneCidrKey:        actions.ActionArgString("10.0.0.0/16"),
				workflow.DataplaneServiceCidrKey: actions.ActionArgString("10.96.0.0/16"),
				workflow.TenantIdKey:             actions.ActionArgString("tenant-1"),
				workflow.CapacityModeKey:         actions.ActionArgString(apiv1.CapacityMode_TOPOLOGY_DRIVEN.String()),
				utils.TraceIdKey:                 actions.ActionArgString(""),
			},
			plan: entity.PremiumServicePlan,
			setupMocks: func(client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "dp-reserved"
				})).Return(nil, status.Error(codes.NotFound, "not found"))
				client.On("CreateDataPlane", mock.Anything, mock.Anything).Return(&dplanemgrv1.DataPlane{}, nil)
			},
			validate: func(t *testing.T, createdReq *dplanemgrv1.CreateDataPlaneRequest) {
				t.Helper()
				require.NotNil(t, createdReq)
				assert.Equal(t, config.PremiumReservedMeteringPlan, createdReq.GetDataPlane().GetTags()[schemav1.LabelServiceTypeKey])
			},
		},
		{
			name: "skip create when dataplane already exists",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey:          actions.ActionArgString("dp-existing"),
				workflow.DataplaneAccountIdKey:   actions.ActionArgString("account-1"),
				workflow.DataplaneCidrKey:        actions.ActionArgString("10.0.0.0/16"),
				workflow.DataplaneServiceCidrKey: actions.ActionArgString("10.96.0.0/16"),
				utils.TraceIdKey:                 actions.ActionArgString(""),
			},
			setupMocks: func(client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "dp-existing"
				})).Return(&dplanemgrv1.DataPlane{Id: "dp-existing"}, nil)
			},
			validate: func(t *testing.T, createdReq *dplanemgrv1.CreateDataPlaneRequest) {
				t.Helper()
				require.Nil(t, createdReq)
			},
		},
		{
			name: "allow missing pca",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey:          actions.ActionArgString("dp-empty-pca"),
				workflow.DataplaneAccountIdKey:   actions.ActionArgString("account-1"),
				workflow.DataplaneCidrKey:        actions.ActionArgString("10.0.0.0/16"),
				workflow.DataplaneServiceCidrKey: actions.ActionArgString("10.96.0.0/16"),
				utils.TraceIdKey:                 actions.ActionArgString(""),
			},
			plan: entity.PremiumServicePlan,
			setupMocks: func(client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "dp-empty-pca"
				})).Return(nil, status.Error(codes.NotFound, "not found"))
				client.On("CreateDataPlane", mock.Anything, mock.Anything).Return(&dplanemgrv1.DataPlane{}, nil)
			},
			validate: func(t *testing.T, createdReq *dplanemgrv1.CreateDataPlaneRequest) {
				t.Helper()
				require.NotNil(t, createdReq)
				assert.Equal(t, entity.PremiumServicePlan, createdReq.GetDataPlane().GetTags()[schemav1.LabelServiceTypeKey])
			},
		},
		{
			name: "premium shared tiproxy dataplane uses essential service type",
			args: actions.ActionArgs{
				workflow.DataplaneIdKey:          actions.ActionArgString("dp-essential"),
				workflow.DataplaneAccountIdKey:   actions.ActionArgString("account-1"),
				workflow.DataplaneCidrKey:        actions.ActionArgString("10.0.0.0/16"),
				workflow.DataplaneServiceCidrKey: actions.ActionArgString("10.96.0.0/16"),
				workflow.TenantIdKey:             actions.ActionArgString("shared-multi-tenant"),
				workflow.SharedPoolDeployTypeKey: actions.ActionArgString(mysqlschema.SharedPoolDeployTypeSharedTiProxy),
				utils.TraceIdKey:                 actions.ActionArgString(""),
			},
			plan: entity.PremiumServicePlan,
			setupMocks: func(client *dataplanemgrmocks.Client) {
				client.On("GetDataPlane", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetDataPlaneRequest) bool {
					return req.GetDataPlaneId() == "dp-essential"
				})).Return(nil, status.Error(codes.NotFound, "not found"))
				client.On("CreateDataPlane", mock.Anything, mock.Anything).Return(&dplanemgrv1.DataPlane{}, nil)
			},
			validate: func(t *testing.T, createdReq *dplanemgrv1.CreateDataPlaneRequest) {
				t.Helper()
				require.NotNil(t, createdReq)
				assert.Equal(t, entity.EssentialServicePlan, createdReq.GetDataPlane().GetTags()[schemav1.LabelServiceTypeKey])
			},
		},
	}

	for i := range tests {
		tt := &tests[i]
		t.Run(tt.name, func(t *testing.T) {
			client := dataplanemgrmocks.NewClient(t)
			tt.setupMocks(client)

			action := NewCreateDataplaneAction(client, tt.bizConfig, tt.plan)

			err := action.Start(tt.args)
			if tt.expectError {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
			if tt.validate != nil {
				tt.validate(t, findCreateDataPlaneRequest(client))
			}
		})
	}
}

func findCreateDataPlaneRequest(client *dataplanemgrmocks.Client) *dplanemgrv1.CreateDataPlaneRequest {
	for i := range client.Calls {
		call := &client.Calls[i]
		if call.Method != "CreateDataPlane" {
			continue
		}
		if len(call.Arguments) < 2 {
			return nil
		}
		req, _ := call.Arguments.Get(1).(*dplanemgrv1.CreateDataPlaneRequest)
		return req
	}
	return nil
}

func mustMarshalPCA(t *testing.T, pca *entity.SharedPoolPCA) string {
	t.Helper()
	data, err := protojson.Marshal(pca)
	require.NoError(t, err)
	return string(data)
}

func mustMarshalSharedPool(t *testing.T, sharedPool *entity.SharedPool) string {
	t.Helper()
	data, err := protojson.Marshal(sharedPool)
	require.NoError(t, err)
	return string(data)
}
