// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package command

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"

	"github.com/hashicorp/cli"
	"github.com/hashicorp/terraform/internal/backend"
	"github.com/hashicorp/terraform/internal/backend/local"
	"github.com/hashicorp/terraform/internal/terraform"
)

func TestMetaColorize(t *testing.T) {
	var m *Meta
	var args, args2 []string

	// Test basic, color
	m = new(Meta)
	m.Color = true
	args = []string{"foo", "bar"}
	args2 = []string{"foo", "bar"}
	args = m.process(args)
	if !reflect.DeepEqual(args, args2) {
		t.Fatalf("bad: %#v", args)
	}
	if m.Colorize().Disable {
		t.Fatal("should not be disabled")
	}

	// Test basic, no change
	m = new(Meta)
	args = []string{"foo", "bar"}
	args2 = []string{"foo", "bar"}
	args = m.process(args)
	if !reflect.DeepEqual(args, args2) {
		t.Fatalf("bad: %#v", args)
	}
	if !m.Colorize().Disable {
		t.Fatal("should be disabled")
	}

	// Test disable #1
	m = new(Meta)
	m.Color = true
	args = []string{"foo", "-no-color", "bar"}
	args2 = []string{"foo", "bar"}
	args = m.process(args)
	if !reflect.DeepEqual(args, args2) {
		t.Fatalf("bad: %#v", args)
	}
	if !m.Colorize().Disable {
		t.Fatal("should be disabled")
	}

	// Test disable #2
	// Verify multiple -no-color options are removed from args slice.
	// E.g. an additional -no-color arg could be added by TF_CLI_ARGS.
	m = new(Meta)
	m.Color = true
	args = []string{"foo", "-no-color", "bar", "-no-color"}
	args2 = []string{"foo", "bar"}
	args = m.process(args)
	if !reflect.DeepEqual(args, args2) {
		t.Fatalf("bad: %#v", args)
	}
	if !m.Colorize().Disable {
		t.Fatal("should be disabled")
	}
}

func TestMetaInputMode(t *testing.T) {
	test = false
	defer func() { test = true }()

	m := new(Meta)
	args := []string{}

	fs := m.extendedFlagSet("foo")
	if err := fs.Parse(args); err != nil {
		t.Fatalf("err: %s", err)
	}

	if m.InputMode() != terraform.InputModeStd {
		t.Fatalf("bad: %#v", m.InputMode())
	}
}

func TestMetaInputMode_envVar(t *testing.T) {
	test = false
	defer func() { test = true }()

	m := new(Meta)
	args := []string{}

	fs := m.extendedFlagSet("foo")
	if err := fs.Parse(args); err != nil {
		t.Fatalf("err: %s", err)
	}

	off := terraform.InputMode(0)
	on := terraform.InputModeStd
	cases := []struct {
		EnvVar   string
		Expected terraform.InputMode
	}{
		{"false", off},
		{"0", off},
		{"true", on},
		{"1", on},
	}

	for _, tc := range cases {
		t.Setenv(InputModeEnvVar, tc.EnvVar)
		if m.InputMode() != tc.Expected {
			t.Fatalf("expected InputMode: %#v, got: %#v", tc.Expected, m.InputMode())
		}
	}
}

func TestMetaInputMode_disable(t *testing.T) {
	test = false
	defer func() { test = true }()

	m := new(Meta)
	args := []string{"-input=false"}

	fs := m.extendedFlagSet("foo")
	if err := fs.Parse(args); err != nil {
		t.Fatalf("err: %s", err)
	}

	if m.InputMode() > 0 {
		t.Fatalf("bad: %#v", m.InputMode())
	}
}

func TestMeta_initStatePaths(t *testing.T) {
	m := new(Meta)
	m.initStatePaths()

	if m.statePath != DefaultStateFilename {
		t.Fatalf("bad: %#v", m)
	}
	if m.stateOutPath != DefaultStateFilename {
		t.Fatalf("bad: %#v", m)
	}
	if m.backupPath != DefaultStateFilename+DefaultBackupExtension {
		t.Fatalf("bad: %#v", m)
	}

	m = new(Meta)
	m.statePath = "foo"
	m.initStatePaths()

	if m.stateOutPath != "foo" {
		t.Fatalf("bad: %#v", m)
	}
	if m.backupPath != "foo"+DefaultBackupExtension {
		t.Fatalf("bad: %#v", m)
	}

	m = new(Meta)
	m.stateOutPath = "foo"
	m.initStatePaths()

	if m.statePath != DefaultStateFilename {
		t.Fatalf("bad: %#v", m)
	}
	if m.backupPath != "foo"+DefaultBackupExtension {
		t.Fatalf("bad: %#v", m)
	}
}

