// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package command

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

	"github.com/opentofu/opentofu/internal/command/arguments"
	"github.com/opentofu/opentofu/internal/command/workdir"

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/backend"
	"github.com/opentofu/opentofu/internal/backend/local"
	"github.com/opentofu/opentofu/internal/backend/remote-state/inmem"
	"github.com/opentofu/opentofu/internal/encryption"
	"github.com/opentofu/opentofu/internal/states"
	"github.com/opentofu/opentofu/internal/states/statefile"
	"github.com/opentofu/opentofu/internal/states/statemgr"
)

func TestWorkspace_createAndChange(t *testing.T) {
	// Create a temporary working directory that is empty
	td := t.TempDir()
	t.Chdir(td)

	newCmd := &WorkspaceNewCommand{
		Meta: Meta{
			WorkingDir: workdir.NewDir("."),
		},
	}

	current, _ := newCmd.Workspace(t.Context())
	if current != backend.DefaultStateName {
		t.Fatal("current workspace should be 'default'")
	}

	args := []string{"test"}
	newCmdView, newCmdDone := testView(t)
	newCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       newCmdView,
	}
	code := newCmd.Run(args)
	newCmdOutput := newCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, newCmdOutput.Stderr())
	}

	current, _ = newCmd.Workspace(t.Context())
	if current != "test" {
		t.Fatalf("current workspace should be 'test', got %q", current)
	}

	selCmd := &WorkspaceSelectCommand{}
	args = []string{backend.DefaultStateName}
	selectCmdView, selectCmdDone := testView(t)
	selCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       selectCmdView,
	}
	code = selCmd.Run(args)
	selectCmdOutput := selectCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, selectCmdOutput.Stderr())
	}

	current, _ = newCmd.Workspace(t.Context())
	if current != backend.DefaultStateName {
		t.Fatal("current workspace should be 'default'")
	}
}

// Create some workspaces and test the list output.
// This also ensures we switch to the correct env after each call
func TestWorkspace_createAndList(t *testing.T) {
	// Create a temporary working directory that is empty
	td := t.TempDir()
	t.Chdir(td)

	// make sure a vars file doesn't interfere
	err := os.WriteFile(
		DefaultVarsFilename,
		[]byte(`foo = "bar"`),
		0644,
	)
	if err != nil {
		t.Fatal(err)
	}

	envs := []string{"test_a", "test_b", "test_c"}

	// create multiple workspaces
	for _, env := range envs {
		newCmdView, newCmdDone := testView(t)
		newCmd := &WorkspaceNewCommand{
			Meta: Meta{
				WorkingDir: workdir.NewDir("."),
				View:       newCmdView,
			},
		}
		code := newCmd.Run([]string{env})
		newCmdOutput := newCmdDone(t)
		if code != 0 {
			t.Fatalf("bad: %d\n\n%s", code, newCmdOutput.Stderr())
		}
	}

	listCmd := &WorkspaceListCommand{}
	listCmdView, listCmdDone := testView(t)
	listCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       listCmdView,
	}

	code := listCmd.Run(nil)
	listCmdOutput := listCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, listCmdOutput.Stderr())
	}

	actual := strings.TrimSpace(listCmdOutput.Stdout())
	expected := "default\n  test_a\n  test_b\n* test_c"

	if actual != expected {
		t.Fatalf("\nexpected: %q\nactual:  %q", expected, actual)
	}
}

// Create some workspaces and test the show output.
func TestWorkspace_createAndShow(t *testing.T) {
	// Create a temporary working directory that is empty
	td := t.TempDir()
	t.Chdir(td)

	// make sure a vars file doesn't interfere
	err := os.WriteFile(
		DefaultVarsFilename,
		[]byte(`foo = "bar"`),
		0644,
	)
	if err != nil {
		t.Fatal(err)
	}

	// make sure current workspace show outputs "default"
	showCmd := &WorkspaceShowCommand{}
	showCmdView, showCmdDone := testView(t)
	showCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       showCmdView,
	}

	code := showCmd.Run(nil)
	showCmdOutput := showCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, showCmdOutput.Stderr())
	}

	actual := strings.TrimSpace(showCmdOutput.Stdout())
	expected := "default"

	if actual != expected {
		t.Fatalf("\nexpected: %q\nactual:  %q", expected, actual)
	}

	newCmd := &WorkspaceNewCommand{}

	env := []string{"test_a"}

	// create test_a workspace
	newCmdView, newCmdDone := testView(t)
	newCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       newCmdView,
	}
	code = newCmd.Run(env)
	newCmdOutput := newCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, newCmdOutput.Stderr())
	}

	selCmd := &WorkspaceSelectCommand{}
	selCmdView, selCmdDone := testView(t)
	selCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       selCmdView,
	}
	code = selCmd.Run(env)
	selCmdOutput := selCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, selCmdOutput.Stderr())
	}

	showCmd = &WorkspaceShowCommand{}
	showCmdView, showCmdDone = testView(t)
	showCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       showCmdView,
	}

	code = showCmd.Run(nil)
	showCmdOutput = showCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, showCmdOutput.Stderr())
	}

	actual = strings.TrimSpace(showCmdOutput.Stdout())
	expected = "test_a"

	if actual != expected {
		t.Fatalf("\nexpected: %q\nactual:  %q", expected, actual)
	}
}

