/*
Copyright the Velero contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package kube

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	corev1api "k8s.io/api/core/v1"
	storagev1api "k8s.io/api/storage/v1"
	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/kubernetes/fake"

	"github.com/vmware-tanzu/velero/pkg/builder"
	velerotest "github.com/vmware-tanzu/velero/pkg/test"
	"github.com/vmware-tanzu/velero/pkg/uploader"
)

func TestNamespaceAndName(t *testing.T) {
	//TODO
}

func TestEnsureNamespaceExistsAndIsReady(t *testing.T) {
	tests := []struct {
		name                          string
		expectNSFound                 bool
		nsPhase                       corev1api.NamespacePhase
		nsDeleting                    bool
		expectCreate                  bool
		alreadyExists                 bool
		expectedResult                bool
		expectedCreatedResult         bool
		nsAlreadyInTerminationTracker bool
		ResourceDeletionStatusTracker ResourceDeletionStatusTracker
	}{
		{
			name:                  "namespace found, not deleting",
			expectNSFound:         true,
			expectedResult:        true,
			expectedCreatedResult: false,
		},
		{
			name:                  "namespace found, terminating phase",
			expectNSFound:         true,
			nsPhase:               corev1api.NamespaceTerminating,
			expectedResult:        false,
			expectedCreatedResult: false,
		},
		{
			name:                  "namespace found, deletiontimestamp set",
			expectNSFound:         true,
			nsDeleting:            true,
			expectedResult:        false,
			expectedCreatedResult: false,
		},
		{
			name:                  "namespace not found, successfully created",
			expectCreate:          true,
			expectedResult:        true,
			expectedCreatedResult: true,
		},
		{
			name:                  "namespace not found initially, create returns already exists error, returned namespace is ready",
			alreadyExists:         true,
			expectedResult:        true,
			expectedCreatedResult: false,
		},
		{
			name:                  "namespace not found initially, create returns already exists error, returned namespace is terminating",
			alreadyExists:         true,
			nsPhase:               corev1api.NamespaceTerminating,
			expectedResult:        false,
			expectedCreatedResult: false,
		},
		{
			name:                          "same namespace found earlier, terminating phase already tracked",
			expectNSFound:                 true,
			nsPhase:                       corev1api.NamespaceTerminating,
			expectedResult:                false,
			expectedCreatedResult:         false,
			nsAlreadyInTerminationTracker: true,
		},
	}

	resourceDeletionStatusTracker := NewResourceDeletionStatusTracker()
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			namespace := &corev1api.Namespace{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
			}

			if test.nsPhase != "" {
				namespace.Status.Phase = test.nsPhase
			}

			if test.nsDeleting {
				namespace.SetDeletionTimestamp(&metav1.Time{Time: time.Now()})
			}

			timeout := time.Millisecond

			nsClient := &velerotest.FakeNamespaceClient{}
			defer nsClient.AssertExpectations(t)

			if test.expectNSFound {
				nsClient.On("Get", "test", metav1.GetOptions{}).Return(namespace, nil)
			} else {
				nsClient.On("Get", "test", metav1.GetOptions{}).Return(&corev1api.Namespace{}, apierrors.NewNotFound(schema.GroupResource{Resource: "namespaces"}, "test"))
			}

			if test.alreadyExists {
				nsClient.On("Create", namespace).Return(namespace, apierrors.NewAlreadyExists(schema.GroupResource{Resource: "namespaces"}, "test"))
			}

			if test.expectCreate {
				nsClient.On("Create", namespace).Return(namespace, nil)
			}

			if test.nsAlreadyInTerminationTracker {
				resourceDeletionStatusTracker.Add(namespace.Kind, "test", "test")
			}

			result, nsCreated, _ := EnsureNamespaceExistsAndIsReady(namespace, nsClient, timeout, resourceDeletionStatusTracker)

			assert.Equal(t, test.expectedResult, result)
			assert.Equal(t, test.expectedCreatedResult, nsCreated)
		})
	}
}

// TestGetVolumeDirectorySuccess tests that the GetVolumeDirectory function
// returns a volume's name or a volume's name plus '/mount' when a PVC is present.
func TestGetVolumeDirectorySuccess(t *testing.T) {
	tests := []struct {
		name string
		pod  *corev1api.Pod
		pvc  *corev1api.PersistentVolumeClaim
		pv   *corev1api.PersistentVolume
		want string
	}{
		{
			name: "Non-CSI volume with a PVC/PV returns the volume's name",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
			pvc:  builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
			pv:   builder.ForPersistentVolume("a-pv").Result(),
			want: "a-pv",
		},
		{
			name: "CSI volume with a PVC/PV appends '/mount' to the volume name",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
			pvc:  builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
			pv:   builder.ForPersistentVolume("a-pv").CSI("csi.test.com", "provider-volume-id").Result(),
			want: "a-pv/mount",
		},
		{
			name: "Block CSI volume with a PVC/PV does not append '/mount' to the volume name",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
			pvc:  builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
			pv:   builder.ForPersistentVolume("a-pv").CSI("csi.test.com", "provider-volume-id").VolumeMode(corev1api.PersistentVolumeBlock).Result(),
			want: "a-pv",
		},
		{
			name: "CSI volume mounted without a PVC appends '/mount' to the volume name",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").CSISource("csi.test.com").Result()).Result(),
			want: "my-vol/mount",
		},
		{
			name: "Non-CSI volume without a PVC returns the volume name",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").Result()).Result(),
			want: "my-vol",
		},
		{
			name: "Volume with CSI annotation appends '/mount' to the volume name",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
			pvc:  builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
			pv:   builder.ForPersistentVolume("a-pv").ObjectMeta(builder.WithAnnotations(KubeAnnDynamicallyProvisioned, "csi.test.com")).Result(),
			want: "a-pv/mount",
		},
		{
			name: "Volume with CSI annotation 'pv.kubernetes.io/migrated-to' appends '/mount' to the volume name",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
			pvc:  builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
			pv:   builder.ForPersistentVolume("a-pv").ObjectMeta(builder.WithAnnotations(KubeAnnMigratedTo, "csi.test.com")).Result(),
			want: "a-pv/mount",
		},
	}

	csiDriver := storagev1api.CSIDriver{
		ObjectMeta: metav1.ObjectMeta{Name: "csi.test.com"},
	}
	for _, tc := range tests {
		objs := []runtime.Object{&csiDriver}
		if tc.pvc != nil {
			objs = append(objs, tc.pvc)
		}
		if tc.pv != nil {
			objs = append(objs, tc.pv)
		}

		fakeKubeClient := fake.NewSimpleClientset(objs...)

		// Function under test
		dir, err := GetVolumeDirectory(t.Context(), logrus.StandardLogger(), tc.pod, tc.pod.Spec.Volumes[0].Name, fakeKubeClient)

		require.NoError(t, err)
		assert.Equal(t, tc.want, dir)
	}
}

// TestGetVolumeModeSuccess tests the GetVolumeMode function
func TestGetVolumeModeSuccess(t *testing.T) {
	tests := []struct {
		name string
		pod  *corev1api.Pod
		pvc  *corev1api.PersistentVolumeClaim
		pv   *corev1api.PersistentVolume
		want uploader.PersistentVolumeMode
	}{
		{
			name: "Filesystem PVC volume",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
			pvc:  builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
			pv:   builder.ForPersistentVolume("a-pv").VolumeMode(corev1api.PersistentVolumeFilesystem).Result(),
			want: uploader.PersistentVolumeFilesystem,
		},
		{
			name: "Block PVC volume",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
			pvc:  builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
			pv:   builder.ForPersistentVolume("a-pv").VolumeMode(corev1api.PersistentVolumeBlock).Result(),
			want: uploader.PersistentVolumeBlock,
		},
		{
			name: "Pod volume without a PVC",
			pod:  builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").Result()).Result(),
			want: uploader.PersistentVolumeFilesystem,
		},
	}

	for _, tc := range tests {
		objs := []runtime.Object{}
		if tc.pvc != nil {
			objs = append(objs, tc.pvc)
		}
		if tc.pv != nil {
			objs = append(objs, tc.pv)
		}

		fakeKubeClient := fake.NewSimpleClientset(objs...)

		// Function under test
		mode, err := GetVolumeMode(t.Context(), logrus.StandardLogger(), tc.pod, tc.pod.Spec.Volumes[0].Name, fakeKubeClient)

		require.NoError(t, err)
		assert.Equal(t, tc.want, mode)
	}
}

func TestIsV1Beta1CRDReady(t *testing.T) {
	tests := []struct {
		name string
		crd  *apiextv1beta1.CustomResourceDefinition
		want bool
	}{
		{
			name: "CRD is not established & not accepting names - not ready",
			crd:  builder.ForCustomResourceDefinitionV1Beta1("MyCRD").Result(),
			want: false,
		},
		{
			name: "CRD is established & not accepting names - not ready",
			crd: builder.ForCustomResourceDefinitionV1Beta1("MyCRD").
				Condition(builder.ForCustomResourceDefinitionV1Beta1Condition().Type(apiextv1beta1.Established).Status(apiextv1beta1.ConditionTrue).Result()).Result(),
			want: false,
		},
		{
			name: "CRD is not established & accepting names - not ready",
			crd: builder.ForCustomResourceDefinitionV1Beta1("MyCRD").
				Condition(builder.ForCustomResourceDefinitionV1Beta1Condition().Type(apiextv1beta1.NamesAccepted).Status(apiextv1beta1.ConditionTrue).Result()).Result(),
			want: false,
		},
		{
			name: "CRD is established & accepting names - ready",
			crd: builder.ForCustomResourceDefinitionV1Beta1("MyCRD").
				Condition(builder.ForCustomResourceDefinitionV1Beta1Condition().Type(apiextv1beta1.Established).Status(apiextv1beta1.ConditionTrue).Result()).
				Condition(builder.ForCustomResourceDefinitionV1Beta1Condition().Type(apiextv1beta1.NamesAccepted).Status(apiextv1beta1.ConditionTrue).Result()).
				Result(),
			want: true,
		},
	}

	for _, tc := range tests {
		result := IsV1Beta1CRDReady(tc.crd)
		assert.Equal(t, tc.want, result)
	}
}

func TestIsV1CRDReady(t *testing.T) {
	tests := []struct {
		name string
		crd  *apiextv1.CustomResourceDefinition
		want bool
	}{
		{
			name: "CRD is not established & not accepting names - not ready",
			crd:  builder.ForV1CustomResourceDefinition("MyCRD").Result(),
			want: false,
		},
		{
			name: "CRD is established & not accepting names - not ready",
			crd: builder.ForV1CustomResourceDefinition("MyCRD").
				Condition(builder.ForV1CustomResourceDefinitionCondition().Type(apiextv1.Established).Status(apiextv1.ConditionTrue).Result()).Result(),
			want: false,
		},
		{
			name: "CRD is not established & accepting names - not ready",
			crd: builder.ForV1CustomResourceDefinition("MyCRD").
				Condition(builder.ForV1CustomResourceDefinitionCondition().Type(apiextv1.NamesAccepted).Status(apiextv1.ConditionTrue).Result()).Result(),
			want: false,
		},
		{
			name: "CRD is established & accepting names - ready",
			crd: builder.ForV1CustomResourceDefinition("MyCRD").
				Condition(builder.ForV1CustomResourceDefinitionCondition().Type(apiextv1.Established).Status(apiextv1.ConditionTrue).Result()).
				Condition(builder.ForV1CustomResourceDefinitionCondition().Type(apiextv1.NamesAccepted).Status(apiextv1.ConditionTrue).Result()).
				Result(),
			want: true,
		},
	}

	for _, tc := range tests {
		result := IsV1CRDReady(tc.crd)
		assert.Equal(t, tc.want, result)
	}
}

func TestIsCRDReady(t *testing.T) {
	v1beta1tests := []struct {
		name string
		crd  *apiextv1beta1.CustomResourceDefinition
		want bool
	}{
		{
			name: "v1beta1CRD is not established & not accepting names - not ready",
			crd:  builder.ForCustomResourceDefinitionV1Beta1("MyCRD").Result(),
			want: false,
		},
		{
			name: "v1beta1CRD is established & not accepting names - not ready",
			crd: builder.ForCustomResourceDefinitionV1Beta1("MyCRD").
				Condition(builder.ForCustomResourceDefinitionV1Beta1Condition().Type(apiextv1beta1.Established).Status(apiextv1beta1.ConditionTrue).Result()).Result(),
			want: false,
		},
		{
			name: "v1beta1CRD is not established & accepting names - not ready",
			crd: builder.ForCustomResourceDefinitionV1Beta1("MyCRD").
				Condition(builder.ForCustomResourceDefinitionV1Beta1Condition().Type(apiextv1beta1.NamesAccepted).Status(apiextv1beta1.ConditionTrue).Result()).Result(),
			want: false,
		},
		{
			name: "v1beta1CRD is established & accepting names - ready",
			crd: builder.ForCustomResourceDefinitionV1Beta1("MyCRD").
				Condition(builder.ForCustomResourceDefinitionV1Beta1Condition().Type(apiextv1beta1.Established).Status(apiextv1beta1.ConditionTrue).Result()).
				Condition(builder.ForCustomResourceDefinitionV1Beta1Condition().Type(apiextv1beta1.NamesAccepted).Status(apiextv1beta1.ConditionTrue).Result()).
				Result(),
			want: true,
		},
	}

	for _, tc := range v1beta1tests {
		m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.crd)
		require.NoError(t, err)
		result, err := IsCRDReady(&unstructured.Unstructured{Object: m})
		require.NoError(t, err)
		assert.Equal(t, tc.want, result)
	}

	v1tests := []struct {
		name string
		crd  *apiextv1.CustomResourceDefinition
		want bool
	}{
		{
			name: "v1CRD is not established & not accepting names - not ready",
			crd:  builder.ForV1CustomResourceDefinition("MyCRD").Result(),
			want: false,
		},
		{
			name: "v1CRD is established & not accepting names - not ready",
			crd: builder.ForV1CustomResourceDefinition("MyCRD").
				Condition(builder.ForV1CustomResourceDefinitionCondition().Type(apiextv1.Established).Status(apiextv1.ConditionTrue).Result()).Result(),
			want: false,
		},
		{
			name: "v1CRD is not established & accepting names - not ready",
			crd: builder.ForV1CustomResourceDefinition("MyCRD").
				Condition(builder.ForV1CustomResourceDefinitionCondition().Type(apiextv1.NamesAccepted).Status(apiextv1.ConditionTrue).Result()).Result(),
			want: false,
		},
		{
			name: "v1CRD is established & accepting names - ready",
			crd: builder.ForV1CustomResourceDefinition("MyCRD").
				Condition(builder.ForV1CustomResourceDefinitionCondition().Type(apiextv1.Established).Status(apiextv1.ConditionTrue).Result()).
				Condition(builder.ForV1CustomResourceDefinitionCondition().Type(apiextv1.NamesAccepted).Status(apiextv1.ConditionTrue).Result()).
				Result(),
			want: true,
		},
	}

	for _, tc := range v1tests {
		m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.crd)
		require.NoError(t, err)
		result, err := IsCRDReady(&unstructured.Unstructured{Object: m})
		require.NoError(t, err)
		assert.Equal(t, tc.want, result)
	}

	// input param is unrecognized
	resBytes := []byte(`
{
	"apiVersion": "apiextensions.k8s.io/v9",
	"kind": "CustomResourceDefinition",
	"metadata": {
		"name": "foos.example.foo.com"
	},
	"spec": {
		"group": "example.foo.com",
		"version": "v1alpha1",
		"scope": "Namespaced",
		"names": {
			"plural": "foos",
			"singular": "foo",
			"kind": "Foo"
		},
		"validation": {
			"openAPIV3Schema": {
				"required": [
					"spec"
				],
				"properties": {
					"spec": {
						"required": [
							"bar"
						],
						"properties": {
							"bar": {
								"type": "integer",
								"minimum": 1
							}
						}
					}
				}
			}
		}
	}
}
`)
	obj := &unstructured.Unstructured{}
	err := json.Unmarshal(resBytes, obj)
	require.NoError(t, err)
	_, err = IsCRDReady(obj)
	assert.Error(t, err)
}

func TestSinglePathMatch(t *testing.T) {
	fakeFS := velerotest.NewFakeFileSystem()
	fakeFS.MkdirAll("testDir1/subpath", 0755)
	fakeFS.MkdirAll("testDir2/subpath", 0755)

	_, err := SinglePathMatch("./*/subpath", fakeFS, logrus.StandardLogger())
	require.ErrorContains(t, err, "expected one matching path")
}

