//go:build e2e && !windows

package e2e

import (
	"context"
	"fmt"
	"path"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/rand"

	"gotest.tools/v3/assert"
	"knative.dev/func/test/common"
	"knative.dev/func/test/testhttp"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"

	"knative.dev/func/pkg/k8s"
)

// setupConfigVolumesTest add to cluster config maps and secrets that will be used as volumes
// during tests
func setupConfigVolumesTest(t *testing.T) {

	config, err := k8s.GetClientConfig().ClientConfig()
	if err != nil {
		t.Fatal(err)
	}
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		t.Fatal(err)
	}
	namespace, _, _ := k8s.GetClientConfig().Namespace()

	// Add Config Map
	configMap := corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{Name: "test-cm-volume"},
		Data: map[string]string{
			"config-key1": "Hi",
			"config-key2": "Hello",
		},
	}
	_, err = clientset.CoreV1().ConfigMaps(namespace).Create(context.Background(), &configMap, metav1.CreateOptions{})
	if err != nil {
		t.Fatal(err)
	}

	// Add Secret
	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{Name: "test-secret-volume"},
		Data: map[string][]byte{
			"secret-key1": []byte("pw1"),
			"secret-key2": []byte("pw2"),
		},
	}
	_, err = clientset.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{})
	if err != nil {
		t.Fatal(err)
	}

}

// tearDownConfigVolumesTest removes cluster config maps and secrets used by the test
func tearDownConfigVolumesTest() {

	config, _ := k8s.GetClientConfig().ClientConfig()
	clientset, _ := kubernetes.NewForConfig(config)
	namespace, _, _ := k8s.GetClientConfig().Namespace()

	_ = clientset.CoreV1().ConfigMaps(namespace).Delete(context.Background(), "test-cm-volume", metav1.DeleteOptions{})
	_ = clientset.CoreV1().Secrets(namespace).Delete(context.Background(), "test-secret-volume", metav1.DeleteOptions{})

}

// ConfigVolumesAdd generates a go function to test `func config volumes add` with user input
func ConfigVolumesAdd(knFunc *common.TestInteractiveCmd) func(userInput ...string) {
	return PrepareInteractiveCommand(knFunc, "config", "volumes", "add")
}

// ConfigVolumesRemove generates a go function to test `func config volumes remove` with user input
func ConfigVolumesRemove(knFunc *common.TestInteractiveCmd) func(userInput ...string) {
	return PrepareInteractiveCommand(knFunc, "config", "volumes", "remove")
}

