//go:build windows
// +build windows

package test_test

import (
	"bytes"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"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 (
	testFixtureDownloadPath                         = "fixtures/download"
	testFixtureLocalRelativeArgsWindowsDownloadPath = "fixtures/download/local-windows"
	testFixtureManifestRemoval                      = "fixtures/manifest-removal"
	testFixtureTflintNoIssuesFound                  = "fixtures/tflint/no-issues-found"
	testFixtureProviderCacheWindowsRemoteURL        = "fixtures/provider-cache/windows-remote-url"

	tempDir = `C:\tmp`
)

func TestMain(m *testing.M) {
	// By default, TempDir creates a temporary directory inside the user directory
	// `C:/Users/circleci/AppData/Local/Temp/`, which ends up exceeding the maximum allowed length
	// and causes the error: "fatal: '$GIT_DIR' too big". Example:
	// "C:/Users/circleci/AppData/Local/Temp/TestWindowsLocalWithRelativeExtraArgsWindows1263358614/001/fixtures/download/local-windows/.terragrunt-cache/rviFlp3V5mrXldwi6Hbi8p2rDL0/U0tL3quoR7Yt-oR6jROJomrYpTs".
	// Solution, shorten the TMP path.

	envVars := map[string]string{"TMP": "", "TEMP": ""}

	// Save current values to restore them at the end.
	for name := range envVars {
		envVars[name] = os.Getenv(name)
	}

	defer func() {
		// Restore previous values.
		for name, val := range envVars {
			os.Setenv(name, val)
		}
	}()

	if _, err := os.Stat(tempDir); errors.Is(err, fs.ErrNotExist) {
		if err := os.Mkdir(tempDir, os.ModePerm); err != nil {
			fmt.Printf("Failed to create temp dir due to error: %v", err)
			os.Exit(1)
		}
	} else {
		// Verify write permissions
		testFile := filepath.Join(tempDir, ".write_test")
		if err := os.WriteFile(testFile, []byte(""), 0o666); err != nil {
			fmt.Printf("Temp dir %s is not writable: %v", tempDir, err)
			os.Exit(1)
		}

		os.Remove(testFile)
	}

	// Set temporary values.
	for name := range envVars {
		if err := os.Setenv(name, tempDir); err != nil {
			fmt.Printf("Failed to set env var %s=%s due to error: %v", name, tempDir, err)
			os.Exit(1)
		}
	}

	os.Exit(m.Run())
}

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

	rootPath := CopyEnvironmentWithTflint(t, testFixtureDownloadPath)
	modulePath := filepath.Join(rootPath, testFixtureLocalRelativeArgsWindowsDownloadPath)

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

	// 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", modulePath))
}

// TestWindowsTerragruntSourceMapDebug copies the test/fixtures/source-map directory to a new Windows path
// and then ensures that the TG_SOURCE_MAP env var can be used to swap out git sources for local modules
func TestWindowsTerragruntSourceMapDebug(t *testing.T) {
	testCases := []struct {
		name string
	}{
		{
			name: "multiple-match",
		},
		{
			name: "multiple-with-dependency",
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			fixtureSourceMapPath := "fixtures/source-map"
			helpers.CleanupTerraformFolder(t, fixtureSourceMapPath)
			targetPath := "C:\\test\\infrastructure-modules/"
			CopyEnvironmentToPath(t, fixtureSourceMapPath, targetPath)
			rootPath := filepath.Join(targetPath, fixtureSourceMapPath)

			t.Setenv(
				"TG_SOURCE_MAP",
				strings.Join(
					[]string{
						fmt.Sprintf("git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=%s", targetPath),
						fmt.Sprintf("git::ssh://git@github.com/gruntwork-io/another-dont-exist.git=%s", targetPath),
					},
					",",
				),
			)
			tgPath := filepath.Join(rootPath, tc.name)
			tgArgs := fmt.Sprintf("terragrunt run --all apply --non-interactive --working-dir '%s'", tgPath)
			helpers.RunTerragrunt(t, tgArgs)
		})
	}
}

// Get rid of this once we have no internal tflint
func TestWindowsTflintIsInvoked(t *testing.T) {
	out := new(bytes.Buffer)
	errOut := new(bytes.Buffer)
	rootPath := CopyEnvironmentWithTflint(t, testFixtureTflintNoIssuesFound)
	modulePath := filepath.Join(rootPath, testFixtureTflintNoIssuesFound)
	err := helpers.RunTerragruntCommand(t, fmt.Sprintf("terragrunt plan --log-level debug --working-dir %s", modulePath), out, errOut)
	assert.NoError(t, err)

	assert.NotContains(t, errOut.String(), "Error while running tflint with args:")
	assert.NotContains(t, errOut.String(), "Tflint found issues in the project. Check for the tflint logs above.")

	// TFLint config should be found in the original working directory, not inside .terragrunt-cache
	// The config path should end with .tflint.hcl but NOT be inside .terragrunt-cache
	// Use cross-platform regex patterns that handle both Unix / and Windows \ path separators
	found, err := regexp.MatchString(`--config\s+[^\s]*\.tflint\.hcl`, errOut.String())
	assert.NoError(t, err)
	assert.True(t, found, "Expected tflint to be invoked with --config pointing to .tflint.hcl")
	assert.NotRegexp(t, `--config\s+[^\s]*[/\\]?\.terragrunt-cache`, errOut.String(), "TFLint config should not be inside cache directory")
}

