package commands

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
	"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/oauth2"
	"google.golang.org/grpc"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/apimachinery/pkg/watch"
	"sigs.k8s.io/yaml"

	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
	accountpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/account"
	applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
	applicationsetpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/applicationset"
	certificatepkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/certificate"
	clusterpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
	gpgkeypkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/gpgkey"
	notificationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/notification"
	projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
	repocredspkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/repocreds"
	repositorypkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/repository"
	sessionpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/session"
	settingspkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/settings"
	versionpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/version"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
)

func Test_getInfos(t *testing.T) {
	testCases := []struct {
		name          string
		infos         []string
		expectedInfos []*v1alpha1.Info
	}{
		{
			name:          "empty",
			infos:         []string{},
			expectedInfos: []*v1alpha1.Info{},
		},
		{
			name:  "simple key value",
			infos: []string{"key1=value1", "key2=value2"},
			expectedInfos: []*v1alpha1.Info{
				{Name: "key1", Value: "value1"},
				{Name: "key2", Value: "value2"},
			},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			infos := getInfos(testCase.infos)
			assert.Len(t, infos, len(testCase.expectedInfos))
			sort := func(a, b *v1alpha1.Info) bool { return a.Name < b.Name }
			assert.Empty(t, cmp.Diff(testCase.expectedInfos, infos, cmpopts.SortSlices(sort)))
		})
	}
}

func Test_getRefreshType(t *testing.T) {
	refreshTypeNormal := string(v1alpha1.RefreshTypeNormal)
	refreshTypeHard := string(v1alpha1.RefreshTypeHard)
	testCases := []struct {
		refresh     bool
		hardRefresh bool
		expected    *string
	}{
		{false, false, nil},
		{false, true, &refreshTypeHard},
		{true, false, &refreshTypeNormal},
		{true, true, &refreshTypeHard},
	}

	for _, testCase := range testCases {
		t.Run(fmt.Sprintf("hardRefresh=%t refresh=%t", testCase.hardRefresh, testCase.refresh), func(t *testing.T) {
			refreshType := getRefreshType(testCase.refresh, testCase.hardRefresh)
			if testCase.expected == nil {
				assert.Nil(t, refreshType)
			} else {
				assert.NotNil(t, refreshType)
				assert.Equal(t, *testCase.expected, *refreshType)
			}
		})
	}
}

func TestFindRevisionHistoryWithoutPassedId(t *testing.T) {
	histories := v1alpha1.RevisionHistories{}

	histories = append(histories, v1alpha1.RevisionHistory{ID: 1})
	histories = append(histories, v1alpha1.RevisionHistory{ID: 2})
	histories = append(histories, v1alpha1.RevisionHistory{ID: 3})

	status := v1alpha1.ApplicationStatus{
		Resources:      nil,
		Sync:           v1alpha1.SyncStatus{},
		Health:         v1alpha1.AppHealthStatus{},
		History:        histories,
		Conditions:     nil,
		ReconciledAt:   nil,
		OperationState: nil,
		ObservedAt:     nil,
		SourceType:     "",
		Summary:        v1alpha1.ApplicationSummary{},
	}

	application := v1alpha1.Application{
		Status: status,
	}

	history, err := findRevisionHistory(&application, -1)
	require.NoError(t, err, "Find revision history should fail without errors")
	require.NotNil(t, history, "History should be found")
}

func TestPrintTreeViewAppGet(t *testing.T) {
	var nodes [3]v1alpha1.ResourceNode
	nodes[0].ResourceRef = v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5-6trpt", UID: "92c3a5fe-d13e-4ae2-b8ec-c10dd3543b28"}
	nodes[0].ParentRefs = []v1alpha1.ResourceRef{{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}}
	nodes[1].ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}
	nodes[1].ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}}
	nodes[2].ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}

	nodeMapping := make(map[string]v1alpha1.ResourceNode)
	mapParentToChild := make(map[string][]string)
	parentNode := make(map[string]struct{})

	for _, node := range nodes {
		nodeMapping[node.UID] = node

		if len(node.ParentRefs) > 0 {
			_, ok := mapParentToChild[node.ParentRefs[0].UID]
			if !ok {
				var temp []string
				mapParentToChild[node.ParentRefs[0].UID] = temp
			}
			mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID)
		} else {
			parentNode[node.UID] = struct{}{}
		}
	}

	output, _ := captureOutput(func() error {
		printTreeView(nodeMapping, mapParentToChild, parentNode, nil)
		return nil
	})

	assert.Contains(t, output, "Pod")
	assert.Contains(t, output, "ReplicaSet")
	assert.Contains(t, output, "Rollout")
	assert.Contains(t, output, "numalogic-rollout-demo-5dcd5457d5-6trpt")
}

func TestPrintTreeViewDetailedAppGet(t *testing.T) {
	var nodes [3]v1alpha1.ResourceNode
	nodes[0].ResourceRef = v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5-6trpt", UID: "92c3a5fe-d13e-4ae2-b8ec-c10dd3543b28"}
	nodes[0].Health = &v1alpha1.HealthStatus{Status: "Degraded", Message: "Readiness Gate failed"}
	nodes[0].ParentRefs = []v1alpha1.ResourceRef{{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}}
	nodes[1].ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}
	nodes[1].ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}}
	nodes[2].ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}

	nodeMapping := make(map[string]v1alpha1.ResourceNode)
	mapParentToChild := make(map[string][]string)
	parentNode := make(map[string]struct{})

	for _, node := range nodes {
		nodeMapping[node.UID] = node

		if len(node.ParentRefs) > 0 {
			_, ok := mapParentToChild[node.ParentRefs[0].UID]
			if !ok {
				var temp []string
				mapParentToChild[node.ParentRefs[0].UID] = temp
			}
			mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID)
		} else {
			parentNode[node.UID] = struct{}{}
		}
	}

	output, _ := captureOutput(func() error {
		printTreeViewDetailed(nodeMapping, mapParentToChild, parentNode, nil)
		return nil
	})

	assert.Contains(t, output, "Pod")
	assert.Contains(t, output, "ReplicaSet")
	assert.Contains(t, output, "Rollout")
	assert.Contains(t, output, "numalogic-rollout-demo-5dcd5457d5-6trpt")
	assert.Contains(t, output, "Degraded")
	assert.Contains(t, output, "Readiness Gate failed")
}

func TestFindRevisionHistoryWithoutPassedIdWithMultipleSources(t *testing.T) {
	histories := v1alpha1.RevisionHistories{}

	histories = append(histories, v1alpha1.RevisionHistory{ID: 1})
	histories = append(histories, v1alpha1.RevisionHistory{ID: 2})
	histories = append(histories, v1alpha1.RevisionHistory{ID: 3})

	status := v1alpha1.ApplicationStatus{
		Resources:      nil,
		Sync:           v1alpha1.SyncStatus{},
		Health:         v1alpha1.AppHealthStatus{},
		History:        histories,
		Conditions:     nil,
		ReconciledAt:   nil,
		OperationState: nil,
		ObservedAt:     nil,
		SourceType:     "",
		Summary:        v1alpha1.ApplicationSummary{},
	}

	application := v1alpha1.Application{
		Status: status,
	}

	history, err := findRevisionHistory(&application, -1)
	require.NoError(t, err, "Find revision history should fail without errors")
	require.NotNil(t, history, "History should be found")
}

func TestDefaultWaitOptions(t *testing.T) {
	watch := watchOpts{
		sync:      false,
		health:    false,
		operation: false,
		suspended: false,
	}
	opts := getWatchOpts(watch)
	assert.True(t, opts.sync)
	assert.True(t, opts.health)
	assert.True(t, opts.operation)
	assert.False(t, opts.suspended)
}

func TestOverrideWaitOptions(t *testing.T) {
	watch := watchOpts{
		sync:      true,
		health:    false,
		operation: false,
		suspended: false,
	}
	opts := getWatchOpts(watch)
	assert.True(t, opts.sync)
	assert.False(t, opts.health)
	assert.False(t, opts.operation)
	assert.False(t, opts.suspended)
}

func TestFindRevisionHistoryWithoutPassedIdAndEmptyHistoryList(t *testing.T) {
	histories := v1alpha1.RevisionHistories{}

	status := v1alpha1.ApplicationStatus{
		Resources:      nil,
		Sync:           v1alpha1.SyncStatus{},
		Health:         v1alpha1.AppHealthStatus{},
		History:        histories,
		Conditions:     nil,
		ReconciledAt:   nil,
		OperationState: nil,
		ObservedAt:     nil,
		SourceType:     "",
		Summary:        v1alpha1.ApplicationSummary{},
	}

	application := v1alpha1.Application{
		Status: status,
	}

	history, err := findRevisionHistory(&application, -1)

	require.Error(t, err, "Find revision history should fail with errors")
	require.Nil(t, history, "History should be empty")
	require.EqualError(t, err, "application '' should have at least two successful deployments", "Find revision history should fail with correct error message")
}

func TestFindRevisionHistoryWithPassedId(t *testing.T) {
	histories := v1alpha1.RevisionHistories{}

	histories = append(histories, v1alpha1.RevisionHistory{ID: 1})
	histories = append(histories, v1alpha1.RevisionHistory{ID: 2})
	histories = append(histories, v1alpha1.RevisionHistory{ID: 3, Revision: "123"})

	status := v1alpha1.ApplicationStatus{
		Resources:      nil,
		Sync:           v1alpha1.SyncStatus{},
		Health:         v1alpha1.AppHealthStatus{},
		History:        histories,
		Conditions:     nil,
		ReconciledAt:   nil,
		OperationState: nil,
		ObservedAt:     nil,
		SourceType:     "",
		Summary:        v1alpha1.ApplicationSummary{},
	}

	application := v1alpha1.Application{
		Status: status,
	}

	history, err := findRevisionHistory(&application, 3)
	require.NoError(t, err, "Find revision history should fail without errors")
	require.NotNil(t, history, "History should be found")
	require.Equal(t, "123", history.Revision, "Failed to find correct history with correct revision")
}

