package whitesource

import (
	"encoding/json"
	"fmt"
	"path/filepath"
	"testing"

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

func TestExecuteScanNPM(t *testing.T) {
	config := ScanOptions{
		ScanType:    "npm",
		OrgToken:    "org-token",
		UserToken:   "user-token",
		ProductName: "mock-product",
		ProjectName: "mock-project",
	}

	t.Parallel()

	t.Run("happy path NPM", func(t *testing.T) {
		// init
		utilsMock := NewScanUtilsMock()
		utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
		scan := newTestScan(&config)
		// test
		err := scan.ExecuteNpmScan(&config, utilsMock)
		// assert
		require.NoError(t, err)
		expectedCalls := []mock.ExecCall{
			{
				Exec: "npm",
				Params: []string{
					"ls",
				},
			},
			{
				Exec: "npx",
				Params: []string{
					"whitesource",
					"run",
				},
			},
		}
		assert.Equal(t, expectedCalls, utilsMock.Calls)
		assert.True(t, utilsMock.HasWrittenFile(whiteSourceConfig))
		assert.True(t, utilsMock.HasRemovedFile(whiteSourceConfig))
	})
	t.Run("happy path with excluded modules", func(t *testing.T) {
		// init
		utilsMock := NewScanUtilsMock()
		utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
		utilsMock.AddFile("sub/package.json", []byte(`{"name":"my-sub-module-name"}`))
		utilsMock.AddFile("deep/sub/package.json", []byte(`{"name":"my-deep-sub-module-name"}`))

		config := ScanOptions{
			ScanType:                   "npm",
			OrgToken:                   "org-token",
			UserToken:                  "user-token",
			ProductName:                "mock-product",
			ProjectName:                "mock-project",
			BuildDescriptorExcludeList: []string{"unrelated/pom.xml", "sub/package.json", "deep/sub/package.json"},
		}

		scan := newTestScan(&config)
		// test
		err := scan.ExecuteNpmScan(&config, utilsMock)
		// assert
		require.NoError(t, err)
		expectedCalls := []mock.ExecCall{
			{
				Exec: "npm",
				Params: []string{
					"ls",
				},
			},
			{
				Exec: "npx",
				Params: []string{
					"whitesource",
					"run",
				},
			},
		}
		assert.Equal(t, expectedCalls, utilsMock.Calls)
		assert.True(t, utilsMock.HasWrittenFile(whiteSourceConfig))
		assert.True(t, utilsMock.HasRemovedFile(whiteSourceConfig))
		assert.False(t, utilsMock.HasWrittenFile(filepath.Join("sub", whiteSourceConfig)))
		assert.False(t, utilsMock.HasWrittenFile(filepath.Join("deep", "sub", whiteSourceConfig)))
	})
	t.Run("no NPM modules", func(t *testing.T) {
		// init
		utilsMock := NewScanUtilsMock()
		scan := newTestScan(&config)
		// test
		err := scan.ExecuteNpmScan(&config, utilsMock)
		// assert
		assert.EqualError(t, err, "found no NPM modules to scan. Configured excludes: []")
		assert.Len(t, utilsMock.Calls, 0)
		assert.False(t, utilsMock.HasWrittenFile(whiteSourceConfig))
	})
	t.Run("package.json needs name", func(t *testing.T) {
		// init
		utilsMock := NewScanUtilsMock()
		utilsMock.AddFile("package.json", []byte(`{"key":"value"}`))
		scan := newTestScan(&config)
		// test
		err := scan.ExecuteNpmScan(&config, utilsMock)
		// assert
		assert.EqualError(t, err, "failed to scan NPM module 'package.json': the file 'package.json' must configure a name")
	})
	t.Run("npm ls fails", func(t *testing.T) {
		// init
		utilsMock := NewScanUtilsMock()
		utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
		utilsMock.AddFile(filepath.Join("app", "package.json"), []byte(`{"name":"my-app-module-name"}`))
		utilsMock.AddFile("package-lock.json", []byte("dummy"))

		utilsMock.ShouldFailOnCommand = make(map[string]error)
		utilsMock.ShouldFailOnCommand["npm ls"] = fmt.Errorf("mock failure")
		scan := newTestScan(&config)
		// test
		err := scan.ExecuteNpmScan(&config, utilsMock)
		// assert
		assert.NoError(t, err)
		expectedNpmInstalls := []NpmInstall{
			{CurrentDir: "app", PackageJSON: []string{"package.json"}},
			{CurrentDir: "", PackageJSON: []string{"package.json"}},
		}
		assert.Equal(t, expectedNpmInstalls, utilsMock.NpmInstalledModules)
		assert.True(t, utilsMock.HasRemovedFile("package-lock.json"))
	})
}

func TestWriteWhitesourceConfigJSON(t *testing.T) {
	config := &ScanOptions{
		OrgToken:       "org-token",
		UserToken:      "user-token",
		ProductName:    "mock-product",
		ProductVersion: "product-version",
		ProjectName:    "mock-project",
		ProductToken:   "mock-product-token",
	}

	expected := make(map[string]interface{})
	expected["apiKey"] = "org-token"
	expected["userKey"] = "user-token"
	expected["checkPolicies"] = true
	expected["forceUpdate"] = true
	expected["productName"] = "mock-product"
	expected["projectName"] = "mock-project"
	expected["productToken"] = "mock-product-token"
	expected["productVer"] = "product-version"
	expected["devDep"] = true
	expected["ignoreNpmLsErrors"] = true

	t.Parallel()

	t.Run("write config from scratch", func(t *testing.T) {
		// init
		utils := NewScanUtilsMock()
		scan := newTestScan(config)
		// test
		err := scan.writeWhitesourceConfigJSON(config, utils, true, true)
		// assert
		if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
			contents, _ := utils.FileRead(whiteSourceConfig)
			actual := make(map[string]interface{})
			_ = json.Unmarshal(contents, &actual)
			assert.Equal(t, expected, actual)
		}
	})

	t.Run("extend and merge config", func(t *testing.T) {
		// init
		initial := make(map[string]interface{})
		initial["checkPolicies"] = false
		initial["productName"] = "mock-product"
		initial["productVer"] = "41"
		initial["unknown"] = "preserved"
		encoded, _ := json.Marshal(initial)

		utils := NewScanUtilsMock()
		utils.AddFile(whiteSourceConfig, encoded)

		scan := newTestScan(config)

		// test
		err := scan.writeWhitesourceConfigJSON(config, utils, true, true)
		// assert
		if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
			contents, _ := utils.FileRead(whiteSourceConfig)
			actual := make(map[string]interface{})
			_ = json.Unmarshal(contents, &actual)

			mergedExpected := expected
			mergedExpected["unknown"] = "preserved"

			assert.Equal(t, mergedExpected, actual)
		}
	})

	t.Run("extend and merge config, omit productToken", func(t *testing.T) {
		// init
		initial := make(map[string]interface{})
		initial["checkPolicies"] = false
		initial["productName"] = "mock-product"
		initial["productVer"] = "41"
		initial["unknown"] = "preserved"
		initial["projectToken"] = "mock-project-token"
		encoded, _ := json.Marshal(initial)

		utils := NewScanUtilsMock()
		utils.AddFile(whiteSourceConfig, encoded)

		scan := newTestScan(config)

		// test
		err := scan.writeWhitesourceConfigJSON(config, utils, true, true)
		// assert
		if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
			contents, _ := utils.FileRead(whiteSourceConfig)
			actual := make(map[string]interface{})
			_ = json.Unmarshal(contents, &actual)

			mergedExpected := expected
			mergedExpected["unknown"] = "preserved"
			mergedExpected["projectToken"] = "mock-project-token"
			delete(mergedExpected, "productToken")

			assert.Equal(t, mergedExpected, actual)
		}
	})
}
