package io.wealthwizards.appsynctesting;

import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

@DisplayName("Evaluator")
public class EvaluatorTest {

    @Test
    @DisplayName("should wire up $context for use in velocity templates")
    public void shouldWireUpContext() throws NoSuchFieldException, IllegalAccessException {
        var prev = new HashMap<String, Object>(
                Map.of(
                    "result", Map.of(
                        "name", "Peter",
                        "details", Map.of(
                                "height", 6f
                        )
                    )
                )
        );

        var context = new AppSyncContext();
        context.setPrev(prev);

        var evaluator = new Evaluator(context, new AppSyncState());

        var template = "{ \"name\": \"$context.prev.result.name\", \"height\": \"$context.prev.result.details.height\" }";
        var actual = evaluator.evaluate(template);

        var expected = "{ \"name\": \"Peter\", \"height\": \"6.0\" }";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should wire up $extensions")
    public void shouldWireUpExtensions() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "$util.toJson($extensions.evictFromApiCache(\"Query\", \"adviserCases\", {\"context.arguments.semester\": \"ADVISER#adviserId\"}))";
        var actual = evaluator.evaluate(template);

        var expected = "{\"apiCacheEntriesDeleted\":0}";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should wire up $ctx and $utils aliases")
    public void shouldWireUpAliases() throws NoSuchFieldException, IllegalAccessException {
        var prev = new HashMap<String, Object>(
                Map.of(
                        "result", Map.of(
                                "name", "Peter",
                                "details", Map.of(
                                        "height", 6f
                                )
                        )
                )
        );

        var context = new AppSyncContext();
        context.setPrev(prev);

        var evaluator = new Evaluator(context, new AppSyncState());

        var template = "{ \"name\": $utils.toJson($ctx.prev.result.name), \"height\": $utils.toJson($ctx.prev.result.details.height) }";
        var actual = evaluator.evaluate(template);

        var expected = "{ \"name\": \"Peter\", \"height\": 6.0 }";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should create and inject $util context with top level utils")
    public void shouldCreateUtilContext() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "$util.isString(\"Hello, World.\")";
        var actual = evaluator.evaluate(template);

        var expected = "true";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should hang nested utils on $util context")
    public void shouldCreateNestedTimeUtils() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "$util.time.epochMilliSecondsToSeconds(1517943695758)";
        var actual = evaluator.evaluate(template);

        var expected = "1517943695";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should halt execution and catch exceptions")
    public void shouldCatchExceptions() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "{\"happy\":\"output to be ignored\"} \n$util.unauthorized()";
        var actual = evaluator.evaluate(template);

        var expected = new AppSyncException(
                "Unauthorized",
                "Unauthorized",
                null,
                null
        );

        assertEquals(expected, actual.getError());
        assertNull(actual.getResult());
    }

    @Test
    @DisplayName("should add custom #return directive")
    public void shouldImplementReturn() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "#set($value = {\"real\": \"content\"})\n{\"ignoredContent\": \"before return\"}\n#return($value)\n{\"ignoredContent\": \"after return\"}";
        var actual = evaluator.evaluate(template);

        var expected = "{\"real\":\"content\"}";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should disable unsupported default directives")
    public void shouldDisableUnsupportedDirectives() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "#include(\"some-path\")";

        var actual = evaluator.evaluate(template);

        var expected = "";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should not allow macros")
    public void shouldDisableMacros() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "#macro(pair $key $variable)\n" +
                " \"$key\": \"$variable\"\n" +
                "#end\n" +
                "#set($key = \"name\")\n" +
                "#set($text = \"value\")\n" +
                "{\n" +
                "  #pair($key $text)\n" +
                "}";

        var actual = evaluator.evaluate(template);

        // ignore the reference to macro defined above
        var expected = "{\n" +
                "  #pair($key $text)\n" +
                "}";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should ignore #evaluate")
    public void shouldIgnoreEvaluate() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "#set($source1 = \"abc\")\n" +
                "#set($select = \"1\")\n" +
                "#set($dynamic = \"$source$select\")\n" +
                "#evaluate($dynamic)";

        var actual = evaluator.evaluate(template);

        // don't evaluate dynamic VTL
        var expected = "#evaluate($dynamic)";

        assertEquals(expected, actual.getResult());
    }

    @Test
    @DisplayName("should return original VTL when a method returns null from invalid input")
    public void shouldPreventEvaluation() throws NoSuchFieldException, IllegalAccessException {
        var evaluator = new Evaluator(new AppSyncContext(), new AppSyncState());

        var template = "$util.transform.toDynamoDBFilterExpression({ \"valid\": { \"eq\": \"value\" } })\n" +
                "$util.transform.toDynamoDBFilterExpression({ \"invalid\": \"pair\" })";

        var actual = evaluator.evaluate(template);

        // don't evaluate dynamic VTL
        var expected = "{\"expression\":\"(#valid \\u003d :valid_eq)\",\"expressionNames\":{\"#valid\":\"valid\"},\"expressionValues\":{\":valid_eq\":{\"S\":\"value\"}}}\n" +
                "$util.transform.toDynamoDBFilterExpression({ \"invalid\": \"pair\" })";

        assertEquals(expected, actual.getResult());
    }
}
