package controller

import (
	"strconv"
	"testing"

	"github.com/argoproj/argo-cd/gitops-engine/pkg/sync"
	synccommon "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
	"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	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"

	"github.com/argoproj/argo-cd/v3/common"
	"github.com/argoproj/argo-cd/v3/controller/testdata"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
	"github.com/argoproj/argo-cd/v3/test"
	"github.com/argoproj/argo-cd/v3/util/argo/diff"
	"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
	"github.com/argoproj/argo-cd/v3/util/settings"
)

func TestPersistRevisionHistory(t *testing.T) {
	app := newFakeApp()
	app.Status.OperationState = nil
	app.Status.History = nil

	defaultProject := &v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: test.FakeArgoCDNamespace,
			Name:      "default",
		},
	}
	data := fakeData{
		apps: []runtime.Object{app, defaultProject},
		manifestResponse: &apiclient.ManifestResponse{
			Manifests: []string{},
			Namespace: test.FakeDestNamespace,
			Server:    test.FakeClusterURL,
			Revision:  "abc123",
		},
		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
	}
	ctrl := newFakeController(t.Context(), &data, nil)

	// Sync with source unspecified
	opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
		Sync: &v1alpha1.SyncOperation{},
	}}
	ctrl.appStateManager.SyncAppState(app, defaultProject, opState)
	// Ensure we record spec.source into sync result
	assert.Equal(t, app.Spec.GetSource(), opState.SyncResult.Source)

	updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
	require.NoError(t, err)
	require.Len(t, updatedApp.Status.History, 1)
	assert.Equal(t, app.Spec.GetSource(), updatedApp.Status.History[0].Source)
	assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
}

func TestPersistManagedNamespaceMetadataState(t *testing.T) {
	app := newFakeApp()
	app.Status.OperationState = nil
	app.Status.History = nil
	app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
		Labels: map[string]string{
			"foo": "bar",
		},
		Annotations: map[string]string{
			"foo": "bar",
		},
	}

	defaultProject := &v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: test.FakeArgoCDNamespace,
			Name:      "default",
		},
	}
	data := fakeData{
		apps: []runtime.Object{app, defaultProject},
		manifestResponse: &apiclient.ManifestResponse{
			Manifests: []string{},
			Namespace: test.FakeDestNamespace,
			Server:    test.FakeClusterURL,
			Revision:  "abc123",
		},
		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
	}
	ctrl := newFakeController(t.Context(), &data, nil)

	// Sync with source unspecified
	opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
		Sync: &v1alpha1.SyncOperation{},
	}}
	ctrl.appStateManager.SyncAppState(app, defaultProject, opState)
	// Ensure we record spec.syncPolicy.managedNamespaceMetadata into sync result
	assert.Equal(t, app.Spec.SyncPolicy.ManagedNamespaceMetadata, opState.SyncResult.ManagedNamespaceMetadata)
}

func TestPersistRevisionHistoryRollback(t *testing.T) {
	app := newFakeApp()
	app.Status.OperationState = nil
	app.Status.History = nil
	defaultProject := &v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: test.FakeArgoCDNamespace,
			Name:      "default",
		},
	}
	data := fakeData{
		apps: []runtime.Object{app, defaultProject},
		manifestResponse: &apiclient.ManifestResponse{
			Manifests: []string{},
			Namespace: test.FakeDestNamespace,
			Server:    test.FakeClusterURL,
			Revision:  "abc123",
		},
		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
	}
	ctrl := newFakeController(t.Context(), &data, nil)

	// Sync with source specified
	source := v1alpha1.ApplicationSource{
		Helm: &v1alpha1.ApplicationSourceHelm{
			Parameters: []v1alpha1.HelmParameter{
				{
					Name:  "test",
					Value: "123",
				},
			},
		},
	}
	opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
		Sync: &v1alpha1.SyncOperation{
			Source: &source,
		},
	}}
	ctrl.appStateManager.SyncAppState(app, defaultProject, opState)
	// Ensure we record opState's source into sync result
	assert.Equal(t, source, opState.SyncResult.Source)

	updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
	require.NoError(t, err)
	assert.Len(t, updatedApp.Status.History, 1)
	assert.Equal(t, source, updatedApp.Status.History[0].Source)
	assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
}

func TestSyncComparisonError(t *testing.T) {
	app := newFakeApp()
	app.Status.OperationState = nil
	app.Status.History = nil

	defaultProject := &v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: test.FakeArgoCDNamespace,
			Name:      "default",
		},
		Spec: v1alpha1.AppProjectSpec{
			SignatureKeys: []v1alpha1.SignatureKey{{KeyID: "test"}}, // nolint:staticcheck
		},
	}
	data := fakeData{
		apps: []runtime.Object{app, defaultProject},
		manifestResponse: &apiclient.ManifestResponse{
			Manifests: []string{},
			Namespace: test.FakeDestNamespace,
			Server:    test.FakeClusterURL,
			Revision:  "abc123",
			SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
				Name:     "GIT/GPG",
				Problems: []string{"Unknown key 'XXX'"},
			}}},
		},
		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
	}
	ctrl := newFakeController(t.Context(), &data, nil)

	// Sync with source unspecified
	opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
		Sync: &v1alpha1.SyncOperation{},
	}}
	t.Setenv("ARGOCD_GPG_ENABLED", "true")
	ctrl.appStateManager.SyncAppState(app, defaultProject, opState)

	conditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{v1alpha1.ApplicationConditionComparisonError: true})
	assert.NotEmpty(t, conditions)
	assert.Equal(t, "abc123", opState.SyncResult.Revision)
}

