package ast_test

import (
	"testing"

	"github.com/robertkrimen/otto/ast"
	"github.com/robertkrimen/otto/file"
	"github.com/robertkrimen/otto/parser"
	"github.com/stretchr/testify/require"
)

type walker struct {
	seen              map[ast.Node]struct{}
	source            string
	stack             []ast.Node
	shift             file.Idx
	duplicate         int
	newExpressionIdx1 file.Idx
}

// push and pop below are to prove the symmetry of Enter/Exit calls

func (w *walker) push(n ast.Node) {
	w.stack = append(w.stack, n)
}

func (w *walker) pop(n ast.Node) {
	size := len(w.stack)
	if size <= 0 {
		panic("pop of empty stack")
	}

	if toPop := w.stack[size-1]; toPop != n {
		panic("pop: nodes do not equal")
	}

	w.stack[size-1] = nil
	w.stack = w.stack[:size-1]
}

func (w *walker) Enter(n ast.Node) ast.Visitor {
	w.push(n)
	if _, ok := w.seen[n]; ok {
		// Skip items we've already seen which occurs due to declarations.
		w.duplicate++
		return w
	}

	w.seen[n] = struct{}{}

	switch t := n.(type) {
	case *ast.Identifier:
		if t != nil {
			idx := n.Idx0() + w.shift - 1
			s := w.source[:idx] + "IDENT_" + w.source[idx:]
			w.source = s
			w.shift += 6
		}
	case *ast.VariableExpression:
		if t != nil {
			idx := n.Idx0() + w.shift - 1
			s := w.source[:idx] + "VAR_" + w.source[idx:]
			w.source = s
			w.shift += 4
		}
	case *ast.NewExpression:
		w.newExpressionIdx1 = n.Idx1()
	}

	return w
}

func (w *walker) Exit(n ast.Node) {
	w.pop(n)
}

func TestVisitorRewrite(t *testing.T) {
	source := `var b = function() {
		test();
		try {} catch(e) {}
		var test = "test(); var test = 1"
	} // test`
	program, err := parser.ParseFile(nil, "", source, 0)
	require.NoError(t, err)

	w := &walker{
		source: source,
		seen:   make(map[ast.Node]struct{}),
	}
	ast.Walk(w, program)

	xformed := `var VAR_b = function() {
		IDENT_test();
		try {} catch(IDENT_e) {}
		var VAR_test = "test(); var test = 1"
	} // test`

	require.Equal(t, xformed, w.source)
	require.Empty(t, w.stack)
	require.Zero(t, w.duplicate)
}

func Test_issue261(t *testing.T) {
	tests := map[string]struct {
		code string
		want file.Idx
	}{
		"no-parenthesis": {
			code: `var i = new Image;`,
			want: 18,
		},
		"no-args": {
			code: `var i = new Image();`,
			want: 20,
		},
		"two-args": {
			code: `var i = new Image(1, 2);`,
			want: 24,
		},
	}

	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {
			prog, err := parser.ParseFile(nil, "", tt.code, 0)
			require.NoError(t, err)

			w := &walker{
				source: tt.code,
				seen:   make(map[ast.Node]struct{}),
			}
			ast.Walk(w, prog)

			require.Equal(t, tt.want, w.newExpressionIdx1)
			require.Empty(t, w.stack)
			require.Zero(t, w.duplicate)
		})
	}
}

func TestBadStatement(t *testing.T) {
	source := `
	var abc;
	break; do {
	} while(true);
`
	program, err := parser.ParseFile(nil, "", source, 0)
	require.ErrorContains(t, err, "Illegal break statement")

	w := &walker{
		source: source,
		seen:   make(map[ast.Node]struct{}),
	}

	require.NotPanics(t, func() {
		ast.Walk(w, program)
	})
}