func TestMeta_Env(t *testing.T) {
	td := t.TempDir()
	os.MkdirAll(td, 0755)
	t.Chdir(td)

	m := new(Meta)

	env, err := m.Workspace()
	if err != nil {
		t.Fatal(err)
	}

	if env != backend.DefaultStateName {
		t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env)
	}

	testEnv := "test_env"
	if err := m.SetWorkspace(testEnv); err != nil {
		t.Fatal("error setting env:", err)
	}

	env, _ = m.Workspace()
	if env != testEnv {
		t.Fatalf("expected env %q, got env %q", testEnv, env)
	}

	if err := m.SetWorkspace(backend.DefaultStateName); err != nil {
		t.Fatal("error setting env:", err)
	}

	env, _ = m.Workspace()
	if env != backend.DefaultStateName {
		t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env)
	}
}

func TestMeta_StatePersistInterval(t *testing.T) {
	m := new(Meta)
	t.Run("when the env var is not defined", func(t *testing.T) {
		interval := m.StatePersistInterval()
		if interval != DefaultStatePersistInterval {
			t.Fatalf("expected state persist interval to be %d, got: %d", DefaultStatePersistInterval, interval)
		}
	})
	t.Run("with valid interval greater than the default", func(t *testing.T) {
		t.Setenv(StatePersistIntervalEnvVar, "25")

		interval := m.StatePersistInterval()
		if interval != 25 {
			t.Fatalf("expected state persist interval to be 25, got: %d", interval)
		}
	})
	t.Run("with a valid interval less than the default", func(t *testing.T) {
		t.Setenv(StatePersistIntervalEnvVar, "10")

		interval := m.StatePersistInterval()
		if interval != DefaultStatePersistInterval {
			t.Fatalf("expected state persist interval to be %d, got: %d", DefaultStatePersistInterval, interval)
		}
	})
	t.Run("with invalid integer interval", func(t *testing.T) {
		t.Setenv(StatePersistIntervalEnvVar, "foo")

		interval := m.StatePersistInterval()
		if interval != DefaultStatePersistInterval {
			t.Fatalf("expected state persist interval to be %d, got: %d", DefaultStatePersistInterval, interval)
		}
	})
	t.Run("with negative integer interval", func(t *testing.T) {
		t.Setenv(StatePersistIntervalEnvVar, "-10")

		interval := m.StatePersistInterval()
		if interval != DefaultStatePersistInterval {
			t.Fatalf("expected state persist interval to be %d, got: %d", DefaultStatePersistInterval, interval)
		}
	})
}

// Invalid workspace names provided via ENV are validated.
func TestMeta_Workspace_override(t *testing.T) {
	m := new(Meta)

	testCases := map[string]struct {
		workspace string
		err       error
	}{
		"": {
			"default",
			nil,
		},
		"development": {
			"development",
			nil,
		},
		"invalid name": {
			"",
			errInvalidWorkspaceNameEnvVar,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			t.Setenv(WorkspaceNameEnvVar, name)
			workspace, err := m.Workspace()
			if workspace != tc.workspace {
				t.Errorf("Unexpected workspace\n got: %s\nwant: %s\n", workspace, tc.workspace)
			}
			if err != tc.err {
				t.Errorf("Unexpected error\n got: %s\nwant: %s\n", err, tc.err)
			}
		})
	}
}

