/**
 * Sealed Sins, 2023.
 */
import { VM, VmReg } from './vm';

describe('Virtual Machine', () => {
	it('is serializable', () => {
		const vm = new VM([
			['let', VmReg.R0, 1],
			['yld'],
			['let', VmReg.R0, 2],
			['yld'],
			['let', VmReg.R0, 3],
		]);
		vm.next();
		const state = vm.save();
		expect(vm.getReg(VmReg.R0)).toEqual(1);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(2);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(3);
		vm.load(state);
		expect(vm.getReg(VmReg.R0)).toEqual(1);
	});

	it('is suitable for writing a visual novel', () => {
		const vm = new VM([
			['let', VmReg.R0, { name: '', text: '' }],
			['sti', VmReg.R0, 'state'],

			// Mark page start.
			['tag', 'pageStart:1'],
			['yld'],

			// Load state & event info.
			['ldi', VmReg.R0, 'state'],
			['ldi', VmReg.R1, 'event'],

			// Check event type and proceed if it's `next`.
			['get', VmReg.R2, VmReg.R1, 'type'],
			['let', VmReg.R3, 'next'],
			['jeq', VmReg.R2, VmReg.R3, 'pageUpdate:1'],
			['jmp', 'pageStart:1'],

			// Update state.
			['tag', 'pageUpdate:1'],
			['let', VmReg.R4, { text: 'Hello!' }],
			['mrg', VmReg.R0, VmReg.R4],

			// Mark next page start...
			['tag', 'pageStart:2'],
			['yld'],
		]);

		vm.next();
		expect(vm.getVar('state')).toEqual({ name: '', text: '' });

		vm.setVar('event', { type: 'not-next' });
		vm.next();
		expect(vm.getVar('state')).toEqual({ name: '', text: '' });

		vm.setVar('event', { type: 'next' });
		vm.next();
		expect(vm.getVar('state')).toEqual({ name: '', text: 'Hello!' });
	});

	it('implements "let" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 1],
			['let', VmReg.R1, 2],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(1);
		expect(vm.getReg(VmReg.R1)).toEqual(2);
	});

	it('implements "mov" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 1],
			['let', VmReg.R1, 2],
			['mov', VmReg.R0, VmReg.R1],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(2);
		expect(vm.getReg(VmReg.R1)).toEqual(2);
	});

	it('implements "get" command', () => {
		const vm = new VM([
			['let', VmReg.R0, { a: 1, b: 2, c: [3, 4] }],
			['get', VmReg.R1, VmReg.R0, 'a'],
			['get', VmReg.R2, VmReg.R0, 'b'],
			['get', VmReg.R3, VmReg.R0, 'c[0]'],
			['get', VmReg.R4, VmReg.R0, 'c[1]'],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R1)).toEqual(1);
		expect(vm.getReg(VmReg.R2)).toEqual(2);
		expect(vm.getReg(VmReg.R3)).toEqual(3);
		expect(vm.getReg(VmReg.R4)).toEqual(4);
	});

	it('implements "set" command', () => {
		const vm = new VM([
			['let', VmReg.R0, {}],
			['let', VmReg.R1, 1],
			['let', VmReg.R2, 2],
			['set', VmReg.R1, VmReg.R0, 'a'],
			['set', VmReg.R2, VmReg.R0, 'b'],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual({ a: 1, b: 2 });
	});

	it('implements "mrg" command', () => {
		const vm = new VM([
			['let', VmReg.R0, { a: 1 }],
			['let', VmReg.R1, { b: 2, c: 3 }],
			['mrg', VmReg.R0, VmReg.R1],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual({ a: 1, b: 2, c: 3 });
		expect(vm.getReg(VmReg.R1)).toEqual({ b: 2, c: 3 });
	});

	it('implements "ldi" command', () => {
		const vm = new VM([
			['ldi', VmReg.R0, 'a'],
			['ldi', VmReg.R1, 'b'],
		]);
		vm.setVar('a', 1);
		vm.setVar('b', 2);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(1);
		expect(vm.getReg(VmReg.R1)).toEqual(2);
	});

	it('implements "sti" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 1],
			['let', VmReg.R1, 2],
			['sti', VmReg.R0, 'a'],
			['sti', VmReg.R1, 'b'],
		]);
		vm.next();
		expect(vm.getVar('a')).toEqual(1);
		expect(vm.getVar('b')).toEqual(2);
	});

	it('implements "tag", "jmp" and "yld" commands', () => {
		const vm = new VM([
			['tag', 'start'],
			['let', VmReg.R0, 0],
			['yld'],
			['let', VmReg.R0, 1],
			['yld'],
			['jmp', 'start'],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(0);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(1);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(0);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(1);
	});

	it('implements "jeq" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 0],
			['let', VmReg.R1, 1],
			['jeq', VmReg.R0, VmReg.R1, 'isEqual'],
			['yld'],
			['let', VmReg.R0, 1],
			['jeq', VmReg.R0, VmReg.R1, 'isEqual'],
			['yld'],
			['tag', 'isEqual'],
			['let', VmReg.R2, true],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R2)).not.toEqual(true);
		vm.next();
		expect(vm.getReg(VmReg.R2)).toEqual(true);
	});

	it('implements "jgt" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 0],
			['let', VmReg.R1, 0],
			['jgt', VmReg.R0, VmReg.R1, 'isGreater'],
			['yld'],
			['let', VmReg.R0, 1],
			['jgt', VmReg.R0, VmReg.R1, 'isGreater'],
			['yld'],
			['tag', 'isGreater'],
			['let', VmReg.R2, true],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R2)).not.toEqual(true);
		vm.next();
		expect(vm.getReg(VmReg.R2)).toEqual(true);
	});

	it('implements "jlt" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 0],
			['let', VmReg.R1, 0],
			['jlt', VmReg.R0, VmReg.R1, 'isSmaller'],
			['yld'],
			['let', VmReg.R1, 1],
			['jlt', VmReg.R0, VmReg.R1, 'isSmaller'],
			['yld'],
			['tag', 'isSmaller'],
			['let', VmReg.R2, true],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R2)).not.toEqual(true);
		vm.next();
		expect(vm.getReg(VmReg.R2)).toEqual(true);
	});

	it('implements "dbg" command', () => {
		const logMock = jest.spyOn(console, 'log').mockImplementation();
		const vm = new VM([
			['let', VmReg.R0, 1],
			['let', VmReg.R1, { a: 2, b: 3 }],
			['sti', VmReg.R0, 'x'],
			['sti', VmReg.R1, 'y'],
			['dbg', '{{ reg.R0 }} {{ reg.R1.a }} {{ reg.R1.b }}'],
			['dbg', '{{ mem.x }} {{ mem.y.a }} {{ mem.y.b }}'],
		]);
		vm.next();
		expect(logMock).toHaveBeenNthCalledWith(1, '1 2 3');
		expect(logMock).toHaveBeenNthCalledWith(2, '1 2 3');
		logMock.mockRestore();
	});

	it('implements "fmt" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 1],
			['let', VmReg.R1, { a: 2, b: 3 }],
			['sti', VmReg.R0, 'x'],
			['sti', VmReg.R1, 'y'],
			['fmt', VmReg.R3, '{{ reg.R0 }} {{ reg.R1.a }} {{ reg.R1.b }}'],
			['fmt', VmReg.R4, '{{ mem.x }} {{ mem.y.a }} {{ mem.y.b }}'],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R3)).toEqual('1 2 3');
		expect(vm.getReg(VmReg.R4)).toEqual('1 2 3');
	});

	it('implements "not" command', () => {
		const vm = new VM([
			['let', VmReg.R0, true],
			['not', VmReg.R0],
			['yld'],
			['not', VmReg.R0],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(false);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(true);
	});

	it('implements "inc" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 0],
			['inc', VmReg.R0],
			['inc', VmReg.R0],
			['inc', VmReg.R0],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(+3);
	});

	it('implements "dec" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 0],
			['dec', VmReg.R0],
			['dec', VmReg.R0],
			['dec', VmReg.R0],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(-3);
	});

	it('implements "add" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 1],
			['let', VmReg.R1, 2],
			['add', VmReg.R0, VmReg.R1],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(3);
		expect(vm.getReg(VmReg.R1)).toEqual(2);
	});

	it('implements "sub" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 3],
			['let', VmReg.R1, 2],
			['sub', VmReg.R0, VmReg.R1],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(1);
		expect(vm.getReg(VmReg.R1)).toEqual(2);
	});

	it('implements "mul" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 4],
			['let', VmReg.R1, 2],
			['mul', VmReg.R0, VmReg.R1],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(8);
		expect(vm.getReg(VmReg.R1)).toEqual(2);
	});

	it('implements "div" command', () => {
		const vm = new VM([
			['let', VmReg.R0, 4],
			['let', VmReg.R1, 2],
			['div', VmReg.R0, VmReg.R1],
		]);
		vm.next();
		expect(vm.getReg(VmReg.R0)).toEqual(2);
		expect(vm.getReg(VmReg.R1)).toEqual(2);
	});
});