package cmd

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"text/template"

	"github.com/SAP/jenkins-library/pkg/command"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/SAP/jenkins-library/pkg/piperutils"
	"github.com/SAP/jenkins-library/pkg/telemetry"
	"github.com/pkg/errors"
)

type newmanExecuteUtils interface {
	Glob(pattern string) (matches []string, err error)
	RunExecutable(executable string, params ...string) error
	Getenv(key string) string
}

type newmanExecuteUtilsBundle struct {
	*command.Command
	*piperutils.Files
}

func newNewmanExecuteUtils() newmanExecuteUtils {
	utils := newmanExecuteUtilsBundle{
		Command: &command.Command{
			ErrorCategoryMapping: map[string][]string{
				log.ErrorConfiguration.String(): {
					"ENOENT: no such file or directory",
				},
				log.ErrorTest.String(): {
					"AssertionError",
					"TypeError",
				},
			},
		},
		Files: &piperutils.Files{},
	}
	// Reroute command output to logging framework
	utils.Stdout(log.Writer())
	utils.Stderr(log.Writer())
	return &utils
}

func newmanExecute(config newmanExecuteOptions, _ *telemetry.CustomData, influx *newmanExecuteInflux) {
	utils := newNewmanExecuteUtils()

	influx.step_data.fields.newman = false
	err := runNewmanExecute(&config, utils)
	if err != nil {
		log.Entry().WithError(err).Fatal("step execution failed")
	}
	influx.step_data.fields.newman = true
}

func runNewmanExecute(config *newmanExecuteOptions, utils newmanExecuteUtils) error {
	if config.NewmanRunCommand != "" {
		log.Entry().Warn("found configuration for deprecated parameter newmanRunCommand, please use runOptions instead")
		log.Entry().Warn("setting runOptions to value of deprecated parameter newmanRunCommand")
		config.RunOptions = strings.Split(config.NewmanRunCommand, " ")
	}

	collectionList, err := utils.Glob(config.NewmanCollection)
	if err != nil {
		log.SetErrorCategory(log.ErrorConfiguration)
		return errors.Wrapf(err, "Could not execute global search for '%v'", config.NewmanCollection)
	}

	if collectionList == nil {
		log.SetErrorCategory(log.ErrorConfiguration)
		return fmt.Errorf("no collection found with pattern '%v'", config.NewmanCollection)
	}
	log.Entry().Infof("Found the following newman collections: %v", collectionList)

	err = logVersions(utils)
	if err != nil {
		return err
	}

	err = installNewman(config.NewmanInstallCommand, utils)
	if err != nil {
		return err
	}

	// resolve environment and globals if not covered by templating
	options := resolveOptions(config)

	for _, collection := range collectionList {
		runOptions, err := resolveTemplate(config, collection)
		if err != nil {
			return err
		}

		commandSecrets := handleCfAppCredentials(config)

		runOptions = append(runOptions, options...)
		runOptions = append(runOptions, commandSecrets...)

		if !config.FailOnError {
			runOptions = append(runOptions, "--suppress-exit-code")
		}

		newmanPath := filepath.Join(utils.Getenv("HOME"), "/.npm-global/bin/newman")
		err = utils.RunExecutable(newmanPath, runOptions...)
		if err != nil {
			return errors.Wrap(err, "The execution of the newman tests failed, see the log for details.")
		}
	}
	return nil
}

func logVersions(utils newmanExecuteUtils) error {
	err := utils.RunExecutable("node", "--version")
	if err != nil {
		log.SetErrorCategory(log.ErrorInfrastructure)
		return errors.Wrap(err, "error logging node version")
	}
	err = utils.RunExecutable("npm", "--version")
	if err != nil {
		log.SetErrorCategory(log.ErrorInfrastructure)
		return errors.Wrap(err, "error logging npm version")
	}
	return nil
}

func installNewman(newmanInstallCommand string, utils newmanExecuteUtils) error {
	installCommandTokens := strings.Split(newmanInstallCommand, " ")
	installCommandTokens = append(installCommandTokens, "--prefix=~/.npm-global")
	err := utils.RunExecutable(installCommandTokens[0], installCommandTokens[1:]...)
	if err != nil {
		log.SetErrorCategory(log.ErrorConfiguration)
		return errors.Wrap(err, "error installing newman")
	}
	return nil
}

func resolveOptions(config *newmanExecuteOptions) []string {
	options := []string{}
	if config.NewmanEnvironment != "" && !contains(config.RunOptions, "{{.Config.NewmanEnvironment}}") {
		options = append(options, "--environment")
		options = append(options, config.NewmanEnvironment)
	}
	if config.NewmanGlobals != "" && !contains(config.RunOptions, "{{.Config.NewmanGlobals}}") {
		options = append(options, "--globals")
		options = append(options, config.NewmanGlobals)
	}
	return options
}

func resolveTemplate(config *newmanExecuteOptions, collection string) ([]string, error) {
	cmd := []string{}
	collectionDisplayName := defineCollectionDisplayName(collection)

	type TemplateConfig struct {
		Config                interface{}
		CollectionDisplayName string
		NewmanCollection      string
	}

	for _, runOption := range config.RunOptions {
		templ, err := template.New("template").Parse(runOption)
		if err != nil {
			log.SetErrorCategory(log.ErrorConfiguration)
			return nil, errors.Wrap(err, "could not parse newman command template")
		}
		buf := new(bytes.Buffer)
		err = templ.Execute(buf, TemplateConfig{
			Config:                config,
			CollectionDisplayName: collectionDisplayName,
			NewmanCollection:      collection,
		})
		if err != nil {
			log.SetErrorCategory(log.ErrorConfiguration)
			return nil, errors.Wrap(err, "error on executing template")
		}
		cmd = append(cmd, buf.String())
	}

	return cmd, nil
}

func defineCollectionDisplayName(collection string) string {
	replacedSeparators := strings.Replace(collection, string(filepath.Separator), "_", -1)
	displayName := strings.Split(replacedSeparators, ".")
	if displayName[0] == "" && len(displayName) >= 2 {
		return displayName[1]
	}
	return displayName[0]
}

func handleCfAppCredentials(config *newmanExecuteOptions) []string {
	commandSecrets := []string{}
	if len(config.CfAppsWithSecrets) > 0 {
		for _, appName := range config.CfAppsWithSecrets {
			var clientID, clientSecret string
			clientID = os.Getenv("PIPER_NEWMANEXECUTE_" + appName + "_clientid")
			clientSecret = os.Getenv("PIPER_NEWMANEXECUTE_" + appName + "_clientsecret")
			if clientID != "" && clientSecret != "" {
				log.RegisterSecret(clientSecret)
				secretVar := fmt.Sprintf("--env-var %v_clientid=%v --env-var %v_clientsecret=%v", appName, clientID, appName, clientSecret)
				commandSecrets = append(commandSecrets, strings.Split(secretVar, " ")...)
				log.Entry().Infof("secrets found for app %v and forwarded to newman as --env-var parameter", appName)
			} else {
				log.Entry().Errorf("cannot fetch secrets from environment variables for app %v", appName)
			}
		}
	}
	return commandSecrets
}

func contains(slice []string, substr string) bool {
	for _, e := range slice {
		if strings.Contains(e, substr) {
			return true
		}
	}
	return false
}

func (utils newmanExecuteUtilsBundle) Getenv(key string) string {
	return os.Getenv(key)
}
