// Copyright 2017 The Bazel Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package syntax_test

import (
	"bufio"
	"bytes"
	"fmt"
	"go/build"
	"io/ioutil"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/k14s/starlark-go/internal/chunkedfile"
	"github.com/k14s/starlark-go/starlarktest"
	"github.com/k14s/starlark-go/syntax"
)

func TestExprParseTrees(t *testing.T) {
	for _, test := range []struct {
		input, want string
	}{
		{`print(1)`,
			`(CallExpr Fn=print Args=(1))`},
		{"print(1)\n",
			`(CallExpr Fn=print Args=(1))`},
		{`x + 1`,
			`(BinaryExpr X=x Op=+ Y=1)`},
		{`[x for x in y]`,
			`(Comprehension Body=x Clauses=((ForClause Vars=x X=y)))`},
		{`[x for x in (a if b else c)]`,
			`(Comprehension Body=x Clauses=((ForClause Vars=x X=(ParenExpr X=(CondExpr Cond=b True=a False=c)))))`},
		{`x[i].f(42)`,
			`(CallExpr Fn=(DotExpr X=(IndexExpr X=x Y=i) Name=f) Args=(42))`},
		{`x.f()`,
			`(CallExpr Fn=(DotExpr X=x Name=f))`},
		{`x+y*z`,
			`(BinaryExpr X=x Op=+ Y=(BinaryExpr X=y Op=* Y=z))`},
		{`x%y-z`,
			`(BinaryExpr X=(BinaryExpr X=x Op=% Y=y) Op=- Y=z)`},
		{`a + b not in c`,
			`(BinaryExpr X=(BinaryExpr X=a Op=+ Y=b) Op=not in Y=c)`},
		{`lambda x, *args, **kwargs: None`,
			`(LambdaExpr Params=(x (UnaryExpr Op=* X=args) (UnaryExpr Op=** X=kwargs)) Body=None)`},
		{`{"one": 1}`,
			`(DictExpr List=((DictEntry Key="one" Value=1)))`},
		{`a[i]`,
			`(IndexExpr X=a Y=i)`},
		{`a[i:]`,
			`(SliceExpr X=a Lo=i)`},
		{`a[:j]`,
			`(SliceExpr X=a Hi=j)`},
		{`a[::]`,
			`(SliceExpr X=a)`},
		{`a[::k]`,
			`(SliceExpr X=a Step=k)`},
		{`[]`,
			`(ListExpr)`},
		{`[1]`,
			`(ListExpr List=(1))`},
		{`[1,]`,
			`(ListExpr List=(1))`},
		{`[1, 2]`,
			`(ListExpr List=(1 2))`},
		{`()`,
			`(TupleExpr)`},
		{`(4,)`,
			`(ParenExpr X=(TupleExpr List=(4)))`},
		{`(4)`,
			`(ParenExpr X=4)`},
		{`(4, 5)`,
			`(ParenExpr X=(TupleExpr List=(4 5)))`},
		{`1, 2, 3`,
			`(TupleExpr List=(1 2 3))`},
		{`1, 2,`,
			`unparenthesized tuple with trailing comma`},
		{`{}`,
			`(DictExpr)`},
		{`{"a": 1}`,
			`(DictExpr List=((DictEntry Key="a" Value=1)))`},
		{`{"a": 1,}`,
			`(DictExpr List=((DictEntry Key="a" Value=1)))`},
		{`{"a": 1, "b": 2}`,
			`(DictExpr List=((DictEntry Key="a" Value=1) (DictEntry Key="b" Value=2)))`},
		{`{x: y for (x, y) in z}`,
			`(Comprehension Curly Body=(DictEntry Key=x Value=y) Clauses=((ForClause Vars=(ParenExpr X=(TupleExpr List=(x y))) X=z)))`},
		{`{x: y for a in b if c}`,
			`(Comprehension Curly Body=(DictEntry Key=x Value=y) Clauses=((ForClause Vars=a X=b) (IfClause Cond=c)))`},
		{`-1 + +2`,
			`(BinaryExpr X=(UnaryExpr Op=- X=1) Op=+ Y=(UnaryExpr Op=+ X=2))`},
		{`"foo" + "bar"`,
			`(BinaryExpr X="foo" Op=+ Y="bar")`},
		{`-1 * 2`, // prec(unary -) > prec(binary *)
			`(BinaryExpr X=(UnaryExpr Op=- X=1) Op=* Y=2)`},
		{`-x[i]`, // prec(unary -) < prec(x[i])
			`(UnaryExpr Op=- X=(IndexExpr X=x Y=i))`},
		{`a | b & c | d`, // prec(|) < prec(&)
			`(BinaryExpr X=(BinaryExpr X=a Op=| Y=(BinaryExpr X=b Op=& Y=c)) Op=| Y=d)`},
		{`a or b and c or d`,
			`(BinaryExpr X=(BinaryExpr X=a Op=or Y=(BinaryExpr X=b Op=and Y=c)) Op=or Y=d)`},
		{`a and b or c and d`,
			`(BinaryExpr X=(BinaryExpr X=a Op=and Y=b) Op=or Y=(BinaryExpr X=c Op=and Y=d))`},
		{`f(1, x=y)`,
			`(CallExpr Fn=f Args=(1 (BinaryExpr X=x Op== Y=y)))`},
		{`f(*args, **kwargs)`,
			`(CallExpr Fn=f Args=((UnaryExpr Op=* X=args) (UnaryExpr Op=** X=kwargs)))`},
		{`lambda *args, *, x=1, **kwargs: 0`,
			`(LambdaExpr Params=((UnaryExpr Op=* X=args) (UnaryExpr Op=*) (BinaryExpr X=x Op== Y=1) (UnaryExpr Op=** X=kwargs)) Body=0)`},
		{`lambda *, a, *b: 0`,
			`(LambdaExpr Params=((UnaryExpr Op=*) a (UnaryExpr Op=* X=b)) Body=0)`},
		{`a if b else c`,
			`(CondExpr Cond=b True=a False=c)`},
		{`a and not b`,
			`(BinaryExpr X=a Op=and Y=(UnaryExpr Op=not X=b))`},
		{`[e for x in y if cond1 if cond2]`,
			`(Comprehension Body=e Clauses=((ForClause Vars=x X=y) (IfClause Cond=cond1) (IfClause Cond=cond2)))`}, // github.com/google/skylark/issues/53
	} {
		e, err := syntax.ParseExpr("foo.star", test.input, 0)
		var got string
		if err != nil {
			got = stripPos(err)
		} else {
			got = treeString(e)
		}
		if test.want != got {
			t.Errorf("parse `%s` = %s, want %s", test.input, got, test.want)
		}
	}
}

