package parser

import (
	"fmt"
	"io/ioutil"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"testing"

	"github.com/hashicorp/hcl/hcl/ast"
	"github.com/hashicorp/hcl/hcl/token"
)

func TestType(t *testing.T) {
	var literals = []struct {
		typ token.Type
		src string
	}{
		{token.STRING, `foo = "foo"`},
		{token.NUMBER, `foo = 123`},
		{token.NUMBER, `foo = -29`},
		{token.FLOAT, `foo = 123.12`},
		{token.FLOAT, `foo = -123.12`},
		{token.BOOL, `foo = true`},
		{token.HEREDOC, "foo = <<EOF\nHello\nWorld\nEOF"},
	}

	for _, l := range literals {
		p := newParser([]byte(l.src))
		item, err := p.objectItem()
		if err != nil {
			t.Error(err)
		}

		lit, ok := item.Val.(*ast.LiteralType)
		if !ok {
			t.Errorf("node should be of type LiteralType, got: %T", item.Val)
		}

		if lit.Token.Type != l.typ {
			t.Errorf("want: %s, got: %s", l.typ, lit.Token.Type)
		}
	}
}

func TestListType(t *testing.T) {
	var literals = []struct {
		src    string
		tokens []token.Type
	}{
		{
			`foo = ["123", 123]`,
			[]token.Type{token.STRING, token.NUMBER},
		},
		{
			`foo = [123, "123",]`,
			[]token.Type{token.NUMBER, token.STRING},
		},
		{
			`foo = [false]`,
			[]token.Type{token.BOOL},
		},
		{
			`foo = []`,
			[]token.Type{},
		},
		{
			`foo = [1,
"string",
<<EOF
heredoc contents
EOF
]`,
			[]token.Type{token.NUMBER, token.STRING, token.HEREDOC},
		},
	}

	for _, l := range literals {
		p := newParser([]byte(l.src))
		item, err := p.objectItem()
		if err != nil {
			t.Error(err)
		}

		list, ok := item.Val.(*ast.ListType)
		if !ok {
			t.Errorf("node should be of type LiteralType, got: %T", item.Val)
		}

		tokens := []token.Type{}
		for _, li := range list.List {
			if tp, ok := li.(*ast.LiteralType); ok {
				tokens = append(tokens, tp.Token.Type)
			}
		}

		equals(t, l.tokens, tokens)
	}
}

func TestListOfMaps(t *testing.T) {
	src := `foo = [
    {key = "bar"},
    {key = "baz", key2 = "qux"},
  ]`
	p := newParser([]byte(src))

	file, err := p.Parse()
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	// Here we make all sorts of assumptions about the input structure w/ type
	// assertions. The intent is only for this to be a "smoke test" ensuring
	// parsing actually performed its duty - giving this test something a bit
	// more robust than _just_ "no error occurred".
	expected := []string{`"bar"`, `"baz"`, `"qux"`}
	actual := make([]string, 0, 3)
	ol := file.Node.(*ast.ObjectList)
	objItem := ol.Items[0]
	list := objItem.Val.(*ast.ListType)
	for _, node := range list.List {
		obj := node.(*ast.ObjectType)
		for _, item := range obj.List.Items {
			val := item.Val.(*ast.LiteralType)
			actual = append(actual, val.Token.Text)
		}

	}
	if !reflect.DeepEqual(expected, actual) {
		t.Fatalf("Expected: %#v, got %#v", expected, actual)
	}
}

func TestListOfMaps_requiresComma(t *testing.T) {
	src := `foo = [
    {key = "bar"}
    {key = "baz"}
  ]`
	p := newParser([]byte(src))

	_, err := p.Parse()
	if err == nil {
		t.Fatalf("Expected error, got none!")
	}

	expected := "error parsing list, expected comma or list end"
	if !strings.Contains(err.Error(), expected) {
		t.Fatalf("Expected err:\n  %s\nTo contain:\n  %s\n", err, expected)
	}
}

