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

package command

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

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

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/backend"
	backendInit "github.com/hashicorp/terraform/internal/backend/init"
	"github.com/hashicorp/terraform/internal/backend/remote-state/inmem"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/states/statefile"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

func TestOutput(t *testing.T) {
	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetOutputValue(
			addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("bar"),
			false,
		)
	})

	statePath := testStateFile(t, originalState)

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{
		"-state", statePath,
		"foo",
	}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}

	actual := strings.TrimSpace(output.Stdout())
	if actual != `"bar"` {
		t.Fatalf("bad: %#v", actual)
	}
}

func TestOutput_stateStore(t *testing.T) {
	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetOutputValue(
			addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("bar"),
			false,
		)
	})

	// Create a temporary working directory that is empty
	td := t.TempDir()
	testCopyDir(t, testFixturePath("state-store-unchanged"), td)
	t.Chdir(td)

	// Get bytes describing the state
	var stateBuf bytes.Buffer
	if err := statefile.Write(statefile.New(originalState, "", 1), &stateBuf); err != nil {
		t.Fatalf("error during test setup: %s", err)
	}

	// Create a mock that contains a persisted "default" state that uses the bytes from above.
	mockProvider := mockPluggableStateStorageProvider()
	mockProvider.MockStates = map[string]interface{}{
		"default": stateBuf.Bytes(),
	}
	mockProviderAddress := addrs.NewDefaultProvider("test")

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			AllowExperimentalFeatures: true,
			testingOverrides: &testingOverrides{
				Providers: map[addrs.Provider]providers.Factory{
					mockProviderAddress: providers.FactoryFixed(mockProvider),
				},
			},
			View: view,
		},
	}

	args := []string{
		"foo",
	}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}

	actual := strings.TrimSpace(output.Stdout())
	if actual != `"bar"` {
		t.Fatalf("bad: %#v", actual)
	}
}

func TestOutput_json(t *testing.T) {
	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetOutputValue(
			addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("bar"),
			false,
		)
	})

	statePath := testStateFile(t, originalState)

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{
		"-state", statePath,
		"-json",
	}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}

	actual := strings.TrimSpace(output.Stdout())
	expected := "{\n  \"foo\": {\n    \"sensitive\": false,\n    \"type\": \"string\",\n    \"value\": \"bar\"\n  }\n}"
	if actual != expected {
		t.Fatalf("wrong output\ngot:  %#v\nwant: %#v", actual, expected)
	}
}

func TestOutput_emptyOutputs(t *testing.T) {
	originalState := states.NewState()
	statePath := testStateFile(t, originalState)

	p := testProvider()
	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(p),
			View:             view,
		},
	}

	args := []string{
		"-no-color",
		"-state", statePath,
	}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}
	// Warning diagnostics should go to stdout
	if got, want := output.Stdout(), "Warning: No outputs found"; !strings.Contains(got, want) {
		t.Fatalf("bad output: expected to contain %q, got:\n%s", want, got)
	}
}

func TestOutput_badVar(t *testing.T) {
	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetOutputValue(
			addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("bar"),
			false,
		)
	})
	statePath := testStateFile(t, originalState)

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{
		"-state", statePath,
		"bar",
	}
	code := c.Run(args)
	output := done(t)
	if code != 1 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}
}

func TestOutput_blank(t *testing.T) {
	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetOutputValue(
			addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("bar"),
			false,
		)
		s.SetOutputValue(
			addrs.OutputValue{Name: "name"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("john-doe"),
			false,
		)
	})
	statePath := testStateFile(t, originalState)

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{
		"-state", statePath,
		"",
	}

	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}

	expectedOutput := "foo = \"bar\"\nname = \"john-doe\"\n"
	if got := output.Stdout(); got != expectedOutput {
		t.Fatalf("wrong output\ngot:  %#v\nwant: %#v", got, expectedOutput)
	}
}

func TestOutput_manyArgs(t *testing.T) {
	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{
		"bad",
		"bad",
	}
	code := c.Run(args)
	output := done(t)
	if code != 1 {
		t.Fatalf("bad: \n%s", output.Stdout())
	}
}

func TestOutput_noArgs(t *testing.T) {
	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stdout())
	}
}

func TestOutput_noState(t *testing.T) {
	originalState := states.NewState()
	statePath := testStateFile(t, originalState)

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{
		"-state", statePath,
		"foo",
	}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}
}

func TestOutput_noVars(t *testing.T) {
	originalState := states.NewState()

	statePath := testStateFile(t, originalState)

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{
		"-state", statePath,
		"bar",
	}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}
}

func TestOutput_stateDefault(t *testing.T) {
	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetOutputValue(
			addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("bar"),
			false,
		)
	})

	// Write the state file in a temporary directory with the
	// default filename.
	td := testTempDir(t)
	statePath := filepath.Join(td, DefaultStateFilename)

	f, err := os.Create(statePath)
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	err = writeStateForTesting(originalState, f)
	f.Close()
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	// Change to that directory
	cwd, err := os.Getwd()
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if err := os.Chdir(filepath.Dir(statePath)); err != nil {
		t.Fatalf("err: %s", err)
	}
	defer os.Chdir(cwd)

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{
		"foo",
	}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("bad: \n%s", output.Stderr())
	}

	actual := strings.TrimSpace(output.Stdout())
	if actual != `"bar"` {
		t.Fatalf("bad: %#v", actual)
	}
}

