package test_test

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/gruntwork-io/terragrunt/internal/runner/run"
	"github.com/gruntwork-io/terragrunt/internal/tf"
	"github.com/gruntwork-io/terragrunt/internal/util"
	"github.com/gruntwork-io/terragrunt/pkg/config"
	"github.com/gruntwork-io/terragrunt/test/helpers"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	testFixtureLocalDownloadPath                      = "fixtures/download/local"
	testFixtureCustomLockFile                         = "fixtures/download/custom-lock-file"
	testFixtureRemoteDownloadPath                     = "fixtures/download/remote"
	testFixtureInvalidRemoteDownloadPath              = "fixtures/download/remote-invalid"
	testFixtureInvalidRemoteDownloadPathWithRetries   = "fixtures/download/remote-invalid-with-retries"
	testFixtureOverrideDownloadPath                   = "fixtures/download/override"
	testFixtureLocalRelativeDownloadPath              = "fixtures/download/local-relative"
	testFixtureRemoteRelativeDownloadPath             = "fixtures/download/remote-relative"
	testFixtureRemoteRelativeDownloadPathWithSlash    = "fixtures/download/remote-relative-with-slash"
	testFixtureLocalWithExcludeDir                    = "fixtures/download/local-with-exclude-dir"
	testFixtureLocalWithIncludeDir                    = "fixtures/download/local-with-include-dir"
	testFixtureRemoteModuleInRoot                     = "fixtures/download/remote-module-in-root"
	testFixtureLocalMissingBackend                    = "fixtures/download/local-with-missing-backend"
	testFixtureLocalWithHiddenFolder                  = "fixtures/download/local-with-hidden-folder"
	testFixtureLocalWithAllowedHidden                 = "fixtures/download/local-with-allowed-hidden"
	testFixtureLocalPreventDestroy                    = "fixtures/download/local-with-prevent-destroy"
	testFixtureLocalPreventDestroyDependencies        = "fixtures/download/local-with-prevent-destroy-dependencies"
	testFixtureLocalIncludePreventDestroyDependencies = "fixtures/download/local-include-with-prevent-destroy-dependencies"
	testFixtureNotExistingSource                      = "fixtures/download/invalid-path"
	testFixtureDisableCopyLockFilePath                = "fixtures/download/local-disable-copy-terraform-lock-file"
	testFixtureIncludeDisableCopyLockFilePath         = "fixtures/download/local-include-disable-copy-lock-file/module-b"
)

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

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	rootPath := filepath.Join(tmpEnvPath, testFixtureLocalDownloadPath)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// As of Terraform 0.14.0 we should be copying the lock file from .terragrunt-cache to the working directory
	assert.FileExists(t, filepath.Join(rootPath, util.TerraformLockFile))

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

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

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	rootPath := filepath.Join(tmpEnvPath, testFixtureDisableCopyLockFilePath)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// The terraform lock file should not be copied if `copy_terraform_lock_file = false`
	assert.NoFileExists(t, filepath.Join(rootPath, util.TerraformLockFile))

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

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

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	rootPath := filepath.Join(tmpEnvPath, testFixtureIncludeDisableCopyLockFilePath)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// The terraform lock file should not be copied if `copy_terraform_lock_file = false`
	assert.NoFileExists(t, filepath.Join(rootPath, util.TerraformLockFile))

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

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

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	rootPath := filepath.Join(tmpEnvPath, testFixtureLocalWithHiddenFolder)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

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

	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalWithAllowedHidden)
	rootPath := filepath.Join(tmpEnvPath, testFixtureLocalWithAllowedHidden)
	helpers.CleanupTerraformFolder(t, rootPath)

	// CopyFolderContents skips hidden files; the fixture's modules/.nonce is required for the apply.
	noncePath := filepath.Join(rootPath, "modules", ".nonce")
	require.NoError(t, os.WriteFile(noncePath, []byte("Hello world\n"), 0600))

	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --non-interactive --working-dir %s/live", rootPath))

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --non-interactive --working-dir %s/live", rootPath))

	// Validate that the hidden file was copied
	var (
		stdout bytes.Buffer
		stderr bytes.Buffer
	)

	err := helpers.RunTerragruntCommand(t, fmt.Sprintf("terragrunt output -raw text --non-interactive --working-dir %s/live", rootPath), &stdout, &stderr)
	helpers.LogBufferContentsLineByLine(t, stdout, "output stdout")
	helpers.LogBufferContentsLineByLine(t, stderr, "output stderr")
	require.NoError(t, err)
	assert.Equal(t, "Hello world", stdout.String())
}

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

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	rootPath := filepath.Join(tmpEnvPath, testFixtureLocalRelativeDownloadPath)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

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

	s3BucketName := "terragrunt-test-bucket-" + strings.ToLower(helpers.UniqueID())
	lockTableName := "terragrunt-lock-table-" + strings.ToLower(helpers.UniqueID())

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	rootPath := filepath.Join(tmpEnvPath, testFixtureLocalMissingBackend)

	rootTerragruntConfigPath := filepath.Join(rootPath, config.DefaultTerragruntConfigPath)
	helpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, lockTableName, "not-used")

	err := helpers.RunTerragruntCommand(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath, os.Stdout, os.Stderr)
	if assert.Error(t, err) {
		var target run.BackendNotDefined
		assert.ErrorAs(t, err, &target)
	}
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := mirror.RenderFixture(testFixtureRemoteDownloadPath)
	rootPath := filepath.Join(tmpEnvPath, testFixtureRemoteDownloadPath)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

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

	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureInvalidRemoteDownloadPath)
	rootPath := filepath.Join(tmpEnvPath, testFixtureInvalidRemoteDownloadPath)
	helpers.CleanupTerraformFolder(t, rootPath)

	applyStdout := bytes.Buffer{}
	applyStderr := bytes.Buffer{}

	err := helpers.RunTerragruntCommand(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath, &applyStdout, &applyStderr)

	helpers.LogBufferContentsLineByLine(t, applyStdout, "apply stdout")
	helpers.LogBufferContentsLineByLine(t, applyStderr, "apply stderr")

	require.Error(t, err)

	errMessage := "downloading source url"
	assert.Containsf(t, err.Error(), errMessage, "expected error containing %q, got %s", errMessage, err)
}

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

	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureInvalidRemoteDownloadPathWithRetries)
	rootPath := filepath.Join(tmpEnvPath, testFixtureInvalidRemoteDownloadPathWithRetries)
	helpers.CleanupTerraformFolder(t, rootPath)

	_, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	require.Error(t, err)

	errMessage := "max retry attempts (2) reached for error"
	assert.Containsf(t, err.Error(), errMessage, "expected error containing %q, got %s", errMessage, err)
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := mirror.RenderFixture(testFixtureRemoteRelativeDownloadPath)
	rootPath := filepath.Join(tmpEnvPath, testFixtureRemoteRelativeDownloadPath)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := mirror.RenderFixture(testFixtureRemoteRelativeDownloadPathWithSlash)
	rootPath := filepath.Join(tmpEnvPath, testFixtureRemoteRelativeDownloadPathWithSlash)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

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

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := mirror.RenderFixture("fixtures/download")
	rootPath := filepath.Join(tmpEnvPath, testFixtureOverrideDownloadPath)
	helpers.CleanupTerraformFolder(t, rootPath)

	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --non-interactive --working-dir %s --source %s", rootPath, "../hello-world"))

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --non-interactive --working-dir %s --source %s", rootPath, "../hello-world"))
}

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

	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureRemoteModuleInRoot)
	rootPath := filepath.Join(tmpEnvPath, testFixtureRemoteModuleInRoot)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	// Run a second time to make sure the temporary folder can be reused without errors
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)
}

