package tui_test

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

	"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui"
	"github.com/gruntwork-io/terragrunt/internal/vfs"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zclconf/go-cty/cty"
)

// unitDir is the shared in-memory test directory the values_test fixtures
// stage their HCL files under. Hoisted out of the individual tests so
// goconst doesn't keep flagging the repeat literal.
const unitDir = "/unit"

// optionalNames returns the names from refs.Optional in their current order.
// Use in tests that want to assert on presence + ordering of optional entries
// without caring about the concrete defaults.
func optionalNames(refs tui.ValuesReferences) []string {
	out := make([]string, 0, len(refs.Optional))
	for _, o := range refs.Optional {
		out = append(out, o.Name)
	}

	return out
}

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

	fsys := vfs.NewMemMapFS()
	path := "/unit/terragrunt.hcl"

	body := `
locals {
  region = values.region
  app    = values.app
}

inputs = {
  region = values.region
  extra  = "${values.env}-literal"
}
`
	require.NoError(t, vfs.WriteFile(fsys, path, []byte(body), 0o644))

	refs, err := tui.CollectValuesReferences(fsys, path)
	require.NoError(t, err)
	assert.Equal(t, []string{"app", "env", "region"}, refs.Required)
	assert.Empty(t, refs.Optional)
}

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

	fsys := vfs.NewMemMapFS()
	path := "/unit/terragrunt.hcl"

	// Required: region, app (app also appears in a try but the required
	// usage wins).
	// optional (literal default): aws_partition, deploy_branch, tags, aud.
	// optional (non-literal default, skipped): issuer (local ref).
	body := `
locals {
  region         = values.region
  app_required   = values.app
  app_fallback   = try(values.app, "default")
  aws_partition  = try(values.aws_partition, "aws")
  deploy_branch  = try(values.deploy_branch, "main")
  aud            = try(values.aud, "sts.amazonaws.com")
  tags           = try(values.tags, {})
  issuer         = try(values.issuer, local.default_issuer)
  interpolated   = try(values.interp, "${local.x}-suffix")
}
`
	require.NoError(t, vfs.WriteFile(fsys, path, []byte(body), 0o644))

	refs, err := tui.CollectValuesReferences(fsys, path)
	require.NoError(t, err)
	assert.Equal(t, []string{"app", "region"}, refs.Required)
	assert.Equal(t, []string{"aud", "aws_partition", "deploy_branch", "tags"}, optionalNames(refs))
}

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

	fsys := vfs.NewMemMapFS()
	path := "/unit/terragrunt.hcl"

	require.NoError(t, vfs.WriteFile(fsys, path, []byte(`
locals {
  region = "us-east-1"
}
`), 0o644))

	refs, err := tui.CollectValuesReferences(fsys, path)
	require.NoError(t, err)
	assert.True(t, refs.IsEmpty())
}

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

	fsys := vfs.NewMemMapFS()
	path := "/stack/terragrunt.stack.hcl"

	// Only assignments; no `values.*` reads. Should return empty.
	body := `
unit "app" {
  source = "./app"
  path   = "app"
  values = {
    foo = "bar"
    baz = "qux"
  }
}
`
	require.NoError(t, vfs.WriteFile(fsys, path, []byte(body), 0o644))

	refs, err := tui.CollectValuesReferences(fsys, path)
	require.NoError(t, err)
	assert.True(t, refs.IsEmpty())
}

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

	fsys := vfs.NewMemMapFS()

	refs, err := tui.CollectValuesReferences(fsys, "/missing/nope.hcl")
	require.NoError(t, err)
	assert.True(t, refs.IsEmpty())
}

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

	fsys := vfs.NewMemMapFS()
	dir := unitDir
	require.NoError(t, fsys.MkdirAll(dir, 0o755))

	refs := tui.ValuesReferences{
		Required: []string{"region", "app"},
		Optional: []tui.OptionalValue{
			{Name: "aws_partition", Default: cty.StringVal("aws")},
			{Name: "deploy_branch", Default: cty.StringVal("main")},
		},
	}

	written, err := tui.WriteValuesStub(fsys, dir, refs)
	require.NoError(t, err)
	assert.True(t, written)

	raw, err := vfs.ReadFile(fsys, filepath.Join(dir, "terragrunt.values.hcl"))
	require.NoError(t, err)

	out := string(raw)

	assert.Contains(t, out, "# Auto-generated by `terragrunt catalog`")
	assert.Contains(t, out, "# === Required ===")
	assert.Contains(t, out, "# === Optional (defaults from try() fallbacks) ===")

	// Required block comes before optional block.
	requiredIdx := strings.Index(out, "# === Required ===")

	optionalIdx := strings.Index(out, "# === Optional")

	assert.Less(t, requiredIdx, optionalIdx)

	// Within Required: app < region.
	appIdx := strings.Index(out, "\napp")

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

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

	// Required entries are TODO placeholders.
	assert.Regexp(t, `app\s*=\s*"TODO"`, out)
	assert.Regexp(t, `region\s*=\s*"TODO"`, out)

	// Optional entries use the evaluated defaults.
	assert.Regexp(t, `aws_partition\s*=\s*"aws"`, out)
	assert.Regexp(t, `deploy_branch\s*=\s*"main"`, out)
}

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

	fsys := vfs.NewMemMapFS()
	dir := unitDir
	require.NoError(t, fsys.MkdirAll(dir, 0o755))

	written, err := tui.WriteValuesStub(fsys, dir, tui.ValuesReferences{})
	require.NoError(t, err)
	assert.False(t, written)

	exists, err := vfs.FileExists(fsys, filepath.Join(dir, "terragrunt.values.hcl"))
	require.NoError(t, err)
	assert.False(t, exists)
}

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

	fsys := vfs.NewMemMapFS()
	dir := unitDir
	path := filepath.Join(dir, "terragrunt.values.hcl")

	original := []byte("# user-authored content\nfoo = \"existing\"\n")
	require.NoError(t, vfs.WriteFile(fsys, path, original, 0o644))

	refs := tui.ValuesReferences{Required: []string{"foo", "bar"}}

	written, err := tui.WriteValuesStub(fsys, dir, refs)
	require.NoError(t, err)
	assert.False(t, written)

	got, err := vfs.ReadFile(fsys, path)
	require.NoError(t, err)
	assert.Equal(t, original, got, "existing values file should be untouched")
}

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

	fsys := vfs.NewMemMapFS()
	dir := unitDir
	require.NoError(t, fsys.MkdirAll(dir, 0o755))

	refs := tui.ValuesReferences{
		Required: []string{"region", "app", "ports"},
		Optional: []tui.OptionalValue{
			{Name: "aws_partition", Default: cty.StringVal("aws")},
			{Name: "tags", Default: cty.StringVal("default")},
		},
	}

	values := map[string]string{
		"region":        `"us-east-1"`,
		"ports":         `[80, 443]`,
		"aws_partition": `"aws-cn"`,
	}

	written, err := tui.WriteValuesFile(fsys, dir, refs, values)
	require.NoError(t, err)
	assert.True(t, written)

	raw, err := vfs.ReadFile(fsys, filepath.Join(dir, "terragrunt.values.hcl"))
	require.NoError(t, err)

	out := string(raw)

	// Required entries that were supplied use the raw HCL fragments.
	assert.Regexp(t, `region\s*=\s*"us-east-1"`, out)
	assert.Regexp(t, `ports\s*=\s*\[80,\s*443\]`, out)

	// Required entries that weren't supplied fall back to TODO.
	assert.Regexp(t, `app\s*=\s*"TODO"`, out)

	// Optional entries that were supplied use the user value; ones that
	// weren't fall back to the try() default.
	assert.Regexp(t, `aws_partition\s*=\s*"aws-cn"`, out)
	assert.Regexp(t, `tags\s*=\s*"default"`, out)
}

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

	fsys := vfs.NewMemMapFS()
	dir := unitDir
	require.NoError(t, fsys.MkdirAll(dir, 0o755))

	refs := tui.ValuesReferences{Required: []string{"broken"}}

	_, err := tui.WriteValuesFile(fsys, dir, refs, map[string]string{
		"broken": `[1, 2`, // unterminated list
	})

	var invalidErr *tui.InvalidHCLValueError

	require.ErrorAs(t, err, &invalidErr)
	assert.Equal(t, "broken", invalidErr.Name)

	exists, err := vfs.FileExists(fsys, filepath.Join(dir, "terragrunt.values.hcl"))
	require.NoError(t, err)
	assert.False(t, exists, "no file should be written on parse failure")
}

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

	stubFS := vfs.NewMemMapFS()
	fileFS := vfs.NewMemMapFS()
	dir := unitDir
	require.NoError(t, stubFS.MkdirAll(dir, 0o755))
	require.NoError(t, fileFS.MkdirAll(dir, 0o755))

	refs := tui.ValuesReferences{
		Required: []string{"region"},
		Optional: []tui.OptionalValue{{Name: "env", Default: cty.StringVal("prod")}},
	}

	_, err := tui.WriteValuesStub(stubFS, dir, refs)
	require.NoError(t, err)

	_, err = tui.WriteValuesFile(fileFS, dir, refs, nil)
	require.NoError(t, err)

	stubBytes, err := vfs.ReadFile(stubFS, filepath.Join(dir, "terragrunt.values.hcl"))
	require.NoError(t, err)

	fileBytes, err := vfs.ReadFile(fileFS, filepath.Join(dir, "terragrunt.values.hcl"))
	require.NoError(t, err)

	assert.Equal(t, string(stubBytes), string(fileBytes))
}
