package configschema

import (
	"sort"
	"testing"

	"github.com/apparentlymart/go-dump/dump"
	"github.com/davecgh/go-spew/spew"
	"github.com/google/go-cmp/cmp"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/hashicorp/hcl/v2/hcltest"
	"github.com/zclconf/go-cty/cty"
)

func TestBlockDecoderSpec(t *testing.T) {
	tests := map[string]struct {
		Schema    *Block
		TestBody  hcl.Body
		Want      cty.Value
		DiagCount int
	}{
		"empty": {
			&Block{},
			hcl.EmptyBody(),
			cty.EmptyObjectVal,
			0,
		},
		"nil": {
			nil,
			hcl.EmptyBody(),
			cty.EmptyObjectVal,
			0,
		},
		"attributes": {
			&Block{
				Attributes: map[string]*Attribute{
					"optional": {
						Type:     cty.Number,
						Optional: true,
					},
					"required": {
						Type:     cty.String,
						Required: true,
					},
					"computed": {
						Type:     cty.List(cty.Bool),
						Computed: true,
					},
					"optional_computed": {
						Type:     cty.Map(cty.Bool),
						Optional: true,
						Computed: true,
					},
					"optional_computed_overridden": {
						Type:     cty.Bool,
						Optional: true,
						Computed: true,
					},
					"optional_computed_unknown": {
						Type:     cty.String,
						Optional: true,
						Computed: true,
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"required": {
						Name: "required",
						Expr: hcltest.MockExprLiteral(cty.NumberIntVal(5)),
					},
					"optional_computed_overridden": {
						Name: "optional_computed_overridden",
						Expr: hcltest.MockExprLiteral(cty.True),
					},
					"optional_computed_unknown": {
						Name: "optional_computed_overridden",
						Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.String)),
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"optional":                     cty.NullVal(cty.Number),
				"required":                     cty.StringVal("5"), // converted from number to string
				"computed":                     cty.NullVal(cty.List(cty.Bool)),
				"optional_computed":            cty.NullVal(cty.Map(cty.Bool)),
				"optional_computed_overridden": cty.True,
				"optional_computed_unknown":    cty.UnknownVal(cty.String),
			}),
			0,
		},
		"dynamically-typed attribute": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.DynamicPseudoType, // any type is permitted
						Required: true,
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"foo": {
						Name: "foo",
						Expr: hcltest.MockExprLiteral(cty.True),
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.True,
			}),
			0,
		},
		"dynamically-typed attribute omitted": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.DynamicPseudoType, // any type is permitted
						Optional: true,
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.DynamicPseudoType),
			}),
			0,
		},
		"required attribute omitted": {
			&Block{
				Attributes: map[string]*Attribute{
					"foo": {
						Type:     cty.Bool,
						Required: true,
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.Bool),
			}),
			1, // missing required attribute
		},
		"wrong attribute type": {
			&Block{
				Attributes: map[string]*Attribute{
					"optional": {
						Type:     cty.Number,
						Optional: true,
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"optional": {
						Name: "optional",
						Expr: hcltest.MockExprLiteral(cty.True),
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"optional": cty.UnknownVal(cty.Number),
			}),
			1, // incorrect type; number required
		},
		"blocks": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"single": {
						Nesting: NestingSingle,
						Block:   Block{},
					},
					"list": {
						Nesting: NestingList,
						Block:   Block{},
					},
					"set": {
						Nesting: NestingSet,
						Block:   Block{},
					},
					"map": {
						Nesting: NestingMap,
						Block:   Block{},
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{
				Blocks: hcl.Blocks{
					&hcl.Block{
						Type: "list",
						Body: hcl.EmptyBody(),
					},
					&hcl.Block{
						Type: "single",
						Body: hcl.EmptyBody(),
					},
					&hcl.Block{
						Type: "list",
						Body: hcl.EmptyBody(),
					},
					&hcl.Block{
						Type: "set",
						Body: hcl.EmptyBody(),
					},
					&hcl.Block{
						Type:        "map",
						Labels:      []string{"foo"},
						LabelRanges: []hcl.Range{hcl.Range{}},
						Body:        hcl.EmptyBody(),
					},
					&hcl.Block{
						Type:        "map",
						Labels:      []string{"bar"},
						LabelRanges: []hcl.Range{hcl.Range{}},
						Body:        hcl.EmptyBody(),
					},
					&hcl.Block{
						Type: "set",
						Body: hcl.EmptyBody(),
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"single": cty.EmptyObjectVal,
				"list": cty.ListVal([]cty.Value{
					cty.EmptyObjectVal,
					cty.EmptyObjectVal,
				}),
				"set": cty.SetVal([]cty.Value{
					cty.EmptyObjectVal,
					cty.EmptyObjectVal,
				}),
				"map": cty.MapVal(map[string]cty.Value{
					"foo": cty.EmptyObjectVal,
					"bar": cty.EmptyObjectVal,
				}),
			}),
			0,
		},
		"blocks with dynamically-typed attributes": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"single": {
						Nesting: NestingSingle,
						Block: Block{
							Attributes: map[string]*Attribute{
								"a": {
									Type:     cty.DynamicPseudoType,
									Optional: true,
								},
							},
						},
					},
					"list": {
						Nesting: NestingList,
						Block: Block{
							Attributes: map[string]*Attribute{
								"a": {
									Type:     cty.DynamicPseudoType,
									Optional: true,
								},
							},
						},
					},
					"map": {
						Nesting: NestingMap,
						Block: Block{
							Attributes: map[string]*Attribute{
								"a": {
									Type:     cty.DynamicPseudoType,
									Optional: true,
								},
							},
						},
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{
				Blocks: hcl.Blocks{
					&hcl.Block{
						Type: "list",
						Body: hcl.EmptyBody(),
					},
					&hcl.Block{
						Type: "single",
						Body: hcl.EmptyBody(),
					},
					&hcl.Block{
						Type: "list",
						Body: hcl.EmptyBody(),
					},
					&hcl.Block{
						Type:        "map",
						Labels:      []string{"foo"},
						LabelRanges: []hcl.Range{hcl.Range{}},
						Body:        hcl.EmptyBody(),
					},
					&hcl.Block{
						Type:        "map",
						Labels:      []string{"bar"},
						LabelRanges: []hcl.Range{hcl.Range{}},
						Body:        hcl.EmptyBody(),
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"single": cty.ObjectVal(map[string]cty.Value{
					"a": cty.NullVal(cty.DynamicPseudoType),
				}),
				"list": cty.TupleVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"a": cty.NullVal(cty.DynamicPseudoType),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"a": cty.NullVal(cty.DynamicPseudoType),
					}),
				}),
				"map": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"a": cty.NullVal(cty.DynamicPseudoType),
					}),
					"bar": cty.ObjectVal(map[string]cty.Value{
						"a": cty.NullVal(cty.DynamicPseudoType),
					}),
				}),
			}),
			0,
		},
		"too many list items": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Nesting:  NestingList,
						Block:    Block{},
						MaxItems: 1,
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{
				Blocks: hcl.Blocks{
					&hcl.Block{
						Type: "foo",
						Body: hcl.EmptyBody(),
					},
					&hcl.Block{
						Type: "foo",
						Body: unknownBody{hcl.EmptyBody()},
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
			}),
			0, // max items cannot be validated during decode
		},
		// dynamic blocks may fulfill MinItems, but there is only one block to
		// decode.
		"required MinItems": {
			&Block{
				BlockTypes: map[string]*NestedBlock{
					"foo": {
						Nesting:  NestingList,
						Block:    Block{},
						MinItems: 2,
					},
				},
			},
			hcltest.MockBody(&hcl.BodyContent{
				Blocks: hcl.Blocks{
					&hcl.Block{
						Type: "foo",
						Body: unknownBody{hcl.EmptyBody()},
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.UnknownVal(cty.List(cty.EmptyObject)),
			}),
			0,
		},
		"extraneous attribute": {
			&Block{},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"extra": {
						Name: "extra",
						Expr: hcltest.MockExprLiteral(cty.StringVal("hello")),
					},
				},
			}),
			cty.EmptyObjectVal,
			1, // extraneous attribute
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			spec := test.Schema.DecoderSpec()

			got, diags := hcldec.Decode(test.TestBody, spec, nil)
			if len(diags) != test.DiagCount {
				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
				for _, diag := range diags {
					t.Logf("- %s", diag.Error())
				}
			}

			if !got.RawEquals(test.Want) {
				t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
				t.Errorf("wrong result\ngot:  %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
			}

			// Double-check that we're producing consistent results for DecoderSpec
			// and ImpliedType.
			impliedType := test.Schema.ImpliedType()
			if errs := got.Type().TestConformance(impliedType); len(errs) != 0 {
				t.Errorf("result does not conform to the schema's implied type")
				for _, err := range errs {
					t.Logf("- %s", err.Error())
				}
			}
		})
	}
}