func TestAddAnnotations(t *testing.T) {
	annotationValues := map[string]string{
		"k1": "v1",
		"k2": "v2",
		"k3": "v3",
		"k4": "v4",
		"k5": "v5",
	}
	testCases := []struct {
		name  string
		o     metav1.ObjectMeta
		toAdd map[string]string
	}{
		{
			name: "should create a new annotation map when annotation is nil",
			o: metav1.ObjectMeta{
				Annotations: nil,
			},
			toAdd: annotationValues,
		},
		{
			name: "should add all supplied annotations into empty annotation",
			o: metav1.ObjectMeta{
				Annotations: map[string]string{},
			},
			toAdd: annotationValues,
		},
		{
			name: "should add all supplied annotations to existing annotation",
			o: metav1.ObjectMeta{
				Annotations: map[string]string{
					"k100": "v100",
					"k200": "v200",
					"k300": "v300",
				},
			},
			toAdd: annotationValues,
		},
		{
			name: "should overwrite some existing annotations",
			o: metav1.ObjectMeta{
				Annotations: map[string]string{
					"k100": "v100",
					"k2":   "v200",
					"k300": "v300",
				},
			},
			toAdd: annotationValues,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			AddAnnotations(&tc.o, tc.toAdd)
			for k, v := range tc.toAdd {
				actual, exists := tc.o.Annotations[k]
				assert.True(t, exists)
				assert.Equal(t, v, actual)
			}
		})
	}
}

