//nolint:goconst
package common

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math/big"
	"net/http"
	"os"
	"os/exec"
	"path"
	"runtime"
	"strings"
	"sync/atomic"
	"time"

	yamlconv "sigs.k8s.io/yaml"
)

const (
	repoRemoteURL = "https://gitlab.com/gitlab-org/ci-cd/gitlab-runner-pipeline-tests/gitlab-test.git"

	repoRefType = RefTypeBranch

	repoSHA       = "69b18e5ed3610cf646119c3e38f462c64ec462b7"
	repoBeforeSHA = "1ea27a9695f80d7816d9e8ce025d9b2df83d0dd7"
	repoRefName   = "main"

	repoLFSSHA       = "c8f2a61def956871b91f73fcd0c320afb257fd6e"
	repoLFSBeforeSHA = "86002a2304d89a193f91b8b0907c4cf2f95a6d28"
	repoLFSRefName   = "add-lfs-object"

	repoSubmoduleLFSSHA       = "86002a2304d89a193f91b8b0907c4cf2f95a6d28"
	repoSubmoduleLFSBeforeSHA = "1ea27a9695f80d7816d9e8ce025d9b2df83d0dd7"
	repoSubmoduleLFSRefName   = "add-lfs-submodule"

	repoStepsSHA       = "1142c6530a1eb81f0a5476db25fbfbf9a4e08f30"
	repoStepsBeforeSHA = "1ea27a9695f80d7816d9e8ce025d9b2df83d0dd7"
	repoStepsRefName   = "add-steps"

	FilesLFSFile1LFSsize = int64(2097152)
)

var (
	gitLabComChain        string
	gitLabComChainFetched atomic.Bool
)

func GetGitInfo(url string) GitInfo {
	return GitInfo{
		RepoURL:   url,
		Sha:       repoSHA,
		BeforeSha: repoBeforeSHA,
		Ref:       repoRefName,
		RefType:   repoRefType,
		Refspecs:  []string{"+refs/heads/*:refs/origin/heads/*", "+refs/tags/*:refs/tags/*"},
	}
}

func GetLFSGitInfo(url string) GitInfo {
	return GitInfo{
		RepoURL:   url,
		Sha:       repoLFSSHA,
		BeforeSha: repoLFSBeforeSHA,
		Ref:       repoLFSRefName,
		RefType:   repoRefType,
		Refspecs:  []string{"+refs/heads/*:refs/origin/heads/*", "+refs/tags/*:refs/tags/*"},
	}
}

func GetSubmoduleLFSGitInfo(url string) GitInfo {
	return GitInfo{
		RepoURL:   url,
		Sha:       repoSubmoduleLFSSHA,
		BeforeSha: repoSubmoduleLFSBeforeSHA,
		Ref:       repoSubmoduleLFSRefName,
		RefType:   repoRefType,
		Refspecs:  []string{"+refs/heads/*:refs/origin/heads/*", "+refs/tags/*:refs/tags/*"},
	}
}

func GetStepsGitInfo(url string) GitInfo {
	return GitInfo{
		RepoURL:   url,
		Sha:       repoStepsSHA,
		BeforeSha: repoStepsBeforeSHA,
		Ref:       repoStepsRefName,
		RefType:   repoRefType,
		Refspecs:  []string{"+refs/heads/*:refs/origin/heads/*", "+refs/tags/*:refs/tags/*"},
	}
}

func GetSuccessfulBuild() (JobResponse, error) {
	return GetLocalBuildResponse("echo Hello World")
}

func GetSuccessfulMultilineCommandBuild() (JobResponse, error) {
	return GetLocalBuildResponse(`echo "Hello
World"`)
}

func GetRemoteSuccessfulBuild() (JobResponse, error) {
	return GetRemoteBuildResponse("echo Hello World")
}

func GetRemoteSuccessfulLFSBuild() (JobResponse, error) {
	response, err := GetRemoteBuildResponse("echo Hello World")
	response.GitInfo = GetLFSGitInfo(repoRemoteURL)

	return response, err
}

func GetRemoteSuccessfulBuildWithAfterScript() (JobResponse, error) {
	jobResponse, err := GetRemoteBuildResponse("echo Hello World")
	jobResponse.Steps = append(
		jobResponse.Steps,
		Step{
			Name:   StepNameAfterScript,
			Script: []string{"echo Hello World"},
			When:   StepWhenAlways,
		},
	)
	return jobResponse, err
}

func GetRemoteSuccessfulBuildPrintVars(shell string, vars ...string) (JobResponse, error) {
	printVarsCmd := getShellPrintVars(shell, vars...)

	return GetRemoteBuildResponse(printVarsCmd...)
}

