package controllers

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"testing"
	"time"

	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	kubefake "k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	crtclient "sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/event"

	"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
	"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"

	"github.com/argoproj/argo-cd/v3/applicationset/generators"
	"github.com/argoproj/argo-cd/v3/applicationset/generators/mocks"
	appsetmetrics "github.com/argoproj/argo-cd/v3/applicationset/metrics"
	"github.com/argoproj/argo-cd/v3/applicationset/utils"
	argocommon "github.com/argoproj/argo-cd/v3/common"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	applog "github.com/argoproj/argo-cd/v3/util/app/log"
	"github.com/argoproj/argo-cd/v3/util/db"
	"github.com/argoproj/argo-cd/v3/util/settings"
)

// getDefaultTestClientSet creates a Clientset with the default argo objects
// and objects specified in parameters
func getDefaultTestClientSet(obj ...runtime.Object) *kubefake.Clientset {
	argoCDSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      argocommon.ArgoCDSecretName,
			Namespace: "argocd",
			Labels: map[string]string{
				"app.kubernetes.io/part-of": "argocd",
			},
		},
		Data: map[string][]byte{
			"admin.password":   nil,
			"server.secretkey": nil,
		},
	}

	emptyArgoCDConfigMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      argocommon.ArgoCDConfigMapName,
			Namespace: "argocd",
			Labels: map[string]string{
				"app.kubernetes.io/part-of": "argocd",
			},
		},
		Data: map[string]string{},
	}

	kubeclientset := kubefake.NewClientset(append(obj, emptyArgoCDConfigMap, argoCDSecret)...)
	return kubeclientset
}

func TestCreateOrUpdateInCluster(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	for _, c := range []struct {
		// name is human-readable test name
		name string
		// appSet is the ApplicationSet we are generating resources for
		appSet v1alpha1.ApplicationSet
		// existingApps are the apps that already exist on the cluster
		existingApps []v1alpha1.Application
		// desiredApps are the generated apps to create/update
		desiredApps []v1alpha1.Application
		// expected is what we expect the cluster Applications to look like, after createOrUpdateInCluster
		expected []v1alpha1.Application
	}{
		{
			name: "Create an app that doesn't exist",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
			},
			existingApps: nil,
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{Project: "default"},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "1",
					},
					Spec: v1alpha1.ApplicationSpec{Project: "default"},
				},
			},
		},
		{
			name: "Update an existing app with a different project name",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "test",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
		{
			name: "Create a new app and check it doesn't replace the existing app",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "test",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app2",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app2",
						Namespace:       "namespace",
						ResourceVersion: "1",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
		{
			name: "Ensure that labels and annotations are added (via update) into an exiting application",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:        "app1",
						Namespace:   "namespace",
						Labels:      map[string]string{"label-key": "label-value"},
						Annotations: map[string]string{"annot-key": "annot-value"},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						Labels:          map[string]string{"label-key": "label-value"},
						Annotations:     map[string]string{"annot-key": "annot-value"},
						ResourceVersion: "3",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
		{
			name: "Ensure that labels and annotations are removed from an existing app",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
						Labels:          map[string]string{"label-key": "label-value"},
						Annotations:     map[string]string{"annot-key": "annot-value"},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
		{
			name: "Ensure that status and operation fields are not overridden by an update, when removing labels/annotations",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
						Labels:          map[string]string{"label-key": "label-value"},
						Annotations:     map[string]string{"annot-key": "annot-value"},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
					Status: v1alpha1.ApplicationStatus{
						Resources: []v1alpha1.ResourceStatus{{Name: "sample-name"}},
					},
					Operation: &v1alpha1.Operation{
						Sync: &v1alpha1.SyncOperation{Revision: "sample-revision"},
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
					Status: v1alpha1.ApplicationStatus{
						Resources: []v1alpha1.ResourceStatus{{Name: "sample-name"}},
					},
					Operation: &v1alpha1.Operation{
						Sync: &v1alpha1.SyncOperation{Revision: "sample-revision"},
					},
				},
			},
		},
		{
			name: "Ensure that status and operation fields are not overridden by an update, when removing labels/annotations and adding other fields",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project:     "project",
							Source:      &v1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"},
							Destination: v1alpha1.ApplicationDestination{Server: "server", Namespace: "namespace"},
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
					Status: v1alpha1.ApplicationStatus{
						Resources: []v1alpha1.ResourceStatus{{Name: "sample-name"}},
					},
					Operation: &v1alpha1.Operation{
						Sync: &v1alpha1.SyncOperation{Revision: "sample-revision"},
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:        "app1",
						Namespace:   "namespace",
						Labels:      map[string]string{"label-key": "label-value"},
						Annotations: map[string]string{"annot-key": "annot-value"},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project:     "project",
						Source:      &v1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"},
						Destination: v1alpha1.ApplicationDestination{Server: "server", Namespace: "namespace"},
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						Labels:          map[string]string{"label-key": "label-value"},
						Annotations:     map[string]string{"annot-key": "annot-value"},
						ResourceVersion: "3",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project:     "project",
						Source:      &v1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"},
						Destination: v1alpha1.ApplicationDestination{Server: "server", Namespace: "namespace"},
					},
					Status: v1alpha1.ApplicationStatus{
						Resources: []v1alpha1.ResourceStatus{{Name: "sample-name"}},
					},
					Operation: &v1alpha1.Operation{
						Sync: &v1alpha1.SyncOperation{Revision: "sample-revision"},
					},
				},
			},
		},
		{
			name: "Ensure that argocd notifications state and refresh annotation is preserved from an existing app",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
						Labels:          map[string]string{"label-key": "label-value"},
						Annotations: map[string]string{
							"annot-key":                   "annot-value",
							NotifiedAnnotationKey:         `{"b620d4600c771a6f4cxxxxxxx:on-deployed:[0].y7b5sbwa2Q329JYHxxxxxx-fBs:slack:slack-test":1617144614}`,
							v1alpha1.AnnotationKeyRefresh: string(v1alpha1.RefreshTypeNormal),
						},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
						Annotations: map[string]string{
							NotifiedAnnotationKey:         `{"b620d4600c771a6f4cxxxxxxx:on-deployed:[0].y7b5sbwa2Q329JYHxxxxxx-fBs:slack:slack-test":1617144614}`,
							v1alpha1.AnnotationKeyRefresh: string(v1alpha1.RefreshTypeNormal),
						},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
		{
			name: "Ensure that hydrate annotation is preserved from an existing app",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
						Annotations: map[string]string{
							"annot-key":                   "annot-value",
							v1alpha1.AnnotationKeyHydrate: string(v1alpha1.RefreshTypeNormal),
						},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
						Annotations: map[string]string{
							v1alpha1.AnnotationKeyHydrate: string(v1alpha1.RefreshTypeNormal),
						},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
		{
			name: "Ensure that configured preserved annotations are preserved from an existing app",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
					PreservedFields: &v1alpha1.ApplicationPreservedFields{
						Annotations: []string{"preserved-annot-key"},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
						Annotations: map[string]string{
							"annot-key":           "annot-value",
							"preserved-annot-key": "preserved-annot-value",
						},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
						Annotations: map[string]string{
							"preserved-annot-key": "preserved-annot-value",
						},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
		{
			name: "Ensure that the app spec is normalized before applying",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
							Source: &v1alpha1.ApplicationSource{
								Directory: &v1alpha1.ApplicationSourceDirectory{
									Jsonnet: v1alpha1.ApplicationSourceJsonnet{},
								},
							},
						},
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Source: &v1alpha1.ApplicationSource{
							Directory: &v1alpha1.ApplicationSourceDirectory{
								Jsonnet: v1alpha1.ApplicationSourceJsonnet{},
							},
						},
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "1",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Source:  &v1alpha1.ApplicationSource{
							// Directory and jsonnet block are removed
						},
					},
				},
			},
		},
		{
			// For this use case: https://github.com/argoproj/argo-cd/issues/9101#issuecomment-1191138278
			name: "Ensure that ignored targetRevision difference doesn't cause an update, even if another field changes",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					IgnoreApplicationDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
						{JQPathExpressions: []string{".spec.source.targetRevision"}},
					},
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
							Source: &v1alpha1.ApplicationSource{
								RepoURL:        "https://git.example.com/test-org/test-repo.git",
								TargetRevision: "foo",
							},
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://git.example.com/test-org/test-repo.git",
							TargetRevision: "bar",
						},
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Source: &v1alpha1.ApplicationSource{
							RepoURL: "https://git.example.com/test-org/test-repo.git",
							// The targetRevision is ignored, so this should not be updated.
							TargetRevision: "foo",
							// This should be updated.
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{Name: "hi", Value: "there"},
								},
							},
						},
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Source: &v1alpha1.ApplicationSource{
							RepoURL: "https://git.example.com/test-org/test-repo.git",
							// This is the existing value from the cluster, which should not be updated because the field is ignored.
							TargetRevision: "bar",
							// This was missing on the cluster, so it should be added.
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{Name: "hi", Value: "there"},
								},
							},
						},
					},
				},
			},
		},
		{
			// For this use case: https://github.com/argoproj/argo-cd/pull/14743#issuecomment-1761954799
			name: "ignore parameters added to a multi-source app in the cluster",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					IgnoreApplicationDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
						{JQPathExpressions: []string{`.spec.sources[] | select(.repoURL | contains("test-repo")).helm.parameters`}},
					},
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
							Sources: []v1alpha1.ApplicationSource{
								{
									RepoURL: "https://git.example.com/test-org/test-repo.git",
									Helm: &v1alpha1.ApplicationSourceHelm{
										Values: "foo: bar",
									},
								},
							},
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Sources: []v1alpha1.ApplicationSource{
							{
								RepoURL: "https://git.example.com/test-org/test-repo.git",
								Helm: &v1alpha1.ApplicationSourceHelm{
									Values: "foo: bar",
									Parameters: []v1alpha1.HelmParameter{
										{Name: "hi", Value: "there"},
									},
								},
							},
						},
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Sources: []v1alpha1.ApplicationSource{
							{
								RepoURL: "https://git.example.com/test-org/test-repo.git",
								Helm: &v1alpha1.ApplicationSourceHelm{
									Values: "foo: bar",
								},
							},
						},
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
						// This should not be updated, because reconciliation shouldn't modify the App.
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Sources: []v1alpha1.ApplicationSource{
							{
								RepoURL: "https://git.example.com/test-org/test-repo.git",
								Helm: &v1alpha1.ApplicationSourceHelm{
									Values: "foo: bar",
									Parameters: []v1alpha1.HelmParameter{
										// This existed only in the cluster, but it shouldn't be removed, because the field is ignored.
										{Name: "hi", Value: "there"},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "Demonstrate limitation of MergePatch", // Maybe we can fix this in Argo CD 3.0: https://github.com/argoproj/argo-cd/issues/15975
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					IgnoreApplicationDifferences: v1alpha1.ApplicationSetIgnoreDifferences{
						{JQPathExpressions: []string{`.spec.sources[] | select(.repoURL | contains("test-repo")).helm.parameters`}},
					},
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
							Sources: []v1alpha1.ApplicationSource{
								{
									RepoURL: "https://git.example.com/test-org/test-repo.git",
									Helm: &v1alpha1.ApplicationSourceHelm{
										Values: "new: values",
									},
								},
							},
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Sources: []v1alpha1.ApplicationSource{
							{
								RepoURL: "https://git.example.com/test-org/test-repo.git",
								Helm: &v1alpha1.ApplicationSourceHelm{
									Values: "foo: bar",
									Parameters: []v1alpha1.HelmParameter{
										{Name: "hi", Value: "there"},
									},
								},
							},
						},
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Sources: []v1alpha1.ApplicationSource{
							{
								RepoURL: "https://git.example.com/test-org/test-repo.git",
								Helm: &v1alpha1.ApplicationSourceHelm{
									Values: "new: values",
								},
							},
						},
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Application",
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						Sources: []v1alpha1.ApplicationSource{
							{
								RepoURL: "https://git.example.com/test-org/test-repo.git",
								Helm: &v1alpha1.ApplicationSourceHelm{
									Values: "new: values",
									// The Parameters field got blown away, because the values field changed. MergePatch
									// doesn't merge list items, it replaces the whole list if an item changes.
									// If we eventually add a `name` field to Sources, we can use StrategicMergePatch.
								},
							},
						},
					},
				},
			},
		},
		{
			name: "Ensure that unnormalized live spec does not cause a spurious patch",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
						// Without normalizing the live object, the equality check
						// sees &SyncPolicy{} vs nil and issues an unnecessary patch.
						SyncPolicy: &v1alpha1.SyncPolicy{},
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project:    "project",
						SyncPolicy: nil,
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project:    "project",
						SyncPolicy: &v1alpha1.SyncPolicy{},
					},
				},
			},
		},
		{
			name: "Ensure that argocd pre-delete and post-delete finalizers are preserved from an existing app",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
						Finalizers: []string{
							"non-argo-finalizer",
							v1alpha1.PreDeleteFinalizerName,
							v1alpha1.PreDeleteFinalizerName + "/stage1",
							v1alpha1.PostDeleteFinalizerName,
							v1alpha1.PostDeleteFinalizerName + "/stage2",
						},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "3",
						Finalizers: []string{
							v1alpha1.PreDeleteFinalizerName,
							v1alpha1.PreDeleteFinalizerName + "/stage1",
							v1alpha1.PostDeleteFinalizerName,
							v1alpha1.PostDeleteFinalizerName + "/stage2",
						},
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
	} {
		t.Run(c.name, func(t *testing.T) {
			initObjs := []crtclient.Object{&c.appSet}

			for _, a := range c.existingApps {
				err = controllerutil.SetControllerReference(&c.appSet, &a, scheme)
				require.NoError(t, err)
				initObjs = append(initObjs, &a)
			}

			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			r := ApplicationSetReconciler{
				Client:   client,
				Scheme:   scheme,
				Recorder: record.NewFakeRecorder(len(initObjs) + len(c.expected)),
				Metrics:  metrics,
			}

			err = r.createOrUpdateInCluster(t.Context(), log.NewEntry(log.StandardLogger()), c.appSet, c.desiredApps)
			require.NoError(t, err)

			for _, obj := range c.expected {
				got := &v1alpha1.Application{}
				_ = client.Get(t.Context(), crtclient.ObjectKey{
					Namespace: obj.Namespace,
					Name:      obj.Name,
				}, got)

				err = controllerutil.SetControllerReference(&c.appSet, &obj, r.Scheme)
				assert.Equal(t, obj, *got)
			}
		})
	}
}

func TestCreateOrUpdateInCluster_Concurrent(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	appSet := v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "name",
			Namespace: "namespace",
		},
	}

	t.Run("all apps are created correctly with concurrency > 1", func(t *testing.T) {
		desiredApps := make([]v1alpha1.Application, 5)
		for i := range desiredApps {
			desiredApps[i] = v1alpha1.Application{
				ObjectMeta: metav1.ObjectMeta{
					Name:      fmt.Sprintf("app%d", i),
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSpec{Project: "project"},
			}
		}

		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(&appSet).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			Build()
		metrics := appsetmetrics.NewFakeAppsetMetrics()

		r := ApplicationSetReconciler{
			Client:                       fakeClient,
			Scheme:                       scheme,
			Recorder:                     record.NewFakeRecorder(10),
			Metrics:                      metrics,
			ConcurrentApplicationUpdates: 5,
		}

		err = r.createOrUpdateInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, desiredApps)
		require.NoError(t, err)

		for _, desired := range desiredApps {
			got := &v1alpha1.Application{}
			require.NoError(t, fakeClient.Get(t.Context(), crtclient.ObjectKey{Namespace: desired.Namespace, Name: desired.Name}, got))
			assert.Equal(t, desired.Spec.Project, got.Spec.Project)
		}
	})

	t.Run("non-context errors from concurrent goroutines are collected and one is returned", func(t *testing.T) {
		existingApps := make([]v1alpha1.Application, 5)
		initObjs := []crtclient.Object{&appSet}
		for i := range existingApps {
			existingApps[i] = v1alpha1.Application{
				TypeMeta: metav1.TypeMeta{
					Kind:       application.ApplicationKind,
					APIVersion: "argoproj.io/v1alpha1",
				},
				ObjectMeta: metav1.ObjectMeta{
					Name:            fmt.Sprintf("app%d", i),
					Namespace:       "namespace",
					ResourceVersion: "1",
				},
				Spec: v1alpha1.ApplicationSpec{Project: "old"},
			}
			app := existingApps[i].DeepCopy()
			require.NoError(t, controllerutil.SetControllerReference(&appSet, app, scheme))
			initObjs = append(initObjs, app)
		}

		desiredApps := make([]v1alpha1.Application, 5)
		for i := range desiredApps {
			desiredApps[i] = v1alpha1.Application{
				ObjectMeta: metav1.ObjectMeta{
					Name:      fmt.Sprintf("app%d", i),
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSpec{Project: "new"},
			}
		}

		patchErr := errors.New("some patch error")
		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(initObjs...).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			WithInterceptorFuncs(interceptor.Funcs{
				Patch: func(_ context.Context, _ crtclient.WithWatch, _ crtclient.Object, _ crtclient.Patch, _ ...crtclient.PatchOption) error {
					return patchErr
				},
			}).
			Build()
		metrics := appsetmetrics.NewFakeAppsetMetrics()

		r := ApplicationSetReconciler{
			Client:                       fakeClient,
			Scheme:                       scheme,
			Recorder:                     record.NewFakeRecorder(10),
			Metrics:                      metrics,
			ConcurrentApplicationUpdates: 5,
		}

		err = r.createOrUpdateInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, desiredApps)
		require.ErrorIs(t, err, patchErr)
	})
}

