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

package controller

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	corev1api "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/clock"
	ctrl "sigs.k8s.io/controller-runtime"

	velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
	"github.com/vmware-tanzu/velero/pkg/builder"
	"github.com/vmware-tanzu/velero/pkg/metrics"
	"github.com/vmware-tanzu/velero/pkg/repository"
	"github.com/vmware-tanzu/velero/pkg/repository/maintenance"
	repomaintenance "github.com/vmware-tanzu/velero/pkg/repository/maintenance"
	repomanager "github.com/vmware-tanzu/velero/pkg/repository/manager"
	repomokes "github.com/vmware-tanzu/velero/pkg/repository/mocks"
	repotypes "github.com/vmware-tanzu/velero/pkg/repository/types"
	velerotest "github.com/vmware-tanzu/velero/pkg/test"
	"github.com/vmware-tanzu/velero/pkg/util/logging"

	"sigs.k8s.io/controller-runtime/pkg/client"
	clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake"

	batchv1api "k8s.io/api/batch/v1"
)

const testMaintenanceFrequency = 10 * time.Minute

func mockBackupRepoReconciler(t *testing.T, mockOn string, arg any, ret ...any) *BackupRepoReconciler {
	t.Helper()
	mgr := &repomokes.Manager{}
	if mockOn != "" {
		mgr.On(mockOn, arg).Return(ret...)
	}
	return NewBackupRepoReconciler(
		velerov1api.DefaultNamespace,
		velerotest.NewLogger(),
		velerotest.NewFakeControllerRuntimeClient(t),
		mgr,
		testMaintenanceFrequency,
		"fake-repo-config",
		"",
		logrus.InfoLevel,
		nil,
		nil,
	)
}

func mockBackupRepositoryCR() *velerov1api.BackupRepository {
	return &velerov1api.BackupRepository{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: velerov1api.DefaultNamespace,
			Name:      "repo",
		},
		Spec: velerov1api.BackupRepositorySpec{
			MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency},
		},
	}
}

