package format_test

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

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

	"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl/format"
	"github.com/gruntwork-io/terragrunt/internal/filter"
	"github.com/gruntwork-io/terragrunt/internal/util"
	"github.com/gruntwork-io/terragrunt/internal/venv"
	"github.com/gruntwork-io/terragrunt/pkg/options"
	"github.com/gruntwork-io/terragrunt/test/helpers"
	"github.com/gruntwork-io/terragrunt/test/helpers/logger"
)

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "./testdata/fixtures"),
		t.Name(),
		func(path string) bool { return true },
	)

	t.Cleanup(func() {
		os.RemoveAll(tmpPath)
	})

	require.NoError(t, err)

	expected, err := util.ReadFileAsString("./testdata/fixtures/expected.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	tgOptions.WorkingDir = tmpPath
	tgOptions.HclExclude = []string{".history"}

	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.NoError(t, err)

	t.Run("group", func(t *testing.T) {
		t.Parallel()

		dirs := []string{
			"terragrunt.hcl",
			"a/terragrunt.hcl",
			"a/b/c/terragrunt.hcl",
			"a/b/c/d/services.hcl",
			"a/b/c/d/e/terragrunt.hcl",
		}
		for _, dir := range dirs {
			// Capture range variable into for block so it doesn't change while looping
			t.Run(dir, func(t *testing.T) {
				t.Parallel()

				tgHclPath := filepath.Join(tmpPath, dir)
				actual, err := util.ReadFileAsString(tgHclPath)
				require.NoError(t, err)
				assert.Equal(t, expected, actual)
			})
		}

		// check to make sure the file in the `.terragrunt-cache` folder was ignored and untouched
		t.Run("terragrunt-cache", func(t *testing.T) {
			t.Parallel()

			originalTgHclPath := "./testdata/fixtures/ignored/.terragrunt-cache/terragrunt.hcl"
			original, err := util.ReadFileAsString(originalTgHclPath)
			require.NoError(t, err)

			tgHclPath := filepath.Join(tmpPath, "ignored/.terragrunt-cache/terragrunt.hcl")
			actual, err := util.ReadFileAsString(tgHclPath)
			require.NoError(t, err)

			assert.Equal(t, original, actual)
		})

		// Finally, check to make sure the file in the `.history` folder was ignored and untouched
		t.Run("history", func(t *testing.T) {
			t.Parallel()

			originalTgHclPath := "./testdata/fixtures/ignored/.history/terragrunt.hcl"
			original, err := util.ReadFileAsString(originalTgHclPath)
			require.NoError(t, err)

			tgHclPath := filepath.Join(tmpPath, "ignored/.history/terragrunt.hcl")
			actual, err := util.ReadFileAsString(tgHclPath)
			require.NoError(t, err)

			assert.Equal(t, original, actual)
		})
	})
}

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "../../../../../test/fixtures/hclfmt-errors"),
		t.Name(),
		func(path string) bool { return true },
	)
	t.Cleanup(func() {
		os.RemoveAll(tmpPath)
	})
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	dirs := []string{
		"dangling-attribute",
		"invalid-character",
		"invalid-key",
	}
	for _, dir := range dirs {
		// Capture range variable into for block so it doesn't change while looping
		t.Run(dir, func(t *testing.T) {
			t.Parallel()

			tgHclDir := filepath.Join(tmpPath, dir)
			l, newTgOptions, err := tgOptions.CloneWithConfigPath(
				logger.CreateLogger(),
				tgOptions.TerragruntConfigPath,
			)
			require.NoError(t, err)

			newTgOptions.WorkingDir = tgHclDir

			err = format.Run(t.Context(), l, venv.OSVenv(), newTgOptions)
			require.Error(t, err)
		})
	}
}

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "../../../../../test/fixtures/hclfmt-check"),
		t.Name(),
		func(path string) bool { return true },
	)

	t.Cleanup(func() {
		os.RemoveAll(tmpPath)
	})

	require.NoError(t, err)

	expected, err := os.ReadFile("../../../../../test/fixtures/hclfmt-check/expected.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	tgOptions.Check = true
	tgOptions.WorkingDir = tmpPath

	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.NoError(t, err)

	dirs := []string{
		"terragrunt.hcl",
		"a/terragrunt.hcl",
		"a/b/c/terragrunt.hcl",
		"a/b/c/d/services.hcl",
		"a/b/c/d/e/terragrunt.hcl",
	}

	for _, dir := range dirs {
		// Capture range variable into for block so it doesn't change while looping
		t.Run(dir, func(t *testing.T) {
			t.Parallel()

			tgHclPath := filepath.Join(tmpPath, dir)
			actual, err := os.ReadFile(tgHclPath)
			require.NoError(t, err)
			assert.Equal(t, expected, actual)
		})
	}
}

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "../../../../../test/fixtures/hclfmt-check-errors"),
		t.Name(),
		func(path string) bool { return true },
	)

	t.Cleanup(func() {
		os.RemoveAll(tmpPath)
	})

	require.NoError(t, err)

	expected, err := os.ReadFile("../../../../../test/fixtures/hclfmt-check-errors/expected.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	tgOptions.Check = true
	tgOptions.WorkingDir = tmpPath

	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.Error(t, err)

	dirs := []string{
		"terragrunt.hcl",
		"a/terragrunt.hcl",
		"a/b/c/terragrunt.hcl",
		"a/b/c/d/services.hcl",
		"a/b/c/d/e/terragrunt.hcl",
	}

	for _, dir := range dirs {
		t.Run(dir, func(t *testing.T) {
			t.Parallel()

			tgHclPath := filepath.Join(tmpPath, dir)
			actual, err := os.ReadFile(tgHclPath)
			require.NoError(t, err)
			assert.Equal(t, expected, actual)
		})
	}
}

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "./testdata/fixtures"),
		t.Name(),
		func(path string) bool { return true },
	)

	t.Cleanup(func() {
		os.RemoveAll(tmpPath)
	})

	require.NoError(t, err)

	expected, err := os.ReadFile("./testdata/fixtures/expected.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	// format only the hcl file contained within the a subdirectory of the fixture
	tgOptions.HclFile = "a/terragrunt.hcl"
	tgOptions.WorkingDir = tmpPath
	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.NoError(t, err)

	// test that the formatting worked on the specified file
	t.Run("formatted", func(t *testing.T) {
		t.Run(tgOptions.HclFile, func(t *testing.T) {
			t.Parallel()

			tgHclPath := filepath.Join(tmpPath, tgOptions.HclFile)
			formatted, readErr := os.ReadFile(tgHclPath)
			require.NoError(t, readErr)
			assert.Equal(t, expected, formatted)
		})
	})

	dirs := []string{
		"terragrunt.hcl",
		"a/b/c/terragrunt.hcl",
	}

	original, err := os.ReadFile("./testdata/fixtures/terragrunt.hcl")
	require.NoError(t, err)

	// test that none of the other files were formatted
	for _, dir := range dirs {
		// Capture range variable into for block so it doesn't change while looping
		t.Run(dir, func(t *testing.T) {
			t.Parallel()

			testingPath := filepath.Join(tmpPath, dir)
			actual, err := os.ReadFile(testingPath)
			require.NoError(t, err)
			assert.Equal(t, original, actual)
		})
	}
}

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

	realStdin := os.Stdin
	realStdout := os.Stdout

	tempStdoutFile, err := os.CreateTemp(helpers.TmpDirWOSymlinks(t), "stdout.hcl")

	defer func() {
		_ = tempStdoutFile.Close()
	}()

	require.NoError(t, err)

	os.Stdout = tempStdoutFile

	defer func() { os.Stdout = realStdout }()

	os.Stdin, err = os.Open("../../../../../test/fixtures/hclfmt-stdin/terragrunt.hcl")

	defer func() { os.Stdin = realStdin }()

	require.NoError(t, err)

	expected, err := os.ReadFile("../../../../../test/fixtures/hclfmt-stdin/expected.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	// format hcl from stdin
	tgOptions.HclFromStdin = true
	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.NoError(t, err)

	formatted, err := os.ReadFile(tempStdoutFile.Name())
	require.NoError(t, err)
	assert.Equal(t, expected, formatted)
}

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "../../../../../test/fixtures/hclfmt-heredoc"),
		t.Name(),
		func(path string) bool { return true },
	)
	defer os.RemoveAll(tmpPath)

	require.NoError(t, err)

	expected, err := os.ReadFile("../../../../../test/fixtures/hclfmt-heredoc/expected.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	tgOptions.WorkingDir = tmpPath

	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.NoError(t, err)

	tgHclPath := filepath.Join(tmpPath, "terragrunt.hcl")
	actual, err := os.ReadFile(tgHclPath)
	require.NoError(t, err)
	assert.Equal(t, expected, actual)
}

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

	tmpPath := t.TempDir()
	err := util.CopyFolderContentsWithFilter(
		logger.CreateLogger(),
		helpers.MustAbs(t, filepath.Join(".", "testdata", "fixtures")),
		tmpPath,
		".copymanifest",
		func(path string) bool { return true },
	)
	require.NoError(t, err)

	expected, err := util.ReadFileAsString(
		filepath.Join(".", "testdata", "fixtures", "expected.hcl"),
	)
	require.NoError(t, err)

	original, err := util.ReadFileAsString(
		filepath.Join(".", "testdata", "fixtures", "terragrunt.hcl"),
	)
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	// Format only specific files, mixing relative and absolute paths, including a non-hcl file
	absPath := filepath.Join(tmpPath, "a", "b", "c", "terragrunt.hcl")
	files := []string{
		"terragrunt.hcl", // relative, should be formatted
		absPath,          // absolute, should be formatted
		filepath.Join("a", "b", "c", "d", "services.hcl"), // relative, should be formatted
		filepath.Join("a", "terragrunt.hcl"),              // relative, should be formatted
		"README.md",                                       // non-hcl, should be skipped
	}

	err = format.RunForFiles(
		t.Context(),
		logger.CreateLogger(),
		venv.OSVenv(),
		tgOptions,
		tmpPath,
		files,
	)
	require.NoError(t, err)

	// Verify formatted files
	for _, rel := range []string{
		"terragrunt.hcl",
		filepath.Join("a", "b", "c", "terragrunt.hcl"),
		filepath.Join("a", "b", "c", "d", "services.hcl"),
		filepath.Join("a", "terragrunt.hcl"),
	} {
		actual, err := util.ReadFileAsString(filepath.Join(tmpPath, rel))
		require.NoError(t, err)
		assert.Equal(t, expected, actual, "File %s should be formatted", rel)
	}

	// Verify file NOT in the list was left untouched
	actual, err := util.ReadFileAsString(
		filepath.Join(tmpPath, "a", "b", "c", "d", "e", "terragrunt.hcl"),
	)
	require.NoError(t, err)
	assert.Equal(t, original, actual, "File a/b/c/d/e/terragrunt.hcl should NOT be formatted")
}

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

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	err = format.RunForFiles(
		t.Context(),
		logger.CreateLogger(),
		venv.OSVenv(),
		tgOptions,
		t.TempDir(),
		nil,
	)
	require.NoError(t, err)
}

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "./testdata/fixtures"),
		t.Name(),
		func(path string) bool { return true },
	)

	t.Cleanup(func() {
		os.RemoveAll(tmpPath)
	})

	require.NoError(t, err)

	expected, err := util.ReadFileAsString("./testdata/fixtures/expected.hcl")
	require.NoError(t, err)

	original, err := util.ReadFileAsString("./testdata/fixtures/terragrunt.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	err = tgOptions.Experiments.EnableExperiment("filter-flag")
	require.NoError(t, err)

	tgOptions.WorkingDir = tmpPath

	filters, parseErr := filter.ParseFilterQueries(logger.CreateLogger(), []string{"./a/b/**"})
	require.NoError(t, parseErr)

	tgOptions.Filters = filters

	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.NoError(t, err)

	t.Run("group", func(t *testing.T) {
		t.Parallel()

		formattedDirs := []string{
			"a/b/c/terragrunt.hcl",
			"a/b/c/d/services.hcl",
			"a/b/c/d/e/terragrunt.hcl",
		}
		for _, dir := range formattedDirs {
			t.Run(dir, func(t *testing.T) {
				t.Parallel()

				tgHclPath := filepath.Join(tmpPath, dir)
				actual, err := util.ReadFileAsString(tgHclPath)
				require.NoError(t, err)
				assert.Equal(t, expected, actual, "File %s should be formatted", dir)
			})
		}

		unformattedDirs := []string{
			"terragrunt.hcl",
			"a/terragrunt.hcl",
		}
		for _, dir := range unformattedDirs {
			t.Run(dir, func(t *testing.T) {
				t.Parallel()

				tgHclPath := filepath.Join(tmpPath, dir)
				actual, err := util.ReadFileAsString(tgHclPath)
				require.NoError(t, err)
				assert.Equal(t, original, actual, "File %s should NOT be formatted", dir)
			})
		}
	})
}

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "./testdata/fixtures"),
		t.Name(),
		func(path string) bool { return true },
	)

	t.Cleanup(func() {
		os.RemoveAll(tmpPath)
	})

	require.NoError(t, err)

	expected, err := util.ReadFileAsString("./testdata/fixtures/expected.hcl")
	require.NoError(t, err)

	original, err := util.ReadFileAsString("./testdata/fixtures/terragrunt.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	err = tgOptions.Experiments.EnableExperiment("filter-flag")
	require.NoError(t, err)

	tgOptions.WorkingDir = tmpPath

	filters, parseErr := filter.ParseFilterQueries(logger.CreateLogger(), []string{
		filepath.Join(tmpPath, "terragrunt.hcl"),
		"./a/b/c/d/e/**",
	})
	require.NoError(t, parseErr)

	tgOptions.Filters = filters

	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.NoError(t, err)

	t.Run("group", func(t *testing.T) {
		t.Parallel()

		formattedDirs := []string{
			"terragrunt.hcl",
			"a/b/c/d/e/terragrunt.hcl",
		}
		for _, dir := range formattedDirs {
			t.Run(dir, func(t *testing.T) {
				t.Parallel()

				tgHclPath := filepath.Join(tmpPath, dir)
				actual, err := util.ReadFileAsString(tgHclPath)
				require.NoError(t, err)
				assert.Equal(t, expected, actual, "File %s should be formatted", dir)
			})
		}

		unformattedDirs := []string{
			"a/terragrunt.hcl",
			"a/b/c/terragrunt.hcl",
			"a/b/c/d/services.hcl",
		}
		for _, dir := range unformattedDirs {
			t.Run(dir, func(t *testing.T) {
				t.Parallel()

				tgHclPath := filepath.Join(tmpPath, dir)
				actual, err := util.ReadFileAsString(tgHclPath)
				require.NoError(t, err)
				assert.Equal(t, original, actual, "File %s should NOT be formatted", dir)
			})
		}
	})
}

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

	tmpPath, err := util.CopyFolderToTemp(
		helpers.MustAbs(t, "./testdata/fixtures"),
		t.Name(),
		func(path string) bool { return true },
	)

	t.Cleanup(func() {
		os.RemoveAll(tmpPath)
	})

	require.NoError(t, err)

	expected, err := util.ReadFileAsString("./testdata/fixtures/expected.hcl")
	require.NoError(t, err)

	original, err := util.ReadFileAsString("./testdata/fixtures/terragrunt.hcl")
	require.NoError(t, err)

	tgOptions, err := options.NewTerragruntOptionsForTest("")
	require.NoError(t, err)

	err = tgOptions.Experiments.EnableExperiment("filter-flag")
	require.NoError(t, err)

	tgOptions.WorkingDir = tmpPath

	filters, parseErr := filter.ParseFilterQueries(logger.CreateLogger(), []string{
		"./a/**",
		"!./a/b/c/d/**",
	})
	require.NoError(t, parseErr)

	tgOptions.Filters = filters

	err = format.Run(t.Context(), logger.CreateLogger(), venv.OSVenv(), tgOptions)
	require.NoError(t, err)

	t.Run("group", func(t *testing.T) {
		t.Parallel()

		formattedDirs := []string{
			"a/terragrunt.hcl",
			"a/b/c/terragrunt.hcl",
		}
		for _, dir := range formattedDirs {
			t.Run(dir, func(t *testing.T) {
				t.Parallel()

				tgHclPath := filepath.Join(tmpPath, dir)
				actual, err := util.ReadFileAsString(tgHclPath)
				require.NoError(t, err)
				assert.Equal(t, expected, actual, "File %s should be formatted", dir)
			})
		}

		unformattedDirs := []string{
			"terragrunt.hcl",
			"a/b/c/d/services.hcl",
			"a/b/c/d/e/terragrunt.hcl",
		}
		for _, dir := range unformattedDirs {
			t.Run(dir, func(t *testing.T) {
				t.Parallel()

				tgHclPath := filepath.Join(tmpPath, dir)
				actual, err := util.ReadFileAsString(tgHclPath)
				require.NoError(t, err)
				assert.Equal(t, original, actual, "File %s should NOT be formatted", dir)
			})
		}
	})
}