func TestStmtParseTrees(t *testing.T) {
	for _, test := range []struct {
		input, want string
	}{
		{`print(1)`,
			`(ExprStmt X=(CallExpr Fn=print Args=(1)))`},
		{`return 1, 2`,
			`(ReturnStmt Result=(TupleExpr List=(1 2)))`},
		{`return`,
			`(ReturnStmt)`},
		{`for i in "abc": break`,
			`(ForStmt Vars=i X="abc" Body=((BranchStmt Token=break)))`},
		{`for i in "abc": continue`,
			`(ForStmt Vars=i X="abc" Body=((BranchStmt Token=continue)))`},
		{`for x, y in z: pass`,
			`(ForStmt Vars=(TupleExpr List=(x y)) X=z Body=((BranchStmt Token=pass)))`},
		{`if True: pass`,
			`(IfStmt Cond=True True=((BranchStmt Token=pass)))`},
		{`if True: break`,
			`(IfStmt Cond=True True=((BranchStmt Token=break)))`},
		{`if True: continue`,
			`(IfStmt Cond=True True=((BranchStmt Token=continue)))`},
		{`if True: pass
else:
	pass`,
			`(IfStmt Cond=True True=((BranchStmt Token=pass)) False=((BranchStmt Token=pass)))`},
		{"if a: pass\nelif b: pass\nelse: pass",
			`(IfStmt Cond=a True=((BranchStmt Token=pass)) False=((IfStmt Cond=b True=((BranchStmt Token=pass)) False=((BranchStmt Token=pass)))))`},
		{`x, y = 1, 2`,
			`(AssignStmt Op== LHS=(TupleExpr List=(x y)) RHS=(TupleExpr List=(1 2)))`},
		{`x[i] = 1`,
			`(AssignStmt Op== LHS=(IndexExpr X=x Y=i) RHS=1)`},
		{`x.f = 1`,
			`(AssignStmt Op== LHS=(DotExpr X=x Name=f) RHS=1)`},
		{`(x, y) = 1`,
			`(AssignStmt Op== LHS=(ParenExpr X=(TupleExpr List=(x y))) RHS=1)`},
		{`load("", "a", b="c")`,
			`(LoadStmt Module="" From=(a c) To=(a b))`},
		{`if True: load("", "a", b="c")`, // load needn't be at toplevel
			`(IfStmt Cond=True True=((LoadStmt Module="" From=(a c) To=(a b))))`},
		{`def f(x, *args, **kwargs):
	pass`,
			`(DefStmt Name=f Params=(x (UnaryExpr Op=* X=args) (UnaryExpr Op=** X=kwargs)) Body=((BranchStmt Token=pass)))`},
		{`def f(**kwargs, *args): pass`,
			`(DefStmt Name=f Params=((UnaryExpr Op=** X=kwargs) (UnaryExpr Op=* X=args)) Body=((BranchStmt Token=pass)))`},
		{`def f(a, b, c=d): pass`,
			`(DefStmt Name=f Params=(a b (BinaryExpr X=c Op== Y=d)) Body=((BranchStmt Token=pass)))`},
		{`def f(a, b=c, d): pass`,
			`(DefStmt Name=f Params=(a (BinaryExpr X=b Op== Y=c) d) Body=((BranchStmt Token=pass)))`}, // TODO(adonovan): fix this
		{`def f():
	def g():
		pass
	pass
def h():
	pass`,
			`(DefStmt Name=f Body=((DefStmt Name=g Body=((BranchStmt Token=pass))) (BranchStmt Token=pass)))`},
		{"f();g()",
			`(ExprStmt X=(CallExpr Fn=f))`},
		{"f();",
			`(ExprStmt X=(CallExpr Fn=f))`},
		{"f();g()\n",
			`(ExprStmt X=(CallExpr Fn=f))`},
		{"f();\n",
			`(ExprStmt X=(CallExpr Fn=f))`},
	} {
		f, err := syntax.Parse("foo.star", test.input, 0)
		if err != nil {
			t.Errorf("parse `%s` failed: %v", test.input, stripPos(err))
			continue
		}
		if got := treeString(f.Stmts[0]); test.want != got {
			t.Errorf("parse `%s` = %s, want %s", test.input, got, test.want)
		}
	}
}

