package cmd

import (
	"fmt"
	"os"
	"path"
	"testing"

	"github.com/SAP/jenkins-library/pkg/kubernetes/mocks"
	"github.com/SAP/jenkins-library/pkg/mock"
	"github.com/SAP/jenkins-library/pkg/piperenv"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type helmMockUtilsBundle struct {
	*mock.ExecMockRunner
	*mock.FilesMock
	*mock.HttpClientMock
}

func newHelmMockUtilsBundle() helmMockUtilsBundle {
	utils := helmMockUtilsBundle{
		ExecMockRunner: &mock.ExecMockRunner{},
		FilesMock:      &mock.FilesMock{},
		HttpClientMock: &mock.HttpClientMock{
			FileUploads: map[string]string{},
		},
	}
	return utils
}

func TestRunHelmUpgrade(t *testing.T) {
	t.Parallel()

	cpe := helmExecuteCommonPipelineEnvironment{}
	testTable := []struct {
		config         helmExecuteOptions
		methodError    error
		expectedErrStr string
	}{
		{
			config: helmExecuteOptions{
				HelmCommand: "upgrade",
			},
			methodError: nil,
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "upgrade",
			},
			methodError:    errors.New("some error"),
			expectedErrStr: "failed to execute upgrade: some error",
		},
	}

	for i, testCase := range testTable {
		t.Run(fmt.Sprint("case ", i), func(t *testing.T) {
			helmExecute := &mocks.HelmExecutor{}
			helmExecute.On("RunHelmUpgrade").Return(testCase.methodError)

			err := runHelmExecute(testCase.config, helmExecute, &cpe)
			if err != nil {
				assert.Equal(t, testCase.expectedErrStr, err.Error())
			}
		})

	}
}

func TestRunHelmLint(t *testing.T) {
	t.Parallel()

	cpe := helmExecuteCommonPipelineEnvironment{}
	testTable := []struct {
		config         helmExecuteOptions
		expectedConfig []string
		methodError    error
		expectedErrStr string
	}{
		{
			config: helmExecuteOptions{
				HelmCommand: "lint",
			},
			methodError: nil,
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "lint",
			},
			methodError:    errors.New("some error"),
			expectedErrStr: "failed to execute helm lint: some error",
		},
	}

	for i, testCase := range testTable {
		t.Run(fmt.Sprint("case ", i), func(t *testing.T) {
			helmExecute := &mocks.HelmExecutor{}
			helmExecute.On("RunHelmLint").Return(testCase.methodError)

			err := runHelmExecute(testCase.config, helmExecute, &cpe)
			if err != nil {
				assert.Equal(t, testCase.expectedErrStr, err.Error())
			}
		})

	}
}

func TestRunHelmInstall(t *testing.T) {
	t.Parallel()

	cpe := helmExecuteCommonPipelineEnvironment{}
	testTable := []struct {
		config         helmExecuteOptions
		expectedConfig []string
		methodError    error
		expectedErrStr string
	}{
		{
			config: helmExecuteOptions{
				HelmCommand: "install",
			},
			methodError: nil,
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "install",
			},
			methodError:    errors.New("some error"),
			expectedErrStr: "failed to execute helm install: some error",
		},
	}

	for i, testCase := range testTable {
		t.Run(fmt.Sprint("case ", i), func(t *testing.T) {
			helmExecute := &mocks.HelmExecutor{}
			helmExecute.On("RunHelmInstall").Return(testCase.methodError)

			err := runHelmExecute(testCase.config, helmExecute, &cpe)
			if err != nil {
				assert.Equal(t, testCase.expectedErrStr, err.Error())
			}
		})

	}
}

func TestRunHelmTest(t *testing.T) {
	t.Parallel()

	cpe := helmExecuteCommonPipelineEnvironment{}
	testTable := []struct {
		config         helmExecuteOptions
		methodError    error
		expectedErrStr string
	}{
		{
			config: helmExecuteOptions{
				HelmCommand: "test",
			},
			methodError: nil,
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "test",
			},
			methodError:    errors.New("some error"),
			expectedErrStr: "failed to execute helm test: some error",
		},
	}

	for i, testCase := range testTable {
		t.Run(fmt.Sprint("case ", i), func(t *testing.T) {
			helmExecute := &mocks.HelmExecutor{}
			helmExecute.On("RunHelmTest").Return(testCase.methodError)

			err := runHelmExecute(testCase.config, helmExecute, &cpe)
			if err != nil {
				assert.Equal(t, testCase.expectedErrStr, err.Error())
			}
		})

	}
}

