package io.leangen.graphql;

import graphql.schema.GraphQLSchema;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.geantyref.TypeToken;
import io.leangen.graphql.annotations.GraphQLArgument;
import io.leangen.graphql.annotations.GraphQLIgnore;
import io.leangen.graphql.annotations.GraphQLNonNull;
import io.leangen.graphql.annotations.GraphQLQuery;
import io.leangen.graphql.domain.Person;
import io.leangen.graphql.execution.GlobalEnvironment;
import io.leangen.graphql.metadata.Resolver;
import io.leangen.graphql.metadata.strategy.DefaultInclusionStrategy;
import io.leangen.graphql.metadata.strategy.InclusionStrategy;
import io.leangen.graphql.metadata.strategy.query.AnnotatedResolverBuilder;
import io.leangen.graphql.metadata.strategy.query.BeanResolverBuilder;
import io.leangen.graphql.metadata.strategy.query.OperationInfoGenerator;
import io.leangen.graphql.metadata.strategy.query.OperationInfoGeneratorParams;
import io.leangen.graphql.metadata.strategy.query.PublicResolverBuilder;
import io.leangen.graphql.metadata.strategy.query.ResolverBuilder;
import io.leangen.graphql.metadata.strategy.query.ResolverBuilderParams;
import io.leangen.graphql.metadata.strategy.type.DefaultTypeTransformer;
import io.leangen.graphql.metadata.strategy.type.TypeTransformer;
import io.leangen.graphql.util.ClassUtils;
import io.leangen.graphql.util.Utils;
import lombok.Getter;
import org.junit.Test;

