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

import (
	"context"
	"testing"

	"github.com/DelineaXPM/dsv-sdk-go/v2/vault"
	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	kubeErrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kubeClient "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"
	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
	"github.com/external-secrets/external-secrets/runtime/esutils"
)

func TestDoesConfigDependOnNamespace(t *testing.T) {
	tests := map[string]struct {
		cfg  esv1.DelineaProvider
		want bool
	}{
		"true when client ID references a secret without explicit namespace": {
			cfg: esv1.DelineaProvider{
				ClientID: &esv1.DelineaProviderSecretRef{
					SecretRef: &v1.SecretKeySelector{Name: "foo"},
				},
				ClientSecret: &esv1.DelineaProviderSecretRef{SecretRef: nil},
			},
			want: true,
		},
		"true when client secret references a secret without explicit namespace": {
			cfg: esv1.DelineaProvider{
				ClientID: &esv1.DelineaProviderSecretRef{SecretRef: nil},
				ClientSecret: &esv1.DelineaProviderSecretRef{
					SecretRef: &v1.SecretKeySelector{Name: "foo"},
				},
			},
			want: true,
		},
		"false when neither client ID nor secret reference a secret": {
			cfg: esv1.DelineaProvider{
				ClientID:     &esv1.DelineaProviderSecretRef{SecretRef: nil},
				ClientSecret: &esv1.DelineaProviderSecretRef{SecretRef: nil},
			},
			want: false,
		},
	}
	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			got := doesConfigDependOnNamespace(&tc.cfg)
			assert.Equal(t, tc.want, got)
		})
	}
}

func TestValidateStore(t *testing.T) {
	validSecretRefUsingValue := makeSecretRefUsingValue("foo")
	ambiguousSecretRef := &esv1.DelineaProviderSecretRef{
		SecretRef: &v1.SecretKeySelector{Name: "foo"}, Value: "foo",
	}
	tests := map[string]struct {
		cfg  esv1.DelineaProvider
		want error
	}{
		"invalid without tenant": {
			cfg: esv1.DelineaProvider{
				Tenant:       "",
				ClientID:     validSecretRefUsingValue,
				ClientSecret: validSecretRefUsingValue,
			},
			want: errEmptyTenant,
		},
		"invalid without clientID": {
			cfg: esv1.DelineaProvider{
				Tenant: "foo",
				// ClientID omitted
				ClientSecret: validSecretRefUsingValue,
			},
			want: errEmptyClientID,
		},
		"invalid without clientSecret": {
			cfg: esv1.DelineaProvider{
				Tenant:   "foo",
				ClientID: validSecretRefUsingValue,
				// ClientSecret omitted
			},
			want: errEmptyClientSecret,
		},
		"invalid with ambiguous clientID": {
			cfg: esv1.DelineaProvider{
				Tenant:       "foo",
				ClientID:     ambiguousSecretRef,
				ClientSecret: validSecretRefUsingValue,
			},
			want: esutils.ErrValueAndRefConflict,
		},
		"invalid with ambiguous clientSecret": {
			cfg: esv1.DelineaProvider{
				Tenant:       "foo",
				ClientID:     validSecretRefUsingValue,
				ClientSecret: ambiguousSecretRef,
			},
			want: esutils.ErrValueAndRefConflict,
		},
		"invalid with invalid clientID": {
			cfg: esv1.DelineaProvider{
				Tenant:       "foo",
				ClientID:     makeSecretRefUsingValue(""),
				ClientSecret: validSecretRefUsingValue,
			},
			want: esutils.ErrValueOrRefMissing,
		},
		"invalid with invalid clientSecret": {
			cfg: esv1.DelineaProvider{
				Tenant:       "foo",
				ClientID:     validSecretRefUsingValue,
				ClientSecret: makeSecretRefUsingValue(""),
			},
			want: esutils.ErrValueOrRefMissing,
		},
		"valid with tenant/clientID/clientSecret": {
			cfg: esv1.DelineaProvider{
				Tenant:       "foo",
				ClientID:     validSecretRefUsingValue,
				ClientSecret: validSecretRefUsingValue,
			},
			want: nil,
		},
	}
	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			s := esv1.SecretStore{
				Spec: esv1.SecretStoreSpec{
					Provider: &esv1.SecretStoreProvider{
						Delinea: &tc.cfg,
					},
				},
			}
			p := &Provider{}
			_, got := p.ValidateStore(&s)
			assert.Equal(t, tc.want, got)
		})
	}
}

