// Copyright 2016-2020, 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 importer

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	"github.com/hashicorp/hcl/v2"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
	"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
	"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
	"github.com/pulumi/pulumi/pkg/v3/codegen/testing/utils"
	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
	"github.com/pulumi/pulumi/pkg/v3/resource/stack"
	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/stretchr/testify/assert"
	"github.com/zclconf/go-cty/cty"
)

var testdataPath = filepath.Join("..", "codegen", "testing", "test", "testdata")

const (
	parentName   = "parent"
	providerName = "provider"
	logicalName  = "logical"
)

var (
	parentURN   = resource.NewURN("stack", "project", "", "my::parent", "parent")
	providerURN = resource.NewURN("stack", "project", "", providers.MakeProviderType("pkg"), "provider")
	logicalURN  = resource.NewURN("stack", "project", "", "random:index/randomId:RandomId", "strange logical name")
)

var names = NameTable{
	parentURN:   parentName,
	providerURN: providerName,
	logicalURN:  logicalName,
}

func renderExpr(t *testing.T, x model.Expression) resource.PropertyValue {
	switch x := x.(type) {
	case *model.LiteralValueExpression:
		return renderLiteralValue(t, x)
	case *model.ScopeTraversalExpression:
		return renderScopeTraversal(t, x)
	case *model.TemplateExpression:
		return renderTemplate(t, x)
	case *model.TupleConsExpression:
		return renderTupleCons(t, x)
	case *model.ObjectConsExpression:
		return renderObjectCons(t, x)
	case *model.FunctionCallExpression:
		return renderFunctionCall(t, x)
	default:
		assert.Failf(t, "", "unexpected expression of type %T", x)
		return resource.NewNullProperty()
	}
}

func renderLiteralValue(t *testing.T, x *model.LiteralValueExpression) resource.PropertyValue {
	switch x.Value.Type() {
	case cty.Bool:
		return resource.NewBoolProperty(x.Value.True())
	case cty.Number:
		f, _ := x.Value.AsBigFloat().Float64()
		return resource.NewNumberProperty(f)
	case cty.String:
		return resource.NewStringProperty(x.Value.AsString())
	default:
		assert.Failf(t, "", "unexpected literal of type %v", x.Value.Type())
		return resource.NewNullProperty()
	}
}

func renderTemplate(t *testing.T, x *model.TemplateExpression) resource.PropertyValue {
	if len(x.Parts) == 1 {
		return renderLiteralValue(t, x.Parts[0].(*model.LiteralValueExpression))
	}
	b := ""
	for _, p := range x.Parts {
		b += p.(*model.LiteralValueExpression).Value.AsString()
	}
	return resource.NewStringProperty(b)
}

func renderObjectCons(t *testing.T, x *model.ObjectConsExpression) resource.PropertyValue {
	obj := resource.PropertyMap{}
	for _, item := range x.Items {
		kv := renderExpr(t, item.Key)
		if !assert.True(t, kv.IsString()) {
			continue
		}
		obj[resource.PropertyKey(kv.StringValue())] = renderExpr(t, item.Value)
	}
	return resource.NewObjectProperty(obj)
}

func renderScopeTraversal(t *testing.T, x *model.ScopeTraversalExpression) resource.PropertyValue {
	if !assert.Len(t, x.Traversal, 1) {
		return resource.NewNullProperty()
	}

	switch x.RootName {
	case "parent":
		return resource.NewStringProperty(string(parentURN))
	case "provider":
		return resource.NewStringProperty(string(providerURN))
	default:
		assert.Failf(t, "", "unexpected variable reference %v", x.RootName)
		return resource.NewNullProperty()
	}
}

func renderTupleCons(t *testing.T, x *model.TupleConsExpression) resource.PropertyValue {
	arr := make([]resource.PropertyValue, len(x.Expressions))
	for i, x := range x.Expressions {
		arr[i] = renderExpr(t, x)
	}
	return resource.NewArrayProperty(arr)
}

