package find_test

import (
	"encoding/json"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/gruntwork-io/terragrunt/internal/cli/commands/find"
	"github.com/gruntwork-io/terragrunt/internal/component"
	"github.com/gruntwork-io/terragrunt/internal/filter"
	"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 TestRun(t *testing.T) {
	t.Parallel()

	tests := []struct {
		setup         func(t *testing.T) string
		validate      func(t *testing.T, output string, expectedPaths []string)
		name          string
		format        string
		mode          string
		expectedPaths []string
		noHidden      bool
		dependencies  bool
		external      bool
		reading       bool
	}{
		{
			name: "basic discovery",
			setup: func(t *testing.T) string {
				t.Helper()

				tmpDir := helpers.TmpDirWOSymlinks(t)

				// Create test directory structure
				testDirs := []string{
					"unit1",
					"unit2",
					"stack1",
					".hidden/unit3",
					"nested/unit4",
				}

				for _, dir := range testDirs {
					err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
					require.NoError(t, err)
				}

				// Create test files
				testFiles := map[string]string{
					"unit1/terragrunt.hcl":         "",
					"unit2/terragrunt.hcl":         "",
					"stack1/terragrunt.stack.hcl":  "",
					".hidden/unit3/terragrunt.hcl": "",
					"nested/unit4/terragrunt.hcl":  "",
				}

				for path, content := range testFiles {
					err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
					require.NoError(t, err)
				}

				return tmpDir
			},
			expectedPaths: []string{"unit1", "unit2", "nested/unit4", "stack1"},
			format:        "text",
			mode:          "normal",
			noHidden:      true,
			dependencies:  false,
			external:      false,
			validate: func(t *testing.T, output string, expectedPaths []string) {
				t.Helper()

				// Split output into lines and trim whitespace
				lines := strings.Split(strings.TrimSpace(output), "\n")

				// Verify we have the expected number of lines
				assert.Len(t, lines, len(expectedPaths))

				// Convert expected paths to use OS-specific path separators
				osExpectedPaths := make([]string, 0, len(expectedPaths))
				for _, path := range expectedPaths {
					osExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))
				}

				// Convert actual paths to use OS-specific path separators
				osPaths := make([]string, 0, len(lines))
				for _, line := range lines {
					osPaths = append(osPaths, filepath.FromSlash(strings.TrimSpace(line)))
				}

				// Verify all expected paths are present
				assert.ElementsMatch(t, osExpectedPaths, osPaths)
			},
		},
		{
			name: "json output format",
			setup: func(t *testing.T) string {
				t.Helper()

				tmpDir := helpers.TmpDirWOSymlinks(t)

				// Create test directory structure
				testDirs := []string{
					"unit1",
					"unit2",
					"stack1",
				}

				for _, dir := range testDirs {
					err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
					require.NoError(t, err)
				}

				// Create test files
				testFiles := map[string]string{
					"unit1/terragrunt.hcl":        "",
					"unit2/terragrunt.hcl":        "",
					"stack1/terragrunt.stack.hcl": "",
				}

				for path, content := range testFiles {
					err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
					require.NoError(t, err)
				}

				return tmpDir
			},
			expectedPaths: []string{"unit1", "unit2", "stack1"},
			format:        "json",
			mode:          "normal",
			dependencies:  false,
			external:      false,
			validate: func(t *testing.T, output string, expectedPaths []string) {
				t.Helper()

				// Verify the output is valid JSON
				var configs find.FoundComponents

				err := json.Unmarshal([]byte(output), &configs)
				require.NoError(t, err)

				// Verify we have the expected number of configs
				assert.Len(t, configs, len(expectedPaths))

				// Convert expected paths to use OS-specific path separators
				osExpectedPaths := make([]string, 0, len(expectedPaths))
				for _, path := range expectedPaths {
					osExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))
				}

				// Extract paths and convert to OS-specific separators
				paths := make([]string, 0, len(configs))
				for _, config := range configs {
					paths = append(paths, filepath.FromSlash(config.Path))
				}

				// Verify all expected paths are present
				assert.ElementsMatch(t, osExpectedPaths, paths)

				// Verify each config has a valid type
				for _, config := range configs {
					assert.NotEmpty(t, config.Type)
					assert.True(t, config.Type == component.UnitKind || config.Type == component.StackKind)
				}
			},
		},
		{
			name: "hidden discovery",
			setup: func(t *testing.T) string {
				t.Helper()

				tmpDir := helpers.TmpDirWOSymlinks(t)

				// Create test directory structure
				testDirs := []string{
					"unit1",
					"unit2",
					"stack1",
					".hidden/unit3",
					"nested/unit4",
				}

				for _, dir := range testDirs {
					err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
					require.NoError(t, err)
				}

				// Create test files
				testFiles := map[string]string{
					"unit1/terragrunt.hcl":         "",
					"unit2/terragrunt.hcl":         "",
					"stack1/terragrunt.stack.hcl":  "",
					".hidden/unit3/terragrunt.hcl": "",
					"nested/unit4/terragrunt.hcl":  "",
				}

				for path, content := range testFiles {
					err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
					require.NoError(t, err)
				}

				return tmpDir
			},
			expectedPaths: []string{"unit1", "unit2", "nested/unit4", "stack1", ".hidden/unit3"},
			format:        "text",
			mode:          "normal",
			dependencies:  false,
			external:      false,
			validate: func(t *testing.T, output string, expectedPaths []string) {
				t.Helper()

				// Split output into lines and trim whitespace
				lines := strings.Split(strings.TrimSpace(output), "\n")

				// Verify we have the expected number of lines
				assert.Len(t, lines, len(expectedPaths))

				// Convert expected paths to use OS-specific path separators
				osExpectedPaths := make([]string, 0, len(expectedPaths))
				for _, path := range expectedPaths {
					osExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))
				}

				// Convert actual paths to use OS-specific path separators
				osPaths := make([]string, 0, len(lines))
				for _, line := range lines {
					osPaths = append(osPaths, filepath.FromSlash(strings.TrimSpace(line)))
				}

				// Verify all expected paths are present
				assert.ElementsMatch(t, osExpectedPaths, osPaths)
			},
		},
		{
			name: "dag sorting - simple dependencies",
			setup: func(t *testing.T) string {
				t.Helper()

				tmpDir := helpers.TmpDirWOSymlinks(t)

				// Create test directory structure with dependencies:
				// unit2 -> unit1
				// unit3 -> unit2
				testDirs := []string{
					"unit1",
					"unit2",
					"unit3",
				}

				for _, dir := range testDirs {
					err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
					require.NoError(t, err)
				}

				// Create test files with dependencies
				testFiles := map[string]string{
					"unit1/terragrunt.hcl": "",
					"unit2/terragrunt.hcl": `
dependency "unit1" {
  config_path = "../unit1"
}`,
					"unit3/terragrunt.hcl": `
dependency "unit2" {
  config_path = "../unit2"
}`,
				}

				for path, content := range testFiles {
					err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
					require.NoError(t, err)
				}

				return tmpDir
			},
			expectedPaths: []string{"unit1", "unit2", "unit3"},
			format:        "text",
			mode:          "dag",
			dependencies:  true,
			external:      false,
			validate: func(t *testing.T, output string, expectedPaths []string) {
				t.Helper()

				// Split output into lines and trim whitespace
				lines := strings.Split(strings.TrimSpace(output), "\n")

				// Verify we have the expected number of lines
				assert.Len(t, lines, len(expectedPaths))

				// Convert paths to use OS-specific separators
				osPaths := make([]string, 0, len(lines))
				for _, line := range lines {
					osPaths = append(osPaths, filepath.FromSlash(strings.TrimSpace(line)))
				}

				// Convert expected paths to use OS-specific separators
				osExpectedPaths := make([]string, 0, len(expectedPaths))
				for _, path := range expectedPaths {
					osExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))
				}

				// For DAG sorting, order matters - verify exact order
				assert.Equal(t, osExpectedPaths, osPaths)
			},
		},
		{
			name: "dag sorting - json output with dependencies",
			setup: func(t *testing.T) string {
				t.Helper()

				tmpDir := helpers.TmpDirWOSymlinks(t)

				// Create test directory structure with dependencies
				testDirs := []string{
					"A", "B", "C",
				}

				for _, dir := range testDirs {
					err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
					require.NoError(t, err)
				}

				// Create test files with dependencies
				testFiles := map[string]string{
					"A/terragrunt.hcl": "",
					"B/terragrunt.hcl": `
dependency "A" {
  config_path = "../A"
}`,
					"C/terragrunt.hcl": `
dependency "B" {
  config_path = "../B"
}`,
				}

				for path, content := range testFiles {
					err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
					require.NoError(t, err)
				}

				return tmpDir
			},
			expectedPaths: []string{"A", "B", "C"},
			format:        "json",
			mode:          "dag",
			dependencies:  true,
			external:      false,
			validate: func(t *testing.T, output string, expectedPaths []string) {
				t.Helper()

				// Verify the output is valid JSON
				var configs []find.FoundComponent

				err := json.Unmarshal([]byte(output), &configs)
				require.NoError(t, err)

				// Verify we have the expected number of configs
				assert.Len(t, configs, len(expectedPaths))

				// Extract paths and verify order
				paths := make([]string, 0, len(configs))
				for _, config := range configs {
					paths = append(paths, filepath.FromSlash(config.Path))
				}

				// Convert expected paths to use OS-specific separators
				osExpectedPaths := make([]string, 0, len(expectedPaths))
				for _, path := range expectedPaths {
					osExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))
				}

				assert.Equal(t, osExpectedPaths, paths)

				// Verify dependencies are correctly represented in JSON
				assert.Empty(t, configs[0].Dependencies, "A should have no dependencies")
				assert.Equal(t, []string{"A"}, configs[1].Dependencies, "B should depend on A")
				assert.Equal(t, []string{"B"}, configs[2].Dependencies, "C should depend on B")
			},
		},
		{
			name: "invalid format",
			setup: func(t *testing.T) string {
				t.Helper()

				tmpDir := helpers.TmpDirWOSymlinks(t)

				return tmpDir
			},
			format: "invalid",
			validate: func(t *testing.T, output string, expectedPaths []string) {
				t.Helper()
				assert.Empty(t, output)
			},
		},
		{
			name: "invalid sort",
			setup: func(t *testing.T) string {
				t.Helper()

				tmpDir := helpers.TmpDirWOSymlinks(t)

				return tmpDir
			},
			mode: "invalid",
			validate: func(t *testing.T, output string, expectedPaths []string) {
				t.Helper()
				assert.Empty(t, output)
			},
		},
		{
			name: "reading flag with json output",
			setup: func(t *testing.T) string {
				t.Helper()

				tmpDir := helpers.TmpDirWOSymlinks(t)
				appDir := filepath.Join(tmpDir, "app")
				require.NoError(t, os.MkdirAll(appDir, 0755))

				// Create shared files that will be read
				sharedHCL := filepath.Join(tmpDir, "shared.hcl")
				sharedTFVars := filepath.Join(tmpDir, "shared.tfvars")

				require.NoError(t, os.WriteFile(sharedHCL, []byte(`
locals {
  common_value = "test"
}
`), 0644))

				require.NoError(t, os.WriteFile(sharedTFVars, []byte(`
test_var = "value"
`), 0644))

				// Create terragrunt config that reads both files
				terragruntConfig := filepath.Join(appDir, "terragrunt.hcl")
				require.NoError(t, os.WriteFile(terragruntConfig, []byte(`
locals {
  shared_config = read_terragrunt_config("../shared.hcl")
  tfvars = read_tfvars_file("../shared.tfvars")
}
`), 0644))

				return tmpDir
			},
			expectedPaths: []string{"app"},
			format:        "json",
			mode:          "normal",
			reading:       true,
			validate: func(t *testing.T, output string, expectedPaths []string) {
				t.Helper()

				// Verify the output is valid JSON
				var configs find.FoundComponents

				err := json.Unmarshal([]byte(output), &configs)
				require.NoError(t, err)

				// Verify we have one config
				require.Len(t, configs, 1)

				// Verify the component has the Reading field populated
				appConfig := configs[0]
				require.NotNil(t, appConfig.Reading, "Reading field should be populated")
				require.NotEmpty(t, appConfig.Reading, "Reading field should contain files")

				// Verify Reading field contains the shared files
				readingPaths := appConfig.Reading
				assert.Len(t, readingPaths, 2, "should have read 2 files")

				// Convert to map for easier checking
				readingMap := make(map[string]bool)
				for _, path := range readingPaths {
					readingMap[filepath.FromSlash(path)] = true
				}

				// Check that shared files are in the reading list
				assert.True(t, readingMap["shared.hcl"], "should contain shared.hcl")
				assert.True(t, readingMap["shared.tfvars"], "should contain shared.tfvars")
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Setup test directory
			tmpDir := tt.setup(t)

			tgOpts := options.NewTerragruntOptions()
			tgOpts.WorkingDir = tmpDir

			l := logger.CreateLogger()
			l.Formatter().SetDisabledColors(true)

			// Create options
			opts := find.NewOptions(tgOpts)
			opts.Format = tt.format
			opts.NoHidden = tt.noHidden
			opts.Mode = tt.mode
			opts.Dependencies = tt.dependencies
			opts.Reading = tt.reading

			// Create a pipe to capture output
			r, w, err := os.Pipe()
			require.NoError(t, err)

			// Set the writer in options
			opts.Writers.Writer = w

			err = find.Run(t.Context(), l, opts)
			if tt.format == "invalid" || tt.mode == "invalid" {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)

			// Close the write end of the pipe
			w.Close()

			// Read all output
			output, err := io.ReadAll(r)
			require.NoError(t, err)

			// Validate the output
			tt.validate(t, string(output), tt.expectedPaths)
		})
	}
}

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

	tmpDir := helpers.TmpDirWOSymlinks(t)

	testDirs := []string{"A"}

	for _, dir := range testDirs {
		err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)
		require.NoError(t, err)
	}

	testFiles := map[string]string{
		filepath.Join("A", "terragrunt.hcl"): `
dependency "target" {
  config_path = read_terragrunt_config("does-not-exist.hcl").locals.aws_region == "x" ? "../a" : "../b"
}
`,
	}

	for path, content := range testFiles {
		err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)
		require.NoError(t, err)
	}

	tgOpts := options.NewTerragruntOptions()
	tgOpts.WorkingDir = tmpDir
	tgOpts.RootWorkingDir = tmpDir

	l := logger.CreateLogger()
	l.Formatter().SetDisabledColors(true)

	filters, err := filter.ParseFilterQueries(l, []string{"...A"})
	require.NoError(t, err)

	opts := find.NewOptions(tgOpts)
	opts.Filters = filters

	r, w, err := os.Pipe()
	require.NoError(t, err)

	opts.Writers.Writer = w

	err = find.Run(t.Context(), l, opts)
	require.NoError(t, err)

	require.NoError(t, w.Close())

	output, err := io.ReadAll(r)
	require.NoError(t, err)
	require.Equal(t, "A\n", string(output))
}

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

	colorizer := find.NewColorizer(true)

	tests := []struct {
		name   string
		config *find.FoundComponent
		// We can't test exact ANSI codes as they might vary by environment,
		// so we'll test that different types result in different outputs
		shouldBeDifferent []component.Kind
	}{
		{
			name: "unit config",
			config: &find.FoundComponent{
				Type: component.UnitKind,
				Path: "path/to/unit",
			},
			shouldBeDifferent: []component.Kind{component.StackKind},
		},
		{
			name: "stack config",
			config: &find.FoundComponent{
				Type: component.StackKind,
				Path: "path/to/stack",
			},
			shouldBeDifferent: []component.Kind{component.UnitKind},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			result := colorizer.Colorize(tt.config)
			assert.NotEmpty(t, result)

			// Test that different types produce different colorized outputs
			for _, diffType := range tt.shouldBeDifferent {
				diffConfig := &find.FoundComponent{
					Type: diffType,
					Path: tt.config.Path,
				}
				diffResult := colorizer.Colorize(diffConfig)
				assert.NotEqual(t, result, diffResult)
			}
		})
	}
}