func TestFindRevisionHistoryWithPassedIdThatNotExist(t *testing.T) {
	histories := v1alpha1.RevisionHistories{}

	histories = append(histories, v1alpha1.RevisionHistory{ID: 1})
	histories = append(histories, v1alpha1.RevisionHistory{ID: 2})
	histories = append(histories, v1alpha1.RevisionHistory{ID: 3, Revision: "123"})

	status := v1alpha1.ApplicationStatus{
		Resources:      nil,
		Sync:           v1alpha1.SyncStatus{},
		Health:         v1alpha1.AppHealthStatus{},
		History:        histories,
		Conditions:     nil,
		ReconciledAt:   nil,
		OperationState: nil,
		ObservedAt:     nil,
		SourceType:     "",
		Summary:        v1alpha1.ApplicationSummary{},
	}

	application := v1alpha1.Application{
		Status: status,
	}

	history, err := findRevisionHistory(&application, 4)

	require.Error(t, err, "Find revision history should fail with errors")
	require.Nil(t, history, "History should be not found")
	require.EqualError(t, err, "application '' does not have deployment id '4' in history", "Find revision history should fail with correct error message")
}

func Test_groupObjsByKey(t *testing.T) {
	localObjs := []*unstructured.Unstructured{
		{
			Object: map[string]any{
				"apiVersion": "v1",
				"kind":       "Pod",
				"metadata": map[string]any{
					"name":      "pod-name",
					"namespace": "default",
				},
			},
		},
		{
			Object: map[string]any{
				"apiVersion": "apiextensions.k8s.io/v1",
				"kind":       "CustomResourceDefinition",
				"metadata": map[string]any{
					"name": "certificates.cert-manager.io",
				},
			},
		},
	}
	liveObjs := []*unstructured.Unstructured{
		{
			Object: map[string]any{
				"apiVersion": "v1",
				"kind":       "Pod",
				"metadata": map[string]any{
					"name":      "pod-name",
					"namespace": "default",
				},
			},
		},
		{
			Object: map[string]any{
				"apiVersion": "apiextensions.k8s.io/v1",
				"kind":       "CustomResourceDefinition",
				"metadata": map[string]any{
					"name": "certificates.cert-manager.io",
				},
			},
		},
	}

	expected := map[kube.ResourceKey]*unstructured.Unstructured{
		{Group: "", Kind: "Pod", Namespace: "default", Name: "pod-name"}:                                                       localObjs[0],
		{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition", Namespace: "", Name: "certificates.cert-manager.io"}: localObjs[1],
	}

	objByKey := groupObjsByKey(localObjs, liveObjs, "default")
	assert.Equal(t, expected, objByKey)
}

func TestFormatSyncPolicy(t *testing.T) {
	t.Run("Policy not defined", func(t *testing.T) {
		app := v1alpha1.Application{}

		policy := formatSyncPolicy(app)

		require.Equalf(t, "Manual", policy, "Incorrect policy %q, should be Manual", policy)
	})

	t.Run("Auto policy", func(t *testing.T) {
		app := v1alpha1.Application{
			Spec: v1alpha1.ApplicationSpec{
				SyncPolicy: &v1alpha1.SyncPolicy{
					Automated: &v1alpha1.SyncPolicyAutomated{},
				},
			},
		}

		policy := formatSyncPolicy(app)

		require.Equalf(t, "Auto", policy, "Incorrect policy %q, should be Auto", policy)
	})

	t.Run("Auto policy with prune", func(t *testing.T) {
		app := v1alpha1.Application{
			Spec: v1alpha1.ApplicationSpec{
				SyncPolicy: &v1alpha1.SyncPolicy{
					Automated: &v1alpha1.SyncPolicyAutomated{
						Prune: new(true),
					},
				},
			},
		}

		policy := formatSyncPolicy(app)

		require.Equalf(t, "Auto-Prune", policy, "Incorrect policy %q, should be Auto-Prune", policy)
	})
}

func TestFormatConditionSummary(t *testing.T) {
	t.Run("No conditions are defined", func(t *testing.T) {
		app := v1alpha1.Application{
			Spec: v1alpha1.ApplicationSpec{
				SyncPolicy: &v1alpha1.SyncPolicy{
					Automated: &v1alpha1.SyncPolicyAutomated{
						Prune: new(true),
					},
				},
			},
		}

		summary := formatConditionsSummary(app)
		require.Equalf(t, "<none>", summary, "Incorrect summary %q, should be <none>", summary)
	})

	t.Run("Few conditions are defined", func(t *testing.T) {
		app := v1alpha1.Application{
			Status: v1alpha1.ApplicationStatus{
				Conditions: []v1alpha1.ApplicationCondition{
					{
						Type: "type1",
					},
					{
						Type: "type1",
					},
					{
						Type: "type2",
					},
				},
			},
		}

		summary := formatConditionsSummary(app)
		require.Equalf(t, "type1(2),type2", summary, "Incorrect summary %q, should be type1(2),type2", summary)
	})

	t.Run("Conditions are sorted for idempotent summary", func(t *testing.T) {
		app := v1alpha1.Application{
			Status: v1alpha1.ApplicationStatus{
				Conditions: []v1alpha1.ApplicationCondition{
					{
						Type: "type2",
					},
					{
						Type: "type1",
					},
					{
						Type: "type1",
					},
				},
			},
		}

		summary := formatConditionsSummary(app)
		require.Equalf(t, "type1(2),type2", summary, "Incorrect summary %q, should be type1(2),type2", summary)
	})
}

func TestPrintOperationResult(t *testing.T) {
	t.Run("Operation state is empty", func(t *testing.T) {
		output, _ := captureOutput(func() error {
			printOperationResult(nil)
			return nil
		})

		require.Emptyf(t, output, "Incorrect print operation output %q, should be ''", output)
	})

	t.Run("Operation state sync result is not empty", func(t *testing.T) {
		time := metav1.Date(2020, time.November, 10, 23, 0, 0, 0, time.UTC)
		output, _ := captureOutput(func() error {
			printOperationResult(&v1alpha1.OperationState{
				SyncResult: &v1alpha1.SyncOperationResult{Revision: "revision"},
				FinishedAt: &time,
			})
			return nil
		})

		expectation := "Operation:          Sync\nSync Revision:      revision\nPhase:              \nStart:              0001-01-01 00:00:00 +0000 UTC\nFinished:           2020-11-10 23:00:00 +0000 UTC\nDuration:           2333448h16m18.871345152s\n"
		require.Equalf(t, output, expectation, "Incorrect print operation output %q, should be %q", output, expectation)
	})

	t.Run("Operation state sync result with message is not empty", func(t *testing.T) {
		time := metav1.Date(2020, time.November, 10, 23, 0, 0, 0, time.UTC)
		output, _ := captureOutput(func() error {
			printOperationResult(&v1alpha1.OperationState{
				SyncResult: &v1alpha1.SyncOperationResult{Revision: "revision"},
				FinishedAt: &time,
				Message:    "test",
			})
			return nil
		})

		expectation := "Operation:          Sync\nSync Revision:      revision\nPhase:              \nStart:              0001-01-01 00:00:00 +0000 UTC\nFinished:           2020-11-10 23:00:00 +0000 UTC\nDuration:           2333448h16m18.871345152s\nMessage:            test\n"
		require.Equalf(t, output, expectation, "Incorrect print operation output %q, should be %q", output, expectation)
	})
}

func TestPrintApplicationHistoryTable(t *testing.T) {
	histories := []v1alpha1.RevisionHistory{
		{
			ID: 1,
			Source: v1alpha1.ApplicationSource{
				TargetRevision: "1",
				RepoURL:        "test",
			},
		},
		{
			ID: 2,
			Source: v1alpha1.ApplicationSource{
				TargetRevision: "2",
				RepoURL:        "test",
			},
		},
		{
			ID: 3,
			Source: v1alpha1.ApplicationSource{
				TargetRevision: "3",
				RepoURL:        "test",
			},
		},
	}

	output, _ := captureOutput(func() error {
		printApplicationHistoryTable(histories)
		return nil
	})

	expectation := "SOURCE  test\nID      DATE                           REVISION\n1       0001-01-01 00:00:00 +0000 UTC  1\n2       0001-01-01 00:00:00 +0000 UTC  2\n3       0001-01-01 00:00:00 +0000 UTC  3\n"

	require.Equalf(t, output, expectation, "Incorrect print operation output %q, should be %q", output, expectation)
}

func TestPrintApplicationHistoryTableWithMultipleSources(t *testing.T) {
	histories := []v1alpha1.RevisionHistory{
		{
			ID: 0,
			Source: v1alpha1.ApplicationSource{
				TargetRevision: "0",
				RepoURL:        "test",
			},
		},
		{
			ID: 1,
			Revisions: []string{
				"1a",
				"1b",
			},
			// added Source just for testing the fuction
			Source: v1alpha1.ApplicationSource{
				TargetRevision: "-1",
				RepoURL:        "ignore",
			},
			Sources: v1alpha1.ApplicationSources{
				v1alpha1.ApplicationSource{
					RepoURL:        "test-1",
					TargetRevision: "1a",
				},
				v1alpha1.ApplicationSource{
					RepoURL:        "test-2",
					TargetRevision: "1b",
				},
			},
		},
		{
			ID: 2,
			Revisions: []string{
				"2a",
				"2b",
			},
			Sources: v1alpha1.ApplicationSources{
				v1alpha1.ApplicationSource{
					RepoURL:        "test-1",
					TargetRevision: "2a",
				},
				v1alpha1.ApplicationSource{
					RepoURL:        "test-2",
					TargetRevision: "2b",
				},
			},
		},
		{
			ID: 3,
			Revisions: []string{
				"3a",
				"3b",
			},
			Sources: v1alpha1.ApplicationSources{
				v1alpha1.ApplicationSource{
					RepoURL:        "test-1",
					TargetRevision: "3a",
				},
				v1alpha1.ApplicationSource{
					RepoURL:        "test-2",
					TargetRevision: "3b",
				},
			},
		},
	}

	output, _ := captureOutput(func() error {
		printApplicationHistoryTable(histories)
		return nil
	})

	expectation := "SOURCE  test\nID      DATE                           REVISION\n0       0001-01-01 00:00:00 +0000 UTC  0\n\nSOURCE  test-1\nID      DATE                           REVISION\n1       0001-01-01 00:00:00 +0000 UTC  1a\n2       0001-01-01 00:00:00 +0000 UTC  2a\n3       0001-01-01 00:00:00 +0000 UTC  3a\n\nSOURCE  test-2\nID      DATE                           REVISION\n1       0001-01-01 00:00:00 +0000 UTC  1b\n2       0001-01-01 00:00:00 +0000 UTC  2b\n3       0001-01-01 00:00:00 +0000 UTC  3b\n"

	require.Equalf(t, output, expectation, "Incorrect print operation output %q, should be %q", output, expectation)
}

func TestPrintAppSummaryTable(t *testing.T) {
	output, _ := captureOutput(func() error {
		app := &v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test",
				Namespace: "argocd",
			},
			Spec: v1alpha1.ApplicationSpec{
				SyncPolicy: &v1alpha1.SyncPolicy{
					Automated: &v1alpha1.SyncPolicyAutomated{
						Prune: new(true),
					},
				},
				Project:     "default",
				Destination: v1alpha1.ApplicationDestination{Server: "local", Namespace: "argocd"},
				Source: &v1alpha1.ApplicationSource{
					RepoURL:        "test",
					TargetRevision: "master",
					Path:           "/test",
					Helm: &v1alpha1.ApplicationSourceHelm{
						ValueFiles: []string{"path1", "path2"},
					},
					Kustomize: &v1alpha1.ApplicationSourceKustomize{NamePrefix: "prefix"},
				},
			},
			Status: v1alpha1.ApplicationStatus{
				Sync: v1alpha1.SyncStatus{
					Status: v1alpha1.SyncStatusCodeOutOfSync,
				},
				Health: v1alpha1.AppHealthStatus{
					Status: health.HealthStatusProgressing,
				},
			},
		}

		windows := &v1alpha1.SyncWindows{
			{
				Kind:     "allow",
				Schedule: "0 0 * * *",
				Duration: "24h",
				Applications: []string{
					"*-prod",
				},
				ManualSync: true,
			},
			{
				Kind:     "deny",
				Schedule: "0 0 * * *",
				Duration: "24h",
				Namespaces: []string{
					"default",
				},
			},
			{
				Kind:     "allow",
				Schedule: "0 0 * * *",
				Duration: "24h",
				Clusters: []string{
					"in-cluster",
					"cluster1",
				},
			},
		}

		printAppSummaryTable(app, "url", windows)
		return nil
	})

	expectation := `Name:               argocd/test
Project:            default
Server:             local
Namespace:          argocd
URL:                url
Source:
- Repo:             test
  Target:           master
  Path:             /test
  Helm Values:      path1,path2
  Name Prefix:      prefix
SyncWindow:         Sync Denied
Assigned Windows:   allow:0 0 * * *:24h,deny:0 0 * * *:24h,allow:0 0 * * *:24h
Sync Policy:        Automated (Prune)
Sync Status:        OutOfSync from master
Health Status:      Progressing
`
	assert.Equalf(t, expectation, output, "Incorrect print app summary output %q, should be %q", output, expectation)
}

