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

package command

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

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/command/clistate"
	"github.com/hashicorp/terraform/internal/command/workdir"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/getproviders"
	"github.com/hashicorp/terraform/internal/providers"
	testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
	"github.com/hashicorp/terraform/internal/states/statefile"
)

func TestStateMigrate_fromBackendToBackend(t *testing.T) {
	fixture := "state-migrate-backend-to-backend"
	wd := tempWorkingDirFixture(t, fixture)
	t.Chdir(wd.RootModuleDir())

	ui := testUiWrapped(t)
	view, done := testView(t)
	c := &StateMigrateCommand{
		Meta: Meta{
			Ui:                        ui,
			View:                      view,
			WorkingDir:                wd,
			AllowExperimentalFeatures: true,
		},
	}

	_ = testInputMap(t, map[string]string{
		"backend-migrate-copy-to-empty": "yes",
	})

	args := []string{"-no-color"}
	code := c.Run(args)
	out := done(t)
	if code != 0 {
		t.Fatalf("expected exit code 1, got %d\nstderr: %q", code, out.Stderr())
	}

	// Assert expected human output is made
	checkGoldenReferenceHumanOutput(t, out, fixture)

	// Assert the migrated state contains expected content
	f, err := os.Open("destination-backend.tfstate")
	if err != nil {
		t.Fatalf("failed to read migrated state: %s", err)
	}
	t.Cleanup(func() {
		err := f.Close()
		if err != nil {
			t.Fatal(err)
		}
	})
	s, err := statefile.Read(f)
	if err != nil {
		t.Fatal(err)
	}
	_, ok := s.State.RootOutputValues["test"]
	if !ok {
		t.Fatalf("unable to find test output in migrated state")
	}

	// Assert the backend state file describes the new backend location,
	statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
	sMgr := &clistate.LocalState{Path: statePath}
	if err := sMgr.RefreshState(); err != nil {
		t.Fatal(err)
	}
	backendState := sMgr.State()
	if backendState.StateStore != nil {
		t.Fatalf("expected backend state file to not describe a state_store during backend=>backend, but it's set")
	}
	if backendState.Backend == nil {
		t.Fatalf("expected backend state file to describe a backend during backend=>backend, but it's nil")
	}
	if backendState.Backend.Type != "local" {
		t.Fatalf("expected backend state file to describe the destination backend \"local\", but got %q", backendState.Backend.Type)
	}
	if got, want := normalizeJSON(t, backendState.Backend.ConfigRaw), `{"path":"destination-backend.tfstate","workspace_dir":null}`; got != want {
		t.Errorf("wrong config\ngot:  %s\nwant: %s", got, want)
	}
}

func TestStateMigrate_fromBackendToStateStore(t *testing.T) {
	fixture := "state-migrate-backend-to-state-store"
	wd := tempWorkingDirFixture(t, fixture)
	t.Chdir(wd.RootModuleDir())

	p := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store"))
	p.MockStates = testing_provider.NewMockStateBytesWithStateIds("test_store", []string{"default"})
	providerSource := newMockProviderSource(t, map[string][]string{
		"hashicorp/test": {"1.2.3"},
	})

	ui := testUiWrapped(t)
	view, done := testView(t)
	c := &StateMigrateCommand{
		Meta: Meta{
			Ui:                        ui,
			View:                      view,
			WorkingDir:                wd,
			AllowExperimentalFeatures: true,
			testingOverrides:          metaOverridesForProvider(p),
			ProviderSource:            providerSource,
		},
	}

	_ = testInputMap(t, map[string]string{
		"backend-migrate-copy-to-empty": "yes",
	})

	args := []string{"-no-color"}
	code := c.Run(args)
	out := done(t)
	if code != 0 {
		t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
	}

	// Assert expected human output is made
	checkGoldenReferenceHumanOutput(t, out, fixture)

	// Assert the migrated state contains expected content
	b, err := p.MockStates.Read("test_store", "default")
	if err != nil {
		t.Fatalf("unable to find migrated state in mock provider: %s", err)
	}
	s, err := statefile.Read(bytes.NewBuffer(b))
	if err != nil {
		t.Fatal(err)
	}
	_, ok := s.State.RootOutputValues["test"]
	if !ok {
		t.Fatalf("unable to find test output in migrated state")
	}

	// Assert the backend state file describes the new state store location.
	statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
	sMgr := &clistate.LocalState{Path: statePath}
	if err := sMgr.RefreshState(); err != nil {
		t.Fatal(err)
	}
	backendState := sMgr.State()
	if backendState.Backend != nil {
		t.Fatalf("expected backend state file to not describe a backend during backend=>state_store migration, but it's set")
	}
	if backendState.StateStore == nil {
		t.Fatalf("expected backend state file to describe a state store during backend=>state_store migration, but it's nil")
	}
	if backendState.StateStore.Provider.Source.Type != "test" {
		t.Fatalf("expected backend state file to describe the destination state store provider \"test\", but got %q", backendState.StateStore.Provider)
	}
	if backendState.StateStore.Provider.Version.String() != "1.2.3" {
		t.Fatalf("expected backend state file to describe the destination state store provider version \"1.2.3\", but got %q", backendState.StateStore.Provider.Version)
	}
	if backendState.StateStore.Type != "test_store" {
		t.Fatalf("expected backend state file to describe the destination state store type \"test_store\", but got %q", backendState.StateStore.Type)
	}
}

