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

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/go-chef/chef"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	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"
	fake "github.com/external-secrets/external-secrets/pkg/provider/chef/fake"
	"github.com/external-secrets/external-secrets/pkg/utils"
)

const (
	name                     = "chef-demo-user"
	baseURL                  = "https://chef.cloudant.com/organizations/myorg/"
	noEndSlashInvalidBaseURL = "no end slash invalid base URL"
	baseInvalidURL           = "invalid base URL/"
	authName                 = "chef-demo-auth-name"
	authKey                  = "chef-demo-auth-key"
	authNamespace            = "chef-demo-auth-namespace"
	kind                     = "SecretStore"
	apiversion               = "external-secrets.io/v1beta1"
	databagName              = "databag01"
)

type chefTestCase struct {
	mockClient      *fake.ChefMockClient
	databagName     string
	databagItemName string
	property        string
	ref             *esv1beta1.ExternalSecretDataRemoteRef
	apiErr          error
	expectError     string
	expectedData    map[string][]byte
	expectedByte    []byte
}

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

// type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore

func makeValidChefTestCase() *chefTestCase {
	smtc := chefTestCase{
		mockClient:      &fake.ChefMockClient{},
		databagName:     "databag01",
		databagItemName: "item01",
		property:        "",
		apiErr:          nil,
		expectError:     "",
		expectedData:    map[string][]byte{"item01": []byte(`"https://chef.com/organizations/dev/data/databag01/item01"`)},
		expectedByte:    []byte(`{"item01":"{\"id\":\"databag01-item01\",\"some_key\":\"fe7f29ede349519a1\",\"some_password\":\"dolphin_123zc\",\"some_username\":\"testuser\"}"}`),
	}

	smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, smtc.property)
	smtc.mockClient.WithListItems(smtc.databagName, smtc.apiErr)
	smtc.mockClient.WithItem(smtc.databagName, smtc.databagItemName, smtc.apiErr)
	return &smtc
}

func makeInValidChefTestCase() *chefTestCase {
	smtc := chefTestCase{
		mockClient:      &fake.ChefMockClient{},
		databagName:     "databag01",
		databagItemName: "item03",
		property:        "",
		apiErr:          errors.New("unable to convert databagItem into JSON"),
		expectError:     "unable to convert databagItem into JSON",
		expectedData:    nil,
		expectedByte:    nil,
	}

	smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, smtc.property)
	smtc.mockClient.WithListItems(smtc.databagName, smtc.apiErr)
	smtc.mockClient.WithItem(smtc.databagName, smtc.databagItemName, smtc.apiErr)
	return &smtc
}

func makeValidRef(databag, dataitem, property string) *esv1beta1.ExternalSecretDataRemoteRef {
	return &esv1beta1.ExternalSecretDataRemoteRef{
		Key:      databag + "/" + dataitem,
		Property: property,
	}
}

func makeinValidRef() *esv1beta1.ExternalSecretDataRemoteRef {
	return &esv1beta1.ExternalSecretDataRemoteRef{
		Key: "",
	}
}

func makeValidRefForGetSecretMap(databag string) *esv1beta1.ExternalSecretDataRemoteRef {
	return &esv1beta1.ExternalSecretDataRemoteRef{
		Key: databag,
	}
}

func makeValidChefTestCaseCustom(tweaks ...func(smtc *chefTestCase)) *chefTestCase {
	smtc := makeValidChefTestCase()
	for _, fn := range tweaks {
		fn(smtc)
	}
	return smtc
}