func TestPrintAppSummaryTable_MultipleSources(t *testing.T) {
	output, _ := captureOutput(func() error {
		app := &v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test",
				Namespace: "argocd",
			},
			Spec: v1alpha1.ApplicationSpec{
				SyncPolicy: &v1alpha1.SyncPolicy{
					Automated: &v1alpha1.SyncPolicyAutomated{
						Prune: new(true),
					},
				},
				Project:     "default",
				Destination: v1alpha1.ApplicationDestination{Server: "local", Namespace: "argocd"},
				Sources: v1alpha1.ApplicationSources{
					{
						RepoURL:        "test",
						TargetRevision: "master",
						Path:           "/test",
						Helm: &v1alpha1.ApplicationSourceHelm{
							ValueFiles: []string{"path1", "path2"},
						},
						Kustomize: &v1alpha1.ApplicationSourceKustomize{NamePrefix: "prefix"},
					}, {
						RepoURL:        "test2",
						TargetRevision: "master2",
						Path:           "/test2",
					},
				},
			},
			Status: v1alpha1.ApplicationStatus{
				Sync: v1alpha1.SyncStatus{
					Status: v1alpha1.SyncStatusCodeOutOfSync,
				},
				Health: v1alpha1.AppHealthStatus{
					Status: health.HealthStatusProgressing,
				},
			},
		}

		windows := &v1alpha1.SyncWindows{
			{
				Kind:     "allow",
				Schedule: "0 0 * * *",
				Duration: "24h",
				Applications: []string{
					"*-prod",
				},
				ManualSync: true,
			},
			{
				Kind:     "deny",
				Schedule: "0 0 * * *",
				Duration: "24h",
				Namespaces: []string{
					"default",
				},
			},
			{
				Kind:     "allow",
				Schedule: "0 0 * * *",
				Duration: "24h",
				Clusters: []string{
					"in-cluster",
					"cluster1",
				},
			},
		}

		printAppSummaryTable(app, "url", windows)
		return nil
	})

	expectation := `Name:               argocd/test
Project:            default
Server:             local
Namespace:          argocd
URL:                url
Sources:
- Repo:             test
  Target:           master
  Path:             /test
  Helm Values:      path1,path2
  Name Prefix:      prefix
- Repo:             test2
  Target:           master2
  Path:             /test2
SyncWindow:         Sync Denied
Assigned Windows:   allow:0 0 * * *:24h,deny:0 0 * * *:24h,allow:0 0 * * *:24h
Sync Policy:        Automated (Prune)
Sync Status:        OutOfSync from master
Health Status:      Progressing
`
	assert.Equalf(t, expectation, output, "Incorrect print app summary output %q, should be %q", output, expectation)
}

func TestPrintAppConditions(t *testing.T) {
	output, _ := captureOutput(func() error {
		app := &v1alpha1.Application{
			Status: v1alpha1.ApplicationStatus{
				Conditions: []v1alpha1.ApplicationCondition{
					{
						Type:    v1alpha1.ApplicationConditionDeletionError,
						Message: "test",
					},
					{
						Type:    v1alpha1.ApplicationConditionExcludedResourceWarning,
						Message: "test2",
					},
					{
						Type:    v1alpha1.ApplicationConditionRepeatedResourceWarning,
						Message: "test3",
					},
				},
			},
		}
		printAppConditions(os.Stdout, app)
		return nil
	})
	expectation := "CONDITION\tMESSAGE\tLAST TRANSITION\nDeletionError\ttest\t<nil>\nExcludedResourceWarning\ttest2\t<nil>\nRepeatedResourceWarning\ttest3\t<nil>\n"
	require.Equalf(t, output, expectation, "Incorrect print app conditions output %q, should be %q", output, expectation)
}

func TestPrintParams(t *testing.T) {
	testCases := []struct {
		name           string
		app            *v1alpha1.Application
		sourcePosition int
		expectedOutput string
	}{
		{
			name: "Single Source application with valid helm parameters",
			app: &v1alpha1.Application{
				Spec: v1alpha1.ApplicationSpec{
					Source: &v1alpha1.ApplicationSource{
						Helm: &v1alpha1.ApplicationSourceHelm{
							Parameters: []v1alpha1.HelmParameter{
								{
									Name:  "name1",
									Value: "value1",
								},
								{
									Name:  "name2",
									Value: "value2",
								},
								{
									Name:  "name3",
									Value: "value3",
								},
							},
						},
					},
				},
			},
			sourcePosition: -1,
			expectedOutput: "\n\nNAME   VALUE\nname1  value1\nname2  value2\nname3  value3\n",
		},
		{
			name: "Multi-source application with a valid Source Position",
			app: &v1alpha1.Application{
				Spec: v1alpha1.ApplicationSpec{
					Sources: []v1alpha1.ApplicationSource{
						{
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{
										Name:  "nameA",
										Value: "valueA",
									},
								},
							},
						},
						{
							Helm: &v1alpha1.ApplicationSourceHelm{
								Parameters: []v1alpha1.HelmParameter{
									{
										Name:  "nameB",
										Value: "valueB",
									},
								},
							},
						},
					},
				},
			},
			sourcePosition: 1,
			expectedOutput: "\n\nNAME   VALUE\nnameA  valueA\n",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			output, _ := captureOutput(func() error {
				printParams(tc.app, tc.sourcePosition)
				return nil
			})

			require.Equalf(t, tc.expectedOutput, output, "Incorrect print params output %q, should be %q\n", output, tc.expectedOutput)
		})
	}
}

func TestAppUrlDefault(t *testing.T) {
	t.Run("Plain text", func(t *testing.T) {
		result := appURLDefault(argocdclient.NewClientOrDie(&argocdclient.ClientOptions{
			ServerAddr: "localhost:80",
			PlainText:  true,
		}), "test")
		expectation := "http://localhost:80/applications/test"
		require.Equalf(t, result, expectation, "Incorrect url %q, should be %q", result, expectation)
	})
	t.Run("https", func(t *testing.T) {
		result := appURLDefault(argocdclient.NewClientOrDie(&argocdclient.ClientOptions{
			ServerAddr: "localhost:443",
			PlainText:  false,
		}), "test")
		expectation := "https://localhost/applications/test"
		require.Equalf(t, result, expectation, "Incorrect url %q, should be %q", result, expectation)
	})
}

