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

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"

	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/providers/v1/onboardbase/client"
	"github.com/external-secrets/external-secrets/providers/v1/onboardbase/fake"
)

const (
	validSecretName        = "API_KEY"
	validSecretValue       = "3a3ea4f5"
	onboardbaseProject     = "ONBOARDBASE_PROJECT"
	onboardbaseEnvironment = "development"
	onboardbaseProjectVal  = "payments-service"
	missingSecret          = "INVALID_NAME"
	invalidSecret          = "unknown_project"
	missingSecretErr       = "could not get secret"
)

type onboardbaseTestCase struct {
	label               string
	fakeClient          *fake.OnboardbaseClient
	request             client.SecretRequest
	response            *client.SecretResponse
	remoteRef           *esv1.ExternalSecretDataRemoteRef
	PushSecretRemoteRef esv1.PushSecretRemoteRef
	apiErr              error
	expectError         string
	expectedSecret      string
	expectedData        map[string][]byte
}

func makeValidAPIRequest() client.SecretRequest {
	return client.SecretRequest{
		Name: validSecretName,
	}
}

func makeValidAPIOutput() *client.SecretResponse {
	return &client.SecretResponse{
		Name:  validSecretName,
		Value: validSecretValue,
	}
}

func makeValidRemoteRef() *esv1.ExternalSecretDataRemoteRef {
	return &esv1.ExternalSecretDataRemoteRef{
		Key: validSecretName,
	}
}

type pushRemoteRef struct {
	secretKey string
}

func (pRef pushRemoteRef) GetProperty() string {
	return ""
}

func (pRef pushRemoteRef) GetRemoteKey() string {
	return pRef.secretKey
}

func makeValidPushRemoteRef(key string) esv1.PushSecretRemoteRef {
	return pushRemoteRef{
		secretKey: key,
	}
}

func makeValidOnboardbaseTestCase() *onboardbaseTestCase {
	return &onboardbaseTestCase{
		fakeClient:          &fake.OnboardbaseClient{},
		request:             makeValidAPIRequest(),
		response:            makeValidAPIOutput(),
		remoteRef:           makeValidRemoteRef(),
		PushSecretRemoteRef: makeValidPushRemoteRef(validSecretName),
		apiErr:              nil,
		expectError:         "",
		expectedSecret:      "",
		expectedData:        make(map[string][]byte),
	}
}

func makeValidOnboardbaseTestCaseCustom(tweaks ...func(pstc *onboardbaseTestCase)) *onboardbaseTestCase {
	pstc := makeValidOnboardbaseTestCase()
	for _, fn := range tweaks {
		fn(pstc)
	}
	pstc.fakeClient.WithValue(pstc.request, pstc.response, pstc.apiErr)
	return pstc
}

func TestGetSecret(t *testing.T) {
	setSecret := func(pstc *onboardbaseTestCase) {
		pstc.label = "set secret"
		pstc.request.Name = onboardbaseProject
		pstc.response.Name = onboardbaseProject
		pstc.response.Value = onboardbaseProjectVal
		pstc.expectedSecret = onboardbaseProjectVal
		pstc.remoteRef.Key = onboardbaseProject
	}

	setMissingSecret := func(pstc *onboardbaseTestCase) {
		pstc.label = "invalid missing secret"
		pstc.remoteRef.Key = missingSecret
		pstc.request.Name = missingSecret
		pstc.response = nil
		pstc.expectError = missingSecretErr
		pstc.apiErr = errors.New("")
	}

	setInvalidSecret := func(pstc *onboardbaseTestCase) {
		pstc.label = "invalid secret name format"
		pstc.remoteRef.Key = invalidSecret
		pstc.request.Name = invalidSecret
		pstc.response = nil
		pstc.expectError = missingSecretErr
		pstc.apiErr = errors.New("")
	}

	setClientError := func(pstc *onboardbaseTestCase) {
		pstc.label = "invalid client error"
		pstc.response = &client.SecretResponse{}
		pstc.expectError = missingSecretErr
		pstc.apiErr = errors.New("")
	}

	testCases := []*onboardbaseTestCase{
		makeValidOnboardbaseTestCaseCustom(setSecret),
		makeValidOnboardbaseTestCaseCustom(setMissingSecret),
		makeValidOnboardbaseTestCaseCustom(setInvalidSecret),
		makeValidOnboardbaseTestCaseCustom(setClientError),
	}

	c := Client{}
	for k, tc := range testCases {
		c.onboardbase = tc.fakeClient
		out, err := c.GetSecret(context.Background(), *tc.remoteRef)
		if !ErrorContains(err, tc.expectError) {
			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), tc.expectError)
		}
		if err == nil && !cmp.Equal(string(out), tc.expectedSecret) {
			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, tc.expectedSecret, string(out))
		}
	}
}

