package cmd

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/ghodss/yaml"
	"github.com/spf13/cobra"
	flag "github.com/spf13/pflag"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/SAP/jenkins-library/pkg/config"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/SAP/jenkins-library/pkg/mock"
)

func resetEnv(e []string) {
	for _, val := range e {
		tmp := strings.Split(val, "=")
		os.Setenv(tmp[0], tmp[1])
	}
}

func TestAddRootFlags(t *testing.T) {
	var testRootCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
	addRootFlags(testRootCmd)

	assert.NotNil(t, testRootCmd.Flag("customConfig"), "expected flag not available")
	assert.NotNil(t, testRootCmd.Flag("defaultConfig"), "expected flag not available")
	assert.NotNil(t, testRootCmd.Flag("parametersJSON"), "expected flag not available")
	assert.NotNil(t, testRootCmd.Flag("stageName"), "expected flag not available")
	assert.NotNil(t, testRootCmd.Flag("stepConfigJSON"), "expected flag not available")
	assert.NotNil(t, testRootCmd.Flag("verbose"), "expected flag not available")

}

func TestAdoptStageNameFromParametersJSON(t *testing.T) {
	tt := []struct {
		name          string
		stageNameArg  string
		stageNameEnv  string
		stageNameJSON string
	}{
		{name: "no stage name", stageNameArg: "", stageNameEnv: "", stageNameJSON: ""},
		{name: "stage name arg+env", stageNameArg: "arg", stageNameEnv: "env", stageNameJSON: "json"},
		{name: "stage name env", stageNameArg: "", stageNameEnv: "env", stageNameJSON: "json"},
		{name: "stage name json", stageNameArg: "", stageNameEnv: "", stageNameJSON: "json"},
		{name: "stage name arg", stageNameArg: "arg", stageNameEnv: "", stageNameJSON: "json"},
	}

	for _, test := range tt {
		t.Run(test.name, func(t *testing.T) {
			// init
			defer resetEnv(os.Environ())
			os.Clearenv()

			//mock Jenkins env
			os.Setenv("JENKINS_HOME", "anything")
			require.NotEmpty(t, os.Getenv("JENKINS_HOME"))
			os.Setenv("STAGE_NAME", test.stageNameEnv)

			GeneralConfig.StageName = test.stageNameArg

			if test.stageNameJSON != "" {
				GeneralConfig.ParametersJSON = fmt.Sprintf("{\"stageName\":\"%s\"}", test.stageNameJSON)
			} else {
				GeneralConfig.ParametersJSON = "{}"
			}
			// test
			initStageName(false)

			// assert
			// Order of if-clauses reflects wanted precedence.
			if test.stageNameArg != "" {
				assert.Equal(t, test.stageNameArg, GeneralConfig.StageName)
			} else if test.stageNameJSON != "" {
				assert.Equal(t, test.stageNameJSON, GeneralConfig.StageName)
			} else if test.stageNameEnv != "" {
				assert.Equal(t, test.stageNameEnv, GeneralConfig.StageName)
			} else {
				assert.Equal(t, "", GeneralConfig.StageName)
			}
		})
	}
}

