package pp.s1184725.boppi.test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

import java.util.List;
import java.util.logging.*;

import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;

import pp.iloc.Simulator;
import pp.iloc.model.Program;
import pp.s1184725.boppi.*;

/**
 * Test cases for the code generating stage for all language features.
 */
public class GeneratorTest {
	private List<LogRecord> log;
	private String[] out;

	private void compileAndRunString(String code, String... input) {
		compileAndRun(ToolChain.getCharStream(code), String.join("\n", input) + "\n");
	}

	private void compileAndRunFile(String file, String... input) {
		compileAndRun(ToolChain.getCharStream(AllTests.directory.resolve(file)), String.join("\n", input) + "\n");
	}

	private void compileAndRun(CharStream stream, String input) {
		Logger logger = Logger.getAnonymousLogger();
		log = ToolChain.makeListLog(logger);
		logger.setLevel(AllTests.logLevel);

		ParseTree ast = ToolChain.getParser(ToolChain.getLexer(stream, logger), logger).program();
		Annotations annotations = ToolChain.getAnnotations(ast, logger);
		Program program = ToolChain.getILOC(ast, logger, annotations);

		if (Simulator.DEBUG)
			System.out.println(program.prettyPrint());

		out = ToolChain.execute(program, logger, input).split("\n");
	}

	@SuppressWarnings("unused")
	private void debugRun(CharStream stream, String... input) {
		Simulator.DEBUG = true;
		String in = String.join("\n", input) + "\n";
		Logger logger = Logger.getAnonymousLogger();
		log = ToolChain.makeListLog(logger);
		logger.setLevel(Level.FINEST);

		ParseTree ast = ToolChain.getParser(ToolChain.getLexer(stream, logger), logger).program();
		Annotations annotations = ToolChain.getAnnotations(ast, logger);
		Program program = ToolChain.getILOC(ast, logger, annotations);

		System.out.println(program.prettyPrint());

		out = ToolChain.execute(program, logger, in).split("\n");

		log.forEach((entry) -> System.out.println(entry.getMessage()));
		Simulator.DEBUG = false;
	}

	/**
	 * Correct basic expressions
	 */
	@Test
	public void correctExpressionTest() {
		compileAndRunFile("simpleExpression.boppi");
		assertThat(log, is(empty()));

		int n = 12;
		String expression = StringUtils.repeat("1+(", n) + "1" + StringUtils.repeat(")", n);
		AllTests.withWarnings(() -> {
			compileAndRunString("print(" + expression + ")", "" + n);
			assertThat(log, hasSize(1));
			assertThat(log.get(0).getLevel(), is(Level.WARNING));
		});
	}

	/**
	 * Basic expression with run-time error
	 */
	@Test
	public void wrongExpressionTest() {
		compileAndRunString("1/0");
		assertThat(log, hasSize(1));
		assertThat(log.get(0).getMessage(), containsString("zero"));
	}

	/**
	 * Correct variable use
	 */
	@Test
	public void correctVariableTest() {
		compileAndRunFile("simpleVariable.boppi");
		assertThat(log, is(empty()));
	}

	/**
	 * Correct evaluation of basic programs
	 */
	@Test
	public void basicPrograms() {
		compileAndRunString("print(5*3)");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("15")));

		compileAndRunString("print({var int x; x := 8; print(x)})");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("8", "8")));

		compileAndRunString("print('T', 'e', 's', 't', '!')");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("T", "e", "s", "t", "!")));

		compileAndRunString("var int x; var int y; x := 3*(y := 4); print(x,y)");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("12", "4")));

		compileAndRunFile("basicProgram1.boppi", "1", "T");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("T", "T")));

		compileAndRunFile("fibonacciIterative.boppi", "6");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("13")));
	}

	/**
	 * Correct evaluation of basic functions
	 */
	@Test
	public void correctSimpleFunctionTest() {
		compileAndRunString("function int id(int a) a; 1");
		assertThat(log, is(empty()));

		compileAndRunString("function int id(int a) a; id(1); 1");
		assertThat(log, is(empty()));

		compileAndRunString("var int a; function int const(int b) a; 1");
		assertThat(log, is(empty()));

		compileAndRunString("var int a; a := 1; function int const(int b) a; print(const(4))");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("1")));

		compileAndRunString("function int add(int a, int b) a+b; print(add(4, 5)+1)");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("10")));

		compileAndRunString("function char const(char a, char b) a; print(const('A', 'T'))");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("A")));
	}

	/**
	 * Correct evaluation of recursive functions, nested functions and function
	 * passing
	 */
	@Test
	public void correctComplexFunctionTest() {
		compileAndRunString("function int factorial(int n) {if (n > 1) then n*factorial(n-1) else 1 fi}");
		assertThat(log, is(empty()));

		compileAndRunFile("recursiveFactorial.boppi", "5");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("120")));

		compileAndRunFile("simpleFunctionPassing.boppi");
		assertThat(log, is(empty()));
		assertThat(out, is(arrayContaining("40", "104", "1", "2", "8")));
	}
}