// As of Terraform 0.14.0, if there's already a lock file in the working directory, we should be copying it into
// .terragrunt-cache
func TestCustomLockFile(t *testing.T) {
	t.Parallel()

	path := fmt.Sprintf("%s-%s", testFixtureCustomLockFile, wrappedBinary(t.Context()))
	tmpEnvPath := helpers.CopyEnvironment(t, filepath.Dir(testFixtureCustomLockFile))
	rootPath := filepath.Join(tmpEnvPath, path)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+rootPath)

	source := "../custom-lock-file-module"
	downloadDir := filepath.Join(rootPath, helpers.TerragruntCache)
	result, err := tf.NewSource(createLogger(), source, downloadDir, rootPath, false)
	require.NoError(t, err)

	lockFilePath := filepath.Join(result.WorkingDir, util.TerraformLockFile)
	assert.FileExists(t, lockFilePath)

	readFile, err := os.ReadFile(lockFilePath)
	require.NoError(t, err)

	// In our lock file, we intentionally have hashes for an older version of the AWS provider. If the lock file
	// copying works, then Terraform will stick with this older version. If there is a bug, Terraform will end up
	// installing a newer version (since the version is not pinned in the .tf code, only in the lock file).
	assert.Contains(t, string(readFile), `version     = "5.23.0"`)
}

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

	// Populate module paths.
	moduleNames := []string{
		"integration-env/aws/module-aws-a",
		"integration-env/gce/module-gce-b",
		"integration-env/gce/module-gce-c",
		"production-env/aws/module-aws-d",
		"production-env/gce/module-gce-e",
	}

	testCases := []struct {
		name                  string
		excludeArgs           string
		excludedModuleOutputs []string
	}{
		{
			name:                  "exclude gce modules with double star",
			excludeArgs:           "--queue-exclude-dir **/gce/**",
			excludedModuleOutputs: []string{"Module GCE B", "Module GCE C", "Module GCE E"},
		},
		{
			name:                  "exclude production env and gce c modules with double star",
			excludeArgs:           "--queue-exclude-dir production-env/**/* --queue-exclude-dir **/module-gce-c",
			excludedModuleOutputs: []string{"Module GCE C", "Module AWS D", "Module GCE E"},
		},
		{
			name:                  "exclude integration env gce b and c modules and aws modules with double star",
			excludeArgs:           "--queue-exclude-dir integration-env/gce/module-gce-b --queue-exclude-dir integration-env/gce/module-gce-c --queue-exclude-dir **/module-aws*",
			excludedModuleOutputs: []string{"Module AWS A", "Module GCE B", "Module GCE C", "Module AWS D"},
		},
	}

	for _, tt := range testCases {
		tmpDir := helpers.NewGitServer(t).RenderFixture("fixtures/download")
		workingDir := filepath.Join(tmpDir, testFixtureLocalWithExcludeDir)
		workingDir, err := filepath.EvalSymlinks(workingDir)
		require.NoError(t, err)

		modulePaths := make(map[string]string, len(moduleNames))
		for _, moduleName := range moduleNames {
			modulePaths[moduleName] = filepath.Join(workingDir, moduleName)
		}

		applyAllStdout := bytes.Buffer{}
		applyAllStderr := bytes.Buffer{}

		// Apply modules according to test cases
		err = helpers.RunTerragruntCommand(
			t,
			fmt.Sprintf(
				"terragrunt run --all apply --non-interactive --working-dir %s %s",
				workingDir,
				tt.excludeArgs,
			),
			&applyAllStdout,
			&applyAllStderr,
		)
		require.NoError(t, err)

		helpers.LogBufferContentsLineByLine(t, applyAllStdout, "run --all apply stdout")
		helpers.LogBufferContentsLineByLine(t, applyAllStderr, "run --all apply stderr")

		// Check that the excluded module output is not present
		for _, modulePath := range modulePaths {
			showStdout := bytes.Buffer{}
			showStderr := bytes.Buffer{}

			err = helpers.RunTerragruntCommand(
				t,
				"terragrunt show --non-interactive --working-dir "+modulePath,
				&showStdout,
				&showStderr,
			)
			helpers.LogBufferContentsLineByLine(t, showStdout, "show stdout for "+modulePath)
			helpers.LogBufferContentsLineByLine(t, showStderr, "show stderr for "+modulePath)

			require.NoError(t, err)

			output := showStdout.String()
			for _, excludedModuleOutput := range tt.excludedModuleOutputs {
				assert.NotContains(t, output, excludedModuleOutput)
			}
		}
	}
}

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

	// Populate module paths.
	moduleNames := []string{
		"integration-env/aws/module-aws-a",
		"integration-env/gce/module-gce-b",
		"integration-env/gce/module-gce-c",
		"production-env/aws/module-aws-d",
		"production-env/gce/module-gce-e",
	}

	testCases := []struct {
		name                  string
		excludeArgs           string
		excludedModuleOutputs []string
	}{
		{
			name:                  "exclude gce modules",
			excludeArgs:           "--filter '!./**/gce/**'",
			excludedModuleOutputs: []string{"Module GCE B", "Module GCE C", "Module GCE E"},
		},
		{
			name:                  "exclude production env and gce c modules",
			excludeArgs:           "--filter '!./production-env/**' --filter '!./**/module-gce-c'",
			excludedModuleOutputs: []string{"Module GCE C", "Module AWS D", "Module GCE E"},
		},
		{
			name:                  "exclude integration env gce b and c modules and aws modules",
			excludeArgs:           "--filter '!./integration-env/gce/module-gce-b' --filter '!./integration-env/gce/module-gce-c' --filter '!./**/module-aws*'",
			excludedModuleOutputs: []string{"Module AWS A", "Module GCE B", "Module GCE C", "Module AWS D"},
		},
	}

	for _, tt := range testCases {
		tmpDir := helpers.NewGitServer(t).RenderFixture("fixtures/download")
		workingDir := filepath.Join(tmpDir, testFixtureLocalWithExcludeDir)
		workingDir, err := filepath.EvalSymlinks(workingDir)
		require.NoError(t, err)

		modulePaths := make(map[string]string, len(moduleNames))
		for _, moduleName := range moduleNames {
			modulePaths[moduleName] = filepath.Join(workingDir, moduleName)
		}

		// Apply modules according to test cases
		_, _, err = helpers.RunTerragruntCommandWithOutput(
			t,
			fmt.Sprintf(
				"terragrunt run --all apply --non-interactive --working-dir %s %s",
				workingDir,
				tt.excludeArgs,
			),
		)
		require.NoError(t, err)

		// Check that the excluded module output is not present
		for _, modulePath := range modulePaths {
			stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt show --non-interactive --working-dir "+modulePath)

			require.NoError(t, err)

			output := stdout
			for _, excludedModuleOutput := range tt.excludedModuleOutputs {
				assert.NotContains(t, output, excludedModuleOutput)
			}
		}
	}
}