func TestAddLabels(t *testing.T) {
	labelValues := map[string]string{
		"l1": "v1",
		"l2": "v2",
		"l3": "v3",
		"l4": "v4",
		"l5": "v5",
	}
	testCases := []struct {
		name  string
		o     metav1.ObjectMeta
		toAdd map[string]string
	}{
		{
			name: "should create a new labels map when labels is nil",
			o: metav1.ObjectMeta{
				Labels: nil,
			},
			toAdd: labelValues,
		},
		{
			name: "should add all supplied labels into empty labels",
			o: metav1.ObjectMeta{
				Labels: map[string]string{},
			},
			toAdd: labelValues,
		},
		{
			name: "should add all supplied labels to existing labels",
			o: metav1.ObjectMeta{
				Labels: map[string]string{
					"l100": "v100",
					"l200": "v200",
					"l300": "v300",
				},
			},
			toAdd: labelValues,
		},
		{
			name: "should overwrite some existing labels",
			o: metav1.ObjectMeta{
				Labels: map[string]string{
					"l100": "v100",
					"l2":   "v200",
					"l300": "v300",
				},
			},
			toAdd: labelValues,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			AddLabels(&tc.o, tc.toAdd)
			for k, v := range tc.toAdd {
				actual, exists := tc.o.Labels[k]
				assert.True(t, exists)
				assert.Equal(t, v, actual)
			}
		})
	}
}

