package com.sebastian_daschner.jaxrs_analyzer.backend;

import com.sebastian_daschner.jaxrs_analyzer.model.Types;
import com.sebastian_daschner.jaxrs_analyzer.model.rest.TypeIdentifier;
import com.sebastian_daschner.jaxrs_analyzer.model.rest.TypeRepresentation;
import com.sebastian_daschner.jaxrs_analyzer.model.rest.TypeRepresentationVisitor;
import org.junit.Before;
import org.junit.Test;

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

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

public class JsonRepresentationAppenderTest {

    private static final TypeIdentifier STRING_LIST_IDENTIFIER = TypeIdentifier.ofType("java.util.List<java.lang.String>");
    private static final TypeIdentifier STRING_IDENTIFIER = TypeIdentifier.ofType(Types.STRING);
    private static final TypeIdentifier INT_IDENTIFIER = TypeIdentifier.ofType(Types.PRIMITIVE_INT);

    private TypeRepresentationVisitor cut;
    private Map<TypeIdentifier, TypeRepresentation> representations;
    private StringBuilder builder;

    @Before
    public void setup() {
        representations = new HashMap<>();
        builder = new StringBuilder();
        cut = new JsonRepresentationAppender(builder, representations);
    }

    @Test
    public void testVisitPrimitive() {
        TypeRepresentation.ofConcrete(TypeIdentifier.ofType(Types.STRING)).accept(cut);
        assertThat(builder.toString(), is("\"string\""));
    }

    @Test
    public void testMissingRepresentation() {
        TypeRepresentation.ofCollection(STRING_LIST_IDENTIFIER, TypeRepresentation.ofConcrete(STRING_IDENTIFIER)).accept(cut);
        assertThat(builder.toString(), is("[\"string\"]"));
    }

    @Test
    public void testEnum() {
        TypeRepresentation.ofEnum(STRING_IDENTIFIER, "Foo", "bar").accept(cut);
        assertThat(builder.toString(), is("\"Foo|bar\""));
    }

    @Test
    public void testEnumEmpty() {
        TypeRepresentation.ofEnum(STRING_IDENTIFIER).accept(cut);
        assertThat(builder.toString(), is("\"string\""));
    }

    @Test
    public void testVisitSimpleList() {
        final TypeRepresentation stringRepresentation = TypeRepresentation.ofConcrete(STRING_IDENTIFIER);
        final TypeRepresentation listRepresentation = TypeRepresentation.ofCollection(STRING_LIST_IDENTIFIER, stringRepresentation);
        representations.put(STRING_LIST_IDENTIFIER, listRepresentation);

        listRepresentation.accept(cut);
        assertThat(builder.toString(), is("[\"string\"]"));

        clear(builder);
        listRepresentation.accept(cut);
        assertThat(builder.toString(), is("[\"string\"]"));

        clear(builder);
        listRepresentation.accept(cut);
        assertThat(builder.toString(), is("[\"string\"]"));

        clear(builder);
        stringRepresentation.accept(cut);
        assertThat(builder.toString(), is("\"string\""));
    }

    @Test
    public void testVisitMultipleList() {
        final TypeIdentifier identifier = TypeIdentifier.ofType("java.util.List<java.util.List<java.lang.String>>");
        final TypeRepresentation representation = TypeRepresentation.ofCollection(identifier, TypeRepresentation.ofCollection(STRING_LIST_IDENTIFIER,
                TypeRepresentation.ofConcrete(STRING_IDENTIFIER)));
        representations.put(identifier, representation);

        representation.accept(cut);
        assertThat(builder.toString(), is("[[\"string\"]]"));

        clear(builder);
        representation.accept(cut);
        assertThat(builder.toString(), is("[[\"string\"]]"));

        clear(builder);
        representation.accept(cut);
        assertThat(builder.toString(), is("[[\"string\"]]"));
    }

    @Test
    public void testVisitDynamic() {
        TypeRepresentation.ofConcrete(TypeIdentifier.ofDynamic()).accept(cut);
        assertThat(builder.toString(), is("{}"));

        clear(builder);
        TypeRepresentation.ofCollection(TypeIdentifier.ofDynamic(), TypeRepresentation.ofConcrete(TypeIdentifier.ofDynamic())).accept(cut);
        assertThat(builder.toString(), is("[{}]"));
    }

