package sync

import (
	"context"
	"fmt"
	"testing"

	provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
	"github.com/grafana/grafana/apps/provisioning/pkg/repository"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
	"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
	"github.com/prometheus/client_golang/prometheus"
	mock "github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type mockReaderWriter struct {
	*repository.MockRepository
	*repository.MockVersioned
}

// FIXME: understand how the MockRepository was generated as it seems
// stale and it's causing collisions for the embedded
func (m *mockReaderWriter) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) {
	return m.MockVersioned.History(ctx, path, ref)
}

func (m *mockReaderWriter) LatestRef(ctx context.Context) (string, error) {
	return m.MockVersioned.LatestRef(ctx)
}

func (m *mockReaderWriter) CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) {
	return m.MockVersioned.CompareFiles(ctx, base, ref)
}

func (m *mockReaderWriter) Move(ctx context.Context, oldPath, newPath, ref, message string) error {
	args := m.MockRepository.Called(ctx, oldPath, newPath, ref, message)
	return args.Error(0)
}

func TestSyncer_Sync(t *testing.T) {
	tests := []struct {
		name             string
		options          provisioning.SyncJobOptions
		setupMocks       func(*mockReaderWriter, *resources.MockRepositoryResources, *resources.MockResourceClients, *jobs.MockJobProgressRecorder, *MockCompareFn, *MockFullSyncFn, *MockIncrementalSyncFn)
		expectedRef      string
		expectedError    string
		expectedMessages []string
		expectedFinalMsg string
	}{
		{
			name: "successful full sync",
			options: provisioning.SyncJobOptions{
				Incremental: false,
			},
			setupMocks: func(repo *mockReaderWriter, repoResources *resources.MockRepositoryResources, clients *resources.MockResourceClients, progress *jobs.MockJobProgressRecorder, compareFn *MockCompareFn, fullSyncFn *MockFullSyncFn, incrementalSyncFn *MockIncrementalSyncFn) {
				repo.MockRepository.On("Config").Return(&provisioning.Repository{
					ObjectMeta: metav1.ObjectMeta{
						Name: "test-repo",
					},
					Spec: provisioning.RepositorySpec{
						Title: "Test Repo",
					},
				})
				repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil)

				progress.On("SetMessage", mock.Anything, "full sync").Return()
				fullSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, mock.Anything, mock.Anything, "new-ref", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
			},
			expectedMessages: []string{"full sync"},
		},
		{
			name: "successful incremental sync",
			options: provisioning.SyncJobOptions{
				Incremental: true,
			},
			setupMocks: func(repo *mockReaderWriter, repoResources *resources.MockRepositoryResources, clients *resources.MockResourceClients, progress *jobs.MockJobProgressRecorder, compareFn *MockCompareFn, fullSyncFn *MockFullSyncFn, incrementalSyncFn *MockIncrementalSyncFn) {
				repo.MockRepository.On("Config").Return(&provisioning.Repository{
					ObjectMeta: metav1.ObjectMeta{
						Name: "test-repo",
					},
					Status: provisioning.RepositoryStatus{
						Sync: provisioning.SyncStatus{
							LastRef: "old-ref",
						},
					},
				})
				repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil)
				progress.On("SetMessage", mock.Anything, "incremental sync").Return()
				incrementalSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
			},
			expectedRef:      "new-ref",
			expectedMessages: []string{"incremental sync"},
		},
		{
			name: "latest ref error",
			options: provisioning.SyncJobOptions{
				Incremental: true,
			},
			setupMocks: func(repo *mockReaderWriter, repoResources *resources.MockRepositoryResources, clients *resources.MockResourceClients, progress *jobs.MockJobProgressRecorder, compareFn *MockCompareFn, fullSyncFn *MockFullSyncFn, incrementalSyncFn *MockIncrementalSyncFn) {
				repo.MockRepository.On("Config").Return(&provisioning.Repository{
					ObjectMeta: metav1.ObjectMeta{
						Name: "test-repo",
					},
					Status: provisioning.RepositoryStatus{
						Sync: provisioning.SyncStatus{
							LastRef: "old-ref",
						},
					},
				})
				repo.MockVersioned.On("LatestRef", mock.Anything).Return("", fmt.Errorf("failed to get latest ref"))
			},
			expectedError: "get latest ref: failed to get latest ref",
		},
		{
			name: "incremental sync error",
			options: provisioning.SyncJobOptions{
				Incremental: true,
			},
			setupMocks: func(repo *mockReaderWriter, repoResources *resources.MockRepositoryResources, clients *resources.MockResourceClients, progress *jobs.MockJobProgressRecorder, compareFn *MockCompareFn, fullSyncFn *MockFullSyncFn, incrementalSyncFn *MockIncrementalSyncFn) {
				repo.MockRepository.On("Config").Return(&provisioning.Repository{
					ObjectMeta: metav1.ObjectMeta{
						Name: "test-repo",
					},
					Status: provisioning.RepositoryStatus{
						Sync: provisioning.SyncStatus{
							LastRef: "old-ref",
						},
					},
				})
				repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil)
				progress.On("SetMessage", mock.Anything, "incremental sync").Return()
				incrementalSyncFn.On("Execute", mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("incremental sync failed"))
			},
			expectedRef:      "new-ref",
			expectedMessages: []string{"incremental sync"},
			expectedError:    "incremental sync failed",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			repoResources := resources.NewMockRepositoryResources(t)
			clients := resources.NewMockResourceClients(t)
			progress := jobs.NewMockJobProgressRecorder(t)
			compareFn := NewMockCompareFn(t)
			fullSyncFn := NewMockFullSyncFn(t)
			incrementalSyncFn := NewMockIncrementalSyncFn(t)

			repo := &mockReaderWriter{
				MockRepository: repository.NewMockRepository(t),
				MockVersioned:  repository.NewMockVersioned(t),
			}

			tt.setupMocks(repo, repoResources, clients, progress, compareFn, fullSyncFn, incrementalSyncFn)

			syncer := NewSyncer(
				compareFn.Execute,
				fullSyncFn.Execute,
				incrementalSyncFn.Execute,
				tracing.NewNoopTracerService(),
				10,
				jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()),
			)

			ref, err := syncer.Sync(context.Background(), repo, tt.options, repoResources, clients, progress)
			if tt.expectedError != "" {
				require.EqualError(t, err, tt.expectedError)
			} else {
				require.NoError(t, err)
				if tt.expectedRef != "" {
					require.Equal(t, tt.expectedRef, ref)
				}
			}

			// Verify expected messages were set
			if len(tt.expectedMessages) > 0 {
				for _, msg := range tt.expectedMessages {
					progress.AssertCalled(t, "SetMessage", mock.Anything, msg)
				}
			}

			if tt.expectedFinalMsg != "" {
				progress.AssertCalled(t, "SetFinalMessage", mock.Anything, tt.expectedFinalMsg)
			}
		})
	}
}
