package templates //nolint:testpackage

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

	"github.com/gruntwork-io/boilerplate/options"
	"github.com/gruntwork-io/boilerplate/testutil"
	"github.com/gruntwork-io/boilerplate/variables"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	pwd, err := os.Getwd()
	require.NoError(t, err)

	testCases := []struct {
		file           string
		templateFolder string
		outputFolder   string
		variables      map[string]any
		expected       string
	}{
		{file: "template-folder/foo.txt", templateFolder: "template-folder", outputFolder: "output-folder", variables: map[string]any{}, expected: "output-folder/foo.txt"},
		{file: "foo/bar/template-folder/foo.txt", templateFolder: "foo/bar/template-folder", outputFolder: "output-folder", variables: map[string]any{}, expected: "output-folder/foo.txt"},
		{file: "template-folder/foo.txt", templateFolder: pwd + "/template-folder", outputFolder: "output-folder", variables: map[string]any{}, expected: "output-folder/foo.txt"},
		{file: "template-folder/foo/bar/baz.txt", templateFolder: pwd + "/template-folder", outputFolder: "output-folder", variables: map[string]any{}, expected: "output-folder/foo/bar/baz.txt"},
		{file: "template-folder/{{.Foo}}.txt", templateFolder: pwd + "/template-folder", outputFolder: "output-folder", variables: map[string]any{"Foo": "foo"}, expected: "output-folder/foo.txt"},
		{file: "template-folder/{{.Foo | dasherize}}.txt", templateFolder: pwd + "/template-folder", outputFolder: "output-folder", variables: map[string]any{"Foo": "Foo Bar Baz"}, expected: "output-folder/foo-bar-baz.txt"},
	}

	for _, testCase := range testCases {
		opts := options.BoilerplateOptions{
			TemplateFolder:  testCase.templateFolder,
			OutputFolder:    testCase.outputFolder,
			NonInteractive:  true,
			OnMissingKey:    options.ExitWithError,
			OnMissingConfig: options.Exit,
		}
		actual, err := outPath(t.Context(), testCase.file, &opts, testCase.variables)
		require.NoError(t, err, "Got unexpected error (file = %s, templateFolder = %s, outputFolder = %s, and variables = %s): %v", testCase.file, testCase.templateFolder, testCase.outputFolder, testCase.variables, err)
		assert.Equal(t, filepath.FromSlash(testCase.expected), filepath.FromSlash(actual), "(file = %s, templateFolder = %s, outputFolder = %s, and variables = %s)", testCase.file, testCase.templateFolder, testCase.outputFolder, testCase.variables)
	}
}

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

	testCases := []struct {
		dependency   *variables.Dependency
		variables    map[string]any
		opts         options.BoilerplateOptions
		expectedOpts options.BoilerplateOptions
	}{
		{
			dependency:   &variables.Dependency{Name: "dep1", TemplateURL: "../dep1", OutputFolder: "../out1"},
			opts:         options.BoilerplateOptions{TemplateFolder: "/template/path/", OutputFolder: "/output/path/", NonInteractive: true, Vars: map[string]any{}, OnMissingKey: options.ExitWithError},
			variables:    map[string]any{},
			expectedOpts: options.BoilerplateOptions{TemplateURL: "../dep1", TemplateFolder: filepath.FromSlash("/template/dep1"), OutputFolder: filepath.FromSlash("/output/out1"), NonInteractive: true, Vars: map[string]any{}, OnMissingKey: options.ExitWithError},
		},
		{
			dependency:   &variables.Dependency{Name: "dep1", TemplateURL: "../dep1", OutputFolder: "../out1"},
			opts:         options.BoilerplateOptions{TemplateFolder: "/template/path/", OutputFolder: "/output/path/", DisableDependencyPrompt: true, Vars: map[string]any{}, OnMissingKey: options.ExitWithError},
			variables:    map[string]any{},
			expectedOpts: options.BoilerplateOptions{TemplateURL: "../dep1", TemplateFolder: filepath.FromSlash("/template/dep1"), OutputFolder: filepath.FromSlash("/output/out1"), DisableDependencyPrompt: true, Vars: map[string]any{}, OnMissingKey: options.ExitWithError},
		},
		{
			dependency:   &variables.Dependency{Name: "dep1", TemplateURL: "../dep1", OutputFolder: "../out1"},
			opts:         options.BoilerplateOptions{TemplateFolder: "/template/path/", OutputFolder: "/output/path/", NonInteractive: false, Vars: map[string]any{"foo": "bar"}, OnMissingKey: options.Invalid},
			variables:    map[string]any{"baz": "blah"},
			expectedOpts: options.BoilerplateOptions{TemplateURL: "../dep1", TemplateFolder: filepath.FromSlash("/template/dep1"), OutputFolder: filepath.FromSlash("/output/out1"), NonInteractive: false, Vars: map[string]any{"foo": "bar", "baz": "blah"}, OnMissingKey: options.Invalid},
		},
		{
			dependency:   &variables.Dependency{Name: "dep1", TemplateURL: "{{ .foo }}", OutputFolder: "{{ .baz }}"},
			opts:         options.BoilerplateOptions{TemplateFolder: "/template/path/", OutputFolder: "/output/path/", NonInteractive: false, Vars: map[string]any{}, OnMissingKey: options.ExitWithError},
			variables:    map[string]any{"foo": "bar", "baz": "blah"},
			expectedOpts: options.BoilerplateOptions{TemplateURL: "bar", TemplateFolder: filepath.FromSlash("/template/path/bar"), OutputFolder: filepath.FromSlash("/output/path/blah"), NonInteractive: false, Vars: map[string]any{"foo": "bar", "baz": "blah"}, OnMissingKey: options.ExitWithError},
		},
	}

	for _, testCase := range testCases {
		tt := testCase
		t.Run(tt.dependency.Name, func(t *testing.T) {
			t.Parallel()

			actualOptions, err := cloneOptionsForDependency(t.Context(), tt.dependency, &tt.opts, nil, tt.variables)
			require.NoError(t, err, "Dependency: %s", tt.dependency)
			assert.Equal(t, tt.expectedOpts, *actualOptions, "Dependency: %s", tt.dependency)
		})
	}
}

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

	templateDir := t.TempDir()
	outputDir := t.TempDir()

	// Create a simple template file
	templateFile := filepath.Join(templateDir, "test.txt")
	err := os.WriteFile(templateFile, []byte("This is a test!"), 0644)
	require.NoError(t, err)

	// Create an empty boilerplate.yml file
	boilerplateYml := filepath.Join(templateDir, "boilerplate.yml")
	err = os.WriteFile(boilerplateYml, []byte{}, 0644)
	require.NoError(t, err)

	// Set up options
	opts := &options.BoilerplateOptions{
		TemplateFolder:  templateDir,
		OutputFolder:    outputDir,
		NonInteractive:  true,
		OnMissingKey:    options.ExitWithError,
		OnMissingConfig: options.Exit,
	}

	// Call ProcessTemplate
	dep := variables.Dependency{}
	err = ProcessTemplate(opts, opts, &dep)
	require.NoError(t, err)

	// Verify the file content
	content, err := os.ReadFile(filepath.Join(outputDir, "test.txt"))
	require.NoError(t, err)
	assert.Equal(t, "This is a test!", string(content))
}

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

	templateDir := t.TempDir()
	outputDir := t.TempDir()

	// Create a simple template file
	templateFile := filepath.Join(templateDir, "test.txt")
	err := os.WriteFile(templateFile, []byte("This is a test!"), 0644)
	require.NoError(t, err)

	// Create an empty boilerplate.yml file
	boilerplateYml := filepath.Join(templateDir, "boilerplate.yml")
	err = os.WriteFile(boilerplateYml, []byte{}, 0644)
	require.NoError(t, err)

	// Set up options
	opts := &options.BoilerplateOptions{
		TemplateFolder:  templateDir,
		OutputFolder:    outputDir,
		NonInteractive:  true,
		OnMissingKey:    options.ExitWithError,
		OnMissingConfig: options.Exit,
	}

	// Call ProcessTemplate function
	dep := variables.Dependency{}
	err = ProcessTemplate(opts, opts, &dep)
	require.NoError(t, err)

	// Verify the file content
	content, err := os.ReadFile(filepath.Join(outputDir, "test.txt"))
	require.NoError(t, err)
	assert.Equal(t, "This is a test!", string(content))
}

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

	testCases := []struct {
		variables         map[string]any
		optsVars          map[string]any
		expectedVariables map[string]any
		dependency        *variables.Dependency
	}{
		{
			dependency:        &variables.Dependency{Name: "dep1", TemplateURL: "../dep1", OutputFolder: "../out1"},
			variables:         map[string]any{},
			optsVars:          map[string]any{},
			expectedVariables: map[string]any{},
		},
		{
			dependency:        &variables.Dependency{Name: "dep1", TemplateURL: "../dep1", OutputFolder: "../out1"},
			variables:         map[string]any{"foo": "bar", "baz": "blah"},
			optsVars:          map[string]any{},
			expectedVariables: map[string]any{"foo": "bar", "baz": "blah"},
		},
		{
			dependency:        &variables.Dependency{Name: "dep1", TemplateURL: "../dep1", OutputFolder: "../out1"},
			variables:         map[string]any{"foo": "bar", "baz": "blah"},
			optsVars:          map[string]any{"dep1.abc": "should-modify-name", "dep2.def": "should-copy-unmodified"},
			expectedVariables: map[string]any{"foo": "bar", "baz": "blah", "abc": "should-modify-name", "dep2.def": "should-copy-unmodified"},
		},
		{
			dependency:        &variables.Dependency{Name: "dep1", TemplateURL: "../dep1", OutputFolder: "../out1"},
			variables:         map[string]any{"foo": "bar", "baz": "blah"},
			optsVars:          map[string]any{"dep1.abc": "should-modify-name", "dep2.def": "should-copy-unmodified", "abc": "should-be-overwritten-by-dep1.abc"},
			expectedVariables: map[string]any{"foo": "bar", "baz": "blah", "abc": "should-modify-name", "dep2.def": "should-copy-unmodified"},
		},
		{
			dependency:        &variables.Dependency{Name: "dep1", TemplateURL: "../dep1", OutputFolder: "../out1", DontInheritVariables: true},
			variables:         map[string]any{"foo": "bar", "baz": "blah"},
			optsVars:          map[string]any{"dep1.abc": "should-modify-name", "dep2.def": "should-copy-unmodified"},
			expectedVariables: map[string]any{},
		},
	}

	for _, testCase := range testCases {
		tt := testCase
		t.Run(tt.dependency.Name, func(t *testing.T) {
			t.Parallel()

			opts := testutil.CreateTestOptionsWithOutput("/template/path/", "/output/path/")
			opts.Vars = tt.optsVars
			actualVariables, err := cloneVariablesForDependency(t.Context(), opts, tt.dependency, nil, tt.variables, nil)
			require.NoError(t, err)
			assert.Equal(t, tt.expectedVariables, actualVariables, "Dependency: %s", tt.dependency)
		})
	}
}

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

	// Create a 3-level hierarchy: grandparent -> parent -> child
	tempDir := t.TempDir()

	grandparentDir := filepath.Join(tempDir, "grandparent")
	parentDir := filepath.Join(tempDir, "parent")
	childDir := filepath.Join(tempDir, "child")
	outputDir := filepath.Join(tempDir, "output")

	require.NoError(t, os.MkdirAll(grandparentDir, 0o755))
	require.NoError(t, os.MkdirAll(parentDir, 0o755))
	require.NoError(t, os.MkdirAll(childDir, 0o755))
	require.NoError(t, os.MkdirAll(outputDir, 0o755))

	// grandparent/boilerplate.yml -> depends on parent
	require.NoError(t, os.WriteFile(filepath.Join(grandparentDir, "boilerplate.yml"), []byte(`
dependencies:
  - name: parent
    template-url: ../parent
    output-folder: ./parent
`), 0o644))

	// parent/boilerplate.yml -> depends on child; has output.txt
	require.NoError(t, os.WriteFile(filepath.Join(parentDir, "boilerplate.yml"), []byte(`
dependencies:
  - name: child
    template-url: ../child
    output-folder: ./child
`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(parentDir, "output.txt"), []byte("parent output"), 0o644))

	// child/boilerplate.yml -> no deps; has output.txt
	require.NoError(t, os.WriteFile(filepath.Join(childDir, "boilerplate.yml"), []byte(""), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(childDir, "output.txt"), []byte("child output"), 0o644))

	opts := &options.BoilerplateOptions{
		TemplateFolder:  grandparentDir,
		OutputFolder:    outputDir,
		NonInteractive:  true,
		OnMissingKey:    options.ExitWithError,
		OnMissingConfig: options.Exit,
		Manifest:        true,
	}

	dep := variables.Dependency{}
	result, err := ProcessTemplateWithContext(t.Context(), opts, opts, &dep)
	require.NoError(t, err)

	// The result should have 1 dependency (parent)
	require.Len(t, result.Dependencies, 1, "expected 1 top-level dependency (parent)")
	parentDep := result.Dependencies[0]
	assert.Equal(t, "parent", parentDep.Name)

	// The parent dependency should have 1 nested dependency (child)
	require.Len(t, parentDep.Dependencies, 1, "expected parent to have 1 nested dependency (child)")
	childDep := parentDep.Dependencies[0]
	assert.Equal(t, "child", childDep.Name)
}

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

	// Test that ForEachReference templates get rendered to resolve variable names dynamically
	tempDir := t.TempDir()

	templateFolder := filepath.Join(tempDir, "template")
	err := os.MkdirAll(templateFolder, 0o755)
	require.NoError(t, err)
	err = os.WriteFile(filepath.Join(templateFolder, "boilerplate.yml"), []byte("variables: []\n"), 0o644)
	require.NoError(t, err)
	err = os.WriteFile(filepath.Join(templateFolder, "test.txt"), []byte("{{ .__each__ }}"), 0o644)
	require.NoError(t, err)

	dependency := &variables.Dependency{
		Name:         "test",
		TemplateURL:  ".",
		OutputFolder: "{{ .__each__ }}",
		// This template should render to "template1" by looking up deployments[region_1].template
		ForEachReference: "{{ index .deployments .region \"template\" }}",
	}

	// Test data: region_1 points to template1, which contains the list to iterate over
	vars := map[string]any{
		"region": "region_1",
		"deployments": map[string]any{
			"region_1": map[string]any{
				"template": "template1", // Points to the variable name to use for iteration
			},
		},
		"template1": []string{"a", "b"}, // The actual list that gets iterated over
	}

	opts := testutil.CreateTestOptionsWithOutput(templateFolder, tempDir)

	_, err = processDependency(t.Context(), dependency, opts, nil, vars)
	require.NoError(t, err)

	// Should create directories "a" and "b" from template1 list
	for _, expected := range []string{"a", "b"} {
		_, err := os.Stat(filepath.Join(tempDir, expected))
		require.NoError(t, err)
	}
}