func TestPatchBackupRepository(t *testing.T) {
	rr := mockBackupRepositoryCR()
	reconciler := mockBackupRepoReconciler(t, "", nil, nil)
	err := reconciler.Client.Create(t.Context(), rr)
	require.NoError(t, err)
	err = reconciler.patchBackupRepository(t.Context(), rr, repoReady())
	require.NoError(t, err)
	assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
	err = reconciler.patchBackupRepository(t.Context(), rr, repoNotReady("not ready"))
	require.NoError(t, err)
	assert.NotEqual(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
}

func TestCheckNotReadyRepo(t *testing.T) {
	// Test for restic repository
	t.Run("restic repository", func(t *testing.T) {
		rr := mockBackupRepositoryCR()
		rr.Spec.BackupStorageLocation = "default"
		rr.Spec.ResticIdentifier = "fake-identifier"
		rr.Spec.VolumeNamespace = "volume-ns-1"
		rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeRestic
		reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
		err := reconciler.Client.Create(t.Context(), rr)
		require.NoError(t, err)
		location := velerov1api.BackupStorageLocation{
			Spec: velerov1api.BackupStorageLocationSpec{
				Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"},
			},
			ObjectMeta: metav1.ObjectMeta{
				Namespace: velerov1api.DefaultNamespace,
				Name:      rr.Spec.BackupStorageLocation,
			},
		}

		_, err = reconciler.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
		require.NoError(t, err)
		assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
		assert.Equal(t, "s3:test.amazonaws.com/bucket/restic/volume-ns-1", rr.Spec.ResticIdentifier)
	})

	// Test for kopia repository
	t.Run("kopia repository", func(t *testing.T) {
		rr := mockBackupRepositoryCR()
		rr.Spec.BackupStorageLocation = "default"
		rr.Spec.VolumeNamespace = "volume-ns-1"
		rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeKopia
		reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
		err := reconciler.Client.Create(t.Context(), rr)
		require.NoError(t, err)
		location := velerov1api.BackupStorageLocation{
			Spec: velerov1api.BackupStorageLocationSpec{
				Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"},
			},
			ObjectMeta: metav1.ObjectMeta{
				Namespace: velerov1api.DefaultNamespace,
				Name:      rr.Spec.BackupStorageLocation,
			},
		}

		_, err = reconciler.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
		require.NoError(t, err)
		assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
		// ResticIdentifier should remain empty for kopia
		assert.Empty(t, rr.Spec.ResticIdentifier)
	})

	// Test for empty repository type (defaults to restic)
	t.Run("empty repository type", func(t *testing.T) {
		rr := mockBackupRepositoryCR()
		rr.Spec.BackupStorageLocation = "default"
		rr.Spec.ResticIdentifier = "fake-identifier"
		rr.Spec.VolumeNamespace = "volume-ns-1"
		// Deliberately leave RepositoryType empty
		reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
		err := reconciler.Client.Create(t.Context(), rr)
		require.NoError(t, err)
		location := velerov1api.BackupStorageLocation{
			Spec: velerov1api.BackupStorageLocationSpec{
				Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"},
			},
			ObjectMeta: metav1.ObjectMeta{
				Namespace: velerov1api.DefaultNamespace,
				Name:      rr.Spec.BackupStorageLocation,
			},
		}

		_, err = reconciler.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
		require.NoError(t, err)
		assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
		assert.Equal(t, "s3:test.amazonaws.com/bucket/restic/volume-ns-1", rr.Spec.ResticIdentifier)
	})
}

func startMaintenanceJobFail(client.Client, context.Context, *velerov1api.BackupRepository, string, logrus.Level, *logging.FormatFlag, logrus.FieldLogger) (string, error) {
	return "", errors.New("fake-start-error")
}

func startMaintenanceJobSucceed(client.Client, context.Context, *velerov1api.BackupRepository, string, logrus.Level, *logging.FormatFlag, logrus.FieldLogger) (string, error) {
	return "fake-job-name", nil
}

func waitMaintenanceJobCompleteFail(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error) {
	return velerov1api.BackupRepositoryMaintenanceStatus{}, errors.New("fake-wait-error")
}

func waitMaintenanceJobCompleteFunc(now time.Time, result velerov1api.BackupRepositoryMaintenanceResult, message string) func(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error) {
	completionTimeStamp := &metav1.Time{Time: now.Add(time.Hour)}
	if result == velerov1api.BackupRepositoryMaintenanceFailed {
		completionTimeStamp = nil
	}

	return func(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error) {
		return velerov1api.BackupRepositoryMaintenanceStatus{
			StartTimestamp:    &metav1.Time{Time: now},
			CompleteTimestamp: completionTimeStamp,
			Result:            result,
			Message:           message,
		}, nil
	}
}

type fakeClock struct {
	now time.Time
}

func (f *fakeClock) After(time.Duration) <-chan time.Time {
	return nil
}

func (f *fakeClock) NewTicker(time.Duration) clock.Ticker {
	return nil
}
func (f *fakeClock) NewTimer(time.Duration) clock.Timer {
	return nil
}

func (f *fakeClock) Now() time.Time {
	return f.now
}

func (f *fakeClock) Since(time.Time) time.Duration {
	return 0
}

func (f *fakeClock) Sleep(time.Duration) {}

func (f *fakeClock) Tick(time.Duration) <-chan time.Time {
	return nil
}

func (f *fakeClock) AfterFunc(time.Duration, func()) clock.Timer {
	return nil
}

func TestRunMaintenanceIfDue(t *testing.T) {
	now := time.Now().Round(time.Second)

	tests := []struct {
		name                    string
		repo                    *velerov1api.BackupRepository
		startJobFunc            func(client.Client, context.Context, *velerov1api.BackupRepository, string, logrus.Level, *logging.FormatFlag, logrus.FieldLogger) (string, error)
		waitJobFunc             func(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error)
		expectedMaintenanceTime time.Time
		expectedHistory         []velerov1api.BackupRepositoryMaintenanceStatus
		expectedErr             string
	}{
		{
			name: "not due",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
				},
				Status: velerov1api.BackupRepositoryStatus{
					LastMaintenanceTime: &metav1.Time{Time: now},
					RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
						{
							StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour)},
							CompleteTimestamp: &metav1.Time{Time: now},
							Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
						},
					},
				},
			},
			expectedMaintenanceTime: now,
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
				},
			},
		},
		{
			name: "start failed",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
				},
				Status: velerov1api.BackupRepositoryStatus{
					LastMaintenanceTime: &metav1.Time{Time: now.Add(-time.Hour - time.Minute)},
					RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
						{
							StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour * 2)},
							CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
							Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
						},
					},
				},
			},
			startJobFunc:            startMaintenanceJobFail,
			expectedMaintenanceTime: now.Add(-time.Hour - time.Minute),
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
				},
				{
					StartTimestamp: &metav1.Time{Time: now},
					Result:         velerov1api.BackupRepositoryMaintenanceFailed,
					Message:        "Failed to start maintenance job, err: fake-start-error",
				},
			},
		},
		{
			name: "wait failed",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
				},
				Status: velerov1api.BackupRepositoryStatus{
					LastMaintenanceTime: &metav1.Time{Time: now.Add(-time.Hour - time.Minute)},
					RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
						{
							StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour * 2)},
							CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
							Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
						},
					},
				},
			},
			startJobFunc:            startMaintenanceJobSucceed,
			waitJobFunc:             waitMaintenanceJobCompleteFail,
			expectedErr:             "error waiting repo maintenance completion status: fake-wait-error",
			expectedMaintenanceTime: now.Add(-time.Hour - time.Minute),
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
				},
			},
		},
		{
			name: "maintenance failed",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
				},
				Status: velerov1api.BackupRepositoryStatus{
					LastMaintenanceTime: &metav1.Time{Time: now.Add(-time.Hour - time.Minute)},
					RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
						{
							StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour * 2)},
							CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
							Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
						},
					},
				},
			},
			startJobFunc:            startMaintenanceJobSucceed,
			waitJobFunc:             waitMaintenanceJobCompleteFunc(now, velerov1api.BackupRepositoryMaintenanceFailed, "fake-maintenance-message"),
			expectedMaintenanceTime: now.Add(-time.Hour - time.Minute),
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
				},
				{
					StartTimestamp: &metav1.Time{Time: now},
					Result:         velerov1api.BackupRepositoryMaintenanceFailed,
					Message:        "fake-maintenance-message",
				},
			},
		},
		{
			name: "maintenance succeeded",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
				},
				Status: velerov1api.BackupRepositoryStatus{
					LastMaintenanceTime: &metav1.Time{Time: now.Add(-time.Hour - time.Minute)},
					RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
						{
							StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour * 2)},
							CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
							Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
						},
					},
				},
			},
			startJobFunc:            startMaintenanceJobSucceed,
			waitJobFunc:             waitMaintenanceJobCompleteFunc(now, velerov1api.BackupRepositoryMaintenanceSucceeded, ""),
			expectedMaintenanceTime: now.Add(time.Hour),
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(-time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(-time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
				},
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
				},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			reconciler := mockBackupRepoReconciler(t, "", test.repo, nil)
			reconciler.clock = &fakeClock{now}
			err := reconciler.Client.Create(t.Context(), test.repo)
			require.NoError(t, err)

			funcStartMaintenanceJob = test.startJobFunc
			funcWaitMaintenanceJobComplete = test.waitJobFunc

			err = reconciler.runMaintenanceIfDue(t.Context(), test.repo, velerotest.NewLogger())
			if test.expectedErr == "" {
				require.NoError(t, err)
			}

			assert.Equal(t, test.expectedMaintenanceTime, test.repo.Status.LastMaintenanceTime.Time)
			assert.Len(t, test.repo.Status.RecentMaintenance, len(test.expectedHistory))

			for i := 0; i < len(test.expectedHistory); i++ {
				assert.Equal(t, test.expectedHistory[i].StartTimestamp.Time, test.repo.Status.RecentMaintenance[i].StartTimestamp.Time)
				if test.expectedHistory[i].CompleteTimestamp == nil {
					assert.Nil(t, test.repo.Status.RecentMaintenance[i].CompleteTimestamp)
				} else {
					assert.Equal(t, test.expectedHistory[i].CompleteTimestamp.Time, test.repo.Status.RecentMaintenance[i].CompleteTimestamp.Time)
				}

				assert.Equal(t, test.expectedHistory[i].Result, test.repo.Status.RecentMaintenance[i].Result)
				assert.Equal(t, test.expectedHistory[i].Message, test.repo.Status.RecentMaintenance[i].Message)
			}
		})
	}
}

