package tui_test

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

	"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui"
	"github.com/gruntwork-io/terragrunt/internal/vfs"
	"github.com/gruntwork-io/terragrunt/pkg/options"
	"github.com/gruntwork-io/terragrunt/test/helpers"
	"github.com/gruntwork-io/terragrunt/test/helpers/logger"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	fsys := vfs.NewMemMapFS()
	repoDir := testRepoDir

	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "terragrunt.hcl"), "# vpc unit\n")
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "inputs.hcl"), "# inputs\n")
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", ".terragrunt-cache", "junk.txt"), "should be skipped")

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)
	require.Equal(t, tui.ComponentKindUnit, components[0].Kind)

	workingDir := testWorkingDir
	require.NoError(t, fsys.MkdirAll(workingDir, 0o755))

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = workingDir

	require.NoError(t, tui.NewCopyCmd(logger.CreateLogger(), opts, components[0]).WithFS(fsys).Run())

	assertFileExistsFS(t, fsys, filepath.Join(workingDir, "terragrunt.hcl"))
	assertFileExistsFS(t, fsys, filepath.Join(workingDir, "inputs.hcl"))
	assertNotExistsFS(t, fsys, filepath.Join(workingDir, ".terragrunt-cache", "junk.txt"))
	assertNotExistsFS(t, fsys, filepath.Join(workingDir, ".terragrunt-cache"))
}

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

	fsys := vfs.NewMemMapFS()
	repoDir := testRepoDir

	unitBody := `
locals {
  region = values.region
}

inputs = {
  app = values.app
  env = values.env
}
`
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "terragrunt.hcl"), unitBody)

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)

	workingDir := testWorkingDir
	require.NoError(t, fsys.MkdirAll(workingDir, 0o755))

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = workingDir

	require.NoError(t, tui.NewCopyCmd(logger.CreateLogger(), opts, components[0]).WithFS(fsys).Run())

	valuesPath := filepath.Join(workingDir, "terragrunt.values.hcl")
	assertFileExistsFS(t, fsys, valuesPath)

	raw, err := vfs.ReadFile(fsys, valuesPath)
	require.NoError(t, err)

	content := string(raw)
	assert.Contains(t, content, "# Auto-generated by `terragrunt catalog`")

	appIdx := strings.Index(content, "\napp")

	envIdx := strings.Index(content, "\nenv")

	regionIdx := strings.Index(content, "\nregion")

	require.NotEqual(t, -1, appIdx)
	require.NotEqual(t, -1, envIdx)
	require.NotEqual(t, -1, regionIdx)
	assert.Less(t, appIdx, envIdx)
	assert.Less(t, envIdx, regionIdx)
}

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

	fsys := vfs.NewMemMapFS()
	repoDir := testRepoDir
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "terragrunt.hcl"),
		`locals { region = values.region }`)

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)

	workingDir := testWorkingDir
	existing := []byte(`region = "us-east-1"` + "\n")
	require.NoError(t, vfs.WriteFile(fsys, filepath.Join(workingDir, "terragrunt.values.hcl"), existing, 0o644))

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = workingDir

	require.NoError(t, tui.NewCopyCmd(logger.CreateLogger(), opts, components[0]).WithFS(fsys).Run())

	got, err := vfs.ReadFile(fsys, filepath.Join(workingDir, "terragrunt.values.hcl"))
	require.NoError(t, err)
	assert.Equal(t, existing, got, "pre-existing values file should be untouched")
}

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

	fsys := vfs.NewMemMapFS()
	repoDir := testRepoDir
	writeFileFS(t, fsys, filepath.Join(repoDir, "stack-a", "terragrunt.stack.hcl"), "# stack\n")

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)

	workingDir := testWorkingDir
	writeFileFS(t, fsys, filepath.Join(workingDir, "terragrunt.stack.hcl"), "# preexisting")

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = workingDir

	err = tui.NewCopyCmd(logger.CreateLogger(), opts, components[0]).WithFS(fsys).Run()

	var destErr *tui.DestinationExistsError

	require.ErrorAs(t, err, &destErr)
	assert.Equal(t, filepath.Join(workingDir, "terragrunt.stack.hcl"), destErr.Path)
}

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

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = t.TempDir()

	err := tui.NewCopyCmd(logger.CreateLogger(), opts, nil).Run()
	require.ErrorIs(t, err, tui.ErrNilComponent)
}

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

	fsys := vfs.NewMemMapFS()
	repoDir := testRepoDir
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "terragrunt.hcl"), "# unit\n")

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = ""

	err = tui.NewCopyCmd(logger.CreateLogger(), opts, components[0]).WithFS(fsys).Run()
	require.ErrorIs(t, err, tui.ErrEmptyWorkingDir)
}

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

	fsys := vfs.NewMemMapFS()
	repoDir := testRepoDir
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "terragrunt.hcl"), "# unit\n")

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)

	// Remove the source directory after discovery so the copy walk fails.
	require.NoError(t, fsys.RemoveAll(filepath.Join(repoDir, "vpc")))

	workingDir := testWorkingDir
	require.NoError(t, fsys.MkdirAll(workingDir, 0o755))

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = workingDir

	err = tui.NewCopyCmd(logger.CreateLogger(), opts, components[0]).WithFS(fsys).Run()
	require.Error(t, err)
}