func TestChefGetSecret(t *testing.T) {
	nilClient := func(smtc *chefTestCase) {
		smtc.mockClient = nil
		smtc.expectedByte = nil
		smtc.expectError = "chef provider is not initialized"
	}

	invalidDatabagName := func(smtc *chefTestCase) {
		smtc.databagName = "databag02"
		smtc.expectedByte = nil
		smtc.ref = makeinValidRef()
		smtc.expectError = "invalid key format in data section. Expected value 'databagName/databagItemName'"
	}

	invalidDatabagItemName := func(smtc *chefTestCase) {
		smtc.expectError = "data bag item item02 not found in data bag databag01"
		smtc.databagName = databagName
		smtc.databagItemName = "item02"
		smtc.expectedByte = nil
		smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "")
	}

	noProperty := func(smtc *chefTestCase) {
		smtc.expectError = "property findProperty not found in data bag item"
		smtc.databagName = databagName
		smtc.databagItemName = "item01"
		smtc.expectedByte = nil
		smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "findProperty")
	}

	withProperty := func(smtc *chefTestCase) {
		smtc.expectedByte = []byte("foundProperty")
		smtc.databagName = "databag03"
		smtc.databagItemName = "item03"
		smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "findProperty")
	}

	successCases := []*chefTestCase{
		makeValidChefTestCase(),
		makeValidChefTestCaseCustom(nilClient),
		makeValidChefTestCaseCustom(invalidDatabagName),
		makeValidChefTestCaseCustom(invalidDatabagItemName),
		makeValidChefTestCaseCustom(noProperty),
		makeValidChefTestCaseCustom(withProperty),
		makeInValidChefTestCase(),
	}

	sm := Providerchef{
		databagService: &chef.DataBagService{},
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	for k, v := range successCases {
		sm.databagService = v.mockClient
		out, err := sm.GetSecret(ctx, *v.ref)
		if err != nil && !utils.ErrorContains(err, v.expectError) {
			t.Errorf("[case %d] expected error: %v, got: %v", k, v.expectError, err)
		} else if v.expectError != "" && err == nil {
			t.Errorf("[case %d] expected error: %v, got: nil", k, v.expectError)
		}
		if !bytes.Equal(out, v.expectedByte) {
			t.Errorf("[case %d] expected secret: %s, got: %s", k, v.expectedByte, out)
		}
	}
}

func TestChefGetSecretMap(t *testing.T) {
	nilClient := func(smtc *chefTestCase) {
		smtc.mockClient = nil
		smtc.expectedByte = nil
		smtc.expectError = "chef provider is not initialized"
	}

	databagHasSlash := func(smtc *chefTestCase) {
		smtc.expectedByte = nil
		smtc.ref = makeinValidRef()
		smtc.ref.Key = "data/Bag02"
		smtc.expectError = "invalid key format in dataForm section. Expected only 'databagName'"
	}

	withProperty := func(smtc *chefTestCase) {
		smtc.expectedByte = []byte(`{"item01":"{\"id\":\"databag01-item01\",\"some_key\":\"fe7f29ede349519a1\",\"some_password\":\"dolphin_123zc\",\"some_username\":\"testuser\"}"}`)
		smtc.databagName = databagName
		smtc.ref = makeValidRefForGetSecretMap(smtc.databagName)
	}

	withProperty2 := func(smtc *chefTestCase) {
		smtc.expectError = "unable to list items in data bag 123, may be given data bag doesn't exists or it is empty"
		smtc.expectedByte = nil
		smtc.databagName = "123"
		smtc.ref = makeValidRefForGetSecretMap(smtc.databagName)
	}

	successCases := []*chefTestCase{
		makeValidChefTestCaseCustom(nilClient),
		makeValidChefTestCaseCustom(databagHasSlash),
		makeValidChefTestCaseCustom(withProperty),
		makeValidChefTestCaseCustom(withProperty2),
	}

	pc := Providerchef{
		databagService: &chef.DataBagService{},
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	for k, v := range successCases {
		pc.databagService = v.mockClient
		out, err := pc.GetSecretMap(ctx, *v.ref)
		if err != nil && !utils.ErrorContains(err, v.expectError) {
			t.Errorf("[case %d] expected error: %v, got: %v", k, v.expectError, err)
		} else if v.expectError != "" && err == nil {
			t.Errorf("[case %d] expected error: %v, got: nil", k, v.expectError)
		}
		if !bytes.Equal(out["item01"], v.expectedByte) {
			t.Errorf("[case %d] unexpected secret: expected %s, got %s", k, v.expectedByte, out)
		}
	}
}

func makeSecretStore(name, baseURL string, auth *esv1beta1.ChefAuth) *esv1beta1.SecretStore {
	store := &esv1beta1.SecretStore{
		Spec: esv1beta1.SecretStoreSpec{
			Provider: &esv1beta1.SecretStoreProvider{
				Chef: &esv1beta1.ChefProvider{
					UserName:  name,
					ServerURL: baseURL,
					Auth:      auth,
				},
			},
		},
	}
	return store
}

func makeAuth(name, namespace, key string) *esv1beta1.ChefAuth {
	return &esv1beta1.ChefAuth{
		SecretRef: esv1beta1.ChefAuthSecretRef{
			SecretKey: v1.SecretKeySelector{
				Name:      name,
				Key:       key,
				Namespace: &namespace,
			},
		},
	}
}

func TestValidateStore(t *testing.T) {
	testCases := []ValidateStoreTestCase{
		{
			store: makeSecretStore("", baseURL, makeAuth(authName, authNamespace, authKey)),
			err:   errors.New("received invalid Chef SecretStore resource: missing username"),
		},
		{
			store: makeSecretStore(name, "", makeAuth(authName, authNamespace, authKey)),
			err:   errors.New("received invalid Chef SecretStore resource: missing serverurl"),
		},
		{
			store: makeSecretStore(name, baseURL, nil),
			err:   errors.New("received invalid Chef SecretStore resource: cannot initialize Chef Client: no valid authType was specified"),
		},
		{
			store: makeSecretStore(name, baseInvalidURL, makeAuth(authName, authNamespace, authKey)),
			err:   errors.New("received invalid Chef SecretStore resource: invalid serverurl: parse \"invalid base URL/\": invalid URI for request"),
		},
		{
			store: makeSecretStore(name, noEndSlashInvalidBaseURL, makeAuth(authName, authNamespace, authKey)),
			err:   errors.New("received invalid Chef SecretStore resource: serverurl does not end with slash(/)"),
		},
		{
			store: makeSecretStore(name, baseURL, makeAuth(authName, authNamespace, "")),
			err:   errors.New("received invalid Chef SecretStore resource: missing Secret Key"),
		},
		{
			store: makeSecretStore(name, baseURL, makeAuth(authName, authNamespace, authKey)),
			err:   errors.New("received invalid Chef SecretStore resource: namespace should either be empty or match the namespace of the SecretStore for a namespaced SecretStore"),
		},
		{
			store: &esv1beta1.SecretStore{
				Spec: esv1beta1.SecretStoreSpec{
					Provider: nil,
				},
			},
			err: errors.New("received invalid Chef SecretStore resource: missing provider"),
		},
		{
			store: &esv1beta1.SecretStore{
				Spec: esv1beta1.SecretStoreSpec{
					Provider: &esv1beta1.SecretStoreProvider{
						Chef: nil,
					},
				},
			},
			err: errors.New("received invalid Chef SecretStore resource: missing chef provider"),
		},
	}
	pc := Providerchef{}
	for _, tc := range testCases {
		_, err := pc.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 TestNewClient(t *testing.T) {
	store := &esv1beta1.SecretStore{TypeMeta: metav1.TypeMeta{Kind: "ClusterSecretStore"},
		Spec: esv1beta1.SecretStoreSpec{
			Provider: &esv1beta1.SecretStoreProvider{
				Chef: &esv1beta1.ChefProvider{
					Auth:      makeAuth(authName, authNamespace, authKey),
					UserName:  name,
					ServerURL: baseURL,
				},
			},
		},
	}

	expected := fmt.Sprintf("could not fetch SecretKey Secret: secrets %q not found", authName)
	expectedMissingStore := "missing or invalid spec: missing store"

	ctx := context.TODO()

	kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "creds",
			Namespace: "default",
		}, TypeMeta: metav1.TypeMeta{
			Kind:       kind,
			APIVersion: apiversion,
		},
	}).Build()

	pc := Providerchef{databagService: &fake.ChefMockClient{}}
	_, errMissingStore := pc.NewClient(ctx, nil, kube, "default")
	if !ErrorContains(errMissingStore, expectedMissingStore) {
		t.Errorf("CheckNewClient unexpected error: %s, expected: '%s'", errMissingStore.Error(), expectedMissingStore)
	}
	_, err := pc.NewClient(ctx, store, kube, "default")
	if !ErrorContains(err, expected) {
		t.Errorf("CheckNewClient unexpected error: %s, expected: '%s'", err.Error(), expected)
	}
}

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

