package test_test

import (
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"github.com/gruntwork-io/terragrunt/internal/util"
	"github.com/gruntwork-io/terragrunt/test/helpers"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	testScaffoldLocalModulePath     = "fixtures/scaffold/scaffold-module"
	testScaffoldWithRootHCL         = "fixtures/scaffold/root-hcl"
	testScaffold3rdPartyModulePath  = "git::https://github.com/Azure/terraform-azurerm-avm-res-compute-virtualmachine.git//.?ref=v0.15.0"
	testScaffoldNoDependencyPrompt  = "fixtures/scaffold/dependency-prompt-template"
	testScaffoldLocalTofuModulePath = "fixtures/scaffold/scaffold-module-tofu"
)

// scaffoldModuleURL returns the canonical scaffold-module source on the
// local server, which it asks the server to serve.
func scaffoldModuleURL(m *helpers.GitServer) string {
	m.AddFixtures("test/fixtures/scaffold/scaffold-module")

	return m.SourceURL("/test/fixtures/scaffold/scaffold-module", "")
}

// scaffoldInputsURL returns the inputs fixture source on the local
// server, which it asks the server to serve.
func scaffoldInputsURL(m *helpers.GitServer) string {
	m.AddFixtures("test/fixtures/inputs")

	return m.SourceURL("/test/fixtures/inputs", "")
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s %s", tmpEnvPath, scaffoldModuleURL(mirror)))
	require.NoError(t, err)
	assert.FileExists(t, tmpEnvPath+"/terragrunt.hcl")
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s %s", tmpEnvPath, scaffoldInputsURL(mirror)))

	require.NoError(t, err)
	// check that find_in_parent_folders is generated in terragrunt.hcl
	content, err := util.ReadFileAsString(tmpEnvPath + "/terragrunt.hcl")
	require.NoError(t, err)
	assert.Contains(t, content, "find_in_parent_folders")
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s %s --var=EnableRootInclude=false", tmpEnvPath, scaffoldInputsURL(mirror)))
	require.NoError(t, err)
	// check that find_in_parent_folders is NOT generated in  terragrunt.hcl
	content, err := util.ReadFileAsString(tmpEnvPath + "/terragrunt.hcl")
	require.NoError(t, err)
	assert.NotContains(t, content, "find_in_parent_folders")
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s %s --var=Ref=v0.67.4", tmpEnvPath, scaffoldInputsURL(mirror)))

	require.NoError(t, err)

	content, err := util.ReadFileAsString(tmpEnvPath + "/terragrunt.hcl")
	require.NoError(t, err)
	assert.Contains(t, content, mirror.URL+"//test/fixtures/inputs?ref=v0.67.4")
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt scaffold --non-interactive --working-dir "+tmpEnvPath)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "No module URL passed")
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	workingDir, err := os.Getwd()
	require.NoError(t, err)

	_, _, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s %s", tmpEnvPath, fmt.Sprintf("%s//%s", workingDir, testScaffoldLocalTofuModulePath)))
	require.NoError(t, err)
	assert.FileExists(t, tmpEnvPath+"/terragrunt.hcl")

	// Verify variables from .tofu files were parsed and included in generated config
	content, err := util.ReadFileAsString(tmpEnvPath + "/terragrunt.hcl")
	require.NoError(t, err)
	assert.Contains(t, content, "project_name", "Required variable from variables.tofu should be in generated config")
	assert.Contains(t, content, "open_port", "Required variable from variables.tofu should be in generated config")
	assert.Contains(t, content, "enable_backups", "Required variable from variables.tofu should be in generated config")
	assert.Contains(t, content, "users", "Required variable from variables.tofu should be in generated config")
	assert.Contains(t, content, "policy_map", "Required variable from variables.tofu should be in generated config")
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	workingDir, err := os.Getwd()
	require.NoError(t, err)

	_, _, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s %s", tmpEnvPath, fmt.Sprintf("%s//%s", workingDir, testScaffoldLocalModulePath)))
	require.NoError(t, err)
	assert.FileExists(t, tmpEnvPath+"/terragrunt.hcl")
}

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

	tmpRoot := helpers.TmpDirWOSymlinks(t)

	tmpEnvPath := filepath.Join(tmpRoot, "app")
	err := os.MkdirAll(tmpEnvPath, 0755)
	require.NoError(t, err)

	// create "root" terragrunt.hcl
	err = os.WriteFile(filepath.Join(tmpRoot, "terragrunt.hcl"), []byte(""), 0644)
	require.NoError(t, err)

	_, _, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s %s", tmpEnvPath, testScaffold3rdPartyModulePath))
	require.NoError(t, err)
	assert.FileExists(t, tmpEnvPath+"/terragrunt.hcl")

	// validate the generated files
	_, _, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt hcl validate --non-interactive --working-dir "+tmpEnvPath)
	require.NoError(t, err)
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	outputFolder := tmpEnvPath + "/foo/bar"
	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt --non-interactive --working-dir %s scaffold %s --output-folder %s", tmpEnvPath, scaffoldModuleURL(mirror), outputFolder))
	require.NoError(t, err)
	assert.FileExists(t, outputFolder+"/terragrunt.hcl")
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := helpers.CopyEnvironment(t, testScaffoldWithRootHCL)
	helpers.CleanupTerraformFolder(t, tmpEnvPath)
	testPath := filepath.Join(tmpEnvPath, testScaffoldWithRootHCL)

	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(
		"terragrunt --non-interactive --working-dir %s scaffold %s",
		filepath.Join(testPath, "unit"),
		scaffoldModuleURL(mirror),
	))
	require.NoError(t, err)

	assert.FileExists(t, filepath.Join(testPath, "unit", "terragrunt.hcl"))

	// Read the file
	content, err := util.ReadFileAsString(filepath.Join(testPath, "unit", "terragrunt.hcl"))
	require.NoError(t, err)
	assert.Contains(t, content, `path = find_in_parent_folders("root.hcl")`)
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	workingDir, err := os.Getwd()
	require.NoError(t, err)

	localBoilerplateModuleDir := fmt.Sprintf("%s/%s//.", workingDir, testScaffoldNoDependencyPrompt)

	outputFolder := tmpEnvPath + "/foo/bar"
	_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt scaffold --non-interactive --working-dir %s --no-dependency-prompt %s --output-folder %s", tmpEnvPath, localBoilerplateModuleDir, outputFolder))
	require.NoError(t, err)
	assert.NotContains(t, stderr, "This boilerplate template has a dependency!")
	assert.FileExists(t, outputFolder+"/base/test.hcl")
	assert.FileExists(t, outputFolder+"/leaf/terragrunt.hcl")
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)
	workingDir, err := os.Getwd()
	require.NoError(t, err)

	templatePath := workingDir + "//fixtures/scaffold/with-shell-commands"

	_, _, err = helpers.RunTerragruntCommandWithOutput(
		t,
		fmt.Sprintf(
			"terragrunt scaffold --non-interactive --working-dir %s %s",
			tmpEnvPath,
			templatePath,
		),
	)

	require.NoError(t, err)

	content, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, "terragrunt.hcl"))
	require.NoError(t, err)

	assert.NotContains(t, content, "{{ shell", "Shell template should be processed")
	assert.Contains(t, content, "SHELL_EXECUTED_VALUE_1", "Shell command output should be present")
	assert.Contains(t, content, "SHELL_EXECUTED_VALUE_2", "Shell command output should be present")
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)
	workingDir, err := os.Getwd()
	require.NoError(t, err)

	templatePath := workingDir + "//fixtures/scaffold/with-shell-commands"

	_, _, err = helpers.RunTerragruntCommandWithOutput(
		t,
		fmt.Sprintf(
			"terragrunt scaffold --non-interactive --no-shell --working-dir %s %s",
			tmpEnvPath,
			templatePath,
		),
	)

	require.NoError(t, err)

	content, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, "terragrunt.hcl"))
	require.NoError(t, err)

	assert.NotContains(t, content, "SHELL_EXECUTED_VALUE_1", "Shell command should not have executed")
	assert.NotContains(t, content, "SHELL_EXECUTED_VALUE_2", "Shell command should not have executed")
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)
	workingDir, err := os.Getwd()
	require.NoError(t, err)

	templatePath := workingDir + "//fixtures/scaffold/with-hooks"

	_, _, err = helpers.RunTerragruntCommandWithOutput(
		t,
		fmt.Sprintf(
			"terragrunt scaffold --non-interactive --working-dir %s %s",
			tmpEnvPath,
			templatePath,
		),
	)

	require.NoError(t, err)

	content, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, "terragrunt.hcl"))
	require.NoError(t, err)
	assert.Contains(t, content, "terraform {", "Generated file should be valid")
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)
	workingDir, err := os.Getwd()
	require.NoError(t, err)

	templatePath := workingDir + "//fixtures/scaffold/with-hooks"

	_, _, err = helpers.RunTerragruntCommandWithOutput(
		t,
		fmt.Sprintf(
			"terragrunt scaffold --non-interactive --no-hooks --working-dir %s %s",
			tmpEnvPath,
			templatePath,
		),
	)

	require.NoError(t, err)

	content, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, "terragrunt.hcl"))
	require.NoError(t, err)
	assert.Contains(t, content, "terraform {", "Generated file should be valid")
}

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

	tmpEnvPath := helpers.TmpDirWOSymlinks(t)
	workingDir, err := os.Getwd()
	require.NoError(t, err)

	templatePath := workingDir + "//fixtures/scaffold/with-shell-and-hooks"

	_, _, err = helpers.RunTerragruntCommandWithOutput(
		t,
		fmt.Sprintf(
			"terragrunt scaffold --non-interactive --no-shell --no-hooks --working-dir %s %s",
			tmpEnvPath,
			templatePath,
		),
	)

	require.NoError(t, err)

	content, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, "terragrunt.hcl"))
	require.NoError(t, err)

	assert.NotContains(t, content, "SHELL_OUTPUT_1", "Shell command should not have executed")
	assert.NotContains(t, content, "SHELL_OUTPUT_2", "Shell command should not have executed")

	assert.Contains(t, content, "terraform {", "Generated file should be valid")
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	// Write a pre-existing HCL file with intentionally poor formatting.
	// Scaffold should only format files it generates, not files that already exist.
	preExistingFile := filepath.Join(tmpEnvPath, "existing.hcl")
	unformattedHCL := `locals {
foo       = "bar"
baz="qux"
}
`
	err := os.WriteFile(preExistingFile, []byte(unformattedHCL), 0644)
	require.NoError(t, err)

	_, _, err = helpers.RunTerragruntCommandWithOutput(
		t,
		fmt.Sprintf(
			"terragrunt scaffold --non-interactive --working-dir %s %s",
			tmpEnvPath,
			scaffoldModuleURL(mirror),
		),
	)
	require.NoError(t, err)
	assert.FileExists(t, filepath.Join(tmpEnvPath, "terragrunt.hcl"))

	// Verify the pre-existing file was not modified by scaffold's formatting step.
	content, err := util.ReadFileAsString(preExistingFile)
	require.NoError(t, err)
	assert.Equal(t, unformattedHCL, content, "Pre-existing HCL file should not be formatted by scaffold")
}

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

	workingDir, err := os.Getwd()
	require.NoError(t, err)

	catalogConfigPath := filepath.Join(workingDir, "fixtures/scaffold/catalog-config-test/terragrunt.hcl")
	templatePath := workingDir + "//fixtures/scaffold/with-shell-and-hooks"
	tmpEnvPath := helpers.TmpDirWOSymlinks(t)

	catalogContent, err := util.ReadFileAsString(catalogConfigPath)
	require.NoError(t, err)

	err = os.WriteFile(filepath.Join(tmpEnvPath, "terragrunt.hcl"), []byte(catalogContent), 0644)
	require.NoError(t, err)

	outputDir := filepath.Join(tmpEnvPath, "output")
	err = os.MkdirAll(outputDir, 0755)
	require.NoError(t, err)

	_, _, err = helpers.RunTerragruntCommandWithOutput(
		t,
		fmt.Sprintf(
			"terragrunt scaffold --non-interactive --working-dir %s --output-folder %s %s",
			tmpEnvPath,
			outputDir,
			templatePath,
		),
	)

	require.NoError(t, err)

	content, err := util.ReadFileAsString(filepath.Join(outputDir, "terragrunt.hcl"))
	require.NoError(t, err)

	assert.NotContains(t, content, "SHELL_OUTPUT_1", "Shell should be disabled by catalog config")
	assert.NotContains(t, content, "SHELL_OUTPUT_2", "Shell should be disabled by catalog config")

	assert.Contains(t, content, "terraform {", "Generated file should be valid")
}
