package hclparse_test

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

	"github.com/gruntwork-io/terragrunt/internal/hclparse"
	"github.com/gruntwork-io/terragrunt/internal/vfs"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zclconf/go-cty/cty/function"
)

// noFuncs is the empty HCL function map shared by tests that exercise only
// literal stack attributes, parse errors, or panic contracts. Any HCL
// function call against it resolves to "function not found", which is the
// intended outcome for these tests.
var noFuncs = map[string]function.Function{}

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

	got := hclparse.BuildComponentRefMap([]hclparse.ComponentRef{
		{Name: "networking", Path: ".terragrunt-stack/networking"},
	})

	networking := got.AsValueMap()["networking"].AsValueMap()
	assert.Equal(t, ".terragrunt-stack/networking", networking["path"].AsString())
	_, hasName := networking["name"]
	assert.False(t, hasName, "ref object should not expose .name")
}

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

	fs := vfs.NewMemMapFS()
	require.NoError(t, fs.MkdirAll("/test", 0755))
	require.NoError(t, vfs.WriteFile(fs, "/test/terragrunt.stack.hcl", []byte(`
unit "vpc" {
  source = "../units/vpc"
  path   = "vpc"
}

unit "db" {
  source = "../units/db"
  path   = "db"
}
`), 0644))

	paths, err := hclparse.UnitPathsFromStackDir(fs, "/test", noFuncs)
	require.NoError(t, err)
	require.Len(t, paths, 2)
	assert.Contains(t, paths[0], ".terragrunt-stack")
	assert.Contains(t, paths[1], ".terragrunt-stack")
}

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

	fs := vfs.NewMemMapFS()
	require.NoError(t, fs.MkdirAll("/test/includes", 0755))
	require.NoError(t, vfs.WriteFile(fs, "/test/includes/units.hcl", []byte(`
unit "vpc" {
  source = "../units/vpc"
  path   = "vpc"
}
`), 0644))
	require.NoError(t, vfs.WriteFile(fs, "/test/terragrunt.stack.hcl", []byte(`
include "units" {
  path = "./includes/units.hcl"
}
`), 0644))

	paths, err := hclparse.UnitPathsFromStackDir(fs, "/test", noFuncs)
	require.NoError(t, err)
	require.Len(t, paths, 1)
	assert.Contains(t, paths[0], filepath.Join(hclparse.StackDir, "vpc"))
}

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

	fs := vfs.NewMemMapFS()
	require.NoError(t, fs.MkdirAll("/test", 0755))
	require.NoError(t, vfs.WriteFile(fs, "/test/terragrunt.stack.hcl", []byte(`
unit "vpc" {
  source = "../units/vpc"
  path   = get_repo_root()
}
`), 0644))

	_, err := hclparse.UnitPathsFromStackDir(fs, "/test", noFuncs)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "get_repo_root")
}

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

	fs := vfs.NewMemMapFS()
	require.NoError(t, fs.MkdirAll("/test", 0755))

	paths, err := hclparse.UnitPathsFromStackDir(fs, "/test", noFuncs)
	require.NoError(t, err)
	assert.Nil(t, paths)
}

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

	fs := vfs.NewMemMapFS()

	paths, err := hclparse.UnitPathsFromStackDir(fs, "/nonexistent", noFuncs)
	require.NoError(t, err)
	assert.Nil(t, paths)
}

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

	fs := vfs.NewMemMapFS()
	require.NoError(t, fs.MkdirAll("/test", 0755))
	require.NoError(t, vfs.WriteFile(fs, "/test/terragrunt.stack.hcl", []byte(`unit "x" { source = "." `), 0644))

	paths, err := hclparse.UnitPathsFromStackDir(fs, "/test", noFuncs)
	require.Error(t, err)
	assert.Nil(t, paths)

	var fpe hclparse.FileParseError
	require.ErrorAs(t, err, &fpe)
}

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

	fs := vfs.NewMemMapFS()
	require.NoError(t, fs.MkdirAll("/test", 0755))
	require.NoError(t, vfs.WriteFile(fs, "/test/terragrunt.stack.hcl", []byte(`
unit "app" {
  source = "../units/app"
  path   = "app"
}
`), 0644))

	result, err := hclparse.ParseStackFileFromPath(fs, "/test")
	require.NoError(t, err)
	require.NotNil(t, result)
	require.Len(t, result.Units, 1)
	assert.Equal(t, "app", result.Units[0].Name)
}

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

	fs := vfs.NewMemMapFS()
	require.NoError(t, fs.MkdirAll("/test", 0755))

	result, err := hclparse.ParseStackFileFromPath(fs, "/test")
	require.NoError(t, err)
	assert.Nil(t, result)
}