func TestAppStateManager_SyncAppState(t *testing.T) {
	t.Parallel()

	type fixture struct {
		application *v1alpha1.Application
		project     *v1alpha1.AppProject
		controller  *ApplicationController
	}

	setup := func(liveObjects map[kube.ResourceKey]*unstructured.Unstructured) *fixture {
		app := newFakeApp()
		app.Status.OperationState = nil
		app.Status.History = nil

		if liveObjects == nil {
			liveObjects = make(map[kube.ResourceKey]*unstructured.Unstructured)
		}

		project := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: test.FakeArgoCDNamespace,
				Name:      "default",
			},
			Spec: v1alpha1.AppProjectSpec{
				SignatureKeys: []v1alpha1.SignatureKey{{KeyID: "test"}}, // nolint:staticcheck
				Destinations: []v1alpha1.ApplicationDestination{
					{
						Namespace: "*",
						Server:    "*",
					},
				},
			},
		}
		data := fakeData{
			apps: []runtime.Object{app, project},
			manifestResponse: &apiclient.ManifestResponse{
				Manifests: []string{},
				Namespace: test.FakeDestNamespace,
				Server:    test.FakeClusterURL,
				Revision:  "abc123",
			},
			managedLiveObjs: liveObjects,
		}
		ctrl := newFakeController(t.Context(), &data, nil)

		return &fixture{
			application: app,
			project:     project,
			controller:  ctrl,
		}
	}

	t.Run("will fail the sync if finds shared resources", func(t *testing.T) {
		// given
		t.Parallel()

		sharedObject := kube.MustToUnstructured(&corev1.ConfigMap{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "v1",
				Kind:       "ConfigMap",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name:      "configmap1",
				Namespace: "default",
				Annotations: map[string]string{
					common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:default/configmap1",
				},
			},
		})
		liveObjects := make(map[kube.ResourceKey]*unstructured.Unstructured)
		liveObjects[kube.GetResourceKey(sharedObject)] = sharedObject
		f := setup(liveObjects)

		// Sync with source unspecified
		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
			Sync: &v1alpha1.SyncOperation{
				Source:      &v1alpha1.ApplicationSource{},
				SyncOptions: []string{"FailOnSharedResource=true"},
			},
		}}

		// when
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then
		assert.Equal(t, synccommon.OperationFailed, opState.Phase)
		assert.Contains(t, opState.Message, "ConfigMap/configmap1 is part of applications fake-argocd-ns/my-app and guestbook")
	})
}

func TestSyncWindowDeniesSync(t *testing.T) {
	t.Parallel()

	type fixture struct {
		application *v1alpha1.Application
		project     *v1alpha1.AppProject
		controller  *ApplicationController
	}

	setup := func() *fixture {
		app := newFakeApp()
		app.Status.OperationState = nil
		app.Status.History = nil

		project := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: test.FakeArgoCDNamespace,
				Name:      "default",
			},
			Spec: v1alpha1.AppProjectSpec{
				SyncWindows: v1alpha1.SyncWindows{{
					Kind:         "deny",
					Schedule:     "0 0 * * *",
					Duration:     "24h",
					Clusters:     []string{"*"},
					Namespaces:   []string{"*"},
					Applications: []string{"*"},
				}},
			},
		}
		data := fakeData{
			apps: []runtime.Object{app, project},
			manifestResponse: &apiclient.ManifestResponse{
				Manifests: []string{},
				Namespace: test.FakeDestNamespace,
				Server:    test.FakeClusterURL,
				Revision:  "abc123",
			},
			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
		}
		ctrl := newFakeController(t.Context(), &data, nil)

		return &fixture{
			application: app,
			project:     project,
			controller:  ctrl,
		}
	}

	t.Run("will keep the sync progressing if a sync window prevents the sync", func(t *testing.T) {
		// given a project with an active deny sync window and an operation in progress
		t.Parallel()
		f := setup()
		opMessage := "Sync operation blocked by sync window"

		opState := &v1alpha1.OperationState{
			Operation: v1alpha1.Operation{
				Sync: &v1alpha1.SyncOperation{
					Source: &v1alpha1.ApplicationSource{},
				},
			},
			Phase: synccommon.OperationRunning,
		}
		// when
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then
		assert.Equal(t, synccommon.OperationRunning, opState.Phase)
		assert.Contains(t, opState.Message, opMessage)
	})
}

func TestNormalizeTargetResources(t *testing.T) {
	type fixture struct {
		comparisonResult *comparisonResult
	}
	setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
		t.Helper()
		dc, err := diff.NewDiffConfigBuilder().
			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
			WithNoCache().
			Build()
		require.NoError(t, err)
		live := test.YamlToUnstructured(testdata.LiveDeploymentYaml)
		target := test.YamlToUnstructured(testdata.TargetDeploymentYaml)
		return &fixture{
			&comparisonResult{
				reconciliationResult: sync.ReconciliationResult{
					Live:   []*unstructured.Unstructured{live},
					Target: []*unstructured.Unstructured{target},
				},
				diffConfig: dc,
			},
		}
	}
	t.Run("will modify target resource adding live state in fields it should ignore", func(t *testing.T) {
		// given
		ignore := v1alpha1.ResourceIgnoreDifferences{
			Group:                 "*",
			Kind:                  "*",
			ManagedFieldsManagers: []string{"janitor"},
		}
		ignores := []v1alpha1.ResourceIgnoreDifferences{ignore}
		f := setup(t, ignores)

		// when
		targets, err := normalizeTargetResources(f.comparisonResult)

		// then
		require.NoError(t, err)
		require.Len(t, targets, 1)
		iksmVersion := targets[0].GetAnnotations()["iksm-version"]
		assert.Equal(t, "2.0", iksmVersion)
	})
	t.Run("will not copy live status into the apply target", func(t *testing.T) {
		// given: status is configured as an ignored field (equivalent to the
		// default ignoreResourceStatusField=crd behavior) and the live resource
		// has an in-flight operationState.
		ignore := v1alpha1.ResourceIgnoreDifferences{
			Group:        "*",
			Kind:         "*",
			JSONPointers: []string{"/status"},
		}
		f := setup(t, []v1alpha1.ResourceIgnoreDifferences{ignore})
		live := f.comparisonResult.reconciliationResult.Live[0]
		require.NoError(t, unstructured.SetNestedField(
			live.Object, "Running", "status", "operationState", "phase"))

		// when
		targets, err := normalizeTargetResources(f.comparisonResult)

		// then: live status must not be merged into the target that gets applied,
		// otherwise the SSA sync manager co-owns and freezes operationState.phase.
		require.NoError(t, err)
		require.Len(t, targets, 1)
		_, found, err := unstructured.NestedMap(targets[0].Object, "status")
		require.NoError(t, err)
		assert.False(t, found, "live status must not be merged into the apply target")
	})
	t.Run("will not modify target resource if ignore difference is not configured", func(t *testing.T) {
		// given
		f := setup(t, []v1alpha1.ResourceIgnoreDifferences{})

		// when
		targets, err := normalizeTargetResources(f.comparisonResult)

		// then
		require.NoError(t, err)
		require.Len(t, targets, 1)
		iksmVersion := targets[0].GetAnnotations()["iksm-version"]
		assert.Equal(t, "1.0", iksmVersion)
	})
	t.Run("will remove fields from target if not present in live", func(t *testing.T) {
		ignore := v1alpha1.ResourceIgnoreDifferences{
			Group:        "apps",
			Kind:         "Deployment",
			JSONPointers: []string{"/metadata/annotations/iksm-version"},
		}
		ignores := []v1alpha1.ResourceIgnoreDifferences{ignore}
		f := setup(t, ignores)
		live := f.comparisonResult.reconciliationResult.Live[0]
		unstructured.RemoveNestedField(live.Object, "metadata", "annotations", "iksm-version")

		// when
		targets, err := normalizeTargetResources(f.comparisonResult)

		// then
		require.NoError(t, err)
		require.Len(t, targets, 1)
		_, ok := targets[0].GetAnnotations()["iksm-version"]
		assert.False(t, ok)
	})
	t.Run("will correctly normalize with multiple ignore configurations", func(t *testing.T) {
		// given
		ignores := []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:        "apps",
				Kind:         "Deployment",
				JSONPointers: []string{"/spec/replicas"},
			},
			{
				Group:                 "*",
				Kind:                  "*",
				ManagedFieldsManagers: []string{"janitor"},
			},
		}
		f := setup(t, ignores)

		// when
		targets, err := normalizeTargetResources(f.comparisonResult)

		// then
		require.NoError(t, err)
		require.Len(t, targets, 1)
		normalized := targets[0]
		iksmVersion, ok := normalized.GetAnnotations()["iksm-version"]
		require.True(t, ok)
		assert.Equal(t, "2.0", iksmVersion)
		replicas, ok, err := unstructured.NestedInt64(normalized.Object, "spec", "replicas")
		require.NoError(t, err)
		require.True(t, ok)
		assert.Equal(t, int64(4), replicas)
	})
	t.Run("will keep new array entries not found in live state if not ignored", func(t *testing.T) {
		t.Skip("limitation in the current implementation")
		// given
		ignores := []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:             "apps",
				Kind:              "Deployment",
				JQPathExpressions: []string{".spec.template.spec.containers[] | select(.name == \"guestbook-ui\")"},
			},
		}
		f := setup(t, ignores)
		target := test.YamlToUnstructured(testdata.TargetDeploymentNewEntries)
		f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}

		// when
		targets, err := normalizeTargetResources(f.comparisonResult)

		// then
		require.NoError(t, err)
		require.Len(t, targets, 1)
		containers, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "template", "spec", "containers")
		require.NoError(t, err)
		require.True(t, ok)
		assert.Len(t, containers, 2)
	})
}