func TestCreateOrUpdateInCluster_ContextCancellation(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	appSet := v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "name",
			Namespace: "namespace",
		},
	}
	existingApp := v1alpha1.Application{
		TypeMeta: metav1.TypeMeta{
			Kind:       application.ApplicationKind,
			APIVersion: "argoproj.io/v1alpha1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:            "app1",
			Namespace:       "namespace",
			ResourceVersion: "1",
		},
		Spec: v1alpha1.ApplicationSpec{Project: "old"},
	}
	desiredApp := v1alpha1.Application{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "app1",
			Namespace: "namespace",
		},
		Spec: v1alpha1.ApplicationSpec{Project: "new"},
	}

	t.Run("context canceled on patch is returned directly", func(t *testing.T) {
		initObjs := []crtclient.Object{&appSet}
		app := existingApp.DeepCopy()
		err = controllerutil.SetControllerReference(&appSet, app, scheme)
		require.NoError(t, err)
		initObjs = append(initObjs, app)

		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(initObjs...).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			WithInterceptorFuncs(interceptor.Funcs{
				Patch: func(_ context.Context, _ crtclient.WithWatch, _ crtclient.Object, _ crtclient.Patch, _ ...crtclient.PatchOption) error {
					return context.Canceled
				},
			}).
			Build()
		metrics := appsetmetrics.NewFakeAppsetMetrics()

		r := ApplicationSetReconciler{
			Client:   fakeClient,
			Scheme:   scheme,
			Recorder: record.NewFakeRecorder(10),
			Metrics:  metrics,
		}

		err = r.createOrUpdateInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, []v1alpha1.Application{desiredApp})
		require.ErrorIs(t, err, context.Canceled)
	})

	t.Run("context deadline exceeded on patch is returned directly", func(t *testing.T) {
		initObjs := []crtclient.Object{&appSet}
		app := existingApp.DeepCopy()
		err = controllerutil.SetControllerReference(&appSet, app, scheme)
		require.NoError(t, err)
		initObjs = append(initObjs, app)

		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(initObjs...).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			WithInterceptorFuncs(interceptor.Funcs{
				Patch: func(_ context.Context, _ crtclient.WithWatch, _ crtclient.Object, _ crtclient.Patch, _ ...crtclient.PatchOption) error {
					return context.DeadlineExceeded
				},
			}).
			Build()
		metrics := appsetmetrics.NewFakeAppsetMetrics()

		r := ApplicationSetReconciler{
			Client:   fakeClient,
			Scheme:   scheme,
			Recorder: record.NewFakeRecorder(10),
			Metrics:  metrics,
		}

		err = r.createOrUpdateInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, []v1alpha1.Application{desiredApp})
		require.ErrorIs(t, err, context.DeadlineExceeded)
	})

	t.Run("non-context error is collected and returned after all goroutines finish", func(t *testing.T) {
		initObjs := []crtclient.Object{&appSet}
		app := existingApp.DeepCopy()
		err = controllerutil.SetControllerReference(&appSet, app, scheme)
		require.NoError(t, err)
		initObjs = append(initObjs, app)

		patchErr := errors.New("some patch error")
		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(initObjs...).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			WithInterceptorFuncs(interceptor.Funcs{
				Patch: func(_ context.Context, _ crtclient.WithWatch, _ crtclient.Object, _ crtclient.Patch, _ ...crtclient.PatchOption) error {
					return patchErr
				},
			}).
			Build()
		metrics := appsetmetrics.NewFakeAppsetMetrics()

		r := ApplicationSetReconciler{
			Client:   fakeClient,
			Scheme:   scheme,
			Recorder: record.NewFakeRecorder(10),
			Metrics:  metrics,
		}

		err = r.createOrUpdateInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, []v1alpha1.Application{desiredApp})
		require.ErrorIs(t, err, patchErr)
	})

	t.Run("context canceled on create is returned directly", func(t *testing.T) {
		initObjs := []crtclient.Object{&appSet}

		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(initObjs...).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			WithInterceptorFuncs(interceptor.Funcs{
				Create: func(_ context.Context, _ crtclient.WithWatch, _ crtclient.Object, _ ...crtclient.CreateOption) error {
					return context.Canceled
				},
			}).
			Build()
		metrics := appsetmetrics.NewFakeAppsetMetrics()

		r := ApplicationSetReconciler{
			Client:   fakeClient,
			Scheme:   scheme,
			Recorder: record.NewFakeRecorder(10),
			Metrics:  metrics,
		}

		newApp := v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{Name: "newapp", Namespace: "namespace"},
			Spec:       v1alpha1.ApplicationSpec{Project: "default"},
		}
		err = r.createOrUpdateInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, []v1alpha1.Application{newApp})
		require.ErrorIs(t, err, context.Canceled)
	})
}

func TestDeleteInCluster_ContextCancellation(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	appSet := v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "name",
			Namespace: "namespace",
		},
	}
	existingApp := v1alpha1.Application{
		TypeMeta: metav1.TypeMeta{
			Kind:       application.ApplicationKind,
			APIVersion: "argoproj.io/v1alpha1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:            "delete-me",
			Namespace:       "namespace",
			ResourceVersion: "1",
		},
		Spec: v1alpha1.ApplicationSpec{Project: "project"},
	}

	makeReconciler := func(t *testing.T, fakeClient crtclient.Client) ApplicationSetReconciler {
		t.Helper()
		kubeclientset := kubefake.NewClientset()
		clusterInformer, err := settings.NewClusterInformer(kubeclientset, "namespace")
		require.NoError(t, err)
		cancel := startAndSyncInformer(t, clusterInformer)
		t.Cleanup(cancel)
		return ApplicationSetReconciler{
			Client:          fakeClient,
			Scheme:          scheme,
			Recorder:        record.NewFakeRecorder(10),
			KubeClientset:   kubeclientset,
			Metrics:         appsetmetrics.NewFakeAppsetMetrics(),
			ClusterInformer: clusterInformer,
		}
	}

	t.Run("context canceled on delete is returned directly", func(t *testing.T) {
		app := existingApp.DeepCopy()
		err = controllerutil.SetControllerReference(&appSet, app, scheme)
		require.NoError(t, err)

		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(&appSet, app).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			WithInterceptorFuncs(interceptor.Funcs{
				Delete: func(_ context.Context, _ crtclient.WithWatch, _ crtclient.Object, _ ...crtclient.DeleteOption) error {
					return context.Canceled
				},
			}).
			Build()

		r := makeReconciler(t, fakeClient)
		err = r.deleteInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, []v1alpha1.Application{})
		require.ErrorIs(t, err, context.Canceled)
	})

	t.Run("context deadline exceeded on delete is returned directly", func(t *testing.T) {
		app := existingApp.DeepCopy()
		err = controllerutil.SetControllerReference(&appSet, app, scheme)
		require.NoError(t, err)

		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(&appSet, app).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			WithInterceptorFuncs(interceptor.Funcs{
				Delete: func(_ context.Context, _ crtclient.WithWatch, _ crtclient.Object, _ ...crtclient.DeleteOption) error {
					return context.DeadlineExceeded
				},
			}).
			Build()

		r := makeReconciler(t, fakeClient)
		err = r.deleteInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, []v1alpha1.Application{})
		require.ErrorIs(t, err, context.DeadlineExceeded)
	})

	t.Run("non-context delete error is collected and returned", func(t *testing.T) {
		app := existingApp.DeepCopy()
		err = controllerutil.SetControllerReference(&appSet, app, scheme)
		require.NoError(t, err)

		deleteErr := errors.New("delete failed")
		fakeClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(&appSet, app).
			WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
			WithInterceptorFuncs(interceptor.Funcs{
				Delete: func(_ context.Context, _ crtclient.WithWatch, _ crtclient.Object, _ ...crtclient.DeleteOption) error {
					return deleteErr
				},
			}).
			Build()

		r := makeReconciler(t, fakeClient)
		err = r.deleteInCluster(t.Context(), log.NewEntry(log.StandardLogger()), appSet, []v1alpha1.Application{})
		require.ErrorIs(t, err, deleteErr)
	})
}

func TestRemoveFinalizerOnInvalidDestination_FinalizerTypes(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	for _, c := range []struct {
		// name is human-readable test name
		name               string
		existingFinalizers []string
		expectedFinalizers []string
	}{
		{
			name:               "no finalizers",
			existingFinalizers: []string{},
			expectedFinalizers: nil,
		},
		{
			name:               "contains only argo finalizer",
			existingFinalizers: []string{v1alpha1.ResourcesFinalizerName},
			expectedFinalizers: nil,
		},
		{
			name:               "contains only non-argo finalizer",
			existingFinalizers: []string{"non-argo-finalizer"},
			expectedFinalizers: []string{"non-argo-finalizer"},
		},
		{
			name:               "contains both argo and non-argo finalizer",
			existingFinalizers: []string{"non-argo-finalizer", v1alpha1.ResourcesFinalizerName},
			expectedFinalizers: []string{"non-argo-finalizer"},
		},
	} {
		t.Run(c.name, func(t *testing.T) {
			appSet := v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			}

			app := v1alpha1.Application{
				ObjectMeta: metav1.ObjectMeta{
					Name:       "app1",
					Finalizers: c.existingFinalizers,
				},
				Spec: v1alpha1.ApplicationSpec{
					Project: "project",
					Source:  &v1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"},
					// Destination is always invalid, for this test:
					Destination: v1alpha1.ApplicationDestination{Name: "my-cluster", Namespace: "namespace"},
				},
			}

			secret := &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "my-secret",
					Namespace: "namespace",
					Labels: map[string]string{
						argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
					},
				},
				Data: map[string][]byte{
					// Since this test requires the cluster to be an invalid destination, we
					// always return a cluster named 'my-cluster2' (different from app 'my-cluster', above)
					"name":   []byte("mycluster2"),
					"server": []byte("https://kubernetes.default.svc"),
					"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
				},
			}

			initObjs := []crtclient.Object{&app, &appSet, secret}

			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()

			objects := append([]runtime.Object{}, secret)
			kubeclientset := kubefake.NewClientset(objects...)
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			settingsMgr := settings.NewSettingsManager(t.Context(), kubeclientset, "argocd")
			// Initialize the settings manager to ensure cluster cache is ready
			_ = settingsMgr.ResyncInformers()
			argodb := db.NewDB("argocd", settingsMgr, kubeclientset)

			clusterInformer, err := settings.NewClusterInformer(kubeclientset, "namespace")
			require.NoError(t, err)

			defer startAndSyncInformer(t, clusterInformer)()

			r := ApplicationSetReconciler{
				Client:        client,
				Scheme:        scheme,
				Recorder:      record.NewFakeRecorder(10),
				KubeClientset: kubeclientset,
				Metrics:       metrics,
				ArgoDB:        argodb,
			}
			clusterList, err := utils.ListClusters(clusterInformer)
			require.NoError(t, err)

			appLog := log.WithFields(applog.GetAppLogFields(&app)).WithField("appSet", "")

			appInputParam := app.DeepCopy()

			err = r.removeFinalizerOnInvalidDestination(t.Context(), appSet, appInputParam, clusterList, appLog)
			require.NoError(t, err)

			retrievedApp := v1alpha1.Application{}
			err = client.Get(t.Context(), crtclient.ObjectKeyFromObject(&app), &retrievedApp)
			require.NoError(t, err)

			// App on the cluster should have the expected finalizers
			assert.ElementsMatch(t, c.expectedFinalizers, retrievedApp.Finalizers)

			// App object passed in as a parameter should have the expected finalizers
			assert.ElementsMatch(t, c.expectedFinalizers, appInputParam.Finalizers)

			bytes, _ := json.MarshalIndent(retrievedApp, "", "  ")
			t.Log("Contents of app after call:", string(bytes))
		})
	}
}

func TestRemoveFinalizerOnInvalidDestination_DestinationTypes(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	for _, c := range []struct {
		// name is human-readable test name
		name                   string
		destinationField       v1alpha1.ApplicationDestination
		expectFinalizerRemoved bool
	}{
		{
			name: "invalid cluster: empty destination",
			destinationField: v1alpha1.ApplicationDestination{
				Namespace: "namespace",
			},
			expectFinalizerRemoved: true,
		},
		{
			name: "invalid cluster: invalid server url",
			destinationField: v1alpha1.ApplicationDestination{
				Namespace: "namespace",
				Server:    "https://1.2.3.4",
			},
			expectFinalizerRemoved: true,
		},
		{
			name: "invalid cluster: invalid cluster name",
			destinationField: v1alpha1.ApplicationDestination{
				Namespace: "namespace",
				Name:      "invalid-cluster",
			},
			expectFinalizerRemoved: true,
		},
		{
			name: "invalid cluster by both valid",
			destinationField: v1alpha1.ApplicationDestination{
				Namespace: "namespace",
				Name:      "mycluster2",
				Server:    "https://kubernetes.default.svc",
			},
			expectFinalizerRemoved: true,
		},
		{
			name: "invalid cluster by both invalid",
			destinationField: v1alpha1.ApplicationDestination{
				Namespace: "namespace",
				Name:      "mycluster3",
				Server:    "https://4.5.6.7",
			},
			expectFinalizerRemoved: true,
		},
		{
			name: "valid cluster by name",
			destinationField: v1alpha1.ApplicationDestination{
				Namespace: "namespace",
				Name:      "mycluster2",
			},
			expectFinalizerRemoved: false,
		},
		{
			name: "valid cluster by server",
			destinationField: v1alpha1.ApplicationDestination{
				Namespace: "namespace",
				Server:    "https://kubernetes.default.svc",
			},
			expectFinalizerRemoved: false,
		},
	} {
		t.Run(c.name, func(t *testing.T) {
			appSet := v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			}

			app := v1alpha1.Application{
				ObjectMeta: metav1.ObjectMeta{
					Name:       "app1",
					Finalizers: []string{v1alpha1.ResourcesFinalizerName},
				},
				Spec: v1alpha1.ApplicationSpec{
					Project:     "project",
					Source:      &v1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"},
					Destination: c.destinationField,
				},
			}

			secret := &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "my-secret",
					Namespace: "argocd",
					Labels: map[string]string{
						argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
					},
				},
				Data: map[string][]byte{
					// Since this test requires the cluster to be an invalid destination, we
					// always return a cluster named 'my-cluster2' (different from app 'my-cluster', above)
					"name":   []byte("mycluster2"),
					"server": []byte("https://kubernetes.default.svc"),
					"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
				},
			}

			initObjs := []crtclient.Object{&app, &appSet, secret}

			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()

			kubeclientset := getDefaultTestClientSet(secret)
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			settingsMgr := settings.NewSettingsManager(t.Context(), kubeclientset, "argocd")
			// Initialize the settings manager to ensure cluster cache is ready
			_ = settingsMgr.ResyncInformers()
			argodb := db.NewDB("argocd", settingsMgr, kubeclientset)

			clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
			require.NoError(t, err)

			defer startAndSyncInformer(t, clusterInformer)()

			r := ApplicationSetReconciler{
				Client:        client,
				Scheme:        scheme,
				Recorder:      record.NewFakeRecorder(10),
				KubeClientset: kubeclientset,
				Metrics:       metrics,
				ArgoDB:        argodb,
			}

			clusterList, err := utils.ListClusters(clusterInformer)
			require.NoError(t, err)

			appLog := log.WithFields(applog.GetAppLogFields(&app)).WithField("appSet", "")

			appInputParam := app.DeepCopy()

			err = r.removeFinalizerOnInvalidDestination(t.Context(), appSet, appInputParam, clusterList, appLog)
			require.NoError(t, err)

			retrievedApp := v1alpha1.Application{}
			err = client.Get(t.Context(), crtclient.ObjectKeyFromObject(&app), &retrievedApp)
			require.NoError(t, err)

			finalizerRemoved := len(retrievedApp.Finalizers) == 0

			assert.Equal(t, c.expectFinalizerRemoved, finalizerRemoved)

			bytes, _ := json.MarshalIndent(retrievedApp, "", "  ")
			t.Log("Contents of app after call:", string(bytes))
		})
	}
}

func TestRemoveOwnerReferencesOnDeleteAppSet(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	for _, c := range []struct {
		// name is human-readable test name
		name string
	}{
		{
			name: "ownerReferences cleared",
		},
	} {
		t.Run(c.name, func(t *testing.T) {
			appSet := v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:       "name",
					Namespace:  "namespace",
					Finalizers: []string{v1alpha1.ResourcesFinalizerName},
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			}

			app := v1alpha1.Application{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "app1",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSpec{
					Project: "project",
					Source:  &v1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"},
					Destination: v1alpha1.ApplicationDestination{
						Namespace: "namespace",
						Server:    "https://kubernetes.default.svc",
					},
				},
			}

			err := controllerutil.SetControllerReference(&appSet, &app, scheme)
			require.NoError(t, err)

			initObjs := []crtclient.Object{&app, &appSet}

			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			r := ApplicationSetReconciler{
				Client:        client,
				Scheme:        scheme,
				Recorder:      record.NewFakeRecorder(10),
				KubeClientset: nil,
				Metrics:       metrics,
			}

			err = r.removeOwnerReferencesOnDeleteAppSet(t.Context(), appSet)
			require.NoError(t, err)

			retrievedApp := v1alpha1.Application{}
			err = client.Get(t.Context(), crtclient.ObjectKeyFromObject(&app), &retrievedApp)
			require.NoError(t, err)

			ownerReferencesRemoved := len(retrievedApp.OwnerReferences) == 0
			assert.True(t, ownerReferencesRemoved)
		})
	}
}

func TestCreateApplications(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	testCases := []struct {
		name       string
		appSet     v1alpha1.ApplicationSet
		existsApps []v1alpha1.Application
		apps       []v1alpha1.Application
		expected   []v1alpha1.Application
	}{
		{
			name: "no existing apps",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
			},
			existsApps: nil,
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "1",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "default",
					},
				},
			},
		},
		{
			name: "existing apps",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existsApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "test",
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app1",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "test",
					},
				},
			},
		},
		{
			name: "existing apps with different project",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existsApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app1",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "test",
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "app2",
						Namespace: "namespace",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "app2",
						Namespace:       "namespace",
						ResourceVersion: "1",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
	}

	for _, c := range testCases {
		t.Run(c.name, func(t *testing.T) {
			initObjs := []crtclient.Object{&c.appSet}
			for _, a := range c.existsApps {
				err = controllerutil.SetControllerReference(&c.appSet, &a, scheme)
				require.NoError(t, err)
				initObjs = append(initObjs, &a)
			}

			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			r := ApplicationSetReconciler{
				Client:   client,
				Scheme:   scheme,
				Recorder: record.NewFakeRecorder(len(initObjs) + len(c.expected)),
				Metrics:  metrics,
			}

			err = r.createInCluster(t.Context(), log.NewEntry(log.StandardLogger()), c.appSet, c.apps)
			require.NoError(t, err)

			for _, obj := range c.expected {
				got := &v1alpha1.Application{}
				_ = client.Get(t.Context(), crtclient.ObjectKey{
					Namespace: obj.Namespace,
					Name:      obj.Name,
				}, got)

				err = controllerutil.SetControllerReference(&c.appSet, &obj, r.Scheme)
				require.NoError(t, err)

				assert.Equal(t, obj, *got)
			}
		})
	}
}