func TestInitializeRepo(t *testing.T) {
	rr := mockBackupRepositoryCR()
	rr.Spec.BackupStorageLocation = "default"
	reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
	err := reconciler.Client.Create(t.Context(), rr)
	require.NoError(t, err)
	location := velerov1api.BackupStorageLocation{
		Spec: velerov1api.BackupStorageLocationSpec{
			Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"},
		},
		ObjectMeta: metav1.ObjectMeta{
			Namespace: velerov1api.DefaultNamespace,
			Name:      rr.Spec.BackupStorageLocation,
		},
	}

	err = reconciler.initializeRepo(t.Context(), rr, &location, reconciler.logger)
	require.NoError(t, err)
	assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
}

func TestBackupRepoReconcile(t *testing.T) {
	tests := []struct {
		name      string
		repo      *velerov1api.BackupRepository
		expectNil bool
	}{
		{
			name: "test on api server not found",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "unknown",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency},
				},
			},
			expectNil: true,
		},
		{
			name: "test on initialize repo",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency},
				},
			},
			expectNil: true,
		},
		{
			name: "test on repo with new phase",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency},
				},
				Status: velerov1api.BackupRepositoryStatus{
					Phase: velerov1api.BackupRepositoryPhaseNew,
				},
			},
			expectNil: true,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			reconciler := mockBackupRepoReconciler(t, "", test.repo, nil)
			err := reconciler.Client.Create(t.Context(), test.repo)
			require.NoError(t, err)
			_, err = reconciler.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.repo.Namespace, Name: "repo"}})
			if test.expectNil {
				assert.NoError(t, err)
			} else {
				assert.Error(t, err)
			}
		})
	}
}

func TestGetRepositoryMaintenanceFrequency(t *testing.T) {
	tests := []struct {
		name            string
		mgr             repotypes.SnapshotIdentifier
		repo            *velerov1api.BackupRepository
		freqReturn      time.Duration
		freqError       error
		userDefinedFreq time.Duration
		expectFreq      time.Duration
	}{
		{
			name:            "user defined valid",
			userDefinedFreq: time.Hour,
			expectFreq:      time.Hour,
		},
		{
			name:       "repo return valid",
			freqReturn: time.Hour * 2,
			expectFreq: time.Hour * 2,
		},
		{
			name:            "fall to default",
			userDefinedFreq: -1,
			freqError:       errors.New("fake-error"),
			expectFreq:      defaultMaintainFrequency,
		},
		{
			name:       "fall to default, no freq error",
			freqReturn: -1,
			expectFreq: defaultMaintainFrequency,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			mgr := repomokes.Manager{}
			mgr.On("DefaultMaintenanceFrequency", mock.Anything).Return(test.freqReturn, test.freqError)
			reconciler := NewBackupRepoReconciler(
				velerov1api.DefaultNamespace,
				velerotest.NewLogger(),
				velerotest.NewFakeControllerRuntimeClient(t),
				&mgr,
				test.userDefinedFreq,
				"",
				"",
				logrus.InfoLevel,
				nil,
				nil,
			)

			freq := reconciler.getRepositoryMaintenanceFrequency(test.repo)
			assert.Equal(t, test.expectFreq, freq)
		})
	}
}

func TestNeedInvalidBackupRepo(t *testing.T) {
	tests := []struct {
		name   string
		oldBSL *velerov1api.BackupStorageLocation
		newBSL *velerov1api.BackupStorageLocation
		expect bool
	}{
		{
			name: "no change",
			oldBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "old-provider",
				},
			},
			newBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "new-provider",
				},
			},
			expect: false,
		},
		{
			name:   "other part change",
			oldBSL: &velerov1api.BackupStorageLocation{},
			newBSL: &velerov1api.BackupStorageLocation{},
			expect: false,
		},
		{
			name: "bucket change",
			oldBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Bucket: "old-bucket",
						},
					},
				},
			},
			newBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Bucket: "new-bucket",
						},
					},
				},
			},
			expect: true,
		},
		{
			name: "prefix change",
			oldBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Prefix: "old-prefix",
						},
					},
				},
			},
			newBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Prefix: "new-prefix",
						},
					},
				},
			},
			expect: true,
		},
		{
			name: "CACert change",
			oldBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							CACert: []byte{0x11, 0x12, 0x13},
						},
					},
				},
			},
			newBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							CACert: []byte{0x21, 0x22, 0x23},
						},
					},
				},
			},
			expect: true,
		},
		{
			name: "config change",
			oldBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					Config: map[string]string{
						"key1": "value1",
					},
				},
			},
			newBSL: &velerov1api.BackupStorageLocation{
				Spec: velerov1api.BackupStorageLocationSpec{
					Config: map[string]string{
						"key2": "value2",
					},
				},
			},
			expect: true,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			reconciler := NewBackupRepoReconciler(
				velerov1api.DefaultNamespace,
				velerotest.NewLogger(),
				velerotest.NewFakeControllerRuntimeClient(t),
				nil,
				time.Duration(0),
				"",
				"",
				logrus.InfoLevel,
				nil,
				nil,
			)

			need := reconciler.needInvalidBackupRepo(test.oldBSL, test.newBSL)
			assert.Equal(t, test.expect, need)
		})
	}
}