func TestTruncateString(t *testing.T) {
	result := truncateString("argocdtool", 2)
	expectation := "ar..."
	require.Equalf(t, result, expectation, "Incorrect truncate string %q, should be %q", result, expectation)
}

func TestGetService(t *testing.T) {
	t.Run("Server", func(t *testing.T) {
		app := &v1alpha1.Application{
			Spec: v1alpha1.ApplicationSpec{
				Destination: v1alpha1.ApplicationDestination{
					Server: "test-server",
				},
			},
		}
		result := getServer(app)
		expectation := "test-server"
		require.Equal(t, result, expectation, "Incorrect server %q, should be %q", result, expectation)
	})
	t.Run("Name", func(t *testing.T) {
		app := &v1alpha1.Application{
			Spec: v1alpha1.ApplicationSpec{
				Destination: v1alpha1.ApplicationDestination{
					Name: "test-name",
				},
			},
		}
		result := getServer(app)
		expectation := "test-name"
		require.Equal(t, result, expectation, "Incorrect server name %q, should be %q", result, expectation)
	})
}

func TestTargetObjects(t *testing.T) {
	resources := []*v1alpha1.ResourceDiff{
		{
			TargetState: "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"name\":\"test-helm-guestbook\",\"namespace\":\"argocd\"},\"spec\":{\"selector\":{\"app\":\"helm-guestbook\",\"release\":\"test\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}",
		},
		{
			TargetState: "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"name\":\"test-helm-guestbook\",\"namespace\":\"ns\"},\"spec\":{\"selector\":{\"app\":\"helm-guestbook\",\"release\":\"test\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}",
		},
	}
	objects, err := targetObjects(resources)
	require.NoError(t, err, "operation should finish without error")
	require.Lenf(t, objects, 2, "incorrect number of objects %v, should be 2", len(objects))
	require.Equalf(t, "test-helm-guestbook", objects[0].GetName(), "incorrect name %q, should be %q", objects[0].GetName(), "test-helm-guestbook")
}

func TestTargetObjects_invalid(t *testing.T) {
	resources := []*v1alpha1.ResourceDiff{{TargetState: "{"}}
	_, err := targetObjects(resources)
	assert.Error(t, err)
}

func TestCheckForDeleteEvent(t *testing.T) {
	fakeClient := new(fakeAcdClient)

	checkForDeleteEvent(t.Context(), fakeClient, "testApp")
}

func TestPrintApplicationNames(t *testing.T) {
	output, _ := captureOutput(func() error {
		app := &v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test",
			},
		}
		printApplicationNames([]v1alpha1.Application{*app, *app})
		return nil
	})
	expectation := "test\ntest\n"
	require.Equalf(t, output, expectation, "Incorrect print params output %q, should be %q", output, expectation)
}

func TestNewApplicationUnsetCommand_Validation(t *testing.T) {
	if os.Getenv("BE_CRASHER") == "1" {
		cmd := NewApplicationUnsetCommand(nil)
		cmd.SetArgs([]string{"my-app", "--source-position", "1", "--source-name", "test"})
		_ = cmd.Execute()
	}

	cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run=TestNewApplicationUnsetCommand_Validation")
	cmd.Env = append(os.Environ(), "BE_CRASHER=1")
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	err := cmd.Run()

	if e, ok := errors.AsType[*exec.ExitError](err); ok && !e.Success() {
		assert.Contains(t, stderr.String(), "Only one of source-position and source-name can be specified.")
		return
	}
	t.Fatalf("process ran with err %v, want exit status 1", err)
}

func TestNewApplicationUnsetCommand_Flags(t *testing.T) {
	cmd := NewApplicationUnsetCommand(nil)
	assert.NotNil(t, cmd)

	flag := cmd.Flags().Lookup("source-name")
	assert.NotNil(t, flag)
	assert.Equal(t, "source-name", flag.Name)

	flag = cmd.Flags().Lookup("source-position")
	assert.NotNil(t, flag)
	assert.Equal(t, "source-position", flag.Name)
}

func Test_unset(t *testing.T) {
	kustomizeSource := &v1alpha1.ApplicationSource{
		Kustomize: &v1alpha1.ApplicationSourceKustomize{
			IgnoreMissingComponents: true,
			NamePrefix:              "some-prefix",
			NameSuffix:              "some-suffix",
			Version:                 "123",
			Images: v1alpha1.KustomizeImages{
				"old1=new:tag",
				"old2=new:tag",
			},
			Replicas: []v1alpha1.KustomizeReplica{
				{
					Name:  "my-deployment",
					Count: intstr.FromInt(2),
				},
				{
					Name:  "my-statefulset",
					Count: intstr.FromInt(4),
				},
			},
		},
	}

	helmSource := &v1alpha1.ApplicationSource{
		Helm: &v1alpha1.ApplicationSourceHelm{
			IgnoreMissingValueFiles: true,
			Parameters: []v1alpha1.HelmParameter{
				{
					Name:  "name-1",
					Value: "value-1",
				},
				{
					Name:  "name-2",
					Value: "value-2",
				},
			},
			PassCredentials: true,
			ValuesObject:    &runtime.RawExtension{Raw: []byte("some: yaml")},
			ValueFiles: []string{
				"values-1.yaml",
				"values-2.yaml",
			},
		},
	}

	pluginSource := &v1alpha1.ApplicationSource{
		Plugin: &v1alpha1.ApplicationSourcePlugin{
			Env: v1alpha1.Env{
				{
					Name:  "env-1",
					Value: "env-value-1",
				},
				{
					Name:  "env-2",
					Value: "env-value-2",
				},
			},
		},
	}

	assert.Equal(t, "some-prefix", kustomizeSource.Kustomize.NamePrefix)
	updated, nothingToUnset := unset(kustomizeSource, unsetOpts{namePrefix: true})
	assert.Empty(t, kustomizeSource.Kustomize.NamePrefix)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{namePrefix: true})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.Equal(t, "some-suffix", kustomizeSource.Kustomize.NameSuffix)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{nameSuffix: true})
	assert.Empty(t, kustomizeSource.Kustomize.NameSuffix)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{nameSuffix: true})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.Equal(t, "123", kustomizeSource.Kustomize.Version)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{kustomizeVersion: true})
	assert.Empty(t, kustomizeSource.Kustomize.Version)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{kustomizeVersion: true})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.Len(t, kustomizeSource.Kustomize.Images, 2)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{kustomizeImages: []string{"old1=new:tag"}})
	assert.Len(t, kustomizeSource.Kustomize.Images, 1)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{kustomizeImages: []string{"old1=new:tag"}})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.Len(t, kustomizeSource.Kustomize.Replicas, 2)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{kustomizeReplicas: []string{"my-deployment"}})
	assert.Len(t, kustomizeSource.Kustomize.Replicas, 1)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{kustomizeReplicas: []string{"my-deployment"}})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.True(t, kustomizeSource.Kustomize.IgnoreMissingComponents)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{ignoreMissingComponents: true})
	assert.False(t, kustomizeSource.Kustomize.IgnoreMissingComponents)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(kustomizeSource, unsetOpts{ignoreMissingComponents: true})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.Len(t, helmSource.Helm.Parameters, 2)
	updated, nothingToUnset = unset(helmSource, unsetOpts{parameters: []string{"name-1"}})
	assert.Len(t, helmSource.Helm.Parameters, 1)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(helmSource, unsetOpts{parameters: []string{"name-1"}})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.Len(t, helmSource.Helm.ValueFiles, 2)
	updated, nothingToUnset = unset(helmSource, unsetOpts{valuesFiles: []string{"values-1.yaml"}})
	assert.Len(t, helmSource.Helm.ValueFiles, 1)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(helmSource, unsetOpts{valuesFiles: []string{"values-1.yaml"}})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.Equal(t, "some: yaml", helmSource.Helm.ValuesString())
	updated, nothingToUnset = unset(helmSource, unsetOpts{valuesLiteral: true})
	assert.Empty(t, helmSource.Helm.ValuesString())
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(helmSource, unsetOpts{valuesLiteral: true})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.True(t, helmSource.Helm.IgnoreMissingValueFiles)
	updated, nothingToUnset = unset(helmSource, unsetOpts{ignoreMissingValueFiles: true})
	assert.False(t, helmSource.Helm.IgnoreMissingValueFiles)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(helmSource, unsetOpts{ignoreMissingValueFiles: true})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.True(t, helmSource.Helm.PassCredentials)
	updated, nothingToUnset = unset(helmSource, unsetOpts{passCredentials: true})
	assert.False(t, helmSource.Helm.PassCredentials)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(helmSource, unsetOpts{passCredentials: true})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)

	assert.Len(t, pluginSource.Plugin.Env, 2)
	updated, nothingToUnset = unset(pluginSource, unsetOpts{pluginEnvs: []string{"env-1"}})
	assert.Len(t, pluginSource.Plugin.Env, 1)
	assert.True(t, updated)
	assert.False(t, nothingToUnset)
	updated, nothingToUnset = unset(pluginSource, unsetOpts{pluginEnvs: []string{"env-1"}})
	assert.False(t, updated)
	assert.False(t, nothingToUnset)
}

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

	testCases := []struct {
		name   string
		source v1alpha1.ApplicationSource
	}{
		{"kustomize", v1alpha1.ApplicationSource{Kustomize: &v1alpha1.ApplicationSourceKustomize{}}},
		{"helm", v1alpha1.ApplicationSource{Helm: &v1alpha1.ApplicationSourceHelm{}}},
		{"plugin", v1alpha1.ApplicationSource{Plugin: &v1alpha1.ApplicationSourcePlugin{}}},
	}

	for _, testCase := range testCases {
		testCaseCopy := testCase

		t.Run(testCaseCopy.name, func(t *testing.T) {
			t.Parallel()

			updated, nothingToUnset := unset(&testCaseCopy.source, unsetOpts{})
			assert.False(t, updated)
			assert.True(t, nothingToUnset)
		})
	}
}