    @Test
    public void testVisitConcrete() {
        final TypeIdentifier identifier = TypeIdentifier.ofType("com.sebastian_daschner.test.Model");
        final TypeIdentifier enumIdentifier = TypeIdentifier.ofType("com.sebastian_daschner.test.TestEnum");
        Map<String, TypeIdentifier> properties = new HashMap<>();
        properties.put("world", INT_IDENTIFIER);
        properties.put("hello", STRING_IDENTIFIER);
        properties.put("abc", STRING_IDENTIFIER);
        properties.put("enumeration", enumIdentifier);
        final TypeRepresentation representation = TypeRepresentation.ofConcrete(identifier, properties);
        final TypeRepresentation enumRepresentation = TypeRepresentation.ofEnum(identifier, "FOO", "BAR", "BAZ");

        representations.put(identifier, representation);
        representations.put(enumIdentifier, enumRepresentation);
        representation.accept(cut);
        assertThat(builder.toString(), is("{\"abc\":\"string\",\"enumeration\":\"BAR|BAZ|FOO\",\"hello\":\"string\",\"world\":0}"));
    }

    @Test
    public void testVisitConcreteWithNested() {
        final TypeIdentifier identifier = TypeIdentifier.ofType("com.sebastian_daschner.test.Model");
        Map<String, TypeIdentifier> properties = new HashMap<>();
        properties.put("world", INT_IDENTIFIER);
        properties.put("hello", STRING_IDENTIFIER);
        properties.put("abc", STRING_IDENTIFIER);
        properties.put("model", identifier);
        final TypeRepresentation representation = TypeRepresentation.ofConcrete(identifier, properties);

        representations.put(identifier, representation);
        representation.accept(cut);
        assertThat(builder.toString(), is("{\"abc\":\"string\",\"hello\":\"string\",\"model\":{},\"world\":0}"));
    }

    @Test
    public void testVisitConcreteWithNestedPreventSuppression() {
        final TypeIdentifier dateIdentifier = TypeIdentifier.ofType(Types.DATE);
        final TypeIdentifier modelIdentifier = TypeIdentifier.ofType("com.sebastian_daschner.test.Model");
        final TypeIdentifier objectIdentifier = TypeIdentifier.ofType(Types.OBJECT);

        Map<String, TypeIdentifier> properties = new HashMap<>();
        properties.put("world", INT_IDENTIFIER);
        properties.put("abc", STRING_IDENTIFIER);
        properties.put("model", modelIdentifier);
        final TypeRepresentation dateRepresentation = TypeRepresentation.ofConcrete(dateIdentifier, properties);
        representations.put(dateIdentifier, dateRepresentation);

        properties = new HashMap<>();
        properties.put("world", INT_IDENTIFIER);
        properties.put("hello", STRING_IDENTIFIER);
        properties.put("abc", STRING_IDENTIFIER);
        properties.put("model", modelIdentifier);
        final TypeRepresentation objectRepresentation = TypeRepresentation.ofConcrete(objectIdentifier, properties);
        representations.put(objectIdentifier, objectRepresentation);

        properties = new HashMap<>();
        properties.put("world", INT_IDENTIFIER);
        properties.put("hello", STRING_IDENTIFIER);
        properties.put("abc", STRING_IDENTIFIER);
        properties.put("date", dateIdentifier);
        properties.put("object", objectIdentifier);
        final TypeRepresentation modelRepresentation = TypeRepresentation.ofConcrete(modelIdentifier, properties);
        representations.put(modelIdentifier, modelRepresentation);

        // date
        // {\"abc\":\"string\",\"model\":{},\"world\":0}
        // object
        // {\"abc\":\"string\",\"hello\":\"string\",\"model\":{},\"world\":0}
        // model
        // {\"abc\":\"string\",\"date\":{},\"hello\":\"string\",\"object\":{},\"world\":0}

        dateRepresentation.accept(cut);
        assertThat(builder.toString(), is("{\"abc\":\"string\",\"model\":{\"abc\":\"string\",\"date\":{},\"hello\":\"string\",\"object\":{\"abc\":\"string\",\"hello\":\"string\",\"model\":{},\"world\":0},\"world\":0},\"world\":0}"));

        clear(builder);
        objectRepresentation.accept(cut);
        assertThat(builder.toString(), is("{\"abc\":\"string\",\"hello\":\"string\",\"model\":{\"abc\":\"string\",\"date\":{\"abc\":\"string\",\"model\":{},\"world\":0},\"hello\":\"string\",\"object\":{},\"world\":0},\"world\":0}"));

        clear(builder);
        modelRepresentation.accept(cut);
        assertThat(builder.toString(), is("{\"abc\":\"string\",\"date\":{\"abc\":\"string\",\"model\":{},\"world\":0},\"hello\":\"string\",\"object\":{\"abc\":\"string\",\"hello\":\"string\",\"model\":{},\"world\":0},\"world\":0}"));

        clear(builder);
        dateRepresentation.accept(cut);
        assertThat(builder.toString(), is("{\"abc\":\"string\",\"model\":{\"abc\":\"string\",\"date\":{},\"hello\":\"string\",\"object\":{\"abc\":\"string\",\"hello\":\"string\",\"model\":{},\"world\":0},\"world\":0},\"world\":0}"));

        clear(builder);
        objectRepresentation.accept(cut);
        assertThat(builder.toString(), is("{\"abc\":\"string\",\"hello\":\"string\",\"model\":{\"abc\":\"string\",\"date\":{\"abc\":\"string\",\"model\":{},\"world\":0},\"hello\":\"string\",\"object\":{},\"world\":0},\"world\":0}"));

        clear(builder);
        modelRepresentation.accept(cut);
        assertThat(builder.toString(), is("{\"abc\":\"string\",\"date\":{\"abc\":\"string\",\"model\":{},\"world\":0},\"hello\":\"string\",\"object\":{\"abc\":\"string\",\"hello\":\"string\",\"model\":{},\"world\":0},\"world\":0}"));
    }

