// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package integrationtest

import (
	"reflect"
	"sort"
	"testing"

	"github.com/davecgh/go-spew/spew"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/ext/dynblock"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/hcl/v2/json"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"
)

// TestTerraformLike parses both a native syntax and a JSON representation
// of the same HashiCorp Terraform-like configuration structure and then makes
// assertions against the result of each.
//
// Terraform exercises a lot of different HCL codepaths, so this is not
// exhaustive but tries to cover a variety of different relevant scenarios.
func TestTerraformLike(t *testing.T) {
	tests := map[string]func() (*hcl.File, hcl.Diagnostics){
		"native syntax": func() (*hcl.File, hcl.Diagnostics) {
			return hclsyntax.ParseConfig(
				[]byte(terraformLikeNativeSyntax),
				"config.tf", hcl.Pos{Line: 1, Column: 1},
			)
		},
		"JSON": func() (*hcl.File, hcl.Diagnostics) {
			return json.Parse(
				[]byte(terraformLikeJSON),
				"config.tf.json",
			)
		},
	}

	type Variable struct {
		Name string `hcl:"name,label"`
	}
	type Resource struct {
		Type      string         `hcl:"type,label"`
		Name      string         `hcl:"name,label"`
		Config    hcl.Body       `hcl:",remain"`
		DependsOn hcl.Expression `hcl:"depends_on,attr"`
	}
	type Module struct {
		Name      string         `hcl:"name,label"`
		Providers hcl.Expression `hcl:"providers"`
	}
	type Locals struct {
		Config hcl.Body `hcl:",remain"`
	}
	type Root struct {
		Variables []*Variable `hcl:"variable,block"`
		Resources []*Resource `hcl:"resource,block"`
		Modules   []*Module   `hcl:"module,block"`
		Locals    []*Locals   `hcl:"locals,block"`
	}
	instanceDecode := &hcldec.ObjectSpec{
		"image_id": &hcldec.AttrSpec{
			Name:     "image_id",
			Required: true,
			Type:     cty.String,
		},
		"instance_type": &hcldec.AttrSpec{
			Name:     "instance_type",
			Required: true,
			Type:     cty.String,
		},
		"tags": &hcldec.AttrSpec{
			Name:     "tags",
			Required: false,
			Type:     cty.Map(cty.String),
		},
	}
	securityGroupDecode := &hcldec.ObjectSpec{
		"ingress": &hcldec.BlockListSpec{
			TypeName: "ingress",
			Nested: &hcldec.ObjectSpec{
				"cidr_block": &hcldec.AttrSpec{
					Name:     "cidr_block",
					Required: true,
					Type:     cty.String,
				},
			},
		},
	}

	for name, loadFunc := range tests {
		t.Run(name, func(t *testing.T) {
			file, diags := loadFunc()
			if len(diags) != 0 {
				t.Errorf("unexpected diagnostics during parse")
				for _, diag := range diags {
					t.Logf("- %s", diag)
				}
				return
			}

			body := file.Body

			var root Root
			diags = gohcl.DecodeBody(body, nil, &root)
			if len(diags) != 0 {
				t.Errorf("unexpected diagnostics during root eval")
				for _, diag := range diags {
					t.Logf("- %s", diag)
				}
				return
			}

			wantVars := []*Variable{
				{
					Name: "image_id",
				},
			}
			if gotVars := root.Variables; !reflect.DeepEqual(gotVars, wantVars) {
				t.Errorf("wrong Variables\ngot:  %swant: %s", spew.Sdump(gotVars), spew.Sdump(wantVars))
			}

			if got, want := len(root.Resources), 3; got != want {
				t.Fatalf("wrong number of Resources %d; want %d", got, want)
			}

			sort.Slice(root.Resources, func(i, j int) bool {
				return root.Resources[i].Name < root.Resources[j].Name
			})

			t.Run("resource 0", func(t *testing.T) {
				r := root.Resources[0]
				if got, want := r.Type, "happycloud_security_group"; got != want {
					t.Errorf("wrong type %q; want %q", got, want)
				}
				if got, want := r.Name, "private"; got != want {
					t.Errorf("wrong type %q; want %q", got, want)
				}

				// For this one we're including support for the dynamic block
				// extension, since Terraform uses this to allow dynamic
				// generation of blocks within resource configuration.
				forEachCtx := &hcl.EvalContext{
					Variables: map[string]cty.Value{
						"var": cty.ObjectVal(map[string]cty.Value{
							"extra_private_cidr_blocks": cty.ListVal([]cty.Value{
								cty.StringVal("172.16.0.0/12"),
								cty.StringVal("169.254.0.0/16"),
							}),
						}),
					},
				}
				dynBody := dynblock.Expand(r.Config, forEachCtx)

				cfg, diags := hcldec.Decode(dynBody, securityGroupDecode, nil)
				if len(diags) != 0 {
					t.Errorf("unexpected diagnostics decoding Config")
					for _, diag := range diags {
						t.Logf("- %s", diag)
					}
					return
				}
				wantCfg := cty.ObjectVal(map[string]cty.Value{
					"ingress": cty.ListVal([]cty.Value{
						cty.ObjectVal(map[string]cty.Value{
							"cidr_block": cty.StringVal("10.0.0.0/8"),
						}),
						cty.ObjectVal(map[string]cty.Value{
							"cidr_block": cty.StringVal("192.168.0.0/16"),
						}),
						cty.ObjectVal(map[string]cty.Value{
							"cidr_block": cty.StringVal("172.16.0.0/12"),
						}),
						cty.ObjectVal(map[string]cty.Value{
							"cidr_block": cty.StringVal("169.254.0.0/16"),
						}),
					}),
				})
				if !cfg.RawEquals(wantCfg) {
					t.Errorf("wrong config\ngot:  %#v\nwant: %#v", cfg, wantCfg)
				}
			})

			t.Run("resource 1", func(t *testing.T) {
				r := root.Resources[1]
				if got, want := r.Type, "happycloud_security_group"; got != want {
					t.Errorf("wrong type %q; want %q", got, want)
				}
				if got, want := r.Name, "public"; got != want {
					t.Errorf("wrong type %q; want %q", got, want)
				}

				cfg, diags := hcldec.Decode(r.Config, securityGroupDecode, nil)
				if len(diags) != 0 {
					t.Errorf("unexpected diagnostics decoding Config")
					for _, diag := range diags {
						t.Logf("- %s", diag)
					}
					return
				}
				wantCfg := cty.ObjectVal(map[string]cty.Value{
					"ingress": cty.ListVal([]cty.Value{
						cty.ObjectVal(map[string]cty.Value{
							"cidr_block": cty.StringVal("0.0.0.0/0"),
						}),
					}),
				})
				if !cfg.RawEquals(wantCfg) {
					t.Errorf("wrong config\ngot:  %#v\nwant: %#v", cfg, wantCfg)
				}
			})

			t.Run("resource 2", func(t *testing.T) {
				r := root.Resources[2]
				if got, want := r.Type, "happycloud_instance"; got != want {
					t.Errorf("wrong type %q; want %q", got, want)
				}
				if got, want := r.Name, "test"; got != want {
					t.Errorf("wrong type %q; want %q", got, want)
				}

				vars := hcldec.Variables(r.Config, &hcldec.AttrSpec{
					Name: "image_id",
					Type: cty.String,
				})
				if got, want := len(vars), 1; got != want {
					t.Errorf("wrong number of variables in image_id %#v; want %#v", got, want)
				}
				if got, want := vars[0].RootName(), "var"; got != want {
					t.Errorf("wrong image_id variable RootName %#v; want %#v", got, want)
				}

				ctx := &hcl.EvalContext{
					Variables: map[string]cty.Value{
						"var": cty.ObjectVal(map[string]cty.Value{
							"image_id": cty.StringVal("image-1234"),
						}),
					},
				}
				cfg, diags := hcldec.Decode(r.Config, instanceDecode, ctx)
				if len(diags) != 0 {
					t.Errorf("unexpected diagnostics decoding Config")
					for _, diag := range diags {
						t.Logf("- %s", diag)
					}
					return
				}
				wantCfg := cty.ObjectVal(map[string]cty.Value{
					"instance_type": cty.StringVal("z3.weedy"),
					"image_id":      cty.StringVal("image-1234"),
					"tags": cty.MapVal(map[string]cty.Value{
						"Name":        cty.StringVal("foo"),
						"Environment": cty.StringVal("prod"),
					}),
				})
				if !cfg.RawEquals(wantCfg) {
					t.Errorf("wrong config\ngot:  %#v\nwant: %#v", cfg, wantCfg)
				}

				exprs, diags := hcl.ExprList(r.DependsOn)
				if len(diags) != 0 {
					t.Errorf("unexpected diagnostics extracting depends_on")
					for _, diag := range diags {
						t.Logf("- %s", diag)
					}
					return
				}
				if got, want := len(exprs), 1; got != want {
					t.Errorf("wrong number of depends_on exprs %#v; want %#v", got, want)
				}

				traversal, diags := hcl.AbsTraversalForExpr(exprs[0])
				if len(diags) != 0 {
					t.Errorf("unexpected diagnostics decoding depends_on[0]")
					for _, diag := range diags {
						t.Logf("- %s", diag)
					}
					return
				}
				if got, want := len(traversal), 2; got != want {
					t.Errorf("wrong number of depends_on traversal steps %#v; want %#v", got, want)
				}
				if got, want := traversal.RootName(), "happycloud_security_group"; got != want {
					t.Errorf("wrong depends_on traversal RootName %#v; want %#v", got, want)
				}
			})

			t.Run("module", func(t *testing.T) {
				if got, want := len(root.Modules), 1; got != want {
					t.Fatalf("wrong number of Modules %d; want %d", got, want)
				}
				mod := root.Modules[0]
				if got, want := mod.Name, "foo"; got != want {
					t.Errorf("wrong module name %q; want %q", got, want)
				}

				pExpr := mod.Providers
				pairs, diags := hcl.ExprMap(pExpr)
				if len(diags) != 0 {
					t.Errorf("unexpected diagnostics extracting providers")
					for _, diag := range diags {
						t.Logf("- %s", diag)
					}
				}
				if got, want := len(pairs), 1; got != want {
					t.Fatalf("wrong number of key/value pairs in providers %d; want %d", got, want)
				}

				pair := pairs[0]
				kt, diags := hcl.AbsTraversalForExpr(pair.Key)
				if len(diags) != 0 {
					t.Errorf("unexpected diagnostics extracting providers key %#v", pair.Key)
					for _, diag := range diags {
						t.Logf("- %s", diag)
					}
				}
				vt, diags := hcl.AbsTraversalForExpr(pair.Value)
				if len(diags) != 0 {
					t.Errorf("unexpected diagnostics extracting providers value  %#v", pair.Value)
					for _, diag := range diags {
						t.Logf("- %s", diag)
					}
				}

				if got, want := len(kt), 1; got != want {
					t.Fatalf("wrong number of key traversal steps %d; want %d", got, want)
				}
				if got, want := len(vt), 2; got != want {
					t.Fatalf("wrong number of value traversal steps %d; want %d", got, want)
				}

				if got, want := kt.RootName(), "null"; got != want {
					t.Errorf("wrong number key traversal root %s; want %s", got, want)
				}
				if got, want := vt.RootName(), "null"; got != want {
					t.Errorf("wrong number value traversal root %s; want %s", got, want)
				}
				if at, ok := vt[1].(hcl.TraverseAttr); ok {
					if got, want := at.Name, "foo"; got != want {
						t.Errorf("wrong number value traversal attribute name %s; want %s", got, want)
					}
				} else {
					t.Errorf("wrong value traversal [1] type %T; want hcl.TraverseAttr", vt[1])
				}
			})

			t.Run("locals", func(t *testing.T) {
				locals := root.Locals[0]
				attrs, diags := locals.Config.JustAttributes()
				if diags.HasErrors() {
					t.Fatal(diags)
				}

				ctx := &hcl.EvalContext{
					Functions: map[string]function.Function{
						"func": function.New(&function.Spec{
							Params: []function.Parameter{{Type: cty.String}},
							Type:   function.StaticReturnType(cty.String),
							Impl: func([]cty.Value, cty.Type) (cty.Value, error) {
								return cty.StringVal("func_result"), nil
							},
						}),
						"scoped::func": function.New(&function.Spec{
							Params: []function.Parameter{{Type: cty.String}},
							Type:   function.StaticReturnType(cty.String),
							Impl: func([]cty.Value, cty.Type) (cty.Value, error) {
								return cty.StringVal("scoped::func_result"), nil
							},
						}),
					},
				}

				res := attrs["func_result"]
				funcVal, diags := res.Expr.Value(ctx)
				if diags.HasErrors() {
					t.Fatal(diags)
				}

				wantVal := cty.StringVal("func_result")

				if !funcVal.RawEquals(wantVal) {
					t.Errorf("expected %#v, got %#v", wantVal, funcVal)
				}

				res = attrs["scoped_func_result"]
				funcVal, diags = res.Expr.Value(ctx)
				if diags.HasErrors() {
					t.Fatal(diags)
				}

				wantVal = cty.StringVal("scoped::func_result")

				if !funcVal.RawEquals(wantVal) {
					t.Errorf("expected %#v, got %#v", wantVal, funcVal)
				}
			})
		})
	}
}

