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

package e2etest

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

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

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/e2e"
	"github.com/hashicorp/terraform/internal/getproviders"
)

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

	// This test reaches out to releases.hashicorp.com to download the
	// template provider, so it can only run if network access is allowed.
	// We intentionally don't try to stub this here, because there's already
	// a stubbed version of this in the "command" package and so the goal here
	// is to test the interaction with the real repository.
	skipIfCannotAccessNetwork(t)

	fixturePath := filepath.Join("testdata", "template-provider")
	tf := e2e.NewBinary(t, terraformBin, fixturePath)

	// zero out any existing cli config file by passing in an empty file.
	configFile := emptyConfigFileForTests(t, tf.WorkDir())
	tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile))

	stdout, stderr, err := tf.Run("init")
	if err != nil {
		t.Errorf("unexpected error: %s", err)
	}

	if stderr != "" {
		t.Errorf("unexpected stderr output:\n%s", stderr)
	}

	if !strings.Contains(stdout, "Terraform has been successfully initialized!") {
		t.Errorf("success message is missing from output:\n%s", stdout)
	}

	if !strings.Contains(stdout, "- Installing hashicorp/template v") {
		t.Errorf("provider download message is missing from output:\n%s", stdout)
		t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)")
	}

	if !strings.Contains(stdout, "Terraform has created a lock file") {
		t.Errorf("lock file notification is missing from output:\n%s", stdout)
	}
}

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

	// This test should _not_ reach out anywhere because the "terraform"
	// provider is internal to the core terraform binary.

	fixturePath := filepath.Join("testdata", "terraform-provider")
	tf := e2e.NewBinary(t, terraformBin, fixturePath)

	// zero out any existing cli config file by passing in an empty file.
	configFile := emptyConfigFileForTests(t, tf.WorkDir())
	tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile))

	stdout, stderr, err := tf.Run("init")
	if err != nil {
		t.Errorf("unexpected error: %s", err)
	}

	if stderr != "" {
		t.Errorf("unexpected stderr output:\n%s", stderr)
	}

	if !strings.Contains(stdout, "Terraform has been successfully initialized!") {
		t.Errorf("success message is missing from output:\n%s", stdout)
	}

	if strings.Contains(stdout, "Installing hashicorp/terraform") {
		// Shouldn't have downloaded anything with this config, because the
		// provider is built in.
		t.Errorf("provider download message appeared in output:\n%s", stdout)
	}

	if strings.Contains(stdout, "Installing terraform.io/builtin/terraform") {
		// Shouldn't have downloaded anything with this config, because the
		// provider is built in.
		t.Errorf("provider download message appeared in output:\n%s", stdout)
	}
}

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

	// This test will try to reach out to registry.terraform.io as one of the
	// possible installation locations for
	// hashicorp/null, where it will find that
	// versions do exist but will ultimately select the version that is
	// vendored due to the version constraint.
	skipIfCannotAccessNetwork(t)

	fixturePath := filepath.Join("testdata", "vendored-provider")
	tf := e2e.NewBinary(t, terraformBin, fixturePath)

	// Our fixture dir has a generic os_arch dir, which we need to customize
	// to the actual OS/arch where this test is running in order to get the
	// desired result.
	fixtMachineDir := tf.Path("terraform.d/plugins/registry.terraform.io/hashicorp/null/1.0.0+local/os_arch")
	wantMachineDir := tf.Path("terraform.d/plugins/registry.terraform.io/hashicorp/null/1.0.0+local/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
	err := os.Rename(fixtMachineDir, wantMachineDir)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	// zero out any existing cli config file by passing in an empty file.
	configFile := emptyConfigFileForTests(t, tf.WorkDir())
	tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile))

	stdout, stderr, err := tf.Run("init")
	if err != nil {
		t.Errorf("unexpected error: %s", err)
	}

	if stderr != "" {
		t.Errorf("unexpected stderr output:\n%s", stderr)
	}

	if !strings.Contains(stdout, "Terraform has been successfully initialized!") {
		t.Errorf("success message is missing from output:\n%s", stdout)
	}

	if !strings.Contains(stdout, "- Installing hashicorp/null v1.0.0+local") {
		t.Errorf("provider download message is missing from output:\n%s", stdout)
	}
}

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

	// This test should not reach out to the network if it is behaving as
	// intended. If it _does_ try to access an upstream registry and encounter
	// an error doing so then that's a legitimate test failure that should be
	// fixed. (If it incorrectly reaches out anywhere then it's likely to be
	// to the host "example.com", which is the placeholder domain we use in
	// the test fixture.)

	fixturePath := filepath.Join("testdata", "local-only-provider")
	tf := e2e.NewBinary(t, terraformBin, fixturePath)

	// Our fixture dir has a generic os_arch dir, which we need to customize
	// to the actual OS/arch where this test is running in order to get the
	// desired result.
	fixtMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/os_arch")
	wantMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
	err := os.Rename(fixtMachineDir, wantMachineDir)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	// If you run this test on a workstation with a plugin-cache directory
	// configured, it will leave a bad directory behind and terraform init will
	// not work until you remove it.
	//
	// To avoid this, we will  "zero out" any existing cli config file by
	// passing in an empty override file.
	configFile := emptyConfigFileForTests(t, wantMachineDir)
	tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile))

	stdout, stderr, err := tf.Run("init")
	if err != nil {
		t.Errorf("unexpected error: %s", err)
	}

	if stderr != "" {
		t.Errorf("unexpected stderr output:\n%s", stderr)
		t.Logf("(a \"Failed to query available provider packages\" error can happen here if you have a .terraformrc CLI configuration file present in your home directory)")
	}

	if !strings.Contains(stdout, "Terraform has been successfully initialized!") {
		t.Errorf("success message is missing from output:\n%s", stdout)
	}

	if !strings.Contains(stdout, "- Installing example.com/awesomecorp/happycloud v1.2.0") {
		t.Errorf("provider download message is missing from output:\n%s", stdout)
		t.Logf("(this can happen if you have a conflicting copy of the plugin in one of the global plugin search dirs)")
	}
}

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

	// This test should not reach out to the network if it is behaving as
	// intended. If it _does_ try to access an upstream registry and encounter
	// an error doing so then that's a legitimate test failure that should be
	// fixed. (If it incorrectly reaches out anywhere then it's likely to be
	// to the host "example.com", which is the placeholder domain we use in
	// the test fixture.)

	for _, configFile := range []string{"cliconfig.tfrc", "cliconfig.tfrc.json"} {
		t.Run(configFile, func(t *testing.T) {
			fixturePath := filepath.Join("testdata", "custom-provider-install-method")
			tf := e2e.NewBinary(t, terraformBin, fixturePath)

			// Our fixture dir has a generic os_arch dir, which we need to customize
			// to the actual OS/arch where this test is running in order to get the
			// desired result.
			fixtMachineDir := tf.Path("fs-mirror/example.com/awesomecorp/happycloud/1.2.0/os_arch")
			wantMachineDir := tf.Path("fs-mirror/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
			err := os.Rename(fixtMachineDir, wantMachineDir)
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			// We'll use a local CLI configuration file taken from our fixture
			// directory so we can force a custom installation method config.
			tf.AddEnv("TF_CLI_CONFIG_FILE=" + tf.Path(configFile))

			stdout, stderr, err := tf.Run("init")
			if err != nil {
				t.Errorf("unexpected error: %s", err)
			}

			if stderr != "" {
				t.Errorf("unexpected stderr output:\n%s", stderr)
			}

			if !strings.Contains(stdout, "Terraform has been successfully initialized!") {
				t.Errorf("success message is missing from output:\n%s", stdout)
			}

			if !strings.Contains(stdout, "- Installing example.com/awesomecorp/happycloud v1.2.0") {
				t.Errorf("provider download message is missing from output:\n%s", stdout)
			}
		})
	}
}

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

	// This test reaches out to releases.hashicorp.com to access plugin
	// metadata, and download the null plugin, though the template plugin
	// should come from local cache.
	skipIfCannotAccessNetwork(t)

	fixturePath := filepath.Join("testdata", "plugin-cache")
	tf := e2e.NewBinary(t, terraformBin, fixturePath)

	// Our fixture dir has a generic os_arch dir, which we need to customize
	// to the actual OS/arch where this test is running in order to get the
	// desired result.
	fixtMachineDir := tf.Path("cache/registry.terraform.io/hashicorp/template/2.1.0/os_arch")
	wantMachineDir := tf.Path("cache/registry.terraform.io/hashicorp/template/2.1.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
	err := os.Rename(fixtMachineDir, wantMachineDir)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	// zero out any existing cli config file by passing in an empty file.
	configFile := emptyConfigFileForTests(t, tf.WorkDir())
	tf.AddEnv(fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", configFile))

	cmd := tf.Cmd("init")

	// convert the slashes if building for windows.
	p := filepath.FromSlash("./cache")
	cmd.Env = append(cmd.Env, "TF_PLUGIN_CACHE_DIR="+p)
	err = cmd.Run()
	if err != nil {
		t.Errorf("unexpected error: %s", err)
	}

	path := filepath.FromSlash(fmt.Sprintf(".terraform/providers/registry.terraform.io/hashicorp/template/2.1.0/%s_%s/terraform-provider-template_v2.1.0_x4", runtime.GOOS, runtime.GOARCH))
	content, err := tf.ReadFile(path)
	if err != nil {
		t.Fatalf("failed to read installed plugin from %s: %s", path, err)
	}
	if strings.TrimSpace(string(content)) != "this is not a real plugin" {
		t.Errorf("template plugin was not installed from local cache")
	}

	nullLinkPath := filepath.FromSlash(fmt.Sprintf(".terraform/providers/registry.terraform.io/hashicorp/null/2.1.0/%s_%s/terraform-provider-null_v2.1.0_x4", runtime.GOOS, runtime.GOARCH))
	if runtime.GOOS == "windows" {
		nullLinkPath = nullLinkPath + ".exe"
	}
	if !tf.FileExists(nullLinkPath) {
		t.Errorf("null plugin was not installed into %s", nullLinkPath)
	}

	nullCachePath := filepath.FromSlash(fmt.Sprintf("cache/registry.terraform.io/hashicorp/null/2.1.0/%s_%s/terraform-provider-null_v2.1.0_x4", runtime.GOOS, runtime.GOARCH))
	if runtime.GOOS == "windows" {
		nullCachePath = nullCachePath + ".exe"
	}
	if !tf.FileExists(nullCachePath) {
		t.Errorf("null plugin is not in cache after install. expected in: %s", nullCachePath)
	}
}

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

	// This test reaches out to registry.terraform.io and github.com to lookup
	// and fetch a module.
	skipIfCannotAccessNetwork(t)

	fixturePath := filepath.Join("testdata", "empty")
	tf := e2e.NewBinary(t, terraformBin, fixturePath)

	cmd := tf.Cmd("init", "-from-module=hashicorp/vault-starter/aws")
	cmd.Stdin = nil
	cmd.Stderr = &bytes.Buffer{}

	err := cmd.Run()
	if err != nil {
		t.Errorf("unexpected error: %s", err)
	}

	stderr := cmd.Stderr.(*bytes.Buffer).String()
	if stderr != "" {
		t.Errorf("unexpected stderr output:\n%s", stderr)
	}

	content, err := tf.ReadFile("main.tf")
	if err != nil {
		t.Fatalf("failed to read main.tf: %s", err)
	}
	if !bytes.Contains(content, []byte("vault")) {
		t.Fatalf("main.tf doesn't appear to be a vault configuration: \n%s", content)
	}
}

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

	// This test will reach out to registry.terraform.io as one of the possible
	// installation locations for hashicorp/nonexist, which should not exist.
	skipIfCannotAccessNetwork(t)

	fixturePath := filepath.Join("testdata", "provider-not-found")
	tf := e2e.NewBinary(t, terraformBin, fixturePath)

	t.Run("registry provider not found", func(t *testing.T) {
		_, stderr, err := tf.Run("init", "-no-color")
		if err == nil {
			t.Fatal("expected error, got success")
		}

		oneLineStderr := strings.ReplaceAll(stderr, "\n", " ")
		if !strings.Contains(oneLineStderr, "provider registry registry.terraform.io does not have a provider named registry.terraform.io/hashicorp/nonexist") {
			t.Errorf("expected error message is missing from output:\n%s", stderr)
		}

		if !strings.Contains(oneLineStderr, "All modules should specify their required_providers") {
			t.Errorf("expected error message is missing from output:\n%s", stderr)
		}
	})

	t.Run("local provider not found", func(t *testing.T) {
		// The -plugin-dir directory must exist for the provider installer to search it.
		pluginDir := tf.Path("empty")
		if err := os.Mkdir(pluginDir, os.ModePerm); err != nil {
			t.Fatal(err)
		}

		_, stderr, err := tf.Run("init", "-no-color", "-plugin-dir="+pluginDir)
		if err == nil {
			t.Fatal("expected error, got success")
		}

		if !strings.Contains(stderr, "provider registry.terraform.io/hashicorp/nonexist was not\nfound in any of the search locations\n\n  - "+pluginDir) {
			t.Errorf("expected error message is missing from output:\n%s", stderr)
		}
	})

	t.Run("special characters enabled", func(t *testing.T) {
		_, stderr, err := tf.Run("init")
		if err == nil {
			t.Fatal("expected error, got success")
		}

		expectedErr := `╷
│ Error: Failed to query available provider packages
│` + ` ` + `
│ Could not retrieve the list of available versions for provider
│ hashicorp/nonexist: provider registry registry.terraform.io does not have a
│ provider named registry.terraform.io/hashicorp/nonexist
│` + ` ` + `
│ All modules should specify their required_providers so that external
│ consumers will get the correct providers when using a module. To see which
│ modules are currently depending on hashicorp/nonexist, run the following
│ command:
│     terraform providers
╵
`
		if stripAnsi(stderr) != expectedErr {
			t.Errorf("wrong output:\n%s", cmp.Diff(stripAnsi(stderr), expectedErr))
		}
	})
}

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

	// This test will reach out to registry.terraform.io as one of the possible
	// installation locations for hashicorp/nonexist, which should not exist.
	skipIfCannotAccessNetwork(t)

	fixturePath := filepath.Join("testdata", "provider-warnings")
	tf := e2e.NewBinary(t, terraformBin, fixturePath)

	stdout, _, err := tf.Run("init")
	if err == nil {
		t.Fatal("expected error, got success")
	}

	if !strings.Contains(stdout, "This provider is archived and no longer needed.") {
		t.Errorf("expected warning message is missing from output:\n%s", stdout)
	}
}

