package config

import (
	"reflect"
	"testing"

	"github.com/zclconf/go-cty/cty"

	hcl2 "github.com/hashicorp/hcl/v2"
	gohcl2 "github.com/hashicorp/hcl/v2/gohcl"
)

func TestHCL2ConfigurableConfigurable(t *testing.T) {
	var _ configurable = new(hcl2Configurable)
}

func TestHCL2Basic(t *testing.T) {
	loader := globalHCL2Loader
	cbl, _, err := loader.loadFile("testdata/basic-hcl2.tf")
	if err != nil {
		if diags, isDiags := err.(hcl2.Diagnostics); isDiags {
			for _, diag := range diags {
				t.Logf("- %s", diag.Error())
			}
			t.Fatalf("unexpected diagnostics in load")
		} else {
			t.Fatalf("unexpected error in load: %s", err)
		}
	}

	cfg, err := cbl.Config()
	if err != nil {
		if diags, isDiags := err.(hcl2.Diagnostics); isDiags {
			for _, diag := range diags {
				t.Logf("- %s", diag.Error())
			}
			t.Fatalf("unexpected diagnostics in decode")
		} else {
			t.Fatalf("unexpected error in decode: %s", err)
		}
	}

	// Unfortunately the config structure isn't DeepEqual-friendly because
	// of all the nested RawConfig, etc structures, so we'll need to
	// hand-assert each item.

	// The "terraform" block
	if cfg.Terraform == nil {
		t.Fatalf("Terraform field is nil")
	}
	if got, want := cfg.Terraform.RequiredVersion, "foo"; got != want {
		t.Errorf("wrong Terraform.RequiredVersion %q; want %q", got, want)
	}
	if cfg.Terraform.Backend == nil {
		t.Fatalf("Terraform.Backend is nil")
	}
	if got, want := cfg.Terraform.Backend.Type, "baz"; got != want {
		t.Errorf("wrong Terraform.Backend.Type %q; want %q", got, want)
	}
	if got, want := cfg.Terraform.Backend.RawConfig.Raw, map[string]interface{}{"something": "nothing"}; !reflect.DeepEqual(got, want) {
		t.Errorf("wrong Terraform.Backend.RawConfig.Raw %#v; want %#v", got, want)
	}

	// The "atlas" block
	if cfg.Atlas == nil {
		t.Fatalf("Atlas field is nil")
	}
	if got, want := cfg.Atlas.Name, "example/foo"; got != want {
		t.Errorf("wrong Atlas.Name %q; want %q", got, want)
	}

	// "module" blocks
	if got, want := len(cfg.Modules), 1; got != want {
		t.Errorf("Modules slice has wrong length %#v; want %#v", got, want)
	} else {
		m := cfg.Modules[0]
		if got, want := m.Name, "child"; got != want {
			t.Errorf("wrong Modules[0].Name %#v; want %#v", got, want)
		}
		if got, want := m.Source, "./baz"; got != want {
			t.Errorf("wrong Modules[0].Source %#v; want %#v", got, want)
		}
		want := map[string]string{"toasty": "true"}
		var got map[string]string
		gohcl2.DecodeBody(m.RawConfig.Body, nil, &got)
		if !reflect.DeepEqual(got, want) {
			t.Errorf("wrong Modules[0].RawConfig.Body %#v; want %#v", got, want)
		}
	}

	// "resource" blocks
	if got, want := len(cfg.Resources), 5; got != want {
		t.Errorf("Resources slice has wrong length %#v; want %#v", got, want)
	} else {
		{
			r := cfg.Resources[0]

			if got, want := r.Id(), "aws_security_group.firewall"; got != want {
				t.Errorf("wrong Resources[0].Id() %#v; want %#v", got, want)
			}

			wantConfig := map[string]string{}
			var gotConfig map[string]string
			gohcl2.DecodeBody(r.RawConfig.Body, nil, &gotConfig)
			if !reflect.DeepEqual(gotConfig, wantConfig) {
				t.Errorf("wrong Resources[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
			}

			wantCount := map[string]string{"count": "5"}
			var gotCount map[string]string
			gohcl2.DecodeBody(r.RawCount.Body, nil, &gotCount)
			if !reflect.DeepEqual(gotCount, wantCount) {
				t.Errorf("wrong Resources[0].RawCount.Body %#v; want %#v", gotCount, wantCount)
			}
			if got, want := r.RawCount.Key, "count"; got != want {
				t.Errorf("wrong Resources[0].RawCount.Key %#v; want %#v", got, want)
			}

			if got, want := len(r.Provisioners), 0; got != want {
				t.Errorf("wrong Resources[0].Provisioners length %#v; want %#v", got, want)
			}
			if got, want := len(r.DependsOn), 0; got != want {
				t.Errorf("wrong Resources[0].DependsOn length %#v; want %#v", got, want)
			}
			if got, want := r.Provider, "another"; got != want {
				t.Errorf("wrong Resources[0].Provider %#v; want %#v", got, want)
			}
			if got, want := r.Lifecycle, (ResourceLifecycle{}); !reflect.DeepEqual(got, want) {
				t.Errorf("wrong Resources[0].Lifecycle %#v; want %#v", got, want)
			}
		}
		{
			r := cfg.Resources[1]

			if got, want := r.Id(), "aws_instance.web"; got != want {
				t.Errorf("wrong Resources[1].Id() %#v; want %#v", got, want)
			}
			if got, want := r.Provider, ""; got != want {
				t.Errorf("wrong Resources[1].Provider %#v; want %#v", got, want)
			}

			if got, want := len(r.Provisioners), 1; got != want {
				t.Errorf("wrong Resources[1].Provisioners length %#v; want %#v", got, want)
			} else {
				p := r.Provisioners[0]

				if got, want := p.Type, "file"; got != want {
					t.Errorf("wrong Resources[1].Provisioners[0].Type %#v; want %#v", got, want)
				}

				wantConfig := map[string]string{
					"source":      "foo",
					"destination": "bar",
				}
				var gotConfig map[string]string
				gohcl2.DecodeBody(p.RawConfig.Body, nil, &gotConfig)
				if !reflect.DeepEqual(gotConfig, wantConfig) {
					t.Errorf("wrong Resources[1].Provisioners[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
				}

				wantConn := map[string]string{
					"default": "true",
				}
				var gotConn map[string]string
				gohcl2.DecodeBody(p.ConnInfo.Body, nil, &gotConn)
				if !reflect.DeepEqual(gotConn, wantConn) {
					t.Errorf("wrong Resources[1].Provisioners[0].ConnInfo.Body %#v; want %#v", gotConn, wantConn)
				}
			}

			// We'll use these throwaway structs to more easily decode and
			// compare the main config body.
			type instanceNetworkInterface struct {
				DeviceIndex int    `hcl:"device_index"`
				Description string `hcl:"description"`
			}
			type instanceConfig struct {
				AMI              string                   `hcl:"ami"`
				SecurityGroups   []string                 `hcl:"security_groups"`
				NetworkInterface instanceNetworkInterface `hcl:"network_interface,block"`
			}
			var gotConfig instanceConfig
			wantConfig := instanceConfig{
				AMI:            "ami-abc123",
				SecurityGroups: []string{"foo", "sg-firewall"},
				NetworkInterface: instanceNetworkInterface{
					DeviceIndex: 0,
					Description: "Main network interface",
				},
			}
			ctx := &hcl2.EvalContext{
				Variables: map[string]cty.Value{
					"var": cty.ObjectVal(map[string]cty.Value{
						"foo": cty.StringVal("ami-abc123"),
					}),
					"aws_security_group": cty.ObjectVal(map[string]cty.Value{
						"firewall": cty.ObjectVal(map[string]cty.Value{
							"foo": cty.StringVal("sg-firewall"),
						}),
					}),
				},
			}
			diags := gohcl2.DecodeBody(r.RawConfig.Body, ctx, &gotConfig)
			if len(diags) != 0 {
				t.Errorf("unexpected diagnostics decoding Resources[1].RawConfig.Body")
				for _, diag := range diags {
					t.Logf("- %s", diag.Error())
				}
			}
			if !reflect.DeepEqual(gotConfig, wantConfig) {
				t.Errorf("wrong Resources[1].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
			}

		}
		{
			r := cfg.Resources[2]

			if got, want := r.Id(), "aws_instance.db"; got != want {
				t.Errorf("wrong Resources[2].Id() %#v; want %#v", got, want)
			}
			if got, want := r.DependsOn, []string{"aws_instance.web"}; !reflect.DeepEqual(got, want) {
				t.Errorf("wrong Resources[2].DependsOn %#v; want %#v", got, want)
			}

			if got, want := len(r.Provisioners), 1; got != want {
				t.Errorf("wrong Resources[2].Provisioners length %#v; want %#v", got, want)
			} else {
				p := r.Provisioners[0]

				if got, want := p.Type, "file"; got != want {
					t.Errorf("wrong Resources[2].Provisioners[0].Type %#v; want %#v", got, want)
				}

				wantConfig := map[string]string{
					"source":      "here",
					"destination": "there",
				}
				var gotConfig map[string]string
				gohcl2.DecodeBody(p.RawConfig.Body, nil, &gotConfig)
				if !reflect.DeepEqual(gotConfig, wantConfig) {
					t.Errorf("wrong Resources[2].Provisioners[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
				}

				wantConn := map[string]string{
					"default": "false",
				}
				var gotConn map[string]string
				gohcl2.DecodeBody(p.ConnInfo.Body, nil, &gotConn)
				if !reflect.DeepEqual(gotConn, wantConn) {
					t.Errorf("wrong Resources[2].Provisioners[0].ConnInfo.Body %#v; want %#v", gotConn, wantConn)
				}
			}
		}
		{
			r := cfg.Resources[3]

			if got, want := r.Id(), "data.do.simple"; got != want {
				t.Errorf("wrong Resources[3].Id() %#v; want %#v", got, want)
			}
			if got, want := r.DependsOn, []string(nil); !reflect.DeepEqual(got, want) {
				t.Errorf("wrong Resources[3].DependsOn %#v; want %#v", got, want)
			}
			if got, want := r.Provider, "do.foo"; got != want {
				t.Errorf("wrong Resources[3].Provider %#v; want %#v", got, want)
			}

			wantConfig := map[string]string{
				"foo": "baz",
			}
			var gotConfig map[string]string
			gohcl2.DecodeBody(r.RawConfig.Body, nil, &gotConfig)
			if !reflect.DeepEqual(gotConfig, wantConfig) {
				t.Errorf("wrong Resources[3].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
			}
		}
		{
			r := cfg.Resources[4]

			if got, want := r.Id(), "data.do.depends"; got != want {
				t.Errorf("wrong Resources[4].Id() %#v; want %#v", got, want)
			}
			if got, want := r.DependsOn, []string{"data.do.simple"}; !reflect.DeepEqual(got, want) {
				t.Errorf("wrong Resources[4].DependsOn %#v; want %#v", got, want)
			}
			if got, want := r.Provider, ""; got != want {
				t.Errorf("wrong Resources[4].Provider %#v; want %#v", got, want)
			}

			wantConfig := map[string]string{}
			var gotConfig map[string]string
			gohcl2.DecodeBody(r.RawConfig.Body, nil, &gotConfig)
			if !reflect.DeepEqual(gotConfig, wantConfig) {
				t.Errorf("wrong Resources[4].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
			}
		}
	}

	// "variable" blocks
	if got, want := len(cfg.Variables), 3; got != want {
		t.Errorf("Variables slice has wrong length %#v; want %#v", got, want)
	} else {
		{
			v := cfg.Variables[0]

			if got, want := v.Name, "foo"; got != want {
				t.Errorf("wrong Variables[0].Name %#v; want %#v", got, want)
			}
			if got, want := v.Default, "bar"; got != want {
				t.Errorf("wrong Variables[0].Default %#v; want %#v", got, want)
			}
			if got, want := v.Description, "barbar"; got != want {
				t.Errorf("wrong Variables[0].Description %#v; want %#v", got, want)
			}
			if got, want := v.DeclaredType, ""; got != want {
				t.Errorf("wrong Variables[0].DeclaredType %#v; want %#v", got, want)
			}
		}
		{
			v := cfg.Variables[1]

			if got, want := v.Name, "bar"; got != want {
				t.Errorf("wrong Variables[1].Name %#v; want %#v", got, want)
			}
			if got, want := v.Default, interface{}(nil); got != want {
				t.Errorf("wrong Variables[1].Default %#v; want %#v", got, want)
			}
			if got, want := v.Description, ""; got != want {
				t.Errorf("wrong Variables[1].Description %#v; want %#v", got, want)
			}
			if got, want := v.DeclaredType, "string"; got != want {
				t.Errorf("wrong Variables[1].DeclaredType %#v; want %#v", got, want)
			}
		}
		{
			v := cfg.Variables[2]

			if got, want := v.Name, "baz"; got != want {
				t.Errorf("wrong Variables[2].Name %#v; want %#v", got, want)
			}
			if got, want := v.Default, map[string]interface{}{"key": "value"}; !reflect.DeepEqual(got, want) {
				t.Errorf("wrong Variables[2].Default %#v; want %#v", got, want)
			}
			if got, want := v.Description, ""; got != want {
				t.Errorf("wrong Variables[2].Description %#v; want %#v", got, want)
			}
			if got, want := v.DeclaredType, "map"; got != want {
				t.Errorf("wrong Variables[2].DeclaredType %#v; want %#v", got, want)
			}
		}
	}

	// "output" blocks
	if got, want := len(cfg.Outputs), 2; got != want {
		t.Errorf("Outputs slice has wrong length %#v; want %#v", got, want)
	} else {
		{
			o := cfg.Outputs[0]

			if got, want := o.Name, "web_ip"; got != want {
				t.Errorf("wrong Outputs[0].Name %#v; want %#v", got, want)
			}
			if got, want := o.DependsOn, []string(nil); !reflect.DeepEqual(got, want) {
				t.Errorf("wrong Outputs[0].DependsOn %#v; want %#v", got, want)
			}
			if got, want := o.Description, ""; got != want {
				t.Errorf("wrong Outputs[0].Description %#v; want %#v", got, want)
			}
			if got, want := o.Sensitive, true; got != want {
				t.Errorf("wrong Outputs[0].Sensitive %#v; want %#v", got, want)
			}

			wantConfig := map[string]string{
				"value": "312.213.645.123",
			}
			var gotConfig map[string]string
			ctx := &hcl2.EvalContext{
				Variables: map[string]cty.Value{
					"aws_instance": cty.ObjectVal(map[string]cty.Value{
						"web": cty.ObjectVal(map[string]cty.Value{
							"private_ip": cty.StringVal("312.213.645.123"),
						}),
					}),
				},
			}
			gohcl2.DecodeBody(o.RawConfig.Body, ctx, &gotConfig)
			if !reflect.DeepEqual(gotConfig, wantConfig) {
				t.Errorf("wrong Outputs[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
			}
		}
		{
			o := cfg.Outputs[1]

			if got, want := o.Name, "web_id"; got != want {
				t.Errorf("wrong Outputs[1].Name %#v; want %#v", got, want)
			}
			if got, want := o.DependsOn, []string{"aws_instance.db"}; !reflect.DeepEqual(got, want) {
				t.Errorf("wrong Outputs[1].DependsOn %#v; want %#v", got, want)
			}
			if got, want := o.Description, "The ID"; got != want {
				t.Errorf("wrong Outputs[1].Description %#v; want %#v", got, want)
			}
			if got, want := o.Sensitive, false; got != want {
				t.Errorf("wrong Outputs[1].Sensitive %#v; want %#v", got, want)
			}
		}
	}

	// "provider" blocks
	if got, want := len(cfg.ProviderConfigs), 2; got != want {
		t.Errorf("ProviderConfigs slice has wrong length %#v; want %#v", got, want)
	} else {
		{
			p := cfg.ProviderConfigs[0]

			if got, want := p.Name, "aws"; got != want {
				t.Errorf("wrong ProviderConfigs[0].Name %#v; want %#v", got, want)
			}
			if got, want := p.Alias, ""; got != want {
				t.Errorf("wrong ProviderConfigs[0].Alias %#v; want %#v", got, want)
			}
			if got, want := p.Version, "1.0.0"; got != want {
				t.Errorf("wrong ProviderConfigs[0].Version %#v; want %#v", got, want)
			}

			wantConfig := map[string]string{
				"access_key": "foo",
				"secret_key": "bar",
			}
			var gotConfig map[string]string
			gohcl2.DecodeBody(p.RawConfig.Body, nil, &gotConfig)
			if !reflect.DeepEqual(gotConfig, wantConfig) {
				t.Errorf("wrong ProviderConfigs[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
			}

		}
		{
			p := cfg.ProviderConfigs[1]

			if got, want := p.Name, "do"; got != want {
				t.Errorf("wrong ProviderConfigs[1].Name %#v; want %#v", got, want)
			}
			if got, want := p.Alias, "fum"; got != want {
				t.Errorf("wrong ProviderConfigs[1].Alias %#v; want %#v", got, want)
			}
			if got, want := p.Version, ""; got != want {
				t.Errorf("wrong ProviderConfigs[1].Version %#v; want %#v", got, want)
			}

		}
	}

	// "locals" definitions
	if got, want := len(cfg.Locals), 5; got != want {
		t.Errorf("Locals slice has wrong length %#v; want %#v", got, want)
	} else {
		{
			l := cfg.Locals[0]

			if got, want := l.Name, "security_group_ids"; got != want {
				t.Errorf("wrong Locals[0].Name %#v; want %#v", got, want)
			}

			wantConfig := map[string][]string{
				"value": []string{"sg-abc123"},
			}
			var gotConfig map[string][]string
			ctx := &hcl2.EvalContext{
				Variables: map[string]cty.Value{
					"aws_security_group": cty.ObjectVal(map[string]cty.Value{
						"firewall": cty.ObjectVal(map[string]cty.Value{
							"id": cty.StringVal("sg-abc123"),
						}),
					}),
				},
			}
			gohcl2.DecodeBody(l.RawConfig.Body, ctx, &gotConfig)
			if !reflect.DeepEqual(gotConfig, wantConfig) {
				t.Errorf("wrong Locals[0].RawConfig.Body %#v; want %#v", gotConfig, wantConfig)
			}
		}
		{
			l := cfg.Locals[1]

			if got, want := l.Name, "web_ip"; got != want {
				t.Errorf("wrong Locals[1].Name %#v; want %#v", got, want)
			}
		}
		{
			l := cfg.Locals[2]

			if got, want := l.Name, "literal"; got != want {
				t.Errorf("wrong Locals[2].Name %#v; want %#v", got, want)
			}
		}
		{
			l := cfg.Locals[3]

			if got, want := l.Name, "literal_list"; got != want {
				t.Errorf("wrong Locals[3].Name %#v; want %#v", got, want)
			}
		}
		{
			l := cfg.Locals[4]

			if got, want := l.Name, "literal_map"; got != want {
				t.Errorf("wrong Locals[4].Name %#v; want %#v", got, want)
			}
		}
	}
}
