/*
Copyright © The ESO Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://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 gitlab

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"reflect"
	"strings"
	"testing"

	"github.com/google/uuid"
	tassert "github.com/stretchr/testify/assert"
	"github.com/yandex-cloud/go-sdk/iamkey"
	gitlab "gitlab.com/gitlab-org/api/client-go"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"

	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
	esv1meta "github.com/external-secrets/external-secrets/apis/meta/v1"
	fakegitlab "github.com/external-secrets/external-secrets/providers/v1/gitlab/fake"
)

const (
	project               = "my-Project"
	username              = "user-name"
	userkey               = "user-key"
	environment           = "prod"
	environmentTest       = "test"
	projectvalue          = "projectvalue"
	groupvalue            = "groupvalue"
	groupid               = "groupId"
	defaultErrorMessage   = "[%d] unexpected error: [%s], expected: [%s]"
	errMissingCredentials = "cannot get Kubernetes secret \"\" from namespace \"namespace\": secrets \"\" not found"
	testKey               = "testKey"
	findTestPrefix        = "test.*"
)

type secretManagerTestCase struct {
	mockProjectsClient       *fakegitlab.GitlabMockProjectsClient
	mockProjectVarClient     *fakegitlab.GitlabMockProjectVariablesClient
	mockGroupVarClient       *fakegitlab.GitlabMockGroupVariablesClient
	apiInputProjectID        string
	apiInputKey              string
	apiInputEnv              string
	projectAPIOutput         *gitlab.ProjectVariable
	projectAPIResponse       *gitlab.Response
	projectAPIOutputs        []*fakegitlab.APIResponse[[]*gitlab.ProjectVariable]
	projectGroupsAPIOutput   []*gitlab.ProjectGroup
	projectGroupsAPIResponse *gitlab.Response
	groupAPIOutputs          []*fakegitlab.APIResponse[[]*gitlab.GroupVariable]
	groupAPIOutput           *gitlab.GroupVariable
	groupAPIResponse         *gitlab.Response
	ref                      *esv1.ExternalSecretDataRemoteRef
	refFind                  *esv1.ExternalSecretFind
	projectID                string
	groupIDs                 []string
	inheritFromGroups        bool
	apiErr                   error
	expectError              string
	expectedSecret           string
	expectedValidationResult esv1.ValidationResult
	// for testing secretmap
	expectedData map[string][]byte
}

func makeValidSecretManagerTestCase() *secretManagerTestCase {
	smtc := secretManagerTestCase{
		mockProjectsClient:   &fakegitlab.GitlabMockProjectsClient{},
		mockProjectVarClient: &fakegitlab.GitlabMockProjectVariablesClient{},
		mockGroupVarClient:   &fakegitlab.GitlabMockGroupVariablesClient{},
		apiInputProjectID:    makeValidAPIInputProjectID(),
		apiInputKey:          makeValidAPIInputKey(),
		//apiInputEnv:              makeValidEnvironment(),
		ref:                      makeValidRef(),
		refFind:                  makeValidFindRef(),
		projectID:                makeValidProjectID(),
		groupIDs:                 makeEmptyGroupIds(),
		projectAPIOutput:         makeValidProjectAPIOutput(),
		projectAPIResponse:       makeValidProjectAPIResponse(),
		projectGroupsAPIOutput:   makeValidProjectGroupsAPIOutput(),
		projectGroupsAPIResponse: makeValidProjectGroupsAPIResponse(),
		groupAPIOutput:           makeValidGroupAPIOutput(),
		groupAPIResponse:         makeValidGroupAPIResponse(),
		apiErr:                   nil,
		expectError:              "",
		expectedSecret:           "",
		expectedValidationResult: esv1.ValidationResultReady,
		expectedData:             map[string][]byte{},
	}
	prepareMockProjectVarClient(&smtc)
	prepareMockGroupVarClient(&smtc)
	return &smtc
}

func makeValidRef() *esv1.ExternalSecretDataRemoteRef {
	return &esv1.ExternalSecretDataRemoteRef{
		Key:     testKey,
		Version: "default",
	}
}

func makeValidFindRef() *esv1.ExternalSecretFind {
	return &esv1.ExternalSecretFind{}
}

func makeValidProjectID() string {
	return "projectId"
}

func makeEmptyGroupIds() []string {
	return []string{}
}

func makeFindName(regexp string) *esv1.FindName {
	return &esv1.FindName{
		RegExp: regexp,
	}
}

func makeValidAPIInputProjectID() string {
	return "testID"
}

func makeValidAPIInputKey() string {
	return testKey
}

func makeValidProjectAPIResponse() *gitlab.Response {
	return &gitlab.Response{
		Response: &http.Response{
			StatusCode: http.StatusOK,
		},
		CurrentPage: 1,
		TotalPages:  1,
	}
}
func make404GitlabAPIResponse() *gitlab.Response {
	return &gitlab.Response{
		Response: &http.Response{
			StatusCode: http.StatusNotFound,
		},
		CurrentPage: 1,
		TotalPages:  1,
	}
}

func makeValidProjectGroupsAPIResponse() *gitlab.Response {
	return &gitlab.Response{
		Response: &http.Response{
			StatusCode: http.StatusOK,
		},
		CurrentPage: 1,
		TotalPages:  1,
	}
}

func makeValidGroupAPIResponse() *gitlab.Response {
	return &gitlab.Response{
		Response: &http.Response{
			StatusCode: http.StatusOK,
		},
		CurrentPage: 1,
		TotalPages:  1,
	}
}

func makeValidProjectAPIOutput() *gitlab.ProjectVariable {
	return &gitlab.ProjectVariable{
		Key:              testKey,
		Value:            "",
		EnvironmentScope: environment,
	}
}

func makeValidProjectGroupsAPIOutput() []*gitlab.ProjectGroup {
	return []*gitlab.ProjectGroup{{
		ID:       1,
		Name:     "Group (1)",
		FullPath: "foo",
	}, {
		ID:       100,
		Name:     "Group (100)",
		FullPath: "foo/bar/baz",
	}, {
		ID:       10,
		Name:     "Group (10)",
		FullPath: "foo/bar",
	}}
}

func make404ProjectAPIOutput(numResponses ...int) []*fakegitlab.APIResponse[[]*gitlab.ProjectVariable] {
	count := 1
	if len(numResponses) > 0 && numResponses[0] > 0 {
		count = numResponses[0]
	}
	responses := make([]*fakegitlab.APIResponse[[]*gitlab.ProjectVariable], count)
	for i := 0; i < count; i++ {
		responses[i] = &fakegitlab.APIResponse[[]*gitlab.ProjectVariable]{
			Response: make404GitlabAPIResponse(),
			Error:    gitlab.ErrNotFound,
		}
	}
	return responses
}

func makeValidGroupAPIOutput() *gitlab.GroupVariable {
	return &gitlab.GroupVariable{
		Key:              "groupKey",
		Value:            "",
		EnvironmentScope: environment,
	}
}

func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTestCase)) *secretManagerTestCase {
	smtc := makeValidSecretManagerTestCase()
	for _, fn := range tweaks {
		fn(smtc)
	}
	smtc.mockProjectsClient.WithValue(smtc.projectGroupsAPIOutput, smtc.projectGroupsAPIResponse, smtc.apiErr)
	prepareMockProjectVarClient(smtc)
	prepareMockGroupVarClient(smtc)
	return smtc
}

func makeValidSecretManagerGetAllTestCaseCustom(tweaks ...func(smtc *secretManagerTestCase)) *secretManagerTestCase {
	smtc := makeValidSecretManagerTestCase()
	smtc.ref = nil
	smtc.refFind.Name = makeFindName(".*")
	for _, fn := range tweaks {
		fn(smtc)
	}
	prepareMockProjectVarClient(smtc)
	prepareMockGroupVarClient(smtc)
	return smtc
}

func prepareMockProjectVarClient(smtc *secretManagerTestCase) {
	responses := make([]fakegitlab.APIResponse[[]*gitlab.ProjectVariable], 0)
	if smtc.projectAPIOutput != nil {
		responses = append(
			responses,
			fakegitlab.APIResponse[[]*gitlab.ProjectVariable]{Output: []*gitlab.ProjectVariable{smtc.projectAPIOutput}, Response: smtc.projectAPIResponse, Error: smtc.apiErr},
		)
	}
	for _, response := range smtc.projectAPIOutputs {
		responses = append(responses, *response)
	}
	smtc.mockProjectVarClient.WithValues(responses)
}

func prepareMockGroupVarClient(smtc *secretManagerTestCase) {
	responses := make([]fakegitlab.APIResponse[[]*gitlab.GroupVariable], 0)
	if smtc.groupAPIOutput != nil {
		responses = append(responses, fakegitlab.APIResponse[[]*gitlab.GroupVariable]{Output: []*gitlab.GroupVariable{smtc.groupAPIOutput}, Response: smtc.groupAPIResponse, Error: smtc.apiErr})
	}
	for _, response := range smtc.groupAPIOutputs {
		responses = append(responses, *response)
	}
	smtc.mockGroupVarClient.WithValues(responses)
}

// This case can be shared by both GetSecret and GetSecretMap tests.
// bad case: set apiErr.
var setAPIErr = func(smtc *secretManagerTestCase) {
	smtc.apiErr = errors.New("oh no")
	smtc.expectError = "oh no"
	smtc.projectAPIResponse.Response.StatusCode = http.StatusInternalServerError
	smtc.expectedValidationResult = esv1.ValidationResultError
}

var setListAPIErr = func(smtc *secretManagerTestCase) {
	err := errors.New("oh no")
	smtc.apiErr = err
	smtc.expectError = fmt.Errorf(errList, err).Error()
	smtc.expectedValidationResult = esv1.ValidationResultError
}

var setProjectListAPIRespNil = func(smtc *secretManagerTestCase) {
	smtc.projectAPIResponse = nil
	smtc.expectError = fmt.Errorf(errProjectAuth, smtc.projectID).Error()
	smtc.expectedValidationResult = esv1.ValidationResultError
}

var setGroupListAPIRespNil = func(smtc *secretManagerTestCase) {
	smtc.groupIDs = []string{groupid}
	smtc.groupAPIResponse = nil
	smtc.expectError = fmt.Errorf(errGroupAuth, groupid).Error()
	smtc.expectedValidationResult = esv1.ValidationResultError
}

var setProjectAndGroup = func(smtc *secretManagerTestCase) {
	smtc.groupIDs = []string{groupid}
}

var setProjectAndInheritFromGroups = func(smtc *secretManagerTestCase) {
	smtc.groupIDs = nil
	smtc.inheritFromGroups = true
}

var setProjectListAPIRespBadCode = func(smtc *secretManagerTestCase) {
	smtc.projectAPIResponse.StatusCode = http.StatusUnauthorized
	smtc.expectError = fmt.Errorf(errProjectAuth, smtc.projectID).Error()
	smtc.expectedValidationResult = esv1.ValidationResultError
}

var setGroupListAPIRespBadCode = func(smtc *secretManagerTestCase) {
	smtc.groupIDs = []string{groupid}
	smtc.groupAPIResponse.StatusCode = http.StatusUnauthorized
	smtc.expectError = fmt.Errorf(errGroupAuth, groupid).Error()
	smtc.expectedValidationResult = esv1.ValidationResultError
}

var setNilMockClient = func(smtc *secretManagerTestCase) {
	smtc.mockProjectVarClient = nil
	smtc.mockGroupVarClient = nil
	smtc.expectError = errUninitializedGitlabProvider
}

func TestNewClient(t *testing.T) {
	ctx := context.Background()
	const namespace = "namespace"

	store := &esv1.SecretStore{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
		},
		Spec: esv1.SecretStoreSpec{
			Provider: &esv1.SecretStoreProvider{
				Gitlab: &esv1.GitlabProvider{},
			},
		},
	}
	esv1.Register(NewProvider(), ProviderSpec(), MaintenanceStatus())
	provider, err := esv1.GetProvider(store)
	tassert.Nil(t, err)

	k8sClient := clientfake.NewClientBuilder().Build()
	secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace)
	tassert.EqualError(t, err, errMissingCredentials)
	tassert.Nil(t, secretClient)

	store.Spec.Provider.Gitlab.Auth = esv1.GitlabAuth{}
	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
	tassert.EqualError(t, err, errMissingCredentials)
	tassert.Nil(t, secretClient)

	store.Spec.Provider.Gitlab.Auth.SecretRef = esv1.GitlabSecretRef{}
	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
	tassert.EqualError(t, err, errMissingCredentials)
	tassert.Nil(t, secretClient)

	store.Spec.Provider.Gitlab.Auth.SecretRef.AccessToken = esv1meta.SecretKeySelector{}
	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
	tassert.EqualError(t, err, errMissingCredentials)
	tassert.Nil(t, secretClient)

	const authorizedKeySecretName = "authorizedKeySecretName"
	const authorizedKeySecretKey = "authorizedKeySecretKey"
	store.Spec.Provider.Gitlab.Auth.SecretRef.AccessToken.Name = authorizedKeySecretName
	store.Spec.Provider.Gitlab.Auth.SecretRef.AccessToken.Key = authorizedKeySecretKey
	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
	tassert.EqualError(t, err, "cannot get Kubernetes secret \"authorizedKeySecretName\" from namespace \"namespace\": secrets \"authorizedKeySecretName\" not found")
	tassert.Nil(t, secretClient)

	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, newFakeAuthorizedKey()))
	tassert.Nil(t, err)

	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
	tassert.Nil(t, err)
	tassert.NotNil(t, secretClient)
}

func toJSON(t *testing.T, v any) []byte {
	jsonBytes, err := json.Marshal(v)
	tassert.Nil(t, err)
	return jsonBytes
}

func createK8sSecret(ctx context.Context, t *testing.T, k8sClient k8sclient.Client, namespace, secretName, secretKey string, secretValue []byte) error {
	err := k8sClient.Create(ctx, &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      secretName,
		},
		Data: map[string][]byte{secretKey: secretValue},
	})
	tassert.Nil(t, err)
	return nil
}

func newFakeAuthorizedKey() *iamkey.Key {
	uniqueLabel := uuid.NewString()
	return &iamkey.Key{
		Id: uniqueLabel,
		Subject: &iamkey.Key_ServiceAccountId{
			ServiceAccountId: uniqueLabel,
		},
		PrivateKey: uniqueLabel,
	}
}

// test the sm<->gcp interface
// make sure correct values are passed and errors are handled accordingly.
func TestGetSecret(t *testing.T) {
	// good case: default version is set
	// key is passed in, output is sent back
	onlyProjectSecret := func(smtc *secretManagerTestCase) {
		smtc.projectAPIOutput.Value = projectvalue
		smtc.groupAPIResponse = nil
		smtc.groupAPIOutput = nil
		smtc.expectedSecret = smtc.projectAPIOutput.Value
	}

	groupSecretProjectOverride := func(smtc *secretManagerTestCase) {
		// given a secre t in the project and a group with the same key
		// the project secret should override the group secret
		smtc.projectAPIOutput.Value = projectvalue
		smtc.groupAPIOutput.Key = testKey
		smtc.groupAPIOutput.Value = groupvalue
		smtc.expectedSecret = smtc.projectAPIOutput.Value
	}

	groupWithoutProjectOverride := func(smtc *secretManagerTestCase) {
		// given a group secret without a project secret
		// the group secret should be returned
		smtc.groupIDs = []string{groupid}
		smtc.projectAPIOutput = nil
		smtc.projectAPIOutputs = make404ProjectAPIOutput()
		smtc.groupAPIOutput.Key = testKey
		smtc.groupAPIOutput.Value = groupvalue
		smtc.expectedSecret = smtc.groupAPIOutput.Value
	}

	successCases := []*secretManagerTestCase{
		makeValidSecretManagerTestCaseCustom(onlyProjectSecret),
		makeValidSecretManagerTestCaseCustom(groupSecretProjectOverride),
		makeValidSecretManagerTestCaseCustom(groupWithoutProjectOverride),
		makeValidSecretManagerTestCaseCustom(setGroupWildcardVariable),
		makeValidSecretManagerTestCaseCustom(setGroupWildcardVariableWithEnvironment),
		makeValidSecretManagerTestCaseCustom(setGroupWildcardVariableNotFoundThenFound),
		makeValidSecretManagerTestCaseCustom(setAPIErr),
		makeValidSecretManagerTestCaseCustom(setNilMockClient),
	}

	sm := gitlabBase{}
	sm.store = &esv1.GitlabProvider{}
	for k, v := range successCases {
		sm.projectVariablesClient = v.mockProjectVarClient
		sm.groupVariablesClient = v.mockGroupVarClient
		sm.store.ProjectID = v.projectID
		sm.store.GroupIDs = v.groupIDs
		sm.store.Environment = v.apiInputEnv
		out, err := sm.GetSecret(context.Background(), *v.ref)
		if !ErrorContains(err, v.expectError) {
			t.Errorf(defaultErrorMessage, k, err.Error(), v.expectError)
		}
		if string(out) != v.expectedSecret {
			t.Errorf("[%d] unexpected secret: [%s], expected [%s]", k, string(out), v.expectedSecret)
		}
	}
}

// TestResolveGroupIDs tests the resolving of group IDs for a GitLab store.
func TestResolveGroupIDs(t *testing.T) {
	v := makeValidSecretManagerTestCaseCustom()
	sm := gitlabBase{}
	sm.store = &esv1.GitlabProvider{}
	sm.projectsClient = v.mockProjectsClient
	sm.store.ProjectID = v.projectID
	sm.store.InheritFromGroups = true

	err := sm.ResolveGroupIDs()
	if err != nil {
		t.Errorf(defaultErrorMessage, 0, err.Error(), "")
	}
	if !reflect.DeepEqual(sm.store.GroupIDs, []string{"1", "10", "100"}) {
		t.Errorf("unexpected groupIDs: %s, expected %s", sm.store.GroupIDs, []string{"1", "10", "100"})
	}
}

func TestGetAllSecrets(t *testing.T) {
	// good case: default version is set
	// key is passed in, output is sent back

	setMissingFindRegex := func(smtc *secretManagerTestCase) {
		smtc.refFind.Name = nil
		smtc.expectError = "'find.name' is mandatory"
	}
	setUnsupportedFindPath := func(smtc *secretManagerTestCase) {
		path := "path"
		smtc.refFind.Path = &path
		smtc.expectError = "'find.path' is not implemented in the GitLab provider"
	}
	setUnsupportedFindTag := func(smtc *secretManagerTestCase) {
		smtc.expectError = "'find.tags' only supports 'environment_scope"
		smtc.refFind.Tags = map[string]string{"foo": ""}
	}
	setMatchingSecretFindString := func(smtc *secretManagerTestCase) {
		smtc.projectAPIOutput = &gitlab.ProjectVariable{
			Key:              testKey,
			Value:            projectvalue,
			EnvironmentScope: environment,
		}
		smtc.expectedSecret = projectvalue
		smtc.refFind.Name = makeFindName(findTestPrefix)
	}
	setNoMatchingRegexpFindString := func(smtc *secretManagerTestCase) {
		smtc.projectAPIOutput = &gitlab.ProjectVariable{
			Key:              testKey,
			Value:            projectvalue,
			EnvironmentScope: environmentTest,
		}
		smtc.expectedSecret = ""
		smtc.refFind.Name = makeFindName("foo.*")
	}
	setUnmatchedEnvironmentFindString := func(smtc *secretManagerTestCase) {
		smtc.apiInputEnv = environment
		smtc.projectAPIOutput = &gitlab.ProjectVariable{
			Key:              testKey,
			Value:            projectvalue,
			EnvironmentScope: environmentTest,
		}
		smtc.expectedSecret = ""
		smtc.refFind.Name = makeFindName(findTestPrefix)
	}
	setMatchingSecretFindTags := func(smtc *secretManagerTestCase) {
		smtc.projectAPIOutput = &gitlab.ProjectVariable{
			Key:              testKey,
			Value:            projectvalue,
			EnvironmentScope: environment,
		}
		smtc.apiInputEnv = "*"
		smtc.expectedSecret = projectvalue
		smtc.refFind.Tags = map[string]string{"environment_scope": environment}
	}
	setEnvironmentConstrainedByStore := func(smtc *secretManagerTestCase) {
		smtc.apiInputEnv = environment
		smtc.expectedSecret = projectvalue
		smtc.expectError = "'find.tags' is constrained by 'environment_scope' of the store"
		smtc.refFind.Tags = map[string]string{"environment_scope": environment}
	}
	setWildcardDoesntOverwriteEnvironmentValue := func(smtc *secretManagerTestCase) {
		var1 := gitlab.ProjectVariable{
			Key:              testKey,
			Value:            "wildcardValue",
			EnvironmentScope: "*",
		}
		var2 := gitlab.ProjectVariable{
			Key:              testKey,
			Value:            "expectedValue",
			EnvironmentScope: environmentTest,
		}
		var3 := gitlab.ProjectVariable{
			Key:              testKey,
			Value:            "wildcardValue",
			EnvironmentScope: "*",
		}
		vars := []*gitlab.ProjectVariable{&var1, &var2, &var3}
		smtc.projectAPIOutputs = []*fakegitlab.APIResponse[[]*gitlab.ProjectVariable]{{Output: vars, Response: smtc.projectAPIResponse, Error: nil}}
		smtc.projectAPIOutput = nil
		smtc.apiInputEnv = environmentTest
		smtc.expectedSecret = "expectedValue"
		smtc.refFind.Name = makeFindName(findTestPrefix)
	}
	setFilterByEnvironmentWithWildcard := func(smtc *secretManagerTestCase) {
		var1 := gitlab.ProjectVariable{
			Key:              testKey,
			Value:            projectvalue,
			EnvironmentScope: "*",
		}
		var2 := gitlab.ProjectVariable{
			Key:              "testKey2",
			Value:            "value2",
			EnvironmentScope: environment,
		}
		var3 := gitlab.ProjectVariable{
			Key:              "testKey3",
			Value:            "value3",
			EnvironmentScope: environmentTest,
		}
		var4 := gitlab.ProjectVariable{
			Key:              "anotherKey4",
			Value:            "value4",
			EnvironmentScope: environment,
		}
		vars := []*gitlab.ProjectVariable{&var1, &var2, &var3, &var4}
		smtc.projectAPIOutput = nil
		smtc.projectAPIOutputs = []*fakegitlab.APIResponse[[]*gitlab.ProjectVariable]{{Output: vars, Response: smtc.projectAPIResponse, Error: nil}}
		smtc.apiInputEnv = environment
		smtc.expectedData = map[string][]byte{testKey: []byte(projectvalue), "testKey2": []byte("value2")}
		smtc.refFind.Name = makeFindName(findTestPrefix)
	}
	setPaginationInGroupAndProjectVars := func(smtc *secretManagerTestCase) {
		smtc.groupIDs = []string{groupid}
		gvar1 := gitlab.GroupVariable{
			Key:              testKey + "Group",
			Value:            "groupValue1",
			EnvironmentScope: environmentTest,
		}
		gvar2 := gitlab.GroupVariable{
			Key:              testKey,
			Value:            "groupValue2",
			EnvironmentScope: environmentTest,
		}
		pvar1 := gitlab.ProjectVariable{
			Key:              testKey,
			Value:            "testValue1",
			EnvironmentScope: environmentTest,
		}
		pvar2a := gitlab.ProjectVariable{
			Key:              testKey + "2a",
			Value:            "testValue2a",
			EnvironmentScope: environmentTest,
		}
		pvar2b := gitlab.ProjectVariable{
			Key:              testKey + "2b",
			Value:            "testValue2b",
			EnvironmentScope: environmentTest,
		}
		gPage1 := []*gitlab.GroupVariable{&gvar1}
		gResponsePage1 := makeValidGroupAPIResponse()
		gResponsePage1.TotalPages = 2
		gResponsePage1.CurrentPage = 1
		gPage2 := []*gitlab.GroupVariable{&gvar2}
		gResponsePage2 := makeValidGroupAPIResponse()
		gResponsePage2.TotalPages = 2
		gResponsePage2.CurrentPage = 1
		pPage1 := []*gitlab.ProjectVariable{&pvar1}
		pResponsePage1 := makeValidProjectAPIResponse()
		pResponsePage1.TotalPages = 2
		pResponsePage1.CurrentPage = 1
		pPage2 := []*gitlab.ProjectVariable{&pvar2a, &pvar2b}
		pResponsePage2 := makeValidProjectAPIResponse()
		pResponsePage2.TotalPages = 2
		pResponsePage2.CurrentPage = 2
		smtc.groupAPIOutputs = []*fakegitlab.APIResponse[[]*gitlab.GroupVariable]{{Output: gPage1, Response: gResponsePage1, Error: nil}, {Output: gPage2, Response: gResponsePage2, Error: nil}}
		smtc.groupAPIOutput = nil
		smtc.projectAPIOutputs = []*fakegitlab.APIResponse[[]*gitlab.ProjectVariable]{{Output: pPage1, Response: pResponsePage1, Error: nil}, {Output: pPage2, Response: pResponsePage2, Error: nil}}
		smtc.projectAPIOutput = nil
		smtc.apiInputEnv = environmentTest
		smtc.expectedData = map[string][]byte{testKey: []byte("testValue1"), "testKey2a": []byte("testValue2a"), "testKey2b": []byte("testValue2b"), "testKeyGroup": []byte("groupValue1")}
		smtc.refFind.Name = makeFindName(findTestPrefix)
	}
	setGroupWildcardVariableInGetAllSecrets := func(smtc *secretManagerTestCase) {
		smtc.groupIDs = []string{groupid}
		smtc.projectAPIOutput = &gitlab.ProjectVariable{
			Key:              testKey,
			Value:            projectvalue,
			EnvironmentScope: environment,
		}
		smtc.groupAPIOutput = &gitlab.GroupVariable{
			Key:              testKey,
			Value:            groupvalue,
			EnvironmentScope: "*",
		}
		// Project variable should override group wildcard variable
		smtc.expectedData = map[string][]byte{testKey: []byte(projectvalue)}
		smtc.refFind.Name = makeFindName(findTestPrefix)
	}
	setGroupWildcardVariableOnlyInGetAllSecrets := func(smtc *secretManagerTestCase) {
		smtc.groupIDs = []string{groupid}
		smtc.projectAPIOutput = &gitlab.ProjectVariable{
			Key:              testKey,
			Value:            projectvalue,
			EnvironmentScope: environmentTest, // Different environment
		}
		smtc.groupAPIOutput = &gitlab.GroupVariable{
			Key:              testKey,
			Value:            groupvalue,
			EnvironmentScope: "*",
		}
		// Group wildcard variable should be used when project variable doesn't match environment
		smtc.apiInputEnv = environment
		smtc.expectedData = map[string][]byte{testKey: []byte(groupvalue)}
		smtc.refFind.Name = makeFindName(findTestPrefix)
	}
	setGroupWildcardVariableWithCollision := func(smtc *secretManagerTestCase) {
		smtc.groupIDs = []string{groupid}
		smtc.projectAPIOutput = &gitlab.ProjectVariable{
			Key:              testKey,
			Value:            projectvalue,
			EnvironmentScope: "*",
		}
		smtc.groupAPIOutput = &gitlab.GroupVariable{
			Key:              testKey,
			Value:            groupvalue,
			EnvironmentScope: "*",
		}
		// Project wildcard variable should override group wildcard variable
		smtc.expectedData = map[string][]byte{testKey: []byte(projectvalue)}
		smtc.refFind.Name = makeFindName(findTestPrefix)
	}

	cases := []*secretManagerTestCase{
		makeValidSecretManagerGetAllTestCaseCustom(setUnmatchedEnvironmentFindString),
		///
		makeValidSecretManagerGetAllTestCaseCustom(setMissingFindRegex),
		makeValidSecretManagerGetAllTestCaseCustom(setUnsupportedFindPath),
		makeValidSecretManagerGetAllTestCaseCustom(setUnsupportedFindTag),
		makeValidSecretManagerGetAllTestCaseCustom(setMatchingSecretFindString),
		makeValidSecretManagerGetAllTestCaseCustom(setNoMatchingRegexpFindString),
		makeValidSecretManagerGetAllTestCaseCustom(setUnmatchedEnvironmentFindString),
		makeValidSecretManagerGetAllTestCaseCustom(setMatchingSecretFindTags),
		makeValidSecretManagerGetAllTestCaseCustom(setWildcardDoesntOverwriteEnvironmentValue),
		makeValidSecretManagerGetAllTestCaseCustom(setEnvironmentConstrainedByStore),
		makeValidSecretManagerGetAllTestCaseCustom(setFilterByEnvironmentWithWildcard),
		makeValidSecretManagerGetAllTestCaseCustom(setPaginationInGroupAndProjectVars),
		makeValidSecretManagerGetAllTestCaseCustom(setGroupWildcardVariableInGetAllSecrets),
		makeValidSecretManagerGetAllTestCaseCustom(setGroupWildcardVariableOnlyInGetAllSecrets),
		makeValidSecretManagerGetAllTestCaseCustom(setGroupWildcardVariableWithCollision),
		makeValidSecretManagerGetAllTestCaseCustom(setAPIErr),
		makeValidSecretManagerGetAllTestCaseCustom(setNilMockClient),
	}

	sm := gitlabBase{}
	sm.store = &esv1.GitlabProvider{}
	for k, v := range cases {
		sm.projectVariablesClient = v.mockProjectVarClient
		sm.groupVariablesClient = v.mockGroupVarClient
		sm.store.Environment = v.apiInputEnv
		sm.store.GroupIDs = v.groupIDs
		if v.expectedSecret != "" {
			v.expectedData = map[string][]byte{testKey: []byte(v.expectedSecret)}
		}
		out, err := sm.GetAllSecrets(context.Background(), *v.refFind)
		if !ErrorContains(err, v.expectError) {
			t.Errorf(defaultErrorMessage, k, err.Error(), v.expectError)
		}
		if err == nil && !reflect.DeepEqual(out, v.expectedData) {
			t.Errorf("[%d] unexpected secret data: [%#v], expected [%#v]", k, out, v.expectedData)
		}
	}
}

func TestGetAllSecretsWithGroups(t *testing.T) {
	onlyProjectSecret := func(smtc *secretManagerTestCase) {
		smtc.projectAPIOutput.Value = projectvalue
		smtc.refFind.Name = makeFindName(findTestPrefix)
		smtc.groupAPIResponse = nil
		smtc.groupAPIOutput = nil
		smtc.expectedSecret = smtc.projectAPIOutput.Value
	}
	groupAndProjectSecrets := func(smtc *secretManagerTestCase) {
		smtc.groupIDs = []string{groupid}
		smtc.projectAPIOutput.Value = projectvalue
		smtc.groupAPIOutput.Value = groupvalue
		smtc.expectedData = map[string][]byte{testKey: []byte(projectvalue), "groupKey": []byte(groupvalue)}
		smtc.refFind.Name = makeFindName(".*Key")
	}
	groupAndOverrideProjectSecrets := func(smtc *secretManagerTestCase) {
		smtc.groupIDs = []string{groupid}
		smtc.projectAPIOutput.Value = projectvalue
		smtc.groupAPIOutput.Key = smtc.projectAPIOutput.Key
		smtc.groupAPIOutput.Value = groupvalue
		smtc.expectedData = map[string][]byte{testKey: []byte(projectvalue)}
		smtc.refFind.Name = makeFindName(".*Key")
	}
	groupAndProjectWithDifferentEnvSecrets := func(smtc *secretManagerTestCase) {
		smtc.groupIDs = []string{groupid}
		smtc.projectAPIOutput.Value = projectvalue
		smtc.projectAPIOutput.EnvironmentScope = environmentTest
		smtc.groupAPIOutput.Key = smtc.projectAPIOutput.Key
		smtc.groupAPIOutput.Value = groupvalue
		smtc.expectedData = map[string][]byte{testKey: []byte(groupvalue)}
		smtc.refFind.Name = makeFindName(".*Key")
	}

	cases := []*secretManagerTestCase{
		makeValidSecretManagerGetAllTestCaseCustom(onlyProjectSecret),
		makeValidSecretManagerGetAllTestCaseCustom(groupAndProjectSecrets),
		makeValidSecretManagerGetAllTestCaseCustom(groupAndOverrideProjectSecrets),
		makeValidSecretManagerGetAllTestCaseCustom(groupAndProjectWithDifferentEnvSecrets),
	}

	sm := gitlabBase{}
	sm.store = &esv1.GitlabProvider{}
	sm.store.Environment = environment
	for k, v := range cases {
		sm.projectVariablesClient = v.mockProjectVarClient
		sm.groupVariablesClient = v.mockGroupVarClient
		sm.store.ProjectID = v.projectID
		sm.store.GroupIDs = v.groupIDs
		out, err := sm.GetAllSecrets(context.Background(), *v.refFind)
		if !ErrorContains(err, v.expectError) {
			t.Errorf(defaultErrorMessage, k, err.Error(), v.expectError)
		}
		if v.expectError == "" {
			if len(v.expectedData) > 0 {
				if !reflect.DeepEqual(v.expectedData, out) {
					t.Errorf("[%d] unexpected secrets: [%s], expected [%s]", k, out, v.expectedData)
				}
			} else if string(out[v.projectAPIOutput.Key]) != v.expectedSecret {
				t.Errorf("[%d] unexpected secret: [%s], expected [%s]", k, string(out[v.projectAPIOutput.Key]), v.expectedSecret)
			}
		}
	}
}

func TestValidate(t *testing.T) {
	successCases := []*secretManagerTestCase{
		makeValidSecretManagerTestCaseCustom(),
		makeValidSecretManagerTestCaseCustom(setProjectAndInheritFromGroups),
		makeValidSecretManagerTestCaseCustom(setProjectAndGroup),
		makeValidSecretManagerTestCaseCustom(setListAPIErr),
		makeValidSecretManagerTestCaseCustom(setProjectListAPIRespNil),
		makeValidSecretManagerTestCaseCustom(setProjectListAPIRespBadCode),
		makeValidSecretManagerTestCaseCustom(setGroupListAPIRespNil),
		makeValidSecretManagerTestCaseCustom(setGroupListAPIRespBadCode),
	}
	sm := gitlabBase{}
	sm.store = &esv1.GitlabProvider{}
	for k, v := range successCases {
		sm.projectsClient = v.mockProjectsClient
		sm.projectVariablesClient = v.mockProjectVarClient
		sm.groupVariablesClient = v.mockGroupVarClient
		sm.store.ProjectID = v.projectID
		sm.store.GroupIDs = v.groupIDs
		sm.store.InheritFromGroups = v.inheritFromGroups
		t.Logf("%+v", v)
		validationResult, err := sm.Validate()
		if !ErrorContains(err, v.expectError) {
			t.Errorf(defaultErrorMessage, k, err.Error(), v.expectError)
		}
		if validationResult != v.expectedValidationResult {
			t.Errorf("[%d], unexpected validationResult: [%s], expected: [%s]", k, validationResult, v.expectedValidationResult)
		}
		if sm.store.InheritFromGroups && sm.store.GroupIDs[0] != "1" {
			t.Errorf("[%d], unexpected groupID: [%s], expected [1]", k, sm.store.GroupIDs[0])
		}
	}
}

func TestGetSecretMap(t *testing.T) {
	// good case: default version & deserialization
	setDeserialization := func(smtc *secretManagerTestCase) {
		smtc.projectAPIOutput.Value = `{"foo":"bar"}`
		smtc.expectedData["foo"] = []byte("bar")
	}

	// bad case: invalid json
	setInvalidJSON := func(smtc *secretManagerTestCase) {
		smtc.projectAPIOutput.Value = `-----------------`
		smtc.expectError = "unable to unmarshal secret"
	}

	successCases := []*secretManagerTestCase{
		makeValidSecretManagerTestCaseCustom(setDeserialization),
		makeValidSecretManagerTestCaseCustom(setInvalidJSON),
		makeValidSecretManagerTestCaseCustom(setNilMockClient),
		makeValidSecretManagerTestCaseCustom(setAPIErr),
	}

	sm := gitlabBase{}
	sm.store = &esv1.GitlabProvider{}
	for k, v := range successCases {
		sm.projectVariablesClient = v.mockProjectVarClient
		sm.groupVariablesClient = v.mockGroupVarClient
		out, err := sm.GetSecretMap(context.Background(), *v.ref)
		if !ErrorContains(err, v.expectError) {
			t.Errorf(defaultErrorMessage, k, err.Error(), v.expectError)
		}
		if err == nil && !reflect.DeepEqual(out, v.expectedData) {
			t.Errorf("[%d] unexpected secret data: [%#v], expected [%#v]", k, out, v.expectedData)
		}
	}
}

func makeSecretStore(projectID, environment string, fn ...storeModifier) *esv1.SecretStore {
	store := &esv1.SecretStore{
		Spec: esv1.SecretStoreSpec{
			Provider: &esv1.SecretStoreProvider{
				Gitlab: &esv1.GitlabProvider{
					Auth:        esv1.GitlabAuth{},
					ProjectID:   projectID,
					Environment: environment,
				},
			},
		},
	}
	for _, f := range fn {
		store = f(store)
	}
	return store
}

func withAccessToken(name, key string, namespace *string) storeModifier {
	return func(store *esv1.SecretStore) *esv1.SecretStore {
		store.Spec.Provider.Gitlab.Auth.SecretRef.AccessToken = esv1meta.SecretKeySelector{
			Name:      name,
			Key:       key,
			Namespace: namespace,
		}
		return store
	}
}

func withGroups(ids []string, inherit bool) storeModifier {
	return func(store *esv1.SecretStore) *esv1.SecretStore {
		store.Spec.Provider.Gitlab.GroupIDs = ids
		store.Spec.Provider.Gitlab.InheritFromGroups = inherit
		return store
	}
}

type ValidateStoreTestCase struct {
	store *esv1.SecretStore
	err   error
}

func TestValidateStore(t *testing.T) {
	namespace := "my-namespace"
	testCases := []ValidateStoreTestCase{
		{
			store: makeSecretStore("", environment),
			err:   errors.New("projectID and groupIDs must not both be empty"),
		},
		{
			store: makeSecretStore(project, environment, withGroups([]string{"group1"}, true)),
			err:   errors.New("defining groupIDs and inheritFromGroups = true is not allowed"),
		},
		{
			store: makeSecretStore(project, environment, withAccessToken("", userkey, nil)),
			err:   errors.New("accessToken.name cannot be empty"),
		},
		{
			store: makeSecretStore(project, environment, withAccessToken(username, "", nil)),
			err:   errors.New("accessToken.key cannot be empty"),
		},
		{
			store: makeSecretStore(project, environment, withAccessToken("userName", "userKey", &namespace)),
			err:   errors.New("namespace should either be empty or match the namespace of the SecretStore for a namespaced SecretStore"),
		},
		{
			store: makeSecretStore(project, environment, withAccessToken("userName", "userKey", nil)),
			err:   nil,
		},
		{
			store: makeSecretStore("", environment, withGroups([]string{"group1"}, false), withAccessToken("userName", "userKey", nil)),
			err:   nil,
		},
	}
	p := Provider{}
	for _, tc := range testCases {
		_, err := p.ValidateStore(tc.store)
		if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
			t.Errorf("test failed! want %v, got %v", tc.err, err)
		} else if tc.err == nil && err != nil {
			t.Errorf("want nil got err %v", err)
		} else if tc.err != nil && err == nil {
			t.Errorf("want err %v got nil", tc.err)
		}
	}
}

func ErrorContains(out error, want string) bool {
	if out == nil {
		return want == ""
	}
	if want == "" {
		return false
	}
	return strings.Contains(out.Error(), want)
}

type storeModifier func(*esv1.SecretStore) *esv1.SecretStore

func setGroupWildcardVariable(smtc *secretManagerTestCase) {
	// Given a group variable with a wildcard environment scope
	// and a provider with no environment,
	// the group variable should be returned
	smtc.groupIDs = []string{groupid}
	smtc.projectAPIOutput = nil
	smtc.projectAPIOutputs = make404ProjectAPIOutput(2)
	smtc.groupAPIOutput.Key = testKey
	smtc.groupAPIOutput.Value = groupvalue
	smtc.groupAPIOutput.EnvironmentScope = "*"
	smtc.expectedSecret = smtc.groupAPIOutput.Value
}

func setGroupWildcardVariableWithEnvironment(smtc *secretManagerTestCase) {
	// Given a group variable with a wildcard environment scope
	// and a provider with a specific environment,
	// the group variable should be returned
	// todo: verify that the flow for wildcard is observed. Have some doubts
	smtc.groupIDs = []string{groupid}
	smtc.projectAPIOutput = nil
	smtc.projectAPIOutputs = make404ProjectAPIOutput(2)
	smtc.groupAPIOutput.Key = testKey
	smtc.groupAPIOutput.Value = groupvalue
	smtc.groupAPIOutput.EnvironmentScope = "*"
	smtc.apiInputEnv = environment
	smtc.expectedSecret = smtc.groupAPIOutput.Value
}

func setGroupWildcardVariableNotFoundThenFound(smtc *secretManagerTestCase) {
	// Given a group variable with a wildcard environment scope
	// and a provider with a specific environment,
	// the group variable should be returned
	smtc.groupIDs = []string{groupid}

	smtc.projectAPIOutput = nil
	smtc.projectAPIOutputs = make404ProjectAPIOutput(2)
	smtc.groupAPIOutput = nil
	smtc.groupAPIOutputs = []*fakegitlab.APIResponse[[]*gitlab.GroupVariable]{
		{Response: make404GitlabAPIResponse(), Error: gitlab.ErrNotFound},
		{Output: []*gitlab.GroupVariable{
			{
				Key:              testKey,
				Value:            groupvalue,
				EnvironmentScope: "*",
			},
		}, Response: makeValidGroupAPIResponse(), Error: nil},
	}

	smtc.apiInputEnv = environment
	smtc.expectedSecret = groupvalue
}