func TestValidateStoreBailsOnUnexpectedStore(t *testing.T) {
	tests := map[string]struct {
		store esv1.GenericStore
		want  error
	}{
		"missing store": {nil, errMissingStore},
		"missing spec":  {&esv1.SecretStore{}, errInvalidSpec},
		"missing provider": {&esv1.SecretStore{
			Spec: esv1.SecretStoreSpec{Provider: nil},
		}, errInvalidSpec},
		"missing delinea": {&esv1.SecretStore{
			Spec: esv1.SecretStoreSpec{Provider: &esv1.SecretStoreProvider{
				Delinea: nil,
			}},
		}, errInvalidSpec},
	}
	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			p := &Provider{}
			_, got := p.ValidateStore(tc.store)
			assert.Equal(t, tc.want, got)
		})
	}
}

func TestNewClient(t *testing.T) {
	tenant := "foo"
	clientIDKey := "username"
	clientIDValue := "client id"
	clientSecretKey := "password"
	clientSecretValue := "client secret"

	clientSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"},
		Data: map[string][]byte{
			clientIDKey:     []byte(clientIDValue),
			clientSecretKey: []byte(clientSecretValue),
		},
	}

	validProvider := &esv1.DelineaProvider{
		Tenant:       tenant,
		ClientID:     makeSecretRefUsingRef(clientSecret.Name, clientIDKey),
		ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
	}

	tests := map[string]struct {
		store    esv1.GenericStore     // leave nil for namespaced store
		provider *esv1.DelineaProvider // discarded when store is set
		kube     kubeClient.Client
		errCheck func(t *testing.T, err error)
	}{
		"missing provider config": {
			provider: nil,
			errCheck: func(t *testing.T, err error) {
				assert.ErrorIs(t, err, errInvalidSpec)
			},
		},
		"namespace-dependent cluster secret store": {
			store: &esv1.ClusterSecretStore{
				TypeMeta: metav1.TypeMeta{Kind: esv1.ClusterSecretStoreKind},
				Spec: esv1.SecretStoreSpec{
					Provider: &esv1.SecretStoreProvider{
						Delinea: validProvider,
					},
				},
			},
			errCheck: func(t *testing.T, err error) {
				assert.ErrorIs(t, err, errClusterStoreRequiresNamespace)
			},
		},
		"dangling client ID ref": {
			provider: &esv1.DelineaProvider{
				Tenant:       tenant,
				ClientID:     makeSecretRefUsingRef("typo", clientIDKey),
				ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
			},
			kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
			errCheck: func(t *testing.T, err error) {
				assert.True(t, kubeErrors.IsNotFound(err))
			},
		},
		"dangling client secret ref": {
			provider: &esv1.DelineaProvider{
				Tenant:       tenant,
				ClientID:     makeSecretRefUsingRef(clientSecret.Name, clientIDKey),
				ClientSecret: makeSecretRefUsingRef("typo", clientSecretKey),
			},
			kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
			errCheck: func(t *testing.T, err error) {
				assert.True(t, kubeErrors.IsNotFound(err))
			},
		},
		"secret ref without name": {
			provider: &esv1.DelineaProvider{
				Tenant:       tenant,
				ClientID:     makeSecretRefUsingRef("", clientIDKey),
				ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
			},
			kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
			errCheck: func(t *testing.T, err error) {
				assert.ErrorIs(t, err, errMissingSecretName)
			},
		},
		"secret ref without key": {
			provider: &esv1.DelineaProvider{
				Tenant:       tenant,
				ClientID:     makeSecretRefUsingRef(clientSecret.Name, ""),
				ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
			},
			kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
			errCheck: func(t *testing.T, err error) {
				assert.ErrorIs(t, err, errMissingSecretKey)
			},
		},
		"secret ref with non-existent keys": {
			provider: &esv1.DelineaProvider{
				Tenant:       tenant,
				ClientID:     makeSecretRefUsingRef(clientSecret.Name, "typo"),
				ClientSecret: makeSecretRefUsingRef(clientSecret.Name, clientSecretKey),
			},
			kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
			errCheck: func(t *testing.T, err error) {
				assert.EqualError(t, err, "cannot find secret data for key: \"typo\"")
			},
		},
		"valid secret refs": {
			provider: validProvider,
			kube:     clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
		},
		"secret values": {
			provider: &esv1.DelineaProvider{
				Tenant:       tenant,
				ClientID:     makeSecretRefUsingValue(clientIDValue),
				ClientSecret: makeSecretRefUsingValue(clientSecretValue),
			},
			kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
		},
		"cluster secret store": {
			store: &esv1.ClusterSecretStore{
				TypeMeta: metav1.TypeMeta{Kind: esv1.ClusterSecretStoreKind},
				Spec: esv1.SecretStoreSpec{
					Provider: &esv1.SecretStoreProvider{
						Delinea: &esv1.DelineaProvider{
							Tenant:       tenant,
							ClientID:     makeSecretRefUsingNamespacedRef(clientSecret.Namespace, clientSecret.Name, clientIDKey),
							ClientSecret: makeSecretRefUsingNamespacedRef(clientSecret.Namespace, clientSecret.Name, clientSecretKey),
						},
					},
				},
			},
			kube: clientfake.NewClientBuilder().WithObjects(clientSecret).Build(),
		},
	}
	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			p := &Provider{}
			store := tc.store
			if store == nil {
				store = &esv1.SecretStore{
					TypeMeta: metav1.TypeMeta{Kind: esv1.SecretStoreKind},
					Spec: esv1.SecretStoreSpec{
						Provider: &esv1.SecretStoreProvider{
							Delinea: tc.provider,
						},
					},
				}
			}
			sc, err := p.NewClient(context.Background(), store, tc.kube, clientSecret.Namespace)
			if tc.errCheck == nil {
				assert.NoError(t, err)
				delineaClient, ok := sc.(*client)
				assert.True(t, ok)
				dsvClient, ok := delineaClient.api.(*vault.Vault)
				assert.True(t, ok)
				assert.Equal(t, vault.Configuration{
					Credentials: vault.ClientCredential{
						ClientID:     clientIDValue,
						ClientSecret: clientSecretValue,
					},
					Tenant:      tenant,
					TLD:         "com",                                     // Default from Delinea
					URLTemplate: "https://%s.secretsvaultcloud.%s/v1/%s%s", // Default from Delinea
				}, dsvClient.Configuration)
			} else {
				assert.Nil(t, sc)
				tc.errCheck(t, err)
			}
		})
	}
}

func makeSecretRefUsingRef(name, key string) *esv1.DelineaProviderSecretRef {
	return &esv1.DelineaProviderSecretRef{
		SecretRef: &v1.SecretKeySelector{Name: name, Key: key},
	}
}

func makeSecretRefUsingNamespacedRef(namespace, name, key string) *esv1.DelineaProviderSecretRef {
	return &esv1.DelineaProviderSecretRef{
		SecretRef: &v1.SecretKeySelector{Namespace: new(namespace), Name: name, Key: key},
	}
}

func makeSecretRefUsingValue(val string) *esv1.DelineaProviderSecretRef {
	return &esv1.DelineaProviderSecretRef{Value: val}
}