func TestGetBackupRepositoryConfig(t *testing.T) {
	configWithNoData := &corev1api.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "config-1",
			Namespace: velerov1api.DefaultNamespace,
		},
	}

	configWithWrongData := &corev1api.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "config-1",
			Namespace: velerov1api.DefaultNamespace,
		},
		Data: map[string]string{
			"fake-repo-type": "",
		},
	}

	configWithData := &corev1api.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "config-1",
			Namespace: velerov1api.DefaultNamespace,
		},
		Data: map[string]string{
			"fake-repo-type":   "{\"cacheLimitMB\": 1000, \"enableCompression\": true, \"fullMaintenanceInterval\": \"fastGC\"}",
			"fake-repo-type-1": "{\"cacheLimitMB\": 1, \"enableCompression\": false}",
		},
	}

	tests := []struct {
		name           string
		congiName      string
		repoName       string
		repoType       string
		kubeClientObj  []runtime.Object
		expectedErr    string
		expectedResult map[string]string
	}{
		{
			name: "empty configName",
		},
		{
			name:        "get error",
			congiName:   "config-1",
			expectedErr: "error getting configMap config-1: configmaps \"config-1\" not found",
		},
		{
			name:      "no config for repo",
			congiName: "config-1",
			repoName:  "fake-repo",
			repoType:  "fake-repo-type",
			kubeClientObj: []runtime.Object{
				configWithNoData,
			},
		},
		{
			name:      "unmarshall error",
			congiName: "config-1",
			repoName:  "fake-repo",
			repoType:  "fake-repo-type",
			kubeClientObj: []runtime.Object{
				configWithWrongData,
			},
			expectedErr: "error unmarshalling config data from config-1 for repo fake-repo, repo type fake-repo-type: unexpected end of JSON input",
		},
		{
			name:      "succeed",
			congiName: "config-1",
			repoName:  "fake-repo",
			repoType:  "fake-repo-type",
			kubeClientObj: []runtime.Object{
				configWithData,
			},
			expectedResult: map[string]string{
				"cacheLimitMB":            "1000",
				"enableCompression":       "true",
				"fullMaintenanceInterval": "fastGC",
			},
		},
	}

	scheme := runtime.NewScheme()
	corev1api.AddToScheme(scheme)

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			fakeClientBuilder := clientFake.NewClientBuilder()
			fakeClientBuilder = fakeClientBuilder.WithScheme(scheme)

			fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build()

			result, err := getBackupRepositoryConfig(t.Context(), fakeClient, test.congiName, velerov1api.DefaultNamespace, test.repoName, test.repoType, velerotest.NewLogger())

			if test.expectedErr != "" {
				assert.EqualError(t, err, test.expectedErr)
			} else {
				require.NoError(t, err)
				assert.Equal(t, test.expectedResult, result)
			}
		})
	}
}

func TestUpdateRepoMaintenanceHistory(t *testing.T) {
	standardTime := time.Now()

	backupRepoWithoutHistory := &velerov1api.BackupRepository{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: velerov1api.DefaultNamespace,
			Name:      "repo",
		},
	}

	backupRepoWithHistory := &velerov1api.BackupRepository{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: velerov1api.DefaultNamespace,
			Name:      "repo",
		},
		Status: velerov1api.BackupRepositoryStatus{
			RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 24)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)},
					Message:           "fake-history-message-1",
				},
			},
		},
	}

	backupRepoWithFullHistory := &velerov1api.BackupRepository{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: velerov1api.DefaultNamespace,
			Name:      "repo",
		},
		Status: velerov1api.BackupRepositoryStatus{
			RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 24)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)},
					Message:           "fake-history-message-2",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 22)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)},
					Message:           "fake-history-message-3",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 20)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)},
					Message:           "fake-history-message-4",
				},
			},
		},
	}

	backupRepoWithOverFullHistory := &velerov1api.BackupRepository{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: velerov1api.DefaultNamespace,
			Name:      "repo",
		},
		Status: velerov1api.BackupRepositoryStatus{
			RecentMaintenance: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 24)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)},
					Message:           "fake-history-message-5",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 22)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)},
					Message:           "fake-history-message-6",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 20)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)},
					Message:           "fake-history-message-7",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 18)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 17)},
					Message:           "fake-history-message-8",
				},
			},
		},
	}

	tests := []struct {
		name            string
		backupRepo      *velerov1api.BackupRepository
		result          velerov1api.BackupRepositoryMaintenanceResult
		expectedHistory []velerov1api.BackupRepositoryMaintenanceStatus
	}{
		{
			name:       "empty history",
			backupRepo: backupRepoWithoutHistory,
			result:     velerov1api.BackupRepositoryMaintenanceSucceeded,
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: standardTime},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)},
					Message:           "fake-message-0",
				},
			},
		},
		{
			name:       "less than history queue length",
			backupRepo: backupRepoWithHistory,
			result:     velerov1api.BackupRepositoryMaintenanceSucceeded,
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 24)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 23)},
					Message:           "fake-history-message-1",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)},
					Message:           "fake-message-0",
				},
			},
		},
		{
			name:       "full history",
			backupRepo: backupRepoWithFullHistory,
			result:     velerov1api.BackupRepositoryMaintenanceSucceeded,
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 22)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 21)},
					Message:           "fake-history-message-3",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 20)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)},
					Message:           "fake-history-message-4",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)},
					Message:           "fake-message-0",
				},
			},
		},
		{
			name:       "over full history",
			backupRepo: backupRepoWithOverFullHistory,
			result:     velerov1api.BackupRepositoryMaintenanceSucceeded,
			expectedHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 20)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 19)},
					Message:           "fake-history-message-7",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime.Add(-time.Hour * 18)},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(-time.Hour * 17)},
					Message:           "fake-history-message-8",
				},
				{
					StartTimestamp:    &metav1.Time{Time: standardTime},
					CompleteTimestamp: &metav1.Time{Time: standardTime.Add(time.Hour)},
					Message:           "fake-message-0",
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			updateRepoMaintenanceHistory(test.backupRepo, test.result, &metav1.Time{Time: standardTime}, &metav1.Time{Time: standardTime.Add(time.Hour)}, "fake-message-0")

			for at := range test.backupRepo.Status.RecentMaintenance {
				assert.Equal(t, test.expectedHistory[at].StartTimestamp.Time, test.backupRepo.Status.RecentMaintenance[at].StartTimestamp.Time)
				assert.Equal(t, test.expectedHistory[at].CompleteTimestamp.Time, test.backupRepo.Status.RecentMaintenance[at].CompleteTimestamp.Time)
				assert.Equal(t, test.expectedHistory[at].Message, test.backupRepo.Status.RecentMaintenance[at].Message)
			}
		})
	}
}

