package elm

import (
	"testing"
	"github.com/stretchr/testify/assert"
)

func All[T any](ts []T, pred func(T) bool) bool {
    for _, t := range ts {
        if !pred(t) {
            return false
        }
    }
    return true
}

func IsZero(b ElmValue) bool { return b.Number == int64(0) }

func TestHaltingDoesNothing(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: HALT})
	
	m.Execute()

	assert.Equal(t, int64(0), m.ip, "Instruction pointer should not be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.True(t, All(m.stack, IsZero), "Stack should be empty");
}

func TestPush(t* testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 69})
	m.Push(Inst{Operation: HALT})
	
	m.Execute()

	assert.Equal(t, int64(1), m.ip, "Instruction pointer should be incremented");
	assert.False(t, m.isHalted, "Virtual Machine should not be Halted");
	assert.Equal(t, int64(69), m.stack[0].Number, "Stack should contain value");
}

func TestPushingSequence(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 69})
	m.Push(Inst{Operation: PUSH, Operand: 42})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(2), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(69), m.stack[0].Number, "Stack should contain value");
	assert.Equal(t, int64(42), m.stack[1].Number, "Stack should contain value");
	assert.Equal(t, int64(0), m.stack[3].Number, "Stack should be zero after");
}

func TestAdd(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 1})
	m.Push(Inst{Operation: PUSH, Operand: 2})
	m.Push(Inst{Operation: ADD})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(3), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(3), m.stack[0].Number, "Stack should contain value");
	// todo: should pass that test?
	//assert.Equal(t, int64(0), m.stack[1].Number, "Stack should be zero after");
}

func TestMul(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 3})
	m.Push(Inst{Operation: PUSH, Operand: 2})
	m.Push(Inst{Operation: MUL})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(3), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(6), m.stack[0].Number, "Stack should contain value");
	// todo: should pass that test?
	//assert.Equal(t, int64(0), m.stack[1].Number, "Stack should be zero after");
}

func TestSub(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 3})
	m.Push(Inst{Operation: PUSH, Operand: 2})
	m.Push(Inst{Operation: SUB})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(3), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(1), m.stack[0].Number, "Stack should contain value");
	// todo: should pass that test?
	//assert.Equal(t, int64(0), m.stack[1].Number, "Stack should be zero after");
}

func TestDiv(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: PUSH, Operand: 2})
	m.Push(Inst{Operation: DIV})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(3), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(3), m.stack[0].Number, "Stack should contain value");
	// todo: should pass that test?
	//assert.Equal(t, int64(0), m.stack[1].Number, "Stack should be zero after");
}

func TestAddNeedTwoItems(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: ADD})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestSubNeedTwoItems(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: SUB})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestMulNeedTwoItems(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: MUL})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestDivNeedTwoItems(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: DIV})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestDivNeedNonZeroValues(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: PUSH, Operand: 0})
	m.Push(Inst{Operation: DIV})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestDivNeedNonZeroValues2(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 0})
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: DIV})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}


func TestPop(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: POP})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(2), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.True(t, All(m.stack, IsZero), "Stack should be empty");
}

func TestPopNeedsNonEmptyStack(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: POP})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestDup(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: DUP})
	m.Push(Inst{Operation: HALT})

	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(2), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(6), m.stack[0].Number, "Stack should contain initial value");
	assert.Equal(t, int64(6), m.stack[1].Number, "Stack should contain duplicated value");
	assert.Equal(t, int64(0), m.stack[2].Number, "Stack should be zero after");
}

func TestDupByPointer(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: PUSH, Operand: 2})
	m.Push(Inst{Operation: DUP, Operand: 1})
	m.Push(Inst{Operation: HALT})

	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(3), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(6), m.stack[0].Number, "Stack should contain initial value");
	assert.Equal(t, int64(2), m.stack[1].Number, "Stack should contain initial value");
	assert.Equal(t, int64(6), m.stack[2].Number, "Stack should contain right duplicated value");
	assert.Equal(t, int64(0), m.stack[3].Number, "Stack should be zero after");
}

func TestEquals(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 2})
	m.Push(Inst{Operation: PUSH, Operand: 2})
	m.Push(Inst{Operation: EQ})
	m.Push(Inst{Operation: PUSH, Operand: 2})
	m.Push(Inst{Operation: PUSH, Operand: 1})
	m.Push(Inst{Operation: EQ})
	m.Push(Inst{Operation: HALT})

	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(6), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(1), m.stack[0].Number, "First quality should be true");
	assert.Equal(t, int64(0), m.stack[1].Number, "Second quality should not be true");
	assert.Equal(t, int64(0), m.stack[3].Number, "Stack should be zero after");
}