func renderFunctionCall(t *testing.T, x *model.FunctionCallExpression) resource.PropertyValue {
	switch x.Name {
	case "fileArchive":
		if !assert.Len(t, x.Args, 1) {
			return resource.NewNullProperty()
		}
		expr := renderExpr(t, x.Args[0])
		if !assert.True(t, expr.IsString()) {
			return resource.NewNullProperty()
		}
		return resource.NewStringProperty(expr.StringValue())
	case "fileAsset":
		if !assert.Len(t, x.Args, 1) {
			return resource.NewNullProperty()
		}
		expr := renderExpr(t, x.Args[0])
		if !assert.True(t, expr.IsString()) {
			return resource.NewNullProperty()
		}
		return resource.NewStringProperty(expr.StringValue())
	case "secret":
		if !assert.Len(t, x.Args, 1) {
			return resource.NewNullProperty()
		}
		return resource.MakeSecret(renderExpr(t, x.Args[0]))
	default:
		assert.Failf(t, "", "unexpected call to %v", x.Name)
		return resource.NewNullProperty()
	}
}

func renderResource(t *testing.T, r *pcl.Resource) *resource.State {
	inputs := resource.PropertyMap{}
	for _, attr := range r.Inputs {
		inputs[resource.PropertyKey(attr.Name)] = renderExpr(t, attr.Value)
	}

	protect := false
	var parent resource.URN
	var providerRef string
	if r.Options != nil {
		if r.Options.Protect != nil {
			v, diags := r.Options.Protect.Evaluate(&hcl.EvalContext{})
			if assert.Len(t, diags, 0) && assert.Equal(t, cty.Bool, v.Type()) {
				protect = v.True()
			}
		}
		if r.Options.Parent != nil {
			v := renderExpr(t, r.Options.Parent)
			if assert.True(t, v.IsString()) {
				parent = resource.URN(v.StringValue())
			}
		}
		if r.Options.Provider != nil {
			v := renderExpr(t, r.Options.Provider)
			if assert.True(t, v.IsString()) {
				providerRef = v.StringValue() + "::id"
			}
		}
	}

	// Pull the raw token from the resource.
	token := tokens.Type(r.Definition.Labels[1])

	var parentType tokens.Type
	if parent != "" {
		parentType = parent.QualifiedType()
	}
	return &resource.State{
		Type:     token,
		URN:      resource.NewURN("stack", "project", parentType, token, r.LogicalName()),
		Custom:   true,
		Inputs:   inputs,
		Parent:   parent,
		Provider: providerRef,
		Protect:  protect,
	}
}

type testCases struct {
	Resources []apitype.ResourceV3 `json:"resources"`
}

func readTestCases(path string) (testCases, error) {
	f, err := os.Open(path)
	if err != nil {
		return testCases{}, err
	}
	defer contract.IgnoreClose(f)

	var cases testCases
	if err = json.NewDecoder(f).Decode(&cases); err != nil {
		return testCases{}, err
	}
	return cases, nil
}

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

	loader := schema.NewPluginLoader(utils.NewHost(testdataPath))
	cases, err := readTestCases("testdata/cases.json")
	if !assert.NoError(t, err) {
		t.Fatal()
	}

	//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
	for _, s := range cases.Resources {
		s := s
		t.Run(string(s.URN), func(t *testing.T) {
			state, err := stack.DeserializeResource(s, config.NopDecrypter, config.NopEncrypter)
			if !assert.NoError(t, err) {
				t.Fatal()
			}

			importState := ImportState{
				Names: names,
			}

			block, err := GenerateHCL2Definition(loader, state, importState)
			if !assert.NoError(t, err) {
				t.Fatal()
			}

			text := fmt.Sprintf("%v", block)

			parser := syntax.NewParser()
			err = parser.ParseFile(strings.NewReader(text), string(state.URN)+".pp")
			if !assert.NoError(t, err) || !assert.False(t, parser.Diagnostics.HasErrors()) {
				t.Fatal()
			}

			p, diags, err := pcl.BindProgram(parser.Files, pcl.Loader(loader), pcl.AllowMissingVariables)
			assert.NoError(t, err)
			assert.False(t, diags.HasErrors())

			if !assert.Len(t, p.Nodes, 1) {
				t.Fatal()
			}

			res, isResource := p.Nodes[0].(*pcl.Resource)
			if !assert.True(t, isResource) {
				t.Fatal()
			}

			actualState := renderResource(t, res)

			assert.Equal(t, state.Type, actualState.Type)
			assert.Equal(t, state.URN, actualState.URN)
			assert.Equal(t, state.Parent, actualState.Parent)
			assert.Equal(t, state.Provider, actualState.Provider)
			assert.Equal(t, state.Protect, actualState.Protect)
			if !assert.True(t, actualState.Inputs.DeepEquals(state.Inputs)) {
				actual, err := stack.SerializeResource(context.Background(), actualState, config.NopEncrypter, false)
				contract.IgnoreError(err)

				sb, err := json.MarshalIndent(s, "", "    ")
				contract.IgnoreError(err)

				ab, err := json.MarshalIndent(actual, "", "    ")
				contract.IgnoreError(err)

				t.Logf("%v", text)
				// We know this will fail, but we want the diff
				assert.Equal(t, string(sb), string(ab))
			}
		})
	}
}

