package controller

import (
	"context"
	"testing"
	"time"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/rest"

	"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller/mocks"
	"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"

	provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
	provisioningv0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/generated/applyconfiguration/provisioning/v0alpha1"
	client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1"
	"github.com/grafana/grafana/apps/provisioning/pkg/repository"
)

type mockProvisioningV0alpha1Interface struct {
	repositoriesFunc func(namespace string) client.RepositoryInterface
}

func (m mockProvisioningV0alpha1Interface) RESTClient() rest.Interface {
	panic("not needed for testing")
}

func (m mockProvisioningV0alpha1Interface) HistoricJobs(namespace string) client.HistoricJobInterface {
	panic("not needed for testing")
}

func (m mockProvisioningV0alpha1Interface) Jobs(namespace string) client.JobInterface {
	panic("not needed for testing")
}

func (m mockProvisioningV0alpha1Interface) Connections(namespace string) client.ConnectionInterface {
	panic("not needed for testing")
}

func (m mockProvisioningV0alpha1Interface) Repositories(namespace string) client.RepositoryInterface {
	if m.repositoriesFunc != nil {
		return m.repositoriesFunc(namespace)
	}
	return nil
}

type mockRepoInterface struct {
	patchFunc func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *provisioning.Repository, err error)
}

func (m mockRepoInterface) Create(ctx context.Context, repository *provisioning.Repository, opts metav1.CreateOptions) (*provisioning.Repository, error) {
	panic("not needed for testing")
}

func (m mockRepoInterface) Update(ctx context.Context, repository *provisioning.Repository, opts metav1.UpdateOptions) (*provisioning.Repository, error) {
	panic("not needed for testing")
}

func (m mockRepoInterface) UpdateStatus(ctx context.Context, repository *provisioning.Repository, opts metav1.UpdateOptions) (*provisioning.Repository, error) {
	panic("not needed for testing")
}

func (m mockRepoInterface) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
	panic("not needed for testing")
}

func (m mockRepoInterface) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
	panic("not needed for testing")
}

func (m mockRepoInterface) Get(ctx context.Context, name string, opts metav1.GetOptions) (*provisioning.Repository, error) {
	panic("not needed for testing")
}

func (m mockRepoInterface) List(ctx context.Context, opts metav1.ListOptions) (*provisioning.RepositoryList, error) {
	panic("not needed for testing")
}

func (m mockRepoInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
	panic("not needed for testing")
}

func (m mockRepoInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *provisioning.Repository, err error) {
	if m.patchFunc != nil {
		return m.patchFunc(ctx, name, pt, data, opts, subresources...)
	}
	return nil, nil
}

func (m mockRepoInterface) Apply(ctx context.Context, repository *provisioningv0alpha1.RepositoryApplyConfiguration, opts metav1.ApplyOptions) (result *provisioning.Repository, err error) {
	panic("not needed for testing")
}

func (m mockRepoInterface) ApplyStatus(ctx context.Context, repository *provisioningv0alpha1.RepositoryApplyConfiguration, opts metav1.ApplyOptions) (result *provisioning.Repository, err error) {
	panic("not needed for testing")
}

var (
	_ client.ProvisioningV0alpha1Interface = (*mockProvisioningV0alpha1Interface)(nil)
	_ client.RepositoryInterface           = (*mockRepoInterface)(nil)
)