func TestDeleteInCluster(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	for _, c := range []struct {
		// appSet is the application set on which the delete function is called
		appSet v1alpha1.ApplicationSet
		// existingApps is the current state of Applications on the cluster
		existingApps []v1alpha1.Application
		// desireApps is the apps generated by the generator that we wish to keep alive
		desiredApps []v1alpha1.Application
		// expected is the list of applications that we expect to exist after calling delete
		expected []v1alpha1.Application
		// notExpected is the list of applications that we expect not to exist after calling delete
		notExpected []v1alpha1.Application
	}{
		{
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "namespace",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Template: v1alpha1.ApplicationSetTemplate{
						Spec: v1alpha1.ApplicationSpec{
							Project: "project",
						},
					},
				},
			},
			existingApps: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "delete",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "keep",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "keep",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			expected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "keep",
						Namespace:       "namespace",
						ResourceVersion: "2",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
			notExpected: []v1alpha1.Application{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       application.ApplicationKind,
						APIVersion: "argoproj.io/v1alpha1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:            "delete",
						Namespace:       "namespace",
						ResourceVersion: "1",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "project",
					},
				},
			},
		},
	} {
		initObjs := []crtclient.Object{&c.appSet}
		for _, a := range c.existingApps {
			temp := a
			err = controllerutil.SetControllerReference(&c.appSet, &temp, scheme)
			require.NoError(t, err)
			initObjs = append(initObjs, &temp)
		}

		client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
		metrics := appsetmetrics.NewFakeAppsetMetrics()

		kubeclientset := kubefake.NewClientset()
		clusterInformer, err := settings.NewClusterInformer(kubeclientset, "namespace")
		require.NoError(t, err)

		defer startAndSyncInformer(t, clusterInformer)()

		r := ApplicationSetReconciler{
			Client:          client,
			Scheme:          scheme,
			Recorder:        record.NewFakeRecorder(len(initObjs) + len(c.expected)),
			KubeClientset:   kubeclientset,
			Metrics:         metrics,
			ClusterInformer: clusterInformer,
		}

		err = r.deleteInCluster(t.Context(), log.NewEntry(log.StandardLogger()), c.appSet, c.desiredApps)
		require.NoError(t, err)

		// For each of the expected objects, verify they exist on the cluster
		for _, obj := range c.expected {
			got := &v1alpha1.Application{}
			_ = client.Get(t.Context(), crtclient.ObjectKey{
				Namespace: obj.Namespace,
				Name:      obj.Name,
			}, got)

			err = controllerutil.SetControllerReference(&c.appSet, &obj, r.Scheme)
			require.NoError(t, err)

			assert.Equal(t, obj, *got)
		}

		// Verify each of the unexpected objs cannot be found
		for _, obj := range c.notExpected {
			got := &v1alpha1.Application{}
			err := client.Get(t.Context(), crtclient.ObjectKey{
				Namespace: obj.Namespace,
				Name:      obj.Name,
			}, got)

			assert.EqualError(t, err, fmt.Sprintf("applications.argoproj.io %q not found", obj.Name))
		}
	}
}

func TestGetMinRequeueAfter(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	client := fake.NewClientBuilder().WithScheme(scheme).Build()
	metrics := appsetmetrics.NewFakeAppsetMetrics()

	generator := v1alpha1.ApplicationSetGenerator{
		List:     &v1alpha1.ListGenerator{},
		Git:      &v1alpha1.GitGenerator{},
		Clusters: &v1alpha1.ClusterGenerator{},
	}

	generatorMock0 := &mocks.Generator{}
	generatorMock0.EXPECT().GetRequeueAfter(&generator).
		Return(generators.NoRequeueAfter)

	generatorMock1 := &mocks.Generator{}
	generatorMock1.EXPECT().GetRequeueAfter(&generator).
		Return(time.Duration(1) * time.Second)

	generatorMock10 := &mocks.Generator{}
	generatorMock10.EXPECT().GetRequeueAfter(&generator).
		Return(time.Duration(10) * time.Second)

	r := ApplicationSetReconciler{
		Client:   client,
		Scheme:   scheme,
		Recorder: record.NewFakeRecorder(0),
		Metrics:  metrics,
		Generators: map[string]generators.Generator{
			"List":     generatorMock10,
			"Git":      generatorMock1,
			"Clusters": generatorMock1,
		},
	}

	got := r.getMinRequeueAfter(&v1alpha1.ApplicationSet{
		Spec: v1alpha1.ApplicationSetSpec{
			Generators: []v1alpha1.ApplicationSetGenerator{generator},
		},
	})

	assert.Equal(t, time.Duration(1)*time.Second, got)
}

func TestRequeueGeneratorFails(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	appSet := v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "name",
			Namespace: "argocd",
		},
		Spec: v1alpha1.ApplicationSetSpec{
			Generators: []v1alpha1.ApplicationSetGenerator{{
				PullRequest: &v1alpha1.PullRequestGenerator{},
			}},
		},
	}
	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet).Build()

	generator := v1alpha1.ApplicationSetGenerator{
		PullRequest: &v1alpha1.PullRequestGenerator{},
	}

	generatorMock := &mocks.Generator{}
	generatorMock.EXPECT().GetTemplate(&generator).
		Return(&v1alpha1.ApplicationSetTemplate{})
	generatorMock.EXPECT().GenerateParams(&generator, mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).
		Return([]map[string]any{}, errors.New("Simulated error generating params that could be related to an external service/API call"))

	metrics := appsetmetrics.NewFakeAppsetMetrics()

	r := ApplicationSetReconciler{
		Client:   client,
		Scheme:   scheme,
		Recorder: record.NewFakeRecorder(0),
		Generators: map[string]generators.Generator{
			"PullRequest": generatorMock,
		},
		Metrics: metrics,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Namespace: "argocd",
			Name:      "name",
		},
	}

	res, err := r.Reconcile(t.Context(), req)
	require.NoError(t, err)
	assert.Equal(t, ReconcileRequeueOnValidationError, res.RequeueAfter)
}

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

	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	// Valid project
	myProject := &v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "namespace"},
		Spec: v1alpha1.AppProjectSpec{
			SourceRepos: []string{"*"},
			Destinations: []v1alpha1.ApplicationDestination{
				{
					Namespace: "*",
					Server:    "*",
				},
			},
			ClusterResourceWhitelist: []v1alpha1.ClusterResourceRestrictionItem{
				{
					Group: "*",
					Kind:  "*",
				},
			},
		},
	}

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(myProject).Build()
	metrics := appsetmetrics.NewFakeAppsetMetrics()

	// Test a subset of the validations that 'validateGeneratedApplications' performs
	for _, cc := range []struct {
		name             string
		apps             []v1alpha1.Application
		validationErrors map[string]error
	}{
		{
			name: "valid app should return true",
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "default",
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://url",
							Path:           "/",
							TargetRevision: "HEAD",
						},
						Destination: v1alpha1.ApplicationDestination{
							Namespace: "namespace",
							Name:      "my-cluster",
						},
					},
				},
			},
			validationErrors: map[string]error{},
		},
		{
			name: "can't have both name and server defined",
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "default",
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://url",
							Path:           "/",
							TargetRevision: "HEAD",
						},
						Destination: v1alpha1.ApplicationDestination{
							Namespace: "namespace",
							Server:    "my-server",
							Name:      "my-cluster",
						},
					},
				},
			},
			validationErrors: map[string]error{"app": errors.New("application destination spec is invalid: application destination can't have both name and server defined: my-cluster my-server")},
		},
		{
			name: "project mismatch should return error",
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "DOES-NOT-EXIST",
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://url",
							Path:           "/",
							TargetRevision: "HEAD",
						},
						Destination: v1alpha1.ApplicationDestination{
							Namespace: "namespace",
							Name:      "my-cluster",
						},
					},
				},
			},
			validationErrors: map[string]error{"app": errors.New("application references project DOES-NOT-EXIST which does not exist")},
		},
		{
			name: "valid app should return true",
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "default",
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://url",
							Path:           "/",
							TargetRevision: "HEAD",
						},
						Destination: v1alpha1.ApplicationDestination{
							Namespace: "namespace",
							Name:      "my-cluster",
						},
					},
				},
			},
			validationErrors: map[string]error{},
		},
		{
			name: "cluster should match",
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app",
					},
					Spec: v1alpha1.ApplicationSpec{
						Project: "default",
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://url",
							Path:           "/",
							TargetRevision: "HEAD",
						},
						Destination: v1alpha1.ApplicationDestination{
							Namespace: "namespace",
							Name:      "nonexistent-cluster",
						},
					},
				},
			},
			validationErrors: map[string]error{"app": errors.New("application destination spec is invalid: there are no clusters with this name: nonexistent-cluster")},
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			t.Parallel()

			secret := &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "my-secret",
					Namespace: "argocd",
					Labels: map[string]string{
						argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
					},
				},
				Data: map[string][]byte{
					"name":   []byte("my-cluster"),
					"server": []byte("https://kubernetes.default.svc"),
					"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
				},
			}

			kubeclientset := getDefaultTestClientSet(secret)

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:          client,
				Scheme:          scheme,
				Recorder:        record.NewFakeRecorder(1),
				Generators:      map[string]generators.Generator{},
				ArgoDB:          argodb,
				ArgoCDNamespace: "namespace",
				KubeClientset:   kubeclientset,
				Metrics:         metrics,
			}

			appSetInfo := v1alpha1.ApplicationSet{}
			validationErrors, _ := r.validateGeneratedApplications(t.Context(), cc.apps, appSetInfo)
			assert.Equal(t, cc.validationErrors, validationErrors)
		})
	}
}

func TestReconcilerValidationProjectErrorBehaviour(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	project := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{Name: "good-project", Namespace: "argocd"},
	}
	appSet := v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "name",
			Namespace: "argocd",
		},
		Spec: v1alpha1.ApplicationSetSpec{
			GoTemplate: true,
			Generators: []v1alpha1.ApplicationSetGenerator{
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{{
							Raw: []byte(`{"project": "good-project"}`),
						}, {
							Raw: []byte(`{"project": "bad-project"}`),
						}},
					},
				},
			},
			Template: v1alpha1.ApplicationSetTemplate{
				ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
					Name:      "{{.project}}",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSpec{
					Source:      &v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argocd-example-apps", Path: "guestbook"},
					Project:     "{{.project}}",
					Destination: v1alpha1.ApplicationDestination{Server: "https://kubernetes.default.svc"},
				},
			},
		},
	}

	kubeclientset := getDefaultTestClientSet()

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet, &project).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
	metrics := appsetmetrics.NewFakeAppsetMetrics()

	argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

	clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
	require.NoError(t, err)

	r := ApplicationSetReconciler{
		Client:   client,
		Scheme:   scheme,
		Renderer: &utils.Render{},
		Recorder: record.NewFakeRecorder(1),
		Generators: map[string]generators.Generator{
			"List": generators.NewListGenerator(),
		},
		ArgoDB:          argodb,
		KubeClientset:   kubeclientset,
		Policy:          v1alpha1.ApplicationsSyncPolicySync,
		ArgoCDNamespace: "argocd",
		Metrics:         metrics,
		ClusterInformer: clusterInformer,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Namespace: "argocd",
			Name:      "name",
		},
	}

	// Verify that on validation error, no error is returned, but the object is requeued
	res, err := r.Reconcile(t.Context(), req)
	require.NoError(t, err)
	assert.Equal(t, ReconcileRequeueOnValidationError, res.RequeueAfter)

	var app v1alpha1.Application

	// make sure good app got created
	err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "good-project"}, &app)
	require.NoError(t, err)
	assert.Equal(t, "good-project", app.Name)

	// make sure bad app was not created
	err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "bad-project"}, &app)
	require.Error(t, err)
}

func TestSetApplicationSetStatusCondition(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
	someTime := &metav1.Time{Time: time.Now().Add(-5 * time.Minute)}
	existingParameterGeneratedCondition := getParametersGeneratedCondition(true, "")
	existingParameterGeneratedCondition.LastTransitionTime = someTime

	for _, c := range []struct {
		name                string
		appset              v1alpha1.ApplicationSet
		condition           v1alpha1.ApplicationSetCondition
		parametersGenerated bool
		testfunc            func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition)
	}{
		{
			name: "has parameters generated condition when false",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionResourcesUpToDate,
				Message: "This is a message",
				Reason:  "test",
				Status:  v1alpha1.ApplicationSetConditionStatusFalse,
			},
			parametersGenerated: false,
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 2)

				// Conditions are ordered by type, so the order is deterministic
				assert.Equal(t, v1alpha1.ApplicationSetConditionParametersGenerated, conditions[0].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusFalse, conditions[0].Status)

				assert.Equal(t, v1alpha1.ApplicationSetConditionResourcesUpToDate, conditions[1].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusFalse, conditions[1].Status)
				assert.Equal(t, "test", conditions[1].Reason)
			},
		},
		{
			name: "parameters generated condition is used when specified",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionParametersGenerated,
				Message: "This is a message",
				Reason:  "test",
				Status:  v1alpha1.ApplicationSetConditionStatusFalse,
			},
			parametersGenerated: true,
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 1)

				assert.Equal(t, v1alpha1.ApplicationSetConditionParametersGenerated, conditions[0].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusFalse, conditions[0].Status)
				assert.Equal(t, "test", conditions[0].Reason)
			},
		},
		{
			name: "has parameter conditions when true",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionResourcesUpToDate,
				Message: "This is a message",
				Reason:  "test",
				Status:  v1alpha1.ApplicationSetConditionStatusFalse,
			},
			parametersGenerated: true,
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 2)

				// Conditions are ordered by type, so the order is deterministic
				assert.Equal(t, v1alpha1.ApplicationSetConditionParametersGenerated, conditions[0].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusTrue, conditions[0].Status)

				assert.Equal(t, v1alpha1.ApplicationSetConditionResourcesUpToDate, conditions[1].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusFalse, conditions[1].Status)
				assert.Equal(t, "test", conditions[1].Reason)
			},
		},
		{
			name: "resource up to date sets error condition to false",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionResourcesUpToDate,
				Message: "Completed",
				Reason:  "test",
				Status:  v1alpha1.ApplicationSetConditionStatusTrue,
			},
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 3)

				assert.Equal(t, v1alpha1.ApplicationSetConditionErrorOccurred, conditions[0].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusFalse, conditions[0].Status)
				assert.Equal(t, "test", conditions[0].Reason)
				assert.Equal(t, "Completed", conditions[0].Message)

				assert.Equal(t, v1alpha1.ApplicationSetConditionParametersGenerated, conditions[1].Type)

				assert.Equal(t, v1alpha1.ApplicationSetConditionResourcesUpToDate, conditions[2].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusTrue, conditions[2].Status)
				assert.Equal(t, "test", conditions[2].Reason)
				assert.Equal(t, "Completed", conditions[2].Message)
			},
		},
		{
			name: "error condition sets resource up to date to false",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionErrorOccurred,
				Message: "Error",
				Reason:  "test",
				Status:  v1alpha1.ApplicationSetConditionStatusTrue,
			},
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 3)

				assert.Equal(t, v1alpha1.ApplicationSetConditionErrorOccurred, conditions[0].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusTrue, conditions[0].Status)
				assert.Equal(t, "test", conditions[0].Reason)
				assert.Equal(t, "Error", conditions[0].Message)

				assert.Equal(t, v1alpha1.ApplicationSetConditionParametersGenerated, conditions[1].Type)

				assert.Equal(t, v1alpha1.ApplicationSetConditionResourcesUpToDate, conditions[2].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusFalse, conditions[2].Status)
				assert.Equal(t, v1alpha1.ApplicationSetReasonErrorOccurred, conditions[2].Reason)
				assert.Equal(t, "Error", conditions[2].Message)
			},
		},
		{
			name: "updating an unchanged condition does not mutate existing conditions",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type:        "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
				Status: v1alpha1.ApplicationSetStatus{
					Conditions: []v1alpha1.ApplicationSetCondition{
						{
							Type:               v1alpha1.ApplicationSetConditionErrorOccurred,
							Message:            "existing",
							LastTransitionTime: someTime,
						},
						existingParameterGeneratedCondition,
						{
							Type:               v1alpha1.ApplicationSetConditionResourcesUpToDate,
							Message:            "existing",
							Status:             v1alpha1.ApplicationSetConditionStatusFalse,
							LastTransitionTime: someTime,
						},
						{
							Type:               v1alpha1.ApplicationSetConditionRolloutProgressing,
							Message:            "existing",
							LastTransitionTime: someTime,
						},
					},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionResourcesUpToDate,
				Message: "existing",
				Status:  v1alpha1.ApplicationSetConditionStatusFalse,
			},
			parametersGenerated: true,
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 4)

				assert.Equal(t, v1alpha1.ApplicationSetConditionErrorOccurred, conditions[0].Type)
				assert.Equal(t, someTime, conditions[0].LastTransitionTime)

				assert.Equal(t, v1alpha1.ApplicationSetConditionParametersGenerated, conditions[1].Type)
				assert.Equal(t, someTime, conditions[1].LastTransitionTime)

				assert.Equal(t, v1alpha1.ApplicationSetConditionResourcesUpToDate, conditions[2].Type)
				assert.Equal(t, someTime, conditions[2].LastTransitionTime)

				assert.Equal(t, v1alpha1.ApplicationSetConditionRolloutProgressing, conditions[3].Type)
				assert.Equal(t, someTime, conditions[3].LastTransitionTime)
			},
		},
		{
			name: "progressing conditions is removed when AppSet is not configured",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					// Strategy removed
					// Strategy: &v1alpha1.ApplicationSetStrategy{
					// 	Type:        "RollingSync",
					// 	RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{},
					// },
					Template: v1alpha1.ApplicationSetTemplate{},
				},
				Status: v1alpha1.ApplicationSetStatus{
					Conditions: []v1alpha1.ApplicationSetCondition{
						{
							Type:               v1alpha1.ApplicationSetConditionErrorOccurred,
							Message:            "existing",
							LastTransitionTime: someTime,
						},
						existingParameterGeneratedCondition,
						{
							Type:               v1alpha1.ApplicationSetConditionResourcesUpToDate,
							Message:            "existing",
							Status:             v1alpha1.ApplicationSetConditionStatusFalse,
							LastTransitionTime: someTime,
						},
						{
							Type:               v1alpha1.ApplicationSetConditionRolloutProgressing,
							Message:            "existing",
							LastTransitionTime: someTime,
						},
					},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionResourcesUpToDate,
				Message: "existing",
				Status:  v1alpha1.ApplicationSetConditionStatusFalse,
			},
			parametersGenerated: true,
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 3)
				for _, c := range conditions {
					assert.NotEqual(t, v1alpha1.ApplicationSetConditionRolloutProgressing, c.Type)
				}
			},
		},
		{
			name: "progressing conditions is ignored when AppSet is not configured",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					// Strategy removed
					// Strategy: &v1alpha1.ApplicationSetStrategy{
					// 	Type:        "RollingSync",
					// 	RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{},
					// },
					Template: v1alpha1.ApplicationSetTemplate{},
				},
				Status: v1alpha1.ApplicationSetStatus{
					Conditions: []v1alpha1.ApplicationSetCondition{
						{
							Type:               v1alpha1.ApplicationSetConditionErrorOccurred,
							Message:            "existing",
							LastTransitionTime: someTime,
						},
						existingParameterGeneratedCondition,
						{
							Type:               v1alpha1.ApplicationSetConditionResourcesUpToDate,
							Message:            "existing",
							Status:             v1alpha1.ApplicationSetConditionStatusFalse,
							LastTransitionTime: someTime,
						},
					},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionRolloutProgressing,
				Message: "do not add me",
				Status:  v1alpha1.ApplicationSetConditionStatusTrue,
			},
			parametersGenerated: true,
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 3)
				for _, c := range conditions {
					assert.NotEqual(t, v1alpha1.ApplicationSetConditionRolloutProgressing, c.Type)
				}
			},
		},
		{
			name: "progressing conditions is updated correctly when configured",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type:        "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
				Status: v1alpha1.ApplicationSetStatus{
					Conditions: []v1alpha1.ApplicationSetCondition{
						{
							Type:               v1alpha1.ApplicationSetConditionErrorOccurred,
							Message:            "existing",
							LastTransitionTime: someTime,
						},
						existingParameterGeneratedCondition,
						{
							Type:               v1alpha1.ApplicationSetConditionResourcesUpToDate,
							Message:            "existing",
							Status:             v1alpha1.ApplicationSetConditionStatusFalse,
							LastTransitionTime: someTime,
						},
						{
							Type:    v1alpha1.ApplicationSetConditionRolloutProgressing,
							Message: "old value",
							Status:  v1alpha1.ApplicationSetConditionStatusTrue,
						},
					},
				},
			},
			condition: v1alpha1.ApplicationSetCondition{
				Type:    v1alpha1.ApplicationSetConditionRolloutProgressing,
				Message: "new value",
				Status:  v1alpha1.ApplicationSetConditionStatusFalse,
			},
			parametersGenerated: true,
			testfunc: func(t *testing.T, conditions []v1alpha1.ApplicationSetCondition) {
				t.Helper()
				require.Len(t, conditions, 4)

				assert.Equal(t, v1alpha1.ApplicationSetConditionRolloutProgressing, conditions[3].Type)
				assert.Equal(t, v1alpha1.ApplicationSetConditionStatusFalse, conditions[3].Status)
				assert.Equal(t, "new value", conditions[3].Message)
			},
		},
	} {
		t.Run(c.name, func(t *testing.T) {
			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&c.appset).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).WithStatusSubresource(&c.appset).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()
			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:   client,
				Scheme:   scheme,
				Renderer: &utils.Render{},
				Recorder: record.NewFakeRecorder(1),
				Generators: map[string]generators.Generator{
					"List": generators.NewListGenerator(),
				},
				ArgoDB:        argodb,
				KubeClientset: kubeclientset,
				Metrics:       metrics,
			}

			err = r.setApplicationSetStatusCondition(t.Context(), &c.appset, c.condition, c.parametersGenerated)
			require.NoError(t, err)

			c.testfunc(t, c.appset.Status.Conditions)
		})
	}
}