// this satisfies hcldec.UnknownBody to simulate a dynamic block with an
// unknown number of values.
type unknownBody struct {
	hcl.Body
}

func (b unknownBody) Unknown() bool {
	return true
}

func TestAttributeDecoderSpec(t *testing.T) {
	tests := map[string]struct {
		Schema    *Attribute
		TestBody  hcl.Body
		Want      cty.Value
		DiagCount int
	}{
		"empty": {
			&Attribute{},
			hcl.EmptyBody(),
			cty.NilVal,
			0,
		},
		"nil": {
			nil,
			hcl.EmptyBody(),
			cty.NilVal,
			0,
		},
		"optional string (null)": {
			&Attribute{
				Type:     cty.String,
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{}),
			cty.NullVal(cty.String),
			0,
		},
		"optional string": {
			&Attribute{
				Type:     cty.String,
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.StringVal("bar")),
					},
				},
			}),
			cty.StringVal("bar"),
			0,
		},
		"NestedType with required string": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingSingle,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
							"foo": cty.StringVal("bar"),
						})),
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
			}),
			0,
		},
		"NestedType with optional attributes": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingSingle,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Optional: true,
						},
						"bar": {
							Type:     cty.String,
							Optional: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
							"foo": cty.StringVal("bar"),
						})),
					},
				},
			}),
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
				"bar": cty.NullVal(cty.String),
			}),
			0,
		},
		"NestedType with missing required string": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingSingle,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.EmptyObjectVal),
					},
				},
			}),
			cty.UnknownVal(cty.Object(map[string]cty.Type{
				"foo": cty.String,
			})),
			1,
		},
		// NestedModes
		"NestedType NestingModeList valid": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingList,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"foo": cty.StringVal("bar"),
							}),
							cty.ObjectVal(map[string]cty.Value{
								"foo": cty.StringVal("baz"),
							}),
						})),
					},
				},
			}),
			cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
				cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
			}),
			0,
		},
		"NestedType NestingModeList invalid": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingList,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
							// "foo" should be a string, not a list
							"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
						})})),
					},
				},
			}),
			cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{"foo": cty.String}))),
			1,
		},
		"NestedType NestingModeSet valid": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingSet,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"foo": cty.StringVal("bar"),
							}),
							cty.ObjectVal(map[string]cty.Value{
								"foo": cty.StringVal("baz"),
							}),
						})),
					},
				},
			}),
			cty.SetVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
				cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
			}),
			0,
		},
		"NestedType NestingModeSet invalid": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingSet,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
							// "foo" should be a string, not a list
							"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
						})})),
					},
				},
			}),
			cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{"foo": cty.String}))),
			1,
		},
		"NestedType NestingModeMap valid": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingMap,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
							"one": cty.ObjectVal(map[string]cty.Value{
								"foo": cty.StringVal("bar"),
							}),
							"two": cty.ObjectVal(map[string]cty.Value{
								"foo": cty.StringVal("baz"),
							}),
						})),
					},
				},
			}),
			cty.MapVal(map[string]cty.Value{
				"one": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
				"two": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("baz")}),
			}),
			0,
		},
		"NestedType NestingModeMap invalid": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingMap,
					Attributes: map[string]*Attribute{
						"foo": {
							Type:     cty.String,
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
							"one": cty.ObjectVal(map[string]cty.Value{
								// "foo" should be a string, not a list
								"foo": cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("baz")}),
							}),
						})),
					},
				},
			}),
			cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{"foo": cty.String}))),
			1,
		},
		"deeply NestedType NestingModeList valid": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingList,
					Attributes: map[string]*Attribute{
						"foo": {
							NestedType: &Object{
								Nesting: NestingList,
								Attributes: map[string]*Attribute{
									"bar": {
										Type:     cty.String,
										Required: true,
									},
								},
							},
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"foo": cty.ListVal([]cty.Value{
									cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
									cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}),
								}),
							}),
							cty.ObjectVal(map[string]cty.Value{
								"foo": cty.ListVal([]cty.Value{
									cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
									cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
								}),
							}),
						})),
					},
				},
			}),
			cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")}),
					cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("boz")}),
				})}),
				cty.ObjectVal(map[string]cty.Value{"foo": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("biz")}),
					cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("buz")}),
				})}),
			}),
			0,
		},
		"deeply NestedType NestingList invalid": {
			&Attribute{
				NestedType: &Object{
					Nesting: NestingList,
					Attributes: map[string]*Attribute{
						"foo": {
							NestedType: &Object{
								Nesting: NestingList,
								Attributes: map[string]*Attribute{
									"bar": {
										Type:     cty.Number,
										Required: true,
									},
								},
							},
							Required: true,
						},
					},
				},
				Optional: true,
			},
			hcltest.MockBody(&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"attr": {
						Name: "attr",
						Expr: hcltest.MockExprLiteral(cty.ListVal([]cty.Value{
							cty.ObjectVal(map[string]cty.Value{
								"foo": cty.ListVal([]cty.Value{
									// bar should be a Number
									cty.ObjectVal(map[string]cty.Value{"bar": cty.True}),
									cty.ObjectVal(map[string]cty.Value{"bar": cty.False}),
								}),
							}),
						})),
					},
				},
			}),
			cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
				"foo": cty.List(cty.Object(map[string]cty.Type{"bar": cty.Number})),
			}))),
			1,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			spec := test.Schema.decoderSpec("attr")
			got, diags := hcldec.Decode(test.TestBody, spec, nil)
			if len(diags) != test.DiagCount {
				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
				for _, diag := range diags {
					t.Logf("- %s", diag.Error())
				}
			}

			if !got.RawEquals(test.Want) {
				t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
				t.Errorf("wrong result\ngot:  %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
			}
		})
	}

}

