// Copyright 2016-2021, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package plugin

import (
	"fmt"
	"runtime"
	"testing"

	"github.com/stretchr/testify/assert"
	"google.golang.org/protobuf/types/known/structpb"

	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/archive"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/asset"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

func setProperty(key resource.PropertyKey, s *structpb.Value, k string, v interface{}) {
	marshaled, err := MarshalPropertyValue(key, resource.NewPropertyValue(v), MarshalOptions{})
	contract.Assertf(err == nil, "error marshaling property value")
	s.GetStructValue().Fields[k] = marshaled
}

func TestAssetSerialize(t *testing.T) {
	t.Parallel()

	// Ensure that asset and archive serialization round trips.
	text := "a test asset"
	pk := resource.PropertyKey("a test asset uri")
	anAsset, err := asset.FromText(text)
	assert.NoError(t, err)
	assert.Equal(t, text, anAsset.Text)
	assert.Equal(t, "e34c74529110661faae4e121e57165ff4cb4dbdde1ef9770098aa3695e6b6704", anAsset.Hash)
	assetProps, err := MarshalPropertyValue(pk, resource.NewAssetProperty(anAsset), MarshalOptions{})
	assert.NoError(t, err)
	t.Logf("%v", assetProps)
	assetValue, err := UnmarshalPropertyValue("", assetProps, MarshalOptions{})
	assert.NoError(t, err)
	assert.True(t, assetValue.IsAsset())
	assetDes := assetValue.AssetValue()
	assert.True(t, assetDes.IsText())
	assert.Equal(t, text, assetDes.Text)
	assert.Equal(t, "e34c74529110661faae4e121e57165ff4cb4dbdde1ef9770098aa3695e6b6704", assetDes.Hash)

	// Ensure that an invalid asset produces an error.
	setProperty(pk, assetProps, resource.AssetHashProperty, 0)
	_, err = UnmarshalPropertyValue("", assetProps, MarshalOptions{})
	assert.EqualError(t, err, "unexpected asset hash of type float64")
	setProperty(pk, assetProps, resource.AssetHashProperty, anAsset.Hash)

	setProperty(pk, assetProps, resource.AssetTextProperty, 0)
	_, err = UnmarshalPropertyValue("", assetProps, MarshalOptions{})
	assert.EqualError(t, err, "unexpected asset text of type float64")
	setProperty(pk, assetProps, resource.AssetTextProperty, "")

	setProperty(pk, assetProps, resource.AssetPathProperty, 0)
	_, err = UnmarshalPropertyValue("", assetProps, MarshalOptions{})
	assert.EqualError(t, err, "unexpected asset path of type float64")
	setProperty(pk, assetProps, resource.AssetPathProperty, "")

	setProperty(pk, assetProps, resource.AssetURIProperty, 0)
	_, err = UnmarshalPropertyValue("", assetProps, MarshalOptions{})
	assert.EqualError(t, err, "unexpected asset URI of type float64")
	setProperty(pk, assetProps, resource.AssetURIProperty, "")

	arch, err := archive.FromAssets(map[string]interface{}{"foo": anAsset})
	assert.NoError(t, err)
	switch runtime.Version() {
	case "go1.9":
		assert.Equal(t, "d8ce0142b3b10300c7c76487fad770f794c1e84e1b0c73a4b2e1503d4fbac093", arch.Hash)
	default:
		// Go 1.10 introduced breaking changes to archive/zip and archive/tar headers
		assert.Equal(t, "27ab4a14a617df10cff3e1cf4e30cf510302afe56bf4cc91f84041c9f7b62fd8", arch.Hash)
	}
	archProps, err := MarshalPropertyValue(pk, resource.NewArchiveProperty(arch), MarshalOptions{})
	assert.NoError(t, err)
	archValue, err := UnmarshalPropertyValue("", archProps, MarshalOptions{})
	assert.NoError(t, err)
	assert.True(t, archValue.IsArchive())
	archDes := archValue.ArchiveValue()
	assert.True(t, archDes.IsAssets())
	assert.Equal(t, 1, len(archDes.Assets))
	assert.True(t, archDes.Assets["foo"].(*asset.Asset).IsText())
	assert.Equal(t, text, archDes.Assets["foo"].(*asset.Asset).Text)
	switch runtime.Version() {
	case "go1.9":
		assert.Equal(t, "d8ce0142b3b10300c7c76487fad770f794c1e84e1b0c73a4b2e1503d4fbac093", archDes.Hash)
	default:
		// Go 1.10 introduced breaking changes to archive/zip and archive/tar headers
		assert.Equal(t, "27ab4a14a617df10cff3e1cf4e30cf510302afe56bf4cc91f84041c9f7b62fd8", archDes.Hash)
	}

	// Ensure that an invalid archive produces an error.
	setProperty(pk, archProps, resource.ArchiveHashProperty, 0)
	_, err = UnmarshalPropertyValue("", archProps, MarshalOptions{})
	assert.EqualError(t, err, "unexpected archive hash of type float64")
	setProperty(pk, archProps, resource.ArchiveHashProperty, arch.Hash)

	setProperty(pk, archProps, resource.ArchiveAssetsProperty, 0)
	_, err = UnmarshalPropertyValue("", archProps, MarshalOptions{})
	assert.EqualError(t, err, "unexpected archive contents of type float64")
	setProperty(pk, archProps, resource.ArchiveAssetsProperty, nil)

	setProperty(pk, archProps, resource.ArchivePathProperty, 0)
	_, err = UnmarshalPropertyValue("", archProps, MarshalOptions{})
	assert.EqualError(t, err, "unexpected archive path of type float64")
	setProperty(pk, archProps, resource.ArchivePathProperty, "")

	setProperty(pk, archProps, resource.ArchiveURIProperty, 0)
	_, err = UnmarshalPropertyValue("", archProps, MarshalOptions{})
	assert.EqualError(t, err, "unexpected archive URI of type float64")
	setProperty(pk, archProps, resource.ArchiveURIProperty, "")
}

func TestComputedSerialize(t *testing.T) {
	t.Parallel()

	// Ensure that computed properties survive round trips.
	opts := MarshalOptions{KeepUnknowns: true}
	pk := resource.PropertyKey("pk")
	{
		cprop, err := MarshalPropertyValue(pk,
			resource.NewComputedProperty(
				resource.Computed{Element: resource.NewStringProperty("")}), opts)
		assert.NoError(t, err)
		cpropU, err := UnmarshalPropertyValue(pk, cprop, opts)
		assert.NoError(t, err)
		assert.True(t, cpropU.IsComputed())
		assert.True(t, cpropU.Input().Element.IsString())
	}
	{
		cprop, err := MarshalPropertyValue(pk,
			resource.NewComputedProperty(
				resource.Computed{Element: resource.NewNumberProperty(0)}), opts)
		assert.NoError(t, err)
		cpropU, err := UnmarshalPropertyValue(pk, cprop, opts)
		assert.NoError(t, err)
		assert.True(t, cpropU.IsComputed())
		assert.True(t, cpropU.Input().Element.IsNumber())
	}
}

func TestComputedSkip(t *testing.T) {
	t.Parallel()

	// Ensure that computed properties are skipped when KeepUnknowns == false.
	opts := MarshalOptions{KeepUnknowns: false}
	pk := resource.PropertyKey("pk")
	{
		cprop, err := MarshalPropertyValue(pk,
			resource.NewComputedProperty(
				resource.Computed{Element: resource.NewStringProperty("")}), opts)
		assert.NoError(t, err)
		assert.Nil(t, cprop)
	}
	{
		cprop, err := MarshalPropertyValue(pk,
			resource.NewComputedProperty(
				resource.Computed{Element: resource.NewNumberProperty(0)}), opts)
		assert.NoError(t, err)
		assert.Nil(t, cprop)
	}
}

func TestComputedReject(t *testing.T) {
	t.Parallel()

	// Ensure that computed properties produce errors when RejectUnknowns == true.
	opts := MarshalOptions{RejectUnknowns: true}
	pk := resource.PropertyKey("pk")
	{
		cprop, err := MarshalPropertyValue(pk,
			resource.NewComputedProperty(
				resource.Computed{Element: resource.NewStringProperty("")}), opts)
		assert.EqualError(t, err, "unexpected unknown property value for \"pk\"")
		assert.Nil(t, cprop)
	}
	{
		cprop, err := MarshalPropertyValue(pk,
			resource.NewComputedProperty(
				resource.Computed{Element: resource.NewStringProperty("")}), MarshalOptions{KeepUnknowns: true})
		assert.NoError(t, err)
		cpropU, err := UnmarshalPropertyValue(pk, cprop, opts)
		assert.EqualError(t, err, "unexpected unknown property value for \"pk\"")
		assert.Nil(t, cpropU)
	}
}

func TestAssetReject(t *testing.T) {
	t.Parallel()

	// Ensure that asset and archive properties produce errors when RejectAssets == true.

	opts := MarshalOptions{RejectAssets: true}

	text := "a test asset"
	pk := resource.PropertyKey("an asset URI")
	asset, err := asset.FromText(text)
	assert.NoError(t, err)
	{
		assetProps, err := MarshalPropertyValue(pk, resource.NewAssetProperty(asset), opts)
		assert.EqualError(t, err, "unexpected Asset property value for \"an asset URI\"")
		assert.Nil(t, assetProps)
	}
	{
		assetProps, err := MarshalPropertyValue(pk, resource.NewAssetProperty(asset), MarshalOptions{})
		assert.NoError(t, err)
		assetPropU, err := UnmarshalPropertyValue(pk, assetProps, opts)
		assert.EqualError(t, err, "unexpected Asset property value for \"an asset URI\"")
		assert.Nil(t, assetPropU)
	}

	arch, err := archive.FromAssets(map[string]interface{}{"foo": asset})
	assert.NoError(t, err)
	{
		archProps, err := MarshalPropertyValue(pk, resource.NewArchiveProperty(arch), opts)
		assert.EqualError(t, err, "unexpected Asset Archive property value for \"an asset URI\"")
		assert.Nil(t, archProps)
	}
	{
		archProps, err := MarshalPropertyValue(pk, resource.NewArchiveProperty(arch), MarshalOptions{})
		assert.NoError(t, err)
		archValue, err := UnmarshalPropertyValue(pk, archProps, opts)
		assert.EqualError(t, err, "unexpected Asset property value for \"foo\"")
		assert.Nil(t, archValue)
	}
}

func TestUnsupportedSecret(t *testing.T) {
	t.Parallel()

	rawProp := resource.NewObjectProperty(resource.NewPropertyMapFromMap(map[string]interface{}{
		resource.SigKey: resource.SecretSig,
		"value":         "foo",
	}))
	pk := resource.PropertyKey("pk")
	prop, err := MarshalPropertyValue(pk, rawProp, MarshalOptions{})
	assert.NoError(t, err)
	val, err := UnmarshalPropertyValue(pk, prop, MarshalOptions{})
	assert.NoError(t, err)
	assert.True(t, val.IsString())
	assert.False(t, val.IsSecret())
	assert.Equal(t, "foo", val.StringValue())
}

func TestSupportedSecret(t *testing.T) {
	t.Parallel()

	rawProp := resource.NewObjectProperty(resource.NewPropertyMapFromMap(map[string]interface{}{
		resource.SigKey: resource.SecretSig,
		"value":         "foo",
	}))
	pk := resource.PropertyKey("pk")

	prop, err := MarshalPropertyValue(pk, rawProp, MarshalOptions{KeepSecrets: true})
	assert.NoError(t, err)
	val, err := UnmarshalPropertyValue(pk, prop, MarshalOptions{KeepSecrets: true})
	assert.NoError(t, err)
	assert.False(t, val.IsString())
	assert.True(t, val.IsSecret())
	assert.Equal(t, "foo", val.SecretValue().Element.StringValue())
}

func TestUnknownSig(t *testing.T) {
	t.Parallel()

	rawProp := resource.NewObjectProperty(resource.NewPropertyMapFromMap(map[string]interface{}{
		resource.SigKey: "foobar",
	}))
	pk := resource.PropertyKey("pk")

	prop, err := MarshalPropertyValue(pk, rawProp, MarshalOptions{})
	assert.NoError(t, err)
	_, err = UnmarshalPropertyValue(pk, prop, MarshalOptions{})
	assert.EqualError(t, err, "unrecognized signature 'foobar' in property map for \"pk\"")
}

func TestSkipInternalKeys(t *testing.T) {
	t.Parallel()

	opts := MarshalOptions{SkipInternalKeys: true}
	expected := &structpb.Struct{
		Fields: map[string]*structpb.Value{
			"keepers": {
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{},
					},
				},
			},
		},
	}
	props := resource.NewPropertyMapFromMap(map[string]interface{}{
		"__defaults": []string{},
		"keepers": map[string]interface{}{
			"__defaults": []string{},
		},
	})
	actual, err := MarshalProperties(props, opts)
	assert.NoError(t, err)
	assert.Equal(t, expected, actual)
}

