// Copyright 2024 The argocd-agent Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package agent

import (
	"context"
	"fmt"
	"testing"

	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/rest"

	backend_mocks "github.com/argoproj-labs/argocd-agent/internal/backend/mocks"
	"github.com/argoproj-labs/argocd-agent/internal/event"
	"github.com/argoproj-labs/argocd-agent/internal/manager"
	"github.com/argoproj-labs/argocd-agent/internal/manager/application"
	"github.com/argoproj-labs/argocd-agent/internal/manager/appproject"
	"github.com/argoproj-labs/argocd-agent/internal/manager/repository"
	"github.com/argoproj-labs/argocd-agent/internal/resources"
	"github.com/argoproj-labs/argocd-agent/pkg/types"
	corev1 "k8s.io/api/core/v1"
	kerrors "k8s.io/apimachinery/pkg/api/errors"
	ktypes "k8s.io/apimachinery/pkg/types"
)

func Test_CreateApplication(t *testing.T) {
	a, _ := newAgent(t)
	be := backend_mocks.NewApplication(t)
	var err error
	a.appManager, err = application.NewApplicationManager(be, "argocd", application.WithAllowUpsert(true))
	require.NoError(t, err)
	require.NotNil(t, a)
	app := &v1alpha1.Application{ObjectMeta: v1.ObjectMeta{
		Name:      "test",
		Namespace: "default",
		OwnerReferences: []v1.OwnerReference{
			{
				APIVersion: "argoproj.io/v1alpha1",
				Kind:       "ApplicationSet",
				Name:       "owner",
				UID:        "uid-1",
			},
		},
	}}
	t.Run("Discard event in unmanaged mode", func(t *testing.T) {
		napp, err := a.createApplication(app)
		require.Nil(t, napp)
		require.ErrorContains(t, err, "not in managed mode")
	})

	t.Run("Discard event because app is already managed", func(t *testing.T) {
		defer a.appManager.Unmanage(app.QualifiedName())
		a.mode = types.AgentModeManaged
		a.appManager.Manage(app.QualifiedName())
		napp, err := a.createApplication(app)
		require.ErrorContains(t, err, "is already managed")
		require.Nil(t, napp)
	})

	t.Run("Create application", func(t *testing.T) {
		defer a.appManager.Unmanage(app.QualifiedName())
		a.mode = types.AgentModeManaged
		createMock := be.On("Create", mock.Anything, mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer createMock.Unset()
		napp, err := a.createApplication(app)
		require.NoError(t, err)
		require.NotNil(t, napp)
		require.Empty(t, napp.OwnerReferences, "OwnerReferences should not be applied on managed app")
	})

}

func Test_ProcessIncomingAppWithUIDMismatch(t *testing.T) {
	a, _ := newAgent(t)
	a.mode = types.AgentModeManaged
	evs := event.NewEventSource("test")
	var be *backend_mocks.Application
	var getMock, createMock, deleteMock, supportsPatchMock, updateMock *mock.Call

	// incomingApp is the app sent by the principal
	incomingApp := &v1alpha1.Application{ObjectMeta: v1.ObjectMeta{
		Name:      "test",
		Namespace: "argocd",
		OwnerReferences: []v1.OwnerReference{
			{
				APIVersion: "argoproj.io/v1alpha1",
				Kind:       "ApplicationSet",
				Name:       "owner",
				UID:        "uid-1",
			},
		},
	}}

	// oldApp is the app that is already present on the agent
	oldApp := incomingApp.DeepCopy()

	incomingApp.UID = ktypes.UID("new_uid")

	// createdApp is the app that is finally created on the cluster.
	createdApp := incomingApp.DeepCopy()
	createdApp.UID = ktypes.UID("random_uid")
	createdApp.Annotations = map[string]string{
		// the final version of the app should have the source UID from the incomingApp
		manager.SourceUIDAnnotation: string(incomingApp.UID),
	}

	configureManager := func(t *testing.T, mode manager.ManagerMode) {
		t.Helper()
		oldApp.Annotations = map[string]string{
			manager.SourceUIDAnnotation: "old_uid",
		}
		a.appManager.Manage(oldApp.QualifiedName())
		defer a.appManager.ClearManaged()

		be = backend_mocks.NewApplication(t)
		var err error
		a.appManager, err = application.NewApplicationManager(be, "argocd", application.WithAllowUpsert(true),
			application.WithRole(manager.ManagerRoleAgent), application.WithMode(mode))
		require.NoError(t, err)
		require.NotNil(t, a)

		// Get is used to retrieve the oldApp and compare its UID annotation.
		getMock = be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(oldApp, nil)

		// Create is used to create the latest version of the app on the agent.
		createMock = be.On("Create", mock.Anything, mock.Anything).Return(createdApp, nil)

		supportsPatchMock = be.On("SupportsPatch").Return(false)
		updateMock = be.On("Update", mock.Anything, mock.Anything).Return(oldApp, nil)

		// Delete is used to delete the oldApp from the agent.
		deleteMock = be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
			Return(nil)
	}

	unsetMocks := func(t *testing.T) {
		t.Helper()
		getMock.Unset()
		createMock.Unset()
		deleteMock.Unset()
		supportsPatchMock.Unset()
		updateMock.Unset()
	}

	t.Run("Create: Old app with diff UID must be deleted before creating the incoming app", func(t *testing.T) {
		configureManager(t, manager.ManagerModeManaged)
		defer unsetMocks(t)
		a.appManager.Manage(oldApp.QualifiedName())
		defer a.appManager.ClearManaged()

		ev := event.New(evs.ApplicationEvent(event.Create, incomingApp), event.TargetApplication)
		err := a.processIncomingApplication(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID, delete old app, and create a new app.
		expectedCalls := []string{"Get", "Delete", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new app has the updated source UID annotation.
		appInterface := be.Calls[2].ReturnArguments[0]
		latestApp, ok := appInterface.(*v1alpha1.Application)
		require.True(t, ok)
		require.Equal(t, string(incomingApp.UID), latestApp.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Create: Old app with the same UID must be updated", func(t *testing.T) {
		configureManager(t, manager.ManagerModeManaged)
		defer unsetMocks(t)
		a.appManager.Manage(oldApp.QualifiedName())
		defer a.appManager.ClearManaged()

		// The incoming app's UID matches with the UID in the annotation
		newApp := incomingApp.DeepCopy()
		newApp.UID = ktypes.UID("old_uid")
		newApp.Labels = map[string]string{
			"name": "test",
		}

		ev := event.New(evs.ApplicationEvent(event.Create, newApp), event.TargetApplication)
		err := a.processIncomingApplication(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		expectedCalls := []string{"Get", "Get", "SupportsPatch", "Update"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		appInterface := be.Calls[3].ReturnArguments[0]
		latestApp, ok := appInterface.(*v1alpha1.Application)
		require.True(t, ok)
		require.Equal(t, newApp.Labels, latestApp.Labels)
	})

	t.Run("Update: Old app with diff UID must be deleted and a new app must be created", func(t *testing.T) {
		configureManager(t, manager.ManagerModeManaged)
		defer unsetMocks(t)
		a.appManager.Manage(oldApp.QualifiedName())
		defer a.appManager.ClearManaged()

		// Create an Update event for the new app
		ev := event.New(evs.ApplicationEvent(event.SpecUpdate, incomingApp), event.TargetApplication)
		err := a.processIncomingApplication(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID, delete old app, and create a new app.
		expectedCalls := []string{"Get", "Delete", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new app has the updated source UID annotation.
		appInterface := be.Calls[2].ReturnArguments[0]

		latestApp, ok := appInterface.(*v1alpha1.Application)
		require.True(t, ok)
		require.Equal(t, string(incomingApp.UID), latestApp.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Update: incoming app must be created if it doesn't exist while handling update event", func(t *testing.T) {
		configureManager(t, manager.ManagerModeManaged)
		defer unsetMocks(t)
		a.appManager.Manage(oldApp.QualifiedName())
		defer a.appManager.ClearManaged()

		newApp := incomingApp.DeepCopy()
		newApp.Name = "new-app"

		getMock.Unset()
		notFoundError := kerrors.NewNotFound(schema.GroupResource{
			Group: "argoproj.io", Resource: "application"}, newApp.Name)
		getMock = be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(nil, notFoundError)

		createMock.Unset()
		createMock = be.On("Create", mock.Anything, mock.Anything).Return(newApp, nil)

		// Create an Update event for the new app
		ev := event.New(evs.ApplicationEvent(event.SpecUpdate, newApp), event.TargetApplication)
		err := a.processIncomingApplication(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		expectedCalls := []string{"Get", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new app has been created.
		appInterface := be.Calls[1].ReturnArguments[0]
		latestApp, ok := appInterface.(*v1alpha1.Application)
		require.True(t, ok)
		require.Equal(t, newApp, latestApp)
	})

	t.Run("Update: Don't compare source UID for the autonomous agent", func(t *testing.T) {
		configureManager(t, manager.ManagerModeAutonomous)
		defer unsetMocks(t)
		a.appManager.Manage(oldApp.QualifiedName())
		defer a.appManager.ClearManaged()

		a.mode = types.AgentModeAutonomous
		defer func() {
			a.mode = types.AgentModeManaged
		}()

		// Create a fake sync operation for the autonomous agent app.
		newApp := incomingApp.DeepCopy()
		newApp.Operation = &v1alpha1.Operation{
			Sync: &v1alpha1.SyncOperation{
				Revision: "1.0.0",
			},
		}

		updateMock.Unset()
		updateMock = be.On("Update", mock.Anything, mock.Anything).Return(newApp, nil)

		ev := event.New(evs.ApplicationEvent(event.SpecUpdate, newApp), event.TargetApplication)
		err := a.processIncomingApplication(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		expectedCalls := []string{"Get", "SupportsPatch", "Update"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		appInterface := be.Calls[2].ReturnArguments[0]
		latestApp, ok := appInterface.(*v1alpha1.Application)
		require.True(t, ok)
		require.Equal(t, newApp.Operation, latestApp.Operation)
	})

	t.Run("Delete: Old app with diff UID must be deleted", func(t *testing.T) {
		configureManager(t, manager.ManagerModeManaged)
		defer unsetMocks(t)
		a.appManager.Manage(oldApp.QualifiedName())
		defer a.appManager.ClearManaged()

		// Create a delete event for the new app
		ev := event.New(evs.ApplicationEvent(event.Delete, incomingApp), event.TargetApplication)
		err := a.processIncomingApplication(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID and delete old app
		expectedCalls := []string{"Get", "Delete"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)
		require.False(t, a.appManager.IsManaged(incomingApp.QualifiedName()))
	})
}

func Test_ProcessIncomingAppProjectWithUIDMismatch(t *testing.T) {
	a, _ := newAgent(t)
	a.mode = types.AgentModeManaged
	evs := event.NewEventSource("test")
	var be *backend_mocks.AppProject
	var getMock, createMock, deleteMock, supportsPatchMock, updateMock *mock.Call

	// incomingAppProject is the appProject sent by the principal
	incomingAppProject := &v1alpha1.AppProject{ObjectMeta: v1.ObjectMeta{
		Name:      "test",
		Namespace: "argocd",
	}}

	// oldAppProject is the appProject that is already present on the agent
	oldAppProject := incomingAppProject.DeepCopy()
	oldAppProject.Annotations = map[string]string{
		manager.SourceUIDAnnotation: "old_uid",
	}

	incomingAppProject.UID = ktypes.UID("new_uid")

	// createdAppProject is the final version of appProject created on the cluster.
	createdAppProject := incomingAppProject.DeepCopy()
	createdAppProject.UID = ktypes.UID("random_uid")
	createdAppProject.Annotations = map[string]string{
		manager.SourceUIDAnnotation: string(incomingAppProject.UID),
	}

	configureManager := func(t *testing.T) {
		t.Helper()
		be = backend_mocks.NewAppProject(t)
		var err error
		a.projectManager, err = appproject.NewAppProjectManager(be, "argocd", appproject.WithAllowUpsert(true))
		require.NoError(t, err)
		require.NotNil(t, a)

		// Get is used to retrieve the oldAppProject and compare its UID annotation.
		getMock = be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(oldAppProject, nil)

		// Create is used to create the latest version of the appProject on the agent.
		createMock = be.On("Create", mock.Anything, mock.Anything).Return(createdAppProject, nil)

		supportsPatchMock = be.On("SupportsPatch").Return(false)
		updateMock = be.On("Update", mock.Anything, mock.Anything).Return(oldAppProject, nil)

		// Delete is used to delete the oldAppProject from the agent.
		deleteMock = be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
			Return(nil)
	}

	unsetMocks := func(t *testing.T) {
		t.Helper()
		getMock.Unset()
		createMock.Unset()
		deleteMock.Unset()
		supportsPatchMock.Unset()
		updateMock.Unset()
	}

	t.Run("Create: Old appProject with diff UID must be deleted before creating the incoming appProject", func(t *testing.T) {
		configureManager(t)
		defer unsetMocks(t)
		a.projectManager.Manage(oldAppProject.Name)
		defer a.projectManager.ClearManaged()
		ev := event.New(evs.AppProjectEvent(event.Create, incomingAppProject), event.TargetAppProject)
		err := a.processIncomingAppProject(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID, delete old appProject, and create a new appProject.
		expectedCalls := []string{"Get", "Delete", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new app has the updated source UID annotation.
		appInterface := be.Calls[2].ReturnArguments[0]
		latestAppProject, ok := appInterface.(*v1alpha1.AppProject)
		require.True(t, ok)
		require.Equal(t, string(incomingAppProject.UID), latestAppProject.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Create: Old appProject with the same UID must be updated", func(t *testing.T) {
		configureManager(t)
		defer unsetMocks(t)
		a.projectManager.Manage(oldAppProject.Name)
		defer a.projectManager.ClearManaged()

		// The incoming app's UID matches with the UID in the annotation
		newAppProject := incomingAppProject.DeepCopy()
		newAppProject.UID = ktypes.UID("old_uid")
		newAppProject.Labels = map[string]string{
			"name": "test",
		}

		ev := event.New(evs.AppProjectEvent(event.Create, newAppProject), event.TargetAppProject)
		err := a.processIncomingAppProject(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		expectedCalls := []string{"Get", "Get", "SupportsPatch", "Update"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		appInterface := be.Calls[3].ReturnArguments[0]
		latestAppProject, ok := appInterface.(*v1alpha1.AppProject)
		require.True(t, ok)
		require.Equal(t, newAppProject.Labels, latestAppProject.Labels)
	})

	t.Run("Update: Old appProject with diff UID must be deleted and a new appProject must be created", func(t *testing.T) {
		configureManager(t)
		defer unsetMocks(t)
		a.projectManager.Manage(oldAppProject.Name)
		defer a.projectManager.ClearManaged()

		// Create an Update event for the new appProject
		ev := event.New(evs.AppProjectEvent(event.SpecUpdate, incomingAppProject), event.TargetAppProject)
		err := a.processIncomingAppProject(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID, delete old appProject, and create a new appProject.
		expectedCalls := []string{"Get", "Delete", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new appProject has the updated source UID annotation.
		appInterface := be.Calls[2].ReturnArguments[0]

		latestAppProject, ok := appInterface.(*v1alpha1.AppProject)
		require.True(t, ok)
		require.Equal(t, string(incomingAppProject.UID), latestAppProject.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Update: incoming appProject must be created if it doesn't exist while handling update event", func(t *testing.T) {
		configureManager(t)
		defer unsetMocks(t)
		a.projectManager.Manage(oldAppProject.Name)
		defer a.projectManager.ClearManaged()

		newAppProject := incomingAppProject.DeepCopy()
		newAppProject.Name = "new-app-project"

		getMock.Unset()
		notFoundError := kerrors.NewNotFound(schema.GroupResource{
			Group: "argoproj.io", Resource: "appproject"}, newAppProject.Name)
		getMock = be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(nil, notFoundError)

		createMock.Unset()
		createMock = be.On("Create", mock.Anything, mock.Anything).Return(newAppProject, nil)

		// Create an Update event for the new appProject
		ev := event.New(evs.AppProjectEvent(event.SpecUpdate, newAppProject), event.TargetAppProject)
		err := a.processIncomingAppProject(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		expectedCalls := []string{"Get", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new app has been created.
		appInterface := be.Calls[1].ReturnArguments[0]
		latestApp, ok := appInterface.(*v1alpha1.AppProject)
		require.True(t, ok)
		require.Equal(t, newAppProject, latestApp)
	})

	t.Run("Delete: Old appProject with the same UID must be deleted", func(t *testing.T) {
		configureManager(t)
		defer unsetMocks(t)
		a.projectManager.Manage(oldAppProject.Name)
		defer a.projectManager.ClearManaged()

		// Create a delete event for the new appProject
		ev := event.New(evs.AppProjectEvent(event.Delete, incomingAppProject), event.TargetAppProject)
		err := a.processIncomingAppProject(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID and delete old appProject
		expectedCalls := []string{"Get", "Delete"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)
		require.False(t, a.appManager.IsManaged(incomingAppProject.Name))
	})

	// Missing Source UID Annotation scenarios - when existing AppProject lacks the source UID annotation
	t.Run("Create: Existing AppProject without source UID annotation should be deleted and recreated", func(t *testing.T) {
		// Setup separate backend for this test to avoid mock conflicts
		beMissing := backend_mocks.NewAppProject(t)
		var err error
		a.projectManager, err = appproject.NewAppProjectManager(beMissing, "argocd", appproject.WithAllowUpsert(true))
		require.NoError(t, err)

		// Setup for missing source UID scenario
		existingAppProject := &v1alpha1.AppProject{
			ObjectMeta: v1.ObjectMeta{
				Name:      "test",
				Namespace: "argocd",
				UID:       ktypes.UID("existing_uid"),
			},
		}

		// Configure separate backend
		getMockMissing := beMissing.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(existingAppProject, nil)
		createMockMissing := beMissing.On("Create", mock.Anything, mock.Anything).Return(createdAppProject, nil)
		deleteMockMissing := beMissing.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

		defer func() {
			getMockMissing.Unset()
			createMockMissing.Unset()
			deleteMockMissing.Unset()
		}()

		a.projectManager.Manage(existingAppProject.Name)
		defer a.projectManager.ClearManaged()

		ev := event.New(evs.AppProjectEvent(event.Create, incomingAppProject), event.TargetAppProject)
		err = a.processIncomingAppProject(ev)

		// The process should succeed after deleting the existing AppProject and creating the new one
		require.NoError(t, err)

		// Verify the sequence: Get (to compare UID), Delete (existing), Create (new)
		expectedCalls := []string{"Get", "Delete", "Create"}
		gotCalls := []string{}
		for _, call := range beMissing.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Verify the created AppProject has the correct source UID annotation
		appInterface := beMissing.Calls[2].ReturnArguments[0]
		latestAppProject, ok := appInterface.(*v1alpha1.AppProject)
		require.True(t, ok)
		require.Equal(t, string(incomingAppProject.UID), latestAppProject.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Update: Existing AppProject without source UID annotation should be deleted and recreated", func(t *testing.T) {
		// Setup separate backend for this test to avoid mock conflicts
		beMissing := backend_mocks.NewAppProject(t)
		var err error
		a.projectManager, err = appproject.NewAppProjectManager(beMissing, "argocd", appproject.WithAllowUpsert(true))
		require.NoError(t, err)

		// Setup for missing source UID scenario
		existingAppProject := &v1alpha1.AppProject{
			ObjectMeta: v1.ObjectMeta{
				Name:      "test",
				Namespace: "argocd",
				UID:       ktypes.UID("existing_uid"),
			},
		}

		// Configure separate backend
		getMockMissing := beMissing.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(existingAppProject, nil)
		createMockMissing := beMissing.On("Create", mock.Anything, mock.Anything).Return(createdAppProject, nil)
		deleteMockMissing := beMissing.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

		defer func() {
			getMockMissing.Unset()
			createMockMissing.Unset()
			deleteMockMissing.Unset()
		}()

		a.projectManager.Manage(existingAppProject.Name)
		defer a.projectManager.ClearManaged()

		ev := event.New(evs.AppProjectEvent(event.SpecUpdate, incomingAppProject), event.TargetAppProject)
		err = a.processIncomingAppProject(ev)

		// The process should succeed after deleting the existing AppProject and creating the new one
		require.NoError(t, err)

		// Verify the sequence: Get (to compare UID), Delete (existing), Create (new)
		expectedCalls := []string{"Get", "Delete", "Create"}
		gotCalls := []string{}
		for _, call := range beMissing.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Verify the created AppProject has the correct source UID annotation
		appInterface := beMissing.Calls[2].ReturnArguments[0]
		latestAppProject, ok := appInterface.(*v1alpha1.AppProject)
		require.True(t, ok)
		require.Equal(t, string(incomingAppProject.UID), latestAppProject.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Delete: Existing AppProject without source UID annotation should be deleted", func(t *testing.T) {
		// Setup separate backend for this test to avoid mock conflicts
		beMissing := backend_mocks.NewAppProject(t)
		var err error
		a.projectManager, err = appproject.NewAppProjectManager(beMissing, "argocd", appproject.WithAllowUpsert(true))
		require.NoError(t, err)

		// Setup for missing source UID scenario
		existingAppProject := &v1alpha1.AppProject{
			ObjectMeta: v1.ObjectMeta{
				Name:      "test",
				Namespace: "argocd",
				UID:       ktypes.UID("existing_uid"),
			},
		}

		// Configure separate backend
		getMockMissing := beMissing.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(existingAppProject, nil)
		deleteMockMissing := beMissing.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

		defer func() {
			getMockMissing.Unset()
			deleteMockMissing.Unset()
		}()

		a.projectManager.Manage(existingAppProject.Name)
		defer a.projectManager.ClearManaged()

		ev := event.New(evs.AppProjectEvent(event.Delete, incomingAppProject), event.TargetAppProject)
		err = a.processIncomingAppProject(ev)

		// Delete events should succeed after deleting the existing AppProject
		require.NoError(t, err)

		// Verify the sequence: Get (to compare UID), Delete (existing)
		expectedCalls := []string{"Get", "Delete"}
		gotCalls := []string{}
		for _, call := range beMissing.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)
	})
}

func Test_UpdateApplication(t *testing.T) {
	a, _ := newAgent(t)
	be := backend_mocks.NewApplication(t)
	var err error
	a.appManager, err = application.NewApplicationManager(be, "argocd", application.WithAllowUpsert(true))
	require.NoError(t, err)
	require.NotNil(t, a)
	app := &v1alpha1.Application{
		ObjectMeta: v1.ObjectMeta{
			Name:            "test",
			Namespace:       "default",
			ResourceVersion: "12345",
			OwnerReferences: []v1.OwnerReference{
				{
					APIVersion: "argoproj.io/v1alpha1",
					Kind:       "ApplicationSet",
					Name:       "owner",
					UID:        "uid-1",
				},
			},
		}}
	t.Run("Discard event because version has been seen already", func(t *testing.T) {
		defer a.appManager.ClearIgnored()
		a.appManager.IgnoreChange(fmt.Sprintf("%s/test", a.namespace), "12345")
		napp, err := a.updateApplication(app)
		require.Nil(t, napp)
		require.ErrorContains(t, err, "has already been seen")
	})

	t.Run("Update application using patch in managed mode", func(t *testing.T) {
		a.mode = types.AgentModeManaged
		a.appManager, err = application.NewApplicationManager(be, "argocd", application.WithAllowUpsert(true), application.WithMode(manager.ManagerModeManaged), application.WithRole(manager.ManagerRoleAgent))
		getEvent := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer getEvent.Unset()
		supportsPatchEvent := be.On("SupportsPatch").Return(true)
		defer supportsPatchEvent.Unset()
		patchEvent := be.On("Patch", mock.Anything, "test", "argocd", mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer patchEvent.Unset()
		napp, err := a.updateApplication(app)
		require.NoError(t, err)
		require.NotNil(t, napp)
		require.Empty(t, napp.OwnerReferences, "OwnerReferences should not be applied on managed app")
	})

	t.Run("Update application using update in managed mode", func(t *testing.T) {
		a.mode = types.AgentModeManaged
		a.appManager, err = application.NewApplicationManager(be, "argocd", application.WithAllowUpsert(true), application.WithMode(manager.ManagerModeManaged), application.WithRole(manager.ManagerRoleAgent))
		getEvent := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer getEvent.Unset()
		supportsPatchEvent := be.On("SupportsPatch").Return(false)
		defer supportsPatchEvent.Unset()
		patchEvent := be.On("Update", mock.Anything, mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer patchEvent.Unset()
		napp, err := a.updateApplication(app)
		require.NoError(t, err)
		require.NotNil(t, napp)
		require.Empty(t, napp.OwnerReferences, "OwnerReferences should not be applied on managed app")
	})

	t.Run("Update application using patch in autonomous mode", func(t *testing.T) {
		a.mode = types.AgentModeAutonomous
		a.appManager, err = application.NewApplicationManager(be, "argocd", application.WithAllowUpsert(true), application.WithMode(manager.ManagerModeAutonomous), application.WithRole(manager.ManagerRoleAgent))

		getEvent := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer getEvent.Unset()
		supportsPatchEvent := be.On("SupportsPatch").Return(true)
		defer supportsPatchEvent.Unset()
		patchEvent := be.On("Patch", mock.Anything, "test", "argocd", mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer patchEvent.Unset()
		napp, err := a.updateApplication(app)
		require.NoError(t, err)
		require.NotNil(t, napp)
	})

	t.Run("Update application using update in autonomous mode", func(t *testing.T) {
		a.mode = types.AgentModeAutonomous
		a.appManager, err = application.NewApplicationManager(be, "argocd", application.WithAllowUpsert(true), application.WithMode(manager.ManagerModeAutonomous), application.WithRole(manager.ManagerRoleAgent))

		getEvent := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer getEvent.Unset()
		supportsPatchEvent := be.On("SupportsPatch").Return(false)
		defer supportsPatchEvent.Unset()
		patchEvent := be.On("Update", mock.Anything, mock.Anything).Return(&v1alpha1.Application{}, nil)
		defer patchEvent.Unset()
		napp, err := a.updateApplication(app)
		require.NoError(t, err)
		require.NotNil(t, napp)
	})

}

func Test_CreateAppProject(t *testing.T) {
	a, _ := newAgent(t)
	be := backend_mocks.NewAppProject(t)
	var err error
	a.projectManager, err = appproject.NewAppProjectManager(be, "argocd", appproject.WithAllowUpsert(true))
	require.NoError(t, err)
	require.NotNil(t, a)
	project := &v1alpha1.AppProject{
		ObjectMeta: v1.ObjectMeta{
			Name:      "test",
			Namespace: "default",
			OwnerReferences: []v1.OwnerReference{
				{
					APIVersion: "argoproj.io/v1alpha1",
					Kind:       "Application",
					Name:       "owner",
					UID:        "uid-1",
				},
			},
		}, Spec: v1alpha1.AppProjectSpec{
			SourceNamespaces: []string{"default", "argocd"},
		},
	}

	t.Run("Discard event in unmanaged mode", func(t *testing.T) {
		napp, err := a.createAppProject(project)
		require.Nil(t, napp)
		require.ErrorContains(t, err, "not in managed mode")
	})

	t.Run("Update appproject when already managed", func(t *testing.T) {
		defer a.projectManager.Unmanage(project.Name)
		a.mode = types.AgentModeManaged
		a.projectManager.Manage(project.Name)

		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil)
		defer getMock.Unset()

		supportsPatchMock := be.On("SupportsPatch").Return(false)
		defer supportsPatchMock.Unset()

		updateMock := be.On("Update", mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil)
		defer updateMock.Unset()

		napp, err := a.createAppProject(project)
		require.NoError(t, err)
		require.NotNil(t, napp)
		require.Empty(t, napp.OwnerReferences, "OwnerReferences should not be applied on managed app project")
	})

	t.Run("Create appproject", func(t *testing.T) {
		defer a.projectManager.Unmanage(project.Name)
		a.mode = types.AgentModeManaged
		createMock := be.On("Create", mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil)
		defer createMock.Unset()
		napp, err := a.createAppProject(project)
		require.NoError(t, err)
		require.NotNil(t, napp)
		require.Empty(t, napp.OwnerReferences, "OwnerReferences should not be applied on managed app project")
	})

}

func Test_UpdateAppProject(t *testing.T) {
	a, _ := newAgent(t)
	be := backend_mocks.NewAppProject(t)
	var err error
	a.projectManager, err = appproject.NewAppProjectManager(be, "argocd", appproject.WithAllowUpsert(true))
	require.NoError(t, err)
	require.NotNil(t, a)
	project := &v1alpha1.AppProject{
		ObjectMeta: v1.ObjectMeta{
			Name:            "test",
			Namespace:       "argocd",
			ResourceVersion: "12345",
			OwnerReferences: []v1.OwnerReference{
				{
					APIVersion: "argoproj.io/v1alpha1",
					Kind:       "Application",
					Name:       "owner",
					UID:        "uid-1",
				},
			},
		},
		Spec: v1alpha1.AppProjectSpec{
			SourceNamespaces: []string{"default", "argocd"},
		}}

	t.Run("Update appproject using patch", func(t *testing.T) {
		a.mode = types.AgentModeManaged
		a.projectManager, err = appproject.NewAppProjectManager(be, "argocd", appproject.WithAllowUpsert(true), appproject.WithMode(manager.ManagerModeManaged), appproject.WithRole(manager.ManagerRoleAgent))
		a.projectManager.Manage(project.Name)
		defer a.projectManager.Unmanage(project.Name)
		getEvent := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil)
		defer getEvent.Unset()
		supportsPatchEvent := be.On("SupportsPatch").Return(true)
		defer supportsPatchEvent.Unset()
		patchEvent := be.On("Patch", mock.Anything, "test", "argocd", mock.Anything).Return(&v1alpha1.AppProject{}, nil)
		defer patchEvent.Unset()
		napp, err := a.updateAppProject(project)
		require.NoError(t, err)
		require.NotNil(t, napp)
		require.Empty(t, napp.OwnerReferences, "OwnerReferences should not be applied on managed app project")
	})

	t.Run("Create the appproject if it doesn't exist", func(t *testing.T) {
		a.mode = types.AgentModeManaged
		a.projectManager, err = appproject.NewAppProjectManager(be, "argocd", appproject.WithAllowUpsert(true), appproject.WithMode(manager.ManagerModeManaged), appproject.WithRole(manager.ManagerRoleAgent))
		createMock := be.On("Create", mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil)
		defer createMock.Unset()
		napp, err := a.updateAppProject(project)
		require.NoError(t, err)
		require.NotNil(t, napp)
		require.Empty(t, napp.OwnerReferences, "OwnerReferences should not be applied on managed app project")
	})

	// Namespace handling tests
	t.Run("CreateAppProject sets correct namespace", func(t *testing.T) {
		agentNamespace := "agent-namespace"
		a, _ := newAgent(t)
		a.namespace = agentNamespace
		a.mode = types.AgentModeManaged

		be := backend_mocks.NewAppProject(t)
		var err error
		a.projectManager, err = appproject.NewAppProjectManager(be, agentNamespace, appproject.WithAllowUpsert(true))
		require.NoError(t, err)

		project := &v1alpha1.AppProject{
			ObjectMeta: v1.ObjectMeta{
				Name:      "test-project",
				Namespace: "wrong-namespace", // This should be overridden
			},
			Spec: v1alpha1.AppProjectSpec{
				SourceNamespaces: []string{"default"},
			},
		}

		// Mock the backend Create method and capture the project passed to it
		var capturedProject *v1alpha1.AppProject
		createMock := be.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
			capturedProject = args[1].(*v1alpha1.AppProject)
		}).Return(&v1alpha1.AppProject{}, nil)
		defer createMock.Unset()

		_, err = a.createAppProject(project)
		require.NoError(t, err)

		// Verify that the namespace was set correctly
		require.NotNil(t, capturedProject, "Project should have been passed to backend")
		assert.Equal(t, agentNamespace, capturedProject.Namespace, "Project namespace should be set to agent namespace")
		assert.Equal(t, "test-project", capturedProject.Name, "Project name should remain unchanged")
	})

	t.Run("UpdateAppProject sets correct namespace", func(t *testing.T) {
		agentNamespace := "agent-namespace"
		a, _ := newAgent(t)
		a.namespace = agentNamespace
		a.mode = types.AgentModeManaged

		be := backend_mocks.NewAppProject(t)
		var err error
		a.projectManager, err = appproject.NewAppProjectManager(be, agentNamespace,
			appproject.WithAllowUpsert(true),
			appproject.WithMode(manager.ManagerModeManaged),
			appproject.WithRole(manager.ManagerRoleAgent))
		require.NoError(t, err)

		project := &v1alpha1.AppProject{
			ObjectMeta: v1.ObjectMeta{
				Name:            "test-project",
				Namespace:       "wrong-namespace", // This should be overridden
				ResourceVersion: "12345",
			},
			Spec: v1alpha1.AppProjectSpec{
				SourceNamespaces: []string{"default"},
			},
		}

		// Set up project as managed
		a.projectManager.Manage(project.Name)
		defer a.projectManager.Unmanage(project.Name)

		// Mock the backend methods
		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.AppProject{}, nil)
		defer getMock.Unset()

		supportsPatchMock := be.On("SupportsPatch").Return(true)
		defer supportsPatchMock.Unset()

		// Capture the namespace passed to Patch method
		var capturedNamespace string
		patchMock := be.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
			capturedNamespace = args[2].(string)
		}).Return(&v1alpha1.AppProject{}, nil)
		defer patchMock.Unset()

		_, err = a.updateAppProject(project)
		require.NoError(t, err)

		// Verify that the namespace was set correctly in the patch call
		assert.Equal(t, agentNamespace, capturedNamespace, "Patch should use agent namespace")
	})

	t.Run("DeleteAppProject sets correct namespace", func(t *testing.T) {
		agentNamespace := "agent-namespace"
		a, _ := newAgent(t)
		a.namespace = agentNamespace
		a.mode = types.AgentModeManaged

		be := backend_mocks.NewAppProject(t)
		var err error
		a.projectManager, err = appproject.NewAppProjectManager(be, agentNamespace, appproject.WithAllowUpsert(true))
		require.NoError(t, err)

		project := &v1alpha1.AppProject{
			ObjectMeta: v1.ObjectMeta{
				Name:      "test-project",
				Namespace: "wrong-namespace", // This should be overridden
			},
			Spec: v1alpha1.AppProjectSpec{
				SourceNamespaces: []string{"default"},
			},
		}

		// Set up project as managed
		a.projectManager.Manage(project.Name)

		// Mock the backend Delete method and capture the namespace passed to it
		var capturedNamespace string
		deleteMock := be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
			capturedNamespace = args[2].(string)
		}).Return(nil)
		defer deleteMock.Unset()

		err = a.deleteAppProject(project)
		require.NoError(t, err)

		// Verify that the namespace was set correctly
		assert.Equal(t, agentNamespace, capturedNamespace, "Delete should use agent namespace")
	})

	t.Run("AppProject operations with custom agent namespace", func(t *testing.T) {
		customAgentNamespace := "custom-agent-ns"
		a, _ := newAgent(t)
		a.namespace = customAgentNamespace
		a.mode = types.AgentModeManaged

		be := backend_mocks.NewAppProject(t)
		var err error
		a.projectManager, err = appproject.NewAppProjectManager(be, customAgentNamespace, appproject.WithAllowUpsert(true))
		require.NoError(t, err)

		project := &v1alpha1.AppProject{
			ObjectMeta: v1.ObjectMeta{
				Name:      "test-project",
				Namespace: "principal-namespace", // Different from agent
			},
			Spec: v1alpha1.AppProjectSpec{
				SourceNamespaces: []string{"default"},
			},
		}

		// Test Create
		var capturedCreateProject *v1alpha1.AppProject
		createMock := be.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
			capturedCreateProject = args[1].(*v1alpha1.AppProject)
		}).Return(&v1alpha1.AppProject{}, nil)

		_, err = a.createAppProject(project)
		require.NoError(t, err)
		createMock.Unset()

		// Verify that the namespace was overridden to agent namespace
		require.NotNil(t, capturedCreateProject, "Project should have been passed to backend")
		assert.Equal(t, customAgentNamespace, capturedCreateProject.Namespace, "Project namespace should be set to custom agent namespace")
		assert.NotEqual(t, "principal-namespace", capturedCreateProject.Namespace, "Project namespace should not remain as principal namespace")

		// Test Delete
		a.projectManager.Manage(project.Name)

		var capturedDeleteNamespace string
		deleteMock := be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
			capturedDeleteNamespace = args[2].(string)
		}).Return(nil)

		err = a.deleteAppProject(project)
		require.NoError(t, err)
		deleteMock.Unset()

		// Verify that the namespace was overridden to agent namespace
		assert.Equal(t, customAgentNamespace, capturedDeleteNamespace, "Delete should use custom agent namespace")
		assert.NotEqual(t, "principal-namespace", capturedDeleteNamespace, "Delete should not use principal namespace")
	})

}

func Test_ProcessIncomingRepositoryWithUIDMismatch(t *testing.T) {
	evs := event.NewEventSource("test")

	// incomingRepo is the repository sent by the principal
	incomingRepo := &corev1.Secret{
		ObjectMeta: v1.ObjectMeta{
			Name:      "test-repo",
			Namespace: "argocd",
		},
		Type: "Opaque",
		Data: map[string][]byte{
			"url": []byte("https://github.com/test/repo"),
		},
	}

	// oldRepo is the repository that is already present on the agent
	oldRepo := incomingRepo.DeepCopy()

	incomingRepo.UID = ktypes.UID("new_uid")

	// createdRepo is the repository that is finally created on the cluster.
	createdRepo := incomingRepo.DeepCopy()
	createdRepo.UID = ktypes.UID("random_uid")
	createdRepo.Annotations = map[string]string{
		// the final version of the repo should have the source UID from the incomingRepo
		manager.SourceUIDAnnotation: string(incomingRepo.UID),
	}

	createAgent := func(t *testing.T) (*Agent, *backend_mocks.Repository) {
		t.Helper()
		a, _ := newAgent(t)
		a.mode = types.AgentModeManaged
		be := backend_mocks.NewRepository(t)
		a.repoManager = repository.NewManager(be, "argocd", true)
		return a, be
	}

	t.Run("Create: Old repository with diff UID must be deleted before creating the incoming repository", func(t *testing.T) {
		a, be := createAgent(t)

		oldRepo.Annotations = map[string]string{
			manager.SourceUIDAnnotation: "old_uid",
		}
		a.repoManager.Manage(oldRepo.Name)
		defer a.repoManager.ClearManaged()

		// Set up mock expectations
		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(oldRepo, nil)
		defer getMock.Unset()
		createMock := be.On("Create", mock.Anything, mock.Anything).Return(createdRepo, nil)
		defer createMock.Unset()
		deleteMock := be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
		defer deleteMock.Unset()

		ev := event.New(evs.RepositoryEvent(event.Create, incomingRepo), event.TargetRepository)
		err := a.processIncomingRepository(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID, delete old repo, and create a new repo.
		expectedCalls := []string{"Get", "Delete", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new repo has the updated source UID annotation.
		repoInterface := be.Calls[2].ReturnArguments[0]
		latestRepo, ok := repoInterface.(*corev1.Secret)
		require.True(t, ok)
		require.Equal(t, string(incomingRepo.UID), latestRepo.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Create: Old repository with the same UID must be updated", func(t *testing.T) {
		a, be := createAgent(t)

		oldRepo.Annotations = map[string]string{
			manager.SourceUIDAnnotation: "old_uid",
		}
		a.repoManager.Manage(oldRepo.Name)
		defer a.repoManager.ClearManaged()

		// The incoming repo's UID matches with the UID in the annotation
		newRepo := incomingRepo.DeepCopy()
		newRepo.UID = ktypes.UID("old_uid")
		newRepo.Labels = map[string]string{
			"name": "test",
		}

		// Set up mock expectations
		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(oldRepo, nil)
		defer getMock.Unset()
		supportsPatchMock := be.On("SupportsPatch").Return(false)
		defer supportsPatchMock.Unset()

		updatedRepo := newRepo.DeepCopy()
		updatedRepo.Annotations = map[string]string{
			manager.SourceUIDAnnotation: string(newRepo.UID),
		}
		updateMock := be.On("Update", mock.Anything, mock.Anything).Return(updatedRepo, nil)
		defer updateMock.Unset()

		ev := event.New(evs.RepositoryEvent(event.Create, newRepo), event.TargetRepository)
		err := a.processIncomingRepository(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		expectedCalls := []string{"Get", "Get", "SupportsPatch", "Update"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		repoInterface := be.Calls[3].ReturnArguments[0]
		latestRepo, ok := repoInterface.(*corev1.Secret)
		require.True(t, ok)
		require.Equal(t, newRepo.Labels, latestRepo.Labels)

		require.Equal(t, string(newRepo.UID), latestRepo.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Update: Old repository with diff UID must be deleted and a new repository must be created", func(t *testing.T) {
		a, be := createAgent(t)

		oldRepo.Annotations = map[string]string{
			manager.SourceUIDAnnotation: "old_uid",
		}
		a.repoManager.Manage(oldRepo.Name)
		defer a.repoManager.ClearManaged()

		// Set up mock expectations
		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(oldRepo, nil)
		defer getMock.Unset()
		createMock := be.On("Create", mock.Anything, mock.Anything).Return(createdRepo, nil)
		defer createMock.Unset()
		deleteMock := be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
		defer deleteMock.Unset()

		// Create an Update event for the new repository
		ev := event.New(evs.RepositoryEvent(event.SpecUpdate, incomingRepo), event.TargetRepository)
		err := a.processIncomingRepository(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID, delete old repo, and create a new repo.
		expectedCalls := []string{"Get", "Delete", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new repository has the same source UID annotation as the incoming repository.
		repoInterface := be.Calls[2].ReturnArguments[0]

		latestRepo, ok := repoInterface.(*corev1.Secret)
		require.True(t, ok)
		require.Equal(t, string(incomingRepo.UID), latestRepo.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Update: incoming repository must be created if it doesn't exist while handling update event", func(t *testing.T) {
		a, be := createAgent(t)

		newRepo := incomingRepo.DeepCopy()

		// Set up mock expectations
		notFoundError := kerrors.NewNotFound(schema.GroupResource{
			Group: "", Resource: "secret"}, newRepo.Name)
		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(nil, notFoundError)
		defer getMock.Unset()
		createMock := be.On("Create", mock.Anything, mock.Anything).Return(createdRepo, nil)
		defer createMock.Unset()

		// Create an Update event for the new repository
		ev := event.New(evs.RepositoryEvent(event.SpecUpdate, newRepo), event.TargetRepository)
		err := a.processIncomingRepository(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		expectedCalls := []string{"Get", "Create"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)

		// Check if the new repository has the same source UID annotation as the incoming repository.
		repoInterface := be.Calls[1].ReturnArguments[0]
		latestRepo, ok := repoInterface.(*corev1.Secret)
		require.True(t, ok)
		require.Equal(t, createdRepo, latestRepo)
		require.Equal(t, string(newRepo.UID), latestRepo.Annotations[manager.SourceUIDAnnotation])
	})

	t.Run("Delete: Old repository with diff UID must be deleted", func(t *testing.T) {
		a, be := createAgent(t)

		oldRepo.Annotations = map[string]string{
			manager.SourceUIDAnnotation: "old_uid",
		}
		a.repoManager.Manage(oldRepo.Name)
		defer a.repoManager.ClearManaged()

		// Set up mock expectations
		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(oldRepo, nil)
		defer getMock.Unset()
		deleteMock := be.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
		defer deleteMock.Unset()

		// Create a delete event for the new repository
		ev := event.New(evs.RepositoryEvent(event.Delete, incomingRepo), event.TargetRepository)
		err := a.processIncomingRepository(ev)
		require.Nil(t, err)

		// Check if the API calls were made in the same order:
		// compare the UID and delete old repository
		expectedCalls := []string{"Get", "Delete"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)
		require.False(t, a.repoManager.IsManaged(incomingRepo.Name))
	})

	t.Run("Unknown event type should not do anything", func(t *testing.T) {
		a, be := createAgent(t)

		oldRepo.Annotations = map[string]string{
			manager.SourceUIDAnnotation: "old_uid",
		}

		// Even for unknown event types, CompareSourceUID is called in managed mode
		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(oldRepo, nil)
		defer getMock.Unset()

		// Create an unknown event type
		ev := event.New(evs.RepositoryEvent(event.EventType("Unknown"), incomingRepo), event.TargetRepository)
		err := a.processIncomingRepository(ev)
		require.Nil(t, err)

		// Only the Get call for CompareSourceUID should be made, no other backend operations
		expectedCalls := []string{"Get"}
		gotCalls := []string{}
		for _, call := range be.Calls {
			gotCalls = append(gotCalls, call.Method)
		}
		require.Equal(t, expectedCalls, gotCalls)
	})
}

func Test_CreateRepository(t *testing.T) {
	a, _ := newAgent(t)
	be := backend_mocks.NewRepository(t)
	repoMgr := repository.NewManager(be, "argocd", true)
	repoMgr.ManagedResources = manager.NewManagedResources()
	repoMgr.ObservedResources = manager.NewObservedResources()
	a.repoManager = repoMgr
	require.NotNil(t, a)
	repo := &corev1.Secret{
		ObjectMeta: v1.ObjectMeta{
			Name:      "test",
			Namespace: "default",
		},
		Type: "Opaque",
		Data: map[string][]byte{
			"url": []byte("https://github.com/test/repo"),
		},
	}
	t.Run("Discard event in unmanaged mode", func(t *testing.T) {
		nrepo, err := a.createRepository(repo)
		require.Nil(t, nrepo)
		require.ErrorContains(t, err, "not in managed mode")
	})

	t.Run("Repository already managed calls updateRepository", func(t *testing.T) {
		defer a.repoManager.Unmanage(repo.Name)
		a.mode = types.AgentModeManaged
		a.repoManager.Manage(repo.Name)

		// Mock the backend calls that updateRepository makes
		getMock := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&corev1.Secret{}, nil)
		defer getMock.Unset()
		supportsPatchMock := be.On("SupportsPatch").Return(false)
		defer supportsPatchMock.Unset()
		updateMock := be.On("Update", mock.Anything, mock.Anything).Return(&corev1.Secret{}, nil)
		defer updateMock.Unset()

		nrepo, err := a.createRepository(repo)
		require.NoError(t, err)
		require.NotNil(t, nrepo)
	})

	t.Run("Create repository", func(t *testing.T) {
		defer a.repoManager.Unmanage(repo.Name)
		a.mode = types.AgentModeManaged
		createMock := be.On("Create", mock.Anything, mock.Anything).Return(&corev1.Secret{}, nil)
		defer createMock.Unset()
		nrepo, err := a.createRepository(repo)
		require.NoError(t, err)
		require.NotNil(t, nrepo)
	})
}

func Test_UpdateRepository(t *testing.T) {
	a, _ := newAgent(t)
	be := backend_mocks.NewRepository(t)
	repoMgr := repository.NewManager(be, "argocd", true)
	repoMgr.ManagedResources = manager.NewManagedResources()
	repoMgr.ObservedResources = manager.NewObservedResources()
	a.repoManager = repoMgr
	require.NotNil(t, a)
	repo := &corev1.Secret{
		ObjectMeta: v1.ObjectMeta{
			Name:            "test",
			Namespace:       "default",
			ResourceVersion: "12345",
		},
		Type: "Opaque",
		Data: map[string][]byte{
			"url": []byte("https://github.com/test/repo"),
		},
	}
	t.Run("Discard event because version has been seen already", func(t *testing.T) {
		a.mode = types.AgentModeManaged
		a.repoManager.Manage(repo.Name)
		defer a.repoManager.Unmanage(repo.Name)
		defer a.repoManager.ClearIgnored()
		a.repoManager.IgnoreChange(repo.Name, "12345")
		nrepo, err := a.updateRepository(repo)
		require.Nil(t, nrepo)
		require.ErrorContains(t, err, "has already been seen")
	})

	t.Run("Update repository using patch in managed mode", func(t *testing.T) {
		a.mode = types.AgentModeManaged
		a.repoManager.Manage(repo.Name)
		defer a.repoManager.Unmanage(repo.Name)

		getEvent := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&corev1.Secret{}, nil)
		defer getEvent.Unset()
		supportsPatchEvent := be.On("SupportsPatch").Return(true)
		defer supportsPatchEvent.Unset()
		patchEvent := be.On("Patch", mock.Anything, "test", "argocd", mock.Anything).Return(&corev1.Secret{}, nil)
		defer patchEvent.Unset()
		nrepo, err := a.updateRepository(repo)
		require.NoError(t, err)
		require.NotNil(t, nrepo)
	})

	t.Run("Update repository using update in managed mode", func(t *testing.T) {
		a.mode = types.AgentModeManaged
		a.repoManager.Manage(repo.Name)
		defer a.repoManager.Unmanage(repo.Name)

		getEvent := be.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&corev1.Secret{}, nil)
		defer getEvent.Unset()
		supportsPatchEvent := be.On("SupportsPatch").Return(false)
		defer supportsPatchEvent.Unset()
		updateEvent := be.On("Update", mock.Anything, mock.Anything).Return(&corev1.Secret{}, nil)
		defer updateEvent.Unset()
		nrepo, err := a.updateRepository(repo)
		require.NoError(t, err)
		require.NotNil(t, nrepo)
	})
}

func Test_processIncomingResourceResyncEvent(t *testing.T) {
	a, _ := newAgent(t)
	a.kubeClient.RestConfig = &rest.Config{}
	a.namespace = "test"
	a.context = context.Background()

	err := a.queues.Create(a.remote.ClientID())
	assert.Nil(t, err)
	a.emitter = event.NewEventSource("test")

	t.Run("discard SyncedResourceList request in managed mode", func(t *testing.T) {
		a.mode = types.AgentModeManaged

		ev, err := a.emitter.RequestSyncedResourceListEvent([]byte{})
		assert.Nil(t, err)

		expected := "agent can only handle SyncedResourceList request in the autonomous mode"
		err = a.processIncomingResourceResyncEvent(event.New(ev, event.TargetResourceResync))
		assert.Equal(t, expected, err.Error())
	})

	t.Run("process SyncedResourceList request in autonomous mode", func(t *testing.T) {
		a.mode = types.AgentModeAutonomous

		ev, err := a.emitter.RequestSyncedResourceListEvent([]byte{})
		assert.Nil(t, err)

		err = a.processIncomingResourceResyncEvent(event.New(ev, event.TargetResourceResync))
		assert.Nil(t, err)
	})

	t.Run("process SyncedResources in managed mode", func(t *testing.T) {
		a.mode = types.AgentModeManaged

		res := resources.ResourceKey{
			Name:      "sample",
			Namespace: "test",
			Kind:      "Application",
		}
		ev, err := a.emitter.SyncedResourceEvent(res)
		assert.Nil(t, err)

		err = a.processIncomingResourceResyncEvent(event.New(ev, event.TargetResourceResync))
		assert.NotNil(t, err)
	})

	t.Run("discard SyncedResources in autonomous mode", func(t *testing.T) {
		a.mode = types.AgentModeAutonomous

		ev, err := a.emitter.SyncedResourceEvent(resources.ResourceKey{})
		assert.Nil(t, err)

		expected := "agent can only handle SyncedResource request in the managed mode"
		err = a.processIncomingResourceResyncEvent(event.New(ev, event.TargetResourceResync))
		assert.Equal(t, expected, err.Error())
	})

	t.Run("process request update in autonomous mode", func(t *testing.T) {
		a.mode = types.AgentModeAutonomous

		update := &event.RequestUpdate{
			Name: "test",
			Kind: "Application",
		}
		ev, err := a.emitter.RequestUpdateEvent(update)
		assert.Nil(t, err)

		err = a.processIncomingResourceResyncEvent(event.New(ev, event.TargetResourceResync))
		assert.NotNil(t, err)
	})

	t.Run("discard request update in managed mode", func(t *testing.T) {
		a.mode = types.AgentModeManaged

		update := &event.RequestUpdate{}
		ev, err := a.emitter.RequestUpdateEvent(update)
		assert.Nil(t, err)

		expected := "agent can only handle RequestUpdate in the autonomous mode"
		err = a.processIncomingResourceResyncEvent(event.New(ev, event.TargetResourceResync))
		assert.Equal(t, expected, err.Error())
	})

	t.Run("process RequestResourceResync in managed mode", func(t *testing.T) {
		a.mode = types.AgentModeManaged

		ev, err := a.emitter.RequestResourceResyncEvent()
		assert.Nil(t, err)

		err = a.processIncomingResourceResyncEvent(event.New(ev, event.TargetResourceResync))
		assert.Nil(t, err)
	})

	t.Run("discard RequestResourceResync in autonomous mode", func(t *testing.T) {
		a.mode = types.AgentModeAutonomous

		ev, err := a.emitter.RequestResourceResyncEvent()
		assert.Nil(t, err)

		expected := "agent can only handle ResourceResync request in the managed mode"
		err = a.processIncomingResourceResyncEvent(event.New(ev, event.TargetResourceResync))
		assert.Equal(t, expected, err.Error())
	})
}