// TestCopyCmd_SkipsSymlinks stays on the real OS filesystem because the
// behavior under test is symlink-skipping, and only OsFs participates in
// the symlink semantics that os.Symlink + lstat exercise.
func TestCopyCmd_SkipsSymlinks(t *testing.T) {
	t.Parallel()

	fsys := vfs.NewOSFS()
	repoDir := helpers.TmpDirWOSymlinks(t)
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "terragrunt.hcl"), "# unit\n")
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "real.txt"), "hello\n")

	// Add a symlink inside the component; copyDir should skip it silently.
	require.NoError(t, os.Symlink("real.txt", filepath.Join(repoDir, "vpc", "link.txt")))

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)

	workingDir := t.TempDir()
	opts := options.NewTerragruntOptions()
	opts.WorkingDir = workingDir

	require.NoError(t, tui.NewCopyCmd(logger.CreateLogger(), opts, components[0]).Run())

	assert.FileExists(t, filepath.Join(workingDir, "real.txt"))
	assert.NoFileExists(t, filepath.Join(workingDir, "link.txt"))
}

// TestCopyCmd_WithFSUsesInjectedFilesystem verifies that WithFS returns the
// same builder and that the injected filesystem is honored during Run.
func TestCopyCmd_WithFSUsesInjectedFilesystem(t *testing.T) {
	t.Parallel()

	fsys := vfs.NewMemMapFS()
	repoDir := testRepoDir
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "terragrunt.hcl"), "# unit\n")

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)

	workingDir := testWorkingDir
	require.NoError(t, fsys.MkdirAll(workingDir, 0o755))

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = workingDir

	cmd := tui.NewCopyCmd(logger.CreateLogger(), opts, components[0])

	chained := cmd.WithFS(fsys)
	assert.Same(t, cmd, chained, "WithFS should return the same builder for chaining")

	require.NoError(t, cmd.Run())

	r := cmd.Result()
	assert.Equal(t, workingDir, r.WorkingDir, "Result should record the working directory used")
	assertFileExistsFS(t, fsys, filepath.Join(workingDir, "terragrunt.hcl"))
}

// TestCopyCmd_ResultZeroValueBeforeRun verifies Result is callable before Run
// and returns the zero CopyResult.
func TestCopyCmd_ResultZeroValueBeforeRun(t *testing.T) {
	t.Parallel()

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = t.TempDir()

	cmd := tui.NewCopyCmd(logger.CreateLogger(), opts, nil)

	assert.Equal(t, tui.CopyResult{}, cmd.Result(), "Result should return the zero value before Run")
}

// TestCopyCmd_StdioSettersAreNoops verifies the tea.ExecCommand stdio setters
// are safe no-ops. The redesign CopyCmd does not read or write through these,
// but they satisfy the tea.ExecCommand interface.
func TestCopyCmd_StdioSettersAreNoops(t *testing.T) {
	t.Parallel()

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = t.TempDir()

	cmd := tui.NewCopyCmd(logger.CreateLogger(), opts, nil)

	assert.NotPanics(t, func() {
		cmd.SetStdin(bytes.NewReader(nil))
		cmd.SetStdout(&bytes.Buffer{})
		cmd.SetStderr(&bytes.Buffer{})
	})
}

// TestCopyCmd_RejectsValuesStubWhenPathIsDirectory verifies the values-stub
// preflight refuses to proceed when the stub path is occupied by something
// other than a regular file (here, a directory), since WriteValuesFile could
// neither overwrite nor leave it alone safely.
func TestCopyCmd_RejectsValuesStubWhenPathIsDirectory(t *testing.T) {
	t.Parallel()

	fsys := vfs.NewMemMapFS()
	repoDir := testRepoDir
	writeFileFS(t, fsys, filepath.Join(repoDir, "vpc", "terragrunt.hcl"),
		`locals { region = values.region }`)

	repo := newFakeRepo(t, fsys, repoDir)

	components, err := tui.NewComponentDiscovery().WithFS(fsys).Discover(repo)
	require.NoError(t, err)
	require.Len(t, components, 1)

	workingDir := testWorkingDir
	require.NoError(t, fsys.MkdirAll(filepath.Join(workingDir, "terragrunt.values.hcl"), 0o755))

	opts := options.NewTerragruntOptions()
	opts.WorkingDir = workingDir

	err = tui.NewCopyCmd(logger.CreateLogger(), opts, components[0]).WithFS(fsys).Run()

	var notRegularErr *tui.DestinationNotRegularError

	require.ErrorAs(t, err, &notRegularErr)
	assert.Equal(t, filepath.Join(workingDir, "terragrunt.values.hcl"), notRegularErr.Path)
}

// assertFileExistsFS asserts a file exists at path on fsys.
func assertFileExistsFS(t *testing.T, fsys vfs.FS, path string) {
	t.Helper()

	exists, err := vfs.FileExists(fsys, path)
	require.NoError(t, err)
	assert.True(t, exists, "expected %q to exist on fsys", path)
}

// assertNotExistsFS asserts that nothing exists at path on fsys.
func assertNotExistsFS(t *testing.T, fsys vfs.FS, path string) {
	t.Helper()

	exists, err := vfs.FileExists(fsys, path)
	require.NoError(t, err)
	assert.False(t, exists, "expected %q to not exist on fsys", path)
}
