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

package views

import (
	"os"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/opentofu/opentofu/internal/command/arguments"
	"github.com/opentofu/opentofu/internal/tfdiags"
)

func TestWorkspaceViews(t *testing.T) {
	tests := map[string]struct {
		viewCall   func(workspace Workspace)
		wantJson   []map[string]any
		wantStdout string
		wantStderr string
	}{
		"workspace_already_exists": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceAlreadyExists("test-workspace")
			},
			wantJson: []map[string]any{
				{
					"@level":   "error",
					"@message": `Workspace "test-workspace" already exists`,
					"@module":  "tofu.ui",
				},
			},
			wantStderr: `
Error: Workspace "test-workspace" already exists

A workspace having the given name already exists
`,
		},
		"workspace_does_not_exist": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceDoesNotExist("missing-workspace")
			},
			wantJson: []map[string]any{
				{
					"@level":   "error",
					"@message": `Workspace "missing-workspace" doesn't exist. You can create this workspace with the "new" subcommand or include the "-or-create" flag with the "select" subcommand`,
					"@module":  "tofu.ui",
				},
			},
			wantStderr: `
Error: Workspace "missing-workspace" doesn't exist

You can create this workspace with the "new" subcommand 
or include the "-or-create" flag with the "select" subcommand.
`,
		},
		"workspace_invalid_name": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceInvalidName("invalid/name")
			},
			wantJson: []map[string]any{
				{
					"@level":   "error",
					"@message": `The workspace name "invalid/name" is not allowed. The name must contain only URL safe characters, and no path separators`,
					"@module":  "tofu.ui",
				},
			},
			wantStderr: `
Error: Invalid workspace name

The workspace name "invalid/name" is not allowed. The name must contain only
URL safe characters, and no path separators.
`,
		},
		"list_workspaces": {
			viewCall: func(workspace Workspace) {
				workspace.ListWorkspaces([]string{"default", "dev", "prod"}, "dev")
			},
			wantJson: []map[string]any{
				{
					"@level":            "info",
					"@message":          "list of workspaces",
					"@module":           "tofu.ui",
					"type":              "workspace_list",
					"workspaces":        []any{"default", "dev", "prod"},
					"current_workspace": "dev",
				},
			},
			wantStdout: withNewline("  default\n* dev\n  prod\n"),
		},
		"workspace_overwritten_by_env_var_warn": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceOverwrittenByEnvVarWarn()
			},
			wantJson: []map[string]any{
				{
					"@level":   "info",
					"@message": "The active workspace is being overridden using the TF_WORKSPACE environment variable",
					"@module":  "tofu.ui",
				},
			},
			wantStdout: withNewline("\n\nThe active workspace is being overridden using the TF_WORKSPACE environment\nvariable.\n"),
		},
		"workspace_created": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceCreated("new-workspace")
			},
			wantJson: []map[string]any{
				{
					"@level":   "info",
					"@message": `Created and switched to workspace "new-workspace". You're now on a new, empty workspace. Workspaces isolate their state, so if you run "tofu plan" OpenTofu will not see any existing state for this configuration`,
					"@module":  "tofu.ui",
				},
			},
			wantStdout: withNewline("Created and switched to workspace \"new-workspace\"!\n\nYou're now on a new, empty workspace. Workspaces isolate their state,\nso if you run \"tofu plan\" OpenTofu will not see any existing state\nfor this configuration."),
		},
		"workspace_changed": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceChanged("other-workspace")
			},
			wantJson: []map[string]any{
				{
					"@level":   "info",
					"@message": `Switched to workspace "other-workspace"`,
					"@module":  "tofu.ui",
				},
			},
			wantStdout: withNewline(`Switched to workspace "other-workspace".`),
		},
		"workspace_is_overridden_select_error": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceIsOverriddenSelectError()
			},
			wantJson: []map[string]any{
				{
					"@level":   "error",
					"@message": "The selected workspace is currently overridden using the TF_WORKSPACE environment variable. To select a new workspace, either update this environment variable or unset it and then run this command again",
					"@module":  "tofu.ui",
				},
			},
			wantStderr: `
Error: The selected workspace is overridden using the TF_WORKSPACE environment variable

To select a new workspace, either update this environment variable or unset
it and then run this command again.
`,
		},
		"workspace_is_overridden_new_error": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceIsOverriddenNewError()
			},
			wantJson: []map[string]any{
				{
					"@level":   "error",
					"@message": "The workspace is currently overridden using the TF_WORKSPACE environment variable. You cannot create a new workspace when using this setting. To create a new workspace, either unset this environment variable or update it to match the workspace name you are trying to create, and then run this command again",
					"@module":  "tofu.ui",
				},
			},
			wantStderr: `
Error: The workspace is overridden using the TF_WORKSPACE environment variable

To create a new workspace, either unset this environment variable or update
it to match the workspace name you are trying to create, and then run this
command again.
`,
		},
		"workspace_deleted": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceDeleted("old-workspace")
			},
			wantJson: []map[string]any{
				{
					"@level":   "info",
					"@message": `Deleted workspace "old-workspace"`,
					"@module":  "tofu.ui",
				},
			},
			wantStdout: withNewline(`Deleted workspace "old-workspace"!`),
		},
		"deleted_workspace_not_empty": {
			viewCall: func(workspace Workspace) {
				workspace.DeletedWorkspaceNotEmpty("workspace-with-resources")
			},
			wantJson: []map[string]any{
				{
					"@level":   "warn",
					"@message": `"workspace-with-resources" was non-empty. The resources managed by the deleted workspace may still exist, but are no longer manageable by OpenTofu since the state has been deleted`,
					"@module":  "tofu.ui",
				},
			},
			wantStdout: `
Warning: "workspace-with-resources" was non-empty

The resources managed by the deleted workspace may still exist, but are no
longer manageable by OpenTofu since the state has been deleted.
`,
		},
		"cannot_delete_current_workspace": {
			viewCall: func(workspace Workspace) {
				workspace.CannotDeleteCurrentWorkspace("current-workspace")
			},
			wantJson: []map[string]any{
				{
					"@level":   "error",
					"@message": `Workspace "current-workspace" is your active workspace. You cannot delete the currently active workspace. Please switch to another workspace and try again`,
					"@module":  "tofu.ui",
				},
			},
			wantStderr: `
Error: Workspace "current-workspace" is your active workspace

You cannot delete the currently active workspace. Please switch to another
workspace and try again.
`,
		},
		"workspace_show": {
			viewCall: func(workspace Workspace) {
				workspace.WorkspaceShow("my-workspace")
			},
			wantJson: []map[string]any{
				{
					"@level":   "info",
					"@message": "my-workspace",
					"@module":  "tofu.ui",
				},
			},
			wantStdout: withNewline("my-workspace"),
		},
		"warn_when_used_as_env_cmd_true": {
			viewCall: func(workspace Workspace) {
				workspace.WarnWhenUsedAsEnvCmd(true)
			},
			wantJson: []map[string]any{
				{
					"@level":   "warn",
					"@message": `The "tofu env" family of commands is deprecated. Use "tofu workspace" instead`,
					"@module":  "tofu.ui",
				},
			},
			wantStdout: `
Warning: The "tofu env" family of commands is deprecated

"Workspace" is now the preferred term for what earlier OpenTofu versions
called "environment", to reduce ambiguity caused by the latter term colliding
with other concepts.

The "tofu workspace" commands should be used instead. "tofu env" will be
removed in a future OpenTofu version.
`,
		},
		"warn_when_used_as_env_cmd_false": {
			viewCall: func(workspace Workspace) {
				workspace.WarnWhenUsedAsEnvCmd(false)
			},
			wantJson:   []map[string]any{{}},
			wantStdout: "",
		},
		// Diagnostics
		"diagnostics_warning": {
			viewCall: func(workspace Workspace) {
				diags := tfdiags.Diagnostics{
					tfdiags.Sourceless(tfdiags.Warning, "A workspace warning", "workspace warning detail"),
				}
				workspace.Diagnostics(diags)
			},
			wantStdout: withNewline("\nWarning: A workspace warning\n\nworkspace warning detail"),
			wantStderr: "",
			wantJson: []map[string]any{
				{
					"@level":   "warn",
					"@message": "Warning: A workspace warning",
					"@module":  "tofu.ui",
					"diagnostic": map[string]any{
						"detail":   "workspace warning detail",
						"severity": "warning",
						"summary":  "A workspace warning",
					},
					"type": "diagnostic",
				},
			},
		},
		"diagnostics_error": {
			viewCall: func(workspace Workspace) {
				diags := tfdiags.Diagnostics{
					tfdiags.Sourceless(tfdiags.Error, "A workspace error", "workspace error detail"),
				}
				workspace.Diagnostics(diags)
			},
			wantStdout: "",
			wantStderr: withNewline("\nError: A workspace error\n\nworkspace error detail"),
			wantJson: []map[string]any{
				{
					"@level":   "error",
					"@message": "Error: A workspace error",
					"@module":  "tofu.ui",
					"diagnostic": map[string]any{
						"detail":   "workspace error detail",
						"severity": "error",
						"summary":  "A workspace error",
					},
					"type": "diagnostic",
				},
			},
		},
	}
	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			testWorkspaceHuman(t, tc.viewCall, tc.wantStdout, tc.wantStderr)
			testWorkspaceJson(t, tc.viewCall, tc.wantJson)
			testWorkspaceMulti(t, tc.viewCall, tc.wantStdout, tc.wantStderr, tc.wantJson)
		})
	}
}

