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

package command

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

	"github.com/hashicorp/cli"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/backend"
	backendInit "github.com/hashicorp/terraform/internal/backend/init"
	backendCloud "github.com/hashicorp/terraform/internal/cloud"
	"github.com/hashicorp/terraform/internal/depsfile"
	"github.com/hashicorp/terraform/internal/getproviders"
)

func TestProvidersLock(t *testing.T) {
	t.Run("noop", func(t *testing.T) {
		// in the most basic case, running providers lock in a directory with no configuration at all should succeed.
		// create an empty working directory
		td := t.TempDir()
		os.MkdirAll(td, 0755)
		t.Chdir(td)

		ui := new(cli.MockUi)
		c := &ProvidersLockCommand{
			Meta: Meta{
				Ui: ui,
			},
		}
		code := c.Run([]string{})
		if code != 0 {
			t.Fatalf("wrong exit code; expected 0, got %d", code)
		}
	})

	// This test depends on the -fs-mirror argument, so we always know what results to expect
	t.Run("basic", func(t *testing.T) {
		testDirectory := "providers-lock/basic"
		expected := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/test" {
  version = "1.0.0"
  hashes = [
    "h1:7MjN4eFisdTv4tlhXH5hL4QQd39Jy4baPhFxwAd/EFE=",
  ]
}
`
		runProviderLockGenericTest(t, testDirectory, expected, false)
	})

	// This test depends on the -fs-mirror argument, so we always know what results to expect
	t.Run("append", func(t *testing.T) {
		testDirectory := "providers-lock/append"
		expected := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/test" {
  version = "1.0.0"
  hashes = [
    "h1:7MjN4eFisdTv4tlhXH5hL4QQd39Jy4baPhFxwAd/EFE=",
    "h1:invalid",
  ]
}
`
		runProviderLockGenericTest(t, testDirectory, expected, false)
	})

	// This test depends on the -fs-mirror argument, so we always know what results to expect
	t.Run("tests", func(t *testing.T) {
		testDirectory := "providers-lock/with-tests"
		expected := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/test" {
  version = "1.0.0"
  hashes = [
    "h1:7MjN4eFisdTv4tlhXH5hL4QQd39Jy4baPhFxwAd/EFE=",
  ]
}
`
		runProviderLockGenericTest(t, testDirectory, expected, true)
	})
}

func runProviderLockGenericTest(t *testing.T, testDirectory, expected string, init bool) {
	td := t.TempDir()
	testCopyDir(t, testFixturePath(testDirectory), td)
	t.Chdir(td)

	// 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 := filepath.Join(td, "fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/os_arch")
	wantMachineDir := filepath.Join(td, "fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
	err := os.Rename(fixtMachineDir, wantMachineDir)
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	if init {
		// optionally execute the get command to fetch local modules if the
		// test case needs them
		c := &GetCommand{
			Meta: Meta{
				Ui: new(cli.MockUi),
			},
		}
		code := c.Run(nil)
		if code != 0 {
			t.Fatal("failed get command")
		}
	}

	p := testProvider()
	ui := new(cli.MockUi)
	c := &ProvidersLockCommand{
		Meta: Meta{
			Ui:               ui,
			testingOverrides: metaOverridesForProvider(p),
		},
	}

	args := []string{"-fs-mirror=fs-mirror"}
	code := c.Run(args)
	if code != 0 {
		t.Fatalf("wrong exit code; expected 0, got %d", code)
	}

	lockfile, err := os.ReadFile(".terraform.lock.hcl")
	if err != nil {
		t.Fatal("error reading lockfile")
	}

	if string(lockfile) != expected {
		t.Fatalf("wrong lockfile content")
	}
}

func TestProvidersLock_constVariable(t *testing.T) {
	t.Run("missing value", func(t *testing.T) {
		wd := tempWorkingDirFixture(t, "dynamic-module-sources/get-const-var")
		t.Chdir(wd.RootModuleDir())

		ui := cli.NewMockUi()
		c := &ProvidersLockCommand{
			Meta: Meta{
				testingOverrides: metaOverridesForProvider(testProvider()),
				Ui:               ui,
				WorkingDir:       wd,
			},
		}

		args := []string{"-fs-mirror=fs-mirror"}
		if code := c.Run(args); code == 0 {
			t.Fatalf("expected error, got 0")
		}
	})

	t.Run("value via cli", func(t *testing.T) {
		wd := tempWorkingDirFixture(t, "dynamic-module-sources/get-const-var")
		t.Chdir(wd.RootModuleDir())

		ui := cli.NewMockUi()
		c := &ProvidersLockCommand{
			Meta: Meta{
				testingOverrides: metaOverridesForProvider(testProvider()),
				Ui:               ui,
				WorkingDir:       wd,
			},
		}

		args := []string{"-var", "module_name=example", "-fs-mirror=fs-mirror"}
		if code := c.Run(args); code == 0 {
			t.Fatalf("expected command to proceed past const variable resolution")
		}

		output := ui.ErrorWriter.String()
		if !strings.Contains(output, "Error: Module not installed") {
			t.Fatalf("expected module installation diagnostic, got: %s", output)
		}
	})

	t.Run("value via backend", func(t *testing.T) {
		server := cloudTestServerWithVars(t)
		defer server.Close()
		d := testDisco(server)

		previousBackend := backendInit.Backend("cloud")
		backendInit.Set("cloud", func() backend.Backend { return backendCloud.New(d) })
		defer backendInit.Set("cloud", previousBackend)

		wd := tempWorkingDirFixture(t, "dynamic-module-sources/get-const-var-backend")
		t.Chdir(wd.RootModuleDir())

		ui := cli.NewMockUi()
		c := &ProvidersLockCommand{
			Meta: Meta{
				testingOverrides: metaOverridesForProvider(testProvider()),
				Ui:               ui,
				WorkingDir:       wd,
				Services:         d,
			},
		}

		args := []string{"-fs-mirror=fs-mirror"}
		if code := c.Run(args); code == 0 {
			t.Fatalf("expected command to proceed past const variable resolution")
		}

		output := ui.ErrorWriter.String()
		if !strings.Contains(output, "Error: Module not installed") {
			t.Fatalf("expected module installation diagnostic, got: %s", output)
		}
	})
}

func TestProvidersLock_args(t *testing.T) {

	t.Run("mirror collision", func(t *testing.T) {
		ui := new(cli.MockUi)
		c := &ProvidersLockCommand{
			Meta: Meta{
				Ui: ui,
			},
		}

		// only one of these arguments can be used at a time
		args := []string{
			"-fs-mirror=/foo/",
			"-net-mirror=www.foo.com",
		}
		code := c.Run(args)

		if code != 1 {
			t.Fatalf("wrong exit code; expected 1, got %d", code)
		}
		output := ui.ErrorWriter.String()
		if !strings.Contains(output, "The -fs-mirror and -net-mirror command line options are mutually-exclusive.") {
			t.Fatalf("missing expected error message: %s", output)
		}
	})

	t.Run("invalid platform", func(t *testing.T) {
		ui := new(cli.MockUi)
		c := &ProvidersLockCommand{
			Meta: Meta{
				Ui: ui,
			},
		}

		// not a valid platform
		args := []string{"-platform=arbitrary_nonsense_that_isnt_valid"}
		code := c.Run(args)

		if code != 1 {
			t.Fatalf("wrong exit code; expected 1, got %d", code)
		}
		output := ui.ErrorWriter.String()
		if !strings.Contains(output, "must be two words separated by an underscore.") {
			t.Fatalf("missing expected error message: %s", output)
		}
	})

	t.Run("invalid provider argument", func(t *testing.T) {
		ui := new(cli.MockUi)
		c := &ProvidersLockCommand{
			Meta: Meta{
				Ui: ui,
			},
		}

		// There is no configuration, so it's not valid to use any provider argument
		args := []string{"hashicorp/random"}
		code := c.Run(args)

		if code != 1 {
			t.Fatalf("wrong exit code; expected 1, got %d", code)
		}
		output := ui.ErrorWriter.String()
		if !strings.Contains(output, "The provider registry.terraform.io/hashicorp/random is not required by the\ncurrent configuration.") {
			t.Fatalf("missing expected error message: %s", output)
		}
	})
}

func TestProvidersLockCalculateChangeType(t *testing.T) {
	provider := addrs.NewDefaultProvider("provider")
	v2 := getproviders.MustParseVersion("2.0.0")
	v2EqConstraints := getproviders.MustParseVersionConstraints("2.0.0")

	t.Run("oldLock == nil", func(t *testing.T) {
		platformLock := depsfile.NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
			"9r3i9a9QmASqMnQM",
			"K43RHM2klOoywtyW",
			"swJPXfuCNhJsTM5c",
		})

		if ct := providersLockCalculateChangeType(nil, platformLock); ct != providersLockChangeTypeNewProvider {
			t.Fatalf("output was %s but should be %s", ct, providersLockChangeTypeNewProvider)
		}
	})

	t.Run("oldLock == platformLock", func(t *testing.T) {
		platformLock := depsfile.NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
			"9r3i9a9QmASqMnQM",
			"K43RHM2klOoywtyW",
			"swJPXfuCNhJsTM5c",
		})

		oldLock := depsfile.NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
			"9r3i9a9QmASqMnQM",
			"K43RHM2klOoywtyW",
			"swJPXfuCNhJsTM5c",
		})

		if ct := providersLockCalculateChangeType(oldLock, platformLock); ct != providersLockChangeTypeNoChange {
			t.Fatalf("output was %s but should be %s", ct, providersLockChangeTypeNoChange)
		}
	})

	t.Run("oldLock > platformLock", func(t *testing.T) {
		platformLock := depsfile.NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
			"9r3i9a9QmASqMnQM",
			"K43RHM2klOoywtyW",
			"swJPXfuCNhJsTM5c",
		})

		oldLock := depsfile.NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
			"9r3i9a9QmASqMnQM",
			"1ZAChGWUMWn4zmIk",
			"K43RHM2klOoywtyW",
			"HWjRvIuWZ1LVatnc",
			"swJPXfuCNhJsTM5c",
			"KwhJK4p/U2dqbKhI",
		})

		if ct := providersLockCalculateChangeType(oldLock, platformLock); ct != providersLockChangeTypeNoChange {
			t.Fatalf("output was %s but should be %s", ct, providersLockChangeTypeNoChange)
		}
	})

	t.Run("oldLock < platformLock", func(t *testing.T) {
		platformLock := depsfile.NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
			"9r3i9a9QmASqMnQM",
			"1ZAChGWUMWn4zmIk",
			"K43RHM2klOoywtyW",
			"HWjRvIuWZ1LVatnc",
			"swJPXfuCNhJsTM5c",
			"KwhJK4p/U2dqbKhI",
		})

		oldLock := depsfile.NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
			"9r3i9a9QmASqMnQM",
			"K43RHM2klOoywtyW",
			"swJPXfuCNhJsTM5c",
		})

		if ct := providersLockCalculateChangeType(oldLock, platformLock); ct != providersLockChangeTypeNewHashes {
			t.Fatalf("output was %s but should be %s", ct, providersLockChangeTypeNoChange)
		}
	})
}