// Testing migration between two state stores in a single provider.
// Different cases describe whether the source provider is already in the dependency lock file or not.
func TestStateMigrate_fromStateStoreToStateStore_inSingleProvider(t *testing.T) {
	// All test cases perform a migration from test_src to test_dst store implementations in v1.2.3 of hashicorp/test provider.
	// So a common assertion func can be re-used.
	assertBackendStateFile := func(t *testing.T, c *StateMigrateCommand) {
		statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
		sMgr := &clistate.LocalState{Path: statePath}
		if err := sMgr.RefreshState(); err != nil {
			t.Fatal(err)
		}

		backendState := sMgr.State()
		if backendState.Backend != nil {
			t.Fatalf("expected backend state file to not describe a backend during state_store=>state_store migration, but it's set")
		}
		if backendState.StateStore == nil {
			t.Fatalf("expected backend state file to describe a state store during state_store=>state_store migration, but it's nil")
		}
		if backendState.StateStore.Provider.Source.Type != "test" {
			t.Fatalf("expected backend state file to describe the destination state store provider \"test\", but got %q", backendState.StateStore.Provider)
		}
		if backendState.StateStore.Provider.Version.String() != "1.2.3" {
			t.Fatalf("expected backend state file to describe the destination state store provider version \"1.2.3\", but got %q", backendState.StateStore.Provider.Version)
		}
		if backendState.StateStore.Type != "test_dst" {
			t.Fatalf("expected backend state file to describe the destination state store type \"test_dst\", but got %q", backendState.StateStore.Type)
		}
		// The state store schema is empty, so not even any null fields in expected JSON
		if got, want := normalizeJSON(t, backendState.StateStore.ConfigRaw), `{}`; got != want {
			t.Errorf("wrong config\ngot:  %s\nwant: %s", got, want)
		}
		// The provider schema includes a 'region' attr, but it's unset in config; null.
		if got, want := normalizeJSON(t, backendState.StateStore.Provider.ConfigRaw), `{"region":null}`; got != want {
			t.Errorf("wrong config\ngot:  %s\nwant: %s", got, want)
		}
	}

	t.Run("provider is already in the dependency lock file", func(t *testing.T) {
		wd := tempWorkingDirFixture(t, "state-migrate-state-store-to-state-store")
		t.Chdir(wd.RootModuleDir())

		b, err := os.ReadFile("source-pss.tfstate")
		if err != nil {
			t.Fatal(err)
		}

		pssSchemas := map[string]providers.Schema{
			"test_src": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
			"test_dst": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		p := mockPluggableStateStorageProvider(pssSchemas)
		p.MockStates = testing_provider.MockStateBytes{
			"test_src": map[string][]byte{"default": []byte(b)},
			"test_dst": map[string][]byte{},
		}
		providerSource := newMockProviderSource(t, map[string][]string{
			"hashicorp/test": {"1.2.3"},
		})

		ui := testUiWrapped(t)
		view, done := testView(t)
		c := &StateMigrateCommand{
			Meta: Meta{
				Ui:                        ui,
				View:                      view,
				WorkingDir:                wd,
				AllowExperimentalFeatures: true,
				testingOverrides:          metaOverridesForProvider(p),
				ProviderSource:            providerSource,
			},
		}

		_ = testInputMap(t, map[string]string{
			"backend-migrate-copy-to-empty": "yes",
		})

		args := []string{"-no-color"}
		code := c.Run(args)
		out := done(t)
		if code != 0 {
			t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
		}

		expectedMsg := []string{
			`Migrating state from state store "test_src" (hashicorp/test) to state store "test_dst" (hashicorp/test)...`,
			"Initializing provider plugin for state store \"test_src\"...\n- Reusing previous version of hashicorp/test from the dependency lock file",
			"Initializing provider plugin for state store \"test_dst\"...\n- Reusing previous version of hashicorp/test from the dependency lock file",
		}
		for _, expectedMsg := range expectedMsg {
			if !strings.Contains(out.Stdout(), expectedMsg) {
				t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
			}
		}

		b, err = p.MockStates.Read("test_dst", "default")
		if err != nil {
			t.Fatalf("unable to find migrated state in mock provider: %s", err)
		}
		s, err := statefile.Read(bytes.NewBuffer(b))
		if err != nil {
			t.Fatal(err)
		}
		_, ok := s.State.RootOutputValues["test"]
		if !ok {
			t.Fatalf("unable to find test output in migrated state")
		}

		// Assert the backend state file describes the new state store location.
		assertBackendStateFile(t, c)
	})

	t.Run("no existing dependency lock file: provider is downloaded and added to the dependency lock file", func(t *testing.T) {
		wd := tempWorkingDirFixture(t, "state-migrate-state-store-to-state-store")
		t.Chdir(wd.RootModuleDir())

		// In this scenario, there is no provider in the dep lock file
		// Achieve this by truncating the dep lock file from the copied fixtures
		if err := os.Truncate(filepath.Join(wd.RootModuleDir(), dependencyLockFilename), 0); err != nil {
			t.Fatalf("error while truncating lock file during test setup: %s", err)
		}

		b, err := os.ReadFile("source-pss.tfstate")
		if err != nil {
			t.Fatal(err)
		}

		pssSchemas := map[string]providers.Schema{
			"test_src": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
			"test_dst": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		p := mockPluggableStateStorageProvider(pssSchemas)
		p.MockStates = testing_provider.MockStateBytes{
			"test_src": map[string][]byte{"default": []byte(b)},
			"test_dst": map[string][]byte{},
		}
		providerSource := newMockProviderSource(t, map[string][]string{
			"hashicorp/test": {"1.2.3"},
		})

		ui := testUiWrapped(t)
		view, done := testView(t)
		c := &StateMigrateCommand{
			Meta: Meta{
				Ui:                        ui,
				View:                      view,
				WorkingDir:                wd,
				AllowExperimentalFeatures: true,
				testingOverrides:          metaOverridesForProvider(p),
				ProviderSource:            providerSource,
			},
		}

		_ = testInputMap(t, map[string]string{
			"backend-migrate-copy-to-empty": "yes",
		})

		args := []string{"-no-color"}
		code := c.Run(args)
		out := done(t)
		if code != 0 {
			t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
		}

		expectedMsg := []string{
			`Migrating state from state store "test_src" (hashicorp/test) to state store "test_dst" (hashicorp/test)...`,
			"Initializing provider plugin for state store \"test_src\"...\n- Finding hashicorp/test versions matching \"1.2.3\"...\n- Installing hashicorp/test v1.2.3...\n- Installed hashicorp/test v1.2.3 (verified checksum)",
			"Initializing provider plugin for state store \"test_dst\"...\n- Reusing previous version of hashicorp/test from the dependency lock file\n- Using previously-installed hashicorp/test v1.2.3",
		}
		for _, expectedMsg := range expectedMsg {
			if !strings.Contains(out.Stdout(), expectedMsg) {
				t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
			}
		}

		// Assert the state is migrated successfully to the
		// destination state store by inspecting the mock.
		b, err = p.MockStates.Read("test_dst", "default")
		if err != nil {
			t.Fatalf("unable to find migrated state in mock provider: %s", err)
		}
		s, err := statefile.Read(bytes.NewBuffer(b))
		if err != nil {
			t.Fatal(err)
		}
		_, ok := s.State.RootOutputValues["test"]
		if !ok {
			t.Fatalf("unable to find test output in migrated state")
		}

		// Assert the provider is added to the dependency lock file
		lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
		lockFileBytes, err := os.ReadFile(lockFilePath)
		if err != nil {
			t.Fatalf("unable to read dependency lock file: %s", err)
		}
		if !strings.Contains(string(lockFileBytes), "hashicorp/test") {
			t.Fatalf("expected provider hashicorp/test to be added to the dependency lock file, got: %s", string(lockFileBytes))
		}

		// Assert the backend state file describes the new state store location.
		assertBackendStateFile(t, c)
	})
}

// Test migration between two state stores in different providers.
// Different cases describe whether the source provider is already in the dependency lock file or not.
func TestStateMigrate_fromStateStoreToStateStore_inDifferentProviders(t *testing.T) {
	// All test cases perform a migration to hashicorp/test2, version v3.2.1, test2_store store.
	// So a common assertion func can be re-used.
	assertBackendStateFile := func(t *testing.T, c *StateMigrateCommand) {
		statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
		sMgr := &clistate.LocalState{Path: statePath}
		if err := sMgr.RefreshState(); err != nil {
			t.Fatal(err)
		}

		backendState := sMgr.State()
		if backendState.Backend != nil {
			t.Fatalf("expected backend state file to not describe a backend during state_store=>state_store migration, but it's set")
		}
		if backendState.StateStore == nil {
			t.Fatalf("expected backend state file to describe a state store during state_store=>state_store migration, but it's nil")
		}
		if backendState.StateStore.Provider.Source.Type != "test2" {
			t.Fatalf("expected backend state file to describe the destination state store provider \"test2\", but got %q", backendState.StateStore.Provider)
		}
		if backendState.StateStore.Provider.Version.String() != "3.2.1" {
			t.Fatalf("expected backend state file to describe the destination state store provider version \"3.2.1\", but got %q", backendState.StateStore.Provider.Version)
		}
		if backendState.StateStore.Type != "test2_store" {
			t.Fatalf("expected backend state file to describe the destination state store type \"test2_store\", but got %q", backendState.StateStore.Type)
		}
		// The state store schema is empty, so not even any null fields in expected JSON
		if got, want := normalizeJSON(t, backendState.StateStore.ConfigRaw), `{}`; got != want {
			t.Errorf("wrong config\ngot:  %s\nwant: %s", got, want)
		}
		// The provider schema includes a 'region' attr, but it's unset in config; null.
		if got, want := normalizeJSON(t, backendState.StateStore.Provider.ConfigRaw), `{"region":"foobar"}`; got != want {
			t.Errorf("wrong config\ngot:  %s\nwant: %s", got, want)
		}
	}
	t.Run("source provider already in the dependency lock file, destination is not", func(t *testing.T) {
		fixture := "state-store-changed/provider-used"
		wd := tempWorkingDirFixture(t, fixture)
		t.Chdir(wd.RootModuleDir())

		b, err := os.ReadFile("source-pss.tfstate")
		if err != nil {
			t.Fatal(err)
		}

		// hashicorp/test
		sourcePssSchema := map[string]providers.Schema{
			"test_src": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema)
		sourceProvider.MockStates = testing_provider.MockStateBytes{
			"test_src": map[string][]byte{"default": []byte(b)},
		}
		// hashicorp/test2
		destinationPssSchema := map[string]providers.Schema{
			"test2_store": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema)
		destinationProvider.MockStates = testing_provider.MockStateBytes{
			"test2_store": map[string][]byte{}, // No existing state in the destination
		}
		providerSource := newMockProviderSource(t, map[string][]string{
			"hashicorp/test":  {"1.2.3"},
			"hashicorp/test2": {"3.2.1"},
		})

		ui := testUiWrapped(t)
		view, done := testView(t)
		c := &StateMigrateCommand{
			Meta: Meta{
				Ui:                        ui,
				View:                      view,
				WorkingDir:                wd,
				AllowExperimentalFeatures: true,
				testingOverrides: &testingOverrides{
					Providers: map[addrs.Provider]providers.Factory{
						addrs.NewDefaultProvider("test"):  providers.FactoryFixed(sourceProvider),
						addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider),
					},
				},
				ProviderSource: providerSource,
			},
		}

		_ = testInputMap(t, map[string]string{
			"backend-migrate-copy-to-empty": "yes",
		})

		args := []string{"-no-color"}
		code := c.Run(args)
		out := done(t)
		if code != 0 {
			t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
		}

		// Assert expected human output is made
		// Parameterized due to output referencing the current platform.
		checkParameterizedGoldenReferenceHumanOutput(t, out, fixture, getproviders.CurrentPlatform.String())

		// Assert the state is migrated successfully to the destination state store by inspecting the mock.
		b, err = destinationProvider.MockStates.Read("test2_store", "default")
		if err != nil {
			t.Fatalf("unable to find migrated state in mock provider: %s", err)
		}
		s, err := statefile.Read(bytes.NewBuffer(b))
		if err != nil {
			t.Fatal(err)
		}
		_, ok := s.State.RootOutputValues["test"]
		if !ok {
			t.Fatalf("unable to find test output in migrated state")
		}

		// Assert the destination provider is added to the dependency lock file
		// Also, no providers have been removed from the file.
		lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
		lockFileBytes, err := os.ReadFile(lockFilePath)
		if err != nil {
			t.Fatalf("unable to read dependency lock file: %s", err)
		}
		expectedContents := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/test" {
  version = "1.2.3"
}

provider "registry.terraform.io/hashicorp/test2" {
  version = "3.2.1"
  hashes = [
    "h1:gv1gFnIZulslzchnaoyMJ5KoPvoRgVvSGb3tVS803iw=",
  ]
}
`
		if diff := cmp.Diff(expectedContents, string(lockFileBytes)); diff != "" {
			t.Fatalf("unexpected dependency lock file contents, diff:\n%s", diff)
		}

		// Assert the backend state file describes the new state store location.
		assertBackendStateFile(t, c)
	})
	t.Run("destination provider already in the dependency lock file, source is not", func(t *testing.T) {
		wd := tempWorkingDirFixture(t, "state-store-changed/provider-used")
		t.Chdir(wd.RootModuleDir())

		// Replace dep lock file in fixtures so that the destination provider is already in the dep lock file, but the source provider is not.
		lockFileContents := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/test2" {
  version = "3.2.1"
  hashes = [
    "h1:gv1gFnIZulslzchnaoyMJ5KoPvoRgVvSGb3tVS803iw=",
  ]
}
`
		if err := os.WriteFile(filepath.Join(wd.RootModuleDir(), dependencyLockFilename), []byte(lockFileContents), 0644); err != nil {
			t.Fatalf("unable to overwrite dependency lock file as part of test setup: %s", err)
		}

		b, err := os.ReadFile("source-pss.tfstate")
		if err != nil {
			t.Fatal(err)
		}

		// hashicorp/test
		sourcePssSchema := map[string]providers.Schema{
			"test_src": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema)
		sourceProvider.MockStates = testing_provider.MockStateBytes{
			"test_src": map[string][]byte{"default": []byte(b)},
		}
		// hashicorp/test2
		destinationPssSchema := map[string]providers.Schema{
			"test2_store": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema)
		destinationProvider.MockStates = testing_provider.MockStateBytes{
			"test2_store": map[string][]byte{}, // No existing state in the destination
		}
		providerSource := newMockProviderSource(t, map[string][]string{
			"hashicorp/test":  {"1.2.3"},
			"hashicorp/test2": {"3.2.1"},
		})

		ui := testUiWrapped(t)
		view, done := testView(t)
		c := &StateMigrateCommand{
			Meta: Meta{
				Ui:                        ui,
				View:                      view,
				WorkingDir:                wd,
				AllowExperimentalFeatures: true,
				testingOverrides: &testingOverrides{
					Providers: map[addrs.Provider]providers.Factory{
						addrs.NewDefaultProvider("test"):  providers.FactoryFixed(sourceProvider),
						addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider),
					},
				},
				ProviderSource: providerSource,
			},
		}

		_ = testInputMap(t, map[string]string{
			"backend-migrate-copy-to-empty": "yes",
		})

		args := []string{"-no-color"}
		code := c.Run(args)
		out := done(t)
		if code != 0 {
			t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
		}

		expectedMsg := []string{
			"Initializing provider plugin for state store \"test_src\"...\n- Finding hashicorp/test versions matching \"1.2.3\"...\n- Installing hashicorp/test v1.2.3...\n- Installed hashicorp/test v1.2.3 (verified checksum)",
			"Initializing provider plugin for state store \"test2_store\"...\n- Reusing previous version of hashicorp/test2 from the dependency lock file",
			`Migrating state from state store "test_src" (hashicorp/test) to state store "test2_store" (hashicorp/test2)...`,
		}
		for _, expectedMsg := range expectedMsg {
			if !strings.Contains(out.Stdout(), expectedMsg) {
				t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
			}
		}

		// Assert the state is migrated successfully to the destination state store by inspecting the mock.
		b, err = destinationProvider.MockStates.Read("test2_store", "default")
		if err != nil {
			t.Fatalf("unable to find migrated state in mock provider: %s", err)
		}
		s, err := statefile.Read(bytes.NewBuffer(b))
		if err != nil {
			t.Fatal(err)
		}
		_, ok := s.State.RootOutputValues["test"]
		if !ok {
			t.Fatalf("unable to find test output in migrated state")
		}

		// Assert the dependency lock file is unchanged, as it was already in the lock file.
		lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
		lockFileBytes, err := os.ReadFile(lockFilePath)
		if err != nil {
			t.Fatalf("unable to read dependency lock file: %s", err)
		}

		if diff := cmp.Diff(lockFileContents, string(lockFileBytes)); diff != "" {
			t.Fatalf("unexpected dependency lock file contents, diff:\n%s", diff)
		}

		// Assert the backend state file describes the new state store location.
		assertBackendStateFile(t, c)
	})
	t.Run("no existing dependency lock file: only destination provider saved to the dependency lock file", func(t *testing.T) {
		wd := tempWorkingDirFixture(t, "state-store-changed/provider-used")
		t.Chdir(wd.RootModuleDir())

		// Remove lock file, so source provider isn't in the dep lock file for this test scenario
		if err := os.Remove(filepath.Join(wd.RootModuleDir(), dependencyLockFilename)); err != nil {
			t.Fatalf("error while deleting lock file during test setup: %s", err)
		}

		b, err := os.ReadFile("source-pss.tfstate")
		if err != nil {
			t.Fatal(err)
		}

		// hashicorp/test
		sourcePssSchema := map[string]providers.Schema{
			"test_src": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema)
		sourceProvider.MockStates = testing_provider.MockStateBytes{
			"test_src": map[string][]byte{"default": []byte(b)},
		}
		// hashicorp/test2
		destinationPssSchema := map[string]providers.Schema{
			"test2_store": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema)
		destinationProvider.MockStates = testing_provider.MockStateBytes{
			"test2_store": map[string][]byte{}, // No existing state in the destination
		}
		providerSource := newMockProviderSource(t, map[string][]string{
			"hashicorp/test":  {"1.2.3"},
			"hashicorp/test2": {"3.2.1"},
		})

		ui := testUiWrapped(t)
		view, done := testView(t)
		c := &StateMigrateCommand{
			Meta: Meta{
				Ui:                        ui,
				View:                      view,
				WorkingDir:                wd,
				AllowExperimentalFeatures: true,
				testingOverrides: &testingOverrides{
					Providers: map[addrs.Provider]providers.Factory{
						addrs.NewDefaultProvider("test"):  providers.FactoryFixed(sourceProvider),
						addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider),
					},
				},
				ProviderSource: providerSource,
			},
		}

		_ = testInputMap(t, map[string]string{
			"backend-migrate-copy-to-empty": "yes",
		})

		args := []string{"-no-color"}
		code := c.Run(args)
		out := done(t)
		if code != 0 {
			t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
		}

		expectedMsg := []string{
			"Initializing provider plugin for state store \"test_src\"...\n- Finding hashicorp/test versions matching \"1.2.3\"...\n- Installing hashicorp/test v1.2.3...\n- Installed hashicorp/test v1.2.3 (verified checksum)",
			"Initializing provider plugin for state store \"test2_store\"...\n- Finding latest version of hashicorp/test2...\n- Installing hashicorp/test2 v3.2.1...\n- Installed hashicorp/test2 v3.2.1 (verified checksum)",
			`Migrating state from state store "test_src" (hashicorp/test) to state store "test2_store" (hashicorp/test2)...`,
		}
		for _, expectedMsg := range expectedMsg {
			if !strings.Contains(out.Stdout(), expectedMsg) {
				t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
			}
		}

		// Assert the state is migrated successfully to the destination state store by inspecting the mock.
		b, err = destinationProvider.MockStates.Read("test2_store", "default")
		if err != nil {
			t.Fatalf("unable to find migrated state in mock provider: %s", err)
		}
		s, err := statefile.Read(bytes.NewBuffer(b))
		if err != nil {
			t.Fatal(err)
		}
		_, ok := s.State.RootOutputValues["test"]
		if !ok {
			t.Fatalf("unable to find test output in migrated state")
		}

		// Assert the provider is added to the dependency lock file
		lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
		lockFileBytes, err := os.ReadFile(lockFilePath)
		if err != nil {
			t.Fatalf("unable to read dependency lock file: %s", err)
		}

		// The source provider is not added here, as it's not used post-migration.
		expectedContents := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/test2" {
  version = "3.2.1"
  hashes = [
    "h1:gv1gFnIZulslzchnaoyMJ5KoPvoRgVvSGb3tVS803iw=",
  ]
}
`
		if diff := cmp.Diff(expectedContents, string(lockFileBytes)); diff != "" {
			t.Fatalf("unexpected dependency lock file contents, diff:\n%s", diff)
		}

		// Assert the backend state file describes the new state store location.
		assertBackendStateFile(t, c)
	})
}

func TestStateMigrate_stateStore_newWorkingDir_inAutomationProviderApproval(t *testing.T) {
	t.Run("both providers already in lock file, user not prompted for provider approval", func(t *testing.T) {
		wd := tempWorkingDirFixture(t, "state-store-changed/provider-used")
		t.Chdir(wd.RootModuleDir())

		// Replace dep lock file in fixtures so that both providers are already in the dep lock file.
		lockFileContents := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/test" {
  version = "1.2.3"
}

provider "registry.terraform.io/hashicorp/test2" {
  version = "3.2.1"
  hashes = [
    "h1:gv1gFnIZulslzchnaoyMJ5KoPvoRgVvSGb3tVS803iw=",
  ]
}`
		if err := os.WriteFile(filepath.Join(wd.RootModuleDir(), dependencyLockFilename), []byte(lockFileContents), 0644); err != nil {
			t.Fatalf("unable to overwrite dependency lock file as part of test setup: %s", err)
		}

		b, err := os.ReadFile("source-pss.tfstate")
		if err != nil {
			t.Fatal(err)
		}

		// hashicorp/test - source
		sourcePssSchema := map[string]providers.Schema{
			"test_src": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		sourceProvider := mockPluggableStateStorageProvider(sourcePssSchema)
		sourceProvider.MockStates = testing_provider.MockStateBytes{
			"test_src": map[string][]byte{"default": []byte(b)},
		}
		// hashicorp/test2 - destination
		destinationPssSchema := map[string]providers.Schema{
			"test2_store": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{},
				},
			},
		}
		destinationProvider := mockPluggableStateStorageProvider(destinationPssSchema)
		destinationProvider.MockStates = testing_provider.MockStateBytes{
			"test2_store": map[string][]byte{}, // No existing state in the destination
		}

		// Supplying providers via HTTP triggers security features.
		providerSource := newMockProviderSourceUsingTestHttpServer(t, map[string][]string{
			"hashicorp/test":  {"1.2.3"},
			"hashicorp/test2": {"3.2.1"},
		})

		ui := testUiWrapped(t)
		view, done := testView(t)
		c := &StateMigrateCommand{
			Meta: Meta{
				Ui:                        ui,
				View:                      view,
				WorkingDir:                wd,
				AllowExperimentalFeatures: true,
				testingOverrides: &testingOverrides{
					Providers: map[addrs.Provider]providers.Factory{
						addrs.NewDefaultProvider("test"):  providers.FactoryFixed(sourceProvider),
						addrs.NewDefaultProvider("test2"): providers.FactoryFixed(destinationProvider),
					},
				},
				ProviderSource: providerSource,
			},
		}

		args := []string{
			"-input=false", // Simulate running in automation where input is disabled
			"-force-copy",  // Suppress prompts to perform the migration
			"-no-color",
		}
		code := c.Run(args)
		out := done(t)
		if code != 0 {
			t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
		}

		expectedMsg := []string{
			"Initializing provider plugin for state store \"test_src\"...\n- Reusing previous version of hashicorp/test from the dependency lock file",
			"Initializing provider plugin for state store \"test2_store\"...\n- Reusing previous version of hashicorp/test2 from the dependency lock file",
			`Migrating state from state store "test_src" (hashicorp/test) to state store "test2_store" (hashicorp/test2)...`,
		}
		for _, expectedMsg := range expectedMsg {
			if !strings.Contains(out.Stdout(), expectedMsg) {
				t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
			}
		}
		notExpectedMsg := []string{
			"The state store provider was approved automatically", // shouldn't be there as no explicit CLI flag used.
		}
		for _, notExpected := range notExpectedMsg {
			if strings.Contains(out.Stdout(), notExpected) {
				t.Fatalf("did not expect output %q, but got %q", notExpected, out.Stdout())
			}
		}

		// Assert the state is migrated successfully to the destination state store by inspecting the mock.
		b, err = destinationProvider.MockStates.Read("test2_store", "default")
		if err != nil {
			t.Fatalf("unable to find migrated state in mock provider: %s", err)
		}
		s, err := statefile.Read(bytes.NewBuffer(b))
		if err != nil {
			t.Fatal(err)
		}
		_, ok := s.State.RootOutputValues["test"]
		if !ok {
			t.Fatalf("unable to find test output in migrated state")
		}

		// Assert the dependency lock file is unchanged, as both providers were already in the lock file.
		lockFilePath := filepath.Join(wd.RootModuleDir(), dependencyLockFilename)
		lockFileBytes, err := os.ReadFile(lockFilePath)
		if err != nil {
			t.Fatalf("unable to read dependency lock file: %s", err)
		}

		if diff := cmp.Diff(lockFileContents, string(lockFileBytes)); diff != "" {
			t.Fatalf("unexpected dependency lock file contents, diff:\n%s", diff)
		}
	})
}

