package evaluator

import (
	"Interp/lexer"
	"Interp/object"
	"Interp/parser"
	"testing"
)

func TestEvalIntegerExpression(t *testing.T) {
	tests := []struct {
		input    string
		expected bool
	}{

		{"true", true},
		{"false", false},
		{"1 < 2", true},
		{"1 > 2", false},
		{"1 < 1", false},
		{"1 > 1", false},
		{"1 == 1", true},
		{"1 != 1", false},
		{"1 == 2", false},
		{"1 != 2", true},
	}

	for _, tt := range tests {
		evaluated := testEval(tt.input)
		testBooleanObject(t, evaluated, tt.expected)
	}
}

func testEval(input string) object.Object {
	l := lexer.NewLexer(input)
	p := parser.NewParser(l)

	program := p.ParseProgram()
	env := object.NewEnvironment()
	return Eval(program, env)
}

func testIntegerObject(t *testing.T, obj object.Object, expected int64) bool {

	result, ok := obj.(*object.Integer)
	if !ok {
		t.Errorf("object is not Integer. got=%T (%+v)", obj, obj)
		return false
	}

	if result.Value != expected {
		t.Errorf("object has wrong value. got=%d, want=%d", result.Value, expected)
		return false
	}

	return true
}

func TestEvalBooleanExpression(t *testing.T) {

	tests := []struct {
		input    string
		expected bool
	}{
		{"true == true", true},
		{"false == false", true},
		{"true == false", false},
		{"true != false", true},
		{"false != true", true},
		{"(1 < 2) == true", true},
		{"(1 < 2) == false", false},
		{"(1 > 2) == true", false},
		{"(1 > 2) == false", true},
	}

	for _, tt := range tests {
		evaluated := testEval(tt.input)
		testBooleanObject(t, evaluated, tt.expected)
	}

}

func testBooleanObject(t *testing.T, obj object.Object, expected bool) bool {

	result, ok := obj.(*object.Boolean)
	if !ok {
		t.Errorf("object is not Boolean. got=%T (%+v)", obj, obj)
		return false
	}

	if result.Value != expected {
		t.Errorf("object has wrong value. got=%t, want=%t", result.Value, expected)
		return false
	}

	return true
}

func TestBangOperator(t *testing.T) {

	tests := []struct {
		input    string
		expected bool
	}{
		{"!true", false},
		{"!false", true},

		{"!5", false},
		{"!!5", true},

		{"!!true", true},
		{"!!false", false},
	}

	for _, tt := range tests {

		evaluated := testEval(tt.input)
		testBooleanObject(t, evaluated, tt.expected)

	}

}

func TestIfElseExpressions(t *testing.T) {

	tests := []struct {
		input    string
		expected interface{}
	}{
		{"if(true){10}", 10},
		{"if(false){10}", nil},
		{"if(1){10}", 10},
		{"if(1<2){10}", 10},
		{"if(1>2){10}", nil},
		{"if(1>2){10}else{20}", 20},
		{"if(1<2){10}else{20}", 10},
	}

	for _, tt := range tests {

		evaluated := testEval(tt.input)

		integer, ok := tt.expected.(int)
		if ok {
			testIntegerObject(t, evaluated, int64(integer))
		} else {
			testNullObject(t, evaluated)
		}

	}

}

func testNullObject(t *testing.T, obj object.Object) bool {
	if obj != NULL {
		t.Errorf("object is not NULL. got=%T (%+v)", obj, obj)
		return false
	}
	return true
}

func TestReturnStatements(t *testing.T) {

	tests := []struct {
		input    string
		expected int64
	}{
		{"return 10;", 10},
		{"return 10; 9;", 10},
		{"return 2*5; 9;", 10},
		{"9; return 2*5; 9;", 10},
		{`
	if(10>1){
		if(10>1){
			return 10;
		}
		return 1;
	}
	`, 10},
	}

	for _, tt := range tests {

		evaluated := testEval(tt.input)

		testIntegerObject(t, evaluated, tt.expected)

	}
}

func TestErrorHandling(t *testing.T) {

	tests := []struct {
		input          string
		expectedErrMsg string
	}{
		{"5+true;", "type mismatch: INTEGER + BOOLEAN"},
		{"5+true; 5;", "type mismatch: INTEGER + BOOLEAN"},
		{"-true;", "unknown operator: -BOOLEAN"},
		{"true+false;", "unknown operator: BOOLEAN + BOOLEAN"},
		{"5; true+false; 5", "unknown operator: BOOLEAN + BOOLEAN"},
		{"if(10>1){true+false;}", "unknown operator: BOOLEAN + BOOLEAN"},
		{`
		if(10>1){
			if(10>1){
				return true+false;
			}
			return 1;
		}
		`, "unknown operator: BOOLEAN + BOOLEAN"},
		{"foobar", "identifier not found: foobar"},
	}

	for _, tt := range tests {

		evaluated := testEval(tt.input)

		errObj, ok := evaluated.(*object.Error)
		if !ok {
			t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
			continue
		}

		if errObj.Message != tt.expectedErrMsg {
			t.Errorf("wrong error message. expected=%q, got=%q", tt.expectedErrMsg, errObj.Message)
		}

	}

}

func TestLetStatements(t *testing.T) {

	tests := []struct {
		input    string
		expected int64
	}{
		{"let a=5; a;", 5},
		{"let a=5*5; a;", 25},
		{"let a=5; let b=a; b;", 5},
		{"let a=5; let b=a; let c=a+b+5; c;", 15},
	}

	for _, tt := range tests {

		testIntegerObject(t, testEval(tt.input), tt.expected)

	}
}

func TestFunctionObject(t *testing.T) {

	input := "fn(x){x+2;};"
	evaluated := testEval(input)

	fn, ok := evaluated.(*object.Function)
	if !ok {
		t.Fatalf("object is not Function. got=%T(%+v)", evaluated, evaluated)
	}

	if len(fn.Parameters) != 1 {
		t.Fatalf("function has wrong parameters. Parameters=%+v", fn.Parameters)
	}

	if fn.Parameters[0].String() != "x" {
		t.Fatalf("parameter is not 'x'. got=%q", fn.Parameters[0])
	}

	expectedBody := "(x + 2)"

	if fn.Body.String() != expectedBody {
		t.Fatalf("body is not %q. got=%q", expectedBody, fn.Body.String())
	}

}

func TestFunctionApplication(t *testing.T) {

	tests := []struct {
		input    string
		expected int64
	}{
		{"let identity=fn(x){x;}; identity(5);", 5},
		{"let identity=fn(x){return x;}; identity(5);", 5},
		{"let double=fn(x){x*2;}; double(5);", 10},
		{"let add=fn(x,y){x+y;}; add(5,5);", 10},
		{"let add=fn(x,y){x+y;}; add(5+5,add(5,5));", 20},
		{"fn(x){x;}(5)", 5},
	}

	for _, tt := range tests {

		testIntegerObject(t, testEval(tt.input), tt.expected)

	}
}

func TestClosure(t *testing.T) {

	input := `
	let newAdder=fn(x){
		fn(y){x+y;};
	};
	let addTwo=newAdder(2);
	addTwo(2);
	`

	testIntegerObject(t, testEval(input), 4)

}