// TestFileParseTrees tests sequences of statements, and particularly
// handling of indentation, newlines, line continuations, and blank lines.
func TestFileParseTrees(t *testing.T) {
	for _, test := range []struct {
		input, want string
	}{
		{`x = 1
print(x)`,
			`(AssignStmt Op== LHS=x RHS=1)
(ExprStmt X=(CallExpr Fn=print Args=(x)))`},
		{"if cond:\n\tpass",
			`(IfStmt Cond=cond True=((BranchStmt Token=pass)))`},
		{"if cond:\n\tpass\nelse:\n\tpass",
			`(IfStmt Cond=cond True=((BranchStmt Token=pass)) False=((BranchStmt Token=pass)))`},
		{`def f():
	pass
pass

pass`,
			`(DefStmt Name=f Body=((BranchStmt Token=pass)))
(BranchStmt Token=pass)
(BranchStmt Token=pass)`},
		{`pass; pass`,
			`(BranchStmt Token=pass)
(BranchStmt Token=pass)`},
		{"pass\npass",
			`(BranchStmt Token=pass)
(BranchStmt Token=pass)`},
		{"pass\n\npass",
			`(BranchStmt Token=pass)
(BranchStmt Token=pass)`},
		{`x = (1 +
2)`,
			`(AssignStmt Op== LHS=x RHS=(ParenExpr X=(BinaryExpr X=1 Op=+ Y=2)))`},
		{`x = 1 \
+ 2`,
			`(AssignStmt Op== LHS=x RHS=(BinaryExpr X=1 Op=+ Y=2))`},
	} {
		f, err := syntax.Parse("foo.star", test.input, 0)
		if err != nil {
			t.Errorf("parse `%s` failed: %v", test.input, stripPos(err))
			continue
		}
		var buf bytes.Buffer
		for i, stmt := range f.Stmts {
			if i > 0 {
				buf.WriteByte('\n')
			}
			writeTree(&buf, reflect.ValueOf(stmt))
		}
		if got := buf.String(); test.want != got {
			t.Errorf("parse `%s` = %s, want %s", test.input, got, test.want)
		}
	}
}