func TestPrepareConfig(t *testing.T) {
	defaultsBak := GeneralConfig.DefaultConfig
	GeneralConfig.DefaultConfig = []string{"testDefaults.yml"}
	defer func() { GeneralConfig.DefaultConfig = defaultsBak }()

	t.Run("using stepConfigJSON", func(t *testing.T) {
		stepConfigJSONBak := GeneralConfig.StepConfigJSON
		GeneralConfig.StepConfigJSON = `{"testParam": "testValueJSON"}`
		defer func() { GeneralConfig.StepConfigJSON = stepConfigJSONBak }()
		testOptions := mock.StepOptions{}
		var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
		testCmd.Flags().StringVar(&testOptions.TestParam, "testParam", "", "test usage")
		metadata := config.StepData{
			Spec: config.StepSpec{
				Inputs: config.StepInputs{
					Parameters: []config.StepParameters{
						{Name: "testParam", Scope: []string{"GENERAL"}},
					},
				},
			},
		}

		PrepareConfig(testCmd, &metadata, "testStep", &testOptions, mock.OpenFileMock)
		assert.Equal(t, "testValueJSON", testOptions.TestParam, "wrong value retrieved from config")
	})

	t.Run("using config files", func(t *testing.T) {
		t.Run("success case", func(t *testing.T) {
			testOptions := mock.StepOptions{}
			var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
			testCmd.Flags().StringVar(&testOptions.TestParam, "testParam", "", "test usage")
			metadata := config.StepData{
				Spec: config.StepSpec{
					Inputs: config.StepInputs{
						Parameters: []config.StepParameters{
							{Name: "testParam", Scope: []string{"GENERAL"}},
						},
					},
				},
			}

			err := PrepareConfig(testCmd, &metadata, "testStep", &testOptions, mock.OpenFileMock)
			assert.NoError(t, err, "no error expected but error occurred")

			//assert config
			assert.Equal(t, "testValue", testOptions.TestParam, "wrong value retrieved from config")

			//assert that flag has been marked as changed
			testCmd.Flags().VisitAll(func(pflag *flag.Flag) {
				if pflag.Name == "testParam" {
					assert.True(t, pflag.Changed, "flag should be marked as changed")
				}
			})
		})

		t.Run("error case", func(t *testing.T) {
			GeneralConfig.DefaultConfig = []string{"testDefaultsInvalid.yml"}
			testOptions := mock.StepOptions{}
			var testCmd = &cobra.Command{Use: "test", Short: "This is just a test"}
			metadata := config.StepData{}

			err := PrepareConfig(testCmd, &metadata, "testStep", &testOptions, mock.OpenFileMock)
			assert.Error(t, err, "error expected but none occurred")
		})
	})
}

func TestRetrieveHookConfig(t *testing.T) {
	tt := []struct {
		hookJSON           []byte
		expectedHookConfig HookConfiguration
	}{
		{hookJSON: []byte(""), expectedHookConfig: HookConfiguration{}},
		{hookJSON: []byte(`{"sentry":{"dsn":"https://my.sentry.dsn"}}`), expectedHookConfig: HookConfiguration{SentryConfig: SentryConfiguration{Dsn: "https://my.sentry.dsn"}}},
		{hookJSON: []byte(`{"sentry":{"dsn":"https://my.sentry.dsn"}, "splunk":{"dsn":"https://my.splunk.dsn", "token": "mytoken", "index": "myindex", "sendLogs": true}}`),
			expectedHookConfig: HookConfiguration{SentryConfig: SentryConfiguration{Dsn: "https://my.sentry.dsn"},
				SplunkConfig: SplunkConfiguration{
					Dsn:      "https://my.splunk.dsn",
					Token:    "mytoken",
					Index:    "myindex",
					SendLogs: true,
				},
			},
		},
	}

	for _, test := range tt {
		var target HookConfiguration
		var hookJSONinterface map[string]interface{}
		if len(test.hookJSON) > 0 {
			err := json.Unmarshal(test.hookJSON, &hookJSONinterface)
			assert.NoError(t, err)
		}
		retrieveHookConfig(hookJSONinterface, &target)
		assert.Equal(t, test.expectedHookConfig, target)
	}
}

func TestGetProjectConfigFile(t *testing.T) {

	tt := []struct {
		filename       string
		filesAvailable []string
		expected       string
	}{
		{filename: ".pipeline/config.yml", filesAvailable: []string{}, expected: ".pipeline/config.yml"},
		{filename: ".pipeline/config.yml", filesAvailable: []string{".pipeline/config.yml"}, expected: ".pipeline/config.yml"},
		{filename: ".pipeline/config.yml", filesAvailable: []string{".pipeline/config.yaml"}, expected: ".pipeline/config.yaml"},
		{filename: ".pipeline/config.yaml", filesAvailable: []string{".pipeline/config.yml", ".pipeline/config.yaml"}, expected: ".pipeline/config.yaml"},
		{filename: ".pipeline/config.yml", filesAvailable: []string{".pipeline/config.yml", ".pipeline/config.yaml"}, expected: ".pipeline/config.yml"},
	}

	for run, test := range tt {
		t.Run(fmt.Sprintf("Run %v", run), func(t *testing.T) {
			dir := t.TempDir()

			if len(test.filesAvailable) > 0 {
				configFolder := filepath.Join(dir, filepath.Dir(test.filesAvailable[0]))
				err := os.MkdirAll(configFolder, 0700)
				assert.NoError(t, err)
			}

			for _, file := range test.filesAvailable {
				if err := ioutil.WriteFile(filepath.Join(dir, file), []byte("general:"), 0700); err != nil {
					t.Fail()
				}
			}

			assert.Equal(t, filepath.Join(dir, test.expected), getProjectConfigFile(filepath.Join(dir, test.filename)))
		})
	}
}