func TestDeleteSecret(t *testing.T) {
	setMissingSecret := func(pstc *onboardbaseTestCase) {
		pstc.label = "invalid missing secret"
		pstc.remoteRef.Key = missingSecret
		pstc.PushSecretRemoteRef = makeValidPushRemoteRef(missingSecret)
		pstc.request.Name = missingSecret
		pstc.response = nil
		pstc.expectError = missingSecretErr
		pstc.apiErr = errors.New("")
	}

	setInvalidSecret := func(pstc *onboardbaseTestCase) {
		pstc.label = "invalid secret name format"
		pstc.remoteRef.Key = invalidSecret
		pstc.PushSecretRemoteRef = makeValidPushRemoteRef(invalidSecret)
		pstc.request.Name = invalidSecret
		pstc.response = nil
		pstc.expectError = missingSecretErr
		pstc.apiErr = errors.New("")
	}

	deleteSecret := func(pstc *onboardbaseTestCase) {
		pstc.label = "delete secret successfully"
		pstc.remoteRef.Key = validSecretName
		pstc.PushSecretRemoteRef = makeValidPushRemoteRef(validSecretName)
		pstc.request.Name = validSecretName
		pstc.response = nil
		pstc.apiErr = nil
	}

	testCases := []*onboardbaseTestCase{
		makeValidOnboardbaseTestCaseCustom(setMissingSecret),
		makeValidOnboardbaseTestCaseCustom(setInvalidSecret),
		makeValidOnboardbaseTestCaseCustom(deleteSecret),
	}

	c := Client{}
	for k, tc := range testCases {
		c.onboardbase = tc.fakeClient
		err := c.DeleteSecret(context.Background(), tc.PushSecretRemoteRef)
		if err != nil && !ErrorContains(err, tc.expectError) {
			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), tc.expectError)
		}
	}
}

func TestGetSecretMap(t *testing.T) {
	simpleJSON := func(pstc *onboardbaseTestCase) {
		pstc.label = "valid unmarshalling"
		pstc.response.Value = `{"API_KEY":"3a3ea4f5"}`
		pstc.expectedData["API_KEY"] = []byte("3a3ea4f5")
	}

	complexJSON := func(pstc *onboardbaseTestCase) {
		pstc.label = "valid unmarshalling for nested json"
		pstc.response.Value = `{"API_KEY": "3a3ea4fs5", "AUTH_SA": {"appID": "a1ea-48bd-8749-b6f5ec3c5a1f"}}`
		pstc.expectedData["API_KEY"] = []byte("3a3ea4fs5")
		pstc.expectedData["AUTH_SA"] = []byte(`{"appID": "a1ea-48bd-8749-b6f5ec3c5a1f"}`)
	}

	setInvalidJSON := func(pstc *onboardbaseTestCase) {
		pstc.label = "invalid json"
		pstc.response.Value = `{"API_KEY": "3a3ea4f`
		pstc.expectError = "unable to unmarshal secret"
	}

	setAPIError := func(pstc *onboardbaseTestCase) {
		pstc.label = "client error"
		pstc.response = &client.SecretResponse{}
		pstc.expectError = missingSecretErr
		pstc.apiErr = errors.New("")
	}

	testCases := []*onboardbaseTestCase{
		makeValidOnboardbaseTestCaseCustom(simpleJSON),
		makeValidOnboardbaseTestCaseCustom(complexJSON),
		makeValidOnboardbaseTestCaseCustom(setInvalidJSON),
		makeValidOnboardbaseTestCaseCustom(setAPIError),
	}

	d := Client{}
	for k, tc := range testCases {
		t.Run(tc.label, func(t *testing.T) {
			d.onboardbase = tc.fakeClient
			out, err := d.GetSecretMap(context.Background(), *tc.remoteRef)
			if !ErrorContains(err, tc.expectError) {
				t.Errorf("[%d] unexpected error: %q, expected: %q", k, err.Error(), tc.expectError)
			}
			if err == nil && !cmp.Equal(out, tc.expectedData) {
				t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, tc.expectedData, out)
			}
		})
	}
}

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 makeSecretStore(fn ...storeModifier) *esv1.SecretStore {
	store := &esv1.SecretStore{
		Spec: esv1.SecretStoreSpec{
			Provider: &esv1.SecretStoreProvider{
				Onboardbase: &esv1.OnboardbaseProvider{
					Auth: &esv1.OnboardbaseAuthSecretRef{},
				},
			},
		},
	}
	for _, f := range fn {
		store = f(store)
	}
	return store
}

func withAuth(name, key string, namespace *string, passcode string) storeModifier {
	return func(store *esv1.SecretStore) *esv1.SecretStore {
		store.Spec.Provider.Onboardbase.Auth.OnboardbaseAPIKeyRef = v1.SecretKeySelector{
			Name:      name,
			Key:       key,
			Namespace: namespace,
		}
		store.Spec.Provider.Onboardbase.Auth.OnboardbasePasscodeRef = v1.SecretKeySelector{
			Name:      passcode,
			Key:       passcode,
			Namespace: namespace,
		}
		return store
	}
}

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

func TestValidateStore(t *testing.T) {
	namespace := "ns"
	secretName := "onboardbase-api-key-secret"
	testCases := []ValidateStoreTestCase{
		{
			label: "invalid store missing onboardbaseAPIKey.name",
			store: makeSecretStore(withAuth("", "", nil, "")),
			err:   errors.New("invalid store: onboardbaseAPIKey.name cannot be empty"),
		},
		{
			label: "invalid store missing onboardbasePasscode.name",
			store: makeSecretStore(withAuth(secretName, "", nil, "")),
			err:   errors.New("invalid store: onboardbasePasscode.name cannot be empty"),
		},
		{
			label: "invalid store namespace not allowed",
			store: makeSecretStore(withAuth(secretName, "", &namespace, "passcode")),
			err:   errors.New("invalid store: namespace should either be empty or match the namespace of the SecretStore for a namespaced SecretStore"),
		},
		{
			label: "valid provide optional onboardbaseAPIKey.key",
			store: makeSecretStore(withAuth(secretName, "customSecretKey", nil, "passcode")),
			err:   nil,
		},
		{
			label: "valid namespace not set",
			store: makeSecretStore(withAuth(secretName, "", nil, "passcode")),
			err:   nil,
		},
	}
	p := Provider{}
	for _, tc := range testCases {
		t.Run(tc.label, func(t *testing.T) {
			_, 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)
			}
		})
	}
}
