package action

import (
	"errors"
	"testing"

	"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/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	dpmgrmocks "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"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
)

func TestCreateKeyspaceRoleAction_Invoke(t *testing.T) {
	t.Run("creates DPM keyspace role with derived params", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", false).Return(tidbCluster, nil).Once()
		client.On("CreateKeyspaceRole", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.CreateKeyspaceRoleRequest) bool {
			role := req.GetKeyspaceRole()
			return req.GetDataplaneId() == "dp-arg" &&
				role.GetKeyspaceName() == "cluster-1" &&
				role.GetExternalId() == "cluster-1" &&
				role.GetBucketId() == "sp-1-us-east-1-internal" &&
				role.GetAllowedPrefix() == "internal/prefix/cluster-1" &&
				role.GetTags()[schemav1.LabelUserTenantKey] == "tenant-1" &&
				role.GetTags()[schemav1.LabelSharedPoolKey] == "sp-1" &&
				role.GetTags()[schemav1.LabelUserClusterKey] == "cluster-1" &&
				role.GetTags()[schemav1.LabelDataplaneKey] == "dp-arg" &&
				role.GetTags()[schemav1.LabelRegionKey] == "us-east-1"
		})).Return(&dplanemgrv1.KeyspaceRole{}, nil).Once()

		action := NewCreateKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("already exists is idempotent", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", false).Return(tidbCluster, nil).Once()
		client.On("CreateKeyspaceRole", mock.Anything, mock.Anything).
			Return(nil, status.Error(codes.AlreadyExists, "already exists")).Once()

		action := NewCreateKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("missing dataplane arg falls back to shared pool", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", false).Return(tidbCluster, nil).Once()
		client.On("CreateKeyspaceRole", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.CreateKeyspaceRoleRequest) bool {
			return req.GetDataplaneId() == "dp-1" &&
				req.GetKeyspaceRole().GetTags()[schemav1.LabelDataplaneKey] == "dp-1"
		})).Return(&dplanemgrv1.KeyspaceRole{}, nil).Once()

		action := NewCreateKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("")))
	})

	t.Run("DPM error retries via action error", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", false).Return(tidbCluster, nil).Once()
		client.On("CreateKeyspaceRole", mock.Anything, mock.Anything).
			Return(nil, status.Error(codes.Internal, "dpm unavailable")).Once()

		action := NewCreateKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		err := action.Invoke(newKeyspaceRoleActionArgs("dp-arg"))
		require.Error(t, err)
		require.Contains(t, err.Error(), "failed to create keyspace role")
	})

	t.Run("TOPOLOGY_DRIVEN shared pool with user keyspace creates DPM keyspace role", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		sharedPool.CapacityMode = apiv1.CapacityMode_TOPOLOGY_DRIVEN
		sharedPool.ComponentGroups = []*apiv1.Component{{
			ComponentType: apiv1.ComponentType_TIDB_WORKER,
			NodeQuota:     &apiv1.NodeQuota{NodeCount: 1},
		}}
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", false).Return(tidbCluster, nil).Once()
		client.On("CreateKeyspaceRole", mock.Anything, mock.Anything).
			Return(&dplanemgrv1.KeyspaceRole{}, nil).Once()

		action := NewCreateKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("TOPOLOGY_DRIVEN shared pool without TiDB worker creates DPM keyspace role", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		sharedPool.CapacityMode = apiv1.CapacityMode_TOPOLOGY_DRIVEN
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", false).Return(newKeyspaceRoleTestCluster(), nil).Once()
		client.On("CreateKeyspaceRole", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.CreateKeyspaceRoleRequest) bool {
			role := req.GetKeyspaceRole()
			return req.GetDataplaneId() == "dp-arg" &&
				role.GetKeyspaceName() == "cluster-1" &&
				role.GetExternalId() == "cluster-1" &&
				role.GetAllowedPrefix() == "internal/prefix/cluster-1"
		})).Return(&dplanemgrv1.KeyspaceRole{}, nil).Once()

		action := NewCreateKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("unsupported provider is skipped", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_GCP)
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()

		action := NewCreateKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("missing internal storage fails", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		sharedPool.InternalStorage = nil
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", false).Return(tidbCluster, nil).Once()

		action := NewCreateKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		err := action.Invoke(newKeyspaceRoleActionArgs("dp-arg"))
		require.Error(t, err)
		require.Contains(t, err.Error(), "internal storage is required")
	})

	t.Run("missing shared pool id fails before repo load", func(t *testing.T) {
		action := NewCreateKeyspaceRoleAction(repomocks.NewSharedPoolRepo(t), repomocks.NewTiDBClusterRepo(t), dpmgrmocks.NewClient(t))
		err := action.Invoke(actions.ActionArgs{
			workflow.TiDBClusterIdKey: actions.ActionArgString("cluster-1"),
		})
		require.Error(t, err)
		require.Contains(t, err.Error(), "failed to get sharedPool id")
	})
}