func TestWindowsManifestFileIsRemoved(t *testing.T) {
	out := new(bytes.Buffer)
	errOut := new(bytes.Buffer)
	rootPath := CopyEnvironmentWithTflint(t, testFixtureManifestRemoval)
	modulePath := filepath.Join(rootPath, testFixtureManifestRemoval, "app")
	err := helpers.RunTerragruntCommand(t, fmt.Sprintf("terragrunt plan --non-interactive --working-dir %s", modulePath), out, errOut)
	assert.NoError(t, err)

	info1, err := fileInfo(modulePath, ".terragrunt-module-manifest")
	assert.NoError(t, err)
	assert.NotNil(t, info1)

	out = new(bytes.Buffer)
	errOut = new(bytes.Buffer)
	err = helpers.RunTerragruntCommand(t, fmt.Sprintf("terragrunt plan --non-interactive --working-dir %s", modulePath), out, errOut)
	assert.NoError(t, err)

	info2, err := fileInfo(modulePath, ".terragrunt-module-manifest")
	assert.NoError(t, err)
	// ensure that .terragrunt-module-manifest still exists after the second run.
	// When the source is unchanged, the module copy may be skipped, but the
	// manifest must remain present.
	assert.NotNil(t, info2)
}

func fileInfo(path, fileName string) (*os.FileInfo, error) {
	var fileInfo *os.FileInfo
	err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
		if fileInfo != nil {
			return nil
		}
		if err != nil {
			return err
		}
		if !info.IsDir() && info.Name() == fileName {
			fileInfo = &info
			return nil
		}
		return nil
	})
	return fileInfo, err
}

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

	helpers.CleanupTerraformFolder(t, testFixtureFindParent)

	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt run --all plan --non-interactive --working-dir %s", testFixtureFindParent))

	// second run shouldn't fail with find_in_parent_folders("root.hcl") issue
	helpers.RunTerragrunt(t, "terragrunt run --all --non-interactive --working-dir "+testFixtureFindParent+" -- apply -auto-approve")
}

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

	// create temp dir
	tmpDir, err := os.MkdirTemp("", "terragrunt-test")
	assert.NoError(t, err)

	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysql --working-dir '%s'", tmpDir))

	// check that terragrunt.hcl was created
	_, err = os.Stat(filepath.Join(tmpDir, "terragrunt.hcl"))
	assert.NoError(t, err)
}

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

	// create temp dir
	tmpDir, err := os.MkdirTemp("", "terragrunt-test")
	assert.NoError(t, err)

	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysql?ref=v0.8.1 --working-dir '%s'", tmpDir))

	// check that terragrunt.hcl was created
	_, err = os.Stat(filepath.Join(tmpDir, "terragrunt.hcl"))
	assert.NoError(t, err)
}

// TestWindowsProviderCacheWithRemoteURL verifies that provider caching works on Windows
// when providers are fetched from remote URLs. Previously, passing a URL like
// "https://github.com/..." to os.Stat caused a "syntax is incorrect" error on Windows
// because the colon in "https:" is invalid Windows path syntax.
func TestWindowsProviderCacheWithRemoteURL(t *testing.T) {
	t.Parallel()

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

	providerCacheDir := filepath.Join(t.TempDir(), "provider-cache-windows-test")

	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt run init --provider-cache --provider-cache-dir %s --non-interactive --working-dir %s", providerCacheDir, rootPath))

	// Verify the provider was cached
	entries, err := os.ReadDir(providerCacheDir)
	require.NoError(t, err)
	assert.NotEmpty(t, entries, "provider cache dir should not be empty after init")
}

func CopyEnvironmentToPath(t *testing.T, environmentPath, targetPath string) {
	if err := os.MkdirAll(targetPath, 0o777); err != nil {
		t.Fatalf("Failed to create temp dir %s due to error %v", targetPath, err)
	}

	copyErr := util.CopyFolderContents(createLogger(), environmentPath, filepath.Join(targetPath, environmentPath), ".terragrunt-test")
	require.NoError(t, copyErr)
}

func CopyEnvironmentWithTflint(t *testing.T, environmentPath string) string {
	currentDir, err := os.Getwd()
	if err != nil {
		t.Fatalf("Failed to get current directory: %v", err)
	}
	tmpDir, err := os.MkdirTemp(currentDir, "terragrunt-test")
	if err != nil {
		t.Fatalf("Failed to create temp dir due to error: %v", err)
	}

	t.Logf("Copying %s to %s", environmentPath, tmpDir)

	require.NoError(
		t,
		util.CopyFolderContents(
			createLogger(),
			environmentPath,
			filepath.Join(tmpDir, environmentPath),
			".terragrunt-test",
			util.WithIncludeInCopy(".tflint.hcl"),
		),
	)

	return tmpDir
}