// Don't allow names that aren't URL safe
func TestWorkspace_createInvalid(t *testing.T) {
	// Create a temporary working directory that is empty
	td := t.TempDir()
	t.Chdir(td)

	envs := []string{"test_a*", "test_b/foo", "../../../test_c", "好_d"}

	// create multiple workspaces
	for _, env := range envs {
		view, done := testView(t)
		newCmd := &WorkspaceNewCommand{
			Meta: Meta{
				WorkingDir: workdir.NewDir("."),
				View:       view,
			},
		}
		code := newCmd.Run([]string{env})
		output := done(t)
		if code == 0 {
			t.Fatalf("expected failure: \n%s", output.All())
		}
	}

	// list workspaces to make sure none were created
	listCmd := &WorkspaceListCommand{}
	listCmdView, listCmdDone := testView(t)
	listCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       listCmdView,
	}

	code := listCmd.Run(nil)
	listCmdOutput := listCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, listCmdOutput.Stderr())
	}

	actual := strings.TrimSpace(listCmdOutput.Stdout())
	expected := "* default"

	if actual != expected {
		t.Fatalf("\nexpected: %q\nactual:  %q", expected, actual)
	}
}

func TestWorkspace_createWithState(t *testing.T) {
	td := t.TempDir()
	testCopyDir(t, testFixturePath("inmem-backend"), td)
	t.Chdir(td)
	defer inmem.Reset()

	// init the backend
	initCmdView, initCmdDone := testView(t)
	initCmd := &InitCommand{
		Meta: Meta{
			WorkingDir: workdir.NewDir("."),
			View:       initCmdView,
		},
	}
	code := initCmd.Run(nil)
	initCmdOutput := initCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", initCmdOutput.Stderr())
	}

	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"bar"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
			addrs.NoKey,
		)
	})

	err := statemgr.WriteAndPersist(t.Context(), statemgr.NewFilesystem("test.tfstate", encryption.StateEncryptionDisabled()), originalState, nil)
	if err != nil {
		t.Fatal(err)
	}

	workspace := "test_workspace"

	args := []string{"-state", "test.tfstate", workspace}
	newCmdView, newCmdDone := testView(t)
	newCmd := &WorkspaceNewCommand{
		Meta: Meta{
			WorkingDir: workdir.NewDir("."),
			View:       newCmdView,
		},
	}
	code = newCmd.Run(args)
	newCmdOutput := newCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, newCmdOutput.Stderr())
	}

	newPath := filepath.Join(local.DefaultWorkspaceDir, "test", arguments.DefaultStateFilename)
	envState := statemgr.NewFilesystem(newPath, encryption.StateEncryptionDisabled())
	err = envState.RefreshState(t.Context())
	if err != nil {
		t.Fatal(err)
	}

	b := backend.TestBackendConfig(t, inmem.New(encryption.StateEncryptionDisabled()), nil)
	sMgr, err := b.StateMgr(t.Context(), workspace)
	if err != nil {
		t.Fatal(err)
	}

	newState := sMgr.State()

	if got, want := newState.String(), originalState.String(); got != want {
		t.Fatalf("states not equal\ngot: %s\nwant: %s", got, want)
	}
}

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

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

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

	delCmdView, delCmdDone := testView(t)
	delCmd := &WorkspaceDeleteCommand{
		Meta: Meta{
			WorkingDir: workdir.NewDir("."),
			View:       delCmdView,
		},
	}

	current, _ := delCmd.Workspace(t.Context())
	if current != "test" {
		t.Fatal("wrong workspace:", current)
	}

	// we can't delete our current workspace
	args := []string{"test"}
	code := delCmd.Run(args)
	delCmdOutput := delCmdDone(t)
	if code == 0 {
		t.Fatalf("expected error deleting current workspace: %s", delCmdOutput.All())
	}

	// change back to default
	if err := delCmd.SetWorkspace(backend.DefaultStateName); err != nil {
		t.Fatal(err)
	}

	// try the delete again
	delCmdView, delCmdDone = testView(t)
	delCmd.Meta.View = delCmdView
	code = delCmd.Run(args)
	delCmdOutput = delCmdDone(t)
	if code != 0 {
		t.Fatalf("error deleting workspace: %s", delCmdOutput.Stderr())
	}

	current, _ = delCmd.Workspace(t.Context())
	if current != backend.DefaultStateName {
		t.Fatalf("wrong workspace: %q", current)
	}
}

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

	// choose an invalid workspace name
	workspace := "test workspace"
	path := filepath.Join(local.DefaultWorkspaceDir, workspace)

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

	delCmdView, delCmdDone := testView(t)
	delCmd := &WorkspaceDeleteCommand{
		Meta: Meta{
			WorkingDir: workdir.NewDir("."),
			View:       delCmdView,
		},
	}

	// delete the workspace
	code := delCmd.Run([]string{workspace})
	delCmdOutput := delCmdDone(t)
	if code != 0 {
		t.Fatalf("error deleting workspace: %s", delCmdOutput.Stderr())
	}

	if _, err := os.Stat(path); err == nil {
		t.Fatalf("should have deleted workspace, but %s still exists", path)
	} else if !os.IsNotExist(err) {
		t.Fatalf("unexpected error for workspace path: %s", err)
	}
}

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

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

	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"bar"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
			addrs.NoKey,
		)
	})

	f, err := os.Create(filepath.Join(local.DefaultWorkspaceDir, "test", "terraform.tfstate"))
	if err != nil {
		t.Fatal(err)
	}
	err = statefile.Write(&statefile.File{
		Serial:  0,
		Lineage: "test-lineage",
		State:   originalState,
	}, f, encryption.StateEncryptionDisabled())
	if err != nil {
		t.Fatal(err)
	}
	f.Close()

	delCmdView, delCmdDone := testView(t)
	delCmd := &WorkspaceDeleteCommand{
		Meta: Meta{
			WorkingDir: workdir.NewDir("."),
			View:       delCmdView,
		},
	}
	args := []string{"test"}
	code := delCmd.Run(args)
	delCmdOutput := delCmdDone(t)
	if code == 0 {
		t.Fatalf("expected failure without -force.\noutput: %s", delCmdOutput.All())
	}
	gotStderr := delCmdOutput.Stderr()
	if want, got := `Workspace "test" is currently tracking the following resource instances`, gotStderr; !strings.Contains(got, want) {
		t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, got)
	}
	if want, got := `- test_instance.foo`, gotStderr; !strings.Contains(got, want) {
		t.Errorf("error message doesn't mention the remaining instance\nwant substring: %s\ngot:\n%s", want, got)
	}

	delCmdView, delCmdDone = testView(t)
	delCmd.Meta.View = delCmdView

	args = []string{"-force", "test"}
	code = delCmd.Run(args)
	delCmdOutput = delCmdDone(t)
	if code != 0 {
		t.Fatalf("failure: %s", delCmdOutput.Stderr())
	}

	if _, err := os.Stat(filepath.Join(local.DefaultWorkspaceDir, "test")); !os.IsNotExist(err) {
		t.Fatal("env 'test' still exists!")
	}
}

func TestWorkspace_selectWithOrCreate(t *testing.T) {
	// Create a temporary working directory that is empty
	td := t.TempDir()
	t.Chdir(td)

	selectCmd := &WorkspaceSelectCommand{
		Meta: Meta{
			WorkingDir: workdir.NewDir("."),
		},
	}

	current, _ := selectCmd.Workspace(t.Context())
	if current != backend.DefaultStateName {
		t.Fatal("current workspace should be 'default'")
	}

	args := []string{"-or-create", "test"}
	selectCmdView, selectCmdDone := testView(t)
	selectCmd.Meta = Meta{
		WorkingDir: workdir.NewDir("."),
		View:       selectCmdView,
	}
	code := selectCmd.Run(args)
	selectCmdOutput := selectCmdDone(t)
	if code != 0 {
		t.Fatalf("bad: %d\n\n%s", code, selectCmdOutput.Stderr())
	}

	current, _ = selectCmd.Workspace(t.Context())
	if current != "test" {
		t.Fatalf("current workspace should be 'test', got %q", current)
	}

}