func TestGenerateHCL2DefinitionsWithDependantResources(t *testing.T) {
	t.Parallel()
	loader := schema.NewPluginLoader(utils.NewHost(testdataPath))

	resources := []apitype.ResourceV3{
		{
			URN:    "urn:pulumi:stack::project::aws:s3/bucket:Bucket::exampleBucket",
			ID:     "provider-generated-bucket-id-abc123",
			Custom: true,
			Type:   "aws:s3/bucket:Bucket",
		},
		{
			URN:    "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::exampleBucketObject",
			ID:     "provider-generated-bucket-object-id-abc123",
			Custom: true,
			Type:   "aws:s3/bucketObject:BucketObject",
			Inputs: map[string]interface{}{
				// this will be replaced with a reference to exampleBucket.id in the generated code
				"bucket":       "provider-generated-bucket-id-abc123",
				"storageClass": "STANDARD",
			},
		},
	}

	states := make([]*resource.State, 0)
	for _, r := range resources {
		state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter)
		if !assert.NoError(t, err) {
			t.Fatal()
		}
		states = append(states, state)
	}

	importState := createImportState(states, names)

	var hcl2Text strings.Builder
	for i, state := range states {
		hcl2Def, err := GenerateHCL2Definition(loader, state, importState)
		if err != nil {
			t.Fatal(err)
		}

		pre := ""
		if i > 0 {
			pre = "\n"
		}
		_, err = fmt.Fprintf(&hcl2Text, "%s%v", pre, hcl2Def)
		contract.IgnoreError(err)
	}

	expectedCode := `resource exampleBucket "aws:s3/bucket:Bucket" {

}

resource exampleBucketObject "aws:s3/bucketObject:BucketObject" {
    bucket = exampleBucket.id
    storageClass = "STANDARD"

}
`

	assert.Equal(t, expectedCode, hcl2Text.String(), "Generated HCL2 code does not match expected code")
}

func TestGenerateHCL2DefinitionsWithAmbiguousReferencesMaintainsLiteralValue(t *testing.T) {
	t.Parallel()
	loader := schema.NewPluginLoader(utils.NewHost(testdataPath))

	resources := []apitype.ResourceV3{
		{
			URN:    "urn:pulumi:stack::project::aws:s3/bucket:Bucket::firstBucket",
			ID:     "provider-generated-bucket-id-abc123",
			Custom: true,
			Type:   "aws:s3/bucket:Bucket",
		},
		{
			URN:    "urn:pulumi:stack::project::aws:s3/bucket:Bucket::secondBucket",
			ID:     "provider-generated-bucket-id-abc123",
			Custom: true,
			Type:   "aws:s3/bucket:Bucket",
		},
		{
			URN:    "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::exampleBucketObject",
			ID:     "provider-generated-bucket-object-id-abc123",
			Custom: true,
			Type:   "aws:s3/bucketObject:BucketObject",
			Inputs: map[string]interface{}{
				// this will *NOT* be replaced with a reference to either firstBucket.id or secondBucket.id
				// because both have the same ID and it would be ambiguous
				"bucket":       "provider-generated-bucket-id-abc123",
				"storageClass": "STANDARD",
			},
		},
	}

	states := make([]*resource.State, 0)
	for _, r := range resources {
		state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter)
		if !assert.NoError(t, err) {
			t.Fatal()
		}
		states = append(states, state)
	}

	importState := createImportState(states, names)

	var hcl2Text strings.Builder
	for i, state := range states {
		hcl2Def, err := GenerateHCL2Definition(loader, state, importState)
		if err != nil {
			t.Fatal(err)
		}

		pre := ""
		if i > 0 {
			pre = "\n"
		}
		_, err = fmt.Fprintf(&hcl2Text, "%s%v", pre, hcl2Def)
		contract.IgnoreError(err)
	}

	expectedCode := `resource firstBucket "aws:s3/bucket:Bucket" {

}

resource secondBucket "aws:s3/bucket:Bucket" {

}

resource exampleBucketObject "aws:s3/bucketObject:BucketObject" {
    bucket = "provider-generated-bucket-id-abc123"
    storageClass = "STANDARD"

}
`

	assert.Equal(t, expectedCode, hcl2Text.String(), "Generated HCL2 code does not match expected code")
}

