package lang

import (
	"bytes"
	"encoding/json"
	"testing"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/configs/configschema"
	"github.com/hashicorp/terraform/instances"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"

	"github.com/zclconf/go-cty/cty"
	ctyjson "github.com/zclconf/go-cty/cty/json"
)

func TestScopeEvalContext(t *testing.T) {
	data := &dataForTests{
		CountAttrs: map[string]cty.Value{
			"index": cty.NumberIntVal(0),
		},
		ForEachAttrs: map[string]cty.Value{
			"key":   cty.StringVal("a"),
			"value": cty.NumberIntVal(1),
		},
		Resources: map[string]cty.Value{
			"null_resource.foo": cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("bar"),
			}),
			"data.null_data_source.foo": cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("bar"),
			}),
			"null_resource.multi": cty.TupleVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"attr": cty.StringVal("multi0"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"attr": cty.StringVal("multi1"),
				}),
			}),
			"null_resource.each": cty.ObjectVal(map[string]cty.Value{
				"each0": cty.ObjectVal(map[string]cty.Value{
					"attr": cty.StringVal("each0"),
				}),
				"each1": cty.ObjectVal(map[string]cty.Value{
					"attr": cty.StringVal("each1"),
				}),
			}),
			"null_resource.multi[1]": cty.ObjectVal(map[string]cty.Value{
				"attr": cty.StringVal("multi1"),
			}),
		},
		LocalValues: map[string]cty.Value{
			"foo": cty.StringVal("bar"),
		},
		Modules: map[string]cty.Value{
			"module.foo": cty.ObjectVal(map[string]cty.Value{
				"output0": cty.StringVal("bar0"),
				"output1": cty.StringVal("bar1"),
			}),
		},
		PathAttrs: map[string]cty.Value{
			"module": cty.StringVal("foo/bar"),
		},
		TerraformAttrs: map[string]cty.Value{
			"workspace": cty.StringVal("default"),
		},
		InputVariables: map[string]cty.Value{
			"baz": cty.StringVal("boop"),
		},
	}

	tests := []struct {
		Expr string
		Want map[string]cty.Value
	}{
		{
			`12`,
			map[string]cty.Value{},
		},
		{
			`count.index`,
			map[string]cty.Value{
				"count": cty.ObjectVal(map[string]cty.Value{
					"index": cty.NumberIntVal(0),
				}),
			},
		},
		{
			`each.key`,
			map[string]cty.Value{
				"each": cty.ObjectVal(map[string]cty.Value{
					"key": cty.StringVal("a"),
				}),
			},
		},
		{
			`each.value`,
			map[string]cty.Value{
				"each": cty.ObjectVal(map[string]cty.Value{
					"value": cty.NumberIntVal(1),
				}),
			},
		},
		{
			`local.foo`,
			map[string]cty.Value{
				"local": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
			},
		},
		{
			`null_resource.foo`,
			map[string]cty.Value{
				"null_resource": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"attr": cty.StringVal("bar"),
					}),
				}),
			},
		},
		{
			`null_resource.foo.attr`,
			map[string]cty.Value{
				"null_resource": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"attr": cty.StringVal("bar"),
					}),
				}),
			},
		},
		{
			`null_resource.multi`,
			map[string]cty.Value{
				"null_resource": cty.ObjectVal(map[string]cty.Value{
					"multi": cty.TupleVal([]cty.Value{
						cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("multi0"),
						}),
						cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("multi1"),
						}),
					}),
				}),
			},
		},
		{
			// at this level, all instance references return the entire resource
			`null_resource.multi[1]`,
			map[string]cty.Value{
				"null_resource": cty.ObjectVal(map[string]cty.Value{
					"multi": cty.TupleVal([]cty.Value{
						cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("multi0"),
						}),
						cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("multi1"),
						}),
					}),
				}),
			},
		},
		{
			// at this level, all instance references return the entire resource
			`null_resource.each["each1"]`,
			map[string]cty.Value{
				"null_resource": cty.ObjectVal(map[string]cty.Value{
					"each": cty.ObjectVal(map[string]cty.Value{
						"each0": cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("each0"),
						}),
						"each1": cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("each1"),
						}),
					}),
				}),
			},
		},
		{
			// at this level, all instance references return the entire resource
			`null_resource.each["each1"].attr`,
			map[string]cty.Value{
				"null_resource": cty.ObjectVal(map[string]cty.Value{
					"each": cty.ObjectVal(map[string]cty.Value{
						"each0": cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("each0"),
						}),
						"each1": cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("each1"),
						}),
					}),
				}),
			},
		},
		{
			`foo(null_resource.multi, null_resource.multi[1])`,
			map[string]cty.Value{
				"null_resource": cty.ObjectVal(map[string]cty.Value{
					"multi": cty.TupleVal([]cty.Value{
						cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("multi0"),
						}),
						cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("multi1"),
						}),
					}),
				}),
			},
		},
		{
			`data.null_data_source.foo`,
			map[string]cty.Value{
				"data": cty.ObjectVal(map[string]cty.Value{
					"null_data_source": cty.ObjectVal(map[string]cty.Value{
						"foo": cty.ObjectVal(map[string]cty.Value{
							"attr": cty.StringVal("bar"),
						}),
					}),
				}),
			},
		},
		{
			`module.foo`,
			map[string]cty.Value{
				"module": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"output0": cty.StringVal("bar0"),
						"output1": cty.StringVal("bar1"),
					}),
				}),
			},
		},
		// any module reference returns the entire module
		{
			`module.foo.output1`,
			map[string]cty.Value{
				"module": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"output0": cty.StringVal("bar0"),
						"output1": cty.StringVal("bar1"),
					}),
				}),
			},
		},
		{
			`path.module`,
			map[string]cty.Value{
				"path": cty.ObjectVal(map[string]cty.Value{
					"module": cty.StringVal("foo/bar"),
				}),
			},
		},
		{
			`self.baz`,
			map[string]cty.Value{
				"self": cty.ObjectVal(map[string]cty.Value{
					"attr": cty.StringVal("multi1"),
				}),
			},
		},
		{
			`terraform.workspace`,
			map[string]cty.Value{
				"terraform": cty.ObjectVal(map[string]cty.Value{
					"workspace": cty.StringVal("default"),
				}),
			},
		},
		{
			`var.baz`,
			map[string]cty.Value{
				"var": cty.ObjectVal(map[string]cty.Value{
					"baz": cty.StringVal("boop"),
				}),
			},
		},
	}

	for _, test := range tests {
		t.Run(test.Expr, func(t *testing.T) {
			expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1})
			if len(parseDiags) != 0 {
				t.Errorf("unexpected diagnostics during parse")
				for _, diag := range parseDiags {
					t.Errorf("- %s", diag)
				}
				return
			}

			refs, refsDiags := ReferencesInExpr(expr)
			if refsDiags.HasErrors() {
				t.Fatal(refsDiags.Err())
			}

			scope := &Scope{
				Data: data,

				// "self" will just be an arbitrary one of the several resource
				// instances we have in our test dataset.
				SelfAddr: addrs.ResourceInstance{
					Resource: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "null_resource",
						Name: "multi",
					},
					Key: addrs.IntKey(1),
				},
			}
			ctx, ctxDiags := scope.EvalContext(refs)
			if ctxDiags.HasErrors() {
				t.Fatal(ctxDiags.Err())
			}

			// For easier test assertions we'll just remove any top-level
			// empty objects from our variables map.
			for k, v := range ctx.Variables {
				if v.RawEquals(cty.EmptyObjectVal) {
					delete(ctx.Variables, k)
				}
			}

			gotVal := cty.ObjectVal(ctx.Variables)
			wantVal := cty.ObjectVal(test.Want)

			if !gotVal.RawEquals(wantVal) {
				// We'll JSON-ize our values here just so it's easier to
				// read them in the assertion output.
				gotJSON := formattedJSONValue(gotVal)
				wantJSON := formattedJSONValue(wantVal)

				t.Errorf(
					"wrong result\nexpr: %s\ngot:  %s\nwant: %s",
					test.Expr, gotJSON, wantJSON,
				)
			}
		})
	}
}

