// Test parser package

package parser_test

import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"github.com/benhoyt/goawk/internal/ast"
	"github.com/benhoyt/goawk/parser"
)

// NOTE: apart from TestParseAndString, the parser doesn't have
// extensive tests of its own; the idea is to test the parser in the
// interp tests.

func TestParseAndString(t *testing.T) {
	// This program should have one of every AST element to ensure
	// we can parse and String()ify each.
	source := strings.TrimSpace(`
BEGIN {
    print "begin one"
}

BEGIN {
    print "begin two"
}

{
    print "empty pattern"
}

$0 {
    print "normal pattern"
    print 1, 2, 3
    printf "%.3f", 3.14159
    print "x" >"file"
    print "x" >>"append"
    print "y" |"prog"
    delete a
    delete a[k]
    if (c) {
        get(a, k)
    }
    if (1 + 2) {
        get(a, k)
    } else {
        set(a, k, v)
    }
    for (i = 0; i < 10; i++) {
        print i
        continue
    }
    for (k in a) {
        break
    }
    while (0) {
        print "x"
    }
    do {
        print "y"
        exit status
    } while (x)
    next
    nextfile
    "cmd" |getline
    "cmd" |getline x
    "cmd" |getline a[1]
    "cmd" |getline $1
    getline
    getline x
    (getline x + 1)
    getline $1
    getline a[1]
    getline <"file"
    getline x <"file"
    (getline x <"file" "x")
    getline $1 <"file"
    getline a[1] <"file"
    x = 0
    y = z = 0
    b += 1
    c -= 2
    d *= 3
    e /= 4
    g ^= 5
    h %= 6
    (x ? "t" : "f")
    ((b && c) || d)
    (k in a)
    ((x, y, z) in a)
    (s ~ "foo")
    (b < 1)
    (c <= 2)
    (d > 3)
    (e >= 4)
    (g == 5)
    (h != 6)
    ((x y) z)
    ((b + c) + d)
    ((b * c) * d)
    ((b - c) - d)
    ((b / c) / d)
    (b ^ (c ^ d))
    x++
    x--
    ++y
    --y
    1234
    1.5
    "This is a string"
    if (/a.b/) {
        print "match"
    }
    $1
    $(1 + 2)
    !x
    +x
    -x
    var
    a[key]
    a[x, y, z]
    f()
    set(a, k, v)
    sub(/regex/, repl, s)
    sub("regex", repl, s)
    sub(regex, repl)
    sub(regex, repl, s)
    gsub(regex, repl)
    gsub(regex, repl, s)
    split(s, a)
    split(s, a, regex)
    match(s, regex)
    rand()
    srand()
    srand(1)
    length()
    length($1)
    sprintf("")
    sprintf("%.3f", 3.14159)
    sprintf("%.3f %d", 3.14159, 42)
    cos(1)
    sin(1)
    exp(1)
    log(1)
    sqrt(1)
    int("42")
    tolower("FOO")
    toupper("foo")
    system("ls")
    close("file")
    atan2(x, y)
    index(haystack, needle)
    {
        print "block statement"
        f()
    }
}

(NR == 1), (NR == 2) {
    print "range pattern"
}

($1 == "foo")

END {
    print "end one"
}

END {
    print "end two"
}

function f() {
}

function get(a, k) {
    return a[k]
}

function set(a, k, v) {
    a[k] = v
    return
}
`)
	prog, err := parser.ParseProgram([]byte(source), nil)
	if err != nil {
		t.Fatalf("error parsing program: %v", err)
	}
	progStr := prog.String()
	if progStr != source {
		t.Fatalf("expected first, got second:\n%s\n----------\n%s", source, progStr)
	}
}