func TestListType_leadComment(t *testing.T) {
	var literals = []struct {
		src     string
		comment []string
	}{
		{
			`foo = [
			1,
			# bar
			2,
			3,
			]`,
			[]string{"", "# bar", ""},
		},
	}

	for _, l := range literals {
		p := newParser([]byte(l.src))
		item, err := p.objectItem()
		if err != nil {
			t.Fatal(err)
		}

		list, ok := item.Val.(*ast.ListType)
		if !ok {
			t.Fatalf("node should be of type LiteralType, got: %T", item.Val)
		}

		if len(list.List) != len(l.comment) {
			t.Fatalf("bad: %d", len(list.List))
		}

		for i, li := range list.List {
			lt := li.(*ast.LiteralType)
			comment := l.comment[i]

			if (lt.LeadComment == nil) != (comment == "") {
				t.Fatalf("bad: %#v", lt)
			}

			if comment == "" {
				continue
			}

			actual := lt.LeadComment.List[0].Text
			if actual != comment {
				t.Fatalf("bad: %q %q", actual, comment)
			}
		}
	}
}

func TestListType_lineComment(t *testing.T) {
	var literals = []struct {
		src     string
		comment []string
	}{
		{
			`foo = [
			1,
			2, # bar
			3,
			]`,
			[]string{"", "# bar", ""},
		},
	}

	for _, l := range literals {
		p := newParser([]byte(l.src))
		item, err := p.objectItem()
		if err != nil {
			t.Fatal(err)
		}

		list, ok := item.Val.(*ast.ListType)
		if !ok {
			t.Fatalf("node should be of type LiteralType, got: %T", item.Val)
		}

		if len(list.List) != len(l.comment) {
			t.Fatalf("bad: %d", len(list.List))
		}

		for i, li := range list.List {
			lt := li.(*ast.LiteralType)
			comment := l.comment[i]

			if (lt.LineComment == nil) != (comment == "") {
				t.Fatalf("bad: %s", lt.Token.Value())
			}

			if comment == "" {
				continue
			}

			actual := lt.LineComment.List[0].Text
			if actual != comment {
				t.Fatalf("bad: %q %q", actual, comment)
			}
		}
	}
}

func TestObjectType(t *testing.T) {
	var literals = []struct {
		src      string
		nodeType []ast.Node
		itemLen  int
	}{
		{
			`foo = {}`,
			nil,
			0,
		},
		{
			`foo = {
				bar = "fatih"
			 }`,
			[]ast.Node{&ast.LiteralType{}},
			1,
		},
		{
			`foo = {
				bar = "fatih"
				baz = ["arslan"]
			 }`,
			[]ast.Node{
				&ast.LiteralType{},
				&ast.ListType{},
			},
			2,
		},
		{
			`foo = {
				bar {}
			 }`,
			[]ast.Node{
				&ast.ObjectType{},
			},
			1,
		},
		{
			`foo {
				bar {}
				foo = true
			 }`,
			[]ast.Node{
				&ast.ObjectType{},
				&ast.LiteralType{},
			},
			2,
		},
	}

	for _, l := range literals {
		t.Logf("Source: %s", l.src)

		p := newParser([]byte(l.src))
		// p.enableTrace = true
		item, err := p.objectItem()
		if err != nil {
			t.Error(err)
			continue
		}

		// we know that the ObjectKey name is foo for all cases, what matters
		// is the object
		obj, ok := item.Val.(*ast.ObjectType)
		if !ok {
			t.Errorf("node should be of type LiteralType, got: %T", item.Val)
			continue
		}

		// check if the total length of items are correct
		equals(t, l.itemLen, len(obj.List.Items))

		// check if the types are correct
		for i, item := range obj.List.Items {
			equals(t, reflect.TypeOf(l.nodeType[i]), reflect.TypeOf(item.Val))
		}
	}
}

func TestObjectKey(t *testing.T) {
	keys := []struct {
		exp []token.Type
		src string
	}{
		{[]token.Type{token.IDENT}, `foo {}`},
		{[]token.Type{token.IDENT}, `foo = {}`},
		{[]token.Type{token.IDENT}, `foo = bar`},
		{[]token.Type{token.IDENT}, `foo = 123`},
		{[]token.Type{token.IDENT}, `foo = "${var.bar}`},
		{[]token.Type{token.STRING}, `"foo" {}`},
		{[]token.Type{token.STRING}, `"foo" = {}`},
		{[]token.Type{token.STRING}, `"foo" = "${var.bar}`},
		{[]token.Type{token.IDENT, token.IDENT}, `foo bar {}`},
		{[]token.Type{token.IDENT, token.STRING}, `foo "bar" {}`},
		{[]token.Type{token.STRING, token.IDENT}, `"foo" bar {}`},
		{[]token.Type{token.IDENT, token.IDENT, token.IDENT}, `foo bar baz {}`},
	}

	for _, k := range keys {
		p := newParser([]byte(k.src))
		keys, err := p.objectKey()
		if err != nil {
			t.Fatal(err)
		}

		tokens := []token.Type{}
		for _, o := range keys {
			tokens = append(tokens, o.Token.Type)
		}

		equals(t, k.exp, tokens)
	}

	errKeys := []struct {
		src string
	}{
		{`foo 12 {}`},
		{`foo bar = {}`},
		{`foo []`},
		{`12 {}`},
	}

	for _, k := range errKeys {
		p := newParser([]byte(k.src))
		_, err := p.objectKey()
		if err == nil {
			t.Errorf("case '%s' should give an error", k.src)
		}
	}
}

