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

package jsonstate

import (
	"encoding/json"
	"fmt"
	"reflect"
	"testing"

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

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/terraform"
)

func ptrOf[T any](v T) *T {
	return &v
}

func TestMarshalOutputs(t *testing.T) {
	tests := []struct {
		Outputs map[string]*states.OutputValue
		Want    map[string]Output
		Err     bool
	}{
		{
			nil,
			nil,
			false,
		},
		{
			map[string]*states.OutputValue{
				"test": {
					Sensitive: true,
					Value:     cty.StringVal("sekret"),
				},
			},
			map[string]Output{
				"test": {
					Sensitive: true,
					Value:     json.RawMessage(`"sekret"`),
					Type:      json.RawMessage(`"string"`),
				},
			},
			false,
		},
		{
			map[string]*states.OutputValue{
				"test": {
					Sensitive: false,
					Value:     cty.StringVal("not_so_sekret"),
				},
			},
			map[string]Output{
				"test": {
					Sensitive: false,
					Value:     json.RawMessage(`"not_so_sekret"`),
					Type:      json.RawMessage(`"string"`),
				},
			},
			false,
		},
		{
			map[string]*states.OutputValue{
				"mapstring": {
					Sensitive: false,
					Value: cty.MapVal(map[string]cty.Value{
						"beep": cty.StringVal("boop"),
					}),
				},
				"setnumber": {
					Sensitive: false,
					Value: cty.SetVal([]cty.Value{
						cty.NumberIntVal(3),
						cty.NumberIntVal(5),
						cty.NumberIntVal(7),
						cty.NumberIntVal(11),
					}),
				},
			},
			map[string]Output{
				"mapstring": {
					Sensitive: false,
					Value:     json.RawMessage(`{"beep":"boop"}`),
					Type:      json.RawMessage(`["map","string"]`),
				},
				"setnumber": {
					Sensitive: false,
					Value:     json.RawMessage(`[3,5,7,11]`),
					Type:      json.RawMessage(`["set","number"]`),
				},
			},
			false,
		},
	}

	for _, test := range tests {
		got, err := MarshalOutputs(test.Outputs)
		if test.Err {
			if err == nil {
				t.Fatal("succeeded; want error")
			}
			return
		} else if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		if !cmp.Equal(test.Want, got) {
			t.Fatalf("wrong result:\n%s", cmp.Diff(test.Want, got))
		}
	}
}

func TestMarshalAttributeValues(t *testing.T) {
	tests := []struct {
		Attr               cty.Value
		Want               AttributeValues
		WantSensitivePaths []cty.Path
	}{
		{
			cty.NilVal,
			nil,
			nil,
		},
		{
			cty.NullVal(cty.String),
			nil,
			nil,
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
			}),
			AttributeValues{"foo": json.RawMessage(`"bar"`)},
			nil,
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.String),
			}),
			AttributeValues{"foo": json.RawMessage(`null`)},
			nil,
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"bar": cty.MapVal(map[string]cty.Value{
					"hello": cty.StringVal("world"),
				}),
				"baz": cty.ListVal([]cty.Value{
					cty.StringVal("goodnight"),
					cty.StringVal("moon"),
				}),
			}),
			AttributeValues{
				"bar": json.RawMessage(`{"hello":"world"}`),
				"baz": json.RawMessage(`["goodnight","moon"]`),
			},
			nil,
		},
		// Sensitive values
		{
			cty.ObjectVal(map[string]cty.Value{
				"bar": cty.MapVal(map[string]cty.Value{
					"hello": cty.StringVal("world"),
				}),
				"baz": cty.ListVal([]cty.Value{
					cty.StringVal("goodnight"),
					cty.StringVal("moon").Mark(marks.Sensitive),
				}),
			}),
			AttributeValues{
				"bar": json.RawMessage(`{"hello":"world"}`),
				"baz": json.RawMessage(`["goodnight","moon"]`),
			},
			[]cty.Path{
				cty.GetAttrPath("baz").IndexInt(1),
			},
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("%#v", test.Attr), func(t *testing.T) {
			val, got, sensitivePaths, err := marshalAttributeValues(test.Attr)
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}
			if !reflect.DeepEqual(got, test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v\n", got, test.Want)
			}
			if !reflect.DeepEqual(sensitivePaths, test.WantSensitivePaths) {
				t.Errorf("wrong marks\ngot:  %#v\nwant: %#v\n", sensitivePaths, test.WantSensitivePaths)
			}
			if _, marks := val.Unmark(); len(marks) != 0 {
				t.Errorf("returned value still has marks; should have been unmarked\n%#v", marks)
			}
		})
	}

	t.Run("reject unsupported marks", func(t *testing.T) {
		_, _, _, err := marshalAttributeValues(cty.ObjectVal(map[string]cty.Value{
			"disallowed": cty.StringVal("a").Mark("unsupported"),
		}))
		if err == nil {
			t.Fatalf("unexpected success; want error")
		}
		got := err.Error()
		want := `.disallowed: cannot serialize value marked as cty.NewValueMarks("unsupported") for inclusion in a state snapshot (this is a bug in Terraform)`
		if got != want {
			t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
		}
	})
}