func TestResolveLargeCallGraph(t *testing.T) {
	const numCalls = 10000

	var buf bytes.Buffer
	var i int
	for i = 0; i < numCalls; i++ {
		fmt.Fprintf(&buf, "function f%d(a) { return f%d(a) }\n", i, i+1)
	}
	fmt.Fprintf(&buf, "function f%d(a) { return a }\n", i)
	fmt.Fprint(&buf, "BEGIN { printf f0(42) }\n")
	_, err := parser.ParseProgram(buf.Bytes(), nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	buf.Reset()
	fmt.Fprint(&buf, "BEGIN { printf f0(42) }\n")
	fmt.Fprintf(&buf, "function f%d(a) { return a }\n", numCalls)
	for i = numCalls - 1; i >= 0; i-- {
		fmt.Fprintf(&buf, "function f%d(a) { return f%d(a) }\n", i, i+1)
	}
	_, err = parser.ParseProgram(buf.Bytes(), nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestPositions(t *testing.T) {
	source := strings.TrimSpace(`
function AddNums(n,   sum,i) {
  sum = 0
  if (n%2 == 0) {
    for (i = 0; i < n; i++) {
      sum += i * 2
    }
  } else if (n%3 == 0) {
    print "Divides by 3"
  } else {
    sum = -1
  }
  while(1) { do { print 123 } while(1) }
  return sum
}`)
	expectedAst := strings.TrimSpace(`
*ast.Program
 *ast.Function
  *ast.ExprStmt [2:3-2:10)
   *ast.AssignExpr
    *ast.VarExpr
    *ast.NumExpr
  *ast.IfStmt [3:3-12:3)
   *ast.BinaryExpr
    *ast.BinaryExpr
     *ast.VarExpr
     *ast.NumExpr
    *ast.NumExpr
   *ast.ForStmt [4:5-6:6)
    *ast.ExprStmt [4:10-4:15)
     *ast.AssignExpr
      *ast.VarExpr
      *ast.NumExpr
    *ast.BinaryExpr
     *ast.VarExpr
     *ast.VarExpr
    *ast.ExprStmt [4:24-4:27)
     *ast.IncrExpr
      *ast.VarExpr
    *ast.ExprStmt [5:7-5:19)
     *ast.AugAssignExpr
      *ast.VarExpr
      *ast.BinaryExpr
       *ast.VarExpr
       *ast.NumExpr
   *ast.IfStmt [7:10-11:4)
    *ast.BinaryExpr
     *ast.BinaryExpr
      *ast.VarExpr
      *ast.NumExpr
     *ast.NumExpr
    *ast.PrintStmt [8:5-8:25)
     *ast.StrExpr
    *ast.ExprStmt [10:5-10:13)
     *ast.AssignExpr
      *ast.VarExpr
      *ast.UnaryExpr
       *ast.NumExpr
  *ast.WhileStmt [12:3-12:41)
   *ast.NumExpr
   *ast.DoWhileStmt [12:14-12:40)
    *ast.PrintStmt [12:19-12:29)
     *ast.NumExpr
    *ast.NumExpr
  *ast.ReturnStmt [13:3-13:13)
   *ast.VarExpr
`)
	prog, err := parser.ParseProgram([]byte(source), nil)
	if err != nil {
		t.Fatalf("error parsing program: %v", err)
	}
	code := &code{}
	ast.Walk(code, &prog.ResolvedProgram.Program)
	result := strings.TrimSpace(code.buf.String())
	if expectedAst != result {
		t.Fatalf("Wrong AST and/or positions:\n%s", result)
	}
}

type code struct {
	indent int
	buf    strings.Builder
}

func (c *code) Visit(node ast.Node) ast.Visitor {
	indent := strings.Repeat(" ", c.indent)
	if node != nil {
		c.buf.WriteString(indent)
		c.buf.WriteString(fmt.Sprintf("%T", node))
		if stmt, ok := node.(ast.Stmt); ok {
			c.buf.WriteString(fmt.Sprintf(" [%s-%s)", stmt.StartPos(), stmt.EndPos()))
		}
		c.buf.WriteString("\n")
		c.indent++
	} else {
		c.indent--
	}
	return c
}

func Example_valid() {
	prog, err := parser.ParseProgram([]byte("$0 { print $1 }"), nil)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(prog)
	}
	// Output:
	// $0 {
	//     print $1
	// }
}

func Example_error() {
	prog, err := parser.ParseProgram([]byte("{ for if }"), nil)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(prog)
	}
	// Output:
	// parse error at 1:7: expected ( instead of if
}