func TestMeta_Workspace_invalidSelected(t *testing.T) {
	td := t.TempDir()
	t.Chdir(td)

	// this is an invalid workspace name
	workspace := "test workspace"

	// create the workspace directories
	if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, workspace), 0755); err != nil {
		t.Fatal(err)
	}

	// create the workspace file to select it
	if err := os.MkdirAll(DefaultDataDir, 0755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte(workspace), 0644); err != nil {
		t.Fatal(err)
	}

	m := new(Meta)

	// Normally, errors are returned when selecting an invalid workspace.
	ws, err := m.Workspace()
	if ws != "" {
		t.Errorf("Unexpected workspace\n got: %s\nwant: %s\n", ws, workspace)
	}
	if err == nil {
		t.Errorf("Expected error but got none")
	}

	// But it is possible to select an invalid workspace, enabling some
	// commands to interact with and correct the issue.
	m.bypassWorkspaceNameValidityCheck = true
	ws, err = m.Workspace()
	if ws != workspace {
		t.Errorf("Unexpected workspace\n got: %s\nwant: %s\n", ws, workspace)
	}
	if err != nil {
		t.Errorf("Unexpected error: %s", err)
	}
}

func TestMeta_process(t *testing.T) {
	test = false
	defer func() { test = true }()

	// Create a temporary directory for our cwd
	d := t.TempDir()
	os.MkdirAll(d, 0755)
	t.Chdir(d)

	// At one point it was the responsibility of this process function to
	// insert fake additional -var-file options into the command line
	// if the automatic tfvars files were present. This is no longer the
	// responsibility of process (it happens in collectVariableValues instead)
	// but we're still testing with these files in place to verify that
	// they _aren't_ being interpreted by process, since that could otherwise
	// cause them to be added more than once and mess up the precedence order.
	defaultVarsfile := "terraform.tfvars"
	err := ioutil.WriteFile(
		filepath.Join(d, defaultVarsfile),
		[]byte(""),
		0644)
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	fileFirstAlphabetical := "a-file.auto.tfvars"
	err = ioutil.WriteFile(
		filepath.Join(d, fileFirstAlphabetical),
		[]byte(""),
		0644)
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	fileLastAlphabetical := "z-file.auto.tfvars"
	err = ioutil.WriteFile(
		filepath.Join(d, fileLastAlphabetical),
		[]byte(""),
		0644)
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	// Regular tfvars files will not be autoloaded
	fileIgnored := "ignored.tfvars"
	err = ioutil.WriteFile(
		filepath.Join(d, fileIgnored),
		[]byte(""),
		0644)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	tests := []struct {
		GivenArgs    []string
		FilteredArgs []string
		ExtraCheck   func(*testing.T, *Meta)
	}{
		{
			[]string{},
			[]string{},
			func(t *testing.T, m *Meta) {
				if got, want := m.color, true; got != want {
					t.Errorf("wrong m.color value %#v; want %#v", got, want)
				}
				if got, want := m.Color, true; got != want {
					t.Errorf("wrong m.Color value %#v; want %#v", got, want)
				}
			},
		},
		{
			[]string{"-no-color"},
			[]string{},
			func(t *testing.T, m *Meta) {
				if got, want := m.color, false; got != want {
					t.Errorf("wrong m.color value %#v; want %#v", got, want)
				}
				if got, want := m.Color, false; got != want {
					t.Errorf("wrong m.Color value %#v; want %#v", got, want)
				}
			},
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("%s", test.GivenArgs), func(t *testing.T) {
			m := new(Meta)
			m.Color = true // this is the default also for normal use, overridden by -no-color
			args := test.GivenArgs
			args = m.process(args)

			if !cmp.Equal(test.FilteredArgs, args) {
				t.Errorf("wrong filtered arguments\n%s", cmp.Diff(test.FilteredArgs, args))
			}

			if test.ExtraCheck != nil {
				test.ExtraCheck(t, m)
			}
		})
	}
}

func TestCommand_checkRequiredVersion(t *testing.T) {
	// Create a temporary working directory that is empty
	td := t.TempDir()
	testCopyDir(t, testFixturePath("command-check-required-version"), td)
	t.Chdir(td)

	ui := cli.NewMockUi()
	meta := Meta{
		Ui: ui,
	}

	diags := meta.checkRequiredVersion()
	if diags == nil {
		t.Fatalf("diagnostics should contain unmet version constraint, but is nil")
	}

	meta.showDiagnostics(diags)

	// Required version diags are correct
	errStr := ui.ErrorWriter.String()
	if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) {
		t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr)
	}
	if strings.Contains(errStr, `required_version = ">= 0.13.0"`) {
		t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr)
	}
}