func TestGenerateHCL2DefinitionsDoesNotMakeSelfReferences(t *testing.T) {
	t.Parallel()
	loader := schema.NewPluginLoader(utils.NewHost(testdataPath))

	resources := []apitype.ResourceV3{
		{
			URN:    "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::exampleBucketObject",
			ID:     "provider-generated-bucket-object-id-abc123",
			Custom: true,
			Type:   "aws:s3/bucketObject:BucketObject",
			Inputs: map[string]interface{}{
				// this literal value will stay as is since it shouldn't self-reference the bucket object itself
				"bucket":       "provider-generated-bucket-object-id-abc123",
				"storageClass": "STANDARD",
			},
		},
	}

	states := make([]*resource.State, 0)
	for _, r := range resources {
		state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter)
		if !assert.NoError(t, err) {
			t.Fatal()
		}
		states = append(states, state)
	}

	importState := createImportState(states, names)

	var hcl2Text strings.Builder
	for i, state := range states {
		hcl2Def, err := GenerateHCL2Definition(loader, state, importState)
		if err != nil {
			t.Fatal(err)
		}

		pre := ""
		if i > 0 {
			pre = "\n"
		}
		_, err = fmt.Fprintf(&hcl2Text, "%s%v", pre, hcl2Def)
		contract.IgnoreError(err)
	}

	expectedCode := `resource exampleBucketObject "aws:s3/bucketObject:BucketObject" {
    bucket = "provider-generated-bucket-object-id-abc123"
    storageClass = "STANDARD"

}
`

	assert.Equal(t, expectedCode, hcl2Text.String(), "Generated HCL2 code does not match expected code")
}

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

	types := []schema.Type{
		schema.BoolType,
		schema.IntType,
		schema.NumberType,
		schema.StringType,
		schema.AssetType,
		schema.ArchiveType,
		schema.JSONType,
		&schema.ArrayType{ElementType: schema.BoolType},
		&schema.ArrayType{ElementType: schema.IntType},
		&schema.MapType{ElementType: schema.BoolType},
		&schema.MapType{ElementType: schema.IntType},
		&schema.ObjectType{},
		&schema.ObjectType{
			Properties: []*schema.Property{
				{
					Name: "foo",
					Type: schema.BoolType,
				},
			},
		},
		&schema.ObjectType{
			Properties: []*schema.Property{
				{
					Name: "foo",
					Type: schema.IntType,
				},
			},
		},
		&schema.ObjectType{
			Properties: []*schema.Property{
				{
					Name: "foo",
					Type: schema.IntType,
				},
				{
					Name: "bar",
					Type: schema.IntType,
				},
			},
		},
		&schema.UnionType{ElementTypes: []schema.Type{schema.BoolType, schema.IntType}},
		&schema.UnionType{ElementTypes: []schema.Type{schema.IntType, schema.JSONType}},
		&schema.UnionType{ElementTypes: []schema.Type{schema.NumberType, schema.StringType}},
		schema.AnyType,
	}

	assert.True(t, sort.SliceIsSorted(types, func(i, j int) bool {
		return simplerType(types[i], types[j])
	}))
}