/*
	TestIncludeDirs tests that the --queue-include-dir flag works as expected.

MAINTAINER NOTE: Why is this test _so slow_? It took 2 mins on my machine...

We really need to start reporting on test durations and decide on a budget for each test.
I'm not sure we're getting good value from the time taken on tests like this.
*/
func TestIncludeDirs(t *testing.T) {
	t.Parallel()

	// Populate module paths.
	unitNames := []string{
		"integration-env/aws/module-aws-a",
		"integration-env/gce/module-gce-b",
		"integration-env/gce/module-gce-c",
		"production-env/aws/module-aws-d",
		"production-env/gce/module-gce-e",
	}

	testCases := []struct {
		name                string
		includeArgs         string
		includedUnitOutputs []string
	}{
		{
			name:                "no-match",
			includeArgs:         "--queue-include-dir xyz",
			includedUnitOutputs: []string{},
		},
		{
			name:                "wildcard-aws",
			includeArgs:         "--queue-include-dir */aws",
			includedUnitOutputs: []string{"Module GCE B", "Module GCE C", "Module GCE E"},
		},
		{
			name:                "production-and-gce-c",
			includeArgs:         "--queue-include-dir production-env --queue-include-dir **/module-gce-c",
			includedUnitOutputs: []string{"Module GCE B", "Module AWS A"},
		},
		{
			name:                "specific-modules",
			includeArgs:         "--queue-include-dir integration-env/gce/module-gce-b --queue-include-dir integration-env/gce/module-gce-c --queue-include-dir **/module-aws*",
			includedUnitOutputs: []string{"Module GCE E"},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			tmpDir := helpers.NewGitServer(t).RenderFixture("fixtures/download")
			workingDir := filepath.Join(tmpDir, testFixtureLocalWithIncludeDir)
			workingDir, err := filepath.EvalSymlinks(workingDir)
			require.NoError(t, err)

			unitPaths := make(map[string]string, len(unitNames))
			for _, unitName := range unitNames {
				unitPaths[unitName] = filepath.Join(workingDir, unitName)
			}

			applyAllStdout := bytes.Buffer{}
			applyAllStderr := bytes.Buffer{}

			// Apply modules according to test case
			err = helpers.RunTerragruntCommand(
				t,
				fmt.Sprintf(
					"terragrunt run --all apply --non-interactive --working-dir %s %s",
					workingDir, tc.includeArgs,
				),
				&applyAllStdout,
				&applyAllStderr,
			)
			require.NoError(t, err)

			helpers.LogBufferContentsLineByLine(t, applyAllStdout, "run --all apply stdout")
			helpers.LogBufferContentsLineByLine(t, applyAllStderr, "run --all apply stderr")

			// Check that the included module output is present
			for _, modulePath := range unitPaths {
				showStdout := bytes.Buffer{}
				showStderr := bytes.Buffer{}

				err = helpers.RunTerragruntCommand(
					t,
					"terragrunt show --non-interactive --working-dir "+modulePath,
					&showStdout,
					&showStderr,
				)
				helpers.LogBufferContentsLineByLine(t, showStdout, "show stdout for "+modulePath)
				helpers.LogBufferContentsLineByLine(t, showStderr, "show stderr for "+modulePath)

				require.NoError(t, err)

				output := showStdout.String()
				for _, includedUnitOutput := range tc.includedUnitOutputs {
					assert.NotContains(t, output, includedUnitOutput)
				}
			}
		})
	}
}

