package io.vertx.test.codegen;

import io.vertx.codegen.type.ClassKind;
import io.vertx.codegen.ClassModel;
import io.vertx.codegen.MethodInfo;
import io.vertx.codegen.MethodKind;
import io.vertx.codegen.type.ParameterizedTypeInfo;
import io.vertx.codegen.type.TypeInfo;
import io.vertx.codegen.testmodel.TestDataObject;
import io.vertx.codegen.testmodel.TestEnum;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.test.codegen.testapi.VertxGenClass1;
import io.vertx.test.codegen.testapi.nullable.MethodWithFutureNullableArgument;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableParams;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableInheritedParams;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableReturn;
import io.vertx.test.codegen.testapi.nullable.DiamondGenericBottomFluentNullableParam;
import io.vertx.test.codegen.testapi.nullable.MethodWithCovariantNullableReturn;
import io.vertx.test.codegen.testapi.nullable.InterfaceWithInvalidListNullableParamOverride;
import io.vertx.test.codegen.testapi.nullable.InterfaceWithInvalidNullableParamOverride;
import io.vertx.test.codegen.testapi.nullable.InterfaceWithInvalidNullableReturnOverride;
import io.vertx.test.codegen.testapi.nullable.InterfaceWithNonNullableParams;
import io.vertx.test.codegen.testapi.nullable.InterfaceWithNullableReturnMethod;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableTypeArgumentHandler;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableTypeArgumentHandlerAsyncResult;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableTypeArgumentParam;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableTypeArgumentReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithListNullableParam;
import io.vertx.test.codegen.testapi.nullable.MethodWithListNullableParamOverride;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableNonAnnotatedObjectParam;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableNonAnnotatedTypeVariableHandlerAsyncResult;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableNonAnnotatedTypeVariableHandler;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableNonAnnotatedTypeVariableParam;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableNonAnnotatedTypeVariableReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableParamOverride;
import io.vertx.test.codegen.testapi.nullable.InterfaceWithNullableReturnOverride;
import io.vertx.test.codegen.testapi.nullable.MethodWithHandlerNullable;
import io.vertx.test.codegen.testapi.nullable.MethodWithHandlerNullableVoid;
import io.vertx.test.codegen.testapi.nullable.MethodWithHandlerAsyncResultNullableVoid;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidHandlerNullableAsyncResult;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableBooleanReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableByteReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableCharReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableDoubleReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableFloatReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableFluentReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableIntReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableLongReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithInvalidNullableShortReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableHandler;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableHandlerAsyncResult;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableParam;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableReturns;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableStringHandlerAsyncResult;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableTypeArgReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableTypeVariableHandlerAsyncResult;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableTypeVariableHandler;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableTypeVariableParam;
import io.vertx.test.codegen.testapi.nullable.MethodWithNullableTypeVariableReturn;
import io.vertx.test.codegen.testapi.nullable.MethodWithOverloadedNullableParam;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;

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

/**
 * @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
 */
public class ClassNullableTest extends ClassTestBase {

  // Invalid params

  @Test
  public void testMethodWithInvalidHandlerNullableAsyncResult() throws Exception {
    assertGenInvalid(MethodWithInvalidHandlerNullableAsyncResult.class);
  }

  @Test
  public void testInterfaceWithInvalidNullableParamOverride() throws Exception {
    assertGenInvalid(InterfaceWithInvalidNullableParamOverride.class, InterfaceWithNonNullableParams.class);
    assertGenInvalid(InterfaceWithInvalidNullableParamOverride.class);
  }

  @Test
  public void testInterfaceWithInvalidListNullableParamOverride() throws Exception {
    assertGenInvalid(InterfaceWithInvalidListNullableParamOverride.class, InterfaceWithNonNullableParams.class);
    assertGenInvalid(InterfaceWithInvalidListNullableParamOverride.class);
  }