func applicationsUpdateSyncPolicyTest(t *testing.T, applicationsSyncPolicy v1alpha1.ApplicationsSyncPolicy, recordBuffer int, allowPolicyOverride bool) v1alpha1.Application {
	t.Helper()
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	defaultProject := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
		Spec:       v1alpha1.AppProjectSpec{SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "https://good-cluster"}}},
	}
	appSet := v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "name",
			Namespace: "argocd",
		},
		Spec: v1alpha1.ApplicationSetSpec{
			Generators: []v1alpha1.ApplicationSetGenerator{
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{{
							Raw: []byte(`{"cluster": "good-cluster","url": "https://good-cluster"}`),
						}},
					},
				},
			},
			SyncPolicy: &v1alpha1.ApplicationSetSyncPolicy{
				ApplicationsSync: &applicationsSyncPolicy,
			},
			Template: v1alpha1.ApplicationSetTemplate{
				ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
					Name:      "{{cluster}}",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSpec{
					Source:      &v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argocd-example-apps", Path: "guestbook"},
					Project:     "default",
					Destination: v1alpha1.ApplicationDestination{Server: "{{url}}"},
				},
			},
		},
	}

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "my-cluster",
			Namespace: "argocd",
			Labels: map[string]string{
				argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
			},
		},
		Data: map[string][]byte{
			// Since this test requires the cluster to be an invalid destination, we
			// always return a cluster named 'my-cluster2' (different from app 'my-cluster', above)
			"name":   []byte("good-cluster"),
			"server": []byte("https://good-cluster"),
			"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
		},
	}

	kubeclientset := getDefaultTestClientSet(secret)

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet, &defaultProject, secret).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
	metrics := appsetmetrics.NewFakeAppsetMetrics()

	argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
	clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
	require.NoError(t, err)

	defer startAndSyncInformer(t, clusterInformer)()

	r := ApplicationSetReconciler{
		Client:   client,
		Scheme:   scheme,
		Renderer: &utils.Render{},
		Recorder: record.NewFakeRecorder(recordBuffer),
		Generators: map[string]generators.Generator{
			"List": generators.NewListGenerator(),
		},
		ArgoDB:               argodb,
		ArgoCDNamespace:      "argocd",
		KubeClientset:        kubeclientset,
		Policy:               v1alpha1.ApplicationsSyncPolicySync,
		EnablePolicyOverride: allowPolicyOverride,
		Metrics:              metrics,
		ClusterInformer:      clusterInformer,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Namespace: "argocd",
			Name:      "name",
		},
	}

	// Verify that on validation error, no error is returned, but the object is requeued
	resCreate, err := r.Reconcile(t.Context(), req)
	require.NoErrorf(t, err, "Reconcile failed with error: %v", err)
	assert.Equal(t, time.Duration(0), resCreate.RequeueAfter)

	var app v1alpha1.Application

	// make sure good app got created
	err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "good-cluster"}, &app)
	require.NoError(t, err)
	assert.Equal(t, "good-cluster", app.Name)

	// Update resource
	var retrievedApplicationSet v1alpha1.ApplicationSet
	err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "name"}, &retrievedApplicationSet)
	require.NoError(t, err)

	retrievedApplicationSet.Spec.Template.Annotations = map[string]string{"annotation-key": "annotation-value"}
	retrievedApplicationSet.Spec.Template.Labels = map[string]string{"label-key": "label-value"}

	retrievedApplicationSet.Spec.Template.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{
		Values: "global.test: test",
	}

	err = r.Update(t.Context(), &retrievedApplicationSet)
	require.NoError(t, err)

	resUpdate, err := r.Reconcile(t.Context(), req)
	require.NoError(t, err)

	err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "good-cluster"}, &app)
	require.NoError(t, err)
	assert.Equal(t, time.Duration(0), resUpdate.RequeueAfter)
	assert.Equal(t, "good-cluster", app.Name)

	return app
}

func TestUpdateNotPerformedWithSyncPolicyCreateOnly(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicyCreateOnly

	app := applicationsUpdateSyncPolicyTest(t, applicationsSyncPolicy, 1, true)

	assert.Nil(t, app.Spec.Source.Helm)
	assert.Nil(t, app.Annotations)
}

func TestUpdateNotPerformedWithSyncPolicyCreateDelete(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicyCreateDelete

	app := applicationsUpdateSyncPolicyTest(t, applicationsSyncPolicy, 1, true)

	assert.Nil(t, app.Spec.Source.Helm)
	assert.Nil(t, app.Annotations)
}

func TestUpdatePerformedWithSyncPolicyCreateUpdate(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicyCreateUpdate

	app := applicationsUpdateSyncPolicyTest(t, applicationsSyncPolicy, 2, true)

	assert.Equal(t, "global.test: test", app.Spec.Source.Helm.Values)
	assert.Equal(t, map[string]string{"annotation-key": "annotation-value"}, app.Annotations)
	assert.Equal(t, map[string]string{"label-key": "label-value"}, app.Labels)
}

func TestUpdatePerformedWithSyncPolicySync(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicySync

	app := applicationsUpdateSyncPolicyTest(t, applicationsSyncPolicy, 2, true)

	assert.Equal(t, "global.test: test", app.Spec.Source.Helm.Values)
	assert.Equal(t, map[string]string{"annotation-key": "annotation-value"}, app.Annotations)
	assert.Equal(t, map[string]string{"label-key": "label-value"}, app.Labels)
}

// TestReconcilePopulatesResourcesStatusOnFirstRun verifies that status.resources and status.resourcesCount
// are populated after the first reconcile, when applications are created.
func TestReconcilePopulatesResourcesStatusOnFirstRun(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	defaultProject := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
		Spec:       v1alpha1.AppProjectSpec{SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "https://good-cluster"}}},
	}
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicySync
	appSet := v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "name",
			Namespace: "argocd",
		},
		Spec: v1alpha1.ApplicationSetSpec{
			Generators: []v1alpha1.ApplicationSetGenerator{
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{{
							Raw: []byte(`{"cluster": "good-cluster","url": "https://good-cluster"}`),
						}},
					},
				},
			},
			SyncPolicy: &v1alpha1.ApplicationSetSyncPolicy{
				ApplicationsSync: &applicationsSyncPolicy,
			},
			Template: v1alpha1.ApplicationSetTemplate{
				ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
					Name:      "{{cluster}}",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSpec{
					Source:      &v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argocd-example-apps", Path: "guestbook"},
					Project:     "default",
					Destination: v1alpha1.ApplicationDestination{Server: "{{url}}"},
				},
			},
		},
	}

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "my-cluster",
			Namespace: "argocd",
			Labels: map[string]string{
				argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
			},
		},
		Data: map[string][]byte{
			"name":   []byte("good-cluster"),
			"server": []byte("https://good-cluster"),
			"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
		},
	}

	kubeclientset := getDefaultTestClientSet(secret)
	client := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(&appSet, &defaultProject, secret).
		WithStatusSubresource(&appSet).
		WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
		Build()
	metrics := appsetmetrics.NewFakeAppsetMetrics()

	argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
	clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
	require.NoError(t, err)

	defer startAndSyncInformer(t, clusterInformer)()

	r := ApplicationSetReconciler{
		Client:          client,
		Scheme:          scheme,
		Renderer:        &utils.Render{},
		Recorder:        record.NewFakeRecorder(1),
		Generators:      map[string]generators.Generator{"List": generators.NewListGenerator()},
		ArgoDB:          argodb,
		ArgoCDNamespace: "argocd",
		KubeClientset:   kubeclientset,
		Policy:          v1alpha1.ApplicationsSyncPolicySync,
		Metrics:         metrics,
		ClusterInformer: clusterInformer,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{Namespace: "argocd", Name: "name"},
	}

	_, err = r.Reconcile(t.Context(), req)
	require.NoError(t, err)

	var retrievedAppSet v1alpha1.ApplicationSet
	err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "name"}, &retrievedAppSet)
	require.NoError(t, err)

	assert.Len(t, retrievedAppSet.Status.Resources, 1, "status.resources should have 1 item after first reconcile")
	assert.Equal(t, int64(1), retrievedAppSet.Status.ResourcesCount, "status.resourcesCount should be 1 after first reconcile")
	assert.Equal(t, "good-cluster", retrievedAppSet.Status.Resources[0].Name)
}

func TestUpdatePerformedWithSyncPolicyCreateOnlyAndAllowPolicyOverrideFalse(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicyCreateOnly

	app := applicationsUpdateSyncPolicyTest(t, applicationsSyncPolicy, 2, false)

	assert.Equal(t, "global.test: test", app.Spec.Source.Helm.Values)
	assert.Equal(t, map[string]string{"annotation-key": "annotation-value"}, app.Annotations)
	assert.Equal(t, map[string]string{"label-key": "label-value"}, app.Labels)
}

func applicationsDeleteSyncPolicyTest(t *testing.T, applicationsSyncPolicy v1alpha1.ApplicationsSyncPolicy, recordBuffer int, allowPolicyOverride bool) v1alpha1.ApplicationList {
	t.Helper()
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	defaultProject := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
		Spec:       v1alpha1.AppProjectSpec{SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "https://good-cluster"}}},
	}
	appSet := v1alpha1.ApplicationSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "name",
			Namespace: "argocd",
		},
		Spec: v1alpha1.ApplicationSetSpec{
			Generators: []v1alpha1.ApplicationSetGenerator{
				{
					List: &v1alpha1.ListGenerator{
						Elements: []apiextensionsv1.JSON{{
							Raw: []byte(`{"cluster": "good-cluster","url": "https://good-cluster"}`),
						}},
					},
				},
			},
			SyncPolicy: &v1alpha1.ApplicationSetSyncPolicy{
				ApplicationsSync: &applicationsSyncPolicy,
			},
			Template: v1alpha1.ApplicationSetTemplate{
				ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
					Name:      "{{cluster}}",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSpec{
					Source:      &v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argocd-example-apps", Path: "guestbook"},
					Project:     "default",
					Destination: v1alpha1.ApplicationDestination{Server: "{{url}}"},
				},
			},
		},
	}

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "my-cluster",
			Namespace: "argocd",
			Labels: map[string]string{
				argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
			},
		},
		Data: map[string][]byte{
			// Since this test requires the cluster to be an invalid destination, we
			// always return a cluster named 'my-cluster2' (different from app 'my-cluster', above)
			"name":   []byte("good-cluster"),
			"server": []byte("https://good-cluster"),
			"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
		},
	}

	kubeclientset := getDefaultTestClientSet(secret)

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet, &defaultProject, secret).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
	metrics := appsetmetrics.NewFakeAppsetMetrics()

	argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

	clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
	require.NoError(t, err)

	defer startAndSyncInformer(t, clusterInformer)()

	r := ApplicationSetReconciler{
		Client:   client,
		Scheme:   scheme,
		Renderer: &utils.Render{},
		Recorder: record.NewFakeRecorder(recordBuffer),
		Generators: map[string]generators.Generator{
			"List": generators.NewListGenerator(),
		},
		ArgoDB:               argodb,
		ArgoCDNamespace:      "argocd",
		KubeClientset:        kubeclientset,
		Policy:               v1alpha1.ApplicationsSyncPolicySync,
		EnablePolicyOverride: allowPolicyOverride,
		Metrics:              metrics,
		ClusterInformer:      clusterInformer,
	}

	req := ctrl.Request{
		NamespacedName: types.NamespacedName{
			Namespace: "argocd",
			Name:      "name",
		},
	}

	// Verify that on validation error, no error is returned, but the object is requeued
	resCreate, err := r.Reconcile(t.Context(), req)
	require.NoError(t, err)
	assert.Equal(t, time.Duration(0), resCreate.RequeueAfter)

	var app v1alpha1.Application

	// make sure good app got created
	err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "good-cluster"}, &app)
	require.NoError(t, err)
	assert.Equal(t, "good-cluster", app.Name)

	// Update resource
	var retrievedApplicationSet v1alpha1.ApplicationSet
	err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "name"}, &retrievedApplicationSet)
	require.NoError(t, err)
	retrievedApplicationSet.Spec.Generators = []v1alpha1.ApplicationSetGenerator{
		{
			List: &v1alpha1.ListGenerator{
				Elements: []apiextensionsv1.JSON{},
			},
		},
	}

	err = r.Update(t.Context(), &retrievedApplicationSet)
	require.NoError(t, err)

	resUpdate, err := r.Reconcile(t.Context(), req)
	require.NoError(t, err)

	var apps v1alpha1.ApplicationList

	err = r.List(t.Context(), &apps)
	require.NoError(t, err)
	assert.Equal(t, time.Duration(0), resUpdate.RequeueAfter)

	return apps
}

func TestDeleteNotPerformedWithSyncPolicyCreateOnly(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicyCreateOnly

	apps := applicationsDeleteSyncPolicyTest(t, applicationsSyncPolicy, 1, true)

	assert.Equal(t, "good-cluster", apps.Items[0].Name)
}

func TestDeleteNotPerformedWithSyncPolicyCreateUpdate(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicyCreateUpdate

	apps := applicationsDeleteSyncPolicyTest(t, applicationsSyncPolicy, 2, true)

	assert.Equal(t, "good-cluster", apps.Items[0].Name)
}

func TestDeletePerformedWithSyncPolicyCreateDelete(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicyCreateDelete

	apps := applicationsDeleteSyncPolicyTest(t, applicationsSyncPolicy, 3, true)

	assert.NotNil(t, apps.Items[0].DeletionTimestamp)
}

func TestDeletePerformedWithSyncPolicySync(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicySync

	apps := applicationsDeleteSyncPolicyTest(t, applicationsSyncPolicy, 3, true)

	assert.NotNil(t, apps.Items[0].DeletionTimestamp)
}

func TestDeletePerformedWithSyncPolicyCreateOnlyAndAllowPolicyOverrideFalse(t *testing.T) {
	applicationsSyncPolicy := v1alpha1.ApplicationsSyncPolicyCreateOnly

	apps := applicationsDeleteSyncPolicyTest(t, applicationsSyncPolicy, 3, false)

	assert.NotNil(t, apps.Items[0].DeletionTimestamp)
}

func TestPolicies(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	err = corev1.AddToScheme(scheme)
	require.NoError(t, err)

	defaultProject := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
		Spec:       v1alpha1.AppProjectSpec{SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "https://kubernetes.default.svc"}}},
	}

	kubeclientset := getDefaultTestClientSet()

	for _, c := range []struct {
		name          string
		policyName    string
		allowedUpdate bool
		allowedDelete bool
	}{
		{
			name:          "Apps are allowed to update and delete",
			policyName:    "sync",
			allowedUpdate: true,
			allowedDelete: true,
		},
		{
			name:          "Apps are not allowed to update and delete",
			policyName:    "create-only",
			allowedUpdate: false,
			allowedDelete: false,
		},
		{
			name:          "Apps are allowed to update, not allowed to delete",
			policyName:    "create-update",
			allowedUpdate: true,
			allowedDelete: false,
		},
		{
			name:          "Apps are allowed to delete, not allowed to update",
			policyName:    "create-delete",
			allowedUpdate: false,
			allowedDelete: true,
		},
	} {
		t.Run(c.name, func(t *testing.T) {
			policy := utils.Policies[c.policyName]
			assert.NotNil(t, policy)

			appSet := v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					GoTemplate: true,
					Generators: []v1alpha1.ApplicationSetGenerator{
						{
							List: &v1alpha1.ListGenerator{
								Elements: []apiextensionsv1.JSON{
									{
										Raw: []byte(`{"name": "my-app"}`),
									},
								},
							},
						},
					},
					Template: v1alpha1.ApplicationSetTemplate{
						ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
							Name:      "{{.name}}",
							Namespace: "argocd",
							Annotations: map[string]string{
								"key": "value",
							},
						},
						Spec: v1alpha1.ApplicationSpec{
							Source:      &v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argocd-example-apps", Path: "guestbook"},
							Project:     "default",
							Destination: v1alpha1.ApplicationDestination{Server: "https://kubernetes.default.svc"},
						},
					},
				},
			}

			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet, &defaultProject).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
			require.NoError(t, err)

			defer startAndSyncInformer(t, clusterInformer)()

			r := ApplicationSetReconciler{
				Client:   client,
				Scheme:   scheme,
				Renderer: &utils.Render{},
				Recorder: record.NewFakeRecorder(10),
				Generators: map[string]generators.Generator{
					"List": generators.NewListGenerator(),
				},
				ArgoDB:          argodb,
				ArgoCDNamespace: "argocd",
				KubeClientset:   kubeclientset,
				Policy:          policy,
				ClusterInformer: clusterInformer,
				Metrics:         metrics,
			}

			req := ctrl.Request{
				NamespacedName: types.NamespacedName{
					Namespace: "argocd",
					Name:      "name",
				},
			}
			ctx := t.Context()
			// Check if the application is created
			res, err := r.Reconcile(ctx, req)
			require.NoError(t, err)
			assert.Equal(t, time.Duration(0), res.RequeueAfter)

			var app v1alpha1.Application
			err = r.Get(ctx, crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
			require.NoError(t, err)
			assert.Equal(t, "value", app.Annotations["key"])

			// Check if the Application is updated
			app.Annotations["key"] = "edited"
			err = r.Update(ctx, &app)
			require.NoError(t, err)

			res, err = r.Reconcile(ctx, req)
			require.NoError(t, err)
			assert.Equal(t, time.Duration(0), res.RequeueAfter)

			err = r.Get(ctx, crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
			require.NoError(t, err)

			if c.allowedUpdate {
				assert.Equal(t, "value", app.Annotations["key"])
			} else {
				assert.Equal(t, "edited", app.Annotations["key"])
			}

			// Check if the Application is deleted
			err = r.Get(ctx, crtclient.ObjectKey{Namespace: "argocd", Name: "name"}, &appSet)
			require.NoError(t, err)
			appSet.Spec.Generators[0] = v1alpha1.ApplicationSetGenerator{
				List: &v1alpha1.ListGenerator{
					Elements: []apiextensionsv1.JSON{},
				},
			}
			err = r.Update(ctx, &appSet)
			require.NoError(t, err)

			res, err = r.Reconcile(ctx, req)
			require.NoError(t, err)
			assert.Equal(t, time.Duration(0), res.RequeueAfter)

			err = r.Get(ctx, crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
			require.NoError(t, err)
			if c.allowedDelete {
				assert.NotNil(t, app.DeletionTimestamp)
			} else {
				assert.Nil(t, app.DeletionTimestamp)
			}
		})
	}
}