// TestAttributeDecodeSpec_panic is a temporary test which verifies that
// decoderSpec panics when an invalid Attribute schema is encountered. It will
// be removed when InternalValidate() is extended to validate Attribute specs
// (and is used). See the #FIXME in decoderSpec.
func TestAttributeDecoderSpec_panic(t *testing.T) {
	attrS := &Attribute{
		Type: cty.Object(map[string]cty.Type{
			"nested_attribute": cty.String,
		}),
		NestedType: &Object{},
		Optional:   true,
	}

	defer func() { recover() }()
	attrS.decoderSpec("attr")
	t.Errorf("expected panic")
}

func TestListOptionalAttrsFromObject(t *testing.T) {
	tests := []struct {
		input *Object
		want  []string
	}{
		{
			nil,
			[]string{},
		},
		{
			&Object{},
			[]string{},
		},
		{
			&Object{
				Nesting: NestingSingle,
				Attributes: map[string]*Attribute{
					"optional":          {Type: cty.String, Optional: true},
					"required":          {Type: cty.Number, Required: true},
					"computed":          {Type: cty.List(cty.Bool), Computed: true},
					"optional_computed": {Type: cty.Map(cty.Bool), Optional: true, Computed: true},
				},
			},
			[]string{"optional", "computed", "optional_computed"},
		},
	}

	for _, test := range tests {
		got := listOptionalAttrsFromObject(test.input)

		// order is irrelevant
		sort.Strings(got)
		sort.Strings(test.want)

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