// TestConfigVolumes verifies configMaps and secrets were properly mounted as volumes and accessible to the function
// Test consist reproduce the user experience to add volumes (both config and secrets) and deploy a function that
// makes use of the data/
// It setup "configMaps" and "secrets" on the cluster. A custom kn Function template (from a remote repository)
// is used to validate the data can be accessed from the deployed function perspective.
func TestConfigVolumes(t *testing.T) {

	setupConfigVolumesTest(t)
	defer tearDownConfigVolumesTest()

	knFunc := common.NewTestShellInteractiveCmd(t)
	knFunc.TestCmd.ShouldDumpOnSuccess = false
	knFunc.CommandSleepInterval = time.Millisecond * 1500

	// On When...
	funcName := "test-config-volumes"
	funcPath := filepath.Join(t.TempDir(), funcName)

	_, thisfile, _, _ := runtime.Caller(0)
	testTemplateFolder := path.Join(path.Dir(thisfile), "..", "templates")

	knFunc.TestCmd.Exec("create",
		"--language", "go",
		"--template", "testvolumes",
		"--repository", "file://"+testTemplateFolder,
		funcPath)
	knFunc.TestCmd.SourceDir = funcPath

	/*
		? What do you want to mount as a Volume?  [Use arrows to move, type to filter]
		> ConfigMap
		  Secret
	*/
	configVolumesAdd := ConfigVolumesAdd(knFunc)

	configVolumesAdd(
		enter,                   // > ConfigMap
		"test-cm-volume", enter, // Which "ConfigMap" do you want to mount?
		"/test/cm-volume", enter) // Please specify the path where the ConfigMap should be mounted:

	configVolumesAdd(
		arrowDown, enter, // > Secret
		"test-secret-volume", enter, // Which "Secret" do you want to mount?
		"/test/secret-volume", enter) // Please specify the path where the Secret should be mounted:

	// Adding unwanted volume entries (to simulate user mistakes)
	configVolumesAdd(
		enter,
		"test-cm-volume", enter,
		"/test/bad-cm", enter)

	configVolumesAdd(
		arrowDown, enter,
		"test-secret-volume", enter,
		"/test/bad-secret", enter)

	// Delete unwanted entries
	configVolumesRemove := ConfigVolumesRemove(knFunc)
	configVolumesRemove("/bad-secret", enter)
	configVolumesRemove("/bad-cm", enter)

	// Deploy
	knFunc.TestCmd.Exec("deploy", "--registry", common.GetRegistry())
	defer knFunc.TestCmd.Exec("delete")
	_, functionUrl := common.WaitForFunctionReady(t, funcName)

	// Validate
	// The function template used by this test will return
	// file content for the file specified as a query parameter named 'v'
	expectedMap := map[string]string{
		"/test/cm-volume/config-key1":     "Hi",
		"/test/cm-volume/config-key2":     "Hello",
		"/test/secret-volume/secret-key1": "pw1",
		"/test/secret-volume/secret-key2": "pw2",
		"/test/bad-cm/config-key1":        "",
		"/test/bad-secret/secret-key1":    "",
	}
	//functionRespValidator := FuncResponsivenessValidator{}
	for expectedVolumeEntry, expectedFileContent := range expectedMap {
		targetUrl := fmt.Sprintf("%s?v=%s", functionUrl, expectedVolumeEntry)
		_, funcResponse := testhttp.TestGet(t, targetUrl)
		assert.Assert(t, funcResponse == expectedFileContent)
	}

}

// enableKnativeVolumeExtentions ensures EmptyDir and PersitentVolumeClaim can be used with knative/functions. More at:
// https://knative.dev/docs/serving/configuration/feature-flags/#kubernetes-emptydir-volume
// https://knative.dev/docs/serving/configuration/feature-flags/#kubernetes-persistentvolumeclaim-pvc
func enableKnativeVolumeExtension(t *testing.T) {
	config, _ := k8s.GetClientConfig().ClientConfig()
	client, _ := kubernetes.NewForConfig(config)
	namespace := "knative-serving"

	// Enable EmptyDir extended feature for Knative
	_, err := client.CoreV1().ConfigMaps(namespace).Patch(context.Background(), "config-features", types.MergePatchType,
		[]byte(`{"data":{"kubernetes.podspec-volumes-emptydir":"enabled"}}`),
		metav1.PatchOptions{})

	if err != nil {
		t.Fatal(err)
	}

	// Enable Persistent Volumes Claim extended feature for Knative
	_, err = client.CoreV1().ConfigMaps(namespace).Patch(context.Background(), "config-features", types.MergePatchType,
		[]byte(`{"data":{"kubernetes.podspec-persistent-volume-claim":"enabled","kubernetes.podspec-persistent-volume-write":"enabled"}}`),
		metav1.PatchOptions{})

	if err != nil {
		t.Fatal(err)
	}

	t.Log("Enabled Knative PVC and EmptyDir extensions")

}