func TestMarshalProperties(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		opts     MarshalOptions
		props    resource.PropertyMap
		expected *structpb.Struct
	}{
		{
			name: "empty (default)",
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{},
			},
		},
		{
			name: "unknown (default)",
			props: resource.PropertyMap{
				"foo": resource.MakeOutput(resource.NewStringProperty("")),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{},
			},
		},
		{
			name: "unknown with deps (default)",
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element:      resource.NewStringProperty(""),
					Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{},
			},
		},
		{
			name: "known (default)",
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element: resource.NewStringProperty("hello"),
					Known:   true,
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StringValue{
							StringValue: "hello",
						},
					},
				},
			},
		},
		{
			name: "known with deps (default)",
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element:      resource.NewStringProperty("hello"),
					Known:        true,
					Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StringValue{
							StringValue: "hello",
						},
					},
				},
			},
		},
		{
			name: "secret (default)",
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element: resource.NewStringProperty("hello"),
					Known:   true,
					Secret:  true,
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StringValue{
							StringValue: "hello",
						},
					},
				},
			},
		},
		{
			name: "secret with deps (default)",
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element:      resource.NewStringProperty("hello"),
					Known:        true,
					Secret:       true,
					Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StringValue{
							StringValue: "hello",
						},
					},
				},
			},
		},
		{
			name: "unknown secret (default)",
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element: resource.NewStringProperty("shhh"),
					Secret:  true,
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{},
			},
		},
		{
			name: "unknown secret with deps (default)",
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element:      resource.NewStringProperty("shhh"),
					Secret:       true,
					Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{},
			},
		},
		{
			name: "empty (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "unknown (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.MakeOutput(resource.NewStringProperty("")),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "unknown with deps (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element:      resource.NewStringProperty(""),
					Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
									"dependencies": {
										Kind: &structpb.Value_ListValue{
											ListValue: &structpb.ListValue{
												Values: []*structpb.Value{
													{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
													{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "known (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element: resource.NewStringProperty("hello"),
					Known:   true,
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
									"value": {
										Kind: &structpb.Value_StringValue{StringValue: "hello"},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "known with deps (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element:      resource.NewStringProperty("hello"),
					Known:        true,
					Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
									"value": {
										Kind: &structpb.Value_StringValue{StringValue: "hello"},
									},
									"dependencies": {
										Kind: &structpb.Value_ListValue{
											ListValue: &structpb.ListValue{
												Values: []*structpb.Value{
													{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
													{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "secret (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element: resource.NewStringProperty("shhh"),
					Known:   true,
					Secret:  true,
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
									"value": {
										Kind: &structpb.Value_StringValue{StringValue: "shhh"},
									},
									"secret": {
										Kind: &structpb.Value_BoolValue{BoolValue: true},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "secret with deps (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element:      resource.NewStringProperty("shhh"),
					Known:        true,
					Secret:       true,
					Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
									"value": {
										Kind: &structpb.Value_StringValue{StringValue: "shhh"},
									},
									"secret": {
										Kind: &structpb.Value_BoolValue{BoolValue: true},
									},
									"dependencies": {
										Kind: &structpb.Value_ListValue{
											ListValue: &structpb.ListValue{
												Values: []*structpb.Value{
													{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
													{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "unknown secret (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element: resource.NewStringProperty("shhh"),
					Secret:  true,
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
									"secret": {
										Kind: &structpb.Value_BoolValue{BoolValue: true},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "unknown secret with deps (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			props: resource.PropertyMap{
				"foo": resource.NewOutputProperty(resource.Output{
					Element:      resource.NewStringProperty("shhh"),
					Secret:       true,
					Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"foo": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
									"secret": {
										Kind: &structpb.Value_BoolValue{BoolValue: true},
									},
									"dependencies": {
										Kind: &structpb.Value_ListValue{
											ListValue: &structpb.ListValue{
												Values: []*structpb.Value{
													{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
													{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			actual, err := MarshalProperties(tt.props, tt.opts)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestResourceReference(t *testing.T) {
	t.Parallel()

	// Test round-trip
	opts := MarshalOptions{KeepResources: true}
	rawProp := resource.MakeCustomResourceReference("fakeURN", "fakeID", "fakeVersion")
	pk := resource.PropertyKey("pk")
	prop, err := MarshalPropertyValue(pk, rawProp, opts)
	assert.NoError(t, err)
	actual, err := UnmarshalPropertyValue(pk, prop, opts)
	assert.NoError(t, err)
	assert.Equal(t, rawProp, *actual)

	// Test unmarshaling as an ID
	opts.KeepResources = false
	actual, err = UnmarshalPropertyValue(pk, prop, opts)
	assert.NoError(t, err)
	assert.Equal(t, resource.NewStringProperty(rawProp.ResourceReferenceValue().ID.StringValue()), *actual)

	// Test marshaling as an ID
	prop, err = MarshalPropertyValue(pk, rawProp, opts)
	assert.NoError(t, err)
	opts.KeepResources = true
	actual, err = UnmarshalPropertyValue(pk, prop, opts)
	assert.NoError(t, err)
	assert.Equal(t, resource.NewStringProperty(rawProp.ResourceReferenceValue().ID.StringValue()), *actual)

	// Test unmarshaling as a URN
	rawProp = resource.MakeComponentResourceReference("fakeURN", "fakeVersion")
	prop, err = MarshalPropertyValue(pk, rawProp, opts)
	assert.NoError(t, err)
	opts.KeepResources = false
	actual, err = UnmarshalPropertyValue(pk, prop, opts)
	assert.NoError(t, err)
	assert.Equal(t, resource.NewStringProperty(string(rawProp.ResourceReferenceValue().URN)), *actual)

	// Test marshaling as a URN
	prop, err = MarshalPropertyValue(pk, rawProp, opts)
	assert.NoError(t, err)
	opts.KeepResources = true
	actual, err = UnmarshalPropertyValue(pk, prop, opts)
	assert.NoError(t, err)
	assert.Equal(t, resource.NewStringProperty(string(rawProp.ResourceReferenceValue().URN)), *actual)
}

func TestOutputValueRoundTrip(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		raw  resource.PropertyValue
	}{
		{
			name: "unknown",
			raw:  resource.NewOutputProperty(resource.Output{}),
		},
		{
			name: "unknown with deps",
			raw: resource.NewOutputProperty(resource.Output{
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
		},
		{
			name: "known",
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("hello"),
				Known:   true,
			}),
		},
		{
			name: "known with deps",
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("hello"),
				Known:        true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
		},
		{
			name: "secret",
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("secret"),
				Known:   true,
				Secret:  true,
			}),
		},
		{
			name: "secret with deps",
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("secret"),
				Known:        true,
				Secret:       true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
		},
		{
			name: "unknown secret",
			raw: resource.NewOutputProperty(resource.Output{
				Secret: true,
			}),
		},
		{
			name: "unknown secret with deps",
			raw: resource.NewOutputProperty(resource.Output{
				Secret:       true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			opts := MarshalOptions{KeepOutputValues: true}
			prop, err := MarshalPropertyValue("", tt.raw, opts)
			assert.NoError(t, err)
			actual, err := UnmarshalPropertyValue("", prop, opts)
			assert.NoError(t, err)
			assert.Equal(t, tt.raw, *actual)
		})
	}
}

func TestOutputValueMarshaling(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		opts     MarshalOptions
		raw      resource.PropertyValue
		expected *structpb.Value
	}{
		{
			name:     "empty (default)",
			raw:      resource.NewOutputProperty(resource.Output{}),
			expected: nil,
		},
		{
			name:     "unknown (default)",
			raw:      resource.MakeOutput(resource.NewStringProperty("")),
			expected: nil,
		},
		{
			name: "unknown with deps (default)",
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("hello"),
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: nil,
		},
		{
			name: "known (default)",
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("hello"),
				Known:   true,
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: "hello"},
			},
		},
		{
			name: "known with deps (default)",
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("hello"),
				Known:        true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: "hello"},
			},
		},
		{
			name: "secret (default)",
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("shhh"),
				Known:   true,
				Secret:  true,
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: "shhh"},
			},
		},
		{
			name: "secret with deps (default)",
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("shhh"),
				Known:        true,
				Secret:       true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: "shhh"},
			},
		},
		{
			name: "unknown secret (default)",
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("shhh"),
				Secret:  true,
			}),
			expected: nil,
		},
		{
			name: "unknown secret with deps (default)",
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("shhh"),
				Secret:       true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: nil,
		},
		{
			name: "empty (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw:  resource.NewOutputProperty(resource.Output{}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
			},
		},
		{
			name: "unknown (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw:  resource.MakeOutput(resource.NewStringProperty("")),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
			},
		},
		{
			name: "unknown with deps (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("hello"),
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
			},
		},
		{
			name: "unknown secret (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("shhh"),
				Secret:  true,
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
			},
		},
		{
			name: "unknown secret with deps (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("shhh"),
				Secret:       true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
			},
		},
		{
			name: "secret (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("shhh"),
				Known:   true,
				Secret:  true,
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: "shhh"},
			},
		},
		{
			name: "secret with deps (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("shhh"),
				Known:        true,
				Secret:       true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StringValue{StringValue: "shhh"},
			},
		},
		{
			name: "secret (KeepSecrets)",
			opts: MarshalOptions{KeepSecrets: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("shhh"),
				Known:   true,
				Secret:  true,
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.SecretSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "shhh"},
							},
						},
					},
				},
			},
		},
		{
			name: "secret with deps (KeepSecrets)",
			opts: MarshalOptions{KeepSecrets: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("shhh"),
				Known:        true,
				Secret:       true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.SecretSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "shhh"},
							},
						},
					},
				},
			},
		},
		{
			name: "unknown secret (KeepUnknowns, KeepSecrets)",
			opts: MarshalOptions{KeepUnknowns: true, KeepSecrets: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element: resource.NewStringProperty("shhh"),
				Secret:  true,
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.SecretSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
							},
						},
					},
				},
			},
		},
		{
			name: "unknown secret with deps (KeepUnknowns, KeepSecrets)",
			opts: MarshalOptions{KeepUnknowns: true, KeepSecrets: true},
			raw: resource.NewOutputProperty(resource.Output{
				Element:      resource.NewStringProperty("shhh"),
				Secret:       true,
				Dependencies: []resource.URN{"fakeURN1", "fakeURN2"},
			}),
			expected: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.SecretSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
							},
						},
					},
				},
			},
		},
		{
			name: "unknown value (KeepOutputValues)",
			opts: MarshalOptions{KeepOutputValues: true},
			raw:  resource.MakeOutput(resource.NewStringProperty("")),
			expected: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
						},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			actual, err := MarshalPropertyValue("", tt.raw, tt.opts)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestOutputValueUnmarshaling(t *testing.T) {
	t.Parallel()

	ptr := func(v resource.PropertyValue) *resource.PropertyValue {
		return &v
	}
	tests := []struct {
		name     string
		opts     MarshalOptions
		raw      *structpb.Value
		expected *resource.PropertyValue
	}{
		{
			name: "unknown (default)",
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
						},
					},
				},
			},
			expected: nil,
		},
		{
			name: "known (default)",
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "hello"},
							},
						},
					},
				},
			},
			expected: ptr(resource.NewStringProperty("hello")),
		},
		{
			name: "known with deps (default)",
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "hello"},
							},
							"dependencies": {
								Kind: &structpb.Value_ListValue{
									ListValue: &structpb.ListValue{
										Values: []*structpb.Value{
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: ptr(resource.NewStringProperty("hello")),
		},
		{
			name: "secret (default)",
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "shhh"},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
						},
					},
				},
			},
			expected: ptr(resource.NewStringProperty("shhh")),
		},
		{
			name: "secret with deps (default)",
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "shhh"},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
							"dependencies": {
								Kind: &structpb.Value_ListValue{
									ListValue: &structpb.ListValue{
										Values: []*structpb.Value{
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: ptr(resource.NewStringProperty("shhh")),
		},
		{
			name: "unknown secret (default)",
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
						},
					},
				},
			},
			expected: nil,
		},
		{
			name: "unknown secret with deps (default)",
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
							"dependencies": {
								Kind: &structpb.Value_ListValue{
									ListValue: &structpb.ListValue{
										Values: []*structpb.Value{
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: nil,
		},
		{
			name: "unknown (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
						},
					},
				},
			},
			expected: ptr(resource.MakeComputed(resource.NewStringProperty(""))),
		},
		{
			name: "unknown with deps (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"dependencies": {
								Kind: &structpb.Value_ListValue{
									ListValue: &structpb.ListValue{
										Values: []*structpb.Value{
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: ptr(resource.MakeComputed(resource.NewStringProperty(""))),
		},
		{
			name: "unknown secret (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
						},
					},
				},
			},
			expected: ptr(resource.MakeComputed(resource.NewStringProperty(""))),
		},
		{
			name: "unknown secret with deps (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
							"dependencies": {
								Kind: &structpb.Value_ListValue{
									ListValue: &structpb.ListValue{
										Values: []*structpb.Value{
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: ptr(resource.MakeComputed(resource.NewStringProperty(""))),
		},
		{
			name: "secret (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "shhh"},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
						},
					},
				},
			},
			expected: ptr(resource.NewStringProperty("shhh")),
		},
		{
			name: "secret with deps (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "shhh"},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
							"dependencies": {
								Kind: &structpb.Value_ListValue{
									ListValue: &structpb.ListValue{
										Values: []*structpb.Value{
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: ptr(resource.NewStringProperty("shhh")),
		},
		{
			name: "secret (KeepSecrets)",
			opts: MarshalOptions{KeepSecrets: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "shhh"},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
						},
					},
				},
			},
			expected: ptr(resource.MakeSecret(resource.NewStringProperty("shhh"))),
		},
		{
			name: "secret with deps (KeepSecrets)",
			opts: MarshalOptions{KeepSecrets: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"value": {
								Kind: &structpb.Value_StringValue{StringValue: "shhh"},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
							"dependencies": {
								Kind: &structpb.Value_ListValue{
									ListValue: &structpb.ListValue{
										Values: []*structpb.Value{
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: ptr(resource.MakeSecret(resource.NewStringProperty("shhh"))),
		},
		{
			name: "unknown secret (KeepUnknowns, KeepSecrets)",
			opts: MarshalOptions{KeepUnknowns: true, KeepSecrets: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
						},
					},
				},
			},
			expected: ptr(resource.MakeSecret(resource.MakeComputed(resource.NewStringProperty("")))),
		},
		{
			name: "unknown secret with deps (KeepUnknowns, KeepSecrets)",
			opts: MarshalOptions{KeepUnknowns: true, KeepSecrets: true},
			raw: &structpb.Value{
				Kind: &structpb.Value_StructValue{
					StructValue: &structpb.Struct{
						Fields: map[string]*structpb.Value{
							resource.SigKey: {
								Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
							},
							"secret": {
								Kind: &structpb.Value_BoolValue{BoolValue: true},
							},
							"dependencies": {
								Kind: &structpb.Value_ListValue{
									ListValue: &structpb.ListValue{
										Values: []*structpb.Value{
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN1"}},
											{Kind: &structpb.Value_StringValue{StringValue: "fakeURN2"}},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: ptr(resource.MakeSecret(resource.MakeComputed(resource.NewStringProperty("")))),
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			prop, err := UnmarshalPropertyValue("", tt.raw, tt.opts)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, prop)
		})
	}
}

func TestMarshalPropertiesDiscardsListNestedUnknowns(t *testing.T) {
	t.Parallel()

	proto := pbStruct("resources", pbList(pbString(UnknownStringValue)))

	propsWithUnknowns, err := UnmarshalProperties(proto, MarshalOptions{KeepUnknowns: true})
	if err != nil {
		t.Errorf("UnmarshalProperties failed: %v", err)
	}

	protobufStruct, err := MarshalProperties(propsWithUnknowns, MarshalOptions{KeepUnknowns: false})
	if err != nil {
		t.Error(err)
		return
	}

	protobufValue := &structpb.Value{
		Kind: &structpb.Value_StructValue{
			StructValue: protobufStruct,
		},
	}

	assertValidProtobufValue(t, protobufValue)
}

func pbString(s string) *structpb.Value {
	return &structpb.Value{
		Kind: &structpb.Value_StringValue{
			StringValue: s,
		},
	}
}

func pbStruct(name string, value *structpb.Value) *structpb.Struct {
	return &structpb.Struct{
		Fields: map[string]*structpb.Value{
			name: value,
		},
	}
}

func pbList(elems ...*structpb.Value) *structpb.Value {
	return &structpb.Value{
		Kind: &structpb.Value_ListValue{
			ListValue: &structpb.ListValue{Values: elems},
		},
	}
}

func assertValidProtobufValue(t *testing.T, value *structpb.Value) {
	err := walkValueSelfWithDescendants(value, "", func(path string, v *structpb.Value) error {
		return nil
	})
	if err != nil {
		t.Errorf("This is not a valid *structpb.Value: %v\n%v", value, err)
	}
}

func walkValueSelfWithDescendants(
	v *structpb.Value,
	path string,
	visit func(path string, v *structpb.Value) error,
) error {
	if v == nil {
		return fmt.Errorf("bad *structpb.Value nil at %s", path)
	}
	err := visit(path, v)
	if err != nil {
		return err
	}
	switch v.Kind.(type) {
	case *structpb.Value_ListValue:
		values := v.GetListValue().GetValues()
		for i, v := range values {
			err = walkValueSelfWithDescendants(v, fmt.Sprintf("%s[%d]", path, i), visit)
			if err != nil {
				return err
			}
		}
	case *structpb.Value_StructValue:
		s := v.GetStructValue()
		for fn, fv := range s.Fields {
			err = walkValueSelfWithDescendants(fv, fmt.Sprintf(`%s["%s"]`, path, fn), visit)
			if err != nil {
				return err
			}
		}
	case *structpb.Value_NumberValue:
		return nil
	case *structpb.Value_StringValue:
		return nil
	case *structpb.Value_BoolValue:
		return nil
	case *structpb.Value_NullValue:
		return nil
	default:
		return fmt.Errorf("bad *structpb.Value of unknown type at %s: %v", path, v)
	}
	return nil
}

func TestMarshalPropertiesDontSkipOutputs(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		opts     MarshalOptions
		props    resource.PropertyMap
		expected *structpb.Struct
	}{
		{
			name: "Computed (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			props: resource.PropertyMap{
				"message": resource.MakeComputed(resource.NewStringProperty("")),
				"nested": resource.NewObjectProperty(resource.PropertyMap{
					"value": resource.MakeComputed(resource.NewStringProperty("")),
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"message": {
						Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
					},
					"nested": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									"value": {
										Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "Output (KeepUnknowns)",
			opts: MarshalOptions{KeepUnknowns: true},
			props: resource.PropertyMap{
				"message": resource.NewOutputProperty(resource.Output{}),
				"nested": resource.NewObjectProperty(resource.PropertyMap{
					"value": resource.NewOutputProperty(resource.Output{}),
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"message": {
						Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
					},
					"nested": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									"value": {
										Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name: "Output (KeepUnknowns, KeepOutputValues)",
			opts: MarshalOptions{KeepUnknowns: true, KeepOutputValues: true},
			props: resource.PropertyMap{
				"message": resource.NewOutputProperty(resource.Output{}),
				"nested": resource.NewObjectProperty(resource.PropertyMap{
					"value": resource.NewOutputProperty(resource.Output{}),
				}),
			},
			expected: &structpb.Struct{
				Fields: map[string]*structpb.Value{
					"message": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									resource.SigKey: {
										Kind: &structpb.Value_StringValue{StringValue: resource.OutputValueSig},
									},
								},
							},
						},
					},
					"nested": {
						Kind: &structpb.Value_StructValue{
							StructValue: &structpb.Struct{
								Fields: map[string]*structpb.Value{
									"value": {
										Kind: &structpb.Value_StructValue{
											StructValue: &structpb.Struct{
												Fields: map[string]*structpb.Value{
													resource.SigKey: {
														Kind: &structpb.Value_StringValue{
															StringValue: resource.OutputValueSig,
														},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			actual, err := MarshalProperties(tt.props, tt.opts)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestUpgradeToOutputValues(t *testing.T) {
	t.Parallel()

	opts := MarshalOptions{
		KeepUnknowns: true, KeepSecrets: true, KeepOutputValues: true, KeepResources: true,
	}
	upgradeOpts := MarshalOptions{
		KeepUnknowns: true, KeepSecrets: true, KeepOutputValues: true, KeepResources: true, UpgradeToOutputValues: true,
	}

	pk := resource.PropertyKey("pk")
	// Unknown
	{
		prop, err := MarshalPropertyValue(pk,
			resource.NewComputedProperty(
				resource.Computed{Element: resource.NewStringProperty("")}), upgradeOpts)
		assert.NoError(t, err)
		propU, err := UnmarshalPropertyValue(pk, prop, opts)
		assert.NoError(t, err)
		assert.True(t, propU.IsOutput())
		assert.False(t, propU.OutputValue().Known)
		assert.False(t, propU.OutputValue().Secret)
	}
	{
		prop, err := MarshalPropertyValue(pk,
			resource.NewComputedProperty(
				resource.Computed{Element: resource.NewStringProperty("")}), opts)
		assert.NoError(t, err)
		propU, err := UnmarshalPropertyValue(pk, prop, upgradeOpts)
		assert.NoError(t, err)
		assert.True(t, propU.IsOutput())
		assert.False(t, propU.OutputValue().Known)
		assert.False(t, propU.OutputValue().Secret)
	}

	// Secrets
	{
		elem := resource.NewStringProperty("hello")
		prop, err := MarshalPropertyValue(pk,
			resource.NewSecretProperty(
				&resource.Secret{Element: elem}), upgradeOpts)
		assert.NoError(t, err)
		propU, err := UnmarshalPropertyValue(pk, prop, opts)
		assert.NoError(t, err)
		assert.True(t, propU.IsOutput())
		assert.True(t, propU.OutputValue().Known)
		assert.True(t, propU.OutputValue().Secret)
		assert.Equal(t, elem, propU.OutputValue().Element)
	}
	{
		elem := resource.NewStringProperty("hello")
		prop, err := MarshalPropertyValue(pk,
			resource.NewSecretProperty(
				&resource.Secret{Element: elem}), opts)
		assert.NoError(t, err)
		propU, err := UnmarshalPropertyValue(pk, prop, upgradeOpts)
		assert.NoError(t, err)
		assert.True(t, propU.IsOutput())
		assert.True(t, propU.OutputValue().Known)
		assert.True(t, propU.OutputValue().Secret)
		assert.Equal(t, elem, propU.OutputValue().Element)
	}

	// Resource reference
	{
		elem := resource.ResourceReference{
			URN: resource.CreateURN("name", "type", "", "project", "stack"),
			ID:  resource.MakeComputed(resource.NewStringProperty("")),
		}
		prop, err := MarshalPropertyValue(pk, resource.NewResourceReferenceProperty(elem), upgradeOpts)
		assert.NoError(t, err)
		propU, err := UnmarshalPropertyValue(pk, prop, opts)
		assert.NoError(t, err)
		assert.True(t, propU.IsResourceReference())
		assert.Equal(t, elem.URN, propU.ResourceReferenceValue().URN)
		id := propU.ResourceReferenceValue().ID
		// ResourceReferences are always Computed even if output upgrades are turned on.
		assert.True(t, id.IsComputed())
	}
	{
		elem := resource.ResourceReference{
			URN: resource.CreateURN("name", "type", "", "project", "stack"),
			ID:  resource.MakeComputed(resource.NewStringProperty("")),
		}
		prop, err := MarshalPropertyValue(pk, resource.NewResourceReferenceProperty(elem), opts)
		assert.NoError(t, err)
		propU, err := UnmarshalPropertyValue(pk, prop, upgradeOpts)
		assert.NoError(t, err)
		assert.True(t, propU.IsResourceReference())
		assert.Equal(t, elem.URN, propU.ResourceReferenceValue().URN)
		id := propU.ResourceReferenceValue().ID
		assert.True(t, id.IsComputed())
	}

	// Some SDKs send the unknown string value instead of "" for the unknown ID
	{
		urn := string(resource.CreateURN("name", "type", "", "project", "stack"))
		prop := &structpb.Value{
			Kind: &structpb.Value_StructValue{
				StructValue: &structpb.Struct{
					Fields: map[string]*structpb.Value{
						resource.SigKey: {
							Kind: &structpb.Value_StringValue{StringValue: resource.ResourceReferenceSig},
						},
						"urn": {
							Kind: &structpb.Value_StringValue{StringValue: urn},
						},
						"id": {
							Kind: &structpb.Value_StringValue{StringValue: UnknownStringValue},
						},
					},
				},
			},
		}

		propU, err := UnmarshalPropertyValue(pk, prop, upgradeOpts)
		assert.NoError(t, err)
		assert.True(t, propU.IsResourceReference())
		assert.Equal(t, resource.URN(urn), propU.ResourceReferenceValue().URN)
		id := propU.ResourceReferenceValue().ID
		assert.True(t, id.IsComputed())
	}
}