/*
	TestIncludeDirsWithFilter tests that the --filter flag works as expected, just like in TestIncludeDirs.

MAINTAINER NOTE: Why is this test _so slow_? It took 2 mins on my machine...

We really need to start reporting on test durations and decide on a budget for each test.
I'm not sure we're getting good value from the time taken on tests like this.
*/
func TestIncludeDirsWithFilter(t *testing.T) {
	t.Parallel()

	// Copy the entire download fixture directory to ensure all referenced sources are available
	tmpDir := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	workingDir := filepath.Join(tmpDir, testFixtureLocalWithIncludeDir)
	workingDir, err := filepath.EvalSymlinks(workingDir)
	require.NoError(t, err)

	// Populate paths.
	unitNames := []string{
		"integration-env/aws/module-aws-a",
		"integration-env/gce/module-gce-b",
		"integration-env/gce/module-gce-c",
		"production-env/aws/module-aws-d",
		"production-env/gce/module-gce-e",
	}

	testCases := []struct {
		includeArgs         string
		includedUnitOutputs []string
	}{
		{
			includeArgs:         "--filter xyz",
			includedUnitOutputs: []string{},
		},
		{
			includeArgs:         "--filter ./*/aws/*",
			includedUnitOutputs: []string{"Module GCE B", "Module GCE C", "Module GCE E"},
		},
		{
			includeArgs:         "--filter production-env --filter ./**/module-gce-c",
			includedUnitOutputs: []string{"Module GCE B", "Module AWS A"},
		},
		{
			includeArgs:         "--filter ./integration-env/gce/module-gce-b --filter ./integration-env/gce/module-gce-c --filter ./**/module-aws**",
			includedUnitOutputs: []string{"Module GCE E"},
		},
	}

	unitPaths := make(map[string]string, len(unitNames))
	for _, unitName := range unitNames {
		unitPaths[unitName] = filepath.Join(workingDir, unitName)
	}

	for _, tc := range testCases {
		applyAllStdout := bytes.Buffer{}
		applyAllStderr := bytes.Buffer{}

		// Cleanup all modules directories.
		helpers.CleanupTerragruntFolder(t, workingDir)

		for _, unitPath := range unitPaths {
			helpers.CleanupTerragruntFolder(t, unitPath)
		}

		// Apply modules according to test cases
		err := helpers.RunTerragruntCommand(
			t,
			fmt.Sprintf(
				"terragrunt run --all apply --non-interactive --working-dir %s %s",
				workingDir, tc.includeArgs,
			),
			&applyAllStdout,
			&applyAllStderr,
		)
		require.NoError(t, err)

		helpers.LogBufferContentsLineByLine(t, applyAllStdout, "run --all apply stdout")
		helpers.LogBufferContentsLineByLine(t, applyAllStderr, "run --all apply stderr")

		// Check that the included module output is present
		for _, unitPath := range unitPaths {
			showStdout := bytes.Buffer{}
			showStderr := bytes.Buffer{}

			err = helpers.RunTerragruntCommand(
				t,
				"terragrunt show --non-interactive --working-dir "+unitPath,
				&showStdout,
				&showStderr,
			)
			helpers.LogBufferContentsLineByLine(t, showStdout, "show stdout for "+unitPath)
			helpers.LogBufferContentsLineByLine(t, showStderr, "show stderr for "+unitPath)

			require.NoError(t, err)

			output := showStdout.String()
			for _, includedUnitOutput := range tc.includedUnitOutputs {
				assert.NotContains(t, output, includedUnitOutput)
			}
		}
	}
}

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

	modulePaths := []string{
		"amazing-app/k8s",
		"clusters/eks",
		"testapp/k8s",
	}

	tmpPath, err := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureRegressions))
	require.NoError(t, err)

	testPath := filepath.Join(tmpPath, testFixtureRegressions, "exclude-dependency")
	for _, modulePath := range modulePaths {
		helpers.CleanupTerragruntFolder(t, filepath.Join(testPath, modulePath))
	}

	includedModulesWithNone := helpers.RunValidateAllWithFilteredPlusDependenciesAndGetIncludedModules(t, testPath, []string{})
	assert.NotEmpty(t, includedModulesWithNone)

	includedModulesWithAmzApp := helpers.RunValidateAllWithFilteredPlusDependenciesAndGetIncludedModules(
		t,
		testPath,
		[]string{"amazing-app/k8s"},
	)
	assert.Equal(
		t,
		[]string{"amazing-app/k8s", "clusters/eks"},
		includedModulesWithAmzApp,
	)

	includedModulesWithTestApp := helpers.RunValidateAllWithFilteredPlusDependenciesAndGetIncludedModules(
		t,
		testPath,
		[]string{"testapp/k8s"},
	)
	assert.Equal(
		t,
		[]string{"clusters/eks", "testapp/k8s"},
		includedModulesWithTestApp,
	)
}

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

	modulePaths := []string{
		"amazing-app/k8s",
		"clusters/eks",
		"testapp/k8s",
	}

	tmpPath, err := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureRegressions))
	require.NoError(t, err)

	testPath := filepath.Join(tmpPath, testFixtureRegressions, "exclude-dependency")
	helpers.CleanupTerragruntFolder(t, testPath)

	for _, modulePath := range modulePaths {
		helpers.CleanupTerragruntFolder(t, filepath.Join(testPath, modulePath))
	}

	includedModulesWithAmzApp := helpers.RunValidateAllWithIncludeAndGetIncludedModules(
		t,
		testPath,
		[]string{"amazing-app/k8s"},
	)
	assert.Equal(
		t,
		[]string{"amazing-app/k8s"},
		includedModulesWithAmzApp,
	)

	includedModulesWithTestApp := helpers.RunValidateAllWithIncludeAndGetIncludedModules(
		t,
		testPath,
		[]string{"testapp/k8s"},
	)
	assert.Equal(
		t,
		[]string{"testapp/k8s"},
		includedModulesWithTestApp,
	)
}

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

	modules := []string{
		"module-a",
		"module-b",
	}

	helpers.CleanupTerraformFolder(t, testFixtureExternalDependence)

	for _, module := range modules {
		helpers.CleanupTerraformFolder(t, filepath.Join(testFixtureExternalDependence, module))
	}

	var (
		applyAllStdout bytes.Buffer
		applyAllStderr bytes.Buffer
	)

	rootPath := helpers.CopyEnvironment(t, testFixtureExternalDependence)
	modulePath := filepath.Join(rootPath, testFixtureExternalDependence, "module-b")

	err := helpers.RunTerragruntCommand(t, "terragrunt run --all apply --non-interactive --queue-include-external --tf-forward-stdout --working-dir "+modulePath, &applyAllStdout, &applyAllStderr)
	helpers.LogBufferContentsLineByLine(t, applyAllStdout, "run --all apply stdout")
	helpers.LogBufferContentsLineByLine(t, applyAllStderr, "run --all apply stderr")

	applyAllStdoutString := applyAllStdout.String()

	if err != nil {
		t.Errorf("Did not expect to get error: %s", err.Error())
	}

	for _, module := range modules {
		assert.Contains(t, applyAllStdoutString, "Hello World, "+module)
	}
}

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

	modules := []string{
		"module-a",
		"module-b",
	}

	helpers.CleanupTerraformFolder(t, testFixtureExternalDependence)

	for _, module := range modules {
		helpers.CleanupTerraformFolder(t, filepath.Join(testFixtureExternalDependence, module))
	}

	var (
		applyAllStdout bytes.Buffer
		applyAllStderr bytes.Buffer
	)

	rootPath := helpers.CopyEnvironment(t, testFixtureExternalDependence)
	modulePath := filepath.Join(rootPath, testFixtureExternalDependence, "module-b")

	err := helpers.RunTerragruntCommand(t, "terragrunt run --all apply --non-interactive --filter '{./**}...' --tf-forward-stdout --working-dir "+modulePath, &applyAllStdout, &applyAllStderr)
	helpers.LogBufferContentsLineByLine(t, applyAllStdout, "run --all apply stdout")
	helpers.LogBufferContentsLineByLine(t, applyAllStderr, "run --all apply stderr")

	applyAllStdoutString := applyAllStdout.String()

	if err != nil {
		t.Errorf("Did not expect to get error: %s", err.Error())
	}

	for _, module := range modules {
		assert.Contains(t, applyAllStdoutString, "Hello World, "+module)
	}
}

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

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	fixtureRoot := filepath.Join(tmpEnvPath, testFixtureLocalPreventDestroy)

	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+fixtureRoot)

	err := helpers.RunTerragruntCommand(t, "terragrunt destroy -auto-approve --non-interactive --working-dir "+fixtureRoot, os.Stdout, os.Stderr)

	if assert.Error(t, err) {
		var target run.ModuleIsProtected
		assert.ErrorAs(t, err, &target)
	}
}

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

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")

	fixtureRoot := filepath.Join(tmpEnvPath, testFixtureLocalPreventDestroy)
	helpers.RunTerragrunt(t, "terragrunt apply -auto-approve --non-interactive --working-dir "+fixtureRoot)

	err := helpers.RunTerragruntCommand(t, "terragrunt apply -destroy -auto-approve --non-interactive --working-dir "+fixtureRoot, os.Stdout, os.Stderr)

	if assert.Error(t, err) {
		var target run.ModuleIsProtected
		assert.ErrorAs(t, err, &target)
	}
}

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

	tmpEnvPath := helpers.NewGitServer(t).RenderFixture("fixtures/download")
	rootPath := filepath.Join(tmpEnvPath, testFixtureLocalPreventDestroyDependencies)

	// Populate module paths.
	moduleNames := []string{
		"module-a",
		"module-b",
		"module-c",
		"module-d",
		"module-e",
	}

	modulePaths := make(map[string]string, len(moduleNames))
	for _, moduleName := range moduleNames {
		modulePaths[moduleName] = filepath.Join(rootPath, moduleName)
	}

	// Cleanup all modules directories.
	helpers.CleanupTerraformFolder(t, rootPath)

	for _, modulePath := range modulePaths {
		helpers.CleanupTerraformFolder(t, modulePath)
	}

	var (
		applyAllStdout bytes.Buffer
		applyAllStderr bytes.Buffer
	)

	// Apply and destroy all modules.
	err := helpers.RunTerragruntCommand(
		t,
		"terragrunt run --all apply --non-interactive --working-dir "+rootPath,
		&applyAllStdout,
		&applyAllStderr,
	)
	require.NoError(t, err)

	helpers.LogBufferContentsLineByLine(t, applyAllStdout, "run --all apply stdout")
	helpers.LogBufferContentsLineByLine(t, applyAllStderr, "run --all apply stderr")

	var (
		destroyAllStdout bytes.Buffer
		destroyAllStderr bytes.Buffer
	)

	err = helpers.RunTerragruntCommand(t, "terragrunt run --all destroy --non-interactive --working-dir "+rootPath, &destroyAllStdout, &destroyAllStderr)
	helpers.LogBufferContentsLineByLine(t, destroyAllStdout, "run --all destroy stdout")
	helpers.LogBufferContentsLineByLine(t, destroyAllStderr, "run --all destroy stderr")

	require.NoError(t, err)

	// Check that modules C, D and E were deleted and modules A and B weren't.
	for moduleName, modulePath := range modulePaths {
		var (
			showStdout bytes.Buffer
			showStderr bytes.Buffer
		)

		err = helpers.RunTerragruntCommand(t, "terragrunt show --non-interactive --tf-forward-stdout --working-dir "+modulePath, &showStdout, &showStderr)
		helpers.LogBufferContentsLineByLine(t, showStdout, "show stdout for "+modulePath)
		helpers.LogBufferContentsLineByLine(t, showStderr, "show stderr for "+modulePath)

		require.NoError(t, err)

		output := showStdout.String()

		switch moduleName {
		case "module-a":
			assert.Contains(t, output, "Hello, Module A")
		case "module-b":
			assert.Contains(t, output, "Hello, Module B")
		case "module-c":
			assert.NotContains(t, output, "Hello, Module C")
		case "module-d":
			assert.NotContains(t, output, "Hello, Module D")
		case "module-e":
			assert.NotContains(t, output, "Hello, Module E")
		}
	}
}

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

	fixturePath := "fixtures/download/remote"

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := mirror.RenderFixture(fixturePath)
	testPath := filepath.Join(tmpEnvPath, fixturePath)
	helpers.CleanupTerraformFolder(t, testPath)

	// CAS is enabled by default.
	var (
		stdout bytes.Buffer
		stderr bytes.Buffer
	)

	cmd := "terragrunt apply --auto-approve --non-interactive --log-level debug --working-dir " + testPath
	err := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)
	require.NoError(t, err)

	assert.Contains(t, stderr.String(), "Downloading Terraform configurations")
}

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

	fixturePath := "fixtures/download/remote-commit-ref"

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := mirror.RenderFixture(fixturePath)
	testPath := filepath.Join(tmpEnvPath, fixturePath)
	helpers.CleanupTerraformFolder(t, testPath)

	applyCmd := "terragrunt apply --auto-approve --non-interactive --working-dir " + testPath
	require.NoError(t, helpers.RunTerragruntCommand(t, applyCmd, io.Discard, io.Discard))

	var (
		stdout bytes.Buffer
		stderr bytes.Buffer
	)

	outputCmd := "terragrunt output -raw test --non-interactive --working-dir " + testPath
	require.NoError(t, helpers.RunTerragruntCommand(t, outputCmd, &stdout, &stderr))

	assert.Equal(t, "Hello, World", stdout.String())
}