func TestNormalizeTargetResourcesWithList(t *testing.T) {
	type fixture struct {
		comparisonResult *comparisonResult
	}
	setupHTTPProxy := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
		t.Helper()
		dc, err := diff.NewDiffConfigBuilder().
			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
			WithNoCache().
			Build()
		require.NoError(t, err)
		live := test.YamlToUnstructured(testdata.LiveHTTPProxy)
		target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
		return &fixture{
			&comparisonResult{
				reconciliationResult: sync.ReconciliationResult{
					Live:   []*unstructured.Unstructured{live},
					Target: []*unstructured.Unstructured{target},
				},
				diffConfig: dc,
			},
		}
	}

	t.Run("will properly ignore nested fields within arrays", func(t *testing.T) {
		// given
		ignores := []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:             "projectcontour.io",
				Kind:              "HTTPProxy",
				JQPathExpressions: []string{".spec.routes[]"},
				// JSONPointers: []string{"/spec/routes"},
			},
		}
		f := setupHTTPProxy(t, ignores)
		target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
		f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}

		// when
		patchedTargets, err := normalizeTargetResources(f.comparisonResult)

		// then
		require.NoError(t, err)
		require.Len(t, f.comparisonResult.reconciliationResult.Live, 1)
		require.Len(t, f.comparisonResult.reconciliationResult.Target, 1)
		require.Len(t, patchedTargets, 1)

		// live should have 1 entry
		require.Len(t, dig(f.comparisonResult.reconciliationResult.Live[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors"), 1)
		// assert some arbitrary field to show `entries[0]` is not an empty object
		require.Equal(t, "sample-header", dig(f.comparisonResult.reconciliationResult.Live[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0, "requestHeader", "headerName"))

		// target has 2 entries
		require.Len(t, dig(f.comparisonResult.reconciliationResult.Target[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries"), 2)
		// assert some arbitrary field to show `entries[0]` is not an empty object
		require.Equal(t, "sample-header", dig(f.comparisonResult.reconciliationResult.Target[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0, "requestHeaderValueMatch", "headers", 0, "name"))

		// It should be *1* entries in the array
		require.Len(t, dig(patchedTargets[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors"), 1)
		// and it should NOT equal an empty object
		require.Len(t, dig(patchedTargets[0].Object, "spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0), 1)
	})
	t.Run("will correctly set array entries if new entries have been added", func(t *testing.T) {
		// given
		ignores := []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:             "apps",
				Kind:              "Deployment",
				JQPathExpressions: []string{".spec.template.spec.containers[].env[] | select(.name == \"SOME_ENV_VAR\")"},
			},
		}
		f := setupHTTPProxy(t, ignores)
		live := test.YamlToUnstructured(testdata.LiveDeploymentEnvVarsYaml)
		target := test.YamlToUnstructured(testdata.TargetDeploymentEnvVarsYaml)
		f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
		f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}

		// when
		targets, err := normalizeTargetResources(f.comparisonResult)

		// then
		require.NoError(t, err)
		require.Len(t, targets, 1)
		containers, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "template", "spec", "containers")
		require.NoError(t, err)
		require.True(t, ok)
		assert.Len(t, containers, 1)

		ports := containers[0].(map[string]any)["ports"].([]any)
		assert.Len(t, ports, 1)

		env := containers[0].(map[string]any)["env"].([]any)
		assert.Len(t, env, 3)

		first := env[0]
		second := env[1]
		third := env[2]

		// Currently the defined order at this time is the insertion order of the target manifest.
		assert.Equal(t, "SOME_ENV_VAR", first.(map[string]any)["name"])
		assert.Equal(t, "some_value", first.(map[string]any)["value"])

		assert.Equal(t, "SOME_OTHER_ENV_VAR", second.(map[string]any)["name"])
		assert.Equal(t, "some_other_value", second.(map[string]any)["value"])

		assert.Equal(t, "YET_ANOTHER_ENV_VAR", third.(map[string]any)["name"])
		assert.Equal(t, "yet_another_value", third.(map[string]any)["value"])
	})

	t.Run("ignore-deployment-image-replicas-changes-additive", func(t *testing.T) {
		// given

		ignores := []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:        "apps",
				Kind:         "Deployment",
				JSONPointers: []string{"/spec/replicas"},
			}, {
				Group:             "apps",
				Kind:              "Deployment",
				JQPathExpressions: []string{".spec.template.spec.containers[].image"},
			},
		}
		f := setupHTTPProxy(t, ignores)
		live := test.YamlToUnstructured(testdata.MinimalImageReplicaDeploymentYaml)
		target := test.YamlToUnstructured(testdata.AdditionalImageReplicaDeploymentYaml)
		f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
		f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}

		// when
		targets, err := normalizeTargetResources(f.comparisonResult)

		// then
		require.NoError(t, err)
		require.Len(t, targets, 1)
		metadata, ok, err := unstructured.NestedMap(targets[0].Object, "metadata")
		require.NoError(t, err)
		require.True(t, ok)
		labels, ok := metadata["labels"].(map[string]any)
		require.True(t, ok)
		assert.Len(t, labels, 2)
		assert.Equal(t, "web", labels["appProcess"])

		spec, ok, err := unstructured.NestedMap(targets[0].Object, "spec")
		require.NoError(t, err)
		require.True(t, ok)

		assert.Equal(t, int64(1), spec["replicas"])

		template, ok := spec["template"].(map[string]any)
		require.True(t, ok)

		tMetadata, ok := template["metadata"].(map[string]any)
		require.True(t, ok)
		tLabels, ok := tMetadata["labels"].(map[string]any)
		require.True(t, ok)
		assert.Len(t, tLabels, 2)
		assert.Equal(t, "web", tLabels["appProcess"])

		tSpec, ok := template["spec"].(map[string]any)
		require.True(t, ok)
		containers, ok, err := unstructured.NestedSlice(tSpec, "containers")
		require.NoError(t, err)
		require.True(t, ok)
		assert.Len(t, containers, 1)

		first := containers[0].(map[string]any)
		assert.Equal(t, "alpine:3", first["image"])

		resources, ok := first["resources"].(map[string]any)
		require.True(t, ok)
		requests, ok := resources["requests"].(map[string]any)
		require.True(t, ok)
		assert.Equal(t, "400m", requests["cpu"])

		env, ok, err := unstructured.NestedSlice(first, "env")
		require.NoError(t, err)
		require.True(t, ok)
		assert.Len(t, env, 1)

		env0 := env[0].(map[string]any)
		assert.Equal(t, "EV", env0["name"])
		assert.Equal(t, "here", env0["value"])
	})
}

// TestNormalizeTargetResourcesPDBSelector reproduces https://github.com/argoproj/argo-cd/issues/18232
// When a PDB (policy/v1) has an ignoreDifferences rule for a matchLabels sub-field and
// RespectIgnoreDifferences=true is set, normalizeTargetResources should only patch the
// ignored field — not clobber the entire selector due to patchStrategy:"replace".
func TestNormalizeTargetResourcesPDBSelector(t *testing.T) {
	setupPDB := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *comparisonResult {
		t.Helper()
		dc, err := diff.NewDiffConfigBuilder().
			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
			WithNoCache().
			Build()
		require.NoError(t, err)
		live := test.YamlToUnstructured(testdata.LivePDBYaml)
		target := test.YamlToUnstructured(testdata.TargetPDBYaml)
		return &comparisonResult{
			reconciliationResult: sync.ReconciliationResult{
				Live:   []*unstructured.Unstructured{live},
				Target: []*unstructured.Unstructured{target},
			},
			diffConfig: dc,
		}
	}

	t.Run("ignoring one matchLabels key should not clobber other selector changes", func(t *testing.T) {
		// User ignores /spec/selector/matchLabels/version but intentionally changes
		// /spec/selector/matchLabels/app from "myapp" to "myapp-v2".
		// With patchStrategy:"replace" on the selector field (policy/v1),
		// normalizeTargetResources should preserve the app label change.
		cr := setupPDB(t, []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:        "policy",
				Kind:         "PodDisruptionBudget",
				JSONPointers: []string{"/spec/selector/matchLabels/version"},
			},
		})

		targets, err := normalizeTargetResources(cr)
		require.NoError(t, err)
		require.Len(t, targets, 1)

		matchLabels, ok, err := unstructured.NestedStringMap(targets[0].Object, "spec", "selector", "matchLabels")
		require.NoError(t, err)
		require.True(t, ok)

		// The "version" label should take the live value (ignored field — respected).
		assert.Equal(t, "v1", matchLabels["version"], "ignored field 'version' should use live value")

		// The "app" label should keep the target value — it is NOT ignored.
		assert.Equal(t, "myapp-v2", matchLabels["app"],
			"non-ignored field 'app' was clobbered by live value; patchStrategy:replace on selector may cause normalizeTargetResources to overwrite the entire selector")
	})

	t.Run("ignoring one matchLabels key should not drop non-ignored sibling fields", func(t *testing.T) {
		// Target has matchExpressions that live does not.
		// Ignoring a matchLabels key should not cause matchExpressions to be
		// dropped due to replace semantics pulling the entire live selector
		// (which lacks matchExpressions) into the target.
		cr := setupPDB(t, []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:        "policy",
				Kind:         "PodDisruptionBudget",
				JSONPointers: []string{"/spec/selector/matchLabels/version"},
			},
		})

		// Add matchExpressions to the target only (not in live).
		target := cr.reconciliationResult.Target[0]
		matchExpr := []any{
			map[string]any{
				"key":      "tier",
				"operator": "In",
				"values":   []any{"frontend"},
			},
		}
		require.NoError(t, unstructured.SetNestedSlice(target.Object, matchExpr, "spec", "selector", "matchExpressions"))

		targets, err := normalizeTargetResources(cr)
		require.NoError(t, err)
		require.Len(t, targets, 1)

		// matchExpressions should be preserved — it was not ignored.
		expr, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "selector", "matchExpressions")
		require.NoError(t, err)
		assert.True(t, ok, "matchExpressions was dropped from target by replace-strategy collateral")
		assert.Len(t, expr, 1)

		// app label should still reflect the target change.
		matchLabels, ok, err := unstructured.NestedStringMap(targets[0].Object, "spec", "selector", "matchLabels")
		require.NoError(t, err)
		require.True(t, ok)
		assert.Equal(t, "myapp-v2", matchLabels["app"])
		assert.Equal(t, "v1", matchLabels["version"])
	})

	t.Run("ignoring one matchLabels key should not add live-only non-ignored selector keys", func(t *testing.T) {
		// Live has an additional non-ignored label ("track") that target does not.
		// Replace semantics should not copy it into the patched target.
		cr := setupPDB(t, []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:        "policy",
				Kind:         "PodDisruptionBudget",
				JSONPointers: []string{"/spec/selector/matchLabels/version"},
			},
		})

		live := cr.reconciliationResult.Live[0]
		matchLabels, ok, err := unstructured.NestedStringMap(live.Object, "spec", "selector", "matchLabels")
		require.NoError(t, err)
		require.True(t, ok)
		matchLabels["track"] = "stable"
		require.NoError(t, unstructured.SetNestedStringMap(live.Object, matchLabels, "spec", "selector", "matchLabels"))

		targets, err := normalizeTargetResources(cr)
		require.NoError(t, err)
		require.Len(t, targets, 1)

		patchedMatchLabels, ok, err := unstructured.NestedStringMap(targets[0].Object, "spec", "selector", "matchLabels")
		require.NoError(t, err)
		require.True(t, ok)

		assert.Equal(t, "myapp-v2", patchedMatchLabels["app"])
		assert.Equal(t, "v1", patchedMatchLabels["version"])
		_, found := patchedMatchLabels["track"]
		assert.False(t, found, "live-only non-ignored selector key was added to target by replace-strategy collateral")
	})

	t.Run("ignoring entire selector should replace target selector with live", func(t *testing.T) {
		cr := setupPDB(t, []v1alpha1.ResourceIgnoreDifferences{
			{
				Group:        "policy",
				Kind:         "PodDisruptionBudget",
				JSONPointers: []string{"/spec/selector"},
			},
		})

		targets, err := normalizeTargetResources(cr)
		require.NoError(t, err)
		require.Len(t, targets, 1)

		matchLabels, ok, err := unstructured.NestedStringMap(targets[0].Object, "spec", "selector", "matchLabels")
		require.NoError(t, err)
		require.True(t, ok)

		// When the entire selector is ignored, both labels should come from live.
		assert.Equal(t, "myapp", matchLabels["app"])
		assert.Equal(t, "v1", matchLabels["version"])
	})
}