func TestScopeExpandEvalBlock(t *testing.T) {
	nestedObjTy := cty.Object(map[string]cty.Type{
		"boop": cty.String,
	})
	schema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"foo":         {Type: cty.String, Optional: true},
			"list_of_obj": {Type: cty.List(nestedObjTy), Optional: true},
		},
		BlockTypes: map[string]*configschema.NestedBlock{
			"bar": {
				Nesting: configschema.NestingMap,
				Block: configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"baz": {Type: cty.String, Optional: true},
					},
				},
			},
		},
	}
	data := &dataForTests{
		LocalValues: map[string]cty.Value{
			"greeting": cty.StringVal("howdy"),
			"list": cty.ListVal([]cty.Value{
				cty.StringVal("elem0"),
				cty.StringVal("elem1"),
			}),
			"map": cty.MapVal(map[string]cty.Value{
				"key1": cty.StringVal("val1"),
				"key2": cty.StringVal("val2"),
			}),
		},
	}

	tests := map[string]struct {
		Config string
		Want   cty.Value
	}{
		"empty": {
			`
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo":         cty.NullVal(cty.String),
				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"baz": cty.String,
				})),
			}),
		},
		"literal attribute": {
			`
			foo = "hello"
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo":         cty.StringVal("hello"),
				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"baz": cty.String,
				})),
			}),
		},
		"variable attribute": {
			`
			foo = local.greeting
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo":         cty.StringVal("howdy"),
				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"baz": cty.String,
				})),
			}),
		},
		"one static block": {
			`
			bar "static" {}
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo":         cty.NullVal(cty.String),
				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
				"bar": cty.MapVal(map[string]cty.Value{
					"static": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.NullVal(cty.String),
					}),
				}),
			}),
		},
		"two static blocks": {
			`
			bar "static0" {
				baz = 0
			}
			bar "static1" {
				baz = 1
			}
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo":         cty.NullVal(cty.String),
				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
				"bar": cty.MapVal(map[string]cty.Value{
					"static0": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("0"),
					}),
					"static1": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("1"),
					}),
				}),
			}),
		},
		"dynamic blocks from list": {
			`
			dynamic "bar" {
				for_each = local.list
				labels = [bar.value]
				content {
					baz = bar.key
				}
			}
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo":         cty.NullVal(cty.String),
				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
				"bar": cty.MapVal(map[string]cty.Value{
					"elem0": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("0"),
					}),
					"elem1": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("1"),
					}),
				}),
			}),
		},
		"dynamic blocks from map": {
			`
			dynamic "bar" {
				for_each = local.map
				labels = [bar.key]
				content {
					baz = bar.value
				}
			}
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo":         cty.NullVal(cty.String),
				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
				"bar": cty.MapVal(map[string]cty.Value{
					"key1": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("val1"),
					}),
					"key2": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("val2"),
					}),
				}),
			}),
		},
		"list-of-object attribute": {
			`
			list_of_obj = [
				{
					boop = local.greeting
				},
			]
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.String),
				"list_of_obj": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"boop": cty.StringVal("howdy"),
					}),
				}),
				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"baz": cty.String,
				})),
			}),
		},
		"list-of-object attribute as blocks": {
			`
			list_of_obj {
				boop = local.greeting
			}
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.String),
				"list_of_obj": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"boop": cty.StringVal("howdy"),
					}),
				}),
				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"baz": cty.String,
				})),
			}),
		},
		"lots of things at once": {
			`
			foo = "whoop"
			bar "static0" {
				baz = "s0"
			}
			dynamic "bar" {
				for_each = local.list
				labels = [bar.value]
				content {
					baz = bar.key
				}
			}
			bar "static1" {
				baz = "s1"
			}
			dynamic "bar" {
				for_each = local.map
				labels = [bar.key]
				content {
					baz = bar.value
				}
			}
			bar "static2" {
				baz = "s2"
			}
			`,
			cty.ObjectVal(map[string]cty.Value{
				"foo":         cty.StringVal("whoop"),
				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
				"bar": cty.MapVal(map[string]cty.Value{
					"key1": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("val1"),
					}),
					"key2": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("val2"),
					}),
					"elem0": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("0"),
					}),
					"elem1": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("1"),
					}),
					"static0": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("s0"),
					}),
					"static1": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("s1"),
					}),
					"static2": cty.ObjectVal(map[string]cty.Value{
						"baz": cty.StringVal("s2"),
					}),
				}),
			}),
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1})
			if len(parseDiags) != 0 {
				t.Errorf("unexpected diagnostics during parse")
				for _, diag := range parseDiags {
					t.Errorf("- %s", diag)
				}
				return
			}

			body := file.Body
			scope := &Scope{
				Data: data,
			}

			body, expandDiags := scope.ExpandBlock(body, schema)
			if expandDiags.HasErrors() {
				t.Fatal(expandDiags.Err())
			}

			got, valDiags := scope.EvalBlock(body, schema)
			if valDiags.HasErrors() {
				t.Fatal(valDiags.Err())
			}

			if !got.RawEquals(test.Want) {
				// We'll JSON-ize our values here just so it's easier to
				// read them in the assertion output.
				gotJSON := formattedJSONValue(got)
				wantJSON := formattedJSONValue(test.Want)

				t.Errorf(
					"wrong result\nconfig: %s\ngot:   %s\nwant:  %s",
					test.Config, gotJSON, wantJSON,
				)
			}

		})
	}

}

func formattedJSONValue(val cty.Value) string {
	val = cty.UnknownAsNull(val) // since JSON can't represent unknowns
	j, err := ctyjson.Marshal(val, val.Type())
	if err != nil {
		panic(err)
	}
	var buf bytes.Buffer
	json.Indent(&buf, j, "", "  ")
	return buf.String()
}

func TestScopeEvalSelfBlock(t *testing.T) {
	data := &dataForTests{
		PathAttrs: map[string]cty.Value{
			"module": cty.StringVal("foo/bar"),
			"cwd":    cty.StringVal("/home/foo/bar"),
			"root":   cty.StringVal("/home/foo"),
		},
		TerraformAttrs: map[string]cty.Value{
			"workspace": cty.StringVal("default"),
		},
	}
	schema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"attr": {
				Type: cty.String,
			},
			"num": {
				Type: cty.Number,
			},
		},
	}

	tests := []struct {
		Config  string
		Self    cty.Value
		KeyData instances.RepetitionData
		Want    map[string]cty.Value
	}{
		{
			Config: `attr = self.foo`,
			Self: cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
			}),
			KeyData: instances.RepetitionData{
				CountIndex: cty.NumberIntVal(0),
			},
			Want: map[string]cty.Value{
				"attr": cty.StringVal("bar"),
				"num":  cty.NullVal(cty.Number),
			},
		},
		{
			Config: `num = count.index`,
			KeyData: instances.RepetitionData{
				CountIndex: cty.NumberIntVal(0),
			},
			Want: map[string]cty.Value{
				"attr": cty.NullVal(cty.String),
				"num":  cty.NumberIntVal(0),
			},
		},
		{
			Config: `attr = each.key`,
			KeyData: instances.RepetitionData{
				EachKey: cty.StringVal("a"),
			},
			Want: map[string]cty.Value{
				"attr": cty.StringVal("a"),
				"num":  cty.NullVal(cty.Number),
			},
		},
		{
			Config: `attr = path.cwd`,
			Want: map[string]cty.Value{
				"attr": cty.StringVal("/home/foo/bar"),
				"num":  cty.NullVal(cty.Number),
			},
		},
		{
			Config: `attr = path.module`,
			Want: map[string]cty.Value{
				"attr": cty.StringVal("foo/bar"),
				"num":  cty.NullVal(cty.Number),
			},
		},
		{
			Config: `attr = path.root`,
			Want: map[string]cty.Value{
				"attr": cty.StringVal("/home/foo"),
				"num":  cty.NullVal(cty.Number),
			},
		},
		{
			Config: `attr = terraform.workspace`,
			Want: map[string]cty.Value{
				"attr": cty.StringVal("default"),
				"num":  cty.NullVal(cty.Number),
			},
		},
	}

	for _, test := range tests {
		t.Run(test.Config, func(t *testing.T) {
			file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1})
			if len(parseDiags) != 0 {
				t.Errorf("unexpected diagnostics during parse")
				for _, diag := range parseDiags {
					t.Errorf("- %s", diag)
				}
				return
			}

			body := file.Body

			scope := &Scope{
				Data: data,
			}

			gotVal, ctxDiags := scope.EvalSelfBlock(body, test.Self, schema, test.KeyData)
			if ctxDiags.HasErrors() {
				t.Fatal(ctxDiags.Err())
			}

			wantVal := cty.ObjectVal(test.Want)

			if !gotVal.RawEquals(wantVal) {
				t.Errorf(
					"wrong result\nexpr: %s\ngot:  %#v\nwant: %#v",
					test.Config, gotVal, wantVal,
				)
			}
		})
	}
}