func TestConvertTypes(t *testing.T) {
	t.Run("Converts strings to booleans", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		options := struct {
			Foo bool `json:"foo,omitempty"`
			Bar bool `json:"bar,omitempty"`
		}{}
		options.Foo = true
		options.Bar = false

		stepConfig := map[string]interface{}{}
		stepConfig["foo"] = "False"
		stepConfig["bar"] = "True"

		// Test
		stepConfig = checkTypes(stepConfig, options)

		confJSON, _ := json.Marshal(stepConfig)
		_ = json.Unmarshal(confJSON, &options)

		// Assert
		assert.Equal(t, false, stepConfig["foo"])
		assert.Equal(t, true, stepConfig["bar"])
		assert.Equal(t, false, options.Foo)
		assert.Equal(t, true, options.Bar)
		assert.False(t, hasFailed, "Expected checkTypes() NOT to exit via logging framework")
	})
	t.Run("Converts numbers to strings", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		options := struct {
			Foo string `json:"foo,omitempty"`
			Bar string `json:"bar,omitempty"`
		}{}

		stepConfig := map[string]interface{}{}
		stepConfig["foo"] = 1.5
		stepConfig["bar"] = 42

		// Test
		stepConfig = checkTypes(stepConfig, options)

		confJSON, _ := json.Marshal(stepConfig)
		_ = json.Unmarshal(confJSON, &options)

		// Assert
		assert.Equal(t, "1.5", stepConfig["foo"])
		assert.Equal(t, "42", stepConfig["bar"])
		assert.Equal(t, "1.5", options.Foo)
		assert.Equal(t, "42", options.Bar)
		assert.False(t, hasFailed, "Expected checkTypes() NOT to exit via logging framework")
	})
	t.Run("Keeps numbers", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		options := struct {
			Foo int     `json:"foo,omitempty"`
			Bar float32 `json:"bar,omitempty"`
		}{}

		stepConfig := map[string]interface{}{}

		content := []byte(`
foo: 1
bar: 42
`)
		err := yaml.Unmarshal(content, &stepConfig)
		assert.NoError(t, err)

		// Test
		stepConfig = checkTypes(stepConfig, options)

		confJSON, _ := json.Marshal(stepConfig)
		_ = json.Unmarshal(confJSON, &options)

		// Assert
		assert.Equal(t, 1, stepConfig["foo"])
		assert.Equal(t, float32(42.0), stepConfig["bar"])
		assert.Equal(t, 1, options.Foo)
		assert.Equal(t, float32(42.0), options.Bar)
		assert.False(t, hasFailed, "Expected checkTypes() NOT to exit via logging framework")
	})
	t.Run("Exits because string found, slice expected", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		options := struct {
			Foo []string `json:"foo,omitempty"`
		}{}

		stepConfig := map[string]interface{}{}
		stepConfig["foo"] = "entry"

		// Test
		_ = checkTypes(stepConfig, options)

		// Assert
		assert.True(t, hasFailed, "Expected checkTypes() to exit via logging framework")
	})
	t.Run("Exits because float found, int expected", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		options := struct {
			Foo int `json:"foo,omitempty"`
		}{}

		stepConfig := map[string]interface{}{}

		content := []byte("foo: 1.1")
		err := yaml.Unmarshal(content, &stepConfig)
		assert.NoError(t, err)

		// Test
		stepConfig = checkTypes(stepConfig, options)

		// Assert
		assert.Equal(t, 1.1, stepConfig["foo"])
		assert.True(t, hasFailed, "Expected checkTypes() to exit via logging framework")
	})
	t.Run("Exits in case number beyond length", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		options := struct {
			Foo string `json:"foo,omitempty"`
		}{}

		stepConfig := map[string]interface{}{}

		content := []byte("foo: 73554900100200011600")
		err := yaml.Unmarshal(content, &stepConfig)
		assert.NoError(t, err)

		// Test
		stepConfig = checkTypes(stepConfig, options)

		// Assert
		assert.True(t, hasFailed, "Expected checkTypes() to exit via logging framework")
	})
	t.Run("Properly handle small ints", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		options := struct {
			Foo string `json:"foo,omitempty"`
		}{}

		stepConfig := map[string]interface{}{}

		content := []byte("foo: 11")
		err := yaml.Unmarshal(content, &stepConfig)
		assert.NoError(t, err)

		// Test
		stepConfig = checkTypes(stepConfig, options)

		// Assert
		assert.False(t, hasFailed, "Expected checkTypes() NOT to exit via logging framework")
	})
	t.Run("Ignores nil values", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		options := struct {
			Foo []string `json:"foo,omitempty"`
			Bar string   `json:"bar,omitempty"`
		}{}

		stepConfig := map[string]interface{}{}
		stepConfig["foo"] = nil
		stepConfig["bar"] = nil

		// Test
		stepConfig = checkTypes(stepConfig, options)
		confJSON, _ := json.Marshal(stepConfig)
		_ = json.Unmarshal(confJSON, &options)

		// Assert
		assert.Nil(t, stepConfig["foo"])
		assert.Nil(t, stepConfig["bar"])
		assert.Equal(t, []string(nil), options.Foo)
		assert.Equal(t, "", options.Bar)
		assert.False(t, hasFailed, "Expected checkTypes() NOT to exit via logging framework")
	})
	t.Run("Logs warning for unknown type-mismatches", func(t *testing.T) {
		// Init
		hasFailed := false

		exitFunc := log.Entry().Logger.ExitFunc
		log.Entry().Logger.ExitFunc = func(int) {
			hasFailed = true
		}
		defer func() { log.Entry().Logger.ExitFunc = exitFunc }()

		logBuffer := new(bytes.Buffer)

		logOutput := log.Entry().Logger.Out
		log.Entry().Logger.Out = logBuffer
		defer func() { log.Entry().Logger.Out = logOutput }()

		options := struct {
			Foo string `json:"foo,omitempty"`
		}{}

		stepConfig := map[string]interface{}{}
		stepConfig["foo"] = true

		// Test
		stepConfig = checkTypes(stepConfig, options)
		confJSON, _ := json.Marshal(stepConfig)
		_ = json.Unmarshal(confJSON, &options)

		// Assert
		assert.Equal(t, true, stepConfig["foo"])
		assert.Equal(t, "", options.Foo)
		assert.Contains(t, logBuffer.String(), "The value may be ignored as a result")
		assert.False(t, hasFailed, "Expected checkTypes() NOT to exit via logging framework")
	})
}