// TestCompoundStmt tests handling of REPL-style compound statements.
func TestCompoundStmt(t *testing.T) {
	for _, test := range []struct {
		input, want string
	}{
		// blank lines
		{"\n",
			``},
		{"   \n",
			``},
		{"# comment\n",
			``},
		// simple statement
		{"1\n",
			`(ExprStmt X=1)`},
		{"print(1)\n",
			`(ExprStmt X=(CallExpr Fn=print Args=(1)))`},
		{"1;2;3;\n",
			`(ExprStmt X=1)(ExprStmt X=2)(ExprStmt X=3)`},
		{"f();g()\n",
			`(ExprStmt X=(CallExpr Fn=f))(ExprStmt X=(CallExpr Fn=g))`},
		{"f();\n",
			`(ExprStmt X=(CallExpr Fn=f))`},
		{"f(\n\n\n\n\n\n\n)\n",
			`(ExprStmt X=(CallExpr Fn=f))`},
		// complex statements
		{"def f():\n  pass\n\n",
			`(DefStmt Name=f Body=((BranchStmt Token=pass)))`},
		{"if cond:\n  pass\n\n",
			`(IfStmt Cond=cond True=((BranchStmt Token=pass)))`},
		// Even as a 1-liner, the following blank line is required.
		{"if cond: pass\n\n",
			`(IfStmt Cond=cond True=((BranchStmt Token=pass)))`},
		// github.com/google/starlark-go/issues/121
		{"a; b; c\n",
			`(ExprStmt X=a)(ExprStmt X=b)(ExprStmt X=c)`},
		{"a; b c\n",
			`invalid syntax`},
	} {

		// Fake readline input from string.
		// The ! suffix, which would cause a parse error,
		// tests that the parser doesn't read more than necessary.
		sc := bufio.NewScanner(strings.NewReader(test.input + "!"))
		readline := func() ([]byte, error) {
			if sc.Scan() {
				return []byte(sc.Text() + "\n"), nil
			}
			return nil, sc.Err()
		}

		var got string
		f, err := syntax.ParseCompoundStmt("foo.star", readline)
		if err != nil {
			got = stripPos(err)
		} else {
			for _, stmt := range f.Stmts {
				got += treeString(stmt)
			}
		}
		if test.want != got {
			t.Errorf("parse `%s` = %s, want %s", test.input, got, test.want)
		}
	}
}

