package action

import (
	"context"
	"errors"
	"testing"

	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"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"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"google.golang.org/protobuf/encoding/protojson"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	apimeta "k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
)

func TestDeletePlacementPolicyAction(t *testing.T) {
	t.Run("deletes existing placement policy", func(t *testing.T) {
		ctx := context.Background()
		remoteCli := newDeletePlacementPolicyTestClient(t,
			newDeletePlacementPolicyTestCR("tidb123", "1001"),
		)
		dataplaneManager := dataplanemocks.NewManager(t)
		dataplaneManager.EXPECT().GetClient("dp-1").Return(remoteCli, nil).Once()

		action := NewDeletePlacementPolicyAction(dataplaneManager)
		require.NoError(t, action.Invoke(newDeletePlacementPolicyTestArgs(t)))

		err := remoteCli.Get(ctx, types.NamespacedName{Namespace: "tidb123", Name: "1001"}, &v1alpha1.PlacementPolicy{})
		require.True(t, apierrors.IsNotFound(err))
	})

	t.Run("missing placement policy is idempotent", func(t *testing.T) {
		remoteCli := newDeletePlacementPolicyTestClient(t)
		dataplaneManager := dataplanemocks.NewManager(t)
		dataplaneManager.EXPECT().GetClient("dp-1").Return(remoteCli, nil).Once()

		action := NewDeletePlacementPolicyAction(dataplaneManager)
		require.NoError(t, action.Invoke(newDeletePlacementPolicyTestArgs(t)))
	})
}

func TestDeletePlacementPolicyActionHandlesDeleteErrors(t *testing.T) {
	tests := []struct {
		name          string
		deleteErr     error
		expectedError bool
	}{
		{
			name: "missing placement policy API is idempotent",
			deleteErr: &apimeta.NoKindMatchError{
				GroupKind:        schema.GroupKind{Group: v1alpha1.SchemeGroupVersion.Group, Kind: "PlacementPolicy"},
				SearchedVersions: []string{v1alpha1.SchemeGroupVersion.Version},
			},
		},
		{
			name: "forbidden deletion fails",
			deleteErr: apierrors.NewForbidden(
				schema.GroupResource{Group: v1alpha1.SchemeGroupVersion.Group, Resource: "placementpolicies"},
				"1001", errors.New("delete denied"),
			),
			expectedError: true,
		},
		{
			name:          "timed out deletion fails",
			deleteErr:     apierrors.NewTimeoutError("delete timed out", 0),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			remoteCli := interceptor.NewClient(newDeletePlacementPolicyTestClient(t), interceptor.Funcs{
				Delete: func(context.Context, client.WithWatch, client.Object, ...client.DeleteOption) error {
					return tt.deleteErr
				},
			})
			dataplaneManager := dataplanemocks.NewManager(t)
			dataplaneManager.EXPECT().GetClient("dp-1").Return(remoteCli, nil).Once()

			action := NewDeletePlacementPolicyAction(dataplaneManager)
			err := action.Invoke(newDeletePlacementPolicyTestArgs(t))
			if tt.expectedError {
				require.ErrorContains(t, err, tt.deleteErr.Error())
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func newDeletePlacementPolicyTestArgs(t *testing.T) actions.ActionArgs {
	t.Helper()
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:            "123",
			DataplaneName: "dp-1",
		},
	}
	sharedPoolBytes, err := protojson.Marshal(sharedPool)
	require.NoError(t, err)

	return actions.ActionArgs{
		workflow.SharedPoolKey:    actions.ActionArgString(string(sharedPoolBytes)),
		workflow.TiDBClusterIdKey: actions.ActionArgString("1001"),
	}
}

func newDeletePlacementPolicyTestClient(t *testing.T, objects ...client.Object) client.WithWatch {
	t.Helper()
	scheme := runtime.NewScheme()
	require.NoError(t, clientgoscheme.AddToScheme(scheme))
	require.NoError(t, v1alpha1.AddToScheme(scheme))
	return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
}

func newDeletePlacementPolicyTestCR(namespace, name string) *v1alpha1.PlacementPolicy {
	return &v1alpha1.PlacementPolicy{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "core.pingcap.com/v1alpha1",
			Kind:       "PlacementPolicy",
		},
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      name,
		},
	}
}