func TestRecallMaintenance(t *testing.T) {
	now := time.Now().Round(time.Second)

	schemeFail := runtime.NewScheme()
	velerov1api.AddToScheme(schemeFail)

	scheme := runtime.NewScheme()
	batchv1api.AddToScheme(scheme)
	corev1api.AddToScheme(scheme)
	velerov1api.AddToScheme(scheme)

	jobSucceeded := &batchv1api.Job{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "job1",
			Namespace:         velerov1api.DefaultNamespace,
			Labels:            map[string]string{maintenance.RepositoryNameLabel: "repo"},
			CreationTimestamp: metav1.Time{Time: now.Add(time.Hour)},
		},
		Status: batchv1api.JobStatus{
			StartTime:      &metav1.Time{Time: now.Add(time.Hour)},
			CompletionTime: &metav1.Time{Time: now.Add(time.Hour * 2)},
			Succeeded:      1,
		},
	}

	jobPodSucceeded := builder.ForPod(velerov1api.DefaultNamespace, "job1").Labels(map[string]string{"job-name": "job1"}).ContainerStatuses(&corev1api.ContainerStatus{
		State: corev1api.ContainerState{
			Terminated: &corev1api.ContainerStateTerminated{},
		},
	}).Result()

	tests := []struct {
		name               string
		kubeClientObj      []runtime.Object
		runtimeScheme      *runtime.Scheme
		repoLastMatainTime metav1.Time
		expectNewHistory   []velerov1api.BackupRepositoryMaintenanceStatus
		expectTimeUpdate   *metav1.Time
		expectedErr        string
	}{
		{
			name:          "wait completion error",
			runtimeScheme: schemeFail,
			expectedErr:   "error waiting incomplete repo maintenance job for repo repo: error listing maintenance job for repo repo: no kind is registered for the type v1.JobList in scheme",
		},
		{
			name:          "no consolidate result",
			runtimeScheme: scheme,
		},
		{
			name:          "no update last time",
			runtimeScheme: scheme,
			kubeClientObj: []runtime.Object{
				jobSucceeded,
				jobPodSucceeded,
			},
			repoLastMatainTime: metav1.Time{Time: now.Add(time.Hour * 5)},
			expectNewHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
				},
			},
		},
		{
			name:          "update last time",
			runtimeScheme: scheme,
			kubeClientObj: []runtime.Object{
				jobSucceeded,
				jobPodSucceeded,
			},
			expectNewHistory: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
				},
			},
			expectTimeUpdate: &metav1.Time{Time: now.Add(time.Hour * 2)},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			r := mockBackupRepoReconciler(t, "", nil, nil)

			backupRepo := mockBackupRepositoryCR()
			backupRepo.Status.LastMaintenanceTime = &test.repoLastMatainTime

			test.kubeClientObj = append(test.kubeClientObj, backupRepo)

			fakeClientBuilder := clientFake.NewClientBuilder()
			fakeClientBuilder = fakeClientBuilder.WithScheme(test.runtimeScheme)
			fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build()
			r.Client = fakeClient

			lastTm := backupRepo.Status.LastMaintenanceTime

			err := r.recallMaintenance(t.Context(), backupRepo, velerotest.NewLogger())
			if test.expectedErr != "" {
				assert.ErrorContains(t, err, test.expectedErr)
			} else {
				assert.NoError(t, err)

				if test.expectNewHistory == nil {
					assert.Nil(t, backupRepo.Status.RecentMaintenance)
				} else {
					assert.Len(t, backupRepo.Status.RecentMaintenance, len(test.expectNewHistory))
					for i := 0; i < len(test.expectNewHistory); i++ {
						assert.Equal(t, test.expectNewHistory[i].StartTimestamp.Time, backupRepo.Status.RecentMaintenance[i].StartTimestamp.Time)
						assert.Equal(t, test.expectNewHistory[i].CompleteTimestamp.Time, backupRepo.Status.RecentMaintenance[i].CompleteTimestamp.Time)
						assert.Equal(t, test.expectNewHistory[i].Result, backupRepo.Status.RecentMaintenance[i].Result)
						assert.Equal(t, test.expectNewHistory[i].Message, backupRepo.Status.RecentMaintenance[i].Message)
					}
				}

				if test.expectTimeUpdate != nil {
					assert.Equal(t, test.expectTimeUpdate.Time, backupRepo.Status.LastMaintenanceTime.Time)
				} else {
					assert.Equal(t, lastTm, backupRepo.Status.LastMaintenanceTime)
				}
			}
		})
	}
}