func TestStateMigrate_fromStateStoreToBackend(t *testing.T) {
	wd := tempWorkingDirFixture(t, "state-migrate-state-store-to-backend")
	t.Chdir(wd.RootModuleDir())

	p := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store"))
	b, err := os.ReadFile("source-pss.tfstate")
	if err != nil {
		t.Fatal(err)
	}
	p.MockStates = testing_provider.NewMockStateBytesWithSingleState(
		"test_store",
		"default",
		b,
	)

	providerSource := newMockProviderSource(t, map[string][]string{
		"hashicorp/test": {"1.2.3"},
	})

	ui := testUiWrapped(t)
	view, done := testView(t)
	c := &StateMigrateCommand{
		Meta: Meta{
			Ui:                        ui,
			View:                      view,
			WorkingDir:                wd,
			AllowExperimentalFeatures: true,
			testingOverrides:          metaOverridesForProvider(p),
			ProviderSource:            providerSource,
		},
	}

	_ = testInputMap(t, map[string]string{
		"backend-migrate-copy-to-empty": "yes",
	})

	args := []string{"-no-color"}
	code := c.Run(args)
	out := done(t)
	if code != 0 {
		t.Fatalf("unexpected exit: %d\nstderr: %q", code, out.Stderr())
	}

	expectedMsg := []string{
		"Initializing provider plugin for state store \"test_store\"...\n- Reusing previous version of hashicorp/test from the dependency lock file",
		`Migrating state from state store "test_store" (hashicorp/test) to backend "local"...`,
	}
	for _, expectedMsg := range expectedMsg {
		if !strings.Contains(out.Stdout(), expectedMsg) {
			t.Fatalf("expected output %q, got %q", expectedMsg, out.Stdout())
		}
	}

	f, err := os.Open("destination-backend.tfstate")
	if err != nil {
		t.Fatalf("failed to read migrated state: %s", err)
	}
	t.Cleanup(func() {
		err := f.Close()
		if err != nil {
			t.Fatal(err)
		}
	})
	s, err := statefile.Read(f)
	if err != nil {
		t.Fatal(err)
	}
	_, ok := s.State.RootOutputValues["test"]
	if !ok {
		t.Fatalf("unable to find test output in migrated state")
	}

	// Assert the backend state file describes the new backend location,
	statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
	sMgr := &clistate.LocalState{Path: statePath}
	if err := sMgr.RefreshState(); err != nil {
		t.Fatal(err)
	}
	backendState := sMgr.State()
	if backendState.StateStore != nil {
		t.Fatalf("expected backend state file to not describe a state_store during state_store=>backend migration, but it's set")
	}
	if backendState.Backend == nil {
		t.Fatalf("expected backend state file to describe a backend during state_store=>backend migration, but it's nil")
	}
	if backendState.Backend.Type != "local" {
		t.Fatalf("expected backend state file to describe the destination backend \"local\", but got %q", backendState.Backend.Type)
	}
	if got, want := normalizeJSON(t, backendState.Backend.ConfigRaw), `{"path":"destination-backend.tfstate","workspace_dir":null}`; got != want {
		t.Errorf("wrong config\ngot:  %s\nwant: %s", got, want)
	}
}