import java.io.Serializable;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import static io.leangen.graphql.support.GraphQLTypeAssertions.assertFieldNamesEqual;
import static io.leangen.graphql.util.GraphQLUtils.name;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class ResolverBuilderTest {

    private static final String[] BASE_PACKAGES = { "io.leangen" };
    private static final InclusionStrategy INCLUSION_STRATEGY = new DefaultInclusionStrategy(BASE_PACKAGES);
    private static final TypeTransformer TYPE_TRANSFORMER = new DefaultTypeTransformer(false, false);
    private static final GlobalEnvironment ENVIRONMENT = new TestGlobalEnvironment();

    @Test
    public void bridgeMethodTest() {
        Collection<Resolver> resolvers = new PublicResolverBuilder(BASE_PACKAGES).buildQueryResolvers(new ResolverBuilderParams(
                BaseServiceImpl::new, new TypeToken<BaseServiceImpl<Number, String>>(){}.getAnnotatedType(),
                INCLUSION_STRATEGY, TYPE_TRANSFORMER, BASE_PACKAGES, ENVIRONMENT));
        assertEquals(1, resolvers.size());
        assertEquals(resolvers.iterator().next().getReturnType().getType(), Number.class);
    }

    @Test
    public void explicitIgnoreTest() {
        for(Collection<Resolver> resolvers : resolvers(new IgnoredMethods(), new BeanResolverBuilder(BASE_PACKAGES), new AnnotatedResolverBuilder())) {
            assertEquals(1, resolvers.size());
            assertEquals("notIgnored", resolvers.iterator().next().getOperationName());
        }
    }

    @Test
    public void fieldIgnoreTest() {
        for(Collection<Resolver> resolvers : resolvers(new IgnoredFields<>(), new BeanResolverBuilder(BASE_PACKAGES), new AnnotatedResolverBuilder())) {
            assertEquals(1, resolvers.size());
            assertEquals("notIgnored", resolvers.iterator().next().getOperationName());
        }
    }

    @Test
    public void parameterIgnoreTest() {
        for(Collection<Resolver> resolvers : resolvers(new IgnoredParameters<>(), new PublicResolverBuilder(BASE_PACKAGES), new AnnotatedResolverBuilder())) {
            Resolver resolver = resolvers.iterator().next();
            assertEquals(1, resolver.getArguments().size());
            assertEquals("notIgnored", resolver.getArguments().get(0).getName());
        }
    }

    @Test
    public void impreciseBeanTypeTest() {
        GraphQLSchema schema = new TestSchemaGenerator()
                .withResolverBuilders(new PublicResolverBuilder(BASE_PACKAGES).withOperationInfoGenerator(new OperationInfoGenerator() {
                    @Override
                    public String name(OperationInfoGeneratorParams params) {
                        return params.getInstanceSupplier().get().getClass().getSimpleName() + "_" + ((Member)params.getElement().getElement()).getName();
                    }

                    @Override
                    public String description(OperationInfoGeneratorParams params) {
                        return null;
                    }

                    @Override
                    public String deprecationReason(OperationInfoGeneratorParams params) {
                        return null;
                    }
                }))
                .withOperationsFromSingleton(new One(), new TypeToken<BaseService<Person, Long>>(){}.getAnnotatedType())
                .withOperationsFromSingleton(new Two(), new TypeToken<BaseService<Number, Long>>(){}.getAnnotatedType())
                .generate();
        assertEquals(2, schema.getQueryType().getFieldDefinitions().size());
        assertEquals("One_findOne", schema.getQueryType().getFieldDefinitions().get(0).getName());
        assertEquals("Person", name(schema.getQueryType().getFieldDefinitions().get(0).getType()));
        assertEquals("Two_findOne", schema.getQueryType().getFieldDefinitions().get(1).getName());
        assertEquals("BigDecimal", name(schema.getQueryType().getFieldDefinitions().get(1).getType()));
    }

    @Test
    public void typeMergeTest() {
        ResolverBuilder[] allBuilders = new ResolverBuilder[] {
                new PublicResolverBuilder(BASE_PACKAGES), new BeanResolverBuilder(BASE_PACKAGES), new AnnotatedResolverBuilder()};
        for(Collection<Resolver> resolvers : resolvers(new MergedTypes(), allBuilders)) {
            assertEquals(2, resolvers.size());

            Optional<AnnotatedType> field1 = resolvers.stream().filter(res -> "field1".equals(res.getOperationName())).findFirst()
                    .map(Resolver::getReturnType);
            assertTrue(field1.isPresent());
            assertTrue(field1.get().isAnnotationPresent(GraphQLNonNull.class));

            Optional<AnnotatedType> field2 = resolvers.stream().filter(res -> "field2".equals(res.getOperationName())).findFirst()
                    .map(Resolver::getReturnType);
            assertTrue(field2.isPresent());
            assertTrue(field2.get().isAnnotationPresent(GraphQLNonNull.class));
        }
    }

    @Test
    public void privateFieldHierarchyTest() {
        ResolverBuilder[] allBuilders = new ResolverBuilder[] {
                new PublicResolverBuilder(BASE_PACKAGES), new BeanResolverBuilder(BASE_PACKAGES), new AnnotatedResolverBuilder()};
        for(Collection<Resolver> resolvers : resolvers(new Concrete(), allBuilders)) {
            assertEquals(3, resolvers.size());
            Stream.of("abstractField", "concreteField", "thing").forEach(field ->
                assertTrue(resolvers.stream().anyMatch(res -> res.getOperationName().equals(field)))
            );
        }
    }

    @Test
    public void typeSpecificResolverBuilderTest() {
        GraphQLSchema schema = new TestSchemaGenerator()
                .withResolverBuilders((config, current) -> current.prepend(new RelaxedBeanResolverBuilder()))
                .withOperationsFromSingletons(new StrangelyNamedProperties(), new StrangelyNamedProperties2())
                .generate();

        assertFieldNamesEqual(schema.getQueryType(), "name", "hasName", "title");
    }

    @Test
    public void basePackageTest() {
        PublicResolverBuilder resolverBuilder = new PublicResolverBuilder(BASE_PACKAGES);
        List<Resolver> resolvers = new ArrayList<>(resolverBuilder.buildQueryResolvers(new ResolverBuilderParams(
                UserHandleService::new, GenericTypeReflector.annotate(UserHandleService.class), INCLUSION_STRATEGY, TYPE_TRANSFORMER, BASE_PACKAGES, ENVIRONMENT)));
        assertEquals(2, resolvers.size());
        assertTrue(resolvers.stream().anyMatch(resolver -> resolver.getOperationName().equals("userHandle")));
        assertTrue(resolvers.stream().anyMatch(resolver -> resolver.getOperationName().equals("nickname")));
    }

    @Test
    public void badBasePackageTest() {
        PublicResolverBuilder resolverBuilder = new PublicResolverBuilder("bad.package");
        List<Resolver> resolvers = new ArrayList<>(resolverBuilder.buildQueryResolvers(new ResolverBuilderParams(
                UserHandleService::new, GenericTypeReflector.annotate(UserHandleService.class), INCLUSION_STRATEGY, TYPE_TRANSFORMER, BASE_PACKAGES, ENVIRONMENT)));
        assertEquals(1, resolvers.size());
        assertTrue(resolvers.stream().anyMatch(resolver -> resolver.getOperationName().equals("userHandle")));
    }

    private Collection<Collection<Resolver>> resolvers(Object bean, ResolverBuilder... builders) {
        Collection<Collection<Resolver>> resolvers = new ArrayList<>(builders.length);
        for (ResolverBuilder builder : builders) {
            resolvers.add(builder.buildQueryResolvers(new ResolverBuilderParams(
                    () -> bean, GenericTypeReflector.annotate(bean.getClass()), INCLUSION_STRATEGY, TYPE_TRANSFORMER, BASE_PACKAGES, ENVIRONMENT)));
        }
        return resolvers;
    }

    private interface BaseService<T, ID> {

        T findOne(@GraphQLArgument(name = "id") ID id);

    }

    private static class BaseServiceImpl<T, ID extends Serializable> implements BaseService<T, ID> {

        @Override
        public T findOne(@GraphQLArgument(name = "id") ID id) {
            return null;
        }
    }

    private static class One implements BaseService<Person, Long> {

        @Override
        @SuppressWarnings("Convert2Lambda")
        public Person findOne(Long aLong) {
            return new Person() {
                @Override
                public String getName() {
                    return "Dude Prime";
                }
            };
        }
    }

    private static class Two implements BaseService<Number, Long> {

        @Override
        public Number findOne(Long aLong) {
            return new BigInteger("2");
        }
    }

    private static class IgnoredMethods {

        @GraphQLQuery(name = "notIgnored")
        public String getNotIgnored() {
            return null;
        }

        @GraphQLIgnore
        @GraphQLQuery(name = "ignored")
        public String getIgnored() {
            return null;
        }

        @GraphQLIgnore
        @GraphQLQuery(name = "ignoredToo")
        public <T> T wouldBreak() { return null; }
    }

    private static class IgnoredFields<T> {

        @GraphQLIgnore
        @GraphQLQuery(name = "ignored")
        public String ignored;

        @GraphQLIgnore
        @GraphQLQuery(name = "ignoredToo")
        public T wouldBreak;

        @GraphQLQuery(name = "notIgnored")
        public String getNotIgnored() {
            return null;
        }
    }

    private static class IgnoredParameters<T> {

        @GraphQLQuery
        public String notIgnored(String notIgnored, @GraphQLIgnore T wouldBreak) {
            return null;
        }
    }

    @Getter
    private static class MergedTypes {
        @GraphQLQuery(name = "field1")
        private @GraphQLNonNull Object badName;
        private Object field2;
        private @GraphQLIgnore Object ignored;

        @GraphQLQuery
        public @GraphQLNonNull Object getField2() {
            return field2;
        }
    }

    private interface Interface {
        Object getThing();
    }

    @Getter
    private static abstract class Abstract implements Interface {
        @GraphQLQuery(name = "abstractField")
        private String a1;
    }

    @Getter
    private static class Concrete extends Abstract {
        @GraphQLQuery
        private Object thing;
        @GraphQLQuery(name = "concreteField")
        private String c1;
    }

    private static class RelaxedBeanResolverBuilder extends BeanResolverBuilder {

        @Override
        protected boolean isQuery(Method method, ResolverBuilderParams params) {
            return (method.getReturnType() == Boolean.TYPE && method.getName().startsWith("has")) || super.isQuery(method, params);
        }

        @Override
        public boolean supports(AnnotatedType type) {
            return ClassUtils.isSuperClass(StrangelyNamedProperties.class, type);
        }
    }

    private static class StrangelyNamedProperties {

        public String getName() {
            return "xyz";
        }

        public boolean hasName() {
            return true;
        }
    }

    private static class StrangelyNamedProperties2 {

        @GraphQLQuery
        public String getTitle() {
            return "xyz";
        }

        public boolean hasTitle() {
            return true;
        }
    }

    @SuppressWarnings("WeakerAccess")
    public static class NicknameService {

        public String getNickname(String name) {
            return Utils.isNotEmpty(name) && name.length() > 3 ? name.substring(0, 3) : name;
        }
    }

    public static class UserHandleService extends NicknameService {

        public String getUserHandle(String name) {
            return "@" + super.getNickname(name);
        }
    }
}