package parser

import (
	"fmt"
	"io/ioutil"
	"path/filepath"
	"reflect"
	"runtime"
	"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": "bar"`},
		{token.NUMBER, `"foo": 123`},
		{token.FLOAT, `"foo": 123.12`},
		{token.FLOAT, `"foo": -123.12`},
		{token.BOOL, `"foo": true`},
		{token.STRING, `"foo": null`},
	}

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

		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": []`,
			[]token.Type{},
		},
		{
			`"foo": ["123", 123]`,
			[]token.Type{token.STRING, token.NUMBER},
		},
		{
			`"foo": ["123", {}]`,
			[]token.Type{token.STRING, token.LBRACE},
		},
	}

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

		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 {
			switch v := li.(type) {
			case *ast.LiteralType:
				tokens = append(tokens, v.Token.Type)
			case *ast.ObjectType:
				tokens = append(tokens, token.LBRACE)
			}
		}

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

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("Testing:\n%s\n", l.src)

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

		// 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)
		}

		// 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 TestFlattenObjects(t *testing.T) {
	var literals = []struct {
		src      string
		nodeType []ast.Node
		itemLen  int
	}{
		{
			`{
					"foo": [
						{
							"foo": "svh",
							"bar": "fatih"
						}
					]
				}`,
			[]ast.Node{
				&ast.ObjectType{},
				&ast.LiteralType{},
				&ast.LiteralType{},
			},
			3,
		},
		{
			`{
					"variable": {
						"foo": {}
					}
				}`,
			[]ast.Node{
				&ast.ObjectType{},
			},
			1,
		},
		{
			`{
				"empty": []
			}`,
			[]ast.Node{
				&ast.ListType{},
			},
			1,
		},
		{
			`{
				"basic": [1, 2, 3]
			}`,
			[]ast.Node{
				&ast.ListType{},
			},
			1,
		},
	}

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

		f, err := Parse([]byte(l.src))
		if err != nil {
			t.Error(err)
		}

		// the first object is always an ObjectList so just assert that one
		// so we can use it as such
		obj, ok := f.Node.(*ast.ObjectList)
		if !ok {
			t.Errorf("node should be *ast.ObjectList, got: %T", f.Node)
		}

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

			if obj, ok := item.Val.(*ast.ObjectType); ok {
				for _, item := range obj.List.Items {
					equals(t, reflect.TypeOf(l.nodeType[i]), reflect.TypeOf(item.Val))
					i++
				}
			}
		}

		// check if the number of items is correct
		equals(t, l.itemLen, i)

	}
}

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

	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)
		}
	}
}

// Official HCL tests
func TestParse(t *testing.T) {
	cases := []struct {
		Name string
		Err  bool
	}{
		{
			"array.json",
			false,
		},
		{
			"basic.json",
			false,
		},
		{
			"object.json",
			false,
		},
		{
			"types.json",
			false,
		},
		{
			"bad_input_128.json",
			true,
		},
		{
			"bad_input_tf_8110.json",
			true,
		},
		{
			"good_input_tf_8110.json",
			false,
		},
	}

	const fixtureDir = "./test-fixtures"

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

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

func TestParse_inline(t *testing.T) {
	cases := []struct {
		Value string
		Err   bool
	}{
		{"{:{", true},
	}

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

// 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: %s\n\n\tgot: %s\033[39m\n\n", filepath.Base(file), line, exp, act)
		tb.FailNow()
	}
}