func TestSetApplicationSetApplicationStatus(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

	for _, cc := range []struct {
		name                string
		appSet              v1alpha1.ApplicationSet
		appStatuses         []v1alpha1.ApplicationSetApplicationStatus
		expectedAppStatuses []v1alpha1.ApplicationSetApplicationStatus
	}{
		{
			name: "sets a single appstatus",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			appStatuses: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application: "app1",
					Message:     "testing SetApplicationSetApplicationStatus to Healthy",
					Status:      v1alpha1.ProgressiveSyncHealthy,
				},
			},
			expectedAppStatuses: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application: "app1",
					Message:     "testing SetApplicationSetApplicationStatus to Healthy",
					Status:      v1alpha1.ProgressiveSyncHealthy,
				},
			},
		},
		{
			name: "order appstatus by name",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			appStatuses: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application: "app2",
					Message:     "testing SetApplicationSetApplicationStatus to Healthy",
					Status:      v1alpha1.ProgressiveSyncHealthy,
				},
				{
					Application: "app1",
					Message:     "testing SetApplicationSetApplicationStatus to Healthy",
					Status:      v1alpha1.ProgressiveSyncHealthy,
				},
			},
			expectedAppStatuses: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application: "app1",
					Message:     "testing SetApplicationSetApplicationStatus to Healthy",
					Status:      v1alpha1.ProgressiveSyncHealthy,
				},
				{
					Application: "app2",
					Message:     "testing SetApplicationSetApplicationStatus to Healthy",
					Status:      v1alpha1.ProgressiveSyncHealthy,
				},
			},
		},
		{
			name: "removes an appstatus",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{
							Elements: []apiextensionsv1.JSON{{
								Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
							}},
						}},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Message:     "testing SetApplicationSetApplicationStatus to Healthy",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
					},
				},
			},
			appStatuses:         []v1alpha1.ApplicationSetApplicationStatus{},
			expectedAppStatuses: nil,
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&cc.appSet).WithStatusSubresource(&cc.appSet).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:   client,
				Scheme:   scheme,
				Renderer: &utils.Render{},
				Recorder: record.NewFakeRecorder(1),
				Generators: map[string]generators.Generator{
					"List": generators.NewListGenerator(),
				},
				ArgoDB:        argodb,
				KubeClientset: kubeclientset,
				Metrics:       metrics,
			}

			err = r.setAppSetApplicationStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.appStatuses)
			require.NoError(t, err)

			assert.Equal(t, cc.expectedAppStatuses, cc.appSet.Status.ApplicationStatus)
		})
	}
}

func TestBuildAppDependencyList(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	client := fake.NewClientBuilder().WithScheme(scheme).Build()
	metrics := appsetmetrics.NewFakeAppsetMetrics()

	for _, cc := range []struct {
		name            string
		appSet          v1alpha1.ApplicationSet
		apps            []v1alpha1.Application
		expectedList    [][]string
		expectedStepMap map[string]int
	}{
		{
			name: "handles an empty set of applications and no strategy",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{},
			},
			apps:            []v1alpha1.Application{},
			expectedList:    [][]string{},
			expectedStepMap: map[string]int{},
		},
		{
			name: "handles an empty set of applications and ignores AllAtOnce strategy",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "AllAtOnce",
					},
				},
			},
			apps:            []v1alpha1.Application{},
			expectedList:    [][]string{},
			expectedStepMap: map[string]int{},
		},
		{
			name: "handles an empty set of applications with good 'In' selectors",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"dev",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{},
			expectedList: [][]string{
				{},
			},
			expectedStepMap: map[string]int{},
		},
		{
			name: "handles selecting 1 application with 1 'In' selector",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"dev",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-dev",
						Labels: map[string]string{
							"env": "dev",
						},
					},
				},
			},
			expectedList: [][]string{
				{"app-dev"},
			},
			expectedStepMap: map[string]int{
				"app-dev": 0,
			},
		},
		{
			name: "handles 'In' selectors that select no applications",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"dev",
											},
										},
									},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"qa",
											},
										},
									},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"prod",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa",
						Labels: map[string]string{
							"env": "qa",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-prod",
						Labels: map[string]string{
							"env": "prod",
						},
					},
				},
			},
			expectedList: [][]string{
				{},
				{"app-qa"},
				{"app-prod"},
			},
			expectedStepMap: map[string]int{
				"app-qa":   1,
				"app-prod": 2,
			},
		},
		{
			name: "multiple 'In' selectors in the same matchExpression only select Applications that match all selectors",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "region",
											Operator: "In",
											Values: []string{
												"us-east-2",
											},
										},
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"qa",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa1",
						Labels: map[string]string{
							"env": "qa",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa2",
						Labels: map[string]string{
							"env":    "qa",
							"region": "us-east-2",
						},
					},
				},
			},
			expectedList: [][]string{
				{"app-qa2"},
			},
			expectedStepMap: map[string]int{
				"app-qa2": 0,
			},
		},
		{
			name: "multiple values in the same 'In' matchExpression can match on any value",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"qa",
												"prod",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-dev",
						Labels: map[string]string{
							"env": "dev",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa",
						Labels: map[string]string{
							"env": "qa",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-prod",
						Labels: map[string]string{
							"env":    "prod",
							"region": "us-east-2",
						},
					},
				},
			},
			expectedList: [][]string{
				{"app-qa", "app-prod"},
			},
			expectedStepMap: map[string]int{
				"app-qa":   0,
				"app-prod": 0,
			},
		},
		{
			name: "handles an empty set of applications with good 'NotIn' selectors",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"dev",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{},
			expectedList: [][]string{
				{},
			},
			expectedStepMap: map[string]int{},
		},
		{
			name: "selects 1 application with 1 'NotIn' selector",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "NotIn",
											Values: []string{
												"qa",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-dev",
						Labels: map[string]string{
							"env": "dev",
						},
					},
				},
			},
			expectedList: [][]string{
				{"app-dev"},
			},
			expectedStepMap: map[string]int{
				"app-dev": 0,
			},
		},
		{
			name: "'NotIn' selectors that select no applications",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "NotIn",
											Values: []string{
												"dev",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa",
						Labels: map[string]string{
							"env": "qa",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-prod",
						Labels: map[string]string{
							"env": "prod",
						},
					},
				},
			},
			expectedList: [][]string{
				{"app-qa", "app-prod"},
			},
			expectedStepMap: map[string]int{
				"app-qa":   0,
				"app-prod": 0,
			},
		},
		{
			name: "multiple 'NotIn' selectors remove Applications with mising labels on any match",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "region",
											Operator: "NotIn",
											Values: []string{
												"us-east-2",
											},
										},
										{
											Key:      "env",
											Operator: "NotIn",
											Values: []string{
												"qa",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa1",
						Labels: map[string]string{
							"env": "qa",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa2",
						Labels: map[string]string{
							"env":    "qa",
							"region": "us-east-2",
						},
					},
				},
			},
			expectedList: [][]string{
				{},
			},
			expectedStepMap: map[string]int{},
		},
		{
			name: "multiple 'NotIn' selectors filter all matching Applications",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "region",
											Operator: "NotIn",
											Values: []string{
												"us-east-2",
											},
										},
										{
											Key:      "env",
											Operator: "NotIn",
											Values: []string{
												"qa",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa1",
						Labels: map[string]string{
							"env":    "qa",
							"region": "us-east-1",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa2",
						Labels: map[string]string{
							"env":    "qa",
							"region": "us-east-2",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-prod1",
						Labels: map[string]string{
							"env":    "prod",
							"region": "us-east-1",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-prod2",
						Labels: map[string]string{
							"env":    "prod",
							"region": "us-east-2",
						},
					},
				},
			},
			expectedList: [][]string{
				{"app-prod1"},
			},
			expectedStepMap: map[string]int{
				"app-prod1": 0,
			},
		},
		{
			name: "multiple values in the same 'NotIn' matchExpression exclude a match from any value",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "NotIn",
											Values: []string{
												"qa",
												"prod",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-dev",
						Labels: map[string]string{
							"env": "dev",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa",
						Labels: map[string]string{
							"env": "qa",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-prod",
						Labels: map[string]string{
							"env":    "prod",
							"region": "us-east-2",
						},
					},
				},
			},
			expectedList: [][]string{
				{"app-dev"},
			},
			expectedStepMap: map[string]int{
				"app-dev": 0,
			},
		},
		{
			name: "in a mix of 'In' and 'NotIn' selectors, 'NotIn' takes precedence",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values: []string{
												"qa",
												"prod",
											},
										},
										{
											Key:      "region",
											Operator: "NotIn",
											Values: []string{
												"us-west-2",
											},
										},
									},
								},
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-dev",
						Labels: map[string]string{
							"env": "dev",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa1",
						Labels: map[string]string{
							"env":    "qa",
							"region": "us-west-2",
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app-qa2",
						Labels: map[string]string{
							"env":    "qa",
							"region": "us-east-2",
						},
					},
				},
			},
			expectedList: [][]string{
				{"app-qa2"},
			},
			expectedStepMap: map[string]int{
				"app-qa2": 0,
			},
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:        client,
				Scheme:        scheme,
				Recorder:      record.NewFakeRecorder(1),
				Generators:    map[string]generators.Generator{},
				ArgoDB:        argodb,
				KubeClientset: kubeclientset,
				Metrics:       metrics,
			}

			appDependencyList, appStepMap := r.buildAppDependencyList(log.NewEntry(log.StandardLogger()), cc.appSet, cc.apps)
			assert.Equal(t, cc.expectedList, appDependencyList, "expected appDependencyList did not match actual")
			assert.Equal(t, cc.expectedStepMap, appStepMap, "expected appStepMap did not match actual")
		})
	}
}

func TestGetAppsToSync(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	client := fake.NewClientBuilder().WithScheme(scheme).Build()
	metrics := appsetmetrics.NewFakeAppsetMetrics()

	for _, cc := range []struct {
		name              string
		appSet            v1alpha1.ApplicationSet
		currentApps       []v1alpha1.Application
		appDependencyList [][]string
		expectedMap       map[string]bool
	}{
		{
			name: "handles an empty app dependency list",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
			},
			appDependencyList: [][]string{},
			expectedMap:       map[string]bool{},
		},
		{
			name: "handles missing applications with statuses",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
						{
							Application: "app2",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
					},
				},
			},
			currentApps: []v1alpha1.Application{
				{ObjectMeta: metav1.ObjectMeta{Name: "app2"}},
			},
			appDependencyList: [][]string{
				{"app1"},
				{"app2"},
			},
			expectedMap: map[string]bool{
				"app1": true,
			},
		},
		{
			name: "handles new applications with no statuses",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
			},
			currentApps: []v1alpha1.Application{
				{ObjectMeta: metav1.ObjectMeta{Name: "app1"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app2"}},
			},
			appDependencyList: [][]string{
				{"app1"},
				{"app2"},
			},
			expectedMap: map[string]bool{
				"app1": true,
			},
		},
		{
			name: "handles an empty step as completed",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
			},
			currentApps: []v1alpha1.Application{
				{ObjectMeta: metav1.ObjectMeta{Name: "app1"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app2"}},
			},
			appDependencyList: [][]string{
				{},
				{"app1", "app2"},
			},
			expectedMap: map[string]bool{
				"app1": true,
				"app2": true,
			},
		},
		{
			name: "handles healthy steps",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
						{
							Application: "app2",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
						{
							Application: "app3",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
						{
							Application: "app4",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
					},
				},
			},
			currentApps: []v1alpha1.Application{
				{ObjectMeta: metav1.ObjectMeta{Name: "app1"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app2"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app3"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app4"}},
			},
			appDependencyList: [][]string{
				{"app1", "app2"},
				{"app3", "app4"},
			},
			expectedMap: map[string]bool{
				"app1": true,
				"app2": true,
				"app3": true,
				"app4": true,
			},
		},
		{
			name: "do not consider waiting steps as completed",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Status:      v1alpha1.ProgressiveSyncWaiting,
						},
						{
							Application: "app2",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
					},
				},
			},
			currentApps: []v1alpha1.Application{
				{ObjectMeta: metav1.ObjectMeta{Name: "app1"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app2"}},
			},
			appDependencyList: [][]string{
				{"app1"},
				{"app2"},
			},
			expectedMap: map[string]bool{
				"app1": true,
			},
		},
		{
			name: "do not consider pending steps as completed",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Status:      v1alpha1.ProgressiveSyncPending,
						},
						{
							Application: "app2",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
					},
				},
			},
			currentApps: []v1alpha1.Application{
				{ObjectMeta: metav1.ObjectMeta{Name: "app1"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app2"}},
			},
			appDependencyList: [][]string{
				{"app1"},
				{"app2"},
			},
			expectedMap: map[string]bool{
				"app1": true,
			},
		},
		{
			name: "do not consider progressing steps as completed",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Status:      v1alpha1.ProgressiveSyncProgressing,
						},
						{
							Application: "app2",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
					},
				},
			},
			currentApps: []v1alpha1.Application{
				{ObjectMeta: metav1.ObjectMeta{Name: "app1"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app2"}},
			},
			appDependencyList: [][]string{
				{"app1"},
				{"app2"},
			},
			expectedMap: map[string]bool{
				"app1": true,
			},
		},
		{
			name: "Ignores applications not selected",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "old_app",
							Status:      v1alpha1.ProgressiveSyncProgressing,
						},
						{
							Application: "app1",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
						{
							Application: "app2",
							Status:      v1alpha1.ProgressiveSyncHealthy,
						},
					},
				},
			},
			currentApps: []v1alpha1.Application{
				{ObjectMeta: metav1.ObjectMeta{Name: "old_app"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app1"}},
				{ObjectMeta: metav1.ObjectMeta{Name: "app2"}},
			},
			appDependencyList: [][]string{
				{"app1"},
				{"app2"},
			},
			expectedMap: map[string]bool{
				"app1": true,
				"app2": true,
			},
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:        client,
				Scheme:        scheme,
				Recorder:      record.NewFakeRecorder(1),
				Generators:    map[string]generators.Generator{},
				ArgoDB:        argodb,
				KubeClientset: kubeclientset,
				Metrics:       metrics,
			}

			appsToSync := r.getAppsToSync(cc.appSet, cc.appDependencyList, cc.currentApps)
			assert.Equal(t, cc.expectedMap, appsToSync, "expected map did not match actual")
		})
	}
}