func TestMarshalResources(t *testing.T) {
	deposedKey := states.NewDeposedKey()
	tests := map[string]struct {
		Resources map[string]*states.Resource
		Schemas   *terraform.Schemas
		Want      []Resource
		Err       bool
	}{
		"nil": {
			nil,
			nil,
			nil,
			false,
		},
		"single resource": {
			map[string]*states.Resource{
				"test_thing.baz": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Current: &states.ResourceInstanceObjectSrc{
								Status:    states.ObjectReady,
								AttrsJSON: []byte(`{"woozles":"confuzles"}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_thing.bar",
					Mode:         "managed",
					Type:         "test_thing",
					Name:         "bar",
					Index:        nil,
					ProviderName: "registry.terraform.io/hashicorp/test",
					AttributeValues: AttributeValues{
						"foozles": json.RawMessage(`null`),
						"woozles": json.RawMessage(`"confuzles"`),
					},
					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
				},
			},
			false,
		},
		"single resource_with_sensitive": {
			map[string]*states.Resource{
				"test_thing.baz": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Current: &states.ResourceInstanceObjectSrc{
								Status:    states.ObjectReady,
								AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles"}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_thing.bar",
					Mode:         "managed",
					Type:         "test_thing",
					Name:         "bar",
					Index:        nil,
					ProviderName: "registry.terraform.io/hashicorp/test",
					AttributeValues: AttributeValues{
						"foozles": json.RawMessage(`"sensuzles"`),
						"woozles": json.RawMessage(`"confuzles"`),
					},
					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
				},
			},
			false,
		},
		"resource with marks": {
			map[string]*states.Resource{
				"test_thing.bar": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Current: &states.ResourceInstanceObjectSrc{
								Status:    states.ObjectReady,
								AttrsJSON: []byte(`{"foozles":"confuzles"}`),
								AttrSensitivePaths: []cty.Path{
									cty.GetAttrPath("foozles"),
								},
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_thing.bar",
					Mode:         "managed",
					Type:         "test_thing",
					Name:         "bar",
					Index:        nil,
					ProviderName: "registry.terraform.io/hashicorp/test",
					AttributeValues: AttributeValues{
						"foozles": json.RawMessage(`"confuzles"`),
						"woozles": json.RawMessage(`null`),
					},
					SensitiveValues: json.RawMessage(`{"foozles":true}`),
				},
			},
			false,
		},
		"single resource wrong schema": {
			map[string]*states.Resource{
				"test_thing.baz": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Current: &states.ResourceInstanceObjectSrc{
								SchemaVersion: 1,
								Status:        states.ObjectReady,
								AttrsJSON:     []byte(`{"woozles":["confuzles"]}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			nil,
			true,
		},
		"resource with count": {
			map[string]*states.Resource{
				"test_thing.bar": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.IntKey(0): {
							Current: &states.ResourceInstanceObjectSrc{
								Status:    states.ObjectReady,
								AttrsJSON: []byte(`{"woozles":"confuzles"}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_thing.bar[0]",
					Mode:         "managed",
					Type:         "test_thing",
					Name:         "bar",
					Index:        json.RawMessage(`0`),
					ProviderName: "registry.terraform.io/hashicorp/test",
					AttributeValues: AttributeValues{
						"foozles": json.RawMessage(`null`),
						"woozles": json.RawMessage(`"confuzles"`),
					},
					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
				},
			},
			false,
		},
		"resource with for_each": {
			map[string]*states.Resource{
				"test_thing.bar": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.StringKey("rockhopper"): {
							Current: &states.ResourceInstanceObjectSrc{
								Status:    states.ObjectReady,
								AttrsJSON: []byte(`{"woozles":"confuzles"}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_thing.bar[\"rockhopper\"]",
					Mode:         "managed",
					Type:         "test_thing",
					Name:         "bar",
					Index:        json.RawMessage(`"rockhopper"`),
					ProviderName: "registry.terraform.io/hashicorp/test",
					AttributeValues: AttributeValues{
						"foozles": json.RawMessage(`null`),
						"woozles": json.RawMessage(`"confuzles"`),
					},
					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
				},
			},
			false,
		},
		"deposed resource": {
			map[string]*states.Resource{
				"test_thing.baz": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Deposed: map[states.DeposedKey]*states.ResourceInstanceObjectSrc{
								states.DeposedKey(deposedKey): {
									Status:    states.ObjectReady,
									AttrsJSON: []byte(`{"woozles":"confuzles"}`),
								},
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_thing.bar",
					Mode:         "managed",
					Type:         "test_thing",
					Name:         "bar",
					Index:        nil,
					ProviderName: "registry.terraform.io/hashicorp/test",
					DeposedKey:   deposedKey.String(),
					AttributeValues: AttributeValues{
						"foozles": json.RawMessage(`null`),
						"woozles": json.RawMessage(`"confuzles"`),
					},
					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
				},
			},
			false,
		},
		"deposed and current resource": {
			map[string]*states.Resource{
				"test_thing.baz": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Deposed: map[states.DeposedKey]*states.ResourceInstanceObjectSrc{
								states.DeposedKey(deposedKey): {
									Status:    states.ObjectReady,
									AttrsJSON: []byte(`{"woozles":"confuzles"}`),
								},
							},
							Current: &states.ResourceInstanceObjectSrc{
								Status:    states.ObjectReady,
								AttrsJSON: []byte(`{"woozles":"confuzles"}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_thing.bar",
					Mode:         "managed",
					Type:         "test_thing",
					Name:         "bar",
					Index:        nil,
					ProviderName: "registry.terraform.io/hashicorp/test",
					AttributeValues: AttributeValues{
						"foozles": json.RawMessage(`null`),
						"woozles": json.RawMessage(`"confuzles"`),
					},
					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
				},
				{
					Address:      "test_thing.bar",
					Mode:         "managed",
					Type:         "test_thing",
					Name:         "bar",
					Index:        nil,
					ProviderName: "registry.terraform.io/hashicorp/test",
					DeposedKey:   deposedKey.String(),
					AttributeValues: AttributeValues{
						"foozles": json.RawMessage(`null`),
						"woozles": json.RawMessage(`"confuzles"`),
					},
					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
				},
			},
			false,
		},
		"resource with marked map attr": {
			map[string]*states.Resource{
				"test_map_attr.bar": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_map_attr",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Current: &states.ResourceInstanceObjectSrc{
								Status:    states.ObjectReady,
								AttrsJSON: []byte(`{"data":{"woozles":"confuzles"}}`),
								AttrSensitivePaths: []cty.Path{
									cty.GetAttrPath("data"),
								},
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_map_attr.bar",
					Mode:         "managed",
					Type:         "test_map_attr",
					Name:         "bar",
					Index:        nil,
					ProviderName: "registry.terraform.io/hashicorp/test",
					AttributeValues: AttributeValues{
						"data": json.RawMessage(`{"woozles":"confuzles"}`),
					},
					SensitiveValues: json.RawMessage(`{"data":true}`),
				},
			},
			false,
		},
		"single resource with identity": {
			map[string]*states.Resource{
				"test_identity.bar": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_identity",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Current: &states.ResourceInstanceObjectSrc{
								Status:       states.ObjectReady,
								AttrsJSON:    []byte(`{"woozles":"confuzles","foozles":"sensuzles","name":"bar"}`),
								IdentityJSON: []byte(`{"foozles":"sensuzles","name":"bar"}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			[]Resource{
				{
					Address:      "test_identity.bar",
					Mode:         "managed",
					Type:         "test_identity",
					Name:         "bar",
					Index:        nil,
					ProviderName: "registry.terraform.io/hashicorp/test",
					AttributeValues: AttributeValues{
						"name":    json.RawMessage(`"bar"`),
						"foozles": json.RawMessage(`"sensuzles"`),
						"woozles": json.RawMessage(`"confuzles"`),
					},
					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
					IdentityValues: IdentityValues{
						"name":    json.RawMessage(`"bar"`),
						"foozles": json.RawMessage(`"sensuzles"`),
					},
					IdentitySchemaVersion: ptrOf[uint64](0),
				},
			},
			false,
		},
		"single resource wrong identity schema": {
			map[string]*states.Resource{
				"test_identity.bar": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_identity",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Current: &states.ResourceInstanceObjectSrc{
								Status:                states.ObjectReady,
								AttrsJSON:             []byte(`{"woozles":"confuzles","foozles":"sensuzles","name":"bar"}`),
								IdentitySchemaVersion: 1,
								IdentityJSON:          []byte(`{"foozles":"sensuzles","name":"bar"}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			nil,
			true,
		},
		"single resource missing identity schema": {
			map[string]*states.Resource{
				"test_thing.bar": {
					Addr: addrs.AbsResource{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "bar",
						},
					},
					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
						addrs.NoKey: {
							Current: &states.ResourceInstanceObjectSrc{
								Status:                states.ObjectReady,
								AttrsJSON:             []byte(`{"woozles":"confuzles","foozles":"sensuzles"}`),
								IdentitySchemaVersion: 0,
								IdentityJSON:          []byte(`{"foozles":"sensuzles","name":"bar"}`),
							},
						},
					},
					ProviderConfig: addrs.AbsProviderConfig{
						Provider: addrs.NewDefaultProvider("test"),
						Module:   addrs.RootModule,
					},
				},
			},
			testSchemas(),
			nil,
			true,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			got, err := marshalResources(test.Resources, test.Schemas)
			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			diff := cmp.Diff(got, test.Want)
			if diff != "" {
				t.Fatalf("wrong result: %s\n", diff)
			}

		})
	}
}

func TestMarshalModules_basic(t *testing.T) {
	childModule, _ := addrs.ParseModuleInstanceStr("module.child")
	subModule, _ := addrs.ParseModuleInstanceStr("module.submodule")
	testState := 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","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(childModule),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   childModule.Module(),
			},
		)
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(subModule),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   subModule.Module(),
			},
		)
	})
	moduleMap := make(map[string][]addrs.ModuleInstance)
	moduleMap[""] = []addrs.ModuleInstance{childModule, subModule}

	got, err := marshalModules(testState, testSchemas(), moduleMap[""], moduleMap)

	if err != nil {
		t.Fatalf("unexpected error: %s", err.Error())
	}

	if len(got) != 2 {
		t.Fatalf("wrong result! got %d modules, expected 2", len(got))
	}

	if got[0].Address != "module.child" || got[1].Address != "module.submodule" {
		t.Fatalf("wrong result! got %#v\n", got)
	}

}

func TestMarshalModules_nested(t *testing.T) {
	childModule, _ := addrs.ParseModuleInstanceStr("module.child")
	subModule, _ := addrs.ParseModuleInstanceStr("module.child.module.submodule")
	testState := 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","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(childModule),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   childModule.Module(),
			},
		)
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(subModule),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   subModule.Module(),
			},
		)
	})
	moduleMap := make(map[string][]addrs.ModuleInstance)
	moduleMap[""] = []addrs.ModuleInstance{childModule}
	moduleMap[childModule.String()] = []addrs.ModuleInstance{subModule}

	got, err := marshalModules(testState, testSchemas(), moduleMap[""], moduleMap)

	if err != nil {
		t.Fatalf("unexpected error: %s", err.Error())
	}

	if len(got) != 1 {
		t.Fatalf("wrong result! got %d modules, expected 1", len(got))
	}

	if got[0].Address != "module.child" {
		t.Fatalf("wrong result! got %#v\n", got)
	}

	if got[0].ChildModules[0].Address != "module.child.module.submodule" {
		t.Fatalf("wrong result! got %#v\n", got)
	}
}

func TestMarshalModules_parent_no_resources(t *testing.T) {
	subModule, _ := addrs.ParseModuleInstanceStr("module.child.module.submodule")
	testState := 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","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(subModule),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   subModule.Module(),
			},
		)
	})
	got, err := marshalRootModule(testState, testSchemas())

	if err != nil {
		t.Fatalf("unexpected error: %s", err.Error())
	}

	if len(got.ChildModules) != 1 {
		t.Fatalf("wrong result! got %d modules, expected 1", len(got.ChildModules))
	}

	if got.ChildModules[0].Address != "module.child" {
		t.Fatalf("wrong result! got %#v\n", got)
	}

	if got.ChildModules[0].ChildModules[0].Address != "module.child.module.submodule" {
		t.Fatalf("wrong result! got %#v\n", got)
	}
}

func testSchemas() *terraform.Schemas {
	return &terraform.Schemas{
		Providers: map[addrs.Provider]providers.ProviderSchema{
			addrs.NewDefaultProvider("test"): {
				ResourceTypes: map[string]providers.Schema{
					"test_thing": {
						Body: &configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"woozles": {Type: cty.String, Optional: true, Computed: true},
								"foozles": {Type: cty.String, Optional: true, Sensitive: true},
							},
						},
					},
					"test_instance": {
						Body: &configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"id":  {Type: cty.String, Optional: true, Computed: true},
								"foo": {Type: cty.String, Optional: true},
								"bar": {Type: cty.String, Optional: true},
							},
						},
					},
					"test_map_attr": {
						Body: &configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"data": {Type: cty.Map(cty.String), Optional: true, Computed: true, Sensitive: true},
							},
						},
					},
					"test_identity": {
						Body: &configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"name":    {Type: cty.String, Required: true},
								"woozles": {Type: cty.String, Optional: true, Computed: true},
								"foozles": {Type: cty.String, Optional: true, Sensitive: true},
							},
						},
						Identity: &configschema.Object{
							Attributes: map[string]*configschema.Attribute{
								"name":    {Type: cty.String, Required: true},
								"foozles": {Type: cty.String, Optional: true},
							},
							Nesting: configschema.NestingSingle,
						},
					},
				},
			},
		},
	}
}

func TestSensitiveAsBool(t *testing.T) {
	tests := []struct {
		Input cty.Value
		Want  cty.Value
	}{
		{
			cty.StringVal("hello"),
			cty.False,
		},
		{
			cty.NullVal(cty.String),
			cty.False,
		},
		{
			cty.StringVal("hello").Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.NullVal(cty.String).Mark(marks.Sensitive),
			cty.True,
		},

		{
			cty.NullVal(cty.DynamicPseudoType).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})),
			cty.False,
		},
		{
			cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.DynamicVal,
			cty.False,
		},
		{
			cty.DynamicVal.Mark(marks.Sensitive),
			cty.True,
		},

		{
			cty.ListValEmpty(cty.String),
			cty.EmptyTupleVal,
		},
		{
			cty.ListValEmpty(cty.String).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("hello"),
				cty.StringVal("friend").Mark(marks.Sensitive),
			}),
			cty.TupleVal([]cty.Value{
				cty.False,
				cty.True,
			}),
		},
		{
			cty.SetValEmpty(cty.String),
			cty.EmptyTupleVal,
		},
		{
			cty.SetValEmpty(cty.String).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.SetVal([]cty.Value{cty.StringVal("hello")}),
			cty.TupleVal([]cty.Value{cty.False}),
		},
		{
			cty.SetVal([]cty.Value{cty.StringVal("hello").Mark(marks.Sensitive)}),
			cty.True,
		},
		{
			cty.EmptyTupleVal.Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.TupleVal([]cty.Value{
				cty.StringVal("hello"),
				cty.StringVal("friend").Mark(marks.Sensitive),
			}),
			cty.TupleVal([]cty.Value{
				cty.False,
				cty.True,
			}),
		},
		{
			cty.MapValEmpty(cty.String),
			cty.EmptyObjectVal,
		},
		{
			cty.MapValEmpty(cty.String).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.MapVal(map[string]cty.Value{
				"greeting": cty.StringVal("hello"),
				"animal":   cty.StringVal("horse"),
			}),
			cty.EmptyObjectVal,
		},
		{
			cty.MapVal(map[string]cty.Value{
				"greeting": cty.StringVal("hello"),
				"animal":   cty.StringVal("horse").Mark(marks.Sensitive),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"animal": cty.True,
			}),
		},
		{
			cty.MapVal(map[string]cty.Value{
				"greeting": cty.StringVal("hello"),
				"animal":   cty.StringVal("horse").Mark(marks.Sensitive),
			}).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.EmptyObjectVal,
			cty.EmptyObjectVal,
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"greeting": cty.StringVal("hello"),
				"animal":   cty.StringVal("horse"),
			}),
			cty.EmptyObjectVal,
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"greeting": cty.StringVal("hello"),
				"animal":   cty.StringVal("horse").Mark(marks.Sensitive),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"animal": cty.True,
			}),
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"greeting": cty.StringVal("hello"),
				"animal":   cty.StringVal("horse").Mark(marks.Sensitive),
			}).Mark(marks.Sensitive),
			cty.True,
		},
		{
			cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.UnknownVal(cty.String),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.StringVal("known").Mark(marks.Sensitive),
				}),
			}),
			cty.TupleVal([]cty.Value{
				cty.EmptyObjectVal,
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.True,
				}),
			}),
		},
		{
			cty.ListVal([]cty.Value{
				cty.MapValEmpty(cty.String),
				cty.MapVal(map[string]cty.Value{
					"a": cty.StringVal("known").Mark(marks.Sensitive),
				}),
				cty.MapVal(map[string]cty.Value{
					"a": cty.UnknownVal(cty.String),
				}),
			}),
			cty.TupleVal([]cty.Value{
				cty.EmptyObjectVal,
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.True,
				}),
				cty.EmptyObjectVal,
			}),
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"list":   cty.UnknownVal(cty.List(cty.String)),
				"set":    cty.UnknownVal(cty.Set(cty.Bool)),
				"tuple":  cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Number})),
				"map":    cty.UnknownVal(cty.Map(cty.String)),
				"object": cty.UnknownVal(cty.Object(map[string]cty.Type{"a": cty.String})),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"list":   cty.EmptyTupleVal,
				"set":    cty.EmptyTupleVal,
				"tuple":  cty.EmptyTupleVal,
				"map":    cty.EmptyObjectVal,
				"object": cty.EmptyObjectVal,
			}),
		},
	}

	for _, test := range tests {
		got := SensitiveAsBool(test.Input)
		if !reflect.DeepEqual(got, test.Want) {
			t.Errorf(
				"wrong result\ninput: %#v\ngot:   %#v\nwant:  %#v",
				test.Input, got, test.Want,
			)
		}
	}
}