func TestHasBackupLabel(t *testing.T) {
	testCases := []struct {
		name       string
		o          metav1.ObjectMeta
		backupName string
		expected   bool
	}{
		{
			name:     "object has no labels",
			o:        metav1.ObjectMeta{},
			expected: false,
		},
		{
			name:       "object has no velero backup label",
			backupName: "csi-b1",
			o: metav1.ObjectMeta{
				Labels: map[string]string{
					"l100": "v100",
					"l2":   "v200",
					"l300": "v300",
				},
			},
			expected: false,
		},
		{
			name:       "object has velero backup label but value not equal to backup name",
			backupName: "csi-b1",
			o: metav1.ObjectMeta{
				Labels: map[string]string{
					"velero.io/backup-name": "does-not-match",
					"l100":                  "v100",
					"l2":                    "v200",
					"l300":                  "v300",
				},
			},
			expected: false,
		},
		{
			name:       "object has backup label with matching backup name value",
			backupName: "does-match",
			o: metav1.ObjectMeta{
				Labels: map[string]string{
					"velero.io/backup-name": "does-match",
					"l100":                  "v100",
					"l2":                    "v200",
					"l300":                  "v300",
				},
			},
			expected: true,
		},
	}

	for _, tc := range testCases {
		actual := HasBackupLabel(&tc.o, tc.backupName)
		assert.Equal(t, tc.expected, actual)
	}
}

