package hcl

import (
	"github.com/hashicorp/hcl/hcl/token"
	"io/ioutil"
	"path/filepath"
	"reflect"
	"testing"
	"time"

	"github.com/davecgh/go-spew/spew"
	"github.com/hashicorp/hcl/hcl/ast"
)

func TestDecode_interface(t *testing.T) {
	cases := []struct {
		File string
		Err  bool
		Out  interface{}
	}{
		{
			"basic.hcl",
			false,
			map[string]interface{}{
				"foo": "bar",
				"bar": "${file(\"bing/bong.txt\")}",
			},
		},
		{
			"basic_squish.hcl",
			false,
			map[string]interface{}{
				"foo":     "bar",
				"bar":     "${file(\"bing/bong.txt\")}",
				"foo-bar": "baz",
			},
		},
		{
			"empty.hcl",
			false,
			map[string]interface{}{
				"resource": []map[string]interface{}{
					map[string]interface{}{
						"foo": []map[string]interface{}{
							map[string]interface{}{},
						},
					},
				},
			},
		},
		{
			"tfvars.hcl",
			false,
			map[string]interface{}{
				"regularvar": "Should work",
				"map.key1":   "Value",
				"map.key2":   "Other value",
			},
		},
		{
			"escape.hcl",
			false,
			map[string]interface{}{
				"foo":          "bar\"baz\\n",
				"qux":          "back\\slash",
				"bar":          "new\nline",
				"qax":          `slash\:colon`,
				"nested":       `${HH\\:mm\\:ss}`,
				"nestedquotes": `${"\"stringwrappedinquotes\""}`,
			},
		},
		{
			"float.hcl",
			false,
			map[string]interface{}{
				"a": 1.02,
				"b": 2,
			},
		},
		{
			"multiline_bad.hcl",
			true,
			nil,
		},
		{
			"multiline_literal.hcl",
			true,
			nil,
		},
		{
			"multiline_literal_with_hil.hcl",
			false,
			map[string]interface{}{"multiline_literal_with_hil": "${hello\n  world}"},
		},
		{
			"multiline_no_marker.hcl",
			true,
			nil,
		},
		{
			"multiline.hcl",
			false,
			map[string]interface{}{"foo": "bar\nbaz\n"},
		},
		{
			"multiline_indented.hcl",
			false,
			map[string]interface{}{"foo": "  bar\n  baz\n"},
		},
		{
			"multiline_no_hanging_indent.hcl",
			false,
			map[string]interface{}{"foo": "  baz\n    bar\n      foo\n"},
		},
		{
			"multiline_no_eof.hcl",
			false,
			map[string]interface{}{"foo": "bar\nbaz\n", "key": "value"},
		},
		{
			"multiline.json",
			false,
			map[string]interface{}{"foo": "bar\nbaz"},
		},
		{
			"null_strings.json",
			false,
			map[string]interface{}{
				"module": []map[string]interface{}{
					map[string]interface{}{
						"app": []map[string]interface{}{
							map[string]interface{}{"foo": ""},
						},
					},
				},
			},
		},
		{
			"scientific.json",
			false,
			map[string]interface{}{
				"a": 1e-10,
				"b": 1e+10,
				"c": 1e10,
				"d": 1.2e-10,
				"e": 1.2e+10,
				"f": 1.2e10,
			},
		},
		{
			"scientific.hcl",
			false,
			map[string]interface{}{
				"a": 1e-10,
				"b": 1e+10,
				"c": 1e10,
				"d": 1.2e-10,
				"e": 1.2e+10,
				"f": 1.2e10,
			},
		},
		{
			"terraform_heroku.hcl",
			false,
			map[string]interface{}{
				"name": "terraform-test-app",
				"config_vars": []map[string]interface{}{
					map[string]interface{}{
						"FOO": "bar",
					},
				},
			},
		},
		{
			"structure_multi.hcl",
			false,
			map[string]interface{}{
				"foo": []map[string]interface{}{
					map[string]interface{}{
						"baz": []map[string]interface{}{
							map[string]interface{}{"key": 7},
						},
					},
					map[string]interface{}{
						"bar": []map[string]interface{}{
							map[string]interface{}{"key": 12},
						},
					},
				},
			},
		},
		{
			"structure_multi.json",
			false,
			map[string]interface{}{
				"foo": []map[string]interface{}{
					map[string]interface{}{
						"baz": []map[string]interface{}{
							map[string]interface{}{"key": 7},
						},
					},
					map[string]interface{}{
						"bar": []map[string]interface{}{
							map[string]interface{}{"key": 12},
						},
					},
				},
			},
		},
		{
			"list_of_lists.hcl",
			false,
			map[string]interface{}{
				"foo": []interface{}{
					[]interface{}{"foo"},
					[]interface{}{"bar"},
				},
			},
		},
		{
			"list_of_maps.hcl",
			false,
			map[string]interface{}{
				"foo": []interface{}{
					map[string]interface{}{"somekey1": "someval1"},
					map[string]interface{}{"somekey2": "someval2", "someextrakey": "someextraval"},
				},
			},
		},
		{
			"assign_deep.hcl",
			false,
			map[string]interface{}{
				"resource": []interface{}{
					map[string]interface{}{
						"foo": []interface{}{
							map[string]interface{}{
								"bar": []map[string]interface{}{
									map[string]interface{}{}}}}}}},
		},
		{
			"structure_list.hcl",
			false,
			map[string]interface{}{
				"foo": []map[string]interface{}{
					map[string]interface{}{
						"key": 7,
					},
					map[string]interface{}{
						"key": 12,
					},
				},
			},
		},
		{
			"structure_list.json",
			false,
			map[string]interface{}{
				"foo": []map[string]interface{}{
					map[string]interface{}{
						"key": 7,
					},
					map[string]interface{}{
						"key": 12,
					},
				},
			},
		},
		{
			"structure_list_deep.json",
			false,
			map[string]interface{}{
				"bar": []map[string]interface{}{
					map[string]interface{}{
						"foo": []map[string]interface{}{
							map[string]interface{}{
								"name": "terraform_example",
								"ingress": []map[string]interface{}{
									map[string]interface{}{
										"from_port": 22,
									},
									map[string]interface{}{
										"from_port": 80,
									},
								},
							},
						},
					},
				},
			},
		},

		{
			"structure_list_empty.json",
			false,
			map[string]interface{}{
				"foo": []interface{}{},
			},
		},

		{
			"nested_block_comment.hcl",
			false,
			map[string]interface{}{
				"bar": "value",
			},
		},

		{
			"unterminated_block_comment.hcl",
			true,
			nil,
		},

		{
			"unterminated_brace.hcl",
			true,
			nil,
		},

		{
			"nested_provider_bad.hcl",
			true,
			nil,
		},

		{
			"object_list.json",
			false,
			map[string]interface{}{
				"resource": []map[string]interface{}{
					map[string]interface{}{
						"aws_instance": []map[string]interface{}{
							map[string]interface{}{
								"db": []map[string]interface{}{
									map[string]interface{}{
										"vpc": "foo",
										"provisioner": []map[string]interface{}{
											map[string]interface{}{
												"file": []map[string]interface{}{
													map[string]interface{}{
														"source":      "foo",
														"destination": "bar",
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},

		// Terraform GH-8295 sanity test that basic decoding into
		// interface{} works.
		{
			"terraform_variable_invalid.json",
			false,
			map[string]interface{}{
				"variable": []map[string]interface{}{
					map[string]interface{}{
						"whatever": "abc123",
					},
				},
			},
		},

		{
			"interpolate.json",
			false,
			map[string]interface{}{
				"default": `${replace("europe-west", "-", " ")}`,
			},
		},

		{
			"block_assign.hcl",
			true,
			nil,
		},

		{
			"escape_backslash.hcl",
			false,
			map[string]interface{}{
				"output": []map[string]interface{}{
					map[string]interface{}{
						"one":  `${replace(var.sub_domain, ".", "\\.")}`,
						"two":  `${replace(var.sub_domain, ".", "\\\\.")}`,
						"many": `${replace(var.sub_domain, ".", "\\\\\\\\.")}`,
					},
				},
			},
		},

		{
			"git_crypt.hcl",
			true,
			nil,
		},

		{
			"object_with_bool.hcl",
			false,
			map[string]interface{}{
				"path": []map[string]interface{}{
					map[string]interface{}{
						"policy": "write",
						"permissions": []map[string]interface{}{
							map[string]interface{}{
								"bool": []interface{}{false},
							},
						},
					},
				},
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.File, func(t *testing.T) {
			d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.File))
			if err != nil {
				t.Fatalf("err: %s", err)
			}

			var out interface{}
			err = Decode(&out, string(d))
			if (err != nil) != tc.Err {
				t.Fatalf("Input: %s\n\nError: %s", tc.File, err)
			}

			if !reflect.DeepEqual(out, tc.Out) {
				t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out)
			}

			var v interface{}
			err = Unmarshal(d, &v)
			if (err != nil) != tc.Err {
				t.Fatalf("Input: %s\n\nError: %s", tc.File, err)
			}

			if !reflect.DeepEqual(v, tc.Out) {
				t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out)
			}
		})
	}
}

func TestDecode_interfaceInline(t *testing.T) {
	cases := []struct {
		Value string
		Err   bool
		Out   interface{}
	}{
		{"t t e{{}}", true, nil},
		{"t=0t d {}", true, map[string]interface{}{"t": 0}},
		{"v=0E0v d{}", true, map[string]interface{}{"v": float64(0)}},
	}

	for _, tc := range cases {
		t.Logf("Testing: %q", tc.Value)

		var out interface{}
		err := Decode(&out, tc.Value)
		if (err != nil) != tc.Err {
			t.Fatalf("Input: %q\n\nError: %s", tc.Value, err)
		}

		if !reflect.DeepEqual(out, tc.Out) {
			t.Fatalf("Input: %q. Actual, Expected.\n\n%#v\n\n%#v", tc.Value, out, tc.Out)
		}

		var v interface{}
		err = Unmarshal([]byte(tc.Value), &v)
		if (err != nil) != tc.Err {
			t.Fatalf("Input: %q\n\nError: %s", tc.Value, err)
		}

		if !reflect.DeepEqual(v, tc.Out) {
			t.Fatalf("Input: %q. Actual, Expected.\n\n%#v\n\n%#v", tc.Value, out, tc.Out)
		}
	}
}

func TestDecode_equal(t *testing.T) {
	cases := []struct {
		One, Two string
	}{
		{
			"basic.hcl",
			"basic.json",
		},
		{
			"float.hcl",
			"float.json",
		},
		/*
			{
				"structure.hcl",
				"structure.json",
			},
		*/
		{
			"structure.hcl",
			"structure_flat.json",
		},
		{
			"terraform_heroku.hcl",
			"terraform_heroku.json",
		},
	}

	for _, tc := range cases {
		p1 := filepath.Join(fixtureDir, tc.One)
		p2 := filepath.Join(fixtureDir, tc.Two)

		d1, err := ioutil.ReadFile(p1)
		if err != nil {
			t.Fatalf("err: %s", err)
		}

		d2, err := ioutil.ReadFile(p2)
		if err != nil {
			t.Fatalf("err: %s", err)
		}

		var i1, i2 interface{}
		err = Decode(&i1, string(d1))
		if err != nil {
			t.Fatalf("err: %s", err)
		}

		err = Decode(&i2, string(d2))
		if err != nil {
			t.Fatalf("err: %s", err)
		}

		if !reflect.DeepEqual(i1, i2) {
			t.Fatalf(
				"%s != %s\n\n%#v\n\n%#v",
				tc.One, tc.Two,
				i1, i2)
		}
	}
}

func TestDecode_flatMap(t *testing.T) {
	var val map[string]map[string]string

	err := Decode(&val, testReadFile(t, "structure_flatmap.hcl"))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	expected := map[string]map[string]string{
		"foo": map[string]string{
			"foo": "bar",
			"key": "7",
		},
	}

	if !reflect.DeepEqual(val, expected) {
		t.Fatalf("Actual: %#v\n\nExpected: %#v", val, expected)
	}
}

func TestDecode_structure(t *testing.T) {
	type Embedded interface{}

	type V struct {
		Embedded `hcl:"-"`
		Key      int
		Foo      string
	}

	var actual V

	err := Decode(&actual, testReadFile(t, "flat.hcl"))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	expected := V{
		Key: 7,
		Foo: "bar",
	}

	if !reflect.DeepEqual(actual, expected) {
		t.Fatalf("Actual: %#v\n\nExpected: %#v", actual, expected)
	}
}

func TestDecode_structurePtr(t *testing.T) {
	type V struct {
		Key int
		Foo string
	}

	var actual *V

	err := Decode(&actual, testReadFile(t, "flat.hcl"))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	expected := &V{
		Key: 7,
		Foo: "bar",
	}

	if !reflect.DeepEqual(actual, expected) {
		t.Fatalf("Actual: %#v\n\nExpected: %#v", actual, expected)
	}
}

func TestDecode_structureArray(t *testing.T) {
	// This test is extracted from a failure in Consul (consul.io),
	// hence the interesting structure naming.

	type KeyPolicyType string

	type KeyPolicy struct {
		Prefix string `hcl:",key"`
		Policy KeyPolicyType
	}

	type Policy struct {
		Keys []KeyPolicy `hcl:"key,expand"`
	}

	expected := Policy{
		Keys: []KeyPolicy{
			KeyPolicy{
				Prefix: "",
				Policy: "read",
			},
			KeyPolicy{
				Prefix: "foo/",
				Policy: "write",
			},
			KeyPolicy{
				Prefix: "foo/bar/",
				Policy: "read",
			},
			KeyPolicy{
				Prefix: "foo/bar/baz",
				Policy: "deny",
			},
		},
	}

	files := []string{
		"decode_policy.hcl",
		"decode_policy.json",
	}

	for _, f := range files {
		var actual Policy

		err := Decode(&actual, testReadFile(t, f))
		if err != nil {
			t.Fatalf("Input: %s\n\nerr: %s", f, err)
		}

		if !reflect.DeepEqual(actual, expected) {
			t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected)
		}
	}
}

func TestDecode_sliceExpand(t *testing.T) {
	type testInner struct {
		Name string `hcl:",key"`
		Key  string
	}

	type testStruct struct {
		Services []testInner `hcl:"service,expand"`
	}

	expected := testStruct{
		Services: []testInner{
			testInner{
				Name: "my-service-0",
				Key:  "value",
			},
			testInner{
				Name: "my-service-1",
				Key:  "value",
			},
		},
	}

	files := []string{
		"slice_expand.hcl",
	}

	for _, f := range files {
		t.Logf("Testing: %s", f)

		var actual testStruct
		err := Decode(&actual, testReadFile(t, f))
		if err != nil {
			t.Fatalf("Input: %s\n\nerr: %s", f, err)
		}

		if !reflect.DeepEqual(actual, expected) {
			t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected)
		}
	}
}

func TestDecode_structureMap(t *testing.T) {
	// This test is extracted from a failure in Terraform (terraform.io),
	// hence the interesting structure naming.

	type hclVariable struct {
		Default     interface{}
		Description string
		Fields      []string `hcl:",decodedFields"`
	}

	type rawConfig struct {
		Variable map[string]hclVariable
	}

	expected := rawConfig{
		Variable: map[string]hclVariable{
			"foo": hclVariable{
				Default:     "bar",
				Description: "bar",
				Fields:      []string{"Default", "Description"},
			},

			"amis": hclVariable{
				Default: []map[string]interface{}{
					map[string]interface{}{
						"east": "foo",
					},
				},
				Fields: []string{"Default"},
			},
		},
	}

	files := []string{
		"decode_tf_variable.hcl",
		"decode_tf_variable.json",
	}

	for _, f := range files {
		t.Logf("Testing: %s", f)

		var actual rawConfig
		err := Decode(&actual, testReadFile(t, f))
		if err != nil {
			t.Fatalf("Input: %s\n\nerr: %s", f, err)
		}

		if !reflect.DeepEqual(actual, expected) {
			t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected)
		}
	}
}

func TestDecode_structureMapInvalid(t *testing.T) {
	// Terraform GH-8295

	type hclVariable struct {
		Default     interface{}
		Description string
		Fields      []string `hcl:",decodedFields"`
	}

	type rawConfig struct {
		Variable map[string]*hclVariable
	}

	var actual rawConfig
	err := Decode(&actual, testReadFile(t, "terraform_variable_invalid.json"))
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestDecode_structureMapExtraKeys(t *testing.T) {
	type hclVariable struct {
		A     int
		B     int
		Found []string `hcl:",decodedFields"`
		Extra map[string][]token.Pos `hcl:",unusedKeyPositions"`
	}

	q := hclVariable{
		A:     1,
		B:     2,
		Found: []string{"A", "B"},
		Extra: map[string][]token.Pos {
			"extra1": {{
				Line: 3,
				Column: 1,
				Offset: 12,
			}},
			"extra2": {{
				Line: 4,
				Column: 1,
				Offset: 23,
			}},
		},
	}

	var p hclVariable
	ast, _ := Parse(testReadFile(t, "structure_map_extra_keys.hcl"))
	DecodeObject(&p, ast)
	if !reflect.DeepEqual(p, q) {
		t.Fatal("not equal")
	}

	p.Extra = map[string][]token.Pos{
		"extra1": {{}},
		"extra2": {{}},
	}

	var j hclVariable
	ast, _ = Parse(testReadFile(t, "structure_map_extra_keys.json"))
	DecodeObject(&j, ast)
	if !reflect.DeepEqual(p, j) {
		t.Fatal("not equal")
	}
}

func TestDecode_interfaceNonPointer(t *testing.T) {
	var value interface{}
	err := Decode(value, testReadFile(t, "basic_int_string.hcl"))
	if err == nil {
		t.Fatal("should error")
	}
}

func TestDecode_intString(t *testing.T) {
	var value struct {
		Count int
	}

	err := Decode(&value, testReadFile(t, "basic_int_string.hcl"))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if value.Count != 3 {
		t.Fatalf("bad: %#v", value.Count)
	}
}

func TestDecode_float32(t *testing.T) {
	var value struct {
		A float32 `hcl:"a"`
		B float32 `hcl:"b"`
	}

	err := Decode(&value, testReadFile(t, "float.hcl"))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if got, want := value.A, float32(1.02); got != want {
		t.Fatalf("wrong result %#v; want %#v", got, want)
	}
	if got, want := value.B, float32(2); got != want {
		t.Fatalf("wrong result %#v; want %#v", got, want)
	}
}

func TestDecode_float64(t *testing.T) {
	var value struct {
		A float64 `hcl:"a"`
		B float64 `hcl:"b"`
	}

	err := Decode(&value, testReadFile(t, "float.hcl"))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if got, want := value.A, float64(1.02); got != want {
		t.Fatalf("wrong result %#v; want %#v", got, want)
	}
	if got, want := value.B, float64(2); got != want {
		t.Fatalf("wrong result %#v; want %#v", got, want)
	}
}

func TestDecode_intStringAliased(t *testing.T) {
	var value struct {
		Count time.Duration
	}

	err := Decode(&value, testReadFile(t, "basic_int_string.hcl"))
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if value.Count != time.Duration(3) {
		t.Fatalf("bad: %#v", value.Count)
	}
}

func TestDecode_Node(t *testing.T) {
	// given
	var value struct {
		Content ast.Node
		Nested  struct {
			Content ast.Node
		}
	}

	content := `
content {
	hello = "world"
}
`

	// when
	err := Decode(&value, content)

	// then
	if err != nil {
		t.Errorf("unable to decode content, %v", err)
		return
	}

	// verify ast.Node can be decoded later
	var v map[string]interface{}
	err = DecodeObject(&v, value.Content)
	if err != nil {
		t.Errorf("unable to decode content, %v", err)
		return
	}

	if v["hello"] != "world" {
		t.Errorf("expected mapping to be returned")
	}
}

func TestDecode_NestedNode(t *testing.T) {
	// given
	var value struct {
		Nested struct {
			Content ast.Node
		}
	}

	content := `
nested "content" {
	hello = "world"
}
`

	// when
	err := Decode(&value, content)

	// then
	if err != nil {
		t.Errorf("unable to decode content, %v", err)
		return
	}

	// verify ast.Node can be decoded later
	var v map[string]interface{}
	err = DecodeObject(&v, value.Nested.Content)
	if err != nil {
		t.Errorf("unable to decode content, %v", err)
		return
	}

	if v["hello"] != "world" {
		t.Errorf("expected mapping to be returned")
	}
}

// https://github.com/hashicorp/hcl/issues/60
func TestDecode_topLevelKeys(t *testing.T) {
	type Template struct {
		Source string
	}

	templates := struct {
		Templates []*Template `hcl:"template"`
	}{}

	err := Decode(&templates, `
	template {
	    source = "blah"
	}

	template {
	    source = "blahblah"
	}`)

	if err != nil {
		t.Fatal(err)
	}

	if templates.Templates[0].Source != "blah" {
		t.Errorf("bad source: %s", templates.Templates[0].Source)
	}

	if templates.Templates[1].Source != "blahblah" {
		t.Errorf("bad source: %s", templates.Templates[1].Source)
	}
}

func TestDecode_flattenedJSON(t *testing.T) {
	// make sure we can also correctly extract a Name key too
	type V struct {
		Name        string `hcl:",key"`
		Description string
		Default     map[string]string
	}
	type Vars struct {
		Variable []*V
	}

	cases := []struct {
		JSON     string
		Out      interface{}
		Expected interface{}
	}{
		{ // Nested object, no sibling keys
			JSON: `
{
  "var_name": {
    "default": {
      "key1": "a",
      "key2": "b"
    }
  }
}
			`,
			Out: &[]*V{},
			Expected: &[]*V{
				&V{
					Name:    "var_name",
					Default: map[string]string{"key1": "a", "key2": "b"},
				},
			},
		},

		{ // Nested object with a sibling key (this worked previously)
			JSON: `
{
  "var_name": {
    "description": "Described",
    "default": {
      "key1": "a",
      "key2": "b"
    }
  }
}
			`,
			Out: &[]*V{},
			Expected: &[]*V{
				&V{
					Name:        "var_name",
					Description: "Described",
					Default:     map[string]string{"key1": "a", "key2": "b"},
				},
			},
		},

		{ // Multiple nested objects, one with a sibling key
			JSON: `
{
  "variable": {
    "var_1": {
      "default": {
        "key1": "a",
        "key2": "b"
      }
    },
    "var_2": {
      "description": "Described",
      "default": {
        "key1": "a",
        "key2": "b"
      }
    }
  }
}
			`,
			Out: &Vars{},
			Expected: &Vars{
				Variable: []*V{
					&V{
						Name:    "var_1",
						Default: map[string]string{"key1": "a", "key2": "b"},
					},
					&V{
						Name:        "var_2",
						Description: "Described",
						Default:     map[string]string{"key1": "a", "key2": "b"},
					},
				},
			},
		},

		{ // Nested object to maps
			JSON: `
{
  "variable": {
    "var_name": {
      "description": "Described",
      "default": {
        "key1": "a",
        "key2": "b"
      }
    }
  }
}
			`,
			Out: &[]map[string]interface{}{},
			Expected: &[]map[string]interface{}{
				{
					"variable": []map[string]interface{}{
						{
							"var_name": []map[string]interface{}{
								{
									"description": "Described",
									"default": []map[string]interface{}{
										{
											"key1": "a",
											"key2": "b",
										},
									},
								},
							},
						},
					},
				},
			},
		},

		{ // Nested object to maps without a sibling key should decode the same as above
			JSON: `
{
  "variable": {
    "var_name": {
      "default": {
        "key1": "a",
        "key2": "b"
      }
    }
  }
}
			`,
			Out: &[]map[string]interface{}{},
			Expected: &[]map[string]interface{}{
				{
					"variable": []map[string]interface{}{
						{
							"var_name": []map[string]interface{}{
								{
									"default": []map[string]interface{}{
										{
											"key1": "a",
											"key2": "b",
										},
									},
								},
							},
						},
					},
				},
			},
		},

		{ // Nested objects, one with a sibling key, and one without
			JSON: `
{
  "variable": {
    "var_1": {
      "default": {
        "key1": "a",
        "key2": "b"
      }
    },
    "var_2": {
      "description": "Described",
      "default": {
        "key1": "a",
        "key2": "b"
      }
    }
  }
}
			`,
			Out: &[]map[string]interface{}{},
			Expected: &[]map[string]interface{}{
				{
					"variable": []map[string]interface{}{
						{
							"var_1": []map[string]interface{}{
								{
									"default": []map[string]interface{}{
										{
											"key1": "a",
											"key2": "b",
										},
									},
								},
							},
						},
					},
				},
				{
					"variable": []map[string]interface{}{
						{
							"var_2": []map[string]interface{}{
								{
									"description": "Described",
									"default": []map[string]interface{}{
										{
											"key1": "a",
											"key2": "b",
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}

	for i, tc := range cases {
		err := Decode(tc.Out, tc.JSON)
		if err != nil {
			t.Fatalf("[%d] err: %s", i, err)
		}

		if !reflect.DeepEqual(tc.Out, tc.Expected) {
			t.Fatalf("[%d]\ngot: %s\nexpected: %s\n", i, spew.Sdump(tc.Out), spew.Sdump(tc.Expected))
		}
	}
}
