package action

import (
	"context"
	"testing"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	tiproxyv1alpha1 "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"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"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/cert"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"google.golang.org/protobuf/encoding/protojson"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	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"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

const (
	cleanUpCertSharedPoolID  = "shared-pool-1"
	cleanUpCertDataplaneName = "dataplane-1"
	cleanUpCertClusterID     = "cluster-1"
)

func TestCleanUpCertAction_DeletesTiProxyCertsWhenCurrentMetadataStaleAndTiProxyGroupGone(t *testing.T) {
	comp := cleanUpCertTiProxyComponent(apiv1.Component_DELETING)
	targetSharedPool := cleanUpCertSharedPool(comp)
	currSharedPool := cleanUpCertSharedPool(cleanUpCertTiProxyComponent(apiv1.Component_AVAILABLE))

	namespace := currSharedPool.GetDataPlaneNamespace()
	compCert := cert.ComponentCert(entity.GetComponentTypeName(comp), comp.GetId(), namespace)
	tiproxyServerCert := &certmanagerv1.Certificate{
		ObjectMeta: metav1.ObjectMeta{
			Name:      cert.ServerCertSecret(entity.GetComponentTypeName(comp), comp.GetId()),
			Namespace: namespace,
		},
	}
	remoteCli := cleanUpCertFakeClient(t,
		compCert,
		cleanUpCertSecret(compCert.GetName(), namespace),
		tiproxyServerCert,
		cleanUpCertSecret(tiproxyServerCert.GetName(), namespace),
	)

	action := newCleanUpCertTestAction(t, currSharedPool, remoteCli)

	err := action.Invoke(cleanUpCertArgs(t, targetSharedPool, comp))
	require.NoError(t, err)

	assertNotFound(t, remoteCli, compCert)
	assertNotFound(t, remoteCli, cleanUpCertSecret(compCert.GetName(), namespace))
	assertNotFound(t, remoteCli, tiproxyServerCert)
	assertNotFound(t, remoteCli, cleanUpCertSecret(tiproxyServerCert.GetName(), namespace))
}

func TestCleanUpCertAction_BlocksWhenCurrentTiProxyGroupStillExists(t *testing.T) {
	comp := cleanUpCertTiProxyComponent(apiv1.Component_DELETING)
	targetSharedPool := cleanUpCertSharedPool(comp)
	currSharedPool := cleanUpCertSharedPool(cleanUpCertTiProxyComponent(apiv1.Component_AVAILABLE))
	remoteCli := cleanUpCertFakeClient(t, &tiproxyv1alpha1.TiProxyGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      entity.GetComponentGroupName(comp),
			Namespace: currSharedPool.GetDataPlaneNamespace(),
		},
	})

	action := newCleanUpCertTestAction(t, currSharedPool, remoteCli)

	err := action.Invoke(cleanUpCertArgs(t, targetSharedPool, comp))
	require.Error(t, err)
	assert.Contains(t, err.Error(), "component is running, skip clean up cert")
}

func newCleanUpCertTestAction(t *testing.T, currSharedPool *entity.SharedPool, remoteCli client.Client) actions.Action {
	t.Helper()

	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	sharedPoolRepo.EXPECT().
		Load(mock.Anything, currSharedPool.GetId(), false).
		Return(currSharedPool, nil)

	dataplaneManager := dataplanemocks.NewManager(t)
	dataplaneManager.EXPECT().
		GetClient(currSharedPool.GetDataplaneName()).
		Return(remoteCli, nil)

	return NewCleanUpCertAction(sharedPoolRepo, dataplaneManager)
}

func cleanUpCertArgs(t *testing.T, sharedPool *entity.SharedPool, comp *entity.Component) actions.ActionArgs {
	t.Helper()

	sharedPoolBytes, err := protojson.Marshal(sharedPool)
	require.NoError(t, err)

	return actions.ActionArgs{
		workflow.SharedPoolKey:     actions.ActionArgString(string(sharedPoolBytes)),
		workflow.ComponentIdsKey:   actions.ActionArgString(comp.GetId()),
		workflow.ComponentTypesKey: actions.ActionArgString(entity.GetComponentTypeName(comp)),
		utils.TraceIdKey:           actions.ActionArgString("trace-id"),
	}
}

func cleanUpCertTiProxyComponent(phase apiv1.Component_Phase) *entity.Component {
	return &entity.Component{
		Id:            cleanUpCertClusterID,
		ComponentType: apiv1.ComponentType_TIPROXY,
		Phase:         phase,
	}
}

func cleanUpCertSharedPool(comps ...*entity.Component) *entity.SharedPool {
	return &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:              cleanUpCertSharedPoolID,
			DataplaneName:   cleanUpCertDataplaneName,
			ComponentGroups: comps,
		},
	}
}

func cleanUpCertFakeClient(t *testing.T, objs ...client.Object) client.Client {
	t.Helper()

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

	return fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(objs...).
		Build()
}

func cleanUpCertSecret(name, namespace string) *corev1.Secret {
	return &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
	}
}

func assertNotFound(t *testing.T, remoteCli client.Client, obj client.Object) {
	t.Helper()

	err := remoteCli.Get(context.Background(), types.NamespacedName{
		Namespace: obj.GetNamespace(),
		Name:      obj.GetName(),
	}, obj)
	require.Error(t, err)
	assert.True(t, apierrors.IsNotFound(err))
}