// deprecatedInmemBackend wraps the inmem backend and injects a deprecation
// warning from PrepareConfig, simulating a backend with deprecated attributes
// (like the S3 backend's dynamodb_table).
type deprecatedInmemBackend struct {
	backend.Backend
}

func (b *deprecatedInmemBackend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
	newObj, diags := b.Backend.PrepareConfig(obj)
	diags = diags.Append(tfdiags.SimpleWarning(`The attribute "deprecated_attr" is deprecated.`))
	return newObj, diags
}

func TestOutputRaw_warningsSuppressed(t *testing.T) {
	// Pre-populate the inmem backend with a state containing an output value
	inmem.Reset()
	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetOutputValue(
			addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("bar"),
			false,
		)
	})

	// Register a backend that wraps inmem with a deprecation warning,
	// simulating a backend like S3 whose PrepareConfig warns about
	// deprecated attributes (e.g. dynamodb_table).
	backendInit.Set("inmem", func() backend.Backend {
		return &deprecatedInmemBackend{Backend: inmem.New()}
	})
	defer backendInit.Set("inmem", inmem.New)

	td := t.TempDir()
	testCopyDir(t, testFixturePath("output-backend-with-deprecation"), td)
	t.Chdir(td)

	// Write the state into the inmem backend's default workspace
	b := inmem.New()
	b.Configure(cty.ObjectVal(map[string]cty.Value{
		"lock_id": cty.NullVal(cty.String),
	}))
	sMgr, sDiags := b.StateMgr(backend.DefaultStateName)
	if sDiags.HasErrors() {
		t.Fatalf("unexpected error: %s", sDiags.Err())
	}
	sMgr.WriteState(originalState)
	if err := sMgr.PersistState(nil); err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{"-raw", "foo"}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("unexpected exit code %d\nstderr:\n%s", code, output.Stderr())
	}

	// The key assertion: warnings must not appear in raw output
	// as they would be indistinguishable from the value.
	stderr := output.Stderr()
	if strings.Contains(stderr, "deprecated") {
		t.Fatalf("warnings should be suppressed, got:\n%s", stderr)
	}

	actual := strings.TrimSpace(output.Stdout())
	if actual != `bar` {
		t.Fatalf("expected output \"bar\", got: %#v", actual)
	}
}

func TestOutputJson_warningsSuppressed(t *testing.T) {
	// Pre-populate the inmem backend with a state containing an output value
	inmem.Reset()
	originalState := states.BuildState(func(s *states.SyncState) {
		s.SetOutputValue(
			addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
			cty.StringVal("bar"),
			false,
		)
	})

	// Register a backend that wraps inmem with a deprecation warning,
	// simulating a backend like S3 whose PrepareConfig warns about
	// deprecated attributes (e.g. dynamodb_table).
	backendInit.Set("inmem", func() backend.Backend {
		return &deprecatedInmemBackend{Backend: inmem.New()}
	})
	defer backendInit.Set("inmem", inmem.New)

	td := t.TempDir()
	testCopyDir(t, testFixturePath("output-backend-with-deprecation"), td)
	t.Chdir(td)

	// Write the state into the inmem backend's default workspace
	b := inmem.New()
	b.Configure(cty.ObjectVal(map[string]cty.Value{
		"lock_id": cty.NullVal(cty.String),
	}))
	sMgr, sDiags := b.StateMgr(backend.DefaultStateName)
	if sDiags.HasErrors() {
		t.Fatalf("unexpected error: %s", sDiags.Err())
	}
	sMgr.WriteState(originalState)
	if err := sMgr.PersistState(nil); err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	view, done := testView(t)
	c := &OutputCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			View:             view,
		},
	}

	args := []string{"-json"}
	code := c.Run(args)
	output := done(t)
	if code != 0 {
		t.Fatalf("unexpected exit code %d\nstderr:\n%s", code, output.Stderr())
	}

	stderr := output.Stderr()
	if strings.Contains(stderr, "deprecated") {
		t.Fatalf("warnings should be suppressed, got:\n%s", stderr)
	}

	expectedJson := `{
	"foo":{
		"sensitive":false,
		"type":"string",
		"value":"bar"
	}
}`
	if diff := cmp.Diff(expectedJson, output.Stdout(), transformJSON); diff != "" {
		t.Fatalf("unexpected output: %s", diff)
	}
}

var transformJSON = cmp.FilterValues(func(x, y string) bool {
	xBytes := []byte(x)
	yBytes := []byte(y)
	return json.Valid(xBytes) && json.Valid(yBytes)
}, cmp.Transformer("ParseJSON", func(in string) (out any) {
	inBytes := []byte(in)
	if err := json.Unmarshal(inBytes, &out); err != nil {
		panic(err)
	}
	return out
}))
