//go:build integration
// +build integration

package integration_test

import (
	"errors"
	"os"
	"os/exec"
	"sync"

	osutils "github.com/projectdiscovery/utils/os"

	"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
	"github.com/projectdiscovery/nuclei/v3/pkg/templates"
	"github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
)

var isCodeDisabled = func() bool { return osutils.IsWindows() && os.Getenv("CI") == "true" }
var signedIntegrationTemplates sync.Once
var signedIntegrationTemplatesErr error

func hasAnyExecutable(names ...string) bool {
	for _, name := range names {
		if _, err := exec.LookPath(name); err == nil {
			return true
		}
	}
	return false
}

var codeTestCases = []integrationCase{
	{Path: "protocols/code/py-snippet.yaml", TestCase: &codeSnippet{}, DisableOn: isCodeDisabled},
	{Path: "protocols/code/py-file.yaml", TestCase: &codeFile{}, DisableOn: isCodeDisabled},
	{Path: "protocols/code/py-env-var.yaml", TestCase: &codeEnvVar{}, DisableOn: isCodeDisabled},
	{Path: "protocols/code/unsigned.yaml", TestCase: &unsignedCode{}, DisableOn: isCodeDisabled},
	{Path: "protocols/code/dast-unsigned.yaml", TestCase: &dastUnsignedCode{}, DisableOn: isCodeDisabled},
	{Path: "protocols/code/py-nosig.yaml", TestCase: &codePyNoSig{}, DisableOn: isCodeDisabled},
	{Path: "protocols/code/py-interactsh.yaml", TestCase: &codeSnippet{}, DisableOn: isCodeDisabled},
	{Path: "protocols/code/ps1-snippet.yaml", TestCase: &codeSnippet{}, DisableOn: func() bool { return !osutils.IsWindows() || isCodeDisabled() }},
	{Path: "protocols/code/pre-condition.yaml", TestCase: &codePreCondition{}, DisableOn: isCodeDisabled},
	{Path: "protocols/code/sh-virtual.yaml", TestCase: &codeSnippet{}, DisableOn: func() bool { return !osutils.IsLinux() || isCodeDisabled() || !hasAnyExecutable("docker", "podman") }, Serial: true},
	{Path: "protocols/code/py-virtual.yaml", TestCase: &codeSnippet{}, DisableOn: func() bool { return !osutils.IsLinux() || isCodeDisabled() || !hasAnyExecutable("docker", "podman") }, Serial: true},
	{Path: "protocols/code/pwsh-echo.yaml", TestCase: &codeSnippet{}, DisableOn: func() bool { return isCodeDisabled() || !hasAnyExecutable("pwsh", "powershell", "powershell.exe") }},
}

const (
	testCertFile = "protocols/keys/ci.crt"
	testKeyFile  = "protocols/keys/ci-private-key.pem"
)

var testcertpath = ""

func ensureSignedIntegrationTemplates() error {
	signedIntegrationTemplates.Do(func() {
		previousWD, err := os.Getwd()
		if err != nil {
			signedIntegrationTemplatesErr = err
			return
		}
		if err := os.Chdir(suite.fixturesDir); err != nil {
			signedIntegrationTemplatesErr = err
			return
		}
		defer func() {
			if chdirErr := os.Chdir(previousWD); chdirErr != nil && signedIntegrationTemplatesErr == nil {
				signedIntegrationTemplatesErr = chdirErr
			}
		}()

		templates.TemplateSignerLFA()

		certPath := fixturePath(testCertFile)
		keyPath := fixturePath(testKeyFile)
		testcertpath = certPath

		tsigner, err := signer.NewTemplateSignerFromFiles(certPath, keyPath)
		if err != nil {
			signedIntegrationTemplatesErr = err
			return
		}

		var templatesToSign []string
		if !isCodeDisabled() {
			templatesToSign = append(templatesToSign,
				"workflow/code-template-1.yaml",
				"workflow/code-template-2.yaml",
			)
		}
		for _, v := range codeTestCases {
			if v.DisableOn != nil && v.DisableOn() {
				continue
			}
			if _, ok := v.TestCase.(*unsignedCode); ok {
				continue
			}
			if _, ok := v.TestCase.(*codePyNoSig); ok {
				continue
			}
			if _, ok := v.TestCase.(*dastUnsignedCode); ok {
				continue
			}
			templatesToSign = append(templatesToSign, v.Path)
		}
		for _, v := range jsTestcases {
			if v.DisableOn != nil && v.DisableOn() {
				continue
			}
			templatesToSign = append(templatesToSign, v.Path)
		}
		for _, templatePath := range templatesToSign {
			if err := templates.SignTemplate(tsigner, fixturePath(templatePath)); err != nil {
				signedIntegrationTemplatesErr = err
				return
			}
		}
	})
	return signedIntegrationTemplatesErr
}