func TestConsolidateHistory(t *testing.T) {
	now := time.Now()

	tests := []struct {
		name     string
		cur      []velerov1api.BackupRepositoryMaintenanceStatus
		coming   []velerov1api.BackupRepositoryMaintenanceStatus
		expected []velerov1api.BackupRepositoryMaintenanceStatus
	}{
		{
			name: "zero coming",
			cur: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
			},
			expected: nil,
		},
		{
			name: "identical coming",
			cur: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
			},
			coming: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
				},
			},
			expected: nil,
		},
		{
			name: "less than limit",
			cur: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-2",
				},
			},
			coming: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
			},
			expected: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-2",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
			},
		},
		{
			name: "more than limit",
			cur: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-2",
				},
			},
			coming: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 3)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-4",
				},
			},
			expected: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-2",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 3)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-4",
				},
			},
		},
		{
			name: "more than limit 2",
			cur: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-2",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
			},
			coming: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-2",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 3)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-4",
				},
			},
			expected: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-2",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 3)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-4",
				},
			},
		},
		{
			name: "coming is not used",
			cur: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 3)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 4)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-4",
				},
			},
			coming: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
				},
			},
			expected: nil,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			consolidated := consolidateHistory(test.coming, test.cur)
			if test.expected == nil {
				assert.Nil(t, consolidated)
			} else {
				assert.Len(t, consolidated, len(test.expected))
				for i := 0; i < len(test.expected); i++ {
					assert.Equal(t, *test.expected[i].StartTimestamp, *consolidated[i].StartTimestamp)
					assert.Equal(t, *test.expected[i].CompleteTimestamp, *consolidated[i].CompleteTimestamp)
					assert.Equal(t, test.expected[i].Result, consolidated[i].Result)
					assert.Equal(t, test.expected[i].Message, consolidated[i].Message)
				}

				assert.Nil(t, consolidateHistory(test.coming, consolidated))
			}
		})
	}
}

func TestGetLastMaintenanceTimeFromHistory(t *testing.T) {
	now := time.Now()

	tests := []struct {
		name     string
		history  []velerov1api.BackupRepositoryMaintenanceStatus
		expected time.Time
	}{
		{
			name: "first one is nil",
			history: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp: &metav1.Time{Time: now},
					Result:         velerov1api.BackupRepositoryMaintenanceFailed,
					Message:        "fake-maintenance-message",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 2)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-2",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
			},
			expected: now.Add(time.Hour * 3),
		},
		{
			name: "another one is nil",
			history: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
				{
					StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:         velerov1api.BackupRepositoryMaintenanceFailed,
					Message:        "fake-maintenance-message-2",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
			},
			expected: now.Add(time.Hour * 3),
		},
		{
			name: "disordered",
			history: []velerov1api.BackupRepositoryMaintenanceStatus{
				{
					StartTimestamp:    &metav1.Time{Time: now.Add(time.Hour * 2)},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour * 3)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message-3",
				},
				{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:            velerov1api.BackupRepositoryMaintenanceSucceeded,
					Message:           "fake-maintenance-message",
				},
				{
					StartTimestamp: &metav1.Time{Time: now.Add(time.Hour)},
					Result:         velerov1api.BackupRepositoryMaintenanceFailed,
					Message:        "fake-maintenance-message-2",
				},
			},
			expected: now.Add(time.Hour * 3),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			time := getLastMaintenanceTimeFromHistory(test.history)
			assert.Equal(t, test.expected, time.Time)
		})
	}
}

func TestDeleteOldMaintenanceJobWithConfigMap(t *testing.T) {
	tests := []struct {
		name               string
		repo               *velerov1api.BackupRepository
		expectedKeptJobs   int
		maintenanceJobs    []batchv1api.Job
		bsl                *velerov1api.BackupStorageLocation
		repoMaintenanceJob *corev1api.ConfigMap
	}{
		{
			name: "test with global config",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency:  metav1.Duration{Duration: testMaintenanceFrequency},
					BackupStorageLocation: "default",
					VolumeNamespace:       "test-ns",
					RepositoryType:        "restic",
				},
				Status: velerov1api.BackupRepositoryStatus{
					Phase: velerov1api.BackupRepositoryPhaseReady,
				},
			},
			expectedKeptJobs: 5,
			maintenanceJobs: []batchv1api.Job{
				*builder.ForJob("velero", "job-01").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
				*builder.ForJob("velero", "job-02").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
				*builder.ForJob("velero", "job-03").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
				*builder.ForJob("velero", "job-04").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
				*builder.ForJob("velero", "job-05").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
				*builder.ForJob("velero", "job-06").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
			},
			bsl: builder.ForBackupStorageLocation("velero", "default").Result(),
			repoMaintenanceJob: &corev1api.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo-maintenance-job-config",
				},
				Data: map[string]string{
					"global": `{"keepLatestMaintenanceJobs": 5}`,
				},
			},
		},
		{
			name: "test with specific repo config overriding global",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency:  metav1.Duration{Duration: testMaintenanceFrequency},
					BackupStorageLocation: "default",
					VolumeNamespace:       "test-ns",
					RepositoryType:        "restic",
				},
				Status: velerov1api.BackupRepositoryStatus{
					Phase: velerov1api.BackupRepositoryPhaseReady,
				},
			},
			expectedKeptJobs: 2,
			maintenanceJobs: []batchv1api.Job{
				*builder.ForJob("velero", "job-01").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
				*builder.ForJob("velero", "job-02").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
				*builder.ForJob("velero", "job-03").ObjectMeta(builder.WithLabels(repomaintenance.RepositoryNameLabel, "repo")).Succeeded(1).Result(),
			},
			bsl: builder.ForBackupStorageLocation("velero", "default").Result(),
			repoMaintenanceJob: &corev1api.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "repo-maintenance-job-config",
				},
				Data: map[string]string{
					"global":                 `{"keepLatestMaintenanceJobs": 5}`,
					"test-ns-default-restic": `{"keepLatestMaintenanceJobs": 2}`,
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			objects := []runtime.Object{test.repo, test.bsl}
			if test.repoMaintenanceJob != nil {
				objects = append(objects, test.repoMaintenanceJob)
			}
			crClient := velerotest.NewFakeControllerRuntimeClient(t, objects...)

			for _, job := range test.maintenanceJobs {
				require.NoError(t, crClient.Create(t.Context(), &job))
			}

			repoLocker := repository.NewRepoLocker()
			mgr := repomanager.NewManager("", crClient, repoLocker, nil, nil, nil)

			repoMaintenanceConfigName := ""
			if test.repoMaintenanceJob != nil {
				repoMaintenanceConfigName = test.repoMaintenanceJob.Name
			}

			reconciler := NewBackupRepoReconciler(
				velerov1api.DefaultNamespace,
				velerotest.NewLogger(),
				crClient,
				mgr,
				time.Duration(0),
				"",
				repoMaintenanceConfigName,
				logrus.InfoLevel,
				nil,
				nil,
			)

			_, err := reconciler.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.repo.Namespace, Name: "repo"}})
			require.NoError(t, err)

			jobList := new(batchv1api.JobList)
			require.NoError(t, reconciler.Client.List(t.Context(), jobList, &client.ListOptions{Namespace: "velero"}))
			assert.Len(t, jobList.Items, test.expectedKeptJobs, "Expected %d jobs to be kept, but got %d", test.expectedKeptJobs, len(jobList.Items))
		})
	}
}