func TestCommentGroup(t *testing.T) {
	var cases = []struct {
		src    string
		groups int
	}{
		{"# Hello\n# World", 1},
		{"# Hello\r\n# Windows", 1},
	}

	for _, tc := range cases {
		t.Run(tc.src, func(t *testing.T) {
			p := newParser([]byte(tc.src))
			file, err := p.Parse()
			if err != nil {
				t.Fatalf("parse error: %s", err)
			}

			if len(file.Comments) != tc.groups {
				t.Fatalf("bad: %#v", file.Comments)
			}
		})
	}
}

// Official HCL tests
func TestParse(t *testing.T) {
	cases := []struct {
		Name string
		Err  bool
	}{
		{
			"assign_colon.hcl",
			true,
		},
		{
			"comment.hcl",
			false,
		},
		{
			"comment_crlf.hcl",
			false,
		},
		{
			"comment_lastline.hcl",
			false,
		},
		{
			"comment_single.hcl",
			false,
		},
		{
			"empty.hcl",
			false,
		},
		{
			"list_comma.hcl",
			false,
		},
		{
			"multiple.hcl",
			false,
		},
		{
			"object_list_comma.hcl",
			false,
		},
		{
			"structure.hcl",
			false,
		},
		{
			"structure_basic.hcl",
			false,
		},
		{
			"structure_empty.hcl",
			false,
		},
		{
			"complex.hcl",
			false,
		},
		{
			"complex_crlf.hcl",
			false,
		},
		{
			"types.hcl",
			false,
		},
		{
			"array_comment.hcl",
			false,
		},
		{
			"array_comment_2.hcl",
			true,
		},
		{
			"missing_braces.hcl",
			true,
		},
		{
			"unterminated_object.hcl",
			true,
		},
		{
			"unterminated_object_2.hcl",
			true,
		},
		{
			"key_without_value.hcl",
			true,
		},
		{
			"object_key_without_value.hcl",
			true,
		},
		{
			"object_key_assign_without_value.hcl",
			true,
		},
		{
			"object_key_assign_without_value2.hcl",
			true,
		},
		{
			"object_key_assign_without_value3.hcl",
			true,
		},
		{
			"git_crypt.hcl",
			true,
		},
	}

	const fixtureDir = "./test-fixtures"

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

			v, err := Parse(d)
			if (err != nil) != tc.Err {
				t.Fatalf("Input: %s\n\nError: %s\n\nAST: %#v", tc.Name, err, v)
			}
		})
	}
}

func TestParse_inline(t *testing.T) {
	cases := []struct {
		Value string
		Err   bool
	}{
		{"t t e{{}}", true},
		{"o{{}}", true},
		{"t t e d N{{}}", true},
		{"t t e d{{}}", true},
		{"N{}N{{}}", true},
		{"v\nN{{}}", true},
		{"v=/\n[,", true},
		{"v=10kb", true},
		{"v=/foo", true},
	}

	for _, tc := range cases {
		t.Logf("Testing: %q", tc.Value)
		ast, err := Parse([]byte(tc.Value))
		if (err != nil) != tc.Err {
			t.Fatalf("Input: %q\n\nError: %s\n\nAST: %#v", tc.Value, err, ast)
		}
	}
}

// equals fails the test if exp is not equal to act.
func equals(tb testing.TB, exp, act interface{}) {
	if !reflect.DeepEqual(exp, act) {
		_, file, line, _ := runtime.Caller(1)
		fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)
		tb.FailNow()
	}
}
