/*
 *
 *  * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
 *  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *  *
 *  * The Universal Permissive License (UPL), Version 1.0
 *  *
 *  * Subject to the condition set forth below, permission is hereby granted to any
 *  * person obtaining a copy of this software, associated documentation and/or
 *  * data (collectively the "Software"), free of charge and under any and all
 *  * copyright rights in the Software, and any and all patent rights owned or
 *  * freely licensable by each licensor hereunder covering either (i) the
 *  * unmodified Software as contributed to or provided by such licensor, or (ii)
 *  * the Larger Works (as defined below), to deal in both
 *  *
 *  * (a) the Software, and
 *  *
 *  * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 *  * one is included with the Software each a "Larger Work" to which the Software
 *  * is contributed by such licensors),
 *  *
 *  * without restriction, including without limitation the rights to copy, create
 *  * derivative works of, display, perform, and distribute the Software and make,
 *  * use, sell, offer for sale, import, export, have made, and have sold the
 *  * Software and the Larger Work(s), and to sublicense the foregoing rights on
 *  * either these or other terms.
 *  *
 *  * This license is subject to the following condition:
 *  *
 *  * The above copyright notice and either this complete permission notice or at a
 *  * minimum a reference to the UPL must be included in all copies or substantial
 *  * portions of the Software.
 *  *
 *  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *  * SOFTWARE.
 *
 */