func TestValidate(t *testing.T) {
	pc := Providerchef{}
	var mockClient *fake.ChefMockClient
	pc.userService = mockClient
	pc.clientName = "correctUser"
	_, err := pc.Validate()
	t.Log("Error: ", err)
	pc.clientName = "wrongUser"
	_, err = pc.Validate()
	t.Log("Error: ", err)
}

func TestCapabilities(t *testing.T) {
	pc := Providerchef{}
	capabilities := pc.Capabilities()
	t.Log(capabilities)
}

// Test Cases To be added when Close function is implemented.
func TestClose(_ *testing.T) {
	pc := Providerchef{}
	pc.Close(context.Background())
}

// Test Cases To be added when GetAllSecrets function is implemented.
func TestGetAllSecrets(_ *testing.T) {
	pc := Providerchef{}
	pc.GetAllSecrets(context.Background(), esv1beta1.ExternalSecretFind{})
}

// Test Cases To be implemented when DeleteSecret function is implemented.
func TestDeleteSecret(_ *testing.T) {
	pc := Providerchef{}
	pc.DeleteSecret(context.Background(), nil)
}

// Test Cases To be implemented when PushSecret function is implemented.
func TestPushSecret(_ *testing.T) {
	pc := Providerchef{}
	pc.PushSecret(context.Background(), &corev1.Secret{}, nil)
}