func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
	nowMinus5 := metav1.Time{Time: time.Now().Add(-5 * time.Minute)}
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	newDefaultAppSet := func(stepsCount int, status []v1alpha1.ApplicationSetApplicationStatus) v1alpha1.ApplicationSet {
		steps := []v1alpha1.ApplicationSetRolloutStep{}
		for range stepsCount {
			steps = append(steps, v1alpha1.ApplicationSetRolloutStep{MatchExpressions: []v1alpha1.ApplicationMatchExpression{}})
		}
		return v1alpha1.ApplicationSet{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "name",
				Namespace: "argocd",
			},
			Spec: v1alpha1.ApplicationSetSpec{
				Strategy: &v1alpha1.ApplicationSetStrategy{
					Type: "RollingSync",
					RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
						Steps: steps,
					},
				},
			},
			Status: v1alpha1.ApplicationSetStatus{
				ApplicationStatus: status,
			},
		}
	}

	newApp := func(name string, health health.HealthStatusCode, sync v1alpha1.SyncStatusCode, revision string, opState *v1alpha1.OperationState) v1alpha1.Application {
		return v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
			Status: v1alpha1.ApplicationStatus{
				ReconciledAt: &metav1.Time{Time: time.Now()},
				Health: v1alpha1.AppHealthStatus{
					Status: health,
				},
				OperationState: opState,
				Sync: v1alpha1.SyncStatus{
					Status:   sync,
					Revision: revision,
				},
			},
		}
	}

	newAppWithSpec := func(name string, health health.HealthStatusCode, sync v1alpha1.SyncStatusCode, revision string, opState *v1alpha1.OperationState, spec v1alpha1.ApplicationSpec) v1alpha1.Application {
		app := newApp(name, health, sync, revision, opState)
		app.Spec = spec
		return app
	}

	newOperationState := func(phase common.OperationPhase) *v1alpha1.OperationState {
		finishedAt := &metav1.Time{Time: time.Now().Add(-1 * time.Second)}
		if !phase.Completed() {
			finishedAt = nil
		}
		return &v1alpha1.OperationState{
			Phase:      phase,
			StartedAt:  metav1.Time{Time: time.Now().Add(-1 * time.Minute)},
			FinishedAt: finishedAt,
		}
	}

	for _, cc := range []struct {
		name              string
		appSet            v1alpha1.ApplicationSet
		apps              []v1alpha1.Application
		desiredApps       []v1alpha1.Application
		appStepMap        map[string]int
		expectedAppStatus []v1alpha1.ApplicationSetApplicationStatus
	}{
		{
			name:              "handles a nil list of statuses and no applications",
			appSet:            newDefaultAppSet(2, nil),
			apps:              []v1alpha1.Application{},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{},
		},
		{
			name:   "handles a nil list of statuses with a healthy application",
			appSet: newDefaultAppSet(2, nil),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeSynced, "next", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource has synced, updating status to Healthy",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name:   "moves a new application to healthy when app is synced and healthy",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeSynced, "current", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource has synced, updating status to Healthy",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
			},
		},
		{
			name: "moves a waiting application to healthy when app is synced and healthy",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
					TargetRevisions:    []string{"current"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeSynced, "current", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource has synced, updating status to Healthy",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
			},
		},
		{
			name:   "moves a new application to progressing when app is synced but not healthy",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusDegraded, v1alpha1.SyncStatusCodeSynced, "current", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource has synced, updating status to Progressing",
					Status:          v1alpha1.ProgressiveSyncProgressing,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
			},
		},
		{
			name: "moves an application with new revision to Healthy when it is not OutOfSync",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "Application resource has synced, updating status to Healthy",
					Status:             v1alpha1.ProgressiveSyncHealthy,
					Step:               "1",
					TargetRevisions:    []string{"previous"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeSynced, "next", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource has synced, updating status to Healthy",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "moves an application with new version to waiting when it is OutOfSync",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncHealthy,
					Step:               "1",
					TargetRevisions:    []string{"previous"},
					LastTransitionTime: &nowMinus5,
				},
				{
					Application:        "app2-multisource",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncHealthy,
					Step:               "1",
					TargetRevisions:    []string{"previous", "removed-source"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "next", nil),
				newApp("app2-multisource", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "next", nil),
			},
			appStepMap: map[string]int{
				"app1":             0,
				"app2-multisource": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         revisionChangedMsg,
					Status:          v1alpha1.ProgressiveSyncWaiting,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
				{
					Application:     "app2-multisource",
					Message:         revisionChangedMsg,
					Status:          v1alpha1.ProgressiveSyncWaiting,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "does not move a Healthy application to another status if the revision has not changed",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncHealthy,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "next", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncHealthy,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			},
		},
		{
			name: "moves a pending application to progressing when operation is running",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "next", newOperationState(common.OperationRunning)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource became Progressing, updating status from Pending to Progressing",
					Status:          v1alpha1.ProgressiveSyncProgressing,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "moves a pending application to progressing when operation is successful",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "next", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource completed a sync successfully, updating status from Pending to Progressing",
					Status:          v1alpha1.ProgressiveSyncProgressing,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "moves a pending application to progressing when sync operation has failed",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "next", newOperationState(common.OperationFailed)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource completed a sync, updating status from Pending to Progressing",
					Status:          v1alpha1.ProgressiveSyncProgressing,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "moves a pending application to progressing when sync operation had error",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "next", newOperationState(common.OperationError)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource completed a sync, updating status from Pending to Progressing",
					Status:          v1alpha1.ProgressiveSyncProgressing,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			// If an application is invalid, we move it to Progressing to avoid calling sync indefinitely.
			// It is the user responsibility to fix the error on the Application. This is different than
			// sync failures were the user is expected to configure retry as part of the sync policy.
			name: "moves a pending application with InvalidSpecError errors to progressing",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app1",
					},
					Status: v1alpha1.ApplicationStatus{
						ReconciledAt: nil,
						Conditions: []v1alpha1.ApplicationCondition{
							{
								Type:               v1alpha1.ApplicationConditionInvalidSpecError,
								Message:            "Fake invalid specs preventing app updates and sync to be trigerred",
								LastTransitionTime: &metav1.Time{Time: time.Now()},
							},
						},
						Health: v1alpha1.AppHealthStatus{
							Status: health.HealthStatusUnknown,
						},
						OperationState: nil,
						Sync: v1alpha1.SyncStatus{
							Status:   v1alpha1.SyncStatusCodeUnknown,
							Revision: "next",
						},
					},
				},
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource has error and cannot sync, updating status to Progressing",
					Status:          v1alpha1.ProgressiveSyncProgressing,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "does not move a pending application to progressing if sync happened before transition",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &metav1.Time{Time: time.Now()},
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusProgressing, v1alpha1.SyncStatusCodeSynced, "next", &v1alpha1.OperationState{
					Phase:      common.OperationSucceeded,
					StartedAt:  nowMinus5,
					FinishedAt: &metav1.Time{Time: nowMinus5.Add(5 * time.Second)},
				}),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "does not move a pending application to progressing if it has not been reconciled since transition",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &metav1.Time{Time: time.Now().Add(-2 * time.Minute)},
				},
			}),
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app1",
					},
					Status: v1alpha1.ApplicationStatus{
						ReconciledAt: &nowMinus5, // This means data is stale and we cannot trust the information in the status.
						Health: v1alpha1.AppHealthStatus{
							Status: health.HealthStatusHealthy,
						},
						OperationState: &v1alpha1.OperationState{
							Phase:      common.OperationSucceeded,
							StartedAt:  metav1.Time{Time: time.Now().Add(-1 * time.Minute)},
							FinishedAt: &metav1.Time{Time: time.Now()},
						},
						Sync: v1alpha1.SyncStatus{
							Status:   v1alpha1.SyncStatusCodeSynced,
							Revision: "next",
						},
					},
				},
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "moves a progressing application to healthy when it is synced and healthy",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncProgressing,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeSynced, "next", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "Application resource became Healthy, updating status from Progressing to Healthy",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "does not move a progressing application to healthy when it is synced and not healthy",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					Message:            "",
					Status:             v1alpha1.ProgressiveSyncProgressing,
					Step:               "1",
					TargetRevisions:    []string{"next"},
					LastTransitionTime: &nowMinus5,
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusDegraded, v1alpha1.SyncStatusCodeSynced, "next", newOperationState(common.OperationSucceeded)),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncProgressing,
					Step:            "1",
					TargetRevisions: []string{"next"},
				},
			},
		},
		{
			name: "application status is removed when applciation is deleted",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
				{
					Application:     "app2",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "current", nil),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
			},
		},
		{
			name: "application status that is not in steps is updated to -1",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
				{
					Application:     "app2",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "current", nil),
				newApp("app2", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "current", nil),
			},
			appStepMap: map[string]int{
				"app1": 0,
				// app2 is removed from step selector
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
				{
					Application:     "app2",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "-1",
					TargetRevisions: []string{"current"},
				},
			},
		},
		{
			name: "update the steps of an existing status",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "1",
					TargetRevisions: []string{"current"},
				},
			}),
			apps: []v1alpha1.Application{
				newApp("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "current", nil),
			},
			appStepMap: map[string]int{
				"app1": 1, // 1 is actually steps 2
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncPending,
					Step:            "2",
					TargetRevisions: []string{"current"},
				},
			},
		},
		{
			name: "detects spec changes when image tag changes in generator (same Git revision)",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"abc123"},
				},
			}),
			apps: []v1alpha1.Application{
				newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "abc123", nil, // Changed to OutOfSync
					v1alpha1.ApplicationSpec{
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://example.com/repo.git",
							TargetRevision: "master",
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{Name: "image.tag", Value: "v1.0.0"},
								},
							},
						},
						Destination: v1alpha1.ApplicationDestination{
							Server:    "https://kubernetes.default.svc",
							Namespace: "default",
						},
					}),
			},
			desiredApps: []v1alpha1.Application{
				newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "abc123", nil, // Changed to OutOfSync
					v1alpha1.ApplicationSpec{
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://example.com/repo.git",
							TargetRevision: "master",
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{Name: "image.tag", Value: "v2.0.0"}, // Different value
								},
							},
						},
						Destination: v1alpha1.ApplicationDestination{
							Server:    "https://kubernetes.default.svc",
							Namespace: "default",
						},
					}),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         specChangedMsg,
					Status:          v1alpha1.ProgressiveSyncWaiting,
					Step:            "1",
					TargetRevisions: []string{"abc123"},
				},
			},
		},
		{
			name: "does not detect changes when spec is identical (same Git revision)",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"abc123"},
				},
			}),
			apps: []v1alpha1.Application{
				newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeSynced, "abc123", nil,
					v1alpha1.ApplicationSpec{
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://example.com/repo.git",
							TargetRevision: "master",
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{Name: "image.tag", Value: "v1.0.0"},
								},
							},
						},
						Destination: v1alpha1.ApplicationDestination{
							Server:    "https://kubernetes.default.svc",
							Namespace: "default",
						},
					}),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			// Desired apps have identical spec
			desiredApps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app1",
					},
					Spec: v1alpha1.ApplicationSpec{
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://example.com/repo.git",
							TargetRevision: "master",
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{Name: "image.tag", Value: "v1.0.0"}, // Same value
								},
							},
						},
						Destination: v1alpha1.ApplicationDestination{
							Server:    "https://kubernetes.default.svc",
							Namespace: "default",
						},
					},
				},
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"abc123"},
				},
			},
		},
		{
			name: "detects both spec and revision changes",
			appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         "",
					Status:          v1alpha1.ProgressiveSyncHealthy,
					Step:            "1",
					TargetRevisions: []string{"abc123"}, // OLD revision in status
				},
			}),
			apps: []v1alpha1.Application{
				newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "def456", nil, // NEW revision, but OutOfSync
					v1alpha1.ApplicationSpec{
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://example.com/repo.git",
							TargetRevision: "master",
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{Name: "image.tag", Value: "v1.0.0"},
								},
							},
						},
						Destination: v1alpha1.ApplicationDestination{
							Server:    "https://kubernetes.default.svc",
							Namespace: "default",
						},
					}),
			},
			desiredApps: []v1alpha1.Application{
				newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "def456", nil,
					v1alpha1.ApplicationSpec{
						Source: &v1alpha1.ApplicationSource{
							RepoURL:        "https://example.com/repo.git",
							TargetRevision: "master",
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{Name: "image.tag", Value: "v2.0.0"}, // Changed value
								},
							},
						},
						Destination: v1alpha1.ApplicationDestination{
							Server:    "https://kubernetes.default.svc",
							Namespace: "default",
						},
					}),
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:     "app1",
					Message:         revisionAndSpecChangedMsg,
					Status:          v1alpha1.ProgressiveSyncWaiting,
					Step:            "1",
					TargetRevisions: []string{"def456"},
				},
			},
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&cc.appSet).WithStatusSubresource(&cc.appSet).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:        client,
				Scheme:        scheme,
				Recorder:      record.NewFakeRecorder(1),
				Generators:    map[string]generators.Generator{},
				ArgoDB:        argodb,
				KubeClientset: kubeclientset,
				Metrics:       metrics,
			}

			desiredApps := cc.desiredApps
			if desiredApps == nil {
				desiredApps = cc.apps
			}
			appStatuses, err := r.updateApplicationSetApplicationStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps, desiredApps, cc.appStepMap)

			// opt out of testing the LastTransitionTime is accurate
			for i := range appStatuses {
				appStatuses[i].LastTransitionTime = nil
			}
			for i := range cc.expectedAppStatus {
				cc.expectedAppStatus[i].LastTransitionTime = nil
			}

			require.NoError(t, err, "expected no errors, but errors occurred")
			assert.Equal(t, cc.expectedAppStatus, appStatuses, "expected appStatuses did not match actual")
		})
	}
}

func TestUpdateApplicationSetApplicationStatusProgress(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	for _, cc := range []struct {
		name              string
		appSet            v1alpha1.ApplicationSet
		appSyncMap        map[string]bool
		appStepMap        map[string]int
		appMap            map[string]v1alpha1.Application
		expectedAppStatus []v1alpha1.ApplicationSetApplicationStatus
	}{
		{
			name: "handles an empty appSync and appStepMap",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{},
				},
			},
			appSyncMap:        map[string]bool{},
			appStepMap:        map[string]int{},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{},
		},
		{
			name: "handles an empty strategy",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{},
				},
			},
			appSyncMap:        map[string]bool{},
			appStepMap:        map[string]int{},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{},
		},
		{
			name: "handles an empty applicationset strategy",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{},
				},
			},
			appSyncMap:        map[string]bool{},
			appStepMap:        map[string]int{},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{},
		},
		{
			name: "handles an appSyncMap with no existing statuses",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{},
				},
			},
			appSyncMap: map[string]bool{
				"app1": true,
			},
			appStepMap: map[string]int{
				"app1": 0,
				"app2": 1,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{},
		},
		{
			name: "handles updating a RollingSync status from Waiting to Pending",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application:     "app1",
							Message:         "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:          v1alpha1.ProgressiveSyncWaiting,
							Step:            "1",
							TargetRevisions: []string{"next"},
						},
					},
				},
			},
			appSyncMap: map[string]bool{
				"app1": true,
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					LastTransitionTime: nil,
					Message:            "Application moved to Pending status, watching for the Application resource to start Progressing",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
					TargetRevisions:    []string{"next"},
				},
			},
		},
		{
			name: "does not update a RollingSync status if appSyncMap is false",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
					},
				},
			},
			appSyncMap: map[string]bool{
				"app1": false,
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
			},
		},
		{
			name: "does not update a status if status is not pending",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Message:     "Application Pending status timed out while waiting to become Progressing, reset status to Healthy",
							Status:      v1alpha1.ProgressiveSyncHealthy,
							Step:        "1",
						},
					},
				},
			},
			appSyncMap: map[string]bool{
				"app1": true,
			},
			appStepMap: map[string]int{
				"app1": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					LastTransitionTime: nil,
					Message:            "Application Pending status timed out while waiting to become Progressing, reset status to Healthy",
					Status:             v1alpha1.ProgressiveSyncHealthy,
					Step:               "1",
				},
			},
		},
		{
			name: "does not update a status if maxUpdate has already been reached with RollingSync",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
									MaxUpdate: &intstr.IntOrString{
										Type:   intstr.Int,
										IntVal: 3,
									},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Message:     "Application resource became Progressing, updating status from Pending to Progressing",
							Status:      v1alpha1.ProgressiveSyncProgressing,
							Step:        "1",
						},
						{
							Application: "app2",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app3",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app4",
							Message:     "Application moved to Pending status, watching for the Application resource to start Progressing",
							Status:      v1alpha1.ProgressiveSyncPending,
							Step:        "1",
						},
					},
				},
			},
			appSyncMap: map[string]bool{
				"app1": true,
				"app2": true,
				"app3": true,
				"app4": true,
			},
			appStepMap: map[string]int{
				"app1": 0,
				"app2": 0,
				"app3": 0,
				"app4": 0,
			},
			appMap: map[string]v1alpha1.Application{
				"app1": {
					ObjectMeta: metav1.ObjectMeta{
						Name: "app1",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeOutOfSync,
						},
					},
				},
				"app2": {
					ObjectMeta: metav1.ObjectMeta{
						Name: "app2",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeOutOfSync,
						},
					},
				},
				"app3": {
					ObjectMeta: metav1.ObjectMeta{
						Name: "app3",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeOutOfSync,
						},
					},
				},
				"app4": {
					ObjectMeta: metav1.ObjectMeta{
						Name: "app4",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeOutOfSync,
						},
					},
				},
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					LastTransitionTime: nil,
					Message:            "Application resource became Progressing, updating status from Pending to Progressing",
					Status:             v1alpha1.ProgressiveSyncProgressing,
					Step:               "1",
				},
				{
					Application:        "app2",
					LastTransitionTime: nil,
					Message:            "Application moved to Pending status, watching for the Application resource to start Progressing",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
				},
				{
					Application:        "app3",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
				{
					Application:        "app4",
					LastTransitionTime: nil,
					Message:            "Application moved to Pending status, watching for the Application resource to start Progressing",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
				},
			},
		},
		{
			name: "rounds down for maxUpdate set to percentage string",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
									MaxUpdate: &intstr.IntOrString{
										Type:   intstr.String,
										StrVal: "50%",
									},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app2",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app3",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
					},
				},
			},
			appSyncMap: map[string]bool{
				"app1": true,
				"app2": true,
				"app3": true,
			},
			appStepMap: map[string]int{
				"app1": 0,
				"app2": 0,
				"app3": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					LastTransitionTime: nil,
					Message:            "Application moved to Pending status, watching for the Application resource to start Progressing",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
				},
				{
					Application:        "app2",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
				{
					Application:        "app3",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
			},
		},
		{
			name: "does not update any applications with maxUpdate set to 0",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
									MaxUpdate: &intstr.IntOrString{
										Type:   intstr.Int,
										IntVal: 0,
									},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app2",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app3",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
					},
				},
			},
			appSyncMap: map[string]bool{
				"app1": true,
				"app2": true,
				"app3": true,
			},
			appStepMap: map[string]int{
				"app1": 0,
				"app2": 0,
				"app3": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
				{
					Application:        "app2",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
				{
					Application:        "app3",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
			},
		},
		{
			name: "updates all applications with maxUpdate set to 100%",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
									MaxUpdate: &intstr.IntOrString{
										Type:   intstr.String,
										StrVal: "100%",
									},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app2",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app3",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
					},
				},
			},
			appSyncMap: map[string]bool{
				"app1": true,
				"app2": true,
				"app3": true,
			},
			appStepMap: map[string]int{
				"app1": 0,
				"app2": 0,
				"app3": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					LastTransitionTime: nil,
					Message:            "Application moved to Pending status, watching for the Application resource to start Progressing",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
				},
				{
					Application:        "app2",
					LastTransitionTime: nil,
					Message:            "Application moved to Pending status, watching for the Application resource to start Progressing",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
				},
				{
					Application:        "app3",
					LastTransitionTime: nil,
					Message:            "Application moved to Pending status, watching for the Application resource to start Progressing",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
				},
			},
		},
		{
			name: "updates at least 1 application with maxUpdate >0%",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
									MaxUpdate: &intstr.IntOrString{
										Type:   intstr.String,
										StrVal: "1%",
									},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{},
								},
							},
						},
					},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "app1",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app2",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
						{
							Application: "app3",
							Message:     "Application is out of date with the current AppSet generation, setting status to Waiting",
							Status:      v1alpha1.ProgressiveSyncWaiting,
							Step:        "1",
						},
					},
				},
			},
			appSyncMap: map[string]bool{
				"app1": true,
				"app2": true,
				"app3": true,
			},
			appStepMap: map[string]int{
				"app1": 0,
				"app2": 0,
				"app3": 0,
			},
			expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
				{
					Application:        "app1",
					LastTransitionTime: nil,
					Message:            "Application moved to Pending status, watching for the Application resource to start Progressing",
					Status:             v1alpha1.ProgressiveSyncPending,
					Step:               "1",
				},
				{
					Application:        "app2",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
				{
					Application:        "app3",
					LastTransitionTime: nil,
					Message:            "Application is out of date with the current AppSet generation, setting status to Waiting",
					Status:             v1alpha1.ProgressiveSyncWaiting,
					Step:               "1",
				},
			},
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&cc.appSet).WithStatusSubresource(&cc.appSet).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:        client,
				Scheme:        scheme,
				Recorder:      record.NewFakeRecorder(1),
				Generators:    map[string]generators.Generator{},
				ArgoDB:        argodb,
				KubeClientset: kubeclientset,
				Metrics:       metrics,
			}

			appStatuses, err := r.updateApplicationSetApplicationStatusProgress(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.appSyncMap, cc.appStepMap)

			// opt out of testing the LastTransitionTime is accurate
			for i := range appStatuses {
				appStatuses[i].LastTransitionTime = nil
			}

			require.NoError(t, err, "expected no errors, but errors occurred")
			assert.Equal(t, cc.expectedAppStatus, appStatuses, "expected appStatuses did not match actual")
		})
	}
}

