package hcldec

import (
	"fmt"
	"reflect"
	"testing"

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

func TestDecode(t *testing.T) {
	tests := []struct {
		config    string
		spec      Spec
		ctx       *hcl.EvalContext
		want      cty.Value
		diagCount int
	}{
		{
			``,
			&ObjectSpec{},
			nil,
			cty.EmptyObjectVal,
			0,
		},
		{
			"a = 1\n",
			&ObjectSpec{},
			nil,
			cty.EmptyObjectVal,
			1, // attribute named "a" is not expected here
		},
		{
			"a = 1\n",
			&ObjectSpec{
				"a": &AttrSpec{
					Name: "a",
					Type: cty.Number,
				},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.NumberIntVal(1),
			}),
			0,
		},
		{
			"a = 1\n",
			&AttrSpec{
				Name: "a",
				Type: cty.Number,
			},
			nil,
			cty.NumberIntVal(1),
			0,
		},
		{
			"a = 1\n",
			&DefaultSpec{
				Primary: &AttrSpec{
					Name: "a",
					Type: cty.Number,
				},
				Default: &LiteralSpec{
					Value: cty.NumberIntVal(10),
				},
			},
			nil,
			cty.NumberIntVal(1),
			0,
		},
		{
			"",
			&DefaultSpec{
				Primary: &AttrSpec{
					Name: "a",
					Type: cty.Number,
				},
				Default: &LiteralSpec{
					Value: cty.NumberIntVal(10),
				},
			},
			nil,
			cty.NumberIntVal(10),
			0,
		},
		{
			"a = 1\n",
			ObjectSpec{
				"foo": &DefaultSpec{
					Primary: &AttrSpec{
						Name: "a",
						Type: cty.Number,
					},
					Default: &LiteralSpec{
						Value: cty.NumberIntVal(10),
					},
				},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{"foo": cty.NumberIntVal(1)}),
			0,
		},
		{
			"a = \"1\"\n",
			&AttrSpec{
				Name: "a",
				Type: cty.Number,
			},
			nil,
			cty.NumberIntVal(1),
			0,
		},
		{
			"a = true\n",
			&AttrSpec{
				Name: "a",
				Type: cty.Number,
			},
			nil,
			cty.UnknownVal(cty.Number),
			1, // incorrect type - number required.
		},
		{
			``,
			&AttrSpec{
				Name:     "a",
				Type:     cty.Number,
				Required: true,
			},
			nil,
			cty.NullVal(cty.Number),
			1, // attribute "a" is required
		},

		{
			`
b {
}
`,
			&BlockSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
			},
			nil,
			cty.EmptyObjectVal,
			0,
		},
		{
			`
b "baz" {
}
`,
			&BlockSpec{
				TypeName: "b",
				Nested: &BlockLabelSpec{
					Index: 0,
					Name:  "name",
				},
			},
			nil,
			cty.StringVal("baz"),
			0,
		},
		{
			`
b "baz" {}
b "foo" {}
`,
			&BlockSpec{
				TypeName: "b",
				Nested: &BlockLabelSpec{
					Index: 0,
					Name:  "name",
				},
			},
			nil,
			cty.StringVal("baz"),
			1, // duplicate "b" block
		},
		{
			`
b {
}
`,
			&BlockSpec{
				TypeName: "b",
				Nested: &BlockLabelSpec{
					Index: 0,
					Name:  "name",
				},
			},
			nil,
			cty.NullVal(cty.String),
			1, // missing name label
		},
		{
			``,
			&BlockSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
			},
			nil,
			cty.NullVal(cty.EmptyObject),
			0,
		},
		{
			"a {}\n",
			&BlockSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
			},
			nil,
			cty.NullVal(cty.EmptyObject),
			1, // blocks of type "a" are not supported
		},
		{
			``,
			&BlockSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
				Required: true,
			},
			nil,
			cty.NullVal(cty.EmptyObject),
			1, // a block of type "b" is required
		},
		{
			`
b {}
b {}
`,
			&BlockSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
				Required: true,
			},
			nil,
			cty.EmptyObjectVal,
			1, // only one "b" block is allowed
		},
		{
			`
b {
}
`,
			&BlockAttrsSpec{
				TypeName:    "b",
				ElementType: cty.String,
			},
			nil,
			cty.MapValEmpty(cty.String),
			0,
		},
		{
			`
b {
  hello = "world"
}
`,
			&BlockAttrsSpec{
				TypeName:    "b",
				ElementType: cty.String,
			},
			nil,
			cty.MapVal(map[string]cty.Value{
				"hello": cty.StringVal("world"),
			}),
			0,
		},
		{
			`
b {
  hello = true
}
`,
			&BlockAttrsSpec{
				TypeName:    "b",
				ElementType: cty.String,
			},
			nil,
			cty.MapVal(map[string]cty.Value{
				"hello": cty.StringVal("true"),
			}),
			0,
		},
		{
			`
b {
  hello   = true
  goodbye = 5
}
`,
			&BlockAttrsSpec{
				TypeName:    "b",
				ElementType: cty.String,
			},
			nil,
			cty.MapVal(map[string]cty.Value{
				"hello":   cty.StringVal("true"),
				"goodbye": cty.StringVal("5"),
			}),
			0,
		},
		{
			``,
			&BlockAttrsSpec{
				TypeName:    "b",
				ElementType: cty.String,
			},
			nil,
			cty.NullVal(cty.Map(cty.String)),
			0,
		},
		{
			``,
			&BlockAttrsSpec{
				TypeName:    "b",
				ElementType: cty.String,
				Required:    true,
			},
			nil,
			cty.NullVal(cty.Map(cty.String)),
			1, // missing b block
		},
		{
			`
b {
}
b {
}
			`,
			&BlockAttrsSpec{
				TypeName:    "b",
				ElementType: cty.String,
			},
			nil,
			cty.MapValEmpty(cty.String),
			1, // duplicate b block
		},
		{
			`
b {
}
b {
}
			`,
			&BlockAttrsSpec{
				TypeName:    "b",
				ElementType: cty.String,
				Required:    true,
			},
			nil,
			cty.MapValEmpty(cty.String),
			1, // duplicate b block
		},
		{
			`
b {}
b {}
`,
			&BlockListSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
			},
			nil,
			cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
			0,
		},
		{
			``,
			&BlockListSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
			},
			nil,
			cty.ListValEmpty(cty.EmptyObject),
			0,
		},
		{
			`
b "foo" {}
b "bar" {}
`,
			&BlockListSpec{
				TypeName: "b",
				Nested: &BlockLabelSpec{
					Name:  "name",
					Index: 0,
				},
			},
			nil,
			cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
			0,
		},
		{
			`
b {}
b {}
b {}
`,
			&BlockListSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
				MaxItems: 2,
			},
			nil,
			cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal, cty.EmptyObjectVal}),
			1, // too many b blocks
		},
		{
			`
b {}
b {}
`,
			&BlockListSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
				MinItems: 10,
			},
			nil,
			cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
			1, // insufficient b blocks
		},
		{
			`
b {
	a = true
}
b {
	a = 1
}
`,
			&BlockListSpec{
				TypeName: "b",
				Nested: &AttrSpec{
					Name: "a",
					Type: cty.DynamicPseudoType,
				},
			},
			nil,
			cty.DynamicVal,
			1, // Unconsistent argument types in b blocks
		},
		{
			`
b {
	a = true
}
b {
	a = "not a bool"
}
`,
			&BlockListSpec{
				TypeName: "b",
				Nested: &AttrSpec{
					Name: "a",
					Type: cty.DynamicPseudoType,
				},
			},
			nil,
			cty.ListVal([]cty.Value{
				cty.StringVal("true"), // type unification generalizes all the values to strings
				cty.StringVal("not a bool"),
			}),
			0,
		},
		{
			`
b {}
b {}
`,
			&BlockSetSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
				MaxItems: 2,
			},
			nil,
			cty.SetVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
			0,
		},
		{
			`
b "foo" "bar" {}
b "bar" "baz" {}
`,
			&BlockSetSpec{
				TypeName: "b",
				Nested: TupleSpec{
					&BlockLabelSpec{
						Name:  "name",
						Index: 1,
					},
					&BlockLabelSpec{
						Name:  "type",
						Index: 0,
					},
				},
			},
			nil,
			cty.SetVal([]cty.Value{
				cty.TupleVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("foo")}),
				cty.TupleVal([]cty.Value{cty.StringVal("baz"), cty.StringVal("bar")}),
			}),
			0,
		},
		{
			`
b {
	a = true
}
b {
	a = 1
}
`,
			&BlockSetSpec{
				TypeName: "b",
				Nested: &AttrSpec{
					Name: "a",
					Type: cty.DynamicPseudoType,
				},
			},
			nil,
			cty.DynamicVal,
			1, // Unconsistent argument types in b blocks
		},
		{
			`
b {
	a = true
}
b {
	a = "not a bool"
}
`,
			&BlockSetSpec{
				TypeName: "b",
				Nested: &AttrSpec{
					Name: "a",
					Type: cty.DynamicPseudoType,
				},
			},
			nil,
			cty.SetVal([]cty.Value{
				cty.StringVal("true"), // type unification generalizes all the values to strings
				cty.StringVal("not a bool"),
			}),
			0,
		},
		{
			`
b "foo" {}
b "bar" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"key"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal, "bar": cty.EmptyObjectVal}),
			0,
		},
		{
			`
b "foo" "bar" {}
b "bar" "baz" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.MapVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{
					"bar": cty.EmptyObjectVal,
				}),
				"bar": cty.MapVal(map[string]cty.Value{
					"baz": cty.EmptyObjectVal,
				}),
			}),
			0,
		},
		{
			`
b "foo" "bar" {}
b "bar" "bar" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.MapVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{
					"bar": cty.EmptyObjectVal,
				}),
				"bar": cty.MapVal(map[string]cty.Value{
					"bar": cty.EmptyObjectVal,
				}),
			}),
			0,
		},
		{
			`
b "foo" "bar" {}
b "foo" "baz" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.MapVal(map[string]cty.Value{
				"foo": cty.MapVal(map[string]cty.Value{
					"bar": cty.EmptyObjectVal,
					"baz": cty.EmptyObjectVal,
				}),
			}),
			0,
		},
		{
			`
b "foo" "bar" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"key"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.MapValEmpty(cty.EmptyObject),
			1, // too many labels
		},
		{
			`
b "bar" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.MapValEmpty(cty.EmptyObject),
			1, // not enough labels
		},
		{
			`
b "foo" {}
b "foo" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"key"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
			1, // duplicate b block
		},
		{
			`
b "foo" "bar" {}
b "foo" "bar" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.MapVal(map[string]cty.Value{"foo": cty.MapVal(map[string]cty.Value{"bar": cty.EmptyObjectVal})}),
			1, // duplicate b block
		},
		{
			`
b "foo" "bar" {}
b "bar" "baz" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"type"},
				Nested: &BlockLabelSpec{
					Name:  "name",
					Index: 0,
				},
			},
			nil,
			cty.MapVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
				"bar": cty.StringVal("baz"),
			}),
			0,
		},
		{
			`
b "foo" {}
`,
			&BlockMapSpec{
				TypeName:   "b",
				LabelNames: []string{"type"},
				Nested: &BlockLabelSpec{
					Name:  "name",
					Index: 0,
				},
			},
			nil,
			cty.MapValEmpty(cty.String),
			1, // missing name
		},
		{
			`
b {}
b {}
`,
			&BlockTupleSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
			},
			nil,
			cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
			0,
		},
		{
			``,
			&BlockTupleSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
			},
			nil,
			cty.EmptyTupleVal,
			0,
		},
		{
			`
b "foo" {}
b "bar" {}
`,
			&BlockTupleSpec{
				TypeName: "b",
				Nested: &BlockLabelSpec{
					Name:  "name",
					Index: 0,
				},
			},
			nil,
			cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
			0,
		},
		{
			`
b {}
b {}
b {}
`,
			&BlockTupleSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
				MaxItems: 2,
			},
			nil,
			cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal, cty.EmptyObjectVal}),
			1, // too many b blocks
		},
		{
			`
b {}
b {}
`,
			&BlockTupleSpec{
				TypeName: "b",
				Nested:   ObjectSpec{},
				MinItems: 10,
			},
			nil,
			cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
			1, // insufficient b blocks
		},
		{
			`
b {
	a = true
}
b {
	a = 1
}
`,
			&BlockTupleSpec{
				TypeName: "b",
				Nested: &AttrSpec{
					Name: "a",
					Type: cty.DynamicPseudoType,
				},
			},
			nil,
			cty.TupleVal([]cty.Value{
				cty.True,
				cty.NumberIntVal(1),
			}),
			0,
		},
		{
			`
b {
	a = true
}
b {
	a = "not a bool"
}
`,
			&BlockTupleSpec{
				TypeName: "b",
				Nested: &AttrSpec{
					Name: "a",
					Type: cty.DynamicPseudoType,
				},
			},
			nil,
			cty.TupleVal([]cty.Value{
				cty.True,
				cty.StringVal("not a bool"),
			}),
			0,
		},
		{
			`
b "foo" {}
b "bar" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"key"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{"foo": cty.EmptyObjectVal, "bar": cty.EmptyObjectVal}),
			0,
		},
		{
			`
b "foo" "bar" {}
b "bar" "baz" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ObjectVal(map[string]cty.Value{
					"bar": cty.EmptyObjectVal,
				}),
				"bar": cty.ObjectVal(map[string]cty.Value{
					"baz": cty.EmptyObjectVal,
				}),
			}),
			0,
		},
		{
			`
b "foo" "bar" {}
b "bar" "bar" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ObjectVal(map[string]cty.Value{
					"bar": cty.EmptyObjectVal,
				}),
				"bar": cty.ObjectVal(map[string]cty.Value{
					"bar": cty.EmptyObjectVal,
				}),
			}),
			0,
		},
		{
			`
b "foo" "bar" {}
b "foo" "baz" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.ObjectVal(map[string]cty.Value{
					"bar": cty.EmptyObjectVal,
					"baz": cty.EmptyObjectVal,
				}),
			}),
			0,
		},
		{
			`
b "foo" "bar" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"key"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.EmptyObjectVal,
			1, // too many labels
		},
		{
			`
b "bar" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.EmptyObjectVal,
			1, // not enough labels
		},
		{
			`
b "foo" {}
b "foo" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"key"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
			1, // duplicate b block
		},
		{
			`
b "foo" "bar" {}
b "foo" "bar" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"key1", "key2"},
				Nested:     ObjectSpec{},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{"foo": cty.ObjectVal(map[string]cty.Value{"bar": cty.EmptyObjectVal})}),
			1, // duplicate b block
		},
		{
			`
b "foo" "bar" {}
b "bar" "baz" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"type"},
				Nested: &BlockLabelSpec{
					Name:  "name",
					Index: 0,
				},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
				"bar": cty.StringVal("baz"),
			}),
			0,
		},
		{
			`
b "foo" {}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"type"},
				Nested: &BlockLabelSpec{
					Name:  "name",
					Index: 0,
				},
			},
			nil,
			cty.EmptyObjectVal,
			1, // missing name
		},
		{
			`
b "foo" {
	arg = true
}
b "bar" {
	arg = 1
}
`,
			&BlockObjectSpec{
				TypeName:   "b",
				LabelNames: []string{"type"},
				Nested: &AttrSpec{
					Name: "arg",
					Type: cty.DynamicPseudoType,
				},
			},
			nil,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.True,
				"bar": cty.NumberIntVal(1),
			}),
			0,
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d-%s", i, test.config), func(t *testing.T) {
			file, parseDiags := hclsyntax.ParseConfig([]byte(test.config), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})
			body := file.Body
			got, valDiags := Decode(body, test.spec, test.ctx)

			var diags hcl.Diagnostics
			diags = append(diags, parseDiags...)
			diags = append(diags, valDiags...)

			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.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
			}
		})
	}
}

func TestSourceRange(t *testing.T) {
	tests := []struct {
		config string
		spec   Spec
		want   hcl.Range
	}{
		{
			"a = 1\n",
			&AttrSpec{
				Name: "a",
			},
			hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
				End:   hcl.Pos{Line: 1, Column: 6, Byte: 5},
			},
		},
		{
			`
b {
  a = 1
}
`,
			&BlockSpec{
				TypeName: "b",
				Nested: &AttrSpec{
					Name: "a",
				},
			},
			hcl.Range{
				Start: hcl.Pos{Line: 3, Column: 7, Byte: 11},
				End:   hcl.Pos{Line: 3, Column: 8, Byte: 12},
			},
		},
		{
			`
b {
  c {
    a = 1
  }
}
`,
			&BlockSpec{
				TypeName: "b",
				Nested: &BlockSpec{
					TypeName: "c",
					Nested: &AttrSpec{
						Name: "a",
					},
				},
			},
			hcl.Range{
				Start: hcl.Pos{Line: 4, Column: 9, Byte: 19},
				End:   hcl.Pos{Line: 4, Column: 10, Byte: 20},
			},
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d-%s", i, test.config), func(t *testing.T) {
			file, diags := hclsyntax.ParseConfig([]byte(test.config), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})
			if len(diags) != 0 {
				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), 0)
				for _, diag := range diags {
					t.Logf(" - %s", diag.Error())
				}
			}
			body := file.Body

			got := SourceRange(body, test.spec)

			if !reflect.DeepEqual(got, test.want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
			}
		})
	}

}
