package compiler_test

import (
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/d5/tengo/assert"
	"github.com/d5/tengo/compiler"
	"github.com/d5/tengo/compiler/parser"
	"github.com/d5/tengo/compiler/source"
	"github.com/d5/tengo/objects"
)

func TestCompiler_Compile(t *testing.T) {
	expect(t, `1 + 2`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpAdd),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `1; 2`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpPop),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `1 - 2`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpSub),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `1 * 2`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpMul),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `2 / 1`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpDiv),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(2),
				intObject(1))))

	expect(t, `true`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpTrue),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray()))

	expect(t, `false`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpFalse),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray()))

	expect(t, `1 > 2`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpGreaterThan),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `1 < 2`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpGreaterThan),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(2),
				intObject(1))))

	expect(t, `1 == 2`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpEqual),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `1 != 2`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpNotEqual),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `true == false`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpTrue),
				compiler.MakeInstruction(compiler.OpFalse),
				compiler.MakeInstruction(compiler.OpEqual),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray()))

	expect(t, `true != false`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpTrue),
				compiler.MakeInstruction(compiler.OpFalse),
				compiler.MakeInstruction(compiler.OpNotEqual),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray()))

	expect(t, `-1`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpMinus),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1))))

	expect(t, `!true`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpTrue),
				compiler.MakeInstruction(compiler.OpLNot),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray()))

	expect(t, `if true { 10 }; 3333`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpTrue),         // 0000
				compiler.MakeInstruction(compiler.OpJumpFalsy, 8), // 0001
				compiler.MakeInstruction(compiler.OpConstant, 0),  // 0004
				compiler.MakeInstruction(compiler.OpPop),          // 0007
				compiler.MakeInstruction(compiler.OpConstant, 1),  // 0008
				compiler.MakeInstruction(compiler.OpPop)),         // 0011
			objectsArray(
				intObject(10),
				intObject(3333))))

	expect(t, `if (true) { 10 } else { 20 }; 3333;`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpTrue),          // 0000
				compiler.MakeInstruction(compiler.OpJumpFalsy, 11), // 0001
				compiler.MakeInstruction(compiler.OpConstant, 0),   // 0004
				compiler.MakeInstruction(compiler.OpPop),           // 0007
				compiler.MakeInstruction(compiler.OpJump, 15),      // 0008
				compiler.MakeInstruction(compiler.OpConstant, 1),   // 0011
				compiler.MakeInstruction(compiler.OpPop),           // 0014
				compiler.MakeInstruction(compiler.OpConstant, 2),   // 0015
				compiler.MakeInstruction(compiler.OpPop)),          // 0018
			objectsArray(
				intObject(10),
				intObject(20),
				intObject(3333))))

	expect(t, `"kami"`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				stringObject("kami"))))

	expect(t, `"ka" + "mi"`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpAdd),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				stringObject("ka"),
				stringObject("mi"))))

	expect(t, `a := 1; b := 2; a += b`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpSetGlobal, 1),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGetGlobal, 1),
				compiler.MakeInstruction(compiler.OpAdd),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `a := 1; b := 2; a /= b`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpSetGlobal, 1),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGetGlobal, 1),
				compiler.MakeInstruction(compiler.OpDiv),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0)),
			objectsArray(
				intObject(1),
				intObject(2))))

	expect(t, `[]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpArray, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray()))

	expect(t, `[1, 2, 3]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpArray, 3),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				intObject(3))))

	expect(t, `[1 + 2, 3 - 4, 5 * 6]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpAdd),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpSub),
				compiler.MakeInstruction(compiler.OpConstant, 4),
				compiler.MakeInstruction(compiler.OpConstant, 5),
				compiler.MakeInstruction(compiler.OpMul),
				compiler.MakeInstruction(compiler.OpArray, 3),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				intObject(3),
				intObject(4),
				intObject(5),
				intObject(6))))

	expect(t, `{}`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpMap, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray()))

	expect(t, `{a: 2, b: 4, c: 6}`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpConstant, 4),
				compiler.MakeInstruction(compiler.OpConstant, 5),
				compiler.MakeInstruction(compiler.OpMap, 6),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				stringObject("a"),
				intObject(2),
				stringObject("b"),
				intObject(4),
				stringObject("c"),
				intObject(6))))

	expect(t, `{a: 2 + 3, b: 5 * 6}`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpAdd),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpConstant, 4),
				compiler.MakeInstruction(compiler.OpConstant, 5),
				compiler.MakeInstruction(compiler.OpMul),
				compiler.MakeInstruction(compiler.OpMap, 4),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				stringObject("a"),
				intObject(2),
				intObject(3),
				stringObject("b"),
				intObject(5),
				intObject(6))))

	expect(t, `[1, 2, 3][1 + 1]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpArray, 3),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpConstant, 4),
				compiler.MakeInstruction(compiler.OpAdd),
				compiler.MakeInstruction(compiler.OpIndex),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				intObject(3),
				intObject(1),
				intObject(1))))

	expect(t, `{a: 2}[2 - 1]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpMap, 2),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpSub),
				compiler.MakeInstruction(compiler.OpIndex),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				stringObject("a"),
				intObject(2),
				intObject(2),
				intObject(1))))

	expect(t, `[1, 2, 3][:]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpArray, 3),
				compiler.MakeInstruction(compiler.OpNull),
				compiler.MakeInstruction(compiler.OpNull),
				compiler.MakeInstruction(compiler.OpSliceIndex),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				intObject(3))))

	expect(t, `[1, 2, 3][0 : 2]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpArray, 3),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpConstant, 4),
				compiler.MakeInstruction(compiler.OpSliceIndex),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				intObject(3),
				intObject(0),
				intObject(2))))

	expect(t, `[1, 2, 3][:2]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpArray, 3),
				compiler.MakeInstruction(compiler.OpNull),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpSliceIndex),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				intObject(3),
				intObject(2))))

	expect(t, `[1, 2, 3][0:]`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpArray, 3),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpNull),
				compiler.MakeInstruction(compiler.OpSliceIndex),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				intObject(3),
				intObject(0))))

	expect(t, `func() { return 5 + 10 }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(5),
				intObject(10),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpConstant, 1),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `func() { 5 + 10 }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(5),
				intObject(10),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpConstant, 1),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpReturn)))))

	expect(t, `func() { 1; 2 }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpConstant, 1),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpReturn)))))

	expect(t, `func() { 1; return 2 }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpConstant, 1),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `func() { if(true) { return 1 } else { return 2 } }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpTrue),              // 0000
					compiler.MakeInstruction(compiler.OpJumpFalsy, 12),     // 0001
					compiler.MakeInstruction(compiler.OpConstant, 0),       // 0004
					compiler.MakeInstruction(compiler.OpReturnValue, 1),    // 0007
					compiler.MakeInstruction(compiler.OpJump, 17),          // 0009
					compiler.MakeInstruction(compiler.OpConstant, 1),       // 0012
					compiler.MakeInstruction(compiler.OpReturnValue, 1))))) // 0014

	expect(t, `func() { 1; if(true) { 2 } else { 3 }; 4 }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 4),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(1),
				intObject(2),
				intObject(3),
				intObject(4),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),   // 0000
					compiler.MakeInstruction(compiler.OpPop),           // 0003
					compiler.MakeInstruction(compiler.OpTrue),          // 0004
					compiler.MakeInstruction(compiler.OpJumpFalsy, 15), // 0005
					compiler.MakeInstruction(compiler.OpConstant, 1),   // 0008
					compiler.MakeInstruction(compiler.OpPop),           // 0011
					compiler.MakeInstruction(compiler.OpJump, 19),      // 0012
					compiler.MakeInstruction(compiler.OpConstant, 2),   // 0015
					compiler.MakeInstruction(compiler.OpPop),           // 0018
					compiler.MakeInstruction(compiler.OpConstant, 3),   // 0019
					compiler.MakeInstruction(compiler.OpPop),           // 0022
					compiler.MakeInstruction(compiler.OpReturn)))))     // 0023

	expect(t, `func() { }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				compiledFunction(0, 0, compiler.MakeInstruction(compiler.OpReturn)))))

	expect(t, `func() { 24 }()`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpCall, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(24),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpReturn)))))

	expect(t, `func() { return 24 }()`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpCall, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(24),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `noArg := func() { 24 }; noArg();`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpCall, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(24),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpReturn)))))

	expect(t, `noArg := func() { return 24 }; noArg();`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpCall, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(24),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `n := 55; func() { n };`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(55),
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpGetGlobal, 0),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpReturn)))))

	expect(t, `func() { n := 55; return n }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(55),
				compiledFunction(1, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpDefineLocal, 0),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `func() { n := 55; n = 23; return n }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(55),
				intObject(23),
				compiledFunction(1, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpDefineLocal, 0),
					compiler.MakeInstruction(compiler.OpConstant, 1),
					compiler.MakeInstruction(compiler.OpSetLocal, 0),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `func() { a := 55; b := 77; return a + b }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(55),
				intObject(77),
				compiledFunction(2, 0,
					compiler.MakeInstruction(compiler.OpConstant, 0),
					compiler.MakeInstruction(compiler.OpDefineLocal, 0),
					compiler.MakeInstruction(compiler.OpConstant, 1),
					compiler.MakeInstruction(compiler.OpDefineLocal, 1),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpGetLocal, 1),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `f1 := func(a) { return a }; f1(24);`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpCall, 1),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				compiledFunction(1, 1,
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)),
				intObject(24))))

	expect(t, `f1 := func(a, b, c) { a; b; return c; }; f1(24, 25, 26);`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpCall, 3),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				compiledFunction(3, 3,
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpGetLocal, 1),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpGetLocal, 2),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)),
				intObject(24),
				intObject(25),
				intObject(26))))

	expect(t, `len([]);`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpGetBuiltin, 3),
				compiler.MakeInstruction(compiler.OpArray, 0),
				compiler.MakeInstruction(compiler.OpCall, 1),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray()))

	expect(t, `func() { return len([]) }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				compiledFunction(0, 0,
					compiler.MakeInstruction(compiler.OpGetBuiltin, 3),
					compiler.MakeInstruction(compiler.OpArray, 0),
					compiler.MakeInstruction(compiler.OpCall, 1),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `func(a) { func(b) { return a + b } }`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				compiledFunction(1, 1,
					compiler.MakeInstruction(compiler.OpGetFree, 0),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)),
				compiledFunction(1, 1,
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpClosure, 0, 1),
					compiler.MakeInstruction(compiler.OpPop),
					compiler.MakeInstruction(compiler.OpReturn)))))

	expect(t, `
func(a) { 
	return func(b) { 
		return func(c) { 
			return a + b + c 
		}
	}
}`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				compiledFunction(1, 1,
					compiler.MakeInstruction(compiler.OpGetFree, 0),
					compiler.MakeInstruction(compiler.OpGetFree, 1),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)),
				compiledFunction(1, 1,
					compiler.MakeInstruction(compiler.OpGetFree, 0),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpClosure, 0, 2),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)),
				compiledFunction(1, 1,
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpClosure, 1, 1),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `
g := 55;

func() {
	a := 66;

	return func() {
		b := 77;

		return func() {
			c := 88;

			return g + a + b + c;
		}
	}
}`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 6),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(55),
				intObject(66),
				intObject(77),
				intObject(88),
				compiledFunction(1, 0,
					compiler.MakeInstruction(compiler.OpConstant, 3),
					compiler.MakeInstruction(compiler.OpDefineLocal, 0),
					compiler.MakeInstruction(compiler.OpGetGlobal, 0),
					compiler.MakeInstruction(compiler.OpGetFree, 0),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpGetFree, 1),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpAdd),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)),
				compiledFunction(1, 0,
					compiler.MakeInstruction(compiler.OpConstant, 2),
					compiler.MakeInstruction(compiler.OpDefineLocal, 0),
					compiler.MakeInstruction(compiler.OpGetFree, 0),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpClosure, 4, 2),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)),
				compiledFunction(1, 0,
					compiler.MakeInstruction(compiler.OpConstant, 1),
					compiler.MakeInstruction(compiler.OpDefineLocal, 0),
					compiler.MakeInstruction(compiler.OpGetLocal, 0),
					compiler.MakeInstruction(compiler.OpClosure, 5, 1),
					compiler.MakeInstruction(compiler.OpReturnValue, 1)))))

	expect(t, `for i:=0; i<10; i++ {}`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGreaterThan),
				compiler.MakeInstruction(compiler.OpJumpFalsy, 29),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpAdd),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpJump, 6)),
			objectsArray(
				intObject(0),
				intObject(10),
				intObject(1))))

	expect(t, `m := {}; for k, v in m {}`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpMap, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpIteratorInit),
				compiler.MakeInstruction(compiler.OpSetGlobal, 1),
				compiler.MakeInstruction(compiler.OpGetGlobal, 1),
				compiler.MakeInstruction(compiler.OpIteratorNext),
				compiler.MakeInstruction(compiler.OpJumpFalsy, 37),
				compiler.MakeInstruction(compiler.OpGetGlobal, 1),
				compiler.MakeInstruction(compiler.OpIteratorKey),
				compiler.MakeInstruction(compiler.OpSetGlobal, 2),
				compiler.MakeInstruction(compiler.OpGetGlobal, 1),
				compiler.MakeInstruction(compiler.OpIteratorValue),
				compiler.MakeInstruction(compiler.OpSetGlobal, 3),
				compiler.MakeInstruction(compiler.OpJump, 13)),
			objectsArray()))

	expect(t, `a := 0; a == 0 && a != 1 || a < 1`,
		bytecode(
			concat(
				compiler.MakeInstruction(compiler.OpConstant, 0),
				compiler.MakeInstruction(compiler.OpSetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 1),
				compiler.MakeInstruction(compiler.OpEqual),
				compiler.MakeInstruction(compiler.OpAndJump, 23),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpConstant, 2),
				compiler.MakeInstruction(compiler.OpNotEqual),
				compiler.MakeInstruction(compiler.OpOrJump, 33),
				compiler.MakeInstruction(compiler.OpConstant, 3),
				compiler.MakeInstruction(compiler.OpGetGlobal, 0),
				compiler.MakeInstruction(compiler.OpGreaterThan),
				compiler.MakeInstruction(compiler.OpPop)),
			objectsArray(
				intObject(0),
				intObject(0),
				intObject(1),
				intObject(1))))

	expectError(t, `import("user1")`) // unknown module name
}

func concat(instructions ...[]byte) []byte {
	concat := make([]byte, 0)
	for _, i := range instructions {
		concat = append(concat, i...)
	}

	return concat
}

func bytecode(instructions []byte, constants []objects.Object) *compiler.Bytecode {
	return &compiler.Bytecode{
		Instructions: instructions,
		Constants:    constants,
	}
}

func expect(t *testing.T, input string, expected *compiler.Bytecode) (ok bool) {
	actual, trace, err := traceCompile(input, nil)

	defer func() {
		if !ok {
			for _, tr := range trace {
				t.Log(tr)
			}
		}
	}()

	if !assert.NoError(t, err) {
		return
	}

	ok = equalBytecode(t, expected, actual)

	return
}

func expectError(t *testing.T, input string) (ok bool) {
	_, trace, err := traceCompile(input, nil)

	defer func() {
		if !ok {
			for _, tr := range trace {
				t.Log(tr)
			}
		}
	}()

	ok = assert.Error(t, err)

	return
}

func equalBytecode(t *testing.T, expected, actual *compiler.Bytecode) bool {
	expectedInstructions := strings.Join(compiler.FormatInstructions(expected.Instructions, 0), "\n")
	actualInstructions := strings.Join(compiler.FormatInstructions(actual.Instructions, 0), "\n")

	return assert.Equal(t, expectedInstructions, actualInstructions) &&
		equalConstants(t, expected.Constants, actual.Constants)
}

func equalConstants(t *testing.T, expected, actual []objects.Object) bool {
	if !assert.Equal(t, len(expected), len(actual)) {
		return false
	}

	for i := 0; i < len(expected); i++ {
		if !assert.Equal(t, expected[i], actual[i]) {
			return false
		}
	}

	return true
}

type tracer struct {
	Out []string
}

func (o *tracer) Write(p []byte) (n int, err error) {
	o.Out = append(o.Out, string(p))
	return len(p), nil
}

func traceCompile(input string, symbols map[string]objects.Object) (res *compiler.Bytecode, trace []string, err error) {
	fileSet := source.NewFileSet()
	file := fileSet.AddFile("test", -1, len(input))

	p := parser.NewParser(file, []byte(input), nil)

	symTable := compiler.NewSymbolTable()
	for name := range symbols {
		symTable.Define(name)
	}
	for idx, fn := range objects.Builtins {
		symTable.DefineBuiltin(idx, fn.Name)
	}

	tr := &tracer{}
	c := compiler.NewCompiler(symTable, nil, tr)
	parsed, err := p.ParseFile()
	if err != nil {
		return
	}

	err = c.Compile(parsed)
	{
		trace = append(trace, fmt.Sprintf("Compiler Trace:\n%s", strings.Join(tr.Out, "")))
		bytecode := c.Bytecode()
		var constStr []string
		for cidx, cn := range bytecode.Constants {
			if cmFn, ok := cn.(*objects.CompiledFunction); ok {
				constStr = append(constStr, fmt.Sprintf("[% 3d] (Compiled Function|%p)", cidx, cn))
				for _, l := range compiler.FormatInstructions(cmFn.Instructions, 0) {
					constStr = append(constStr, fmt.Sprintf("     %s", l))
				}
			} else {
				constStr = append(constStr, fmt.Sprintf("[% 3d] %s (%s|%p)", cidx, cn, reflect.TypeOf(cn).Name(), cn))
			}
		}
		trace = append(trace, fmt.Sprintf("Compiled Constants:\n%s", strings.Join(constStr, "\n")))
		trace = append(trace, fmt.Sprintf("Compiled Instructions:\n%s\n", strings.Join(compiler.FormatInstructions(bytecode.Instructions, 0), "\n")))
	}
	if err != nil {
		return
	}

	res = c.Bytecode()

	return
}

func objectsArray(o ...objects.Object) []objects.Object {
	return o
}

func intObject(v int64) *objects.Int {
	return &objects.Int{Value: v}
}

func stringObject(v string) *objects.String {
	return &objects.String{Value: v}
}

func compiledFunction(numLocals, numParams int, insts ...[]byte) *objects.CompiledFunction {
	return &objects.CompiledFunction{Instructions: concat(insts...), NumLocals: numLocals, NumParameters: numParams}
}