func TestRunHelmUninstall(t *testing.T) {
	t.Parallel()

	cpe := helmExecuteCommonPipelineEnvironment{}
	testTable := []struct {
		config         helmExecuteOptions
		methodError    error
		expectedErrStr string
	}{
		{
			config: helmExecuteOptions{
				HelmCommand: "uninstall",
			},
			methodError: nil,
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "uninstall",
			},
			methodError:    errors.New("some error"),
			expectedErrStr: "failed to execute helm uninstall: some error",
		},
	}

	for i, testCase := range testTable {
		t.Run(fmt.Sprint("case ", i), func(t *testing.T) {
			helmExecute := &mocks.HelmExecutor{}
			helmExecute.On("RunHelmUninstall").Return(testCase.methodError)

			err := runHelmExecute(testCase.config, helmExecute, &cpe)
			if err != nil {
				assert.Equal(t, testCase.expectedErrStr, err.Error())
			}
		})

	}
}

func TestRunHelmDependency(t *testing.T) {
	t.Parallel()

	cpe := helmExecuteCommonPipelineEnvironment{}
	testTable := []struct {
		config         helmExecuteOptions
		methodError    error
		expectedErrStr string
	}{
		{
			config: helmExecuteOptions{
				HelmCommand: "dependency",
			},
			methodError: nil,
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "dependency",
			},
			methodError:    errors.New("some error"),
			expectedErrStr: "failed to execute helm dependency: some error",
		},
	}

	for i, testCase := range testTable {
		t.Run(fmt.Sprint("case ", i), func(t *testing.T) {
			helmExecute := &mocks.HelmExecutor{}
			helmExecute.On("RunHelmDependency").Return(testCase.methodError)

			err := runHelmExecute(testCase.config, helmExecute, &cpe)
			if err != nil {
				assert.Equal(t, testCase.expectedErrStr, err.Error())
			}
		})

	}
}

func TestRunHelmPush(t *testing.T) {
	t.Parallel()

	cpe := helmExecuteCommonPipelineEnvironment{}
	testTable := []struct {
		config         helmExecuteOptions
		methodString   string
		methodError    error
		expectedErrStr string
	}{
		{
			config: helmExecuteOptions{
				HelmCommand: "publish",
			},
			methodString: "https://my.target.repository",
			methodError:  nil,
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "publish",
			},
			methodError:    errors.New("some error"),
			expectedErrStr: "failed to execute helm publish: some error",
		},
	}

	for i, testCase := range testTable {
		t.Run(fmt.Sprint("case ", i), func(t *testing.T) {
			helmExecute := &mocks.HelmExecutor{}
			helmExecute.On("RunHelmPublish").Return(testCase.methodString, testCase.methodError)

			err := runHelmExecute(testCase.config, helmExecute, &cpe)
			if err != nil {
				assert.Equal(t, testCase.expectedErrStr, err.Error())
			}
		})

	}
}

func TestRunHelmDefaultCommand(t *testing.T) {
	t.Parallel()

	cpe := helmExecuteCommonPipelineEnvironment{}
	testTable := []struct {
		config             helmExecuteOptions
		methodLintError    error
		methodPackageError error
		methodPublishError error
		expectedErrStr     string
	}{
		{
			config: helmExecuteOptions{
				HelmCommand: "",
			},
			methodLintError:    nil,
			methodPackageError: nil,
			methodPublishError: nil,
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "",
			},
			methodLintError: errors.New("some error"),
			expectedErrStr:  "failed to execute helm lint: some error",
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "",
			},
			methodPackageError: errors.New("some error"),
			expectedErrStr:     "failed to execute helm dependency: some error",
		},
		{
			config: helmExecuteOptions{
				HelmCommand: "",
			},
			methodPublishError: errors.New("some error"),
			expectedErrStr:     "failed to execute helm publish: some error",
		},
	}

	for i, testCase := range testTable {
		t.Run(fmt.Sprint("case ", i), func(t *testing.T) {
			helmExecute := &mocks.HelmExecutor{}
			helmExecute.On("RunHelmLint").Return(testCase.methodLintError)
			helmExecute.On("RunHelmDependency").Return(testCase.methodPackageError)
			helmExecute.On("RunHelmPublish").Return(testCase.methodPublishError)

			err := runHelmExecute(testCase.config, helmExecute, &cpe)
			if err != nil {
				assert.Equal(t, testCase.expectedErrStr, err.Error())
			}

		})
	}

}