func TestResolveAccessTokens(t *testing.T) {
	tt := []struct {
		description      string
		tokenList        []string
		expectedTokenMap map[string]string
	}{
		{description: "empty tokens", tokenList: []string{}, expectedTokenMap: map[string]string{}},
		{description: "invalid token", tokenList: []string{"onlyToken"}, expectedTokenMap: map[string]string{}},
		{description: "one token", tokenList: []string{"github.com:token1"}, expectedTokenMap: map[string]string{"github.com": "token1"}},
		{description: "more tokens", tokenList: []string{"github.com:token1", "github.corp:token2"}, expectedTokenMap: map[string]string{"github.com": "token1", "github.corp": "token2"}},
	}

	for _, test := range tt {
		assert.Equal(t, test.expectedTokenMap, ResolveAccessTokens(test.tokenList), test.description)
	}
}

func TestAccessTokensFromEnvJSON(t *testing.T) {
	tt := []struct {
		description       string
		inputJSON         string
		expectedTokenList []string
	}{
		{description: "empty ENV", inputJSON: "", expectedTokenList: []string{}},
		{description: "invalid JSON", inputJSON: "{", expectedTokenList: []string{}},
		{description: "empty JSON 1", inputJSON: "{}", expectedTokenList: []string{}},
		{description: "empty JSON 2", inputJSON: "[]]", expectedTokenList: []string{}},
		{description: "invalid JSON format", inputJSON: `{"test":"test"}`, expectedTokenList: []string{}},
		{description: "one token", inputJSON: `["github.com:token1"]`, expectedTokenList: []string{"github.com:token1"}},
		{description: "more tokens", inputJSON: `["github.com:token1","github.corp:token2"]`, expectedTokenList: []string{"github.com:token1", "github.corp:token2"}},
	}

	for _, test := range tt {
		assert.Equal(t, test.expectedTokenList, AccessTokensFromEnvJSON(test.inputJSON), test.description)
	}
}