// TestDownloadWithCASMutable exercises the end-to-end path for `mutable = true`
// on a `terraform` block: CAS materializes the source as a writable copy
// rather than a read-only hardlink, so every `.tf` file must land with the
// user-write bit set. The non-mutable path would strip write bits, producing
// 0o444 / 0o555 instead.
func TestDownloadWithCASMutable(t *testing.T) {
	t.Parallel()

	fixturePath := "fixtures/download/remote-mutable"

	mirror := helpers.NewGitServer(t)
	tmpEnvPath := mirror.RenderFixture(fixturePath)
	testPath := filepath.Join(tmpEnvPath, fixturePath)
	helpers.CleanupTerraformFolder(t, testPath)

	var (
		stdout bytes.Buffer
		stderr bytes.Buffer
	)

	cmd := "terragrunt apply --auto-approve --non-interactive --log-level debug --working-dir " + testPath
	require.NoError(t, helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr))

	cacheDir := filepath.Join(testPath, ".terragrunt-cache")

	var checked int

	require.NoError(t, filepath.WalkDir(cacheDir, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if d.IsDir() || filepath.Ext(path) != ".tf" {
			return nil
		}

		info, statErr := d.Info()
		if statErr != nil {
			return statErr
		}

		assert.NotZero(t, info.Mode().Perm()&0o200,
			"mutable=true must materialize %s as writable (got perms %#o)", path, info.Mode().Perm())

		checked++

		return nil
	}))

	require.Positive(t, checked, "expected at least one .tf file under %s", cacheDir)
}