func makeUnionType(types ...schema.Type) *schema.UnionType {
	return &schema.UnionType{ElementTypes: types}
}

func makeArrayType(elementType schema.Type) *schema.ArrayType {
	return &schema.ArrayType{ElementType: elementType}
}

func makeProperty(name string, t schema.Type) *schema.Property {
	return &schema.Property{Name: name, Type: t}
}

func makeObjectType(properties ...*schema.Property) *schema.ObjectType {
	return &schema.ObjectType{Properties: properties}
}

func makeOptionalType(t schema.Type) schema.Type {
	return &schema.OptionalType{ElementType: t}
}

func makeObject(input map[string]resource.PropertyValue) resource.PropertyValue {
	properties := make(map[resource.PropertyKey]resource.PropertyValue)
	for key, value := range input {
		properties[resource.PropertyKey(key)] = value
	}

	return resource.NewObjectProperty(properties)
}

func TestStructuralTypeChecks(t *testing.T) {
	t.Run("String", func(t *testing.T) {
		t.Parallel()
		value := resource.NewStringProperty("foo")
		assert.True(t, valueStructurallyTypedAs(value, schema.StringType))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType)))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType, schema.NumberType)))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, schema.StringType)))

		assert.False(t, valueStructurallyTypedAs(value, schema.BoolType))
		assert.False(t, valueStructurallyTypedAs(value, schema.NumberType))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.BoolType, schema.NumberType)))
	})

	t.Run("Bool", func(t *testing.T) {
		t.Parallel()
		value := resource.NewBoolProperty(true)
		assert.True(t, valueStructurallyTypedAs(value, schema.BoolType))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.BoolType)))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.BoolType, schema.NumberType)))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, schema.BoolType)))

		assert.False(t, valueStructurallyTypedAs(value, schema.StringType))
		assert.False(t, valueStructurallyTypedAs(value, schema.NumberType))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType, schema.NumberType)))
	})

	t.Run("Number", func(t *testing.T) {
		t.Parallel()
		value := resource.NewNumberProperty(42)
		assert.True(t, valueStructurallyTypedAs(value, schema.NumberType))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType)))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, schema.StringType)))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType, schema.NumberType)))

		assert.False(t, valueStructurallyTypedAs(value, schema.StringType))
		assert.False(t, valueStructurallyTypedAs(value, schema.BoolType))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.StringType, schema.BoolType)))
	})

	t.Run("Array", func(t *testing.T) {
		t.Parallel()
		value := resource.NewArrayProperty([]resource.PropertyValue{
			resource.NewStringProperty("foo"),
			resource.NewStringProperty("bar"),
		})

		assert.True(t, valueStructurallyTypedAs(value, makeArrayType(schema.StringType)))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.StringType))))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.StringType), schema.NumberType)))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, makeArrayType(schema.StringType))))
		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(
			makeArrayType(schema.StringType),
			makeArrayType(schema.NumberType))))

		assert.True(t, valueStructurallyTypedAs(value, makeUnionType(
			makeArrayType(schema.NumberType),
			makeArrayType(schema.StringType))))

		assert.False(t, valueStructurallyTypedAs(value, makeArrayType(schema.BoolType)))
		assert.False(t, valueStructurallyTypedAs(value, makeArrayType(schema.NumberType)))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.BoolType), schema.NumberType)))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, makeArrayType(schema.BoolType))))
	})

	t.Run("ArrayMixedTypes", func(t *testing.T) {
		t.Parallel()
		value := resource.NewArrayProperty([]resource.PropertyValue{
			resource.NewStringProperty("foo"),
			resource.NewNumberProperty(42),
		})

		// base case: value of type array[union[string, number]]
		assert.True(t, valueStructurallyTypedAs(value, makeArrayType(makeUnionType(schema.StringType, schema.NumberType))))

		assert.False(t, valueStructurallyTypedAs(value, makeArrayType(schema.NumberType)))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.StringType))))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.NumberType))))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(makeArrayType(schema.StringType), schema.NumberType)))
		assert.False(t, valueStructurallyTypedAs(value, makeUnionType(schema.NumberType, makeArrayType(schema.StringType))))
	})

	t.Run("Object", func(t *testing.T) {
		t.Parallel()

		value := makeObject(map[string]resource.PropertyValue{
			"foo": resource.NewStringProperty("foo"),
			"bar": resource.NewNumberProperty(42),
		})

		assert.True(t, valueStructurallyTypedAs(value, makeObjectType(
			makeProperty("foo", schema.StringType),
			makeProperty("bar", schema.NumberType),
		)))

		assert.False(t, valueStructurallyTypedAs(value, makeObjectType(
			makeProperty("foo", schema.StringType),
			makeProperty("bar", schema.StringType),
		)))

		anotherValue := makeObject(map[string]resource.PropertyValue{
			"a": resource.NewStringProperty("A"),
		})

		// property "a" is missing from the type
		assert.False(t, valueStructurallyTypedAs(anotherValue, makeObjectType(
			makeProperty("b", schema.StringType),
		)))

		objectA := makeObject(map[string]resource.PropertyValue{
			"foo": resource.NewStringProperty("foo"),
		})

		objectATypeWithRequiredPropertyBar := makeObjectType(
			makeProperty("foo", schema.StringType),
			makeProperty("bar", schema.NumberType))

		// property "bar" is missing from the value
		// but the type requires it to be present
		// so the value is _not_ structurally typed
		assert.False(t, valueStructurallyTypedAs(objectA, objectATypeWithRequiredPropertyBar))

		objectATypeWithOptionalPropertyBar := makeObjectType(
			makeProperty("foo", schema.StringType),
			makeProperty("bar", makeOptionalType(schema.NumberType)))

		// property "bar" is missing from the value, but it is optional
		// so the value is structurally typed just fine
		assert.True(t, valueStructurallyTypedAs(objectA, objectATypeWithOptionalPropertyBar))

		complexUnionOfObjects := makeUnionType(
			makeObjectType(
				makeProperty("foo", schema.StringType),
				makeProperty("bar", schema.NumberType),
			),
			makeObjectType(
				makeProperty("foo", makeUnionType(schema.NumberType, schema.StringType)),
			))

		// fits the second object of the union
		complexFittingValue := makeObject(map[string]resource.PropertyValue{
			"foo": resource.NewNumberProperty(100),
		})

		assert.True(t, valueStructurallyTypedAs(complexFittingValue, complexUnionOfObjects))
	})
}

