package pp.s1184725.boppi.test;

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

import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

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

/**
 * Test suite for all Boppi language features. This suite contains helper
 * functions and attributes to assist test cases. Each test case should consider
 * a single language feature across all compiler stages.
 * 
 * @author Frank Wibbelink
 */
@RunWith(Suite.class)
@SuiteClasses({ ExpressionTest.class, SimpleVariableTest.class, ConditionalTest.class, SimpleFunctionTest.class })
public class BoppiTests {
	/**
	 * The path for test programs
	 */
	public static final Path TEST_PROGRAM_LOCATION = Paths
			.get("src/" + BoppiTests.class.getPackage().getName().replaceAll("\\.", "/") + "/programs/");

	/**
	 * The default logging level
	 */
	public static final Level DEFAULT_LOG_LEVEL = Level.SEVERE;

	/**
	 * Log records of last test
	 */
	public static List<LogRecord> log;

	/**
	 * Output of last program split by line breaks
	 */
	public static String[] out;

	/**
	 * The level of error reporting to use for the tests. Can be changed to
	 * include finer tests.
	 */
	private static Level logLevel = DEFAULT_LOG_LEVEL;

	/**
	 * Runs a test with warnings enabled.
	 * 
	 * @param test
	 *            the test to run
	 */
	public static void withWarnings(Runnable test) {
		logLevel = Level.WARNING;
		test.run();
		logLevel = DEFAULT_LOG_LEVEL;
	}

	/**
	 * Parses a string of code, logging to {@link #log}.
	 * 
	 * @param code
	 *            the code to parse
	 */
	public static void parseString(String code) {
		parse(ToolChain.getCharStream(code));
	}

	/**
	 * Parses code from a file, logging to {@link #log}.
	 * 
	 * @param file
	 *            the file name
	 */
	public static void parseFile(String file) {
		parse(ToolChain.getCharStream(TEST_PROGRAM_LOCATION.resolve(file)));
	}

	private static void parse(CharStream stream) {
		Logger logger = Logger.getAnonymousLogger();
		log = ToolChain.makeListLog(logger);
		logger.setLevel(logLevel);

		ToolChain.getParser(ToolChain.getLexer(stream, logger), logger).program();
	}

	/**
	 * Parses and type checks a string of code, logging to {@link #log}.
	 * 
	 * @param code
	 *            the code to type check
	 */
	public static void checkString(String code) {
		check(ToolChain.getCharStream(code));
	}

	/**
	 * Parses and type checks code from a file, logging to {@link #log}.
	 * 
	 * @param file
	 *            the file name
	 */
	public static void checkFile(String file) {
		check(ToolChain.getCharStream(TEST_PROGRAM_LOCATION.resolve(file)));
	}

	private static void check(CharStream stream) {
		Logger logger = Logger.getAnonymousLogger();
		log = ToolChain.makeListLog(logger);
		logger.setLevel(logLevel);

		ToolChain.getAnnotations(ToolChain.getParser(ToolChain.getLexer(stream, logger), logger).program(), logger);
	}

	/**
	 * Compiles and runs a program from a string, logging to {@link #log} and
	 * writing lines of output to {@link #out}.
	 * 
	 * @param code
	 *            the code to type check
	 * @param input
	 *            lines of input for the program
	 */
	public static void compileAndRunString(String code, String... input) {
		compileAndRun(ToolChain.getCharStream(code), String.join("\n", input) + "\n");
	}

	/**
	 * Compiles and runs a program from a file, logging to {@link #log} and
	 * writing lines of output to {@link #out}.
	 * 
	 * @param file
	 *            the file name
	 * @param input
	 *            lines of input for the program
	 */
	public static void compileAndRunFile(String file, String... input) {
		compileAndRun(ToolChain.getCharStream(TEST_PROGRAM_LOCATION.resolve(file)), String.join("\n", input) + "\n");
	}

	private static void compileAndRun(CharStream stream, String input) {
		Logger logger = Logger.getAnonymousLogger();
		log = ToolChain.makeListLog(logger);
		logger.setLevel(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");
	}

	/**
	 * Compiles and runs a program in debug mode, printing the generated
	 * program, a simulation of the program and all log messages.
	 * 
	 * @param stream
	 *            a character stream with the program code
	 * @param input
	 *            lines of input for the program
	 */
	public static 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;
	}

}