func TestFilterAppResources(t *testing.T) {
	// App resources
	var (
		appReplicaSet1 = v1alpha1.ResourceStatus{
			Group:     "apps",
			Kind:      "ReplicaSet",
			Namespace: "default",
			Name:      "replicaSet-name1",
		}
		appReplicaSet2 = v1alpha1.ResourceStatus{
			Group:     "apps",
			Kind:      "ReplicaSet",
			Namespace: "default",
			Name:      "replicaSet-name2",
		}
		appJob = v1alpha1.ResourceStatus{
			Group:     "batch",
			Kind:      "Job",
			Namespace: "default",
			Name:      "job-name",
		}
		appService1 = v1alpha1.ResourceStatus{
			Group:     "",
			Kind:      "Service",
			Namespace: "default",
			Name:      "service-name1",
		}
		appService2 = v1alpha1.ResourceStatus{
			Group:     "",
			Kind:      "Service",
			Namespace: "default",
			Name:      "service-name2",
		}
		appDeployment = v1alpha1.ResourceStatus{
			Group:     "apps",
			Kind:      "Deployment",
			Namespace: "default",
			Name:      "deployment-name",
		}
	)
	app := v1alpha1.Application{
		Status: v1alpha1.ApplicationStatus{
			Resources: []v1alpha1.ResourceStatus{
				appReplicaSet1, appReplicaSet2, appJob, appService1, appService2, appDeployment,
			},
		},
	}
	// Resource filters
	var (
		blankValues = v1alpha1.SyncOperationResource{
			Group:     "",
			Kind:      "",
			Name:      "",
			Namespace: "",
			Exclude:   false,
		}
		// *:*:*
		includeAllResources = v1alpha1.SyncOperationResource{
			Group:     "*",
			Kind:      "*",
			Name:      "*",
			Namespace: "",
			Exclude:   false,
		}
		// !*:*:*
		excludeAllResources = v1alpha1.SyncOperationResource{
			Group:     "*",
			Kind:      "*",
			Name:      "*",
			Namespace: "",
			Exclude:   true,
		}
		// *:Service:*
		includeAllServiceResources = v1alpha1.SyncOperationResource{
			Group:     "*",
			Kind:      "Service",
			Name:      "*",
			Namespace: "",
			Exclude:   false,
		}
		// !*:Service:*
		excludeAllServiceResources = v1alpha1.SyncOperationResource{
			Group:     "*",
			Kind:      "Service",
			Name:      "*",
			Namespace: "",
			Exclude:   true,
		}
		// apps:ReplicaSet:*
		includeAllReplicaSetResource = v1alpha1.SyncOperationResource{
			Group:     "apps",
			Kind:      "ReplicaSet",
			Name:      "*",
			Namespace: "",
			Exclude:   false,
		}
		// apps:ReplicaSet:replicaSet-name1
		includeReplicaSet1Resource = v1alpha1.SyncOperationResource{
			Group:     "apps",
			Kind:      "ReplicaSet",
			Name:      "replicaSet-name1",
			Namespace: "",
			Exclude:   false,
		}
		// !apps:ReplicaSet:replicaSet-name2
		excludeReplicaSet2Resource = v1alpha1.SyncOperationResource{
			Group:     "apps",
			Kind:      "ReplicaSet",
			Name:      "replicaSet-name2",
			Namespace: "",
			Exclude:   true,
		}
	)

	// Filtered resources
	var (
		replicaSet1 = v1alpha1.SyncOperationResource{
			Group:     "apps",
			Kind:      "ReplicaSet",
			Namespace: "default",
			Name:      "replicaSet-name1",
		}
		replicaSet2 = v1alpha1.SyncOperationResource{
			Group:     "apps",
			Kind:      "ReplicaSet",
			Namespace: "default",
			Name:      "replicaSet-name2",
		}
		job = v1alpha1.SyncOperationResource{
			Group:     "batch",
			Kind:      "Job",
			Namespace: "default",
			Name:      "job-name",
		}
		service1 = v1alpha1.SyncOperationResource{
			Group:     "",
			Kind:      "Service",
			Namespace: "default",
			Name:      "service-name1",
		}
		service2 = v1alpha1.SyncOperationResource{
			Group:     "",
			Kind:      "Service",
			Namespace: "default",
			Name:      "service-name2",
		}
		deployment = v1alpha1.SyncOperationResource{
			Group:     "apps",
			Kind:      "Deployment",
			Namespace: "default",
			Name:      "deployment-name",
		}
	)
	tests := []struct {
		testName          string
		selectedResources []*v1alpha1.SyncOperationResource
		expectedResult    []*v1alpha1.SyncOperationResource
	}{
		// --resource apps:ReplicaSet:replicaSet-name1 --resource *:Service:*
		{
			testName:          "Include ReplicaSet replicaSet-name1 resource and all service resources",
			selectedResources: []*v1alpha1.SyncOperationResource{&includeAllServiceResources, &includeReplicaSet1Resource},
			expectedResult:    []*v1alpha1.SyncOperationResource{&replicaSet1, &service1, &service2},
		},
		// --resource apps:ReplicaSet:replicaSet-name1 --resource !*:Service:*
		{
			testName:          "Include ReplicaSet replicaSet-name1 resource and exclude all service resources",
			selectedResources: []*v1alpha1.SyncOperationResource{&excludeAllServiceResources, &includeReplicaSet1Resource},
			expectedResult:    []*v1alpha1.SyncOperationResource{&replicaSet1},
		},
		// --resource !apps:ReplicaSet:replicaSet-name2 --resource !*:Service:*
		{
			testName:          "Exclude ReplicaSet replicaSet-name2 resource and all service resources",
			selectedResources: []*v1alpha1.SyncOperationResource{&excludeReplicaSet2Resource, &excludeAllServiceResources},
			expectedResult:    []*v1alpha1.SyncOperationResource{&replicaSet1, &job, &deployment},
		},
		// --resource !apps:ReplicaSet:replicaSet-name2
		{
			testName:          "Exclude ReplicaSet replicaSet-name2 resource",
			selectedResources: []*v1alpha1.SyncOperationResource{&excludeReplicaSet2Resource},
			expectedResult:    []*v1alpha1.SyncOperationResource{&replicaSet1, &job, &service1, &service2, &deployment},
		},
		// --resource apps:ReplicaSet:replicaSet-name1
		{
			testName:          "Include ReplicaSet replicaSet-name1 resource",
			selectedResources: []*v1alpha1.SyncOperationResource{&includeReplicaSet1Resource},
			expectedResult:    []*v1alpha1.SyncOperationResource{&replicaSet1},
		},
		// --resource apps:ReplicaSet:* --resource !apps:ReplicaSet:replicaSet-name2
		{
			testName:          "Include All ReplicaSet resource and exclude replicaSet-name1 resource",
			selectedResources: []*v1alpha1.SyncOperationResource{&includeAllReplicaSetResource, &excludeReplicaSet2Resource},
			expectedResult:    []*v1alpha1.SyncOperationResource{&replicaSet1},
		},
		// --resource !*:Service:*
		{
			testName:          "Exclude Service resources",
			selectedResources: []*v1alpha1.SyncOperationResource{&excludeAllServiceResources},
			expectedResult:    []*v1alpha1.SyncOperationResource{&replicaSet1, &replicaSet2, &job, &deployment},
		},
		// --resource *:Service:*
		{
			testName:          "Include Service resources",
			selectedResources: []*v1alpha1.SyncOperationResource{&includeAllServiceResources},
			expectedResult:    []*v1alpha1.SyncOperationResource{&service1, &service2},
		},
		// --resource !*:*:*
		{
			testName:          "Exclude all resources",
			selectedResources: []*v1alpha1.SyncOperationResource{&excludeAllResources},
			expectedResult:    nil,
		},
		// --resource *:*:*
		{
			testName:          "Include all resources",
			selectedResources: []*v1alpha1.SyncOperationResource{&includeAllResources},
			expectedResult:    []*v1alpha1.SyncOperationResource{&replicaSet1, &replicaSet2, &job, &service1, &service2, &deployment},
		},
		{
			testName:          "No Filters",
			selectedResources: []*v1alpha1.SyncOperationResource{&blankValues},
			expectedResult:    nil,
		},
		{
			testName:          "Empty Filter",
			selectedResources: []*v1alpha1.SyncOperationResource{},
			expectedResult:    nil,
		},
	}

	for _, test := range tests {
		t.Run(test.testName, func(t *testing.T) {
			filteredResources := filterAppResources(&app, test.selectedResources)
			assert.Equal(t, test.expectedResult, filteredResources)
		})
	}
}

func TestParseSelectedResources(t *testing.T) {
	resources := []string{
		"v1alpha:Application:test",
		"v1alpha:Application:namespace/test",
		"!v1alpha:Application:test",
		"apps:Deployment:default/test",
		"!*:*:*",
	}
	operationResources, err := parseSelectedResources(resources)
	require.NoError(t, err)
	assert.Len(t, operationResources, 5)
	assert.Equal(t, v1alpha1.SyncOperationResource{
		Namespace: "",
		Name:      "test",
		Kind:      application.ApplicationKind,
		Group:     "v1alpha",
	}, *operationResources[0])
	assert.Equal(t, v1alpha1.SyncOperationResource{
		Namespace: "namespace",
		Name:      "test",
		Kind:      application.ApplicationKind,
		Group:     "v1alpha",
	}, *operationResources[1])
	assert.Equal(t, v1alpha1.SyncOperationResource{
		Namespace: "",
		Name:      "test",
		Kind:      "Application",
		Group:     "v1alpha",
		Exclude:   true,
	}, *operationResources[2])
	assert.Equal(t, v1alpha1.SyncOperationResource{
		Namespace: "default",
		Name:      "test",
		Kind:      "Deployment",
		Group:     "apps",
		Exclude:   false,
	}, *operationResources[3])
	assert.Equal(t, v1alpha1.SyncOperationResource{
		Namespace: "",
		Name:      "*",
		Kind:      "*",
		Group:     "*",
		Exclude:   true,
	}, *operationResources[4])
}