func TestRepositoryController_handleDelete(t *testing.T) {
	testCases := []struct {
		name          string
		repoFactory   repository.Factory
		finalizer     finalizerProcessor
		client        client.ProvisioningV0alpha1Interface
		statusPatcher StatusPatcher
		repo          *provisioning.Repository
		expectedErr   string
	}{
		{
			name:          "No finalizers",
			repoFactory:   nil,
			finalizer:     nil,
			client:        nil,
			statusPatcher: nil,
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Finalizers: []string{},
				},
			},
		},
		{
			name: "Finalizers deleted successfully",
			repoFactory: func() repository.Factory {
				f := repository.NewMockFactory(t)

				f.
					On("Build", context.Background(), mock.Anything).
					Once().
					Return(nil, nil)

				return f
			}(),
			finalizer: func() finalizerProcessor {
				f := NewMockFinalizerProcessor(t)

				f.
					On("process", context.Background(), nil, []string{
						repository.RemoveOrphanResourcesFinalizer,
					}).
					Once().
					Return(nil)

				return f
			}(),
			client: func() client.ProvisioningV0alpha1Interface {
				repo := &mockRepoInterface{
					patchFunc: func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *provisioning.Repository, err error) {
						return &provisioning.Repository{}, nil
					},
				}
				c := &mockProvisioningV0alpha1Interface{
					repositoriesFunc: func(namespace string) client.RepositoryInterface {
						return repo
					},
				}

				return c
			}(),
			statusPatcher: nil,
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Finalizers: []string{
						repository.RemoveOrphanResourcesFinalizer,
					},
				},
			},
		},
		{
			name: "Error when building repository",
			repoFactory: func() repository.Factory {
				f := repository.NewMockFactory(t)

				f.
					On("Build", context.Background(), mock.Anything).
					Once().
					Return(nil, assert.AnError)

				return f
			}(),
			finalizer:     nil,
			client:        nil,
			statusPatcher: nil,
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Finalizers: []string{
						repository.RemoveOrphanResourcesFinalizer,
					},
				},
			},
			expectedErr: "create repository from configuration: " + assert.AnError.Error(),
		},
		{
			name: "Error when processing finalizer",
			repoFactory: func() repository.Factory {
				f := repository.NewMockFactory(t)

				f.
					On("Build", context.Background(), mock.Anything).
					Once().
					Return(nil, nil)

				return f
			}(),
			finalizer: func() finalizerProcessor {
				f := NewMockFinalizerProcessor(t)

				f.
					On("process", context.Background(), nil, []string{
						repository.RemoveOrphanResourcesFinalizer,
					}).
					Once().
					Return(assert.AnError)

				return f
			}(),
			statusPatcher: func() StatusPatcher {
				s := mocks.NewStatusPatcher(t)

				s.
					On("Patch", context.Background(), mock.AnythingOfType("*v0alpha1.Repository"), mock.AnythingOfType("map[string]interface {}")).
					Once().
					Return(nil) // Return nil error for the status patch

				return s
			}(),
			client: nil,
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Finalizers: []string{
						repository.RemoveOrphanResourcesFinalizer,
					},
				},
			},
			expectedErr: "process finalizers: " + assert.AnError.Error(),
		},
		{
			name: "Error when patching finalizers",
			repoFactory: func() repository.Factory {
				f := repository.NewMockFactory(t)

				f.
					On("Build", context.Background(), mock.Anything).
					Once().
					Return(nil, nil)

				return f
			}(),
			finalizer: func() finalizerProcessor {
				f := NewMockFinalizerProcessor(t)

				f.
					On("process", context.Background(), nil, []string{
						repository.RemoveOrphanResourcesFinalizer,
					}).
					Once().
					Return(nil)

				return f
			}(),
			client: func() client.ProvisioningV0alpha1Interface {
				repo := &mockRepoInterface{
					patchFunc: func(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *provisioning.Repository, err error) {
						return &provisioning.Repository{}, assert.AnError
					},
				}
				c := &mockProvisioningV0alpha1Interface{
					repositoriesFunc: func(namespace string) client.RepositoryInterface {
						return repo
					},
				}

				return c
			}(),
			statusPatcher: nil,
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Finalizers: []string{
						repository.RemoveOrphanResourcesFinalizer,
					},
				},
			},
			expectedErr: "remove finalizers: " + assert.AnError.Error(),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			c := &RepositoryController{
				repoFactory:   tc.repoFactory,
				finalizer:     tc.finalizer,
				client:        tc.client,
				statusPatcher: tc.statusPatcher,
			}

			err := c.handleDelete(context.Background(), tc.repo)
			if tc.expectedErr != "" {
				assert.Error(t, err)
				assert.ErrorContains(t, err, tc.expectedErr)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestShouldUseIncrementalSync(t *testing.T) {
	versioned := repository.NewMockVersioned(t)
	obj := &provisioning.Repository{
		Status: provisioning.RepositoryStatus{
			Sync: provisioning.SyncStatus{
				LastRef: "123",
			},
		},
	}
	latestRef := "456"
	t.Run("should use incremental sync", func(t *testing.T) {
		versioned.On("CompareFiles", context.Background(), obj.Status.Sync.LastRef, latestRef).Return([]repository.VersionedFileChange{
			{
				Action: repository.FileActionDeleted,
				Path:   "test.json",
			},
		}, nil).Once()
		got, err := shouldUseIncrementalSync(context.Background(), versioned, obj, latestRef)
		assert.NoError(t, err)
		assert.True(t, got)
	})

	t.Run("should not use incremental sync", func(t *testing.T) {
		versioned.On("CompareFiles", context.Background(), obj.Status.Sync.LastRef, latestRef).Return([]repository.VersionedFileChange{
			{
				Action: repository.FileActionDeleted,
				Path:   "test/.keep",
			},
		}, nil).Once()
		got, err := shouldUseIncrementalSync(context.Background(), versioned, obj, latestRef)
		assert.NoError(t, err)
		assert.False(t, got)
	})
}

// mockJobsQueueStore implements both jobs.Queue and jobs.Store for testing
type mockJobsQueueStore struct {
	*jobs.MockQueue
	*jobs.MockStore
}

func TestRepositoryController_process_UnhealthyRepositoryStatusUpdate(t *testing.T) {
	testCases := []struct {
		name                     string
		repo                     *provisioning.Repository
		healthStatus             provisioning.HealthStatus
		hasHealthStatusChanged   bool
		expectedUnhealthyMessage bool
		description              string
	}{
		{
			name: "unhealthy repository should set unhealthy message in sync status",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:       "test-repo",
					Namespace:  "default",
					Generation: 1,
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					ObservedGeneration: 1,
					Health: provisioning.HealthStatus{
						Healthy: true,
						Checked: time.Now().Add(-10 * time.Minute).UnixMilli(),
					},
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStateSuccess,
						Finished: time.Now().Add(-1 * time.Minute).UnixMilli(),
						Message:  []string{},
					},
				},
			},
			healthStatus: provisioning.HealthStatus{
				Healthy: false,
				Error:   provisioning.HealthFailureHealth,
				Checked: time.Now().UnixMilli(),
				Message: []string{"connection failed"},
			},
			hasHealthStatusChanged:   true,
			expectedUnhealthyMessage: true,
			description:              "should set unhealthy message when repository becomes unhealthy",
		},
		{
			name: "unhealthy repository should not duplicate unhealthy message",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:       "test-repo",
					Namespace:  "default",
					Generation: 1,
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					ObservedGeneration: 1,
					Health: provisioning.HealthStatus{
						Healthy: false,
						Checked: time.Now().Add(-2 * time.Minute).UnixMilli(),
					},
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStateError,
						Finished: time.Now().Add(-1 * time.Minute).UnixMilli(),
						Message:  []string{"Repository is unhealthy"},
					},
				},
			},
			healthStatus: provisioning.HealthStatus{
				Healthy: false,
				Error:   provisioning.HealthFailureHealth,
				Checked: time.Now().UnixMilli(),
				Message: []string{"connection failed"},
			},
			hasHealthStatusChanged:   false,
			expectedUnhealthyMessage: false,
			description:              "should not set unhealthy message when it already exists",
		},
		{
			name: "healthy repository should clear unhealthy message",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:       "test-repo",
					Namespace:  "default",
					Generation: 1,
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					ObservedGeneration: 1,
					Health: provisioning.HealthStatus{
						Healthy: false,
						Checked: time.Now().Add(-2 * time.Minute).UnixMilli(),
					},
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStateError,
						Finished: time.Now().Add(-1 * time.Minute).UnixMilli(),
						Message:  []string{"Repository is unhealthy"},
					},
				},
			},
			healthStatus: provisioning.HealthStatus{
				Healthy: true,
				Checked: time.Now().UnixMilli(),
				Message: []string{},
			},
			hasHealthStatusChanged:   true,
			expectedUnhealthyMessage: false,
			description:              "should clear unhealthy message when repository becomes healthy",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Create controller
			rc := &RepositoryController{}

			// Determine sync status ops (this is a pure function, no mocks needed)
			syncOps := rc.determineSyncStatusOps(tc.repo, nil, tc.healthStatus)

			// Verify expectations
			hasUnhealthyOp := false
			hasClearUnhealthyOp := false
			for _, op := range syncOps {
				if path, ok := op["path"].(string); ok {
					if path == "/status/sync/message" {
						if messages, ok := op["value"].([]string); ok {
							if len(messages) > 0 && messages[0] == "Repository is unhealthy" {
								hasUnhealthyOp = true
							} else if len(messages) == 0 {
								hasClearUnhealthyOp = true
							}
						}
					}
				}
			}

			if tc.expectedUnhealthyMessage {
				assert.True(t, hasUnhealthyOp, tc.description+": expected unhealthy message operation")
			} else if len(tc.repo.Status.Sync.Message) > 0 && tc.healthStatus.Healthy {
				assert.True(t, hasClearUnhealthyOp, tc.description+": expected clear unhealthy message operation")
			}
		})
	}
}