// setupTestPvc adds a test Persistent Volume Claim used by PVC test
func setupTestPvc(t *testing.T, pvcName string) {
	config, _ := k8s.GetClientConfig().ClientConfig()
	client, _ := kubernetes.NewForConfig(config)
	namespace, _, _ := k8s.GetClientConfig().Namespace()

	// Add Testing PVC
	pvc := &corev1.PersistentVolumeClaim{
		ObjectMeta: metav1.ObjectMeta{Name: pvcName},
		Spec: corev1.PersistentVolumeClaimSpec{
			AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
			Resources: corev1.VolumeResourceRequirements{
				Requests: corev1.ResourceList{},
			},
		},
	}
	pvc.Spec.Resources.Requests[corev1.ResourceStorage] = resource.MustParse("1Mi")
	_, err := client.CoreV1().PersistentVolumeClaims(namespace).Create(context.Background(), pvc, metav1.CreateOptions{})
	if err != nil {
		t.Fatal(err)
	}

	t.Log("Created test PVC " + pvcName)

	t.Cleanup(func() {
		client.CoreV1().PersistentVolumeClaims(namespace).Delete(context.Background(), pvcName, metav1.DeleteOptions{})
	})

}

// TestConfigVolumesPvcEmptyDir verifies PersistentVolumeClaim and EmptyDir Volumes can be added and can be accessible
// by the function by writing and reading an arbitrary file on the volumes
func TestConfigVolumesPvcEmptyDir(t *testing.T) {

	enableKnativeVolumeExtension(t)
	pvcName := "test-pvc-" + rand.String(5)
	setupTestPvc(t, pvcName)

	knFunc := common.NewTestShellInteractiveCmd(t)
	knFunc.TestCmd.ShouldDumpOnSuccess = false
	knFunc.CommandSleepInterval = time.Millisecond * 1500

	// On When...
	funcName := "test-config-vol-pvc"
	funcPath := filepath.Join(t.TempDir(), funcName)

	_, thisfile, _, _ := runtime.Caller(0)
	testTemplateFolder := path.Join(path.Dir(thisfile), "..", "templates")

	knFunc.TestCmd.Exec("create",
		"--language", "go",
		"--template", "testvolumes",
		"--repository", "file://"+testTemplateFolder,
		funcPath)
	knFunc.TestCmd.SourceDir = funcPath

	/*
		? What do you want to mount as a Volume?  [Use arrows to move, type to filter]
	*/
	configVolumesAdd := ConfigVolumesAdd(knFunc)

	configVolumesAdd(
		"PersistentVolumeClaim", enter,
		pvcName, enter, // ? Which "PersistentVolumeClaim" do you want to mount?
		"/test/pvc", enter, // ? Please specify the path where the PersistentVolumeClaim should be mounted:
		"N", enter) // ? Is this volume read-only? (y/N)

	configVolumesAdd(
		"EmptyDir", enter,
		"/test/empty-dir", enter) // ? Please specify the path where the EmptyDir should be mounted:

	// Deploy

	knFunc.TestCmd.Exec("deploy", "--registry", common.GetRegistry())
	t.Cleanup(func() {
		knFunc.TestCmd.Exec("delete")
	})
	_, functionUrl := common.WaitForFunctionReady(t, funcName)

	// Validation
	// The function template used by this test will help by read/write a file to the volume
	filesPath := []string{"/test/pvc/a.txt", "/test/empty-dir/a.txt"}
	fileContentToWrite := "A_CONTENT"

	for _, filePath := range filesPath {
		// - Write to volume test
		targetUrl := fmt.Sprintf("%s?v=%s&w=%s", functionUrl, filePath, fileContentToWrite)
		_, funcResponse := testhttp.TestGet(t, targetUrl)
		assert.Assert(t, funcResponse == fileContentToWrite, "Write volume test failed writing to file %s", filePath)

		// - Read from volume test
		targetUrl = fmt.Sprintf("%s?v=%s", functionUrl, filePath)
		_, funcResponse = testhttp.TestGet(t, targetUrl)
		assert.Assert(t, funcResponse == fileContentToWrite,
			"Read volume test failed. File %s content should be %s but it is %s", filePath, fileContentToWrite, funcResponse)
	}
}