func stripPos(err error) string {
	s := err.Error()
	if i := strings.Index(s, ": "); i >= 0 {
		s = s[i+len(": "):] // strip file:line:col
	}
	return s
}

// treeString prints a syntax node as a parenthesized tree.
// Idents are printed as foo and Literals as "foo" or 42.
// Structs are printed as (type name=value ...).
// Only non-empty fields are shown.
func treeString(n syntax.Node) string {
	var buf bytes.Buffer
	writeTree(&buf, reflect.ValueOf(n))
	return buf.String()
}

func writeTree(out *bytes.Buffer, x reflect.Value) {
	switch x.Kind() {
	case reflect.String, reflect.Int, reflect.Bool:
		fmt.Fprintf(out, "%v", x.Interface())
	case reflect.Ptr, reflect.Interface:
		if elem := x.Elem(); elem.Kind() == 0 {
			out.WriteString("nil")
		} else {
			writeTree(out, elem)
		}
	case reflect.Struct:
		switch v := x.Interface().(type) {
		case syntax.Literal:
			if v.Token == syntax.STRING {
				fmt.Fprintf(out, "%q", v.Value)
			} else if v.Token == syntax.INT {
				fmt.Fprintf(out, "%d", v.Value)
			}
			return
		case syntax.Ident:
			out.WriteString(v.Name)
			return
		}
		fmt.Fprintf(out, "(%s", strings.TrimPrefix(x.Type().String(), "syntax."))
		for i, n := 0, x.NumField(); i < n; i++ {
			f := x.Field(i)
			if f.Type() == reflect.TypeOf(syntax.Position{}) {
				continue // skip positions
			}
			name := x.Type().Field(i).Name
			if name == "commentsRef" {
				continue // skip comments fields
			}
			if f.Type() == reflect.TypeOf(syntax.Token(0)) {
				fmt.Fprintf(out, " %s=%s", name, f.Interface())
				continue
			}

			switch f.Kind() {
			case reflect.Slice:
				if n := f.Len(); n > 0 {
					fmt.Fprintf(out, " %s=(", name)
					for i := 0; i < n; i++ {
						if i > 0 {
							out.WriteByte(' ')
						}
						writeTree(out, f.Index(i))
					}
					out.WriteByte(')')
				}
				continue
			case reflect.Ptr, reflect.Interface:
				if f.IsNil() {
					continue
				}
			case reflect.Int:
				if f.Int() != 0 {
					fmt.Fprintf(out, " %s=%d", name, f.Int())
				}
				continue
			case reflect.Bool:
				if f.Bool() {
					fmt.Fprintf(out, " %s", name)
				}
				continue
			}
			fmt.Fprintf(out, " %s=", name)
			writeTree(out, f)
		}
		fmt.Fprintf(out, ")")
	default:
		fmt.Fprintf(out, "%T", x.Interface())
	}
}

func TestParseErrors(t *testing.T) {
	filename := starlarktest.DataFile("syntax", "testdata/errors.star")
	for _, chunk := range chunkedfile.Read(filename, t) {
		_, err := syntax.Parse(filename, chunk.Source, 0)
		switch err := err.(type) {
		case nil:
			// ok
		case syntax.Error:
			chunk.GotError(int(err.Pos.Line), err.Msg)
		default:
			t.Error(err)
		}
		chunk.Done()
	}
}

// dataFile is the same as starlarktest.DataFile.
// We make a copy to avoid a dependency cycle.
var dataFile = func(pkgdir, filename string) string {
	return filepath.Join(build.Default.GOPATH, "src/go.starlark.net", pkgdir, filename)
}

func BenchmarkParse(b *testing.B) {
	filename := dataFile("syntax", "testdata/scan.star")
	b.StopTimer()
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		b.Fatal(err)
	}
	b.StartTimer()

	for i := 0; i < b.N; i++ {
		_, err := syntax.Parse(filename, data, 0)
		if err != nil {
			b.Fatal(err)
		}
	}
}