func GetRemoteSuccessfulMultistepBuild() (JobResponse, error) {
	jobResponse, err := GetRemoteBuildResponse("echo Hello World")
	if err != nil {
		return JobResponse{}, err
	}

	jobResponse.Steps = append(
		jobResponse.Steps,
		Step{
			Name:   "release",
			Script: []string{"echo Release"},
			When:   StepWhenOnSuccess,
		},
		Step{
			Name:   StepNameAfterScript,
			Script: []string{"echo After Script"},
			When:   StepWhenAlways,
		},
	)

	return jobResponse, nil
}

func GetRemoteFailingMultistepBuild(failingStepName StepName) (JobResponse, error) {
	jobResponse, err := GetRemoteSuccessfulMultistepBuild()
	if err != nil {
		return JobResponse{}, err
	}

	for i, step := range jobResponse.Steps {
		if step.Name == failingStepName {
			jobResponse.Steps[i].Script = append(step.Script, "exit 1") //nolint:gocritic
		}
	}

	return jobResponse, nil
}

func GetRemoteFailingMultistepBuildPrintVars(shell string, fail bool, vars ...string) (JobResponse, error) {
	jobResponse, err := GetRemoteBuildResponse("echo 'Hello World'")
	if err != nil {
		return JobResponse{}, err
	}

	printVarsCmd := getShellPrintVars(shell, vars...)

	exitCommand := "exit 0"
	if fail {
		exitCommand = "exit 1"
	}

	jobResponse.Steps = append(
		jobResponse.Steps,
		Step{
			Name:   "env",
			Script: append(printVarsCmd, exitCommand),
			When:   StepWhenOnSuccess,
		},
		Step{
			Name:   StepNameAfterScript,
			Script: printVarsCmd,
			When:   StepWhenAlways,
		},
	)

	return jobResponse, nil
}

func getShellPrintVars(shell string, vars ...string) []string {
	var envCommand []string
	var fmtStr string

	switch shell {
	case "powershell", "pwsh":
		fmtStr = "echo %s=$env:%s"
	default:
		fmtStr = "echo %s=$%s"
	}

	for _, v := range vars {
		envCommand = append(envCommand, fmt.Sprintf(fmtStr, v, v))
	}

	return envCommand
}

func GetRemoteSuccessfulBuildWithDumpedVariables() (JobResponse, error) {
	variableName := "test_dump"
	variableValue := "test"

	response, err := GetRemoteBuildResponse(
		fmt.Sprintf("[[ \"${%s}\" != \"\" ]]", variableName),
		fmt.Sprintf("[[ $(cat $%s) == \"%s\" ]]", variableName, variableValue),
	)
	if err != nil {
		return JobResponse{}, err
	}

	dumpedVariable := JobVariable{
		Key: variableName, Value: variableValue,
		Internal: true, Public: true, File: true,
	}
	response.Variables = append(response.Variables, dumpedVariable)

	return response, nil
}

func GetFailedBuild() (JobResponse, error) {
	return GetLocalBuildResponse("exit 1")
}

func GetRemoteFailedBuild() (JobResponse, error) {
	return GetRemoteBuildResponse("exit 1")
}

func GetLongRunningBuild() (JobResponse, error) {
	return GetLocalBuildResponse("sleep 3600")
}

func GetRemoteLongRunningBuild() (JobResponse, error) {
	return GetRemoteBuildResponse("sleep 3600")
}

func GetRemoteLongRunningBuildWithAfterScript(shell string) (JobResponse, error) {
	var jobResponse JobResponse
	var err error

	jobResponse, err = GetRemoteLongRunningBuild()
	if err != nil {
		return JobResponse{}, err
	}

	switch shell {
	default:
		jobResponse.Steps = append(jobResponse.Steps, Step{
			Name: StepNameAfterScript,
			Script: []string{
				"echo \"Hello World from after_script\"",
				"echo \"job status $CI_JOB_STATUS\"",
			},
		})

	case "pwsh":
		jobResponse.Steps = append(jobResponse.Steps, Step{
			Name: StepNameAfterScript,
			Script: []string{
				"echo \"Hello World from after_script\"",
				"echo \"job status $env:CI_JOB_STATUS\"",
			},
		})

	case "cmd":
		jobResponse.Steps = append(jobResponse.Steps, Step{
			Name: StepNameAfterScript,
			Script: []string{
				"echo \"Hello World from after_script\"",
				"echo \"job status %CI_JOB_STATUS%\"",
			},
		})
	}

	return jobResponse, nil
}

func GetMultilineBashBuild() (JobResponse, error) {
	return GetRemoteBuildResponse(`if true; then
	echo 'Hello World'
fi
`)
}

func GetMultilineBashBuildPowerShell() (JobResponse, error) {
	return GetRemoteBuildResponse("if (0 -eq 0) {\n\recho \"Hello World\"\n\r}")
}