func TestRepositoryController_shouldResync_StaleSyncStatus(t *testing.T) {
	testCases := []struct {
		name           string
		repo           *provisioning.Repository
		jobGetError    error
		expectedResync bool
		description    string
	}{
		{
			name: "stale sync status with Pending state - job not found",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-repo",
					Namespace: "default",
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStatePending,
						JobID:    "test-job-123",
						Started:  time.Now().Add(-10 * time.Minute).UnixMilli(),
						Finished: time.Now().Add(-10 * time.Minute).UnixMilli(),
					},
				},
			},
			jobGetError:    apierrors.NewNotFound(schema.GroupResource{Resource: "jobs"}, "test-job-123"),
			expectedResync: true,
			description:    "should return true to trigger resync when job is not found",
		},
		{
			name: "stale sync status with Working state - job not found",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-repo",
					Namespace: "default",
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStateWorking,
						JobID:    "test-job-456",
						Started:  time.Now().Add(-5 * time.Minute).UnixMilli(),
						Finished: time.Now().Add(-5 * time.Minute).UnixMilli(),
					},
				},
			},
			jobGetError:    apierrors.NewNotFound(schema.GroupResource{Resource: "jobs"}, "test-job-456"),
			expectedResync: true,
			description:    "should return true to trigger resync when working job is not found",
		},
		{
			name: "non-stale sync status - job exists",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-repo",
					Namespace: "default",
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStatePending,
						JobID:    "test-job-789",
						Started:  time.Now().Add(-2 * time.Minute).UnixMilli(),
						Finished: time.Now().Add(-2 * time.Minute).UnixMilli(),
					},
				},
			},
			jobGetError:    nil,   // Job exists
			expectedResync: false, // Should continue with normal logic
			description:    "should continue with normal logic when job exists",
		},
		{
			name: "non-stale sync status - no JobID",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-repo",
					Namespace: "default",
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStatePending,
						JobID:    "",
						Started:  time.Now().Add(-2 * time.Minute).UnixMilli(),
						Finished: time.Now().Add(-2 * time.Minute).UnixMilli(),
					},
				},
			},
			jobGetError:    nil,
			expectedResync: false,
			description:    "should not check when JobID is empty",
		},
		{
			name: "non-stale sync status - already finished",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-repo",
					Namespace: "default",
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStateSuccess,
						JobID:    "test-job-999",
						Finished: time.Now().Add(-1 * time.Minute).UnixMilli(),
					},
				},
			},
			jobGetError:    nil,
			expectedResync: false,
			description:    "should not check when sync status is already finished",
		},
		{
			name: "stale sync status - job lookup error (non-NotFound)",
			repo: &provisioning.Repository{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-repo",
					Namespace: "default",
				},
				Spec: provisioning.RepositorySpec{
					Sync: provisioning.SyncOptions{
						Enabled:         true,
						IntervalSeconds: 300,
					},
				},
				Status: provisioning.RepositoryStatus{
					Sync: provisioning.SyncStatus{
						State:    provisioning.JobStatePending,
						JobID:    "test-job-error",
						Started:  time.Now().Add(-2 * time.Minute).UnixMilli(),
						Finished: time.Now().Add(-2 * time.Minute).UnixMilli(),
					},
				},
			},
			jobGetError:    assert.AnError, // Non-NotFound error
			expectedResync: false,          // Should continue with normal logic
			description:    "should handle non-NotFound errors gracefully and continue with normal logic",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Create mocks
			mockQueue := jobs.NewMockQueue(t)
			mockStore := jobs.NewMockStore(t)
			mockJobs := &mockJobsQueueStore{
				MockQueue: mockQueue,
				MockStore: mockStore,
			}

			// Set up job Get mock
			if tc.repo.Status.Sync.JobID != "" && (tc.repo.Status.Sync.State == provisioning.JobStatePending || tc.repo.Status.Sync.State == provisioning.JobStateWorking) {
				mockStore.On("Get", mock.Anything, tc.repo.Namespace, tc.repo.Status.Sync.JobID).Return(nil, tc.jobGetError).Once()
			}

			// Create controller
			rc := &RepositoryController{
				jobs: mockJobs,
			}

			// Test shouldResync
			ctx := context.Background()
			result := rc.shouldResync(ctx, tc.repo)

			// Verify
			assert.Equal(t, tc.expectedResync, result, tc.description)
		})
	}
}