func TestInitializeRepoWithRepositoryTypes(t *testing.T) {
	scheme := runtime.NewScheme()
	corev1api.AddToScheme(scheme)
	velerov1api.AddToScheme(scheme)

	// Test for restic repository
	t.Run("restic repository", func(t *testing.T) {
		rr := mockBackupRepositoryCR()
		rr.Spec.BackupStorageLocation = "default"
		rr.Spec.VolumeNamespace = "volume-ns-1"
		rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeRestic

		location := &velerov1api.BackupStorageLocation{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: velerov1api.DefaultNamespace,
				Name:      "default",
			},
			Spec: velerov1api.BackupStorageLocationSpec{
				Provider: "aws",
				StorageType: velerov1api.StorageType{
					ObjectStorage: &velerov1api.ObjectStorageLocation{
						Bucket: "test-bucket",
						Prefix: "test-prefix",
					},
				},
				Config: map[string]string{
					"region": "us-east-1",
				},
			},
		}

		fakeClient := clientFake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(rr, location).Build()
		mgr := &repomokes.Manager{}
		mgr.On("PrepareRepo", rr).Return(nil)

		reconciler := NewBackupRepoReconciler(
			velerov1api.DefaultNamespace,
			velerotest.NewLogger(),
			fakeClient,
			mgr,
			testMaintenanceFrequency,
			"",
			"",
			logrus.InfoLevel,
			nil,
			nil,
		)

		err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
		require.NoError(t, err)

		// Verify ResticIdentifier is set for restic
		assert.NotEmpty(t, rr.Spec.ResticIdentifier)
		assert.Contains(t, rr.Spec.ResticIdentifier, "volume-ns-1")
		assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
	})

	// Test for kopia repository
	t.Run("kopia repository", func(t *testing.T) {
		rr := mockBackupRepositoryCR()
		rr.Spec.BackupStorageLocation = "default"
		rr.Spec.VolumeNamespace = "volume-ns-1"
		rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeKopia

		location := &velerov1api.BackupStorageLocation{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: velerov1api.DefaultNamespace,
				Name:      "default",
			},
			Spec: velerov1api.BackupStorageLocationSpec{
				Provider: "aws",
				StorageType: velerov1api.StorageType{
					ObjectStorage: &velerov1api.ObjectStorageLocation{
						Bucket: "test-bucket",
						Prefix: "test-prefix",
					},
				},
				Config: map[string]string{
					"region": "us-east-1",
				},
			},
		}

		fakeClient := clientFake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(rr, location).Build()
		mgr := &repomokes.Manager{}
		mgr.On("PrepareRepo", rr).Return(nil)

		reconciler := NewBackupRepoReconciler(
			velerov1api.DefaultNamespace,
			velerotest.NewLogger(),
			fakeClient,
			mgr,
			testMaintenanceFrequency,
			"",
			"",
			logrus.InfoLevel,
			nil,
			nil,
		)

		err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
		require.NoError(t, err)

		// Verify ResticIdentifier is NOT set for kopia
		assert.Empty(t, rr.Spec.ResticIdentifier)
		assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
	})

	// Test for empty repository type (defaults to restic)
	t.Run("empty repository type", func(t *testing.T) {
		rr := mockBackupRepositoryCR()
		rr.Spec.BackupStorageLocation = "default"
		rr.Spec.VolumeNamespace = "volume-ns-1"
		// Leave RepositoryType empty

		location := &velerov1api.BackupStorageLocation{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: velerov1api.DefaultNamespace,
				Name:      "default",
			},
			Spec: velerov1api.BackupStorageLocationSpec{
				Provider: "aws",
				StorageType: velerov1api.StorageType{
					ObjectStorage: &velerov1api.ObjectStorageLocation{
						Bucket: "test-bucket",
						Prefix: "test-prefix",
					},
				},
				Config: map[string]string{
					"region": "us-east-1",
				},
			},
		}

		fakeClient := clientFake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(rr, location).Build()
		mgr := &repomokes.Manager{}
		mgr.On("PrepareRepo", rr).Return(nil)

		reconciler := NewBackupRepoReconciler(
			velerov1api.DefaultNamespace,
			velerotest.NewLogger(),
			fakeClient,
			mgr,
			testMaintenanceFrequency,
			"",
			"",
			logrus.InfoLevel,
			nil,
			nil,
		)

		err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
		require.NoError(t, err)

		// Verify ResticIdentifier is set when type is empty (defaults to restic)
		assert.NotEmpty(t, rr.Spec.ResticIdentifier)
		assert.Contains(t, rr.Spec.ResticIdentifier, "volume-ns-1")
		assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
	})
}