func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
	t.Parallel()

	type fixture struct {
		project     *v1alpha1.AppProject
		application *v1alpha1.Application
		cluster     *v1alpha1.Cluster
	}

	setup := func(destinationServiceAccounts []v1alpha1.ApplicationDestinationServiceAccount, destinationNamespace, destinationServerURL, applicationNamespace string) *fixture {
		project := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "argocd-ns",
				Name:      "testProj",
			},
			Spec: v1alpha1.AppProjectSpec{
				DestinationServiceAccounts: destinationServiceAccounts,
			},
		}
		app := &v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: applicationNamespace,
				Name:      "testApp",
			},
			Spec: v1alpha1.ApplicationSpec{
				Project: "testProj",
				Destination: v1alpha1.ApplicationDestination{
					Server:    destinationServerURL,
					Namespace: destinationNamespace,
				},
			},
		}
		cluster := &v1alpha1.Cluster{
			Server: "https://kubernetes.svc.local",
			Name:   "test-cluster",
		}
		return &fixture{
			project:     project,
			application: app,
			cluster:     cluster,
		}
	}

	t.Run("empty destination service accounts", func(t *testing.T) {
		// given an application referring a project with no destination service accounts
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := ""

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
		assert.Equal(t, expectedSA, sa)
		assert.NoError(t, err)
	})

	t.Run("exact match of destination namespace", func(t *testing.T) {
		// given an application referring a project with exactly one destination service account that matches the application destination,
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should be no error and should use the right service account for impersonation
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("exact one match with multiple destination service accounts", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts having one exact match for application destination
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "guestbook",
				DefaultServiceAccount: "guestbook-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "guestbook-test",
				DefaultServiceAccount: "guestbook-test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should be no error and should use the right service account for impersonation
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("first match to be used when multiple matches are available", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts having multiple match for application destination
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa-2",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa-3",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "guestbook",
				DefaultServiceAccount: "guestbook-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should be no error and it should use the first matching service account for impersonation
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("first match to be used when glob pattern is used", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts with glob patterns matching the application destination
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "test*",
				DefaultServiceAccount: "test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa-2",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should not be any error and should use the first matching glob pattern service account for impersonation
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("no match among a valid list", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts with no matches for application destination
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "test1",
				DefaultServiceAccount: "test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "test2",
				DefaultServiceAccount: "test-sa-2",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := ""

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("app destination namespace is empty", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts with empty application destination namespace
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				DefaultServiceAccount: "test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "*",
				DefaultServiceAccount: "test-sa-2",
			},
		}
		destinationNamespace := ""
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:argocd-ns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should not be any error and the service account configured for with empty namespace should be used.
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("match done via catch all glob pattern", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts having a catch all glob pattern
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns1",
				DefaultServiceAccount: "test-sa-2",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "*",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should not be any error and the catch all service account should be returned
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("match done via invalid glob pattern", func(t *testing.T) {
		// given an application referring a project with a destination service account having an invalid glob pattern for namespace
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "e[[a*",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := ""

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there must be an error as the glob pattern is invalid.
		require.ErrorContains(t, err, "invalid glob pattern for destination namespace")
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("sa specified with a namespace", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts having a matching service account specified with its namespace
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "myns:test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "*",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:myns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
		assert.Equal(t, expectedSA, sa)

		// then, there should not be any error and the service account with its namespace should be returned.
		require.NoError(t, err)
	})

	t.Run("app destination name instead of server URL", func(t *testing.T) {
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "*",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)

		// Use destination name instead of server URL
		f.application.Spec.Destination.Server = ""
		f.application.Spec.Destination.Name = f.cluster.Name

		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
		assert.Equal(t, expectedSA, sa)

		// then, there should not be any error and the service account with its namespace should be returned.
		require.NoError(t, err)
	})
}

func TestDeriveServiceAccountMatchingServers(t *testing.T) {
	t.Parallel()

	type fixture struct {
		project     *v1alpha1.AppProject
		application *v1alpha1.Application
		cluster     *v1alpha1.Cluster
	}

	setup := func(destinationServiceAccounts []v1alpha1.ApplicationDestinationServiceAccount, destinationNamespace, destinationServerURL, applicationNamespace string) *fixture {
		project := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "argocd-ns",
				Name:      "testProj",
			},
			Spec: v1alpha1.AppProjectSpec{
				DestinationServiceAccounts: destinationServiceAccounts,
			},
		}
		app := &v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: applicationNamespace,
				Name:      "testApp",
			},
			Spec: v1alpha1.ApplicationSpec{
				Project: "testProj",
				Destination: v1alpha1.ApplicationDestination{
					Server:    destinationServerURL,
					Namespace: destinationNamespace,
				},
			},
		}
		cluster := &v1alpha1.Cluster{
			Server: "https://kubernetes.svc.local",
			Name:   "test-cluster",
		}
		return &fixture{
			project:     project,
			application: app,
			cluster:     cluster,
		}
	}

	t.Run("exact one match with multiple destination service accounts", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts and one exact match for application destination
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "guestbook",
				DefaultServiceAccount: "guestbook-sa",
			},
			{
				Server:                "https://abc.svc.local",
				Namespace:             "guestbook",
				DefaultServiceAccount: "guestbook-test-sa",
			},
			{
				Server:                "https://cde.svc.local",
				Namespace:             "guestbook",
				DefaultServiceAccount: "default-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should not be any error and the right service account must be returned.
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("first match to be used when multiple matches are available", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts and multiple matches for application destination
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa-2",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "guestbook",
				DefaultServiceAccount: "guestbook-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should not be any error and first matching service account should be used
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("first match to be used when glob pattern is used", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts with a matching glob pattern and exact match
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "test*",
				DefaultServiceAccount: "test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa-2",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
		assert.Equal(t, expectedSA, sa)

		// then, there should not be any error and the service account of the glob pattern, being the first match should be returned.
		require.NoError(t, err)
	})

	t.Run("no match among a valid list", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts with no match
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa",
			},
			{
				Server:                "https://abc.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "test-sa-2",
			},
			{
				Server:                "https://cde.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://xyz.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := ""

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, &v1alpha1.Cluster{Server: destinationServerURL})

		// then, there an error with appropriate message must be returned
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("match done via catch all glob pattern", func(t *testing.T) {
		// given an application referring a project with multiple destination service accounts with matching catch all glob pattern
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "testns1",
				DefaultServiceAccount: "test-sa-2",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
			{
				Server:                "*",
				Namespace:             "*",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://localhost:6443"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there should not be any error and the service account of the glob pattern match must be returned.
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("match done via invalid glob pattern", func(t *testing.T) {
		// given an application referring a project with a destination service account having an invalid glob pattern for server
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "e[[a*",
				Namespace:             "test-ns",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := ""

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)

		// then, there must be an error as the glob pattern is invalid.
		require.ErrorContains(t, err, "invalid glob pattern for destination server")
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("sa specified with a namespace", func(t *testing.T) {
		// given app sync impersonation feature is enabled and matching service account is prefixed with a namespace
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://abc.svc.local",
				Namespace:             "testns",
				DefaultServiceAccount: "myns:test-sa",
			},
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "default",
				DefaultServiceAccount: "default-sa",
			},
			{
				Server:                "*",
				Namespace:             "*",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://abc.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:myns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, &v1alpha1.Cluster{Server: destinationServerURL})

		// then, there should not be any error and the service account with the given namespace prefix must be returned.
		require.NoError(t, err)
		assert.Equal(t, expectedSA, sa)
	})

	t.Run("app destination name instead of server URL", func(t *testing.T) {
		t.Parallel()
		destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
			{
				Server:                "https://kubernetes.svc.local",
				Namespace:             "*",
				DefaultServiceAccount: "test-sa",
			},
		}
		destinationNamespace := "testns"
		destinationServerURL := "https://kubernetes.svc.local"
		applicationNamespace := "argocd-ns"
		expectedSA := "system:serviceaccount:testns:test-sa"

		f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)

		// Use destination name instead of server URL
		f.application.Spec.Destination.Server = ""
		f.application.Spec.Destination.Name = f.cluster.Name

		// when
		sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
		assert.Equal(t, expectedSA, sa)

		// then, there should not be any error and the service account with its namespace should be returned.
		require.NoError(t, err)
	})
}

func TestSyncWithImpersonate(t *testing.T) {
	type fixture struct {
		application *v1alpha1.Application
		project     *v1alpha1.AppProject
		controller  *ApplicationController
	}

	setup := func(impersonationEnabled bool, destinationNamespace, serviceAccountName string) *fixture {
		app := newFakeApp()
		app.Status.OperationState = nil
		app.Status.History = nil
		project := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: test.FakeArgoCDNamespace,
				Name:      "default",
			},
			Spec: v1alpha1.AppProjectSpec{
				DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
					{
						Server:                "https://localhost:6443",
						Namespace:             destinationNamespace,
						DefaultServiceAccount: serviceAccountName,
					},
				},
			},
		}
		additionalObjs := []runtime.Object{}
		if serviceAccountName != "" {
			syncServiceAccount := &corev1.ServiceAccount{
				ObjectMeta: metav1.ObjectMeta{
					Name:      serviceAccountName,
					Namespace: test.FakeDestNamespace,
				},
			}
			additionalObjs = append(additionalObjs, syncServiceAccount)
		}
		data := fakeData{
			apps: []runtime.Object{app, project},
			manifestResponse: &apiclient.ManifestResponse{
				Manifests: []string{},
				Namespace: test.FakeDestNamespace,
				Server:    "https://localhost:6443",
				Revision:  "abc123",
			},
			managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{},
			configMapData: map[string]string{
				"application.sync.impersonation.enabled": strconv.FormatBool(impersonationEnabled),
			},
			additionalObjs: additionalObjs,
		}
		ctrl := newFakeController(t.Context(), &data, nil)
		return &fixture{
			application: app,
			project:     project,
			controller:  ctrl,
		}
	}

	t.Run("sync with impersonation and no matching service account", func(t *testing.T) {
		// given app sync impersonation feature is enabled with enforcement (default) and no matching service account
		f := setup(true, test.FakeArgoCDNamespace, "")
		opMessage := "no matching service account found for destination server https://localhost:6443 and namespace fake-dest-ns"

		opState := &v1alpha1.OperationState{
			Operation: v1alpha1.Operation{
				Sync: &v1alpha1.SyncOperation{
					Source: &v1alpha1.ApplicationSource{},
				},
			},
			Phase: synccommon.OperationRunning,
		}
		// when
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then, app sync should fail with expected error message in operation state
		assert.Equal(t, synccommon.OperationError, opState.Phase)
		assert.Contains(t, opState.Message, opMessage)
	})

	t.Run("sync with impersonation and empty service account match", func(t *testing.T) {
		// given app sync impersonation feature is enabled with enforcement (default) and a project with empty service account configured
		f := setup(true, test.FakeDestNamespace, "")
		opMessage := "default service account contains invalid chars"

		opState := &v1alpha1.OperationState{
			Operation: v1alpha1.Operation{
				Sync: &v1alpha1.SyncOperation{
					Source: &v1alpha1.ApplicationSource{},
				},
			},
			Phase: synccommon.OperationRunning,
		}
		// when
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then app sync should fail with expected error message in operation state
		assert.Equal(t, synccommon.OperationError, opState.Phase)
		assert.Contains(t, opState.Message, opMessage)
	})

	t.Run("sync with impersonation and matching sa", func(t *testing.T) {
		// given app sync impersonation feature is enabled with an application referring a project matching service account
		f := setup(true, test.FakeDestNamespace, "test-sa")
		opMessage := "successfully synced (no more tasks)"

		opState := &v1alpha1.OperationState{
			Operation: v1alpha1.Operation{
				Sync: &v1alpha1.SyncOperation{
					Source: &v1alpha1.ApplicationSource{},
				},
			},
			Phase: synccommon.OperationRunning,
		}
		// when
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then app sync should not fail
		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
		assert.Contains(t, opState.Message, opMessage)
	})

	t.Run("sync without impersonation", func(t *testing.T) {
		// given app sync impersonation feature is disabled with an application referring a project matching service account
		f := setup(false, test.FakeDestNamespace, "")
		opMessage := "successfully synced (no more tasks)"

		opState := &v1alpha1.OperationState{
			Operation: v1alpha1.Operation{
				Sync: &v1alpha1.SyncOperation{
					Source: &v1alpha1.ApplicationSource{},
				},
			},
			Phase: synccommon.OperationRunning,
		}
		// when
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then application sync should pass using the control plane service account
		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
		assert.Contains(t, opState.Message, opMessage)
	})

	t.Run("sync with impersonation enabled, enforcement disabled, no matching SA", func(t *testing.T) {
		// given app sync impersonation feature is enabled with enforcement disabled and no matching service account
		app := newFakeApp()
		app.Status.OperationState = nil
		app.Status.History = nil
		project := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: test.FakeArgoCDNamespace,
				Name:      "default",
			},
			Spec: v1alpha1.AppProjectSpec{
				DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
					{
						Server:                "https://localhost:6443",
						Namespace:             test.FakeArgoCDNamespace,
						DefaultServiceAccount: "",
					},
				},
			},
		}
		data := fakeData{
			apps: []runtime.Object{app, project},
			manifestResponse: &apiclient.ManifestResponse{
				Manifests: []string{},
				Namespace: test.FakeDestNamespace,
				Server:    "https://localhost:6443",
				Revision:  "abc123",
			},
			managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{},
			configMapData: map[string]string{
				"application.sync.impersonation.enabled":  strconv.FormatBool(true),
				"application.sync.impersonation.enforced": strconv.FormatBool(false),
			},
			additionalObjs: []runtime.Object{},
		}
		ctrl := newFakeController(t.Context(), &data, nil)
		opMessage := "successfully synced (no more tasks)"

		opState := &v1alpha1.OperationState{
			Operation: v1alpha1.Operation{
				Sync: &v1alpha1.SyncOperation{
					Source: &v1alpha1.ApplicationSource{},
				},
			},
			Phase: synccommon.OperationRunning,
		}
		// when
		ctrl.appStateManager.SyncAppState(app, project, opState)

		// then app sync should succeed with fallback to controller SA
		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
		assert.Contains(t, opState.Message, opMessage)
	})

	t.Run("sync with impersonation enabled, enforcement disabled, matching SA", func(t *testing.T) {
		// given app sync impersonation feature is enabled with enforcement disabled and matching service account
		app := newFakeApp()
		app.Status.OperationState = nil
		app.Status.History = nil
		project := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: test.FakeArgoCDNamespace,
				Name:      "default",
			},
			Spec: v1alpha1.AppProjectSpec{
				DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
					{
						Server:                "https://localhost:6443",
						Namespace:             test.FakeDestNamespace,
						DefaultServiceAccount: "test-sa",
					},
				},
			},
		}
		syncServiceAccount := &corev1.ServiceAccount{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-sa",
				Namespace: test.FakeDestNamespace,
			},
		}
		data := fakeData{
			apps: []runtime.Object{app, project},
			manifestResponse: &apiclient.ManifestResponse{
				Manifests: []string{},
				Namespace: test.FakeDestNamespace,
				Server:    "https://localhost:6443",
				Revision:  "abc123",
			},
			managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{},
			configMapData: map[string]string{
				"application.sync.impersonation.enabled":  strconv.FormatBool(true),
				"application.sync.impersonation.enforced": strconv.FormatBool(false),
			},
			additionalObjs: []runtime.Object{syncServiceAccount},
		}
		ctrl := newFakeController(t.Context(), &data, nil)
		opMessage := "successfully synced (no more tasks)"

		opState := &v1alpha1.OperationState{
			Operation: v1alpha1.Operation{
				Sync: &v1alpha1.SyncOperation{
					Source: &v1alpha1.ApplicationSource{},
				},
			},
			Phase: synccommon.OperationRunning,
		}
		// when
		ctrl.appStateManager.SyncAppState(app, project, opState)

		// then app sync should succeed using impersonation
		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
		assert.Contains(t, opState.Message, opMessage)
	})

	t.Run("app destination name instead of server URL", func(t *testing.T) {
		// given app sync impersonation feature is enabled with an application referring a project matching service account
		f := setup(true, test.FakeDestNamespace, "test-sa")
		opMessage := "successfully synced (no more tasks)"

		opState := &v1alpha1.OperationState{
			Operation: v1alpha1.Operation{
				Sync: &v1alpha1.SyncOperation{
					Source: &v1alpha1.ApplicationSource{},
				},
			},
			Phase: synccommon.OperationRunning,
		}

		f.application.Spec.Destination.Server = ""
		f.application.Spec.Destination.Name = "minikube"

		// when
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then app sync should not fail
		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
		assert.Contains(t, opState.Message, opMessage)
	})
}