func TestParseSelectedResourcesIncorrect(t *testing.T) {
	resources := []string{"v1alpha:test", "v1alpha:Application:namespace/test"}
	_, err := parseSelectedResources(resources)
	assert.ErrorContains(t, err, "v1alpha:test")
}

func TestParseSelectedResourcesIncorrectNamespace(t *testing.T) {
	resources := []string{"v1alpha:Application:namespace/test/unknown"}
	_, err := parseSelectedResources(resources)
	assert.ErrorContains(t, err, "v1alpha:Application:namespace/test/unknown")
}

func TestParseSelectedResourcesEmptyList(t *testing.T) {
	var resources []string
	operationResources, err := parseSelectedResources(resources)
	require.NoError(t, err)
	assert.Empty(t, operationResources)
}

func TestPrintApplicationTableNotWide(t *testing.T) {
	output, err := captureOutput(func() error {
		app := &v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Name: "app-name",
			},
			Spec: v1alpha1.ApplicationSpec{
				Destination: v1alpha1.ApplicationDestination{
					Server:    "http://localhost:8080",
					Namespace: "default",
				},
				Project: "prj",
			},
			Status: v1alpha1.ApplicationStatus{
				Sync: v1alpha1.SyncStatus{
					Status: "OutOfSync",
				},
				Health: v1alpha1.AppHealthStatus{
					Status: "Healthy",
				},
			},
		}
		output := "table"
		printApplicationTable([]v1alpha1.Application{*app, *app}, &output)
		return nil
	})
	require.NoError(t, err)
	expectation := "NAME      CLUSTER                NAMESPACE  PROJECT  STATUS     HEALTH   SYNCPOLICY  CONDITIONS\napp-name  http://localhost:8080  default    prj      OutOfSync  Healthy  Manual      <none>\napp-name  http://localhost:8080  default    prj      OutOfSync  Healthy  Manual      <none>\n"
	assert.Equal(t, output, expectation)
}

func TestPrintApplicationTableWide(t *testing.T) {
	output, err := captureOutput(func() error {
		app := &v1alpha1.Application{
			ObjectMeta: metav1.ObjectMeta{
				Name: "app-name",
			},
			Spec: v1alpha1.ApplicationSpec{
				Destination: v1alpha1.ApplicationDestination{
					Server:    "http://localhost:8080",
					Namespace: "default",
				},
				Source: &v1alpha1.ApplicationSource{
					RepoURL:        "https://github.com/argoproj/argocd-example-apps",
					Path:           "guestbook",
					TargetRevision: "123",
				},
				Project: "prj",
			},
			Status: v1alpha1.ApplicationStatus{
				Sync: v1alpha1.SyncStatus{
					Status: "OutOfSync",
				},
				Health: v1alpha1.AppHealthStatus{
					Status: "Healthy",
				},
			},
		}
		output := "wide"
		printApplicationTable([]v1alpha1.Application{*app, *app}, &output)
		return nil
	})
	require.NoError(t, err)
	expectation := "NAME      CLUSTER                NAMESPACE  PROJECT  STATUS     HEALTH   SYNCPOLICY  CONDITIONS  REPO                                             PATH       TARGET\napp-name  http://localhost:8080  default    prj      OutOfSync  Healthy  Manual      <none>      https://github.com/argoproj/argocd-example-apps  guestbook  123\napp-name  http://localhost:8080  default    prj      OutOfSync  Healthy  Manual      <none>      https://github.com/argoproj/argocd-example-apps  guestbook  123\n"
	assert.Equal(t, output, expectation)
}

func TestResourceStateKey(t *testing.T) {
	rst := resourceState{
		Group:     "group",
		Kind:      "kind",
		Namespace: "namespace",
		Name:      "name",
	}

	key := rst.Key()
	assert.Equal(t, "group/kind/namespace/name", key)
}

func TestFormatItems(t *testing.T) {
	rst := resourceState{
		Group:     "group",
		Kind:      "kind",
		Namespace: "namespace",
		Name:      "name",
		Status:    "status",
		Health:    "health",
		Hook:      "hook",
		Message:   "message",
	}
	items := rst.FormatItems()
	assert.Equal(t, "group", items[1])
	assert.Equal(t, "kind", items[2])
	assert.Equal(t, "namespace", items[3])
	assert.Equal(t, "name", items[4])
	assert.Equal(t, "status", items[5])
	assert.Equal(t, "health", items[6])
	assert.Equal(t, "hook", items[7])
	assert.Equal(t, "message", items[8])
}

func TestMerge(t *testing.T) {
	rst := resourceState{
		Group:     "group",
		Kind:      "kind",
		Namespace: "namespace",
		Name:      "name",
		Status:    "status",
		Health:    "health",
		Hook:      "hook",
		Message:   "message",
	}

	rstNew := resourceState{
		Group:     "group",
		Kind:      "kind",
		Namespace: "namespace",
		Name:      "name",
		Status:    "status",
		Health:    "health",
		Hook:      "hook2",
		Message:   "message2",
	}

	updated := rst.Merge(&rstNew)
	assert.True(t, updated)
	assert.Equal(t, rstNew.Hook, rst.Hook)
	assert.Equal(t, rstNew.Message, rst.Message)
	assert.Equal(t, rstNew.Status, rst.Status)
}

func TestMergeWitoutUpdate(t *testing.T) {
	rst := resourceState{
		Group:     "group",
		Kind:      "kind",
		Namespace: "namespace",
		Name:      "name",
		Status:    "status",
		Health:    "health",
		Hook:      "hook",
		Message:   "message",
	}

	rstNew := resourceState{
		Group:     "group",
		Kind:      "kind",
		Namespace: "namespace",
		Name:      "name",
		Status:    "status",
		Health:    "health",
		Hook:      "hook",
		Message:   "message",
	}

	updated := rst.Merge(&rstNew)
	assert.False(t, updated)
}

func TestCheckResourceStatus(t *testing.T) {
	t.Run("Degraded, Suspended and health status passed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: true,
			health:    true,
			degraded:  true,
		}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.True(t, res)
	})
	t.Run("Degraded, Suspended and health status failed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: true,
			health:    true,
			degraded:  true,
		}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.False(t, res)
	})
	t.Run("Suspended and health status passed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: true,
			health:    true,
		}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.True(t, res)
	})
	t.Run("Suspended and health status failed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: true,
			health:    true,
		}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.False(t, res)
	})
	t.Run("Suspended passed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: true,
			health:    false,
		}, string(health.HealthStatusSuspended), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.True(t, res)
	})
	t.Run("Suspended failed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: true,
			health:    false,
		}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.False(t, res)
	})
	t.Run("Health passed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: false,
			health:    true,
		}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.True(t, res)
	})
	t.Run("Health failed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: false,
			health:    true,
		}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.False(t, res)
	})
	t.Run("Synced passed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.True(t, res)
	})
	t.Run("Synced failed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeOutOfSync), &v1alpha1.Operation{}, true)
		assert.True(t, res)
	})
	t.Run("Degraded passed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: false,
			health:    false,
			degraded:  true,
		}, string(health.HealthStatusDegraded), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.True(t, res)
	})
	t.Run("Degraded failed", func(t *testing.T) {
		res := checkResourceStatus(watchOpts{
			suspended: false,
			health:    false,
			degraded:  true,
		}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
		assert.False(t, res)
	})
}

func Test_hasAppChanged(t *testing.T) {
	type args struct {
		appReq *v1alpha1.Application
		appRes *v1alpha1.Application
		upsert bool
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "App has changed - Labels, Annotations, Finalizers empty",
			args: args{
				appReq: testApp("foo", "default", map[string]string{}, map[string]string{}, []string{}),
				appRes: testApp("foo", "foo", nil, nil, nil),
				upsert: true,
			},
			want: true,
		},
		{
			name: "App unchanged - Labels, Annotations, Finalizers populated",
			args: args{
				appReq: testApp("foo", "default", map[string]string{"foo": "bar"}, map[string]string{"foo": "bar"}, []string{"foo"}),
				appRes: testApp("foo", "default", map[string]string{"foo": "bar"}, map[string]string{"foo": "bar"}, []string{"foo"}),
				upsert: true,
			},
			want: false,
		},
		{
			name: "Apps unchanged - Using empty maps/list locally versus server returning nil",
			args: args{
				appReq: testApp("foo", "default", map[string]string{}, map[string]string{}, []string{}),
				appRes: testApp("foo", "default", nil, nil, nil),
				upsert: true,
			},
			want: false,
		},
		{
			name: "App unchanged - Using empty project locally versus server returning default",
			args: args{
				appReq: testApp("foo", "", map[string]string{}, map[string]string{}, []string{}),
				appRes: testApp("foo", "default", nil, nil, nil),
			},
			want: false,
		},
		{
			name: "App unchanged - From upsert=false",
			args: args{
				appReq: testApp("foo", "foo", map[string]string{}, map[string]string{}, []string{}),
				appRes: testApp("foo", "default", nil, nil, nil),
				upsert: false,
			},
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := hasAppChanged(tt.args.appReq, tt.args.appRes, tt.args.upsert)
			assert.Equalf(t, tt.want, got, "hasAppChanged() = %v, want %v", got, tt.want)
		})
	}
}

