package agentk

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"testing"

	notificationv1 "github.com/fluxcd/notification-controller/api/v1"
	sourcev1 "github.com/fluxcd/source-controller/api/v1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_k8s"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testhelpers"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	"go.uber.org/mock/gomock"
	v1 "k8s.io/api/core/v1"
	kubeerrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

var (
	_ controller = &gitRepositoryController{}
)

func TestGitRepositoryController_getProjectPathFromRepositoryUrl(t *testing.T) {
	testcases := []struct {
		name             string
		fullURL          string
		expectedFullPath string
	}{
		{
			name:             "HTTPS Url with .git extension",
			fullURL:          "https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent.git",
			expectedFullPath: "gitlab-org/cluster-integration/gitlab-agent",
		},
		{
			name:             "HTTPS Url without .git extension",
			fullURL:          "https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent",
			expectedFullPath: "gitlab-org/cluster-integration/gitlab-agent",
		},
		{
			name:             "SSH Url with .git extension",
			fullURL:          "ssh://git@gitlab.com/gitlab-org/cluster-integration/gitlab-agent.git",
			expectedFullPath: "gitlab-org/cluster-integration/gitlab-agent",
		},
		{
			name:             "SSH Url without .git extension",
			fullURL:          "ssh://git@gitlab.com/gitlab-org/cluster-integration/gitlab-agent",
			expectedFullPath: "gitlab-org/cluster-integration/gitlab-agent",
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			// WHEN
			actualFullPath, err := getProjectPathFromRepositoryURL(tc.fullURL)

			// THEN
			require.NoError(t, err)
			assert.Equal(t, tc.expectedFullPath, actualFullPath)
		})
	}
}

func TestGitRepositoryController_ReconcileWithInvalidKeyError(t *testing.T) {
	// GIVEN
	c := &gitRepositoryController{}

	// WHEN
	res := c.reconcile(context.Background(), "foo/bar/too-much")

	// THEN
	assert.Equal(t, Error, res.status)
	assert.ErrorContains(t, res.error, "invalid key format")
}