func TestVerifyJsonConfigs(t *testing.T) {
	testCases := []struct {
		name          string
		configMapName string
		configMap     *corev1api.ConfigMap
		configType    any
		expectedErr   string
	}{
		{
			name:          "ConfigMap not exist",
			configMapName: "non-exist",
			expectedErr:   "fail to find ConfigMap non-exist: configmaps \"non-exist\" not found",
		},
		{
			name:          "ConfigMap doesn't have data",
			configMapName: "no-data",
			expectedErr:   "data is not available in ConfigMap no-data",
			configMap:     builder.ForConfigMap("velero", "no-data").Result(),
		},
		{
			name:          "ConfigMap data is invalid",
			configMapName: "invalid",
			expectedErr:   "error to unmarshall data from ConfigMap invalid: unexpected end of JSON input",
			configMap:     builder.ForConfigMap("velero", "invalid").Data("global", "{\"podResources\": {\"cpuRequest\": \"100m\", \"cpuLimit\": \"200m\", \"memoryRequest\": \"100Mi\", \"memoryLimit\": \"200Mi\"}, \"keepLatestMaintenanceJobs\": 1}", "other", "{\"podResources\": {\"cpuRequest\": \"100m\", \"cpuLimit\": \"200m\", \"memoryRequest\": \"100Mi\", \"memoryLimit\": \"200Mi\"}, \"keepLatestMaintenanceJobs: 1}").Result(),
		},
		{
			name:          "Normal case",
			configMapName: "normal",
			configMap:     builder.ForConfigMap("velero", "normal").Data("global", "{\"podResources\": {\"cpuRequest\": \"100m\", \"cpuLimit\": \"200m\", \"memoryRequest\": \"100Mi\", \"memoryLimit\": \"200Mi\"}, \"keepLatestMaintenanceJobs\": 1}", "other", "{\"podResources\": {\"cpuRequest\": \"100m\", \"cpuLimit\": \"200m\", \"memoryRequest\": \"100Mi\", \"memoryLimit\": \"200Mi\"}, \"keepLatestMaintenanceJobs\": 1}").Result(),
			configType:    make(map[string]any),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			objects := make([]runtime.Object, 0)
			if tc.configMap != nil {
				objects = append(objects, tc.configMap)
			}
			fakeClient := velerotest.NewFakeControllerRuntimeClient(t, objects...)
			err := VerifyJSONConfigs(t.Context(), "velero", fakeClient, tc.configMapName, tc.configMap)
			if len(tc.expectedErr) > 0 {
				require.EqualError(t, err, tc.expectedErr)
			} else {
				require.NoError(t, err)
			}
		})
	}
}
