package interpreter;

import static error.ErrorManager.Severity.CRITICAL;
import static interpreter.InteractiveLispInterpreter.PROMPT;
import static java.text.MessageFormat.format;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static testutil.TestUtilities.createInputStreamFromString;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.HashSet;
import java.util.Set;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import environment.RuntimeEnvironment;
import interpreter.LispInterpreterBuilderImpl.InterpreterAlreadyBuiltException;

public class LispInterpreterTest {

    private static final String TERMINATED = "terminated";

    private Set<String> indicatorSet;
    private ByteArrayOutputStream outputStream;
    private ByteArrayOutputStream errorOutputStream;
    private RuntimeEnvironment environment;
    private LispInterpreterBuilder builder;

    public LispInterpreterTest() {
        this.environment = RuntimeEnvironment.getInstance();

        this.builder = new LispInterpreterBuilderImpl() {

            @Override
            public void reset() {
                this.isBuilt = false;
            }
        };
    }

    private void setCommonFeatures() {
        builder.setOutput(new PrintStream(outputStream));
        builder.setErrorOutput(new PrintStream(errorOutputStream));
        builder.setTerminationFunction(() -> {});
        builder.setErrorTerminationFunction(() -> indicatorSet.add(TERMINATED));
    }

    private void assertTerminated() {
        assertTrue(indicatorSet.contains(TERMINATED));
    }

    private void assertErrorMessageWritten() {
        assertTrue(errorOutputStream.toByteArray().length > 0);
    }

    @Before
    public void setUp() {
        indicatorSet = new HashSet<>();
        outputStream = new ByteArrayOutputStream();
        errorOutputStream = new ByteArrayOutputStream();
        builder.reset();
        environment.reset();
    }

    @After
    public void tearDown() {
        builder.reset();
        environment.reset();
    }

    @Test
    public void buildInteractiveInterpreter() {
        setCommonFeatures();
        builder.setInput(System.in, "stdin");
        LispInterpreter interpreter = builder.build();

        assertTrue(interpreter instanceof InteractiveLispInterpreter);
    }

    @Test
    public void buildNonInteractiveInterpreter() {
        setCommonFeatures();
        builder.setInput(System.in, "stdin");
        builder.setNotInteractive();
        LispInterpreter interpreter = builder.build();

        assertFalse(interpreter instanceof InteractiveLispInterpreter);
        assertFalse(interpreter instanceof FileLispInterpreter);
    }

    @Test
    public void buildFileBasedInterpreter() {
        setCommonFeatures();
        builder.useFile("test/interpreter/test-files/file.lisp");
        LispInterpreter interpreter = builder.build();

        assertTrue(interpreter instanceof FileLispInterpreter);
    }

    @Test(expected = InterpreterAlreadyBuiltException.class)
    public void cannotBuildMoreThanOneInterpreter() {
        builder.build();
        builder.build();
    }

    @Test
    public void interpreterAlreadyBuiltException_HasCorrectAttributes() {
        InterpreterAlreadyBuiltException e = new InterpreterAlreadyBuiltException();

        assertEquals(CRITICAL, e.getSeverity());
        assertNotNull(e.getMessage());
        assertTrue(e.getMessage().length() > 0);
    }

    @Test(expected = InterpreterAlreadyBuiltException.class)
    public void resetNormallyDoesNothing() {
        builder = new LispInterpreterBuilderImpl();

        builder.build();
        builder.reset();
        builder.build();
    }

    @Test
    public void attemptToBuildInterpreterOnBadFile() {
        setCommonFeatures();
        builder.useFile("test/interpreter/test-files/does-not-exist.lisp");
        builder.build();

        assertErrorMessageWritten();
        assertTerminated();
    }

    @Test
    public void makeSureDecoratorsAreInitializedWithDefaults() {
        builder.build();

        assertEquals("", environment.decoratePrompt(""));
        assertEquals("", environment.decorateValueOutput(""));
        assertEquals("", environment.decorateWarningOutput(""));
        assertEquals("", environment.decorateErrorOutput(""));
        assertEquals("", environment.decorateCriticalOutput(""));
    }

    @Test
    public void makeSureDecoratorsAreSetCorrectly() {
        builder.setPromptDecorator(s -> "#" + s + "#");
        builder.setValueOutputDecorator(s -> "@" + s + "@");
        builder.setWarningOutputDecorator(s -> "%" + s + "%");
        builder.setErrorOutputDecorator(s -> "*" + s + "*");
        builder.setCriticalOutputDecorator(s -> "$" + s + "$");
        builder.build();

        assertEquals("#x#", environment.decoratePrompt("x"));
        assertEquals("@x@", environment.decorateValueOutput("x"));
        assertEquals("%x%", environment.decorateWarningOutput("x"));
        assertEquals("*x*", environment.decorateErrorOutput("x"));
        assertEquals("$x$", environment.decorateCriticalOutput("x"));
    }

    @Test
    public void fileBasedInterpreterWorks_PrintsLastValueOnly() {
        setCommonFeatures();
        builder.useFile("test/interpreter/test-files/file.lisp");
        builder.build().interpret();

        assertEquals("PICKLE\n\n", outputStream.toString());
        assertEquals("", errorOutputStream.toString());
    }

    @Test
    public void interactiveInterpreterWorks() {
        setCommonFeatures();
        builder.setInput(createInputStreamFromString("'pickle"), "input");
        builder.build().interpret();

        assertEquals(format("{0}\n{1}\n{0}\n", PROMPT, "PICKLE"), outputStream.toString());
        assertEquals("", errorOutputStream.toString());
    }

    @Test
    public void interpreterHandlesError() {
        setCommonFeatures();
        builder.setNotInteractive();
        builder.setInput(createInputStreamFromString("pickle"), "input");
        builder.build().interpret();

        assertEquals("\n", outputStream.toString());
        assertEquals("[error] symbol PICKLE has no value\n", errorOutputStream.toString());
    }

}