package ninja.soroosh.hashem.lang.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.ArrayDeque;
import java.util.Deque;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.management.ExecutionEvent;
import org.graalvm.polyglot.management.ExecutionListener;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class HashemExecutionListenerTest {

    private Context context;
    private final Deque<ExecutionEvent> events = new ArrayDeque<>();

    private String expectedRootName;

    @Before
    public void setUp() {
        context = Context.create("hashemi");
    }

    @After
    public void tearDown() {
        assertTrue(events.isEmpty());
        context.close();
        context = null;
    }

    private void add(ExecutionEvent e) {
        events.add(e);
    }

    @Test
    public void testRootsAndStatements() {
        ExecutionListener.newBuilder().onEnter(this::add).onReturn(this::add).//
                        roots(true).statements(true).//
                        collectExceptions(true).collectInputValues(true).collectReturnValue(true).//
                        attach(context.getEngine());

        eval("bede 2;");

        enterRoot(rootSourceSection("bede 2;"));
        enterStatement("bede 2");
        leaveStatement("bede 2", null);
        leaveRoot(rootSourceSection("bede 2;"), 2);
    }

    @Test
    public void testStatements() {
        ExecutionListener.newBuilder().onEnter(this::add).onReturn(this::add).//
                        statements(true).//
                        collectExceptions(true).collectInputValues(true).collectReturnValue(true).//
                        attach(context.getEngine());

        eval("2 + 3;");
        enterStatement("2 + 3");
        leaveStatement("2 + 3", 5);

        eval("2 + 3; 3 + 6;");
        enterStatement("2 + 3");
        leaveStatement("2 + 3", 5);
        enterStatement("3 + 6");
        leaveStatement("3 + 6", 9);
    }

    @Test
    public void testExpressions() {
        ExecutionListener.newBuilder().onEnter(this::add).onReturn(this::add).//
                        expressions(true).//
                        collectExceptions(true).collectInputValues(true).collectReturnValue(true).//
                        attach(context.getEngine());
        eval("2 + 3;");

        enterStatement("2 + 3");
        enterExpression("2");
        leaveExpression("2", 2);
        enterExpression("3");
        leaveExpression("3", 3);
        leaveStatement("2 + 3", 5, 2, 3);
    }

    @Test
    public void testRoots() {
        ExecutionListener.newBuilder().onEnter(this::add).onReturn(this::add).//
                        roots(true).//
                        collectExceptions(true).collectInputValues(true).collectReturnValue(true).//
                        attach(context.getEngine());

        eval("bede 2;");

        enterRoot(rootSourceSection("bede 2;"));
        leaveRoot(rootSourceSection("bede 2;"), 2);
    }

    @Test
    public void testExpressionsStatementsAndRoots() {
        ExecutionListener.newBuilder().onEnter(this::add).onReturn(this::add).//
                        expressions(true).statements(true).//
                        collectExceptions(true).collectInputValues(true).collectReturnValue(true).//
                        attach(context.getEngine());

        eval("2 + 3;");

        enterStatement("2 + 3");
        enterExpression("2");
        leaveExpression("2", 2);
        enterExpression("3");
        leaveExpression("3", 3);
        leaveStatement("2 + 3", 5, 2, 3);
    }

    @Test
    public void testFactorial() {
        // @formatter:off
        String characters =
                        "fac(n) {" +
                        "  age (n <= 1) bood {" +
                        "    bede 1;" +
                        "  }" +
                        "  bede fac(n - 1) * n;" +
                        "}";
        // @formatter:on
        context.eval("hashemi", "bebin " + characters);
        Value factorial = context.getBindings("hashemi").getMember("fac");
        ExecutionListener.newBuilder().onReturn(this::add).onEnter(this::add).//
                        expressions(true).statements(true).roots(true).//
                        collectExceptions(true).collectInputValues(true).collectReturnValue(true).//
                        attach(context.getEngine());
        expectedRootName = "fac";
        assertEquals(0, events.size());
        for (int i = 0; i < 10; i++) {
            testFactorial(characters, factorial);
        }
    }

    private Value eval(String s) {
        expectedRootName = "wrapper";
        context.eval("hashemi", wrapInFunction(s));
        return context.getBindings("hashemi").getMember("wrapper").execute();
    }

    private static String wrapInFunction(String s) {
        return "bebin " + rootSourceSection(s);
    }

    private static String rootSourceSection(String s) {
        return "wrapper() {\n  " + s + " \n}";
    }

    private void testFactorial(String characters, Value factorial) {
        factorial.execute(3);
        enterRoot(characters);
        enterStatement("n <= 1");
        enterExpression("n");
        leaveExpression("n", 3);
        enterExpression("1");
        leaveExpression("1", 1);
        leaveStatement("n <= 1", false, 3, 1);
        enterStatement("bede fac(n - 1) * n");
        enterExpression("fac(n - 1) * n");
        enterExpression("fac(n - 1)");
        enterExpression("fac");
        leaveExpression("fac", factorial);
        enterExpression("n - 1");
        enterExpression("n");
        leaveExpression("n", 3);
        enterExpression("1");
        leaveExpression("1", 1);
        leaveExpression("n - 1", 2, 3, 1);

        enterRoot(characters);
        enterStatement("n <= 1");
        enterExpression("n");
        leaveExpression("n", 2);
        enterExpression("1");
        leaveExpression("1", 1);
        leaveStatement("n <= 1", false, 2, 1);
        enterStatement("bede fac(n - 1) * n");
        enterExpression("fac(n - 1) * n");
        enterExpression("fac(n - 1)");
        enterExpression("fac");
        leaveExpression("fac", factorial);
        enterExpression("n - 1");
        enterExpression("n");
        leaveExpression("n", 2);
        enterExpression("1");
        leaveExpression("1", 1);
        leaveExpression("n - 1", 1, 2, 1);

        enterRoot(characters);
        enterStatement("n <= 1");
        enterExpression("n");
        leaveExpression("n", 1);
        enterExpression("1");
        leaveExpression("1", 1);
        leaveStatement("n <= 1", true, 1, 1);
        enterStatement("bede 1");
        enterExpression("1");
        leaveExpression("1", 1);
        leaveStatement("bede 1", null, 1);
        leaveRoot(characters, 1);

        leaveExpression("fac(n - 1)", 1, factorial, 1);
        enterExpression("n");
        leaveExpression("n", 2);
        leaveExpression("fac(n - 1) * n", 2, 1, 2);
        leaveStatement("bede fac(n - 1) * n", null, 2);
        leaveRoot(characters, 2);

        leaveExpression("fac(n - 1)", 2, factorial, 2);
        enterExpression("n");
        leaveExpression("n", 3);
        leaveExpression("fac(n - 1) * n", 6, 2, 3);
        leaveStatement("bede fac(n - 1) * n", null, 6);
        leaveRoot(characters, 6);

        assertTrue(events.isEmpty());
    }

    private void enterExpression(String characters) {
        ExecutionEvent event = assertEvent(characters, null);
        assertTrue(event.isExpression());
        assertFalse(event.isStatement());
        assertFalse(event.isRoot());
    }

    private void enterStatement(String characters) {
        ExecutionEvent event = assertEvent(characters, null);
        assertTrue(event.isStatement());
        // statements are sometimes expressions
        assertFalse(event.isRoot());
    }

    private void enterRoot(String characters) {
        ExecutionEvent event = assertEvent(characters, null);
        assertTrue(event.isRoot());
        assertFalse(event.isStatement());
        assertFalse(event.isExpression());
    }

    private void leaveExpression(String characters, Object returnValue, Object... inputs) {
        ExecutionEvent event = assertEvent(characters, returnValue, inputs);
        assertTrue(event.isExpression());
        assertFalse(event.isStatement());
        assertFalse(event.isRoot());
    }

    private void leaveStatement(String characters, Object returnValue, Object... inputs) {
        ExecutionEvent event = assertEvent(characters, returnValue, inputs);
        assertTrue(event.isStatement());
        // statements are sometimes expressions
        assertFalse(event.isRoot());
    }

    private void leaveRoot(String characters, Object returnValue, Object... inputs) {
        ExecutionEvent event = assertEvent(characters, returnValue, inputs);
        assertTrue(event.isRoot());
        assertFalse(event.isStatement());
        assertFalse(event.isExpression());
    }

    private ExecutionEvent assertEvent(String characters, Object returnValue, Object... inputs) {
        ExecutionEvent event = events.pop();
        assertEquals(expectedRootName, event.getRootName());
        assertEquals(characters, event.getLocation().getCharacters());
        assertEquals(inputs.length, event.getInputValues().size());
        for (int i = 0; i < inputs.length; i++) {
            assertValue(inputs[i], event.getInputValues().get(i));
        }

        if (returnValue == null) {
            assertNull(event.getReturnValue());
        } else {
            assertValue(returnValue, event.getReturnValue());
        }

        assertNotNull(event.toString());
        return event;
    }

    private static void assertValue(Object expected, Value actual) throws AssertionError {
        if (actual.isNumber()) {
            assertEquals(expected, actual.asInt());
        } else if (actual.isBoolean()) {
            assertEquals(expected, actual.asBoolean());
        } else if (actual.canExecute()) {
            assertEquals(((Value) expected).getSourceLocation(), actual.getSourceLocation());
        } else {
            throw new AssertionError(expected.toString());
        }
    }

}