func TestClientSideApplyMigration(t *testing.T) {
	t.Parallel()

	type fixture struct {
		application *v1alpha1.Application
		project     *v1alpha1.AppProject
		controller  *ApplicationController
	}

	setup := func(disableMigration bool, customManager string) *fixture {
		app := newFakeApp()
		app.Status.OperationState = nil
		app.Status.History = nil

		// Add sync options
		if disableMigration {
			app.Spec.SyncPolicy.SyncOptions = append(app.Spec.SyncPolicy.SyncOptions, "DisableClientSideApplyMigration=true")
		}

		// Add custom manager annotation if specified
		if customManager != "" {
			app.Annotations = map[string]string{
				"argocd.argoproj.io/client-side-apply-migration-manager": customManager,
			}
		}

		project := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: test.FakeArgoCDNamespace,
				Name:      "default",
			},
		}
		data := fakeData{
			apps: []runtime.Object{app, project},
			manifestResponse: &apiclient.ManifestResponse{
				Manifests: []string{},
				Namespace: test.FakeDestNamespace,
				Server:    test.FakeClusterURL,
				Revision:  "abc123",
			},
			managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
		}
		ctrl := newFakeController(t.Context(), &data, nil)

		return &fixture{
			application: app,
			project:     project,
			controller:  ctrl,
		}
	}

	t.Run("client-side apply migration enabled by default", func(t *testing.T) {
		// given
		t.Parallel()
		f := setup(false, "")

		// when
		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
			Sync: &v1alpha1.SyncOperation{
				Source: &v1alpha1.ApplicationSource{},
			},
		}}
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then
		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
		assert.Contains(t, opState.Message, "successfully synced")
	})

	t.Run("client-side apply migration disabled", func(t *testing.T) {
		// given
		t.Parallel()
		f := setup(true, "")

		// when
		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
			Sync: &v1alpha1.SyncOperation{
				Source: &v1alpha1.ApplicationSource{},
			},
		}}
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then
		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
		assert.Contains(t, opState.Message, "successfully synced")
	})

	t.Run("client-side apply migration with custom manager", func(t *testing.T) {
		// given
		t.Parallel()
		f := setup(false, "my-custom-manager")

		// when
		opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
			Sync: &v1alpha1.SyncOperation{
				Source: &v1alpha1.ApplicationSource{},
			},
		}}
		f.controller.appStateManager.SyncAppState(f.application, f.project, opState)

		// then
		assert.Equal(t, synccommon.OperationSucceeded, opState.Phase)
		assert.Contains(t, opState.Message, "successfully synced")
	})
}