func TestEqualsNeedTwoItems(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: DIV})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestJump(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: JMP, Operand: 3})
	m.Push(Inst{Operation: PUSH, Operand: 5})
	m.Push(Inst{Operation: PUSH, Operand: 3})
	m.Push(Inst{Operation: HALT})

	err := m.Run()

	notEqualToFive := func (x ElmValue) bool { return x.Number != int64(5) }

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(4), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.True(t, All(m.stack, notEqualToFive), "Stack should not contain skipped value");
	assert.Equal(t, int64(6), m.stack[0].Number, "Stack should contain pushed value");
	assert.Equal(t, int64(3), m.stack[1].Number, "Stack should contain pushed value");
	assert.Equal(t, int64(0), m.stack[2].Number, "Stack should be zero afterall");
}

func TestJumpDontPassIllegalAccess(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: JMP, Operand: 3})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestJumpCanPassLastInstruction(t * testing.T) {
	m := Constructor()
	
	m.Push(Inst{Operation: JMP, Operand: 2})
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(2), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
}

func TestJumpDontPassIllegalAccess2(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: JMP, Operand: -1})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestJumpIfDontPassWithoutStackValue(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: JMPIF, Operand: 2})
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestJumpIfDontPassIfNotTrue(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 0})
	m.Push(Inst{Operation: JMPIF, Operand: 3})
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(3), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(0), m.stack[0].Number, "")
	assert.Equal(t, int64(6), m.stack[1].Number, "Should not skip the pushed value")
}


func TestJumpIfPassIfTrue(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 1})
	m.Push(Inst{Operation: JMPIF, Operand: 3})
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: PUSH, Operand: 69})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(4), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(69), m.stack[0].Number, "Should skip the pushed value")
	assert.Equal(t, int64(0), m.stack[1].Number, "Be zero afterall")
}

func TestJumpIfDontPassIllegalAccess2(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: JMPIF, Operand: -1})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestJumpIfDontPassIllegalAccess(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: JMPIF, Operand: 3})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestCallDontPassIllegallAccess(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: CALL, Operand: 3})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestCallDontPassIllegallAccess2(t *testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: CALL, Operand: -3})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestCallPass(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 1})
	m.Push(Inst{Operation: CALL, Operand: 3})
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: PUSH, Operand: 69})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(4), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(1), m.stack[0].Number, "Should skip the pushed value")
	assert.Equal(t, int64(69), m.stack[1].Number, "Should skip the pushed value")
	assert.Equal(t, 2, len(m.stackFrame), "Should create the function stack frame")
	assert.Equal(t, int64(0), m.stack[2].Number, "Should be zero afterall")
}

func TestRetPass(t * testing.T) {
	m := Constructor()
	m.Push(Inst{Operation: PUSH, Operand: 1})
	m.Push(Inst{Operation: CALL, Operand: 4})
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: HALT})
	m.Push(Inst{Operation: PUSH, Operand: 69})
	m.Push(Inst{Operation: RET})

	
	err := m.Run()

	assert.Nil(t, err, "Should be successfull")
	assert.Equal(t, int64(3), m.ip, "Instruction pointer should be incremented");
	assert.True(t, m.isHalted, "Virtual Machine should be Halted");
	assert.Equal(t, int64(1), m.stack[0].Number, "Should skip the pushed value")
	assert.Equal(t, int64(69), m.stack[1].Number, "Should skip the pushed value")
	assert.Equal(t, int64(6), m.stack[2].Number, "Should skip the pushed value")
	assert.Equal(t, int64(0), m.stack[3].Number, "Should be zero afterall")
	assert.Equal(t, 1, len(m.stackFrame), "Should pop the function stack frame")
}

func TestStoreWontPassIfStackEmpty(t *testing.T) {
	m := Constructor()

	m.Push(Inst{Operation: STORE, Operand: 1})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestStoreWontPassIfNegativeOperand(t *testing.T) {
	m := Constructor()
	
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: STORE, Operand: -1})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.NotNil(t, err, "Should be errored");
}

func TestStoreShouldStoreOnTheFrame(t *testing.T) {
	m := Constructor()
	
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: STORE, Operand: 0})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should not be errored");
	assert.Equal(t, int64(0), m.sp, "Should decrement the stack pointer")
	assert.Equal(t, int64(6), m.stackFrame[0].Locals[0].Number, "Should contain the value")
	assert.Equal(t, int64(2), m.ip, "Instruction pointer should be incremented");
}

func TestGetShouldGetVariableFromFrame(t *testing.T) {
	m := Constructor()
	
	m.Push(Inst{Operation: PUSH, Operand: 6})
	m.Push(Inst{Operation: STORE, Operand: 0})
	m.Push(Inst{Operation: PUSH, Operand: 1})
	m.Push(Inst{Operation: GET, Operand: 0})
	m.Push(Inst{Operation: HALT})
	
	err := m.Run()

	assert.Nil(t, err, "Should not be errored");
	assert.Equal(t, int64(6), m.stack[1].Number, "Should contain the value")
	assert.Equal(t, int64(4), m.ip, "Instruction pointer should be incremented");
}