func TestUpdateResourceStatus(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	for _, cc := range []struct {
		name                    string
		appSet                  v1alpha1.ApplicationSet
		apps                    []v1alpha1.Application
		expectedResources       []v1alpha1.ResourceStatus
		maxResourcesStatusCount int
	}{
		{
			name: "handles an empty application list",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Status: v1alpha1.ApplicationSetStatus{
					Resources: []v1alpha1.ResourceStatus{},
				},
			},
			apps:              []v1alpha1.Application{},
			expectedResources: nil,
		},
		{
			name: "adds status if no existing statuses",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app1",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeSynced,
						},
						Health: v1alpha1.AppHealthStatus{
							Status: health.HealthStatusHealthy,
						},
					},
				},
			},
			expectedResources: []v1alpha1.ResourceStatus{
				{
					Name:   "app1",
					Status: v1alpha1.SyncStatusCodeSynced,
					Health: &v1alpha1.HealthStatus{
						Status: health.HealthStatusHealthy,
					},
				},
			},
		},
		{
			name: "handles an applicationset with existing and up-to-date status",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Status: v1alpha1.ApplicationSetStatus{
					Resources: []v1alpha1.ResourceStatus{
						{
							Name:   "app1",
							Status: v1alpha1.SyncStatusCodeSynced,
							Health: &v1alpha1.HealthStatus{
								Status: health.HealthStatusHealthy,
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app1",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeSynced,
						},
						Health: v1alpha1.AppHealthStatus{
							Status: health.HealthStatusHealthy,
						},
					},
				},
			},
			expectedResources: []v1alpha1.ResourceStatus{
				{
					Name:   "app1",
					Status: v1alpha1.SyncStatusCodeSynced,
					Health: &v1alpha1.HealthStatus{
						Status: health.HealthStatusHealthy,
					},
				},
			},
		},
		{
			name: "updates an applicationset with existing and out of date status",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Status: v1alpha1.ApplicationSetStatus{
					Resources: []v1alpha1.ResourceStatus{
						{
							Name:   "app1",
							Status: v1alpha1.SyncStatusCodeOutOfSync,
							Health: &v1alpha1.HealthStatus{
								Status:  health.HealthStatusProgressing,
								Message: "this is progressing",
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app1",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeSynced,
						},
						Health: v1alpha1.AppHealthStatus{
							Status: health.HealthStatusHealthy,
						},
					},
				},
			},
			expectedResources: []v1alpha1.ResourceStatus{
				{
					Name:   "app1",
					Status: v1alpha1.SyncStatusCodeSynced,
					Health: &v1alpha1.HealthStatus{
						Status: health.HealthStatusHealthy,
					},
				},
			},
		},
		{
			name: "deletes an applicationset status if the application no longer exists",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Status: v1alpha1.ApplicationSetStatus{
					Resources: []v1alpha1.ResourceStatus{
						{
							Name:   "app1",
							Status: v1alpha1.SyncStatusCodeSynced,
							Health: &v1alpha1.HealthStatus{
								Status:  health.HealthStatusHealthy,
								Message: "OK",
							},
						},
					},
				},
			},
			apps:              []v1alpha1.Application{},
			expectedResources: nil,
		},
		{
			name: "truncates resources status list to",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Status: v1alpha1.ApplicationSetStatus{
					Resources: []v1alpha1.ResourceStatus{
						{
							Name:   "app1",
							Status: v1alpha1.SyncStatusCodeOutOfSync,
							Health: &v1alpha1.HealthStatus{
								Status:  health.HealthStatusProgressing,
								Message: "this is progressing",
							},
						},
						{
							Name:   "app2",
							Status: v1alpha1.SyncStatusCodeOutOfSync,
							Health: &v1alpha1.HealthStatus{
								Status:  health.HealthStatusProgressing,
								Message: "this is progressing",
							},
						},
					},
				},
			},
			apps: []v1alpha1.Application{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app1",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeSynced,
						},
						Health: v1alpha1.AppHealthStatus{
							Status: health.HealthStatusHealthy,
						},
					},
				},
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: "app2",
					},
					Status: v1alpha1.ApplicationStatus{
						Sync: v1alpha1.SyncStatus{
							Status: v1alpha1.SyncStatusCodeSynced,
						},
						Health: v1alpha1.AppHealthStatus{
							Status: health.HealthStatusHealthy,
						},
					},
				},
			},
			expectedResources: []v1alpha1.ResourceStatus{
				{
					Name:   "app1",
					Status: v1alpha1.SyncStatusCodeSynced,
					Health: &v1alpha1.HealthStatus{
						Status: health.HealthStatusHealthy,
					},
				},
			},
			maxResourcesStatusCount: 1,
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

			client := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&cc.appSet).WithObjects(&cc.appSet).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:                  client,
				Scheme:                  scheme,
				Recorder:                record.NewFakeRecorder(1),
				Generators:              map[string]generators.Generator{},
				ArgoDB:                  argodb,
				KubeClientset:           kubeclientset,
				Metrics:                 metrics,
				MaxResourcesStatusCount: cc.maxResourcesStatusCount,
			}

			err := r.updateResourcesStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps)

			require.NoError(t, err, "expected no errors, but errors occurred")
			assert.Equal(t, cc.expectedResources, cc.appSet.Status.Resources, "expected resources did not match actual")
		})
	}
}

func generateNAppResourceStatuses(n int) []v1alpha1.ResourceStatus {
	var r []v1alpha1.ResourceStatus
	for i := range n {
		r = append(r, v1alpha1.ResourceStatus{
			Name:   "app" + strconv.Itoa(i),
			Status: v1alpha1.SyncStatusCodeSynced,
			Health: &v1alpha1.HealthStatus{
				Status: health.HealthStatusHealthy,
			},
		},
		)
	}
	return r
}

func generateNHealthyApps(n int) []v1alpha1.Application {
	var r []v1alpha1.Application
	for i := range n {
		r = append(r, v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Name: "app" + strconv.Itoa(i),
			},
			Status: v1alpha1.ApplicationStatus{
				Sync: v1alpha1.SyncStatus{
					Status: v1alpha1.SyncStatusCodeSynced,
				},
				Health: v1alpha1.AppHealthStatus{
					Status: health.HealthStatusHealthy,
				},
			},
		})
	}
	return r
}

func TestResourceStatusAreOrdered(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	err = v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)
	for _, cc := range []struct {
		name              string
		appSet            v1alpha1.ApplicationSet
		apps              []v1alpha1.Application
		expectedResources []v1alpha1.ResourceStatus
	}{
		{
			name: "Ensures AppSet is always ordered",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "name",
					Namespace: "argocd",
				},
				Status: v1alpha1.ApplicationSetStatus{
					Resources: []v1alpha1.ResourceStatus{},
				},
			},
			apps:              generateNHealthyApps(10),
			expectedResources: generateNAppResourceStatuses(10),
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

			client := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&cc.appSet).WithObjects(&cc.appSet).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:        client,
				Scheme:        scheme,
				Recorder:      record.NewFakeRecorder(1),
				Generators:    map[string]generators.Generator{},
				ArgoDB:        argodb,
				KubeClientset: kubeclientset,
				Metrics:       metrics,
			}

			err := r.updateResourcesStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps)
			require.NoError(t, err, "expected no errors, but errors occurred")

			err = r.updateResourcesStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps)
			require.NoError(t, err, "expected no errors, but errors occurred")

			err = r.updateResourcesStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps)
			require.NoError(t, err, "expected no errors, but errors occurred")

			assert.Equal(t, cc.expectedResources, cc.appSet.Status.Resources, "expected resources did not match actual")
		})
	}
}

func TestApplicationOwnsHandler(t *testing.T) {
	// progressive syncs do not affect create, delete, or generic
	ownsHandler := getApplicationOwnsHandler(true)
	assert.False(t, ownsHandler.CreateFunc(event.CreateEvent{}))
	assert.True(t, ownsHandler.DeleteFunc(event.DeleteEvent{}))
	assert.True(t, ownsHandler.GenericFunc(event.GenericEvent{}))
	ownsHandler = getApplicationOwnsHandler(false)
	assert.False(t, ownsHandler.CreateFunc(event.CreateEvent{}))
	assert.True(t, ownsHandler.DeleteFunc(event.DeleteEvent{}))
	assert.True(t, ownsHandler.GenericFunc(event.GenericEvent{}))

	now := metav1.Now()
	type args struct {
		e                      event.UpdateEvent
		enableProgressiveSyncs bool
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{name: "SameApplicationReconciledAtDiff", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{ReconciledAt: &now}},
			ObjectNew: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{ReconciledAt: &now}},
		}}, want: false},
		{name: "SameApplicationResourceVersionDiff", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{
				ResourceVersion: "foo",
			}},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{
				ResourceVersion: "bar",
			}},
		}}, want: false},
		{name: "ApplicationHealthStatusDiff", args: args{
			e: event.UpdateEvent{
				ObjectOld: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{
					Health: v1alpha1.AppHealthStatus{
						Status: health.HealthStatusUnknown,
					},
				}},
				ObjectNew: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{
					Health: v1alpha1.AppHealthStatus{
						Status: health.HealthStatusHealthy,
					},
				}},
			},
			enableProgressiveSyncs: true,
		}, want: true},
		{name: "ApplicationSyncStatusDiff", args: args{
			e: event.UpdateEvent{
				ObjectOld: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{
					Sync: v1alpha1.SyncStatus{
						Status: v1alpha1.SyncStatusCodeOutOfSync,
					},
				}},
				ObjectNew: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{
					Sync: v1alpha1.SyncStatus{
						Status: v1alpha1.SyncStatusCodeSynced,
					},
				}},
			},
			enableProgressiveSyncs: true,
		}, want: true},
		{name: "ApplicationOperationStateDiff", args: args{
			e: event.UpdateEvent{
				ObjectOld: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{
					OperationState: &v1alpha1.OperationState{
						Phase: "foo",
					},
				}},
				ObjectNew: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{
					OperationState: &v1alpha1.OperationState{
						Phase: "bar",
					},
				}},
			},
			enableProgressiveSyncs: true,
		}, want: true},
		{name: "ApplicationOperationStartedAtDiff", args: args{
			e: event.UpdateEvent{
				ObjectOld: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{
					OperationState: &v1alpha1.OperationState{
						StartedAt: now,
					},
				}},
				ObjectNew: &v1alpha1.Application{Status: v1alpha1.ApplicationStatus{
					OperationState: &v1alpha1.OperationState{
						StartedAt: metav1.NewTime(now.Add(time.Minute * 1)),
					},
				}},
			},
			enableProgressiveSyncs: true,
		}, want: true},
		{name: "SameApplicationGeneration", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{
				Generation: 1,
			}},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{
				Generation: 2,
			}},
		}}, want: false},
		{name: "DifferentApplicationSpec", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Project: "default"}},
			ObjectNew: &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Project: "not-default"}},
		}}, want: true},
		{name: "DifferentApplicationLabels", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"bar": "foo"}}},
		}}, want: true},
		{name: "DifferentApplicationLabelsNil", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Labels: nil}},
		}}, want: false},
		{name: "DifferentApplicationAnnotations", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"foo": "bar"}}},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"bar": "foo"}}},
		}}, want: true},
		{name: "DifferentApplicationAnnotationsNil", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: nil}},
		}}, want: false},
		{name: "DifferentApplicationFinalizers", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"argo"}}},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"none"}}},
		}}, want: true},
		{name: "DifferentApplicationFinalizersNil", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{}}},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Finalizers: nil}},
		}}, want: false},
		{name: "ApplicationDestinationSame", args: args{
			e: event.UpdateEvent{
				ObjectOld: &v1alpha1.Application{
					Spec: v1alpha1.ApplicationSpec{
						Destination: v1alpha1.ApplicationDestination{
							Server:    "server",
							Namespace: "ns",
							Name:      "name",
						},
					},
				},
				ObjectNew: &v1alpha1.Application{
					Spec: v1alpha1.ApplicationSpec{
						Destination: v1alpha1.ApplicationDestination{
							Server:    "server",
							Namespace: "ns",
							Name:      "name",
						},
					},
				},
			},
			enableProgressiveSyncs: true,
		}, want: false},
		{name: "ApplicationDestinationDiff", args: args{
			e: event.UpdateEvent{
				ObjectOld: &v1alpha1.Application{
					Spec: v1alpha1.ApplicationSpec{
						Destination: v1alpha1.ApplicationDestination{
							Server:    "server",
							Namespace: "ns",
							Name:      "name",
						},
					},
				},
				ObjectNew: &v1alpha1.Application{
					Spec: v1alpha1.ApplicationSpec{
						Destination: v1alpha1.ApplicationDestination{
							Server:    "notSameServer",
							Namespace: "ns",
							Name:      "name",
						},
					},
				},
			},
			enableProgressiveSyncs: true,
		}, want: true},
		{name: "NotAnAppOld", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.AppProject{},
			ObjectNew: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"bar": "foo"}}},
		}}, want: false},
		{name: "NotAnAppNew", args: args{e: event.UpdateEvent{
			ObjectOld: &v1alpha1.Application{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}},
			ObjectNew: &v1alpha1.AppProject{},
		}}, want: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ownsHandler = getApplicationOwnsHandler(tt.args.enableProgressiveSyncs)
			assert.Equalf(t, tt.want, ownsHandler.UpdateFunc(tt.args.e), "UpdateFunc(%v)", tt.args.e)
		})
	}
}

func TestMigrateStatus(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	err = v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	for _, tc := range []struct {
		name           string
		appset         v1alpha1.ApplicationSet
		expectedStatus v1alpha1.ApplicationSetStatus
	}{
		{
			name: "status without applicationstatus target revisions set will default to empty list",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{},
					},
				},
			},
			expectedStatus: v1alpha1.ApplicationSetStatus{
				ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
					{
						TargetRevisions: []string{},
					},
				},
			},
		},
		{
			name: "status with applicationstatus target revisions set will do nothing",
			appset: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							TargetRevisions: []string{"current"},
						},
					},
				},
			},
			expectedStatus: v1alpha1.ApplicationSetStatus{
				ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
					{
						TargetRevisions: []string{"current"},
					},
				},
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			client := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&tc.appset).WithObjects(&tc.appset).Build()
			r := ApplicationSetReconciler{
				Client: client,
			}

			err := r.migrateStatus(t.Context(), &tc.appset)
			require.NoError(t, err)
			assert.Equal(t, tc.expectedStatus, tc.appset.Status)
		})
	}
}

func TestApplicationSetOwnsHandlerUpdate(t *testing.T) {
	buildAppSet := func(annotations map[string]string) *v1alpha1.ApplicationSet {
		return &v1alpha1.ApplicationSet{
			ObjectMeta: metav1.ObjectMeta{
				Annotations: annotations,
			},
		}
	}

	tests := []struct {
		name                   string
		appSetOld              crtclient.Object
		appSetNew              crtclient.Object
		enableProgressiveSyncs bool
		want                   bool
	}{
		{
			name: "Different Spec",
			appSetOld: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{}},
					},
				},
			},
			appSetNew: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{Git: &v1alpha1.GitGenerator{}},
					},
				},
			},
			enableProgressiveSyncs: false,
			want:                   true,
		},
		{
			name:                   "Different Annotations",
			appSetOld:              buildAppSet(map[string]string{"key1": "value1"}),
			appSetNew:              buildAppSet(map[string]string{"key1": "value2"}),
			enableProgressiveSyncs: false,
			want:                   true,
		},
		{
			name: "Different Labels",
			appSetOld: &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{"key1": "value1"},
				},
			},
			appSetNew: &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{"key1": "value2"},
				},
			},
			enableProgressiveSyncs: false,
			want:                   true,
		},
		{
			name: "Different Finalizers",
			appSetOld: &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Finalizers: []string{"finalizer1"},
				},
			},
			appSetNew: &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Finalizers: []string{"finalizer2"},
				},
			},
			enableProgressiveSyncs: false,
			want:                   true,
		},
		{
			name: "No Changes",
			appSetOld: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{}},
					},
				},
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{"key1": "value1"},
					Labels:      map[string]string{"key1": "value1"},
					Finalizers:  []string{"finalizer1"},
				},
			},
			appSetNew: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{
						{List: &v1alpha1.ListGenerator{}},
					},
				},
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{"key1": "value1"},
					Labels:      map[string]string{"key1": "value1"},
					Finalizers:  []string{"finalizer1"},
				},
			},
			enableProgressiveSyncs: false,
			want:                   false,
		},
		{
			name: "annotation removed",
			appSetOld: buildAppSet(map[string]string{
				argocommon.AnnotationApplicationSetRefresh: "true",
			}),
			appSetNew:              buildAppSet(map[string]string{}),
			enableProgressiveSyncs: false,
			want:                   false,
		},
		{
			name: "annotation not removed",
			appSetOld: buildAppSet(map[string]string{
				argocommon.AnnotationApplicationSetRefresh: "true",
			}),
			appSetNew: buildAppSet(map[string]string{
				argocommon.AnnotationApplicationSetRefresh: "true",
			}),
			enableProgressiveSyncs: false,
			want:                   false,
		},
		{
			name:      "annotation added",
			appSetOld: buildAppSet(map[string]string{}),
			appSetNew: buildAppSet(map[string]string{
				argocommon.AnnotationApplicationSetRefresh: "true",
			}),
			enableProgressiveSyncs: false,
			want:                   true,
		},
		{
			name:                   "old object is not an appset",
			appSetOld:              &v1alpha1.Application{},
			appSetNew:              buildAppSet(map[string]string{}),
			enableProgressiveSyncs: false,
			want:                   false,
		},
		{
			name:                   "new object is not an appset",
			appSetOld:              buildAppSet(map[string]string{}),
			appSetNew:              &v1alpha1.Application{},
			enableProgressiveSyncs: false,
			want:                   false,
		},
		{
			name:      "deletionTimestamp present when progressive sync enabled",
			appSetOld: buildAppSet(map[string]string{}),
			appSetNew: &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					DeletionTimestamp: &metav1.Time{Time: time.Now()},
				},
			},
			enableProgressiveSyncs: true,
			want:                   true,
		},
		{
			name:      "deletionTimestamp present when progressive sync disabled",
			appSetOld: buildAppSet(map[string]string{}),
			appSetNew: &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					DeletionTimestamp: &metav1.Time{Time: time.Now()},
				},
			},
			enableProgressiveSyncs: false,
			want:                   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ownsHandler := getApplicationSetOwnsHandler(tt.enableProgressiveSyncs)
			requeue := ownsHandler.UpdateFunc(event.UpdateEvent{
				ObjectOld: tt.appSetOld,
				ObjectNew: tt.appSetNew,
			})
			assert.Equalf(t, tt.want, requeue, "ownsHandler.UpdateFunc(%v, %v, %t)", tt.appSetOld, tt.appSetNew, tt.enableProgressiveSyncs)
		})
	}
}