func testWorkspaceHuman(t *testing.T, call func(workspace Workspace), wantStdout, wantStderr string) {
	view, done := testView(t)
	workspaceView := NewWorkspace(arguments.ViewOptions{ViewType: arguments.ViewHuman}, view)
	call(workspaceView)
	output := done(t)
	if diff := cmp.Diff(wantStderr, output.Stderr()); diff != "" {
		t.Errorf("invalid stderr (-want, +got):\n%s", diff)
	}
	if diff := cmp.Diff(wantStdout, output.Stdout()); diff != "" {
		t.Errorf("invalid stdout (-want, +got):\n%s", diff)
	}
}

func testWorkspaceJson(t *testing.T, call func(workspace Workspace), want []map[string]interface{}) {
	view, done := testView(t)
	workspaceView := NewWorkspace(arguments.ViewOptions{ViewType: arguments.ViewJSON}, view)
	call(workspaceView)
	output := done(t)
	if output.Stderr() != "" {
		t.Errorf("expected no stderr but got:\n%s", output.Stderr())
	}

	testJSONViewOutputEquals(t, output.Stdout(), want)
}

func testWorkspaceMulti(t *testing.T, call func(workspace Workspace), wantStdout string, wantStderr string, want []map[string]interface{}) {
	jsonInto, err := os.CreateTemp(t.TempDir(), "json-into-*")
	if err != nil {
		t.Fatalf("failed to create the file to write json content into: %s", err)
	}
	view, done := testView(t)
	workspaceView := NewWorkspace(arguments.ViewOptions{ViewType: arguments.ViewHuman, JSONInto: jsonInto}, view)
	call(workspaceView)
	{
		if err := jsonInto.Close(); err != nil {
			t.Fatalf("failed to close the jsonInto file: %s", err)
		}
		// check the fileInto content
		fileContent, err := os.ReadFile(jsonInto.Name())
		if err != nil {
			t.Fatalf("failed to read the file content with the json output: %s", err)
		}
		testJSONViewOutputEquals(t, string(fileContent), want)
	}
	{
		// check the human output
		output := done(t)
		if diff := cmp.Diff(wantStderr, output.Stderr()); diff != "" {
			t.Errorf("invalid stderr (-want, +got):\n%s", diff)
		}
		if diff := cmp.Diff(wantStdout, output.Stdout()); diff != "" {
			t.Errorf("invalid stdout (-want, +got):\n%s", diff)
		}
	}
}