func TestStateMigrate_missingModuleFiles(t *testing.T) {
	wd := tempWorkingDirFixture(t, "state-migrate-missing-mod-files")
	t.Chdir(wd.RootModuleDir())

	providerSource := newMockProviderSource(t, map[string][]string{
		"hashicorp/test": {"1.2.3"},
	})

	ui := testUiWrapped(t)
	view, done := testView(t)
	c := &StateMigrateCommand{
		Meta: Meta{
			Ui:                        ui,
			View:                      view,
			WorkingDir:                wd,
			AllowExperimentalFeatures: true,
			ProviderSource:            providerSource,
		},
	}

	args := []string{
		"-input=false",
		"-no-color",
	}
	code := c.Run(args)
	if code != 1 {
		t.Fatalf("expected exit code 1, got %d", code)
	}
	out := done(t)

	expectedMsg := "Error: Unknown migration destination"
	if !strings.Contains(out.Stderr(), expectedMsg) {
		t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
	}
}

func TestStateMigrate_emptyModuleFiles(t *testing.T) {
	wd := tempWorkingDirFixture(t, "state-migrate-empty-mod-files")
	t.Chdir(wd.RootModuleDir())

	providerSource := newMockProviderSource(t, map[string][]string{
		"hashicorp/test": {"1.2.3"},
	})

	ui := testUiWrapped(t)
	view, done := testView(t)
	c := &StateMigrateCommand{
		Meta: Meta{
			Ui:                        ui,
			View:                      view,
			WorkingDir:                wd,
			AllowExperimentalFeatures: true,
			ProviderSource:            providerSource,
		},
	}

	args := []string{
		"-input=false",
		"-no-color",
	}
	code := c.Run(args)
	if code != 1 {
		t.Fatalf("expected exit code 1, got %d", code)
	}
	out := done(t)

	expectedMsg := "Error: Unknown migration destination"
	if !strings.Contains(out.Stderr(), expectedMsg) {
		t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
	}
}

