/*
Copyright 2019 The Rook Authors. All rights reserved.

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 util

import (
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"testing"

	"github.com/rook/rook/pkg/operator/k8sutil"
	"github.com/stretchr/testify/assert"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/fake"
)

func TestCommandMarshallingUnmarshalling(t *testing.T) {
	type args struct {
		cmd  []string
		args []string
	}
	tests := []struct {
		name string
		args args
	}{
		{name: "one command only", args: args{cmd: []string{"one"}, args: []string{}}},
		{name: "no command or args", args: args{
			cmd: []string{}, args: []string{}}},
		{name: "no command w/ args", args: args{
			cmd: []string{}, args: []string{"arg1", "arg2"}}},
		{name: "one command w/ args", args: args{
			cmd: []string{"one"}, args: []string{"arg1", "arg2"}}},
		{name: "multi command only", args: args{
			cmd: []string{"one", "two", "three"}, args: []string{}}},
		{name: "multi command and arg", args: args{
			cmd: []string{"one", "two", "three"}, args: []string{"arg1", "arg2", "--", "arg3"}}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			flagArg, err1 := CommandToCmdReporterFlagArgument(tt.args.cmd, tt.args.args)
			if err1 != nil {
				t.Errorf("CommandToFlagArgument() error = %+v, wanted no err", err1)
			}
			cmd, args, err2 := CmdReporterFlagArgumentToCommand(flagArg)
			if err2 != nil {
				t.Errorf("FlagArgumentToCommand() error = %+v, wanted no err", err2)
			}
			if err1 != nil || err2 != nil {
				return
			}
			assert.Equal(t, tt.args.cmd, cmd)
			assert.Equal(t, tt.args.args, args)
		})
	}
}

func TestNew(t *testing.T) {
	client := fake.NewSimpleClientset
	type fields struct {
		clientset     kubernetes.Interface
		cmd           []string
		args          []string
		configMapName string
		namespace     string
	}
	tests := []struct {
		name    string
		fields  fields
		wantErr bool
	}{
		{"all is well", fields{client(), []string{"cmd"}, []string{"args"}, "myConfigMap", "myNamespace"}, false},
		{"no k8s client", fields{nil, []string{"cmd"}, []string{"args"}, "myConfigMap", "myNamespace"}, true},
		{"no command", fields{client(), []string{}, []string{"args"}, "myConfigMap", "myNamespace"}, true},
		{"empty command", fields{client(), []string{""}, []string{"args"}, "myConfigMap", "myNamespace"}, true},
		{"three commands", fields{client(), []string{"one", "two", "three"}, []string{"args"}, "myConfigMap", "myNamespace"}, false},
		{"no args", fields{client(), []string{"cmd"}, []string{}, "myConfigMap", "myNamespace"}, false},
		{"empty arg", fields{client(), []string{"cmd"}, []string{""}, "myConfigMap", "myNamespace"}, false}, // an empty arg can still be valid
		{"three args", fields{client(), []string{"cmd"}, []string{"arg1", "arg2", "arg3"}, "myConfigMap", "myNamespace"}, false},
		{"no configmap name", fields{client(), []string{"cmd"}, []string{"args"}, "", "myNamespace"}, true},
		{"no namespace", fields{client(), []string{"cmd"}, []string{"args"}, "myConfigMap", ""}, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r, err := NewCmdReporter(
				tt.fields.clientset,
				tt.fields.cmd,
				tt.fields.args,
				tt.fields.configMapName,
				tt.fields.namespace,
			)
			if (err != nil) != tt.wantErr {
				t.Errorf("Runner.Run() error = %v, wantErr %v", err, tt.wantErr)
			}
			if err == nil {
				assert.NotNil(t, r)
				assert.Equal(t, tt.fields.clientset, r.clientset)
				assert.Equal(t, tt.fields.cmd, r.cmd)
				assert.Equal(t, tt.fields.args, r.args)
				assert.Equal(t, tt.fields.configMapName, r.configMapName)
				assert.Equal(t, tt.fields.namespace, r.namespace)
			}
		})
	}
}

func TestRunner_Run(t *testing.T) {
	origExecCommand := execCommand
	execCommand = mockExecCommand
	defer func() { execCommand = origExecCommand }()

	newClient := fake.NewSimpleClientset

	verifyConfigMap := func(client kubernetes.Interface, stdout, stderr, retval, cmName, namespace string) {
		cm, err := client.CoreV1().ConfigMaps(namespace).Get(cmName, metav1.GetOptions{})
		fmt.Println("configmap:", cm)
		assert.NoError(t, err)
		assert.Equal(t, stdout, cm.Data[CmdReporterConfigMapStdoutKey])
		assert.Equal(t, stderr, cm.Data[CmdReporterConfigMapStderrKey])
		assert.Equal(t, retval, cm.Data[CmdReporterConfigMapRetcodeKey])
	}

	verifyCommand := func(cmd, args []string, command string) {
		err := os.Setenv("GO_HELPER_PROCESS_PRINT_COMMAND", "1")
		assert.NoError(t, err)
		defer func() { os.Unsetenv("GO_HELPER_PROCESS_PRINT_COMMAND") }()

		k8s := newClient()
		r, err := NewCmdReporter(k8s, cmd, args, "command-configmap", "command-namespace")
		assert.NoError(t, err)
		assert.NoError(t, r.Run())

		verifyConfigMap(k8s, command, "", "0", "command-configmap", "command-namespace")
	}

	verifyCommand([]string{"grep"}, []string{"-e", ".*time"}, "grep -e .*time")
	verifyCommand([]string{"ceph-volume", "inventory"}, []string{"--format=json-pretty"}, "ceph-volume inventory --format=json-pretty")
	verifyCommand([]string{"ceph-volume", "lvm", "list"}, []string{}, "ceph-volume lvm list")

	verifyOutputs := func(stdout, stderr, retcode string) {
		err := os.Setenv("GO_HELPER_PROCESS_STDOUT", stdout)
		assert.NoError(t, err)
		defer func() { os.Unsetenv("GO_HELPER_PROCESS_STDOUT") }()
		err = os.Setenv("GO_HELPER_PROCESS_STDERR", stderr)
		assert.NoError(t, err)
		defer func() { os.Unsetenv("GO_HELPER_PROCESS_STDERR") }()
		err = os.Setenv("GO_HELPER_PROCESS_RETCODE", retcode)
		assert.NoError(t, err)
		defer func() { os.Unsetenv("GO_HELPER_PROCESS_RETCODE") }()

		k8s := newClient()
		r, err := NewCmdReporter(k8s, []string{"standin-cmd"}, []string{"--some", "arg"}, "outputs-configmap", "outputs-namespace")
		assert.NoError(t, err)
		assert.NoError(t, r.Run())

		verifyConfigMap(k8s, stdout, stderr, retcode, "outputs-configmap", "outputs-namespace")
	}

	verifyOutputs("", "", "0")
	verifyOutputs("", "", "11")
	verifyOutputs("this is my stdout", "", "0")
	verifyOutputs("", "this is my stderr", "23")
	verifyOutputs("this is ...", "... mixed outputs", "23")

	// Verify that cmd-reporter won't overwrite a preexisting configmap with a different app name
	k8s := newClient()
	cm := &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "preexisting-configmap",
			Namespace: "preexisting-namespace",
			Labels: map[string]string{
				k8sutil.AppAttr: "some-other-application",
			},
		},
		Data: map[string]string{},
	}
	k8s.CoreV1().ConfigMaps("preexisting-namespace").Create(cm)
	r, err := NewCmdReporter(k8s, []string{"some-command"}, []string{"some", "args"}, "preexisting-configmap", "preexisting-namespace")
	assert.NoError(t, err)
	assert.Error(t, r.Run())
	cm, err = k8s.CoreV1().ConfigMaps("preexisting-namespace").Get("preexisting-configmap", metav1.GetOptions{})
	assert.NoError(t, err)
	assert.NotContains(t, cm.Data, CmdReporterConfigMapStdoutKey)
	assert.NotContains(t, cm.Data, CmdReporterConfigMapStderrKey)
	assert.NotContains(t, cm.Data, CmdReporterConfigMapRetcodeKey)

	// Verify that cmd-reporter WILL overwrite a preexisting configmap with cmd-reporter's app name
	k8s = newClient()
	cm = &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "preexisting-configmap",
			Namespace: "preexisting-namespace",
			Labels: map[string]string{
				k8sutil.AppAttr: CmdReporterAppName,
			},
		},
		Data: map[string]string{},
	}
	k8s.CoreV1().ConfigMaps("preexisting-namespace").Create(cm)
	r, err = NewCmdReporter(k8s, []string{"some-command"}, []string{"some", "args"}, "preexisting-configmap", "preexisting-namespace")
	assert.NoError(t, err)
	assert.NoError(t, r.Run())
	verifyConfigMap(k8s, "", "", "0", "preexisting-configmap", "preexisting-namespace")
}

// Inspired by: https://github.com/golang/go/blob/master/src/os/exec/exec_test.go
func mockExecCommand(command string, args ...string) *exec.Cmd {
	cs := []string{"-test.run=TestCmdReporterHelperProcess", "--", command}
	cs = append(cs, args...)
	cmd := exec.Command(os.Args[0], cs...)
	// the existing environment will contain variables which define the desired return from the
	// fake command which will be run.
	cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
	return cmd
}

// TestHelperProcess isn't a real test. It's used as a helper process
// for TestParameterRun.
// Inspired by: https://github.com/golang/go/blob/master/src/os/exec/exec_test.go
func TestCmdReporterHelperProcess(*testing.T) {
	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
		return
	}

	args := os.Args
	for len(args) > 0 {
		if args[0] == "--" {
			args = args[1:]
			break
		}
		args = args[1:]
	}
	if len(args) == 0 {
		fmt.Fprintf(os.Stderr, "No command\n")
		os.Exit(2)
	}
	userCommand := args

	// test should set these in its environment to control the output of the test commands
	stdout := os.Getenv("GO_HELPER_PROCESS_STDOUT")
	stderr := os.Getenv("GO_HELPER_PROCESS_STDERR")
	retcode := os.Getenv("GO_HELPER_PROCESS_RETCODE")

	if os.Getenv("GO_HELPER_PROCESS_PRINT_COMMAND") == "1" {
		stdout = strings.Join(userCommand, " ")
		stderr = ""
		retcode = ""
	}

	if stdout != "" {
		fmt.Fprintf(os.Stdout, stdout)
	}
	if stderr != "" {
		fmt.Fprintf(os.Stderr, stderr)
	}
	if retcode != "" {
		rc, err := strconv.Atoi(retcode)
		if err != nil {
			panic(err)
		}
		os.Exit(rc)
	}
	os.Exit(0)
}