// ParseStackFileFromPath is strict: passing a regular file produces an error. Callers that may receive non-directory paths (e.g. discovery) must filter them upstream.
func TestParseStackFileFromPath_StackDirIsFileReturnsError(t *testing.T) {
	t.Parallel()

	tmpDir := t.TempDir()
	filePath := filepath.Join(tmpDir, "another-name.hcl")
	require.NoError(t, os.WriteFile(filePath, []byte(`# regular file, not a directory`), 0644))

	result, err := hclparse.ParseStackFileFromPath(vfs.NewOSFS(), filePath)
	require.Error(t, err)
	assert.Nil(t, result)

	var readErr hclparse.FileReadError
	require.ErrorAs(t, err, &readErr)
	// On macOS, t.TempDir() returns paths under /var/folders/... where /var is a symlink to /private/var; util.ResolvePath follows it, so resolve our side too before comparing.
	resolvedFilePath, evalErr := filepath.EvalSymlinks(filePath)
	require.NoError(t, evalErr)
	assert.Equal(t, filepath.Join(resolvedFilePath, "terragrunt.stack.hcl"), readErr.FilePath)
}

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

	tmpDir := t.TempDir()

	// Create the real stack directory
	realDir := filepath.Join(tmpDir, "real-stack")
	require.NoError(t, os.MkdirAll(realDir, 0755))
	require.NoError(t, os.WriteFile(filepath.Join(realDir, "terragrunt.stack.hcl"), []byte(`
unit "app" {
  source = "../units/app"
  path   = "app"
}
`), 0644))

	// Create a symlink pointing to the real directory
	symlinkDir := filepath.Join(tmpDir, "symlinked-stack")
	require.NoError(t, os.Symlink(realDir, symlinkDir))

	// Parse via the symlink - should work the same as via real path
	result, err := hclparse.ParseStackFileFromPath(vfs.NewOSFS(), symlinkDir)
	require.NoError(t, err)
	require.NotNil(t, result)
	require.Len(t, result.Units, 1)
	assert.Equal(t, "app", result.Units[0].Name)
}

// Uses OSFS because MemMapFS does not faithfully reproduce os.Symlink semantics that util.ResolvePath relies on.
func TestUnitPathsFromStackDir_Symlink(t *testing.T) {
	t.Parallel()

	tmpDir := t.TempDir()

	realDir := filepath.Join(tmpDir, "real-stack")
	require.NoError(t, os.MkdirAll(realDir, 0755))
	require.NoError(t, os.WriteFile(filepath.Join(realDir, "terragrunt.stack.hcl"), []byte(`
unit "vpc" {
  source = "../units/vpc"
  path   = "vpc"
}
`), 0644))

	symlinkDir := filepath.Join(tmpDir, "symlinked-stack")
	require.NoError(t, os.Symlink(realDir, symlinkDir))

	paths, err := hclparse.UnitPathsFromStackDir(vfs.NewOSFS(), symlinkDir, noFuncs)
	require.NoError(t, err)
	require.Len(t, paths, 1)
	// Path should resolve to the real directory, not the symlink path.
	assert.Contains(t, paths[0], "real-stack")
	assert.NotContains(t, paths[0], "symlinked-stack")
}

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

	fs := vfs.NewMemMapFS()
	require.NoError(t, fs.MkdirAll("/test/includes", 0755))
	require.NoError(t, vfs.WriteFile(fs, "/test/includes/extra.stack.hcl", []byte(`
unit "monitoring" {
  source = "../catalog/units/monitoring"
  path   = "monitoring"
}
`), 0644))

	// Create the main stack file
	mainSrc := `
include "extra" {
  path = "/test/includes/extra.stack.hcl"
}

unit "vpc" {
  source = "../catalog/units/vpc"
  path   = "vpc"
}
`

	result, err := hclparse.ParseStackFile(fs, &hclparse.ParseStackFileInput{Src: []byte(mainSrc), Filename: "/test/terragrunt.stack.hcl", StackDir: "/test"})
	require.NoError(t, err)

	// Should have both units: vpc from main + monitoring from include
	require.Len(t, result.Units, 2)

	names := []string{result.Units[0].Name, result.Units[1].Name}
	assert.Contains(t, names, "vpc")
	assert.Contains(t, names, "monitoring")
}

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

	result := hclparse.BuildComponentRefMap(nil)
	assert.True(t, result.Type().IsObjectType())
}

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

	refs := []hclparse.ComponentRef{
		{Name: "vpc", Path: "vpc"},
		{Name: "app", Path: "app-service"},
	}

	result := hclparse.BuildComponentRefMap(refs)

	require.True(t, result.Type().IsObjectType())

	vpcVal := result.GetAttr("vpc")
	require.True(t, vpcVal.Type().IsObjectType())
	assert.Equal(t, "vpc", vpcVal.GetAttr("path").AsString())

	appVal := result.GetAttr("app")
	require.True(t, appVal.Type().IsObjectType())
	assert.Equal(t, "app-service", appVal.GetAttr("path").AsString())
}