func GetRemoteBrokenTLSBuild() (JobResponse, error) {
	invalidCert, err := buildSnakeOilCert()
	if err != nil {
		return JobResponse{}, err
	}

	return getRemoteCustomTLSBuild(invalidCert)
}

func GetRemoteGitLabComTLSBuild() (JobResponse, error) {
	cert, err := getGitLabComTLSChain()
	if err != nil {
		return JobResponse{}, err
	}

	return getRemoteCustomTLSBuild(cert)
}

func getRemoteCustomTLSBuild(chain string) (JobResponse, error) {
	job, err := GetRemoteBuildResponse("echo Hello World")
	if err != nil {
		return JobResponse{}, err
	}

	job.TLSCAChain = chain
	job.Variables = append(
		job.Variables,
		JobVariable{Key: "GIT_STRATEGY", Value: "clone"},
		JobVariable{Key: "GIT_SUBMODULE_STRATEGY", Value: "normal"},
	)

	return job, nil
}

func getBuildResponse(repoURL string, commands []string) JobResponse {
	return JobResponse{
		GitInfo: GetGitInfo(repoURL),
		Steps: Steps{
			Step{
				Name:         StepNameScript,
				Script:       commands,
				When:         StepWhenAlways,
				AllowFailure: false,
			},
		},
		RunnerInfo: RunnerInfo{
			Timeout: DefaultTimeout,
		},
	}
}

func getStepsBuildResponse(repoURL string, stepsYAML string) (JobResponse, error) {
	// steps come from GitLab in json, but it's a whole lot more convenient to write them in yaml in tests. Let's
	// support the latter and convert the yaml to json here so everything else downstream works as if the steps were
	// specified in json.
	stepsJSON, err := yamlconv.YAMLToJSON([]byte(stepsYAML))
	if err != nil {
		return JobResponse{}, fmt.Errorf("converting json to yaml: %w", err)
	}
	return JobResponse{
		GitInfo: GetStepsGitInfo(repoURL),
		Run:     string(stepsJSON),
		Steps:   Steps{Step{Name: StepNameRun}},
		RunnerInfo: RunnerInfo{
			Timeout: DefaultTimeout,
		},
	}, nil
}

func GetRemoteStepsBuildResponse(stepsYAML string) (JobResponse, error) {
	return getStepsBuildResponse(repoRemoteURL, stepsYAML)
}

func GetRemoteBuildResponse(commands ...string) (JobResponse, error) {
	return getBuildResponse(repoRemoteURL, commands), nil
}

func GetLocalBuildResponse(commands ...string) (JobResponse, error) {
	localRepoURL, err := getLocalRepoURL()
	if err != nil {
		if os.IsNotExist(err) {
			panic("Local repo not found, please run `make development_setup`")
		}
		return JobResponse{}, err
	}

	return getBuildResponse(localRepoURL, commands), nil
}

func getLocalRepoURL() (string, error) {
	_, filename, _, _ := runtime.Caller(0) //nolint:dogsled

	directory := path.Dir(filename)
	if strings.Contains(directory, "_test/_obj_test") {
		pwd, err := os.Getwd()
		if err != nil {
			return "", err
		}
		directory = pwd
	}

	localRepoURL := path.Clean(directory + "/../tmp/gitlab-test/.git")

	_, err := os.Stat(localRepoURL)
	if err != nil {
		return "", err
	}

	return localRepoURL, nil
}

func RunLocalRepoGitCommand(arguments ...string) error {
	url, err := getLocalRepoURL()
	if err != nil {
		return err
	}

	cmd := exec.Command("git", arguments...)
	cmd.Dir = path.Dir(url)

	return cmd.Run()
}

func buildSnakeOilCert() (string, error) {
	priv, err := rsa.GenerateKey(rand.Reader, 1024)
	if err != nil {
		return "", err
	}

	notBefore := time.Now()
	notAfter := notBefore.Add(time.Hour)

	template := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			Organization: []string{"Snake Oil Co"},
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,

		IsCA:                  true,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
	}

	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
	if err != nil {
		return "", err
	}

	certificate := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})

	return string(certificate), nil
}

func getGitLabComTLSChain() (string, error) {
	if gitLabComChainFetched.Load() {
		return gitLabComChain, nil
	}

	resp, err := http.Head("https://gitlab.com/users/sign_in")
	if err != nil {
		return "", err
	}
	defer func() { _ = resp.Body.Close() }()

	var buff strings.Builder
	for _, certs := range resp.TLS.VerifiedChains {
		for _, cert := range certs {
			err = pem.Encode(&buff, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
			if err != nil {
				return "", err
			}
		}
	}

	gitLabComChain = buff.String()
	gitLabComChainFetched.Store(true)

	return gitLabComChain, nil
}