func TestStateMigrate_missingMigrationFiles(t *testing.T) {
	wd := tempWorkingDirFixture(t, "state-migrate-missing-migrate-files")
	t.Chdir(wd.RootModuleDir())

	ui := testUiWrapped(t)
	view, done := testView(t)
	c := &StateMigrateCommand{
		Meta: Meta{
			Ui:                        ui,
			View:                      view,
			WorkingDir:                wd,
			AllowExperimentalFeatures: true,
		},
	}

	args := []string{
		"-input=false",
		"-no-color",
	}
	code := c.Run(args)
	if code != 1 {
		t.Fatalf("expected exit code 1, got %d", code)
	}
	out := done(t)

	expectedMsg := "Error: No state migration instructions found"
	if !strings.Contains(out.Stderr(), expectedMsg) {
		t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
	}
}

func TestStateMigrate_nonExistentLockFiles(t *testing.T) {
	// use temporary directory to ensure the lock files certainly do not exist
	tmpDir := t.TempDir()
	t.Chdir(tmpDir)

	ui := testUiWrapped(t)
	view, done := testView(t)
	c := &StateMigrateCommand{
		Meta: Meta{
			Ui:                        ui,
			View:                      view,
			WorkingDir:                workdir.NewDir(tmpDir),
			AllowExperimentalFeatures: true,
		},
	}

	args := []string{
		"-input=false",
		"-source-provider-lock-file", filepath.Join(tmpDir, ".terraform.lock.hcl"),
		"-destination-provider-lock-file", filepath.Join(tmpDir, ".terraform.lock.hcl"),
	}
	code := c.Run(args)
	if code != 1 {
		t.Fatalf("expected exit code 1, got %d", code)
	}
	out := done(t)

	expectedMsg := "Unreadable source provider lock file"
	if !strings.Contains(out.Stderr(), expectedMsg) {
		t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
	}

	expectedMsg = "Unreadable destination provider lock file"
	if !strings.Contains(out.Stderr(), expectedMsg) {
		t.Fatalf("expected output %q, got %q", expectedMsg, out.All())
	}
}