func testApp(name, project string, labels map[string]string, annotations map[string]string, finalizers []string) *v1alpha1.Application {
	return &v1alpha1.Application{
		ObjectMeta: metav1.ObjectMeta{
			Name:        name,
			Labels:      labels,
			Annotations: annotations,
			Finalizers:  finalizers,
		},
		Spec: v1alpha1.ApplicationSpec{
			Source: &v1alpha1.ApplicationSource{
				RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
			},
			Project: project,
		},
	}
}

func TestWaitOnApplicationStatus_JSON_YAML_WideOutput(t *testing.T) {
	acdClient := &customAcdClient{&fakeAcdClient{}}
	ctx := t.Context()
	var selectResource []*v1alpha1.SyncOperationResource
	watch := watchOpts{
		sync:      false,
		health:    false,
		operation: true,
		suspended: false,
	}
	watch = getWatchOpts(watch)

	output, err := captureOutput(func() error {
		_, _, _ = waitOnApplicationStatus(ctx, acdClient, "app-name", 0, watch, selectResource, "json")
		return nil
	},
	)
	require.NoError(t, err)
	assert.True(t, json.Valid([]byte(output)))

	output, err = captureOutput(func() error {
		_, _, _ = waitOnApplicationStatus(ctx, acdClient, "app-name", 0, watch, selectResource, "yaml")
		return nil
	})

	require.NoError(t, err)
	err = yaml.Unmarshal([]byte(output), &v1alpha1.Application{})
	require.NoError(t, err)

	output, _ = captureOutput(func() error {
		_, _, _ = waitOnApplicationStatus(ctx, acdClient, "app-name", 0, watch, selectResource, "")
		return nil
	})
	timeStr := time.Now().Format("2006-01-02T15:04:05-07:00")

	expectation := `TIMESTAMP                  GROUP        KIND   NAMESPACE                  NAME    STATUS   HEALTH        HOOK  MESSAGE
%s            Service     default         service-name1    Synced  Healthy              
%s   apps  Deployment     default                  test    Synced  Healthy              

Name:               argocd/test
Project:            default
Server:             local
Namespace:          argocd
URL:                http://localhost:8080/applications/app-name
Source:
- Repo:             test
  Target:           master
  Path:             /test
  Helm Values:      path1,path2
  Name Prefix:      prefix
SyncWindow:         Sync Allowed
Sync Policy:        Automated (Prune)
Sync Status:        OutOfSync from master
Health Status:      Progressing

Operation:          Sync
Sync Revision:      revision
Phase:              
Start:              0001-01-01 00:00:00 +0000 UTC
Finished:           2020-11-10 23:00:00 +0000 UTC
Duration:           2333448h16m18.871345152s
Message:            test

GROUP  KIND        NAMESPACE  NAME           STATUS  HEALTH   HOOK  MESSAGE
       Service     default    service-name1  Synced  Healthy        
apps   Deployment  default    test           Synced  Healthy        
`
	expectation = fmt.Sprintf(expectation, timeStr, timeStr)
	expectationParts := strings.Split(expectation, "\n")
	slices.Sort(expectationParts)
	expectationSorted := strings.Join(expectationParts, "\n")
	outputParts := strings.Split(output, "\n")
	slices.Sort(outputParts)
	outputSorted := strings.Join(outputParts, "\n")
	// Need to compare sorted since map entries may not keep a specific order during serialization, leading to flakiness.
	assert.Equalf(t, expectationSorted, outputSorted, "Incorrect output %q, should be %q (items order doesn't matter)", output, expectation)
}

func TestWaitOnApplicationStatus_JSON_YAML_WideOutput_With_Timeout(t *testing.T) {
	acdClient := &customAcdClient{&fakeAcdClient{simulateTimeout: 15}}
	ctx := t.Context()
	var selectResource []*v1alpha1.SyncOperationResource
	watch := watchOpts{
		sync:      false,
		health:    false,
		operation: true,
		suspended: false,
	}
	watch = getWatchOpts(watch)

	output, _ := captureOutput(func() error {
		_, _, _ = waitOnApplicationStatus(ctx, acdClient, "app-name", 5, watch, selectResource, "")
		return nil
	})
	timeStr := time.Now().Format("2006-01-02T15:04:05-07:00")

	expectation := `TIMESTAMP                  GROUP        KIND   NAMESPACE                  NAME    STATUS   HEALTH        HOOK  MESSAGE
%s            Service     default         service-name1    Synced  Healthy              
%s   apps  Deployment     default                  test    Synced  Healthy              

The command timed out waiting for the conditions to be met.

This is the state of the app after wait timed out:

Name:               argocd/test
Project:            default
Server:             local
Namespace:          argocd
URL:                http://localhost:8080/applications/app-name
Source:
- Repo:             test
  Target:           master
  Path:             /test
  Helm Values:      path1,path2
  Name Prefix:      prefix
SyncWindow:         Sync Allowed
Sync Policy:        Automated (Prune)
Sync Status:        OutOfSync from master
Health Status:      Progressing

Operation:          Sync
Sync Revision:      revision
Phase:              
Start:              0001-01-01 00:00:00 +0000 UTC
Finished:           2020-11-10 23:00:00 +0000 UTC
Duration:           2333448h16m18.871345152s
Message:            test

GROUP  KIND        NAMESPACE  NAME           STATUS  HEALTH   HOOK  MESSAGE
       Service     default    service-name1  Synced  Healthy        
apps   Deployment  default    test           Synced  Healthy        
`
	expectation = fmt.Sprintf(expectation, timeStr, timeStr)
	expectationParts := strings.Split(expectation, "\n")
	slices.Sort(expectationParts)
	expectationSorted := strings.Join(expectationParts, "\n")
	outputParts := strings.Split(output, "\n")
	slices.Sort(outputParts)
	outputSorted := strings.Join(outputParts, "\n")
	// Need to compare sorted since map entries may not keep a specific order during serialization, leading to flakiness.
	assert.Equalf(t, expectationSorted, outputSorted, "Incorrect output %q, should be %q (items order doesn't matter)", output, expectation)
}

type customAcdClient struct {
	*fakeAcdClient
}

func (c *customAcdClient) WatchApplicationWithRetry(ctx context.Context, _ string, _ string) chan *v1alpha1.ApplicationWatchEvent {
	appEventsCh := make(chan *v1alpha1.ApplicationWatchEvent)
	_, appClient := c.NewApplicationClientOrDie()
	app, _ := appClient.Get(ctx, &applicationpkg.ApplicationQuery{})

	newApp := v1alpha1.Application{
		TypeMeta:   app.TypeMeta,
		ObjectMeta: app.ObjectMeta,
		Spec:       app.Spec,
		Status:     app.Status,
		Operation:  app.Operation,
	}

	go func() {
		time.Sleep(time.Duration(c.simulateTimeout) * time.Second)
		appEventsCh <- &v1alpha1.ApplicationWatchEvent{
			Type:        watch.Bookmark,
			Application: newApp,
		}
		close(appEventsCh)
	}()

	return appEventsCh
}

func (c *customAcdClient) NewApplicationClientOrDie() (io.Closer, applicationpkg.ApplicationServiceClient) {
	return &fakeConnection{}, &fakeAppServiceClient{}
}

func (c *customAcdClient) NewSettingsClientOrDie() (io.Closer, settingspkg.SettingsServiceClient) {
	return &fakeConnection{}, &fakeSettingsServiceClient{}
}

type fakeConnection struct{}

func (c *fakeConnection) Close() error {
	return nil
}

type fakeSettingsServiceClient struct{}

func (f fakeSettingsServiceClient) Get(_ context.Context, _ *settingspkg.SettingsQuery, _ ...grpc.CallOption) (*settingspkg.Settings, error) {
	return &settingspkg.Settings{
		URL: "http://localhost:8080",
	}, nil
}

func (f fakeSettingsServiceClient) GetPlugins(_ context.Context, _ *settingspkg.SettingsQuery, _ ...grpc.CallOption) (*settingspkg.SettingsPluginsResponse, error) {
	return nil, nil
}

type fakeAppServiceClient struct{}

func (c *fakeAppServiceClient) Get(_ context.Context, _ *applicationpkg.ApplicationQuery, _ ...grpc.CallOption) (*v1alpha1.Application, error) {
	time := metav1.Date(2020, time.November, 10, 23, 0, 0, 0, time.UTC)
	return &v1alpha1.Application{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test",
			Namespace: "argocd",
		},
		Spec: v1alpha1.ApplicationSpec{
			SyncPolicy: &v1alpha1.SyncPolicy{
				Automated: &v1alpha1.SyncPolicyAutomated{
					Prune: new(true),
				},
			},
			Project:     "default",
			Destination: v1alpha1.ApplicationDestination{Server: "local", Namespace: "argocd"},
			Source: &v1alpha1.ApplicationSource{
				RepoURL:        "test",
				TargetRevision: "master",
				Path:           "/test",
				Helm: &v1alpha1.ApplicationSourceHelm{
					ValueFiles: []string{"path1", "path2"},
				},
				Kustomize: &v1alpha1.ApplicationSourceKustomize{NamePrefix: "prefix"},
			},
		},
		Status: v1alpha1.ApplicationStatus{
			Resources: []v1alpha1.ResourceStatus{
				{
					Group:     "",
					Kind:      "Service",
					Namespace: "default",
					Name:      "service-name1",
					Status:    "Synced",
					Health: &v1alpha1.HealthStatus{
						Status:  health.HealthStatusHealthy,
						Message: "health-message",
					},
				},
				{
					Group:     "apps",
					Kind:      "Deployment",
					Namespace: "default",
					Name:      "test",
					Status:    "Synced",
					Health: &v1alpha1.HealthStatus{
						Status:  health.HealthStatusHealthy,
						Message: "health-message",
					},
				},
			},
			OperationState: &v1alpha1.OperationState{
				SyncResult: &v1alpha1.SyncOperationResult{
					Revision: "revision",
				},
				FinishedAt: &time,
				Message:    "test",
			},
			Sync: v1alpha1.SyncStatus{
				Status: v1alpha1.SyncStatusCodeOutOfSync,
			},
			Health: v1alpha1.AppHealthStatus{
				Status: health.HealthStatusProgressing,
			},
		},
	}, nil
}