func TestDeleteKeyspaceRoleAction_Invoke(t *testing.T) {
	t.Run("deletes DPM keyspace role", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_ALICLOUD)
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", true).Return(tidbCluster, nil).Once()
		client.On("DeleteKeyspaceRole", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.DeleteKeyspaceRoleRequest) bool {
			return req.GetDataplaneId() == "dp-arg" && req.GetKeyspaceName() == "cluster-1"
		})).Return(&emptypb.Empty{}, nil).Once()

		action := NewDeleteKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("not found is idempotent", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", true).Return(tidbCluster, nil).Once()
		client.On("DeleteKeyspaceRole", mock.Anything, mock.Anything).
			Return(nil, status.Error(codes.NotFound, "not found")).Once()

		action := NewDeleteKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("DPM error retries via action error", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", true).Return(tidbCluster, nil).Once()
		client.On("DeleteKeyspaceRole", mock.Anything, mock.Anything).
			Return(nil, status.Error(codes.Unavailable, "dpm unavailable")).Once()

		action := NewDeleteKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		err := action.Invoke(newKeyspaceRoleActionArgs("dp-arg"))
		require.Error(t, err)
		require.Contains(t, err.Error(), "failed to delete keyspace role")
	})

	t.Run("TOPOLOGY_DRIVEN shared pool with user keyspace deletes DPM keyspace role", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		sharedPool.CapacityMode = apiv1.CapacityMode_TOPOLOGY_DRIVEN
		sharedPool.ComponentGroups = []*apiv1.Component{{
			ComponentType: apiv1.ComponentType_TIDB_WORKER,
			NodeQuota:     &apiv1.NodeQuota{NodeCount: 1},
		}}
		tidbCluster := newKeyspaceRoleTestCluster()
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", true).Return(tidbCluster, nil).Once()
		client.On("DeleteKeyspaceRole", mock.Anything, mock.Anything).
			Return(&emptypb.Empty{}, nil).Once()

		action := NewDeleteKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("TOPOLOGY_DRIVEN shared pool without TiDB worker deletes DPM keyspace role", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_AWS)
		sharedPool.CapacityMode = apiv1.CapacityMode_TOPOLOGY_DRIVEN
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()
		tidbClusterRepo.On("Load", mock.Anything, "cluster-1", true).Return(newKeyspaceRoleTestCluster(), nil).Once()
		client.On("DeleteKeyspaceRole", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.DeleteKeyspaceRoleRequest) bool {
			return req.GetDataplaneId() == "dp-arg" &&
				req.GetKeyspaceName() == "cluster-1"
		})).Return(&emptypb.Empty{}, nil).Once()

		action := NewDeleteKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("unsupported provider is skipped", func(t *testing.T) {
		sharedPool := newKeyspaceRoleTestSharedPool(apiv1.ProviderType_GCP)
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()

		action := NewDeleteKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		require.NoError(t, action.Invoke(newKeyspaceRoleActionArgs("dp-arg")))
	})

	t.Run("repo load failure returns error", func(t *testing.T) {
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
		client := dpmgrmocks.NewClient(t)

		sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(nil, errors.New("storage failed")).Once()

		action := NewDeleteKeyspaceRoleAction(sharedPoolRepo, tidbClusterRepo, client)
		err := action.Invoke(newKeyspaceRoleActionArgs("dp-arg"))
		require.Error(t, err)
		require.Contains(t, err.Error(), "failed to load sharedPool")
	})
}

func newKeyspaceRoleActionArgs(dataplaneID string) actions.ActionArgs {
	args := actions.ActionArgs{
		workflow.SharedPoolIdKey:  actions.ActionArgString("sp-1"),
		workflow.TiDBClusterIdKey: actions.ActionArgString("cluster-1"),
	}
	if dataplaneID != "" {
		args[workflow.DataplaneIdKey] = actions.ActionArgString(dataplaneID)
	}
	return args
}

func newKeyspaceRoleTestSharedPool(provider apiv1.ProviderType) *entity.SharedPool {
	return &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:            "sp-1",
			DataplaneName: "dp-1",
			Provider: &apiv1.Provider{
				Type:   provider,
				Region: "us-east-1",
			},
			InternalStorage: &apiv1.SharedPool_ObjectStorage{
				Bucket: "internal-bucket",
				Prefix: "internal/prefix",
				Region: "us-east-1",
			},
		},
		TenantId: "tenant-from-shared-pool",
	}
}

func newKeyspaceRoleTestCluster() *entity.TiDBCluster {
	return &entity.TiDBCluster{
		Id:           "cluster-1",
		TenantId:     "tenant-1",
		SharedPoolId: "sp-1",
	}
}