    @Test
    public void testVisitRecursiveType() {
        final TypeIdentifier modelIdentifier = TypeIdentifier.ofType("com.sebastian_daschner.test.Model");

        final Map<String, TypeIdentifier> properties = new HashMap<>();
        properties.put("world", INT_IDENTIFIER);
        properties.put("hello", STRING_IDENTIFIER);
        properties.put("abc", STRING_IDENTIFIER);
        properties.put("model", modelIdentifier);
        final TypeRepresentation modelRepresentation = TypeRepresentation.ofConcrete(modelIdentifier, properties);
        representations.put(modelIdentifier, modelRepresentation);

        modelRepresentation.accept(cut);
        assertThat(builder.toString(), is("{" +
                "\"abc\":\"string\"," +
                "\"hello\":\"string\"," +
                "\"model\":{}," +
                "\"world\":0" +
                "}"));
    }

    @Test
    public void testVisitCyclicRecursiveType() {
        final TypeIdentifier firstModelIdentifier = TypeIdentifier.ofType("com.sebastian_daschner.test.first.Model");
        final TypeIdentifier secondModelIdentifier = TypeIdentifier.ofType("com.sebastian_daschner.test.second.Model");

        Map<String, TypeIdentifier> properties = new HashMap<>();
        properties.put("world", INT_IDENTIFIER);
        properties.put("model", secondModelIdentifier);
        final TypeRepresentation firstModelRepresentation = TypeRepresentation.ofConcrete(firstModelIdentifier, properties);
        representations.put(firstModelIdentifier, firstModelRepresentation);

        properties = new HashMap<>();
        properties.put("hello", STRING_IDENTIFIER);
        properties.put("model", firstModelIdentifier);
        final TypeRepresentation secondModelRepresentation = TypeRepresentation.ofConcrete(secondModelIdentifier, properties);
        representations.put(secondModelIdentifier, secondModelRepresentation);

        firstModelRepresentation.accept(cut);
        assertThat(builder.toString(), is("{" +
                "\"model\":{\"hello\":\"string\",\"model\":{}}," +
                "\"world\":0" +
                "}"));

        clear(builder);
        secondModelRepresentation.accept(cut);
        assertThat(builder.toString(), is("{" +
                "\"hello\":\"string\"," +
                "\"model\":{\"model\":{},\"world\":0}" +
                "}"));
    }

    private static void clear(final StringBuilder builder) {
        builder.delete(0, builder.length());
    }

}