func (c *fakeAppServiceClient) List(_ context.Context, _ *applicationpkg.ApplicationQuery, _ ...grpc.CallOption) (*v1alpha1.ApplicationList, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) ListResourceEvents(_ context.Context, _ *applicationpkg.ApplicationResourceEventsQuery, _ ...grpc.CallOption) (*corev1.EventList, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) Watch(_ context.Context, _ *applicationpkg.ApplicationQuery, _ ...grpc.CallOption) (applicationpkg.ApplicationService_WatchClient, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) Create(_ context.Context, _ *applicationpkg.ApplicationCreateRequest, _ ...grpc.CallOption) (*v1alpha1.Application, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) GetApplicationSyncWindows(_ context.Context, _ *applicationpkg.ApplicationSyncWindowsQuery, _ ...grpc.CallOption) (*applicationpkg.ApplicationSyncWindowsResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) GetOCIMetadata(_ context.Context, _ *applicationpkg.RevisionMetadataQuery, _ ...grpc.CallOption) (*v1alpha1.OCIMetadata, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) RevisionMetadata(_ context.Context, _ *applicationpkg.RevisionMetadataQuery, _ ...grpc.CallOption) (*v1alpha1.RevisionMetadata, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) RevisionChartDetails(_ context.Context, _ *applicationpkg.RevisionMetadataQuery, _ ...grpc.CallOption) (*v1alpha1.ChartDetails, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) GetManifests(_ context.Context, _ *applicationpkg.ApplicationManifestQuery, _ ...grpc.CallOption) (*apiclient.ManifestResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) GetManifestsWithFiles(_ context.Context, _ ...grpc.CallOption) (applicationpkg.ApplicationService_GetManifestsWithFilesClient, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) Update(_ context.Context, _ *applicationpkg.ApplicationUpdateRequest, _ ...grpc.CallOption) (*v1alpha1.Application, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) UpdateSpec(_ context.Context, _ *applicationpkg.ApplicationUpdateSpecRequest, _ ...grpc.CallOption) (*v1alpha1.ApplicationSpec, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) Patch(_ context.Context, _ *applicationpkg.ApplicationPatchRequest, _ ...grpc.CallOption) (*v1alpha1.Application, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) Delete(_ context.Context, _ *applicationpkg.ApplicationDeleteRequest, _ ...grpc.CallOption) (*applicationpkg.ApplicationResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) Sync(_ context.Context, _ *applicationpkg.ApplicationSyncRequest, _ ...grpc.CallOption) (*v1alpha1.Application, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) ManagedResources(_ context.Context, _ *applicationpkg.ResourcesQuery, _ ...grpc.CallOption) (*applicationpkg.ManagedResourcesResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) ResourceTree(_ context.Context, _ *applicationpkg.ResourcesQuery, _ ...grpc.CallOption) (*v1alpha1.ApplicationTree, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) WatchResourceTree(_ context.Context, _ *applicationpkg.ResourcesQuery, _ ...grpc.CallOption) (applicationpkg.ApplicationService_WatchResourceTreeClient, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) Rollback(_ context.Context, _ *applicationpkg.ApplicationRollbackRequest, _ ...grpc.CallOption) (*v1alpha1.Application, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) TerminateOperation(_ context.Context, _ *applicationpkg.OperationTerminateRequest, _ ...grpc.CallOption) (*applicationpkg.OperationTerminateResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) GetResource(_ context.Context, _ *applicationpkg.ApplicationResourceRequest, _ ...grpc.CallOption) (*applicationpkg.ApplicationResourceResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) PatchResource(_ context.Context, _ *applicationpkg.ApplicationResourcePatchRequest, _ ...grpc.CallOption) (*applicationpkg.ApplicationResourceResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) ListResourceActions(_ context.Context, _ *applicationpkg.ApplicationResourceRequest, _ ...grpc.CallOption) (*applicationpkg.ResourceActionsListResponse, error) {
	return nil, nil
}

// nolint:staticcheck // ResourceActionRunRequest is deprecated, but we still need to implement it to satisfy the server interface.
func (c *fakeAppServiceClient) RunResourceAction(_ context.Context, _ *applicationpkg.ResourceActionRunRequest, _ ...grpc.CallOption) (*applicationpkg.ApplicationResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) RunResourceActionV2(_ context.Context, _ *applicationpkg.ResourceActionRunRequestV2, _ ...grpc.CallOption) (*applicationpkg.ApplicationResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) DeleteResource(_ context.Context, _ *applicationpkg.ApplicationResourceDeleteRequest, _ ...grpc.CallOption) (*applicationpkg.ApplicationResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) PodLogs(_ context.Context, _ *applicationpkg.ApplicationPodLogsQuery, _ ...grpc.CallOption) (applicationpkg.ApplicationService_PodLogsClient, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) ListLinks(_ context.Context, _ *applicationpkg.ListAppLinksRequest, _ ...grpc.CallOption) (*applicationpkg.LinksResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) ListResourceLinks(_ context.Context, _ *applicationpkg.ApplicationResourceRequest, _ ...grpc.CallOption) (*applicationpkg.LinksResponse, error) {
	return nil, nil
}

func (c *fakeAppServiceClient) ServerSideDiff(_ context.Context, _ *applicationpkg.ApplicationServerSideDiffQuery, _ ...grpc.CallOption) (*applicationpkg.ApplicationServerSideDiffResponse, error) {
	return nil, nil
}

type fakeAcdClient struct {
	simulateTimeout uint
}

func (c *fakeAcdClient) ClientOptions() argocdclient.ClientOptions {
	return argocdclient.ClientOptions{}
}
func (c *fakeAcdClient) HTTPClient() (*http.Client, error) { return nil, nil }
func (c *fakeAcdClient) OIDCConfig(context.Context, *settingspkg.Settings) (*oauth2.Config, *oidc.Provider, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewRepoClient() (io.Closer, repositorypkg.RepositoryServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewRepoClientOrDie() (io.Closer, repositorypkg.RepositoryServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewRepoCredsClient() (io.Closer, repocredspkg.RepoCredsServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewRepoCredsClientOrDie() (io.Closer, repocredspkg.RepoCredsServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewCertClient() (io.Closer, certificatepkg.CertificateServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewCertClientOrDie() (io.Closer, certificatepkg.CertificateServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewClusterClient() (io.Closer, clusterpkg.ClusterServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewClusterClientOrDie() (io.Closer, clusterpkg.ClusterServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewGPGKeyClient() (io.Closer, gpgkeypkg.GPGKeyServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewGPGKeyClientOrDie() (io.Closer, gpgkeypkg.GPGKeyServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewApplicationClient() (io.Closer, applicationpkg.ApplicationServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewApplicationSetClient() (io.Closer, applicationsetpkg.ApplicationSetServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewApplicationClientOrDie() (io.Closer, applicationpkg.ApplicationServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewApplicationSetClientOrDie() (io.Closer, applicationsetpkg.ApplicationSetServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewNotificationClient() (io.Closer, notificationpkg.NotificationServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewNotificationClientOrDie() (io.Closer, notificationpkg.NotificationServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewSessionClient() (io.Closer, sessionpkg.SessionServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewSessionClientOrDie() (io.Closer, sessionpkg.SessionServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewSettingsClient() (io.Closer, settingspkg.SettingsServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewSettingsClientOrDie() (io.Closer, settingspkg.SettingsServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewVersionClient() (io.Closer, versionpkg.VersionServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewVersionClientOrDie() (io.Closer, versionpkg.VersionServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewProjectClient() (io.Closer, projectpkg.ProjectServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewProjectClientOrDie() (io.Closer, projectpkg.ProjectServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) NewAccountClient() (io.Closer, accountpkg.AccountServiceClient, error) {
	return nil, nil, nil
}

func (c *fakeAcdClient) NewAccountClientOrDie() (io.Closer, accountpkg.AccountServiceClient) {
	return nil, nil
}

func (c *fakeAcdClient) WatchApplicationWithRetry(_ context.Context, _ string, _ string) chan *v1alpha1.ApplicationWatchEvent {
	appEventsCh := make(chan *v1alpha1.ApplicationWatchEvent)

	go func() {
		modifiedEvent := new(v1alpha1.ApplicationWatchEvent)
		modifiedEvent.Type = watch.Modified
		appEventsCh <- modifiedEvent
		deletedEvent := new(v1alpha1.ApplicationWatchEvent)
		deletedEvent.Type = watch.Deleted
		appEventsCh <- deletedEvent
	}()
	return appEventsCh
}

func (c *fakeAcdClient) WatchApplicationSetWithRetry(_ context.Context, _ string, _ string) chan *v1alpha1.ApplicationSetWatchEvent {
	appSetEventsCh := make(chan *v1alpha1.ApplicationSetWatchEvent)
	go func() {
		defer close(appSetEventsCh)
		addedEvent := &v1alpha1.ApplicationSetWatchEvent{
			Type: watch.Added,
			ApplicationSet: v1alpha1.ApplicationSet{
				Status: v1alpha1.ApplicationSetStatus{
					Conditions: []v1alpha1.ApplicationSetCondition{
						{Type: v1alpha1.ApplicationSetConditionResourcesUpToDate, Status: v1alpha1.ApplicationSetConditionStatusTrue},
					},
				},
			},
		}
		appSetEventsCh <- addedEvent
	}()
	return appSetEventsCh
}