func dig(obj any, path ...any) any {
	i := obj

	for _, segment := range path {
		switch segment := segment.(type) {
		case int:
			i = i.([]any)[segment]
		case string:
			i = i.(map[string]any)[segment]
		default:
			panic("invalid path for object")
		}
	}

	return i
}

func TestValidateSyncPermissions(t *testing.T) {
	t.Parallel()

	newResource := func(group, kind, name, namespace string) *unstructured.Unstructured {
		obj := &unstructured.Unstructured{}
		obj.SetGroupVersionKind(schema.GroupVersionKind{Group: group, Version: "v1", Kind: kind})
		obj.SetName(name)
		obj.SetNamespace(namespace)
		return obj
	}

	project := &v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-project",
			Namespace: "argocd",
		},
		Spec: v1alpha1.AppProjectSpec{
			Destinations: []v1alpha1.ApplicationDestination{
				{Namespace: "default", Server: "*"},
			},
		},
	}

	destCluster := &v1alpha1.Cluster{
		Server: "https://kubernetes.default.svc",
	}

	noopGetClusters := func(_ string) ([]*v1alpha1.Cluster, error) {
		return nil, nil
	}

	t.Run("nil APIResource returns error", func(t *testing.T) {
		t.Parallel()
		un := newResource("apps", "Deployment", "my-deploy", "default")

		err := validateSyncPermissions(project, destCluster, noopGetClusters, un, nil)

		require.Error(t, err)
		assert.Contains(t, err.Error(), "failed to get API resource info for apps/Deployment")
		assert.Contains(t, err.Error(), "unable to verify permissions")
	})

	t.Run("permitted namespaced resource returns no error", func(t *testing.T) {
		t.Parallel()
		un := newResource("", "ConfigMap", "my-cm", "default")
		res := &metav1.APIResource{Name: "configmaps", Namespaced: true}

		err := validateSyncPermissions(project, destCluster, noopGetClusters, un, res)

		assert.NoError(t, err)
	})

	t.Run("group kind not permitted returns error", func(t *testing.T) {
		t.Parallel()
		projectWithDenyList := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "restricted-project",
				Namespace: "argocd",
			},
			Spec: v1alpha1.AppProjectSpec{
				Destinations: []v1alpha1.ApplicationDestination{
					{Namespace: "*", Server: "*"},
				},
				ClusterResourceBlacklist: []v1alpha1.ClusterResourceRestrictionItem{
					{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"},
				},
			},
		}
		un := newResource("rbac.authorization.k8s.io", "ClusterRole", "my-role", "")
		res := &metav1.APIResource{Name: "clusterroles", Namespaced: false}

		err := validateSyncPermissions(projectWithDenyList, destCluster, noopGetClusters, un, res)

		require.Error(t, err)
		assert.Contains(t, err.Error(), "is not permitted in project")
	})

	t.Run("namespace not permitted returns error", func(t *testing.T) {
		t.Parallel()
		un := newResource("", "ConfigMap", "my-cm", "kube-system")
		res := &metav1.APIResource{Name: "configmaps", Namespaced: true}

		err := validateSyncPermissions(project, destCluster, noopGetClusters, un, res)

		require.Error(t, err)
		assert.Contains(t, err.Error(), "namespace kube-system is not permitted in project")
	})

	t.Run("cluster-scoped resource skips namespace check", func(t *testing.T) {
		t.Parallel()
		projectWithClusterResources := &v1alpha1.AppProject{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-project",
				Namespace: "argocd",
			},
			Spec: v1alpha1.AppProjectSpec{
				Destinations: []v1alpha1.ApplicationDestination{
					{Namespace: "default", Server: "*"},
				},
				ClusterResourceWhitelist: []v1alpha1.ClusterResourceRestrictionItem{
					{Group: "*", Kind: "*"},
				},
			},
		}
		un := newResource("", "Namespace", "my-ns", "")
		res := &metav1.APIResource{Name: "namespaces", Namespaced: false}

		err := validateSyncPermissions(projectWithClusterResources, destCluster, noopGetClusters, un, res)

		assert.NoError(t, err)
	})
}