func TestApplicationSetOwnsHandlerGeneric(t *testing.T) {
	ownsHandler := getApplicationSetOwnsHandler(false)
	tests := []struct {
		name string
		obj  crtclient.Object
		want bool
	}{
		{
			name: "Object is ApplicationSet",
			obj:  &v1alpha1.ApplicationSet{},
			want: true,
		},
		{
			name: "Object is not ApplicationSet",
			obj:  &v1alpha1.Application{},
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			requeue := ownsHandler.GenericFunc(event.GenericEvent{
				Object: tt.obj,
			})
			assert.Equalf(t, tt.want, requeue, "ownsHandler.GenericFunc(%v)", tt.obj)
		})
	}
}

func TestApplicationSetOwnsHandlerCreate(t *testing.T) {
	ownsHandler := getApplicationSetOwnsHandler(false)
	tests := []struct {
		name string
		obj  crtclient.Object
		want bool
	}{
		{
			name: "Object is ApplicationSet",
			obj:  &v1alpha1.ApplicationSet{},
			want: true,
		},
		{
			name: "Object is not ApplicationSet",
			obj:  &v1alpha1.Application{},
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			requeue := ownsHandler.CreateFunc(event.CreateEvent{
				Object: tt.obj,
			})
			assert.Equalf(t, tt.want, requeue, "ownsHandler.CreateFunc(%v)", tt.obj)
		})
	}
}

func TestApplicationSetOwnsHandlerDelete(t *testing.T) {
	ownsHandler := getApplicationSetOwnsHandler(false)
	tests := []struct {
		name string
		obj  crtclient.Object
		want bool
	}{
		{
			name: "Object is ApplicationSet",
			obj:  &v1alpha1.ApplicationSet{},
			want: true,
		},
		{
			name: "Object is not ApplicationSet",
			obj:  &v1alpha1.Application{},
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			requeue := ownsHandler.DeleteFunc(event.DeleteEvent{
				Object: tt.obj,
			})
			assert.Equalf(t, tt.want, requeue, "ownsHandler.DeleteFunc(%v)", tt.obj)
		})
	}
}

func TestShouldRequeueForApplicationSet(t *testing.T) {
	type args struct {
		appSetOld              *v1alpha1.ApplicationSet
		appSetNew              *v1alpha1.ApplicationSet
		enableProgressiveSyncs bool
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "NilAppSet",
			args: args{
				appSetNew:              &v1alpha1.ApplicationSet{},
				appSetOld:              nil,
				enableProgressiveSyncs: false,
			},
			want: false,
		},
		{
			name: "ApplicationSetApplicationStatusChanged",
			args: args{
				appSetOld: &v1alpha1.ApplicationSet{
					Status: v1alpha1.ApplicationSetStatus{
						ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
							{
								Application: "app1",
								Status:      v1alpha1.ProgressiveSyncHealthy,
							},
						},
					},
				},
				appSetNew: &v1alpha1.ApplicationSet{
					Status: v1alpha1.ApplicationSetStatus{
						ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
							{
								Application: "app1",
								Status:      v1alpha1.ProgressiveSyncWaiting,
							},
						},
					},
				},
				enableProgressiveSyncs: true,
			},
			want: true,
		},
		{
			name: "ApplicationSetWithDeletionTimestamp",
			args: args{
				appSetOld: &v1alpha1.ApplicationSet{
					Status: v1alpha1.ApplicationSetStatus{
						ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
							{
								Application: "app1",
								Status:      v1alpha1.ProgressiveSyncHealthy,
							},
						},
					},
				},
				appSetNew: &v1alpha1.ApplicationSet{
					ObjectMeta: metav1.ObjectMeta{
						DeletionTimestamp: &metav1.Time{Time: time.Now()},
					},
					Status: v1alpha1.ApplicationSetStatus{
						ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
							{
								Application: "app1",
								Status:      v1alpha1.ProgressiveSyncWaiting,
							},
						},
					},
				},
				enableProgressiveSyncs: false,
			},
			want: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equalf(t, tt.want, shouldRequeueForApplicationSet(tt.args.appSetOld, tt.args.appSetNew, tt.args.enableProgressiveSyncs), "shouldRequeueForApplicationSet(%v, %v)", tt.args.appSetOld, tt.args.appSetNew)
		})
	}
}

func TestIgnoreNotAllowedNamespaces(t *testing.T) {
	tests := []struct {
		name       string
		namespaces []string
		objectNS   string
		expected   bool
	}{
		{
			name:       "Namespace allowed",
			namespaces: []string{"allowed-namespace"},
			objectNS:   "allowed-namespace",
			expected:   true,
		},
		{
			name:       "Namespace not allowed",
			namespaces: []string{"allowed-namespace"},
			objectNS:   "not-allowed-namespace",
			expected:   false,
		},
		{
			name:       "Empty allowed namespaces",
			namespaces: []string{},
			objectNS:   "any-namespace",
			expected:   false,
		},
		{
			name:       "Multiple allowed namespaces",
			namespaces: []string{"allowed-namespace-1", "allowed-namespace-2"},
			objectNS:   "allowed-namespace-2",
			expected:   true,
		},
		{
			name:       "Namespace not in multiple allowed namespaces",
			namespaces: []string{"allowed-namespace-1", "allowed-namespace-2"},
			objectNS:   "not-allowed-namespace",
			expected:   false,
		},
		{
			name:       "Namespace matched by glob pattern",
			namespaces: []string{"allowed-namespace-*"},
			objectNS:   "allowed-namespace-1",
			expected:   true,
		},
		{
			name:       "Namespace matched by regex pattern",
			namespaces: []string{"/^allowed-namespace-[^-]+$/"},
			objectNS:   "allowed-namespace-1",
			expected:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			predicate := ignoreNotAllowedNamespaces(tt.namespaces)
			object := &v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: tt.objectNS,
				},
			}

			t.Run(tt.name+":Create", func(t *testing.T) {
				result := predicate.Create(event.CreateEvent{Object: object})
				assert.Equal(t, tt.expected, result)
			})

			t.Run(tt.name+":Update", func(t *testing.T) {
				result := predicate.Update(event.UpdateEvent{ObjectNew: object})
				assert.Equal(t, tt.expected, result)
			})

			t.Run(tt.name+":Delete", func(t *testing.T) {
				result := predicate.Delete(event.DeleteEvent{Object: object})
				assert.Equal(t, tt.expected, result)
			})

			t.Run(tt.name+":Generic", func(t *testing.T) {
				result := predicate.Generic(event.GenericEvent{Object: object})
				assert.Equal(t, tt.expected, result)
			})
		})
	}
}

func TestIsRollingSyncStrategy(t *testing.T) {
	tests := []struct {
		name     string
		appset   *v1alpha1.ApplicationSet
		expected bool
	}{
		{
			name: "RollingSync strategy is explicitly set",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{},
						},
					},
				},
			},
			expected: true,
		},
		{
			name: "AllAtOnce strategy is explicitly set",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "AllAtOnce",
					},
				},
			},
			expected: false,
		},
		{
			name: "Strategy is empty",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{},
				},
			},
			expected: false,
		},
		{
			name: "Strategy is nil",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: nil,
				},
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := isRollingSyncStrategy(tt.appset)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestFirstAppError(t *testing.T) {
	errA := errors.New("error from app-a")
	errB := errors.New("error from app-b")
	errC := errors.New("error from app-c")

	t.Run("returns nil for empty map", func(t *testing.T) {
		assert.NoError(t, firstAppError(map[string]error{}))
	})

	t.Run("returns the single error", func(t *testing.T) {
		assert.ErrorIs(t, firstAppError(map[string]error{"app-a": errA}), errA)
	})

	t.Run("returns error from lexicographically first app name", func(t *testing.T) {
		appErrors := map[string]error{
			"app-c": errC,
			"app-a": errA,
			"app-b": errB,
		}
		assert.ErrorIs(t, firstAppError(appErrors), errA)
	})

	t.Run("result is stable across multiple calls with same input", func(t *testing.T) {
		appErrors := map[string]error{
			"app-c": errC,
			"app-a": errA,
			"app-b": errB,
		}
		for range 10 {
			assert.ErrorIs(t, firstAppError(appErrors), errA, "firstAppError must return the same error on every call")
		}
	})
}

func TestSyncApplication(t *testing.T) {
	tests := []struct {
		name     string
		input    v1alpha1.Application
		prune    bool
		expected v1alpha1.Application
	}{
		{
			name: "Default retry limit with no SyncPolicy",
			input: v1alpha1.Application{
				Spec: v1alpha1.ApplicationSpec{},
			},
			prune: false,
			expected: v1alpha1.Application{
				Spec: v1alpha1.ApplicationSpec{},
				Operation: &v1alpha1.Operation{
					InitiatedBy: v1alpha1.OperationInitiator{
						Username:  "applicationset-controller",
						Automated: true,
					},
					Info: []*v1alpha1.Info{
						{
							Name:  "Reason",
							Value: "ApplicationSet RollingSync triggered a sync of this Application resource",
						},
					},
					Sync: &v1alpha1.SyncOperation{
						Prune: false,
					},
					Retry: v1alpha1.RetryStrategy{
						Limit: 5,
					},
				},
			},
		},
		{
			name: "Retry and SyncOptions from SyncPolicy are applied",
			input: v1alpha1.Application{
				Spec: v1alpha1.ApplicationSpec{
					SyncPolicy: &v1alpha1.SyncPolicy{
						Retry: &v1alpha1.RetryStrategy{
							Limit: 10,
						},
						SyncOptions: []string{"CreateNamespace=true"},
					},
				},
			},
			prune: true,
			expected: v1alpha1.Application{
				Spec: v1alpha1.ApplicationSpec{
					SyncPolicy: &v1alpha1.SyncPolicy{
						Retry: &v1alpha1.RetryStrategy{
							Limit: 10,
						},
						SyncOptions: []string{"CreateNamespace=true"},
					},
				},
				Operation: &v1alpha1.Operation{
					InitiatedBy: v1alpha1.OperationInitiator{
						Username:  "applicationset-controller",
						Automated: true,
					},
					Info: []*v1alpha1.Info{
						{
							Name:  "Reason",
							Value: "ApplicationSet RollingSync triggered a sync of this Application resource",
						},
					},
					Sync: &v1alpha1.SyncOperation{
						SyncOptions: []string{"CreateNamespace=true"},
						Prune:       true,
					},
					Retry: v1alpha1.RetryStrategy{
						Limit: 10,
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := syncApplication(tt.input, tt.prune)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsRollingSyncDeletionReversed(t *testing.T) {
	tests := []struct {
		name     string
		appset   *v1alpha1.ApplicationSet
		expected bool
	}{
		{
			name: "Deletion Order on strategy is set as Reverse",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "environment",
											Operator: "In",
											Values: []string{
												"dev",
											},
										},
									},
								},
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "environment",
											Operator: "In",
											Values: []string{
												"staging",
											},
										},
									},
								},
							},
						},
						DeletionOrder: ReverseDeletionOrder,
					},
				},
			},
			expected: true,
		},
		{
			name: "Deletion Order on strategy is set as AllAtOnce",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{},
						},
						DeletionOrder: AllAtOnceDeletionOrder,
					},
				},
			},
			expected: false,
		},
		{
			name: "Deletion Order on strategy is set as Reverse but no steps in RollingSync",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{},
						},
						DeletionOrder: ReverseDeletionOrder,
					},
				},
			},
			expected: false,
		},
		{
			name: "Deletion Order on strategy is set as Reverse, but AllAtOnce is explicitly set",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "AllAtOnce",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{},
						},
						DeletionOrder: ReverseDeletionOrder,
					},
				},
			},
			expected: false,
		},
		{
			name: "Strategy is Nil",
			appset: &v1alpha1.ApplicationSet{
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{},
				},
			},
			expected: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := isProgressiveSyncDeletionOrderReversed(tt.appset)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestReconcileAddsFinalizer_WhenDeletionOrderReverse(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

	for _, cc := range []struct {
		name                   string
		appSet                 v1alpha1.ApplicationSet
		progressiveSyncEnabled bool
		expectedFinalizers     []string
	}{
		{
			name: "adds finalizer when DeletionOrder is Reverse",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-appset",
					Namespace: "argocd",
					// No finalizers initially
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values:   []string{"dev"},
										},
									},
								},
							},
						},
						DeletionOrder: ReverseDeletionOrder,
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			progressiveSyncEnabled: true,
			expectedFinalizers:     []string{v1alpha1.ResourcesFinalizerName},
		},
		{
			name: "does not add finalizer when already exists and DeletionOrder is Reverse",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-appset",
					Namespace: "argocd",
					Finalizers: []string{
						v1alpha1.ResourcesFinalizerName,
					},
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values:   []string{"dev"},
										},
									},
								},
							},
						},
						DeletionOrder: ReverseDeletionOrder,
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			progressiveSyncEnabled: true,
			expectedFinalizers:     []string{v1alpha1.ResourcesFinalizerName},
		},
		{
			name: "does not add finalizer when DeletionOrder is AllAtOnce",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-appset",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values:   []string{"dev"},
										},
									},
								},
							},
						},
						DeletionOrder: AllAtOnceDeletionOrder,
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			progressiveSyncEnabled: true,
			expectedFinalizers:     nil,
		},
		{
			name: "does not add finalizer when DeletionOrder is not set",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-appset",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values:   []string{"dev"},
										},
									},
								},
							},
						},
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			progressiveSyncEnabled: true,
			expectedFinalizers:     nil,
		},
		{
			name: "does not add finalizer when progressive sync not enabled",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-appset",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Strategy: &v1alpha1.ApplicationSetStrategy{
						Type: "RollingSync",
						RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
							Steps: []v1alpha1.ApplicationSetRolloutStep{
								{
									MatchExpressions: []v1alpha1.ApplicationMatchExpression{
										{
											Key:      "env",
											Operator: "In",
											Values:   []string{"dev"},
										},
									},
								},
							},
						},
						DeletionOrder: ReverseDeletionOrder,
					},
					Template: v1alpha1.ApplicationSetTemplate{},
				},
			},
			progressiveSyncEnabled: false,
			expectedFinalizers:     nil,
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			client := fake.NewClientBuilder().
				WithScheme(scheme).
				WithObjects(&cc.appSet).
				WithStatusSubresource(&cc.appSet).
				WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).
				Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()
			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:                 client,
				Scheme:                 scheme,
				Renderer:               &utils.Render{},
				Recorder:               record.NewFakeRecorder(1),
				Generators:             map[string]generators.Generator{},
				ArgoDB:                 argodb,
				KubeClientset:          kubeclientset,
				Metrics:                metrics,
				EnableProgressiveSyncs: cc.progressiveSyncEnabled,
			}

			req := ctrl.Request{
				NamespacedName: types.NamespacedName{
					Namespace: cc.appSet.Namespace,
					Name:      cc.appSet.Name,
				},
			}

			// Run reconciliation
			_, err = r.Reconcile(t.Context(), req)
			require.NoError(t, err)

			// Fetch the updated ApplicationSet
			var updatedAppSet v1alpha1.ApplicationSet
			err = r.Get(t.Context(), req.NamespacedName, &updatedAppSet)
			require.NoError(t, err)

			// Verify the finalizers
			assert.Equal(t, cc.expectedFinalizers, updatedAppSet.Finalizers,
				"finalizers should match expected value")
		})
	}
}

func TestReconcileProgressiveSyncDisabled(t *testing.T) {
	scheme := runtime.NewScheme()
	err := v1alpha1.AddToScheme(scheme)
	require.NoError(t, err)

	kubeclientset := kubefake.NewClientset([]runtime.Object{}...)

	for _, cc := range []struct {
		name                   string
		appSet                 v1alpha1.ApplicationSet
		enableProgressiveSyncs bool
		expectedAppStatuses    []v1alpha1.ApplicationSetApplicationStatus
	}{
		{
			name: "clears applicationStatus when Progressive Sync is disabled",
			appSet: v1alpha1.ApplicationSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-appset",
					Namespace: "argocd",
				},
				Spec: v1alpha1.ApplicationSetSpec{
					Generators: []v1alpha1.ApplicationSetGenerator{},
					Template:   v1alpha1.ApplicationSetTemplate{},
				},
				Status: v1alpha1.ApplicationSetStatus{
					ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
						{
							Application: "test-appset-guestbook",
							Message:     "Application resource became Healthy, updating status from Progressing to Healthy.",
							Status:      "Healthy",
							Step:        "1",
						},
					},
				},
			},
			enableProgressiveSyncs: false,
			expectedAppStatuses:    nil,
		},
	} {
		t.Run(cc.name, func(t *testing.T) {
			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&cc.appSet).WithStatusSubresource(&cc.appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
			metrics := appsetmetrics.NewFakeAppsetMetrics()

			argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)

			r := ApplicationSetReconciler{
				Client:                 client,
				Scheme:                 scheme,
				Renderer:               &utils.Render{},
				Recorder:               record.NewFakeRecorder(1),
				Generators:             map[string]generators.Generator{},
				ArgoDB:                 argodb,
				KubeClientset:          kubeclientset,
				Metrics:                metrics,
				EnableProgressiveSyncs: cc.enableProgressiveSyncs,
			}

			req := ctrl.Request{
				NamespacedName: types.NamespacedName{
					Namespace: cc.appSet.Namespace,
					Name:      cc.appSet.Name,
				},
			}

			// Run reconciliation
			_, err = r.Reconcile(t.Context(), req)
			require.NoError(t, err)

			// Fetch the updated ApplicationSet
			var updatedAppSet v1alpha1.ApplicationSet
			err = r.Get(t.Context(), req.NamespacedName, &updatedAppSet)
			require.NoError(t, err)

			// Verify the applicationStatus field
			assert.Equal(t, cc.expectedAppStatuses, updatedAppSet.Status.ApplicationStatus, "applicationStatus should match expected value")
		})
	}
}

func startAndSyncInformer(t *testing.T, informer cache.SharedIndexInformer) context.CancelFunc {
	t.Helper()
	ctx, cancel := context.WithCancel(t.Context())
	go informer.Run(ctx.Done())
	if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
		cancel()
		t.Fatal("Timed out waiting for caches to sync")
	}
	return cancel
}