func TestGitRepositoryController_RetryIfUnableToGetObjectToReconcile(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	c := &gitRepositoryController{
		gitRepositoryLister: mockGitRepositoryLister,
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(nil, errors.New("test"))

	// WHEN
	res := c.reconcile(context.Background(), "namespace/name")

	// THEN
	assert.Equal(t, RetryRateLimited, res.status)
	assert.ErrorContains(t, res.error, "unable to list GitRepository object namespace/name")
}

func TestGitRepositoryController_DropNotExistingObjectToReconcileWithSuccess(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	c := &gitRepositoryController{
		log:                 testlogger.New(t),
		gitRepositoryLister: mockGitRepositoryLister,
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(nil, kubeerrors.NewNotFound(schema.GroupResource{}, "test"))

	// WHEN
	res := c.reconcile(context.Background(), "namespace/name")

	// THEN
	assert.Equal(t, Success, res.status)
}

func TestGitRepositoryController_DropGitRepositoryNotInConfiguredGitLabWithSuccess(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	c := &gitRepositoryController{
		log:                 testlogger.New(t),
		gitRepositoryLister: mockGitRepositoryLister,
		gitLabExternalURL:   url.URL{Scheme: "https", Host: "another-host.example.com"},
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(getTestGitRepositoryAsRuntimeObject(t), nil)

	// WHEN
	res := c.reconcile(context.Background(), "namespace/name")

	// THEN
	assert.Equal(t, Success, res.status)
}

func TestGitRepositoryController_SuccessfullyCreateReceiverAndSecretForGitRepository(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	mockReceiverAPIClient := mock_k8s.NewMockNamespaceableResourceInterface(ctrl)
	mockNamespacedReceiverAPIClient := mock_k8s.NewMockResourceInterface(ctrl)
	mockCoreV1APIClient := mock_k8s.NewMockCoreV1Interface(ctrl)
	mockSecretsAPIClient := mock_k8s.NewMockSecretInterface(ctrl)
	c := &gitRepositoryController{
		log:                 testlogger.New(t),
		gitRepositoryLister: mockGitRepositoryLister,
		receiverAPIClient:   mockReceiverAPIClient,
		corev1APIClient:     mockCoreV1APIClient,
		gitLabExternalURL:   url.URL{Scheme: "https", Host: "gitlab.example.com:8080"},
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(getTestGitRepositoryAsRuntimeObject(t), nil)

	// Secret
	mockCoreV1APIClient.EXPECT().Secrets("namespace").Return(mockSecretsAPIClient)
	mockSecretsAPIClient.EXPECT().Apply(gomock.Any(), gomock.Any(), gomock.Any())
	// The following expectations are for the removal of the receiver secret with the deprecated name
	mockSecretsAPIClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("dummy error to abort removal"))

	// Receiver
	mockReceiverAPIClient.EXPECT().Namespace("namespace").Return(mockNamespacedReceiverAPIClient)
	mockNamespacedReceiverAPIClient.EXPECT().Apply(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())

	// WHEN
	res := c.reconcile(context.Background(), "namespace/name")

	// THEN
	assert.Equal(t, Success, res.status)
}

func TestGitRepositoryController_DeleteDeprecatedReceiverSecret(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockCoreV1APIClient := mock_k8s.NewMockCoreV1Interface(ctrl)
	mockSecretsAPIClient := mock_k8s.NewMockSecretInterface(ctrl)
	c := &gitRepositoryController{
		log:               testlogger.New(t),
		corev1APIClient:   mockCoreV1APIClient,
		gitLabExternalURL: url.URL{Scheme: "https", Host: "gitlab.example.com:8080"},
		agentKey:          testhelpers.AgentkKey1,
	}

	// Secret
	mockSecretsAPIClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				api.AgentIDKey: strconv.FormatInt(testhelpers.AgentkKey1.ID, 10),
			},
		},
	}, nil)
	mockSecretsAPIClient.EXPECT().Delete(gomock.Any(), "gitlab-test", gomock.Any())

	// WHEN
	c.deleteDeprecatedReceiverSecret(context.Background(), mockSecretsAPIClient, "gitlab-test")
}

func TestGitRepositoryController_IgnoreUnmanagedDeprecatedReceiverSecret(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockCoreV1APIClient := mock_k8s.NewMockCoreV1Interface(ctrl)
	mockSecretsAPIClient := mock_k8s.NewMockSecretInterface(ctrl)
	c := &gitRepositoryController{
		log:               testlogger.New(t),
		corev1APIClient:   mockCoreV1APIClient,
		gitLabExternalURL: url.URL{Scheme: "https", Host: "gitlab.example.com:8080"},
		agentKey:          testhelpers.AgentkKey1,
	}

	// Secret
	mockSecretsAPIClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{
				api.AgentIDKey: "another-agent",
			},
		},
	}, nil)

	// WHEN
	c.deleteDeprecatedReceiverSecret(context.Background(), mockSecretsAPIClient, "gitlab-test")
}