func TestRepoMaintenanceMetricsRecording(t *testing.T) {
	now := time.Now().Round(time.Second)

	tests := []struct {
		name           string
		repo           *velerov1api.BackupRepository
		startJobFunc   func(client.Client, context.Context, *velerov1api.BackupRepository, string, logrus.Level, *logging.FormatFlag, logrus.FieldLogger) (string, error)
		waitJobFunc    func(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error)
		expectSuccess  bool
		expectFailure  bool
		expectDuration bool
	}{
		{
			name: "metrics recorded on successful maintenance",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "test-repo-success",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
				},
				Status: velerov1api.BackupRepositoryStatus{
					LastMaintenanceTime: &metav1.Time{Time: now.Add(-2 * time.Hour)},
				},
			},
			startJobFunc:   startMaintenanceJobSucceed,
			waitJobFunc:    waitMaintenanceJobCompleteFunc(now, velerov1api.BackupRepositoryMaintenanceSucceeded, ""),
			expectSuccess:  true,
			expectFailure:  false,
			expectDuration: true,
		},
		{
			name: "metrics recorded on failed maintenance",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "test-repo-failure",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
				},
				Status: velerov1api.BackupRepositoryStatus{
					LastMaintenanceTime: &metav1.Time{Time: now.Add(-2 * time.Hour)},
				},
			},
			startJobFunc: startMaintenanceJobSucceed,
			waitJobFunc: func(client.Client, context.Context, string, string, logrus.FieldLogger) (velerov1api.BackupRepositoryMaintenanceStatus, error) {
				return velerov1api.BackupRepositoryMaintenanceStatus{
					StartTimestamp:    &metav1.Time{Time: now},
					CompleteTimestamp: &metav1.Time{Time: now.Add(time.Minute)}, // Job ran for 1 minute then failed
					Result:            velerov1api.BackupRepositoryMaintenanceFailed,
					Message:           "test error",
				}, nil
			},
			expectSuccess:  false,
			expectFailure:  true,
			expectDuration: true,
		},
		{
			name: "metrics recorded on job start failure",
			repo: &velerov1api.BackupRepository{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: velerov1api.DefaultNamespace,
					Name:      "test-repo-start-fail",
				},
				Spec: velerov1api.BackupRepositorySpec{
					MaintenanceFrequency: metav1.Duration{Duration: time.Hour},
				},
				Status: velerov1api.BackupRepositoryStatus{
					LastMaintenanceTime: &metav1.Time{Time: now.Add(-2 * time.Hour)},
				},
			},
			startJobFunc:   startMaintenanceJobFail,
			expectSuccess:  false,
			expectFailure:  true,
			expectDuration: false, // No duration when job fails to start
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			// Create metrics instance
			m := metrics.NewServerMetrics()

			// Create reconciler with metrics
			reconciler := mockBackupRepoReconciler(t, "", test.repo, nil)
			reconciler.metrics = m
			reconciler.clock = &fakeClock{now}

			err := reconciler.Client.Create(t.Context(), test.repo)
			require.NoError(t, err)

			// Set up job functions
			funcStartMaintenanceJob = test.startJobFunc
			funcWaitMaintenanceJobComplete = test.waitJobFunc

			// Run maintenance
			_ = reconciler.runMaintenanceIfDue(t.Context(), test.repo, velerotest.NewLogger())

			// Verify metrics were recorded
			successCount := getMaintenanceMetricValue(t, m, "repo_maintenance_success_total", test.repo.Name)
			failureCount := getMaintenanceMetricValue(t, m, "repo_maintenance_failure_total", test.repo.Name)
			durationCount := getMaintenanceDurationCount(t, m, test.repo.Name)

			if test.expectSuccess {
				assert.Equal(t, float64(1), successCount, "Success metric should be recorded")
			} else {
				assert.Equal(t, float64(0), successCount, "Success metric should not be recorded")
			}

			if test.expectFailure {
				assert.Equal(t, float64(1), failureCount, "Failure metric should be recorded")
			} else {
				assert.Equal(t, float64(0), failureCount, "Failure metric should not be recorded")
			}

			if test.expectDuration {
				assert.Equal(t, uint64(1), durationCount, "Duration metric should be recorded")
			} else {
				assert.Equal(t, uint64(0), durationCount, "Duration metric should not be recorded")
			}
		})
	}
}

// Helper to get maintenance metric value from ServerMetrics
func getMaintenanceMetricValue(t *testing.T, m *metrics.ServerMetrics, metricName, repoName string) float64 {
	t.Helper()

	metricMap := m.Metrics()
	collector, ok := metricMap[metricName]
	if !ok {
		return 0
	}

	ch := make(chan prometheus.Metric, 1)
	collector.Collect(ch)
	close(ch)

	for metric := range ch {
		dto := &dto.Metric{}
		err := metric.Write(dto)
		require.NoError(t, err)

		for _, label := range dto.Label {
			if *label.Name == "repository_name" && *label.Value == repoName {
				if dto.Counter != nil {
					return *dto.Counter.Value
				}
			}
		}
	}
	return 0
}

// Helper to get maintenance duration histogram count
func getMaintenanceDurationCount(t *testing.T, m *metrics.ServerMetrics, repoName string) uint64 {
	t.Helper()

	metricMap := m.Metrics()
	collector, ok := metricMap["repo_maintenance_duration_seconds"]
	if !ok {
		return 0
	}

	ch := make(chan prometheus.Metric, 1)
	collector.Collect(ch)
	close(ch)

	for metric := range ch {
		dto := &dto.Metric{}
		err := metric.Write(dto)
		require.NoError(t, err)

		for _, label := range dto.Label {
			if *label.Name == "repository_name" && *label.Value == repoName {
				if dto.Histogram != nil {
					return *dto.Histogram.SampleCount
				}
			}
		}
	}
	return 0
}