// This is a regression test asserting that `terraform init -upgrade` doesn't error
// when the state storage provider is unmanaged by Terraform. The check to see if the
// state storage provider was affected by the upgrade process should be skipped when
// the provider is not managed by Terraform. Previously the check wasn't skipped and
// panicked due to how the provider was supplied.
//
// See the TestInit_getUpgradePlugins integration test for similar testing when using a
// dev_override provider.
func TestInitStateStoreUsingUnmanagedProvider(t *testing.T) {
	if !canRunGoBuild {
		// We're running in a separate-build-then-run context, so we can't
		// currently execute this test which depends on being able to build
		// new executable at runtime.
		//
		// (See the comment on canRunGoBuild's declaration for more information.)
		t.Skip("can't run without building a new provider executable")
	}

	// In temp dir create a plugin cache to be used in the test cases.
	// The cache is supplied to commands using the -plugin-dir init flag.
	// There are 2 versions of the simple6 provider: 0.0.1 and 2.0.0
	// This enables us to test an upgrade scenario where a newer provider version is available.
	td := t.TempDir()
	providerVersionOld := "0.0.1"
	providerVersionNew := "2.0.0"
	platform := getproviders.CurrentPlatform.String()
	absolutePathToCache := filepath.Join(td, "cache")
	simple6Provider := filepath.Join(td, "terraform-provider-simple6")
	simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
	for _, v := range []string{providerVersionOld, providerVersionNew} {
		dir := filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple6", v, platform)
		if err := os.MkdirAll(dir, os.ModePerm); err != nil {
			t.Fatal(err)
		}
		// Create an executable copy of the simple6ProviderExe file per version in the cache dir
		data, err := os.ReadFile(simple6ProviderExe)
		if err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(dir, "terraform-provider-simple6"), data, 0755); err != nil {
			t.Fatal(err)
		}
	}

	fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-unmanaged")
	tf := e2e.NewBinary(t, experimentalTerraformBin, fixturePath)

	// Assert the existing lockfile describes the older version of the provider.
	lockFile := tf.Path(".terraform.lock.hcl")
	buf, err := os.ReadFile(lockFile)
	if err != nil {
		t.Fatalf("unexpected error accessing lock file: %s", err)
	}
	buf = bytes.TrimSpace(buf)

	expectedLockFileContent := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/simple6" {
  version = "%s"
}`, providerVersionOld)
	if diff := cmp.Diff(expectedLockFileContent, string(buf)); diff != "" {
		t.Errorf("unexpected difference in lock file content: %s", diff)
	}

	// The simple6 provider is unmanaged
	reattachConfig, _ := reattachedProviderForTest(t, addrs.NewDefaultProvider("simple6"), 6)
	tf.AddEnv("TF_REATTACH_PROVIDERS=" + reattachConfig)

	// The init -upgrade process should succeed.
	stdout, stderr, err := tf.Run(
		"init",
		"-upgrade",
		"-enable-pluggable-state-storage-experiment",
		fmt.Sprintf("-plugin-dir=%s", absolutePathToCache),
	)
	if err != nil {
		t.Fatalf("unexpected error: %s\nstdout: %s\nstderr: %s", err, stdout, stderr)
	}

	// Lockfile should be unchanged because the only provider in the config is unmanaged.
	buf, err = os.ReadFile(lockFile)
	if err != nil {
		t.Fatalf("unexpected error accessing lock file: %s", err)
	}
	buf = bytes.TrimSpace(buf)

	if diff := cmp.Diff(expectedLockFileContent, string(buf)); diff != "" {
		t.Errorf("unexpected difference in lock file content: %s", diff)
	}
}

// emptyConfigFileForTests creates a blank .terraformrc file in the requested
// path and returns the path to the new file. It is the caller's responsibility
// to cleanup the file after use.
func emptyConfigFileForTests(t testing.TB, path string) string {
	// zero out any existing cli config file by passing in an empty file.
	configFile, err := os.Create(filepath.Join(path, ".terraformrc"))
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	configFile.Close()
	return configFile.Name()
}