const terraformLikeNativeSyntax = `

variable "image_id" {
}

locals {
  func_result        = func("arg")
  scoped_func_result = scoped::func("arg")
}

resource "happycloud_instance" "test" {
  instance_type = "z3.weedy"
  image_id      = var.image_id

  tags = {
  "Name" = "foo"
  "${"Environment"}" = "prod"
  }

  depends_on = [
    happycloud_security_group.public,
  ]
}

resource "happycloud_security_group" "public" {
  ingress {
    cidr_block = "0.0.0.0/0"
  }
}

resource "happycloud_security_group" "private" {
  ingress {
    cidr_block = "10.0.0.0/8"
  }
  ingress {
    cidr_block = "192.168.0.0/16"
  }
  dynamic "ingress" {
    for_each = var.extra_private_cidr_blocks
    content {
      cidr_block = ingress.value
    }
  }
}

module "foo" {
  providers = {
    null = null.foo
  }
}

`

const terraformLikeJSON = `
{
  "variable": {
    "image_id": {}
  },
  "locals": {
    "func_result": "${func(\"arg\")}",
	"scoped_func_result": "${scoped::func(\"arg\")}"
  },
  "resource": {
    "happycloud_instance": {
      "test": {
        "instance_type": "z3.weedy",
        "image_id": "${var.image_id}",
        "tags": {
            "Name": "foo",
            "${\"Environment\"}": "prod"
        },
        "depends_on": [
          "happycloud_security_group.public"
        ]
      }
    },
    "happycloud_security_group": {
      "public": {
        "ingress": {
          "cidr_block": "0.0.0.0/0"
        }
      },
      "private": {
        "ingress": [
          {
            "cidr_block": "10.0.0.0/8"
          },
          {
            "cidr_block": "192.168.0.0/16"
          }
        ],
        "dynamic": {
          "ingress": {
            "for_each": "${var.extra_private_cidr_blocks}",
            "iterator": "block",
            "content": {
              "cidr_block": "${block.value}"
            }
          }
        }
      }
    }
  },
  "module": {
    "foo": {
      "providers": {
        "null": "null.foo"
      }
    }
  }
}
`