func TestReduceUnionTypeEliminatesUnionsBasicCase(t *testing.T) {
	t.Parallel()
	value := resource.NewStringProperty("hello")
	unionTypeA := makeUnionType(schema.StringType, schema.NumberType)
	unionTypeB := makeUnionType(schema.BoolType, schema.StringType)
	reducedA := reduceUnionType(unionTypeA, value)
	reducedB := reduceUnionType(unionTypeB, value)
	assert.Equal(t, schema.StringType, reducedA)
	assert.Equal(t, schema.StringType, reducedB)
}

func TestReduceUnionTypeEliminatesUnionsRecursively(t *testing.T) {
	t.Parallel()
	value := resource.NewStringProperty("hello")
	unionType := makeUnionType(
		makeUnionType(schema.NumberType, schema.BoolType),
		makeUnionType(
			makeUnionType(
				schema.StringType,
				schema.BoolType)))

	reduced := reduceUnionType(unionType, value)
	assert.Equal(t, schema.StringType, reduced)
}

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

	// array[union[string, number]]
	mixedTypeArray := resource.NewArrayProperty([]resource.PropertyValue{
		resource.NewStringProperty("hello"),
		resource.NewNumberProperty(42),
	})

	// union[array[union[string, number]]]
	arrayType := makeUnionType(&schema.ArrayType{
		ElementType: makeUnionType(schema.StringType, schema.NumberType),
	})

	reduced := reduceUnionType(arrayType, mixedTypeArray)
	schemaArrayType, isArray := reduced.(*schema.ArrayType)
	assert.True(t, isArray)
	unionType, isUnion := schemaArrayType.ElementType.(*schema.UnionType)
	assert.True(t, isUnion)
	assert.Equal(t, schema.StringType, unionType.ElementTypes[0])
	assert.Equal(t, schema.NumberType, unionType.ElementTypes[1])
}