func TestParseAndRenderCPETemplate(t *testing.T) {
	commonPipelineEnvironment := "commonPipelineEnvironment"
	valuesYaml := []byte(`
image: "image_1"
tag: {{ cpe "artifactVersion" }}
`)
	values1Yaml := []byte(`
image: "image_2"
tag: {{ cpe "artVersion" }}
`)
	values3Yaml := []byte(`
image: "image_3"
tag: {{ .CPE.artVersion
`)
	values4Yaml := []byte(`
image: "test-image"
tag: {{ imageTag "test-image" }}
`)

	tmpDir := t.TempDir()
	require.DirExists(t, tmpDir)
	err := os.Mkdir(path.Join(tmpDir, commonPipelineEnvironment), 0700)
	require.NoError(t, err)
	cpe := piperenv.CPEMap{
		"artifactVersion":         "1.0.0-123456789",
		"container/imageNameTags": []string{"test-image:1.0.0-123456789"},
	}
	err = cpe.WriteToDisk(tmpDir)
	require.NoError(t, err)

	defaultValueFile := "values.yaml"
	config := helmExecuteOptions{
		ChartPath: ".",
	}

	tt := []struct {
		name             string
		defaultValueFile string
		config           helmExecuteOptions
		expectedErr      error
		valueFile        []byte
	}{
		{
			name:             "'artifactVersion' file exists in CPE",
			defaultValueFile: defaultValueFile,
			config:           config,
			expectedErr:      nil,
			valueFile:        valuesYaml,
		},
		{
			name:             "'artVersion' file does not exist in CPE",
			defaultValueFile: defaultValueFile,
			config:           config,
			expectedErr:      nil,
			valueFile:        values1Yaml,
		},
		{
			name:             "Good template ({{ imageTag 'test-image' }})",
			defaultValueFile: defaultValueFile,
			config:           config,
			expectedErr:      nil,
			valueFile:        values4Yaml,
		},
		{
			name:             "Wrong template ({{ .CPE.artVersion)",
			defaultValueFile: defaultValueFile,
			config:           config,
			expectedErr:      fmt.Errorf("failed to parse template: failed to parse cpe template '\nimage: \"image_3\"\ntag: {{ .CPE.artVersion\n': template: cpetemplate:4: unclosed action started at cpetemplate:3"),
			valueFile:        values3Yaml,
		},
		{
			name:             "Multiple value files",
			defaultValueFile: defaultValueFile,
			config: helmExecuteOptions{
				ChartPath:  ".",
				HelmValues: []string{"./values_1.yaml", "./values_2.yaml"},
			},
			expectedErr: nil,
			valueFile:   valuesYaml,
		},
		{
			name:             "No value file is provided",
			defaultValueFile: "",
			config: helmExecuteOptions{
				ChartPath:  ".",
				HelmValues: []string{},
			},
			expectedErr: fmt.Errorf("no value file to proccess, please provide value file(s)"),
			valueFile:   valuesYaml,
		},
		{
			name:             "Wrong path to value file",
			defaultValueFile: defaultValueFile,
			config: helmExecuteOptions{
				ChartPath:  ".",
				HelmValues: []string{"wrong/path/to/values_1.yaml"},
			},
			expectedErr: fmt.Errorf("failed to read file: could not read 'wrong/path/to/values_1.yaml'"),
			valueFile:   valuesYaml,
		},
	}

	for _, test := range tt {
		t.Run(test.name, func(t *testing.T) {
			utils := newHelmMockUtilsBundle()
			utils.AddFile(fmt.Sprintf("%s/%s", config.ChartPath, test.defaultValueFile), test.valueFile)

			if len(test.config.HelmValues) == 2 {
				for _, value := range test.config.HelmValues {
					utils.AddFile(value, test.valueFile)
				}
			}

			err := parseAndRenderCPETemplate(test.config, tmpDir, utils)
			if test.expectedErr != nil {
				assert.EqualError(t, err, test.expectedErr.Error())
			} else {
				assert.NoError(t, err)
			}
		})
	}
}