func TestGitRepositoryController_RetryOnSecretReconciliationFailureForGitRepository(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	mockReceiverAPIClient := mock_k8s.NewMockNamespaceableResourceInterface(ctrl)
	mockCoreV1APIClient := mock_k8s.NewMockCoreV1Interface(ctrl)
	mockSecretsAPIClient := mock_k8s.NewMockSecretInterface(ctrl)
	c := &gitRepositoryController{
		log:                 testlogger.New(t),
		gitRepositoryLister: mockGitRepositoryLister,
		receiverAPIClient:   mockReceiverAPIClient,
		corev1APIClient:     mockCoreV1APIClient,
		gitLabExternalURL:   url.URL{Scheme: "https", Host: "gitlab.example.com:8080"},
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(getTestGitRepositoryAsRuntimeObject(t), nil)

	// Secret
	mockCoreV1APIClient.EXPECT().Secrets("namespace").Return(mockSecretsAPIClient)
	mockSecretsAPIClient.EXPECT().Apply(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("expected apply failure for secret")).Times(1)

	// WHEN
	res := c.reconcile(context.Background(), "namespace/name")

	// THEN
	assert.Equal(t, RetryRateLimited, res.status)
}

func TestGitRepositoryController_RetryOnReceiverReconciliationFailureForGitRepository(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	mockReceiverAPIClient := mock_k8s.NewMockNamespaceableResourceInterface(ctrl)
	mockNamespacedReceiverAPIClient := mock_k8s.NewMockResourceInterface(ctrl)
	mockCoreV1APIClient := mock_k8s.NewMockCoreV1Interface(ctrl)
	mockSecretsAPIClient := mock_k8s.NewMockSecretInterface(ctrl)
	c := &gitRepositoryController{
		log:                 testlogger.New(t),
		gitRepositoryLister: mockGitRepositoryLister,
		receiverAPIClient:   mockReceiverAPIClient,
		corev1APIClient:     mockCoreV1APIClient,
		gitLabExternalURL:   url.URL{Scheme: "https", Host: "gitlab.example.com:8080"},
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(getTestGitRepositoryAsRuntimeObject(t), nil)

	// Secret
	mockCoreV1APIClient.EXPECT().Secrets("namespace").Return(mockSecretsAPIClient)
	mockSecretsAPIClient.EXPECT().Apply(gomock.Any(), gomock.Any(), gomock.Any())
	// The following expectations are for the removal of the receiver secret with the deprecated name
	mockSecretsAPIClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("dummy error to abort removal"))

	// Receiver
	mockReceiverAPIClient.EXPECT().Namespace("namespace").Return(mockNamespacedReceiverAPIClient)
	mockNamespacedReceiverAPIClient.EXPECT().Apply(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("expected apply failure for receiver")).Times(1)

	// WHEN
	res := c.reconcile(context.Background(), "namespace/name")

	// THEN
	assert.Equal(t, RetryRateLimited, res.status)
}

func TestGitRepositoryController_IgnoreConflictOnReceiverReconciliationFailureForGitRepository(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	mockReceiverAPIClient := mock_k8s.NewMockNamespaceableResourceInterface(ctrl)
	mockNamespacedReceiverAPIClient := mock_k8s.NewMockResourceInterface(ctrl)
	mockCoreV1APIClient := mock_k8s.NewMockCoreV1Interface(ctrl)
	mockSecretsAPIClient := mock_k8s.NewMockSecretInterface(ctrl)
	c := &gitRepositoryController{
		log:                 testlogger.New(t),
		gitRepositoryLister: mockGitRepositoryLister,
		receiverAPIClient:   mockReceiverAPIClient,
		corev1APIClient:     mockCoreV1APIClient,
		gitLabExternalURL:   url.URL{Scheme: "https", Host: "gitlab.example.com:8080"},
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(getTestGitRepositoryAsRuntimeObject(t), nil)

	// Secret
	mockCoreV1APIClient.EXPECT().Secrets("namespace").Return(mockSecretsAPIClient)
	mockSecretsAPIClient.EXPECT().Apply(gomock.Any(), gomock.Any(), gomock.Any())
	// The following expectations are for the removal of the receiver secret with the deprecated name
	mockSecretsAPIClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("dummy error to abort removal"))

	// Receiver
	mockReceiverAPIClient.EXPECT().Namespace("namespace").Return(mockNamespacedReceiverAPIClient)
	mockNamespacedReceiverAPIClient.EXPECT().Apply(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, kubeerrors.NewConflict(schema.GroupResource{}, "test", errors.New("conflict"))).Times(1)

	// WHEN
	res := c.reconcile(context.Background(), "namespace/name")

	// THEN
	assert.Equal(t, Success, res.status)
}

func TestGitRepositoryController_ReceiverObjUpdateChangeTriggersProjectReconciliation(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	mockProjectReconciler := NewMockprojectReconciler(ctrl)
	mockWorkqueue := mock_k8s.NewMockRateLimitingWorkqueue[string](ctrl)
	c := &gitRepositoryController{
		log:                 testlogger.New(t),
		gitRepositoryLister: mockGitRepositoryLister,
		projectReconciler:   mockProjectReconciler,
		workqueue:           mockWorkqueue,
		gitLabExternalURL:   url.URL{Scheme: "https", Host: "gitlab.example.com:8080"},
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(getTestGitRepositoryAsRuntimeObject(t), nil)
	mockWorkqueue.EXPECT().Add(gomock.Any())

	mockProjectReconciler.EXPECT().ReconcileIndexedProjects(gomock.Any())

	// WHEN
	c.handleReceiverObj(context.Background(), getTestReceiverAsInterface())
}

func TestGitRepositoryController_ReceiverObjUpdateChangeTriggersProjectReconciliationForDelete(t *testing.T) {
	// GIVEN
	ctrl := gomock.NewController(t)
	mockGitRepositoryLister := mock_k8s.NewMockGenericLister(ctrl)
	mockNamespaceLister := mock_k8s.NewMockGenericNamespaceLister(ctrl)
	mockProjectReconciler := NewMockprojectReconciler(ctrl)
	mockWorkqueue := mock_k8s.NewMockRateLimitingWorkqueue[string](ctrl)
	c := &gitRepositoryController{
		log:                 testlogger.New(t),
		gitRepositoryLister: mockGitRepositoryLister,
		projectReconciler:   mockProjectReconciler,
		workqueue:           mockWorkqueue,
		gitLabExternalURL:   url.URL{Scheme: "https", Host: "gitlab.example.com:8080"},
	}

	// setup mock expectations
	mockGitRepositoryLister.EXPECT().ByNamespace("namespace").Return(mockNamespaceLister)
	mockNamespaceLister.EXPECT().Get("name").Return(nil, kubeerrors.NewNotFound(schema.GroupResource{}, "test"))

	mockProjectReconciler.EXPECT().ReconcileIndexedProjects(gomock.Any())

	// WHEN
	c.handleReceiverObj(context.Background(), getTestReceiverAsInterface())
}

func TestGitRepositoryController_ObjectWithPrefix(t *testing.T) {
	testcases := []struct {
		name            string
		n               int
		expectedGenName string
	}{
		{
			name:            "foobar",
			n:               20,
			expectedGenName: "gitlab-foobar",
		},
		{
			name:            "foobar",
			n:               len(objectNamePrefix) + 3,
			expectedGenName: "gitlab-foo",
		},
		{
			name:            "foo+bar",
			n:               len(objectNamePrefix) + 4,
			expectedGenName: "gitlab-fo-x",
		},
	}

	for i, tc := range testcases {
		t.Run(fmt.Sprintf("%d - %s", i, tc.name), func(t *testing.T) {
			// WHEN
			actualGenName := objectWithPrefix(tc.name, tc.n)

			// THEN
			require.Equal(t, tc.expectedGenName, actualGenName)
		})
	}
}

func getTestGitRepositoryAsRuntimeObject(t *testing.T) runtime.Object {
	gitRepository := &sourcev1.GitRepository{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "namespace",
			Name:      "name",
		},
		Spec: sourcev1.GitRepositorySpec{
			URL: "https://gitlab.example.com/some-org/some-repo.git",
		},
	}
	o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(gitRepository)
	assert.NoError(t, err)
	u := &unstructured.Unstructured{Object: o}
	return u
}

func getTestReceiver() *notificationv1.Receiver {
	isController := true
	return &notificationv1.Receiver{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "namespace",
			Name:      "gitlab-name",
			OwnerReferences: []metav1.OwnerReference{{
				APIVersion: sourcev1.GroupVersion.String(),
				Kind:       sourcev1.GitRepositoryKind,
				Name:       "name",
				Controller: &isController,
			}},
		},
	}
}

func getTestReceiverAsInterface() any {
	var o metav1.Object = getTestReceiver()
	return o
}
