package io.leangen.graphql;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLSchema;
import io.leangen.graphql.annotations.GraphQLArgument;
import io.leangen.graphql.annotations.GraphQLInputField;
import io.leangen.graphql.annotations.GraphQLQuery;
import io.leangen.graphql.metadata.strategy.value.ValueMapperFactory;
import io.leangen.graphql.metadata.strategy.value.gson.GsonValueMapperFactory;
import io.leangen.graphql.metadata.strategy.value.jackson.JacksonValueMapperFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import static io.leangen.graphql.support.QueryResultAssertions.assertNoErrors;
import static io.leangen.graphql.support.QueryResultAssertions.assertValueAtPathEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

@RunWith(Parameterized.class)
public class PolymorphicJsonTest {

    @Parameterized.Parameter
    public ValueMapperFactory valueMapperFactory;

    @Parameterized.Parameters(name = "{index}: {0}")
    public static Object[] data() {
        return new Object[] { new JacksonValueMapperFactory(), new GsonValueMapperFactory() };
    }
    
    @Test
    public void testPolymorphicInput() {
        GraphQLSchema schema = new TestSchemaGenerator()
                .withValueMapperFactory(valueMapperFactory)
                .withAbstractInputTypeResolution()
                .withOperationsFromSingleton(new Operations())
                .generate();

        GraphQL exe = GraphQL.newGraphQL(schema).build();
        ExecutionResult result = exe.execute("{" +
                "test (container: {" +
                "       item: \"yay\"," +
                "       _type_: Child}) {" +
                "   item}}");
        assertNoErrors(result);
        assertValueAtPathEquals("yayChild", result, "test.item");
    }

    @Test
    public void testExplicitDeserializableType() {
        //Only test with Jackson as the feature is Jackson specific
        GraphQLSchema schema = new TestSchemaGenerator()
                .withOperationsFromSingleton(new Service())
                .generate();
        GraphQL exe = GraphQL.newGraphQL(schema).build();
        ExecutionResult result = exe.execute("{ item (in: { item: {}})}");
        assertTrue(result.getErrors().toString(), result.getErrors().isEmpty());
        assertValueAtPathEquals("Concrete", result, "item");
    }

    @Test
    public void testUnambiguousAbstractType() {
        GraphQLSchema schema = new TestSchemaGenerator()
                .withOperationsFromSingleton(new VehicleService())
                .withAbstractInputTypeResolution()
                .withValueMapperFactory(valueMapperFactory)
                .generate();
        assertNull(((GraphQLInputObjectType) schema.getType("VehicleInput")).getFieldDefinition("_type_"));
        GraphQL exe = GraphQL.newGraphQL(schema).build();
        ExecutionResult result = exe.execute("{ vehicle(in: {mode: \"flying\"}) {mode}}");
        assertTrue(result.getErrors().toString(), result.getErrors().isEmpty());
        assertValueAtPathEquals("flying", result, "vehicle.mode");
    }

    public static abstract class Vehicle {

        String mode;

        public abstract String getMode();

        public abstract void setMode(String mode);
    }

    public static class Plane extends Vehicle {

        @Override
        public String getMode() {
            return mode;
        }

        @Override
        public void setMode(String mode) {
            this.mode = mode;
        }
    }

    public static class VehicleService {

        @GraphQLQuery
        public Vehicle vehicle(Vehicle in) {
            return in;
        }
    }

    public static abstract class Parent<T> {
        String item;
        
        @GraphQLQuery(name = "item")
        @GraphQLInputField(name = "item")
        public abstract T getItem();
        public abstract void setItem(T item);
    }

    public static class Child extends Parent<String> {

        @Override
        @GraphQLQuery(name = "item")
        public String getItem() {
            return item + getClass().getSimpleName();
        }

        public void setItem(String item) {
            this.item = item;
        }
    }

    public static class ChildTwo extends Parent<String> {

        @Override
        @GraphQLQuery(name = "item")
        public String getItem() {
            return item + getClass().getSimpleName();
        }

        public void setItem(String item) {
            this.item = item;
        }
    }

    public static class Operations {

        @GraphQLQuery
        public Parent<String> test(@GraphQLArgument(name = "container") Parent<String> container) {
            return container;
        }
    }

    public static class Service {

        @GraphQLQuery
        public String item(Wrapper in) {
            return in.item.getItem();
        }
    }

    public static class Wrapper {
        @JsonDeserialize(as = Concrete.class)
        Abstract<String> item;
    }

    public interface Abstract<T> {
        T getItem();
    }

    public static class Concrete<T> implements Abstract<T> {

        @SuppressWarnings("unchecked")
        @Override
        public T getItem() {
            return (T) getClass().getSimpleName();
        }
    }
}