func getEnvValues() []string {
	return []string{
		signer.CertEnvVarName + "=" + testcertpath,
	}
}

func runSignedNucleiTemplateAndGetResults(template, url string, debug bool, extra ...string) ([]string, error) {
	if err := ensureSignedIntegrationTemplates(); err != nil {
		return nil, err
	}
	args := []string{"-t", template, "-target", url}
	args = append(args, extra...)
	return testutils.RunNucleiBareArgsAndGetResults(debug, getEnvValues(), args...)
}

type codeSnippet struct{}

// Execute executes a test case and returns an error if occurred
func (h *codeSnippet) Execute(filePath string) error {
	results, err := testutils.RunNucleiArgsWithEnvAndGetResults(debug, getEnvValues(), "-t", filePath, "-u", "input", "-code")
	if err != nil {
		return err
	}
	return expectResultsCount(results, 1)
}

type codePreCondition struct{}

// Execute executes a test case and returns an error if occurred
func (h *codePreCondition) Execute(filePath string) error {
	results, err := testutils.RunNucleiArgsWithEnvAndGetResults(debug, getEnvValues(), "-t", filePath, "-u", "input", "-code", "-esc")
	if err != nil {
		return err
	}
	if osutils.IsLinux() {
		return expectResultsCount(results, 1)
	} else {
		return expectResultsCount(results, 0)

	}
}

type codeFile struct{}

// Execute executes a test case and returns an error if occurred
func (h *codeFile) Execute(filePath string) error {
	results, err := testutils.RunNucleiArgsWithEnvAndGetResults(debug, getEnvValues(), "-t", filePath, "-u", "input", "-code")
	if err != nil {
		return err
	}
	return expectResultsCount(results, 1)
}

type codeEnvVar struct{}

// Execute executes a test case and returns an error if occurred
func (h *codeEnvVar) Execute(filePath string) error {
	results, err := testutils.RunNucleiArgsWithEnvAndGetResults(debug, getEnvValues(), "-t", filePath, "-u", "input", "-V", "baz=baz", "-code")
	if err != nil {
		return err
	}
	return expectResultsCount(results, 1)
}

type unsignedCode struct{}

// Execute executes a test case and returns an error if occurred
func (h *unsignedCode) Execute(filePath string) error {
	results, err := testutils.RunNucleiArgsWithEnvAndGetResults(debug, getEnvValues(), "-t", filePath, "-u", "input", "-code")

	// should error out
	if err != nil {
		return nil
	}

	// this point should never be reached
	return errors.Join(expectResultsCount(results, 1), errors.New("unsigned template was executed"))
}

type dastUnsignedCode struct{}

// Execute runs an unsigned template that pairs a fuzzable request with a code
// block under -dast, which must not bypass the code-signing check.
func (h *dastUnsignedCode) Execute(filePath string) error {
	results, err := testutils.RunNucleiArgsWithEnvAndGetResults(debug, getEnvValues(), "-t", filePath, "-u", "input", "-dast")

	// should error out
	if err != nil {
		return nil
	}

	// this point should never be reached
	return errors.Join(expectResultsCount(results, 1), errors.New("unsigned template was executed"))
}

type codePyNoSig struct{}

// Execute executes a test case and returns an error if occurred
func (h *codePyNoSig) Execute(filePath string) error {
	results, err := testutils.RunNucleiArgsWithEnvAndGetResults(debug, getEnvValues(), "-t", filePath, "-u", "input", "-code")

	// should error out
	if err != nil {
		return nil
	}

	// this point should never be reached
	return errors.Join(expectResultsCount(results, 1), errors.New("unsigned template was executed"))
}