  @Test
  public void testMethodWithInvalidNullableTypeArgumentParam() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableTypeArgumentParam.class);
  }

  @Test
  public void testMethodWithInvalidNullableTypeArgumentHandler() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableTypeArgumentHandler.class);
  }

  @Test
  public void testMethodWithInvalidNullableTypeArgumentHandlerAsyncResult() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableTypeArgumentHandlerAsyncResult.class);
  }

  // Invalid returns

  @Test
  public void testMethodWithInvalidNullableBooleanReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableBooleanReturn.class);
  }

  @Test
  public void testMethodWithInvalidNullableByteReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableByteReturn.class);
  }

  @Test
  public void testMethodWithInvalidNullableShortReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableShortReturn.class);
  }

  @Test
  public void testMethodWithInvalidNullableIntReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableIntReturn.class);
  }

  @Test
  public void testMethodWithInvalidNullableLongReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableLongReturn.class);
  }

  @Test
  public void testMethodWithInvalidNullableFloatReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableFloatReturn.class);
  }

  @Test
  public void testMethodWithInvalidNullableDoubleReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableDoubleReturn.class);
  }

  @Test
  public void testMethodWithInvalidNullableCharReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableCharReturn.class);
  }

  @Test
  public void testInterfaceWithInvalidNullableReturnOverride() throws Exception {
    assertGenInvalid(InterfaceWithInvalidNullableReturnOverride.class);
  }

  @Test
  public void testMethodWithInvalidNullableFluentReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableFluentReturn.class);
  }

  @Test
  public void testMethodWithInvalidNullableTypeArgumentReturn() throws Exception {
    assertGenInvalid(MethodWithInvalidNullableTypeArgumentReturn.class);
  }

  // Valid params

  @Test
  public void testMethodWithNullableParam() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.OTHER);
      assertTrue(mi1.getParams().get(0).isNullable());
    }, MethodWithNullableParam.class);
  }

  @Test
  public void testMethodWithNullableHandler() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.HANDLER);
      assertTrue(mi1.getParams().get(0).isNullable());
    }, MethodWithNullableHandler.class);
  }

  @Test
  public void testMethodWithNullableHandlerAsyncResult() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.CALLBACK);
      assertTrue(mi1.getParams().get(0).isNullable());
    }, MethodWithNullableHandlerAsyncResult.class);
  }

  @Test
  public void testInterfaceWithNullableParamOverride() throws Exception {
    Consumer<ClassModel> test = model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      assertTrue(mi1.getParams().get(0).isNullable());
    };
    // generateClass(test, MethodWithNullableParamOverride.class, MethodWithNullableParam.class);
    generateClass(test, MethodWithNullableParamOverride.class);
  }

  @Test
  public void testInterfaceWithListNullableParamOverride() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi2 = methods.get(0);
      assertTrue(((ParameterizedTypeInfo) mi2.getParams().get(0).getType()).getArg(0).isNullable());
    }, MethodWithListNullableParamOverride.class, MethodWithListNullableParam.class);
  }

  @Test
  public void testMethodWithNullableInheritedParams() throws Exception {
    Consumer<ClassModel> check = model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi2 = methods.get(0);
      assertTrue(mi2.getParams().get(0).getType().isNullable());
      assertTrue(((ParameterizedTypeInfo) mi2.getParams().get(1).getType()).getArg(0).isNullable());
      assertTrue(((ParameterizedTypeInfo) ((ParameterizedTypeInfo) mi2.getParams().get(2).getType()).getArg(0)).getArg(0).isNullable());
    };
    generateClass(check, MethodWithNullableInheritedParams.class, MethodWithNullableParams.class);
    generateClass(check, MethodWithNullableInheritedParams.class);
  }

  @Test
  public void testMethodWithCovariantNullableReturn() throws Exception {
    Consumer<ClassModel> check = model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi2 = methods.get(0);
      assertTrue(mi2.getReturnType().isNullable());
    };
    generateClass(check, MethodWithCovariantNullableReturn.class, MethodWithNullableReturn.class);
    generateClass(check, MethodWithCovariantNullableReturn.class);
  }

  @Test
  public void testMethodWithNullableTypeVariableParam() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.OTHER);
      assertTrue(mi1.getParams().get(0).isNullable());
    }, MethodWithNullableTypeVariableParam.class);
  }

  @Test
  public void testMethodWithNullableNonAnnotatedTypeVariableParam() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.OTHER);
      assertTrue(mi1.getParams().get(0).isNullable());
    }, MethodWithNullableNonAnnotatedTypeVariableParam.class);
  }

  @Test
  public void testMethodWithNullableNonAnnotatedObjectParam() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.OTHER);
      assertTrue(mi1.getParams().get(0).isNullable());
    }, MethodWithNullableNonAnnotatedObjectParam.class);
  }

  @Test
  public void testMethodWithHandlerNullable() throws Exception {
    for (Class<?> clazz : Arrays.asList(
      MethodWithHandlerNullable.class,
      MethodWithNullableTypeVariableHandler.class,
      MethodWithHandlerNullableVoid.class)) {
      generateClass(model -> {
        List<MethodInfo> methods = model.getMethods();
        assertEquals(1, methods.size());
        MethodInfo mi1 = methods.get(0);
        assertTrue(((ParameterizedTypeInfo) mi1.getParams().get(0).getType()).getArg(0).isNullable());
      }, clazz);
    }
  }

  @Test
  public void testMethodWithNullableNonAnnotatedTypeVariableHandler() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.HANDLER);
      assertFalse(mi1.getParams().get(0).isNullableCallback());
    }, MethodWithNullableNonAnnotatedTypeVariableHandler.class);
  }

  @Test
  public void testMethodWithHandlerAsyncResultNullable() throws Exception {
    for (Class<?> clazz : Arrays.asList(
      MethodWithNullableTypeVariableHandlerAsyncResult.class,
      MethodWithNullableStringHandlerAsyncResult.class,
      MethodWithHandlerAsyncResultNullableVoid.class)) {
      generateClass(model -> {
        List<MethodInfo> methods = model.getMethods();
        assertEquals(1, methods.size());
        MethodInfo mi1 = methods.get(0);
        checkMethod(mi1, "method", 1, "void", MethodKind.CALLBACK);
        assertTrue(mi1.getParams().get(0).isNullableCallback());
      }, clazz);
    }
  }

  @Test
  public void testMethodWithNullableStringHandlerAsyncResult() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.CALLBACK);
      assertTrue(mi1.getParams().get(0).isNullableCallback());
    }, MethodWithNullableStringHandlerAsyncResult.class);
  }

  @Test
  public void testMethodWithNullableNonAnnotatedTypeVariableHandlerAsyncResult() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 1, "void", MethodKind.CALLBACK);
      assertFalse(mi1.getParams().get(0).isNullableCallback());
    }, MethodWithNullableNonAnnotatedTypeVariableHandlerAsyncResult.class);
  }

  @Test
  public void testMethodWithOverloadedNullableParam() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(2, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 2, "void", MethodKind.OTHER);
      assertEquals(ClassKind.STRING, mi1.getParams().get(0).getType().getKind());
      checkParam(mi1.getParams().get(0), "s", String.class);
      assertTrue(mi1.getParams().get(0).isNullable());
      checkParam(mi1.getParams().get(1), "i", Integer.class);
      assertFalse(mi1.getParams().get(1).isNullable());
      MethodInfo mi2 = methods.get(1);
      checkMethod(mi2, "method", 2, "void", MethodKind.OTHER);
      checkParam(mi2.getParams().get(0), "i", Integer.class);
      assertFalse(mi2.getParams().get(0).isNullable());
      checkParam(mi2.getParams().get(1), "s", String.class);
      assertTrue(mi2.getParams().get(1).isNullable());
    }, MethodWithOverloadedNullableParam.class);
  }

  // Valid returns

  @Test
  public <T> void testMethodWithNullableReturns() throws Exception {
    TypeLiteral<T> typeLiteral = new TypeLiteral<T>() {};
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(50, methods.size());
      checkMethod(methods.get(0), "nullableByteReturn", 0, new TypeLiteral<Byte>() {}, MethodKind.OTHER);
      checkMethod(methods.get(1), "nullableShortReturn", 0, new TypeLiteral<Short>() {}, MethodKind.OTHER);
      checkMethod(methods.get(2), "nullableIntegerReturn", 0, new TypeLiteral<Integer>() {}, MethodKind.OTHER);
      checkMethod(methods.get(3), "nullableLongReturn", 0, new TypeLiteral<Long>() {}, MethodKind.OTHER);
      checkMethod(methods.get(4), "nullableFloatReturn", 0, new TypeLiteral<Float>() {}, MethodKind.OTHER);
      checkMethod(methods.get(5), "nullableDoubleReturn", 0, new TypeLiteral<Double>() {}, MethodKind.OTHER);
      checkMethod(methods.get(6), "nullableCharacterReturn", 0, new TypeLiteral<Character>() {}, MethodKind.OTHER);
      checkMethod(methods.get(7), "nullableStringReturn", 0, new TypeLiteral<String>() {}, MethodKind.OTHER);
      checkMethod(methods.get(8), "nullableJsonObjectReturn", 0, new TypeLiteral<JsonObject>() {}, MethodKind.OTHER);
      checkMethod(methods.get(9), "nullableJsonArrayReturn", 0, new TypeLiteral<JsonArray>() {}, MethodKind.OTHER);
      checkMethod(methods.get(10), "nullableTypeVariableReturn", 0, typeLiteral, MethodKind.OTHER);
      checkMethod(methods.get(11), "nullableEnumReturn", 0, new TypeLiteral<TestEnum>() {}, MethodKind.OTHER);
      checkMethod(methods.get(12), "nullableDataObjectReturn", 0, new TypeLiteral<TestDataObject>() {}, MethodKind.OTHER);
      checkMethod(methods.get(13), "nullableApiReturn", 0, new TypeLiteral<VertxGenClass1>() {}, MethodKind.OTHER);
      checkMethod(methods.get(14), "nullableListByteReturn", 0, new TypeLiteral<List<Byte>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(15), "nullableListShortReturn", 0, new TypeLiteral<List<Short>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(16), "nullableListIntegerReturn", 0, new TypeLiteral<List<Integer>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(17), "nullableListLongReturn", 0, new TypeLiteral<List<Long>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(18), "nullableListFloatReturn", 0, new TypeLiteral<List<Float>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(19), "nullableListDoubleReturn", 0, new TypeLiteral<List<Double>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(20), "nullableListCharacterReturn", 0, new TypeLiteral<List<Character>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(21), "nullableListStringReturn", 0, new TypeLiteral<List<String>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(22), "nullableListJsonObjectReturn", 0, new TypeLiteral<List<JsonObject>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(23), "nullableListJsonArrayReturn", 0, new TypeLiteral<List<JsonArray>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(24), "nullableListEnumReturn", 0, new TypeLiteral<List<TestEnum>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(25), "nullableListDataObjectReturn", 0, new TypeLiteral<List<TestDataObject>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(26), "nullableListApiReturn", 0, new TypeLiteral<List<VertxGenClass1>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(27), "nullableSetByteReturn", 0, new TypeLiteral<Set<Byte>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(28), "nullableSetShortReturn", 0, new TypeLiteral<Set<Short>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(29), "nullableSetIntegerReturn", 0, new TypeLiteral<Set<Integer>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(30), "nullableSetLongReturn", 0, new TypeLiteral<Set<Long>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(31), "nullableSetFloatReturn", 0, new TypeLiteral<Set<Float>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(32), "nullableSetDoubleReturn", 0, new TypeLiteral<Set<Double>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(33), "nullableSetCharacterReturn", 0, new TypeLiteral<Set<Character>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(34), "nullableSetStringReturn", 0, new TypeLiteral<Set<String>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(35), "nullableSetJsonObjectReturn", 0, new TypeLiteral<Set<JsonObject>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(36), "nullableSetJsonArrayReturn", 0, new TypeLiteral<Set<JsonArray>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(37), "nullableSetEnumReturn", 0, new TypeLiteral<Set<TestEnum>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(38), "nullableSetDataObjectReturn", 0, new TypeLiteral<Set<TestDataObject>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(39), "nullableSetApiReturn", 0, new TypeLiteral<Set<VertxGenClass1>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(40), "nullableMapByteReturn", 0, new TypeLiteral<Map<String, Byte>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(41), "nullableMapShortReturn", 0, new TypeLiteral<Map<String, Short>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(42), "nullableMapIntegerReturn", 0, new TypeLiteral<Map<String, Integer>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(43), "nullableMapLongReturn", 0, new TypeLiteral<Map<String, Long>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(44), "nullableMapFloatReturn", 0, new TypeLiteral<Map<String, Float>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(45), "nullableMapDoubleReturn", 0, new TypeLiteral<Map<String, Double>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(46), "nullableMapCharacterReturn", 0, new TypeLiteral<Map<String, Character>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(47), "nullableMapStringReturn", 0, new TypeLiteral<Map<String, String>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(48), "nullableMapJsonObjectReturn", 0, new TypeLiteral<Map<String, JsonObject>>() {}, MethodKind.OTHER);
      checkMethod(methods.get(49), "nullableMapJsonArrayReturn", 0, new TypeLiteral<Map<String, JsonArray>>() {}, MethodKind.OTHER);
      methods.forEach(m -> {
        assertTrue("Expects " + m.getName() + " to have nullable return type", m.isNullableReturn());
      });
    }, MethodWithNullableReturns.class);
  }

  @Test
  public void testMethodWithNullableTypeArgReturn() throws Exception {
    ClassModel model = new GeneratorHelper().generateClass(MethodWithNullableTypeArgReturn.class);
    List<MethodInfo> methods = model.getMethods();
    assertEquals(36, methods.size());
    checkMethod(methods.get(0), "listNullableByteReturn", 0, new TypeLiteral<List<Byte>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(1), "listNullableShortReturn", 0, new TypeLiteral<List<Short>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(2), "listNullableIntegerReturn", 0, new TypeLiteral<List<Integer>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(3), "listNullableLongReturn", 0, new TypeLiteral<List<Long>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(4), "listNullableFloatReturn", 0, new TypeLiteral<List<Float>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(5), "listNullableDoubleReturn", 0, new TypeLiteral<List<Double>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(6), "listNullableCharacterReturn", 0, new TypeLiteral<List<Character>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(7), "listNullableStringReturn", 0, new TypeLiteral<List<String>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(8), "listNullableJsonObjectReturn", 0, new TypeLiteral<List<JsonObject>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(9), "listNullableJsonArrayReturn", 0, new TypeLiteral<List<JsonArray>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(10), "listNullableEnumReturn", 0, new TypeLiteral<List<TestEnum>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(11), "listNullableDataObjectReturn", 0, new TypeLiteral<List<TestDataObject>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(12), "listNullableApiReturn", 0, new TypeLiteral<List<VertxGenClass1>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(13), "setNullableByteReturn", 0, new TypeLiteral<Set<Byte>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(14), "setNullableShortReturn", 0, new TypeLiteral<Set<Short>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(15), "setNullableIntegerReturn", 0, new TypeLiteral<Set<Integer>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(16), "setNullableLongReturn", 0, new TypeLiteral<Set<Long>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(17), "setNullableFloatReturn", 0, new TypeLiteral<Set<Float>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(18), "setNullableDoubleReturn", 0, new TypeLiteral<Set<Double>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(19), "setNullableCharacterReturn", 0, new TypeLiteral<Set<Character>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(20), "setNullableStringReturn", 0, new TypeLiteral<Set<String>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(21), "setNullableJsonObjectReturn", 0, new TypeLiteral<Set<JsonObject>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(22), "setNullableJsonArrayReturn", 0, new TypeLiteral<Set<JsonArray>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(23), "setNullableEnumReturn", 0, new TypeLiteral<Set<TestEnum>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(24), "setNullableDataObjectReturn", 0, new TypeLiteral<Set<TestDataObject>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(25), "setNullableApiReturn", 0, new TypeLiteral<Set<VertxGenClass1>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(26), "mapNullableByteReturn", 0, new TypeLiteral<Map<String, Byte>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(27), "mapNullableShortReturn", 0, new TypeLiteral<Map<String, Short>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(28), "mapNullableIntegerReturn", 0, new TypeLiteral<Map<String, Integer>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(29), "mapNullableLongReturn", 0, new TypeLiteral<Map<String, Long>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(30), "mapNullableFloatReturn", 0, new TypeLiteral<Map<String, Float>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(31), "mapNullableDoubleReturn", 0, new TypeLiteral<Map<String, Double>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(32), "mapNullableCharacterReturn", 0, new TypeLiteral<Map<String, Character>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(33), "mapNullableStringReturn", 0, new TypeLiteral<Map<String, String>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(34), "mapNullableJsonObjectReturn", 0, new TypeLiteral<Map<String, JsonObject>>() {}, MethodKind.OTHER);
    checkMethod(methods.get(35), "mapNullableJsonArrayReturn", 0, new TypeLiteral<Map<String, JsonArray>>() {}, MethodKind.OTHER);

    methods.forEach(m -> {
      assertFalse("Expects " + m.getName() + " to have nullable return type", m.isNullableReturn());
      List<TypeInfo> args = ((ParameterizedTypeInfo) m.getReturnType()).getArgs();
      assertTrue("Expects " + m.getName() + " to have nullable return type", args.get(args.size() - 1).isNullable());
    });
  }

  @Test
  public void testInterfaceWithNullableReturnOverride() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi3 = methods.get(0);
      assertTrue(mi3.getReturnType().isNullable());
    }, InterfaceWithNullableReturnOverride.class, InterfaceWithNullableReturnMethod.class);
  }

  @Test
  public void testDiamondFluentNullableReturn() throws Exception {
    ClassModel model = new GeneratorHelper().generateClass(DiamondGenericBottomFluentNullableParam.class);
  }

  @Test
  public void testMethodWithNullableTypeVariableReturn() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 0, "T", MethodKind.OTHER);
      assertTrue(mi1.isNullableReturn());
    }, MethodWithNullableTypeVariableReturn.class);
  }

  @Test
  public void testMethodWithNullableNonAnnotatedTypeVariableReturn() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(1, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "method", 0, "T", MethodKind.OTHER);
      assertFalse(mi1.isNullableReturn());
    }, MethodWithNullableNonAnnotatedTypeVariableReturn.class);
  }

  @Test
  public void testMethodWithFutureNullableArgument() throws Exception {
    generateClass(model -> {
      List<MethodInfo> methods = model.getMethods();
      assertEquals(3, methods.size());
      MethodInfo mi1 = methods.get(0);
      checkMethod(mi1, "m1", 0, "io.vertx.core.Future<java.lang.String>", MethodKind.OTHER);
      assertTrue(((ParameterizedTypeInfo)mi1.getReturnType()).getArg(0).isNullable());
      MethodInfo mi2 = methods.get(1);
      checkMethod(mi2, "m2", 1, "void", MethodKind.OTHER);
      assertTrue(((ParameterizedTypeInfo)mi2.getParam(0).getType()).getArg(0).isNullable());
      MethodInfo mi3 = methods.get(2);
      checkMethod(mi3, "m3", 1, "void", MethodKind.OTHER);
      ParameterizedTypeInfo functionType = (ParameterizedTypeInfo) mi3.getParams().get(0).getType();
      assertTrue(((ParameterizedTypeInfo)functionType.getArg(0)).getArg(0).isNullable());
      assertTrue(((ParameterizedTypeInfo)functionType.getArg(1)).getArg(0).isNullable());
    }, MethodWithFutureNullableArgument.class);
  }

  private void generateClass(Consumer<ClassModel> test, Class<?> clazz, Class<?>... rest) throws Exception {
    blacklist(() -> {
      try {
        test.accept(new GeneratorHelper().generateClass(clazz));
      } catch (Exception e) {
        throw new AssertionError(e);
      }
    }, Stream.of(clazz));
    blacklist(() -> {
      try {
        test.accept(new GeneratorHelper().generateClass(clazz, rest));
      } catch (Exception e) {
        throw new AssertionError(e);
      }
    }, Stream.concat(Stream.of(clazz), Stream.of(rest)));
  }

  @Override
  void assertGenInvalid(Class<?> c, Class<?>... rest) throws Exception {
    blacklist(() -> {
      try {
        ClassNullableTest.super.assertGenInvalid(c, rest);
      } catch (Exception e) {
        throw new AssertionError(e);
      }
    }, Stream.concat(Stream.of(c), Stream.of(rest)));
  }
}
