/*
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 keyvault

import (
	"context"
	"net/http"
	"os"
	"strings"
	"testing"

	"github.com/Azure/go-autorest/autorest"
	"github.com/Azure/go-autorest/autorest/adal"
	tassert "github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"

	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
	utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
)

var vaultURL = "https://local.vault.url"

func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
	namespace := "internal"
	identityID := "1234"
	authType := esv1beta1.AzureManagedIdentity
	store := esv1beta1.SecretStore{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
		},
		Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{AzureKV: &esv1beta1.AzureKVProvider{
			AuthType:   &authType,
			IdentityID: &identityID,
			VaultURL:   &vaultURL,
		}}},
	}
	k8sClient := clientfake.NewClientBuilder().Build()
	az := &Azure{
		crClient:  k8sClient,
		namespace: namespace,
		provider:  store.Spec.Provider.AzureKV,
		store:     &store,
	}
	authorizer, err := az.authorizerForManagedIdentity()
	if err != nil {
		// On non Azure environment, MSI auth not available, so this error should be returned
		tassert.EqualError(t, err, "failed to get oauth token from MSI: MSI not available")
	} else {
		// On Azure (where GitHub Actions are running) a secretClient is returned, as only an Authorizer is configured, but no token is requested for MI
		tassert.NotNil(t, authorizer)
	}
}

func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
	const (
		tenantID      = "my-tenant-id"
		clientID      = "my-client-id"
		azAccessToken = "my-access-token"
		saToken       = "FAKETOKEN"
		saName        = "az-wi"
		namespace     = "default"
	)

	// create a temporary file to imitate
	// azure workload identity webhook
	// see AZURE_FEDERATED_TOKEN_FILE
	tf, err := os.CreateTemp("", "")
	tassert.Nil(t, err)
	defer os.RemoveAll(tf.Name())
	_, err = tf.WriteString(saToken)
	tassert.Nil(t, err)
	tokenFile := tf.Name()

	authType := esv1beta1.AzureWorkloadIdentity
	defaultProvider := &esv1beta1.AzureKVProvider{
		VaultURL: &vaultURL,
		AuthType: &authType,
		ServiceAccountRef: &v1.ServiceAccountSelector{
			Name: saName,
		},
	}

	type testCase struct {
		name       string
		provider   *esv1beta1.AzureKVProvider
		k8sObjects []client.Object
		prep       func(*testing.T)
		expErr     string
	}

	for _, row := range []testCase{
		{
			name:     "missing service account",
			provider: defaultProvider,
			expErr:   "serviceaccounts \"" + saName + "\" not found",
		},
		{
			name:     "missing webhook env vars",
			provider: &esv1beta1.AzureKVProvider{},
			expErr:   "missing environment variables. AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE must be set",
		},
		{
			name:     "missing workload identity token file",
			provider: &esv1beta1.AzureKVProvider{},
			prep: func(t *testing.T) {
				t.Setenv("AZURE_CLIENT_ID", clientID)
				t.Setenv("AZURE_TENANT_ID", tenantID)
				t.Setenv("AZURE_FEDERATED_TOKEN_FILE", "invalid file")
			},
			expErr: "unable to read token file invalid file: open invalid file: no such file or directory",
		},
		{
			name:     "correct workload identity",
			provider: &esv1beta1.AzureKVProvider{},
			prep: func(t *testing.T) {
				t.Setenv("AZURE_CLIENT_ID", clientID)
				t.Setenv("AZURE_TENANT_ID", tenantID)
				t.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFile)
			},
		},
		{
			name:     "missing sa annotations",
			provider: defaultProvider,
			k8sObjects: []client.Object{
				&corev1.ServiceAccount{
					ObjectMeta: metav1.ObjectMeta{
						Name:        saName,
						Namespace:   namespace,
						Annotations: map[string]string{},
					},
				},
			},
			expErr: "missing service account annotation: azure.workload.identity/client-id",
		},
		{
			name:     "successful case",
			provider: defaultProvider,
			k8sObjects: []client.Object{
				&corev1.ServiceAccount{
					ObjectMeta: metav1.ObjectMeta{
						Name:      saName,
						Namespace: namespace,
						Annotations: map[string]string{
							AnnotationClientID: clientID,
							AnnotationTenantID: tenantID,
						},
					},
				},
			},
		},
	} {
		t.Run(row.name, func(t *testing.T) {
			store := esv1beta1.SecretStore{
				Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{
					AzureKV: row.provider,
				}},
			}
			k8sClient := clientfake.NewClientBuilder().
				WithObjects(row.k8sObjects...).
				Build()
			az := &Azure{
				store:      &store,
				namespace:  namespace,
				crClient:   k8sClient,
				kubeClient: utilfake.NewCreateTokenMock().WithToken(saToken),
				provider:   store.Spec.Provider.AzureKV,
			}
			tokenProvider := func(ctx context.Context, token, clientID, tenantID, aadEndpoint, kvResource string) (adal.OAuthTokenProvider, error) {
				tassert.Equal(t, token, saToken)
				tassert.Equal(t, clientID, clientID)
				tassert.Equal(t, tenantID, tenantID)
				return &tokenProvider{accessToken: azAccessToken}, nil
			}
			if row.prep != nil {
				row.prep(t)
			}
			authorizer, err := az.authorizerForWorkloadIdentity(context.Background(), tokenProvider)
			if row.expErr == "" {
				tassert.NotNil(t, authorizer)
				tassert.Equal(t, getTokenFromAuthorizer(t, authorizer), azAccessToken)
			} else {
				tassert.EqualError(t, err, row.expErr)
			}
		})
	}
}

func TestAuth(t *testing.T) {
	defaultStore := esv1beta1.SecretStore{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "default",
		},
		Spec: esv1beta1.SecretStoreSpec{
			Provider: &esv1beta1.SecretStoreProvider{},
		},
	}
	authType := esv1beta1.AzureServicePrincipal

	type testCase struct {
		name     string
		provider *esv1beta1.AzureKVProvider
		store    esv1beta1.GenericStore
		objects  []client.Object
		expErr   string
	}
	for _, row := range []testCase{
		{
			name:   "bad config",
			expErr: "missing secretRef in provider config",
			store:  &defaultStore,
			provider: &esv1beta1.AzureKVProvider{
				AuthType: &authType,
				VaultURL: &vaultURL,
				TenantID: pointer.String("mytenant"),
			},
		},
		{
			name:   "bad config",
			expErr: "missing accessKeyID/secretAccessKey in store config",
			store:  &defaultStore,
			provider: &esv1beta1.AzureKVProvider{
				AuthType:      &authType,
				VaultURL:      &vaultURL,
				TenantID:      pointer.String("mytenant"),
				AuthSecretRef: &esv1beta1.AzureKVAuth{},
			},
		},
		{
			name:   "bad config: missing secret",
			expErr: "could not find secret default/password: secrets \"password\" not found",
			store:  &defaultStore,
			provider: &esv1beta1.AzureKVProvider{
				AuthType: &authType,
				VaultURL: &vaultURL,
				TenantID: pointer.String("mytenant"),
				AuthSecretRef: &esv1beta1.AzureKVAuth{
					ClientSecret: &v1.SecretKeySelector{Name: "password"},
					ClientID:     &v1.SecretKeySelector{Name: "password"},
				},
			},
		},
		{
			name:   "cluster secret store",
			expErr: "could not find secret foo/password: secrets \"password\" not found",
			store: &esv1beta1.ClusterSecretStore{
				TypeMeta: metav1.TypeMeta{
					Kind: esv1beta1.ClusterSecretStoreKind,
				},
				Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
			},
			provider: &esv1beta1.AzureKVProvider{
				AuthType: &authType,
				VaultURL: &vaultURL,
				TenantID: pointer.String("mytenant"),
				AuthSecretRef: &esv1beta1.AzureKVAuth{
					ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.String("foo")},
					ClientID:     &v1.SecretKeySelector{Name: "password", Namespace: pointer.String("foo")},
				},
			},
		},
		{
			name: "correct cluster secret store",
			objects: []client.Object{&corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "password",
					Namespace: "foo",
				},
				Data: map[string][]byte{
					"id":     []byte("foo"),
					"secret": []byte("bar"),
				},
			}},
			store: &esv1beta1.ClusterSecretStore{
				TypeMeta: metav1.TypeMeta{
					Kind: esv1beta1.ClusterSecretStoreKind,
				},
				Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
			},
			provider: &esv1beta1.AzureKVProvider{
				AuthType: &authType,
				VaultURL: &vaultURL,
				TenantID: pointer.String("mytenant"),
				AuthSecretRef: &esv1beta1.AzureKVAuth{
					ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.String("foo"), Key: "secret"},
					ClientID:     &v1.SecretKeySelector{Name: "password", Namespace: pointer.String("foo"), Key: "id"},
				},
			},
		},
	} {
		t.Run(row.name, func(t *testing.T) {
			k8sClient := clientfake.NewClientBuilder().WithObjects(row.objects...).Build()
			spec := row.store.GetSpec()
			spec.Provider.AzureKV = row.provider
			az := &Azure{
				crClient:  k8sClient,
				namespace: "default",
				provider:  spec.Provider.AzureKV,
				store:     row.store,
			}
			authorizer, err := az.authorizerForServicePrincipal(context.Background())
			if row.expErr == "" {
				tassert.Nil(t, err)
				tassert.NotNil(t, authorizer)
			} else {
				tassert.EqualError(t, err, row.expErr)
			}
		})
	}
}

func getTokenFromAuthorizer(t *testing.T, authorizer autorest.Authorizer) string {
	rq, _ := http.NewRequest("POST", "http://example.com", http.NoBody)
	_, err := authorizer.WithAuthorization()(
		autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
			return rq, nil
		})).Prepare(rq)
	tassert.Nil(t, err)
	return strings.TrimPrefix(rq.Header.Get("Authorization"), "Bearer ")
}
