/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.api.server.spi.tools;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.api.server.spi.ObjectMapperUtil;
import com.google.api.server.spi.ServiceContext;
import com.google.api.server.spi.config.AnnotationBoolean;
import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiCacheControl;
import com.google.api.server.spi.config.ApiClass;
import com.google.api.server.spi.config.ApiConfigException;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiMethod.HttpMethod;
import com.google.api.server.spi.config.ApiNamespace;
import com.google.api.server.spi.config.ApiResourceProperty;
import com.google.api.server.spi.config.ApiTransformer;
import com.google.api.server.spi.config.AuthLevel;
import com.google.api.server.spi.config.DefaultValue;
import com.google.api.server.spi.config.ResourcePropertySchema;
import com.google.api.server.spi.config.ResourceSchema;
import com.google.api.server.spi.config.ResourceTransformer;
import com.google.api.server.spi.config.jsonwriter.JsonConfigWriter;
import com.google.api.server.spi.config.model.ApiMethodConfig;
import com.google.api.server.spi.config.validation.CollectionResourceException;
import com.google.api.server.spi.config.validation.DuplicateParameterNameException;
import com.google.api.server.spi.config.validation.DuplicateRestPathException;
import com.google.api.server.spi.config.validation.GenericTypeException;
import com.google.api.server.spi.config.validation.InconsistentApiConfigurationException;
import com.google.api.server.spi.config.validation.InvalidNamespaceException;
import com.google.api.server.spi.config.validation.InvalidParameterAnnotationsException;
import com.google.api.server.spi.config.validation.InvalidReturnTypeException;
import com.google.api.server.spi.config.validation.MissingParameterNameException;
import com.google.api.server.spi.config.validation.NestedCollectionException;
import com.google.api.server.spi.config.validation.OverloadedMethodException;
import com.google.api.server.spi.response.CollectionResponse;
import com.google.api.server.spi.testing.ArrayEndpoint;
import com.google.api.server.spi.testing.Bar;
import com.google.api.server.spi.testing.Baz;
import com.google.api.server.spi.testing.BoundedGenericEndpoint;
import com.google.api.server.spi.testing.BridgeInheritanceEndpoint;
import com.google.api.server.spi.testing.CollectionContravarianceEndpoint;
import com.google.api.server.spi.testing.CollectionCovarianceEndpoint;
import com.google.api.server.spi.testing.DeepGenericHierarchyFailEndpoint;
import com.google.api.server.spi.testing.DeepGenericHierarchySuccessEndpoint;
import com.google.api.server.spi.testing.DefaultValueSerializer;
import com.google.api.server.spi.testing.DuplicateMethodEndpoint;
import com.google.api.server.spi.testing.Endpoint0;
import com.google.api.server.spi.testing.Endpoint1;
import com.google.api.server.spi.testing.Endpoint2;
import com.google.api.server.spi.testing.Endpoint3;
import com.google.api.server.spi.testing.Endpoint4;
import com.google.api.server.spi.testing.Endpoint5;
import com.google.api.server.spi.testing.ParentChildEndpoint;
import com.google.api.server.spi.testing.PrimitiveEndpoint;
import com.google.api.server.spi.testing.RecursiveEndpoint;
import com.google.api.server.spi.testing.RestfulResourceEndpointBase;
import com.google.api.server.spi.testing.SimpleContravarianceEndpoint;
import com.google.api.server.spi.testing.SimpleCovarianceEndpoint;
import com.google.api.server.spi.testing.SimpleLevelOverridingApi;
import com.google.api.server.spi.testing.SimpleOverloadEndpoint;
import com.google.api.server.spi.testing.SimpleOverrideEndpoint;
import com.google.api.server.spi.testing.SubclassedEndpoint;
import com.google.api.server.spi.testing.SubclassedOverridingEndpoint;
import com.google.appengine.api.users.User;
import com.google.common.reflect.TypeToken;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;
import javax.inject.Named;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

/**
 * Tests for {@link AnnotationApiConfigGenerator}.
 */
@RunWith(JUnit4.class)
public class AnnotationApiConfigGeneratorTest {

  private final ObjectMapper objectMapper = ObjectMapperUtil.createStandardObjectMapper();

  protected ApiConfigGenerator g;

  @Before
  public void setUp() throws Exception {
    g = createApiConfigGenerator();
  }

  protected ApiConfigGenerator createApiConfigGenerator() throws Exception {
    return new AnnotationApiConfigGenerator();
  }

  @Test
  public void testEndpointWithOnlyDefaultConfiguration() throws Exception {
    String apiConfigSource = g.generateConfig(Endpoint0.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);

    verifyApi(root, "thirdParty.api", "https://myapp.appspot.com/_ah/api",
        "myapi", "v1", "", "https://myapp.appspot.com/_ah/spi", false, true);

    JsonNode methodGetFoo = root.path("methods").path("myapi.endpoint0.getFoo");
    verifyMethod(methodGetFoo, "foo/{id}", HttpMethod.GET, Endpoint0.class.getName() + ".getFoo",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodGetFoo.path("request"), "empty", 1);
    verifyMethodRequestParameter(methodGetFoo.path("request"), "id", "string", true, false);

    verifyEndpoint0Schema(root, Endpoint0.class.getName());
  }

  @SuppressWarnings("unused")
  @Api
  private static class MultipleEndpoint1 {
    @ApiMethod(name = "add", path = "item", httpMethod = HttpMethod.POST)
    public void add(@Named("id") String id) {}
  }

  @SuppressWarnings("unused")
  @Api
  private static class MultipleEndpoint2 {
    @ApiMethod(name = "delete", path = "item/{id}", httpMethod = HttpMethod.DELETE)
    public void delete(@Named("id") String id) {}
  }

  @Test
  public void testMultipleServiceClasses() throws Exception {
    String apiConfigSource =
        g.generateConfig(MultipleEndpoint1.class, MultipleEndpoint2.class).get("myapi-v1.api");
    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);

    assertEquals("item", root.path("methods").path("myapi.add").path("path").asText());
    assertEquals("item/{id}", root.path("methods").path("myapi.delete").path("path").asText());
  }

  @Test
  public void testFullyConfiguredEndpoint() throws Exception {
    String apiConfigSource = g.generateConfig(Endpoint1.class).get("myapi-v1.api");
    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifyEndpoint1(Endpoint1.class, root);
    verifyEndpoint1Schema(Endpoint1.class, root);
  }

  @Test
  public void testEmptyRequestBodies() throws Exception {
    String apiConfigSource = g.generateConfig(Endpoint1.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");

    // myapi.foos.get
    JsonNode methodGetFoo = methods.path("myapi.foos.get");
    assertTrue(methodGetFoo.path("request").path("body").asText().equals("empty"));
    assertFalse(root.path("descriptor").path("methods")
        .path(methodGetFoo.path("rosyMethod").asText()).has("request"));

    // myapi.foos.insert
    JsonNode methodInsertFoo = methods.path("myapi.foos.insert");
    assertFalse(methodInsertFoo.path("request").path("body").asText().equals("empty"));
    assertTrue(root.path("descriptor").path("methods")
        .path(methodInsertFoo.path("rosyMethod").asText()).has("request"));

    // myapi.foos.execute0
    JsonNode methodExecute0 = methods.path("myapi.foos.execute0");
    assertTrue(methodExecute0.path("request").path("body").asText().equals("empty"));
    assertFalse(root.path("descriptor").path("methods")
        .path(methodExecute0.path("rosyMethod").asText()).has("request"));
  }

  @Test
  public void testEmptyParameterBodies() throws Exception {
    String apiConfigSource = g.generateConfig(Endpoint1.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");

    // foos.list
    JsonNode methodListFoo = methods.path("myapi.foos.list");
    assertFalse(methodListFoo.path("request").has("parameters"));

    // foos.get
    JsonNode methodGetFoo = methods.path("myapi.foos.get");
    assertTrue(methodGetFoo.path("request").has("parameters"));
    assertFalse(0 == methodGetFoo.path("request").path("parameters").size());

    //foos.insert
    JsonNode methodInsertFoo = methods.path("myapi.foos.insert");
    assertFalse(methodInsertFoo.path("request").has("parameters"));
  }

  @Test
  public void testEndpointWithNoPublicMethods() throws Exception {
    String apiConfigSource = g.generateConfig(Endpoint2.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);

    JsonNode methods = root.path("methods");
    assertNull(methods.findValue("api2.foos.invisible0"));
    assertNull(methods.findValue("api2.foos.invisible1"));
  }

  @Test
  public void testEndpointWithInheritance() throws Exception {
    String apiConfigSource = g.generateConfig(Endpoint3.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifyEndpoint1(Endpoint3.class, root);
    verifyEndpoint3(root);
    verifyEndpoint1Schema(Endpoint3.class, root);
    verifyEndpoint3Schema(root);
  }

  @Test
  public void testEndpointWithBridgeMethods() throws Exception {
    String apiConfigSource = g.generateConfig(BridgeInheritanceEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");

    assertEquals(2, methods.size());
    assertNull(methods.findValue("myapi.fn1"));
    assertNotNull(methods.findValue("myapi.api6.foos.fn1"));
    assertNotNull(methods.findValue("myapi.api6.foos.fn2"));
  }

  @Test
  public void testDuplicateMethodEndpoint() throws Exception {
    try {
      g.generateConfig(DuplicateMethodEndpoint.class);
      fail("Config generation for endpoint with overloaded method should have failed.");
    } catch (OverloadedMethodException expected) {
      // expected
    }
  }

  @Test
  public void testSimpleOverrideEndpoint() throws Exception {
    String apiConfigSource = g.generateConfig(SimpleOverrideEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");
    assertEquals(1, methods.size());
  }

  @Test
  public void testSimpleOverloadEndpoint() throws Exception {
    try {
      g.generateConfig(SimpleOverloadEndpoint.class);
      fail("Config generation for endpoint with overloaded inherited method should have failed");
    } catch (OverloadedMethodException expected) {
      // expected
    }
  }

  @Test
  public void testSimpleCovarianceEndpoint() throws Exception {
    try {
      g.generateConfig(SimpleCovarianceEndpoint.class);
      fail("Config generation for endpoint with covariant inherited method should have failed");
    } catch (OverloadedMethodException expected) {
      // expected
    }
  }

  @Test
  public void testSimpleContravarianceEndpoint() throws Exception {
    try {
      g.generateConfig(SimpleContravarianceEndpoint.class);
      fail("Config generation for endpoint with contravariant inherited method should have failed");
    } catch (OverloadedMethodException expected) {
      // expected
    }
  }

  @Test
  public void testCollectionCovarianceEndpoint() throws Exception {
    try {
      g.generateConfig(CollectionCovarianceEndpoint.class);
      fail("Config generation for endpoint with covariant inherited method should have failed");
    } catch (OverloadedMethodException expected) {
      // expected
    }
  }

  @Test
  public void testCollectionContravarianceEndpoint() throws Exception {
    try {
      g.generateConfig(CollectionContravarianceEndpoint.class);
      fail("Config generation for endpoint with contravariant inherited method should have failed");
    } catch (OverloadedMethodException expected) {
      // expected
    }
  }

  @Test
  public void testFullySpecializedDateEndpoint() throws Exception {
    String apiConfigSource = g.generateConfig(
        RestfulResourceEndpointBase.FullySpecializedEndpoint.class).get("fullApi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");
    assertEquals(6, methods.size());
  }

  @Test
  public void testGenericBasePartiallySpecializedEndpoint() throws Exception {
    String apiConfigSource = g.generateConfig(
        RestfulResourceEndpointBase.PartiallySpecializedEndpoint.class).get("partialApi-v1.api");
    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");
    assertEquals(6, methods.size());

    JsonNode rootPartial = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methodsPartial = rootPartial.path("descriptor").path("methods");
    JsonNode rootFull = objectMapper.readValue(
        g.generateConfig(RestfulResourceEndpointBase.FullySpecializedEndpoint.class)
        .get("fullApi-v1.api"), JsonNode.class);
    JsonNode methodsFull = rootFull.path("descriptor").path("methods");

    String partialPrefix = RestfulResourceEndpointBase.PartiallySpecializedEndpoint.class.getName();
    String fullPrefix = RestfulResourceEndpointBase.FullySpecializedEndpoint.class.getName();
    // list
    JsonNode methodListPartial = methodsFull.path(partialPrefix + ".list");
    JsonNode methodListFull = methodsFull.path(fullPrefix + ".list");
    assertNotNull(methodListPartial);
    assertEquals(
        methodListPartial.path("request").asText(), methodListFull.path("request").asText());
    // misc
    JsonNode methodMiscPartial = methodsFull.path("GenericRestBasePartiallySpecialized.misc");
    JsonNode methodMiscFull = methodsFull.path("GenericRestBaseFullySpecialized.misc");
    assertEquals(
        methodMiscPartial.path("request").asText(), methodMiscFull.path("request").asText());
  }

  @Test
  public void testDeepGenericHierarchySuccessEndpoint() throws Exception {
    String apiConfigSource =
        g.generateConfig(DeepGenericHierarchySuccessEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");
    assertEquals(1, methods.size());
  }

  @Test
  public void testBoundedGenericEndpoint() throws Exception {
    String apiConfigSource = g.generateConfig(BoundedGenericEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");
    assertEquals(1, methods.size());
  }

  @Test
  public void testWildcardParameterTypes() throws Exception {
    @Api
    final class WildcardEndpoint {
      @SuppressWarnings("unused")
      public void foo(Map<String, ? extends Integer> map) {}
    }
    try {
      g.generateConfig(WildcardEndpoint.class);
      fail("Config generation for service class with wildcard parameter type should have failed");
    } catch (IllegalArgumentException e) {
      // expected
    }
  }

  @Test
  public void testDeepGenericHierarchyFailEndpoint() throws Exception {
    try {
      g.generateConfig(DeepGenericHierarchyFailEndpoint.class);
      fail("Config generation for endpoint with contravariant inherited method should have failed");
    } catch (OverloadedMethodException expected) {
      // expected
    }
  }

  @Test
  public void testServiceWithMergedInheritance() throws Exception {
    String apiConfigSource = g.generateConfig(SubclassedEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifyEndpoint1(SubclassedEndpoint.class, root);
    verifyEndpoint1Schema(SubclassedEndpoint.class, root);
  }

  @Test
  public void testServiceWithOverridingInheritance() throws Exception {
    String apiConfigSource =
        g.generateConfig(SubclassedOverridingEndpoint.class).get("myapi-v2.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifySubclassedOverridingEndpoint(SubclassedOverridingEndpoint.class, root);
    verifySubclassedOverridingEndpointSchema(SubclassedOverridingEndpoint.class, root);
  }

  @Test
  public void testServiceWithApiNameOverride() throws Exception {
    String apiConfigSource = g.generateConfig(
        ServiceContext.create("abc", "xyz"), Endpoint4.class).get("api4-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    assertEquals("api4", root.path("name").asText());

    JsonNode methods = root.path("methods");

    verifyEndpoint0Schema(root, Endpoint4.class.getName());
  }

  @Test
  public void testDefaultEndpointOnGooglePlex() throws Exception {
    String apiConfigSource = g.generateConfig(
        ServiceContext.create("google.com:myapp", "myapi"), Endpoint0.class)
        .get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    assertEquals("myapi", root.path("name").asText());
    assertEquals("https://myapp.googleplex.com/_ah/api", root.path("root").asText());
    assertEquals("https://myapp.googleplex.com/_ah/spi", root.path("adapter").path("bns").asText());
  }

  @Test
  public void testEndpointWithNestedBeans() throws Exception {
    String apiConfigSource = g.generateConfig(Endpoint5.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifyEndpoint5Schema(root);
  }

  @Test
  public void testPrimitiveSchemas() throws Exception {
    String apiConfigSource = g.generateConfig(PrimitiveEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifyPrimitiveSchemas(root);
  }

  @Test
  public void testEndpointNoApiAnnotation() throws Exception {
    try {
      g.generateConfig(Object.class);
      fail("Config generation for service class with no @Api annotation should have failed");
    } catch (ApiConfigException e) {
      // expected
    }
  }

  @Api
  static class TestUnnamedParameterType {
    public String getFoo(String id) {
      return null;
    }
  }

  @Test
  public void testEndpointWithUnnamedParameterTypes() throws Exception {
    try {
      g.generateConfig(TestUnnamedParameterType.class);
      fail("Config generation for service class with unnamed parameter type should have failed");
    } catch (MissingParameterNameException e) {
      // expected
    }
  }

  @Test
  public void testSimpleLevelOverriding() throws Exception {
    String apiConfigSource = g.generateConfig(SimpleLevelOverridingApi.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    JsonNode methods = root.path("methods");

    // myapi.resource1.noOverrides
    JsonNode noOverrides = methods.path("myapi.resource1.noOverrides");
    assertFalse(noOverrides.isMissingNode());
    verifyStrings(objectMapper.convertValue(noOverrides.path("scopes"), String[].class),
        new String[] {"s0a", "s1a"});
    verifyStrings(objectMapper.convertValue(noOverrides.path("audiences"), String[].class),
        new String[] {"a0a", "a1a"});
    verifyStrings(objectMapper.convertValue(noOverrides.path("clientIds"), String[].class),
        new String[] {"c0a", "c1a"});
    assertEquals("resource1", objectMapper.convertValue(noOverrides.path("path"), String.class));
    assertEquals(AuthLevel.REQUIRED,
        objectMapper.convertValue(noOverrides.path("authLevel"), AuthLevel.class));

    // myapi.resource1.overrides
    JsonNode overrides = methods.path("myapi.resource1.overrides");
    assertFalse(overrides.isMissingNode());
    verifyStrings(objectMapper.convertValue(overrides.path("scopes"), String[].class),
        new String[] {"s0b", "s1b"});
    verifyStrings(objectMapper.convertValue(overrides.path("audiences"), String[].class),
        new String[] {"a0b", "a1b"});
    verifyStrings(objectMapper.convertValue(overrides.path("clientIds"), String[].class),
        new String[] {"c0b", "c1b"});
    assertEquals("overridden", objectMapper.convertValue(overrides.path("path"), String.class));
    assertEquals(AuthLevel.OPTIONAL,
        objectMapper.convertValue(overrides.path("authLevel"), AuthLevel.class));
  }

  private void verifyPrimitiveSchemas(JsonNode root) {
    JsonNode schemas = root.path("descriptor").path("schemas");

    JsonNode primitive = schemas.path("PrimitiveBean");
    verifyPrimitiveSchema(primitive);

    JsonNode primitiveCollection = schemas.path("PrimitiveBeanCollection");
    verifyArraySchemaRef(primitiveCollection.path("properties").path("items"), "PrimitiveBean");

    String className = PrimitiveEndpoint.class.getName();
    verifyEmptyMethodRequest(root, className + ".getPrimitive");
    verifyMethodResponseRef(root, className + ".getPrimitive", "PrimitiveBean");
    verifyEmptyMethodRequest(root, className + ".getPrimitives");
    verifyMethodResponseRef(root, className + ".getPrimitives", "PrimitiveBeanCollection");
  }

  private void verifyPrimitiveSchema(JsonNode primitive) {
    verifyObjectSchema(primitive, "PrimitiveBean", "object");
    verifyObjectPropertySchema(primitive, "bool", "boolean");
    verifyObjectPropertySchema(primitive, "byte", "integer");
    verifyObjectPropertySchema(primitive, "char", "string");
    verifyObjectPropertySchema(primitive, "double", "number");
    verifyObjectPropertySchema(primitive, "float", "number", "float");
    verifyObjectPropertySchema(primitive, "int", "integer");
    verifyObjectPropertySchema(primitive, "long", "string", "int64");
    verifyObjectPropertySchema(primitive, "short", "integer");
    verifyObjectPropertySchema(primitive, "str", "string");
  }

  @Test
  public void testRecursiveSchemas() throws Exception {
    String apiConfigSource = g.generateConfig(RecursiveEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifyRecursiveSchemas(root);
  }

  private void verifyRecursiveSchemas(JsonNode root) {
    JsonNode schemas = root.path("descriptor").path("schemas");

    JsonNode recursive = schemas.path("RecursiveBean");
    verifyObjectSchema(recursive, "RecursiveBean", "object");
    verifyObjectPropertySchema(recursive, "name", "string");
    verifyObjectPropertyRef(recursive, "child", "RecursiveBean");
    verifyObjectPropertyRef(recursive, "primitive", "PrimitiveBean");

    JsonNode primitive = schemas.path("PrimitiveBean");
    verifyPrimitiveSchema(primitive);

    String className = RecursiveEndpoint.class.getName();
    verifyEmptyMethodRequest(root, className + ".getRecursive");
    verifyMethodResponseRef(root, className + ".getRecursive", "RecursiveBean");
    verifyMethodRequestRef(root, className + ".updateRecursive", "RecursiveBean");
    verifyMethodResponseRef(root, className + ".updateRecursive", "RecursiveBean");
  }

  @Test
  public void testParentChildSchemas() throws Exception {
    String apiConfigSource = g.generateConfig(ParentChildEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifyParentChildSchemas(root);
  }

  private void verifyParentChildSchemas(JsonNode root) {
    JsonNode schemas = root.path("descriptor").path("schemas");

    JsonNode parent = schemas.path("ParentBean");
    verifyObjectSchema(parent, "ParentBean", "object");
    verifyObjectPropertySchema(parent, "name", "string");
    verifyArraySchemaRef(parent.path("properties").path("children"), "ChildBean");

    JsonNode child = schemas.path("ChildBean");
    verifyObjectSchema(child, "ChildBean", "object");
    verifyObjectPropertySchema(child, "id", "integer");
    verifyObjectPropertyRef(child, "parent", "ParentBean");
    verifyArraySchema(child.path("properties").path("names"), "string");

    JsonNode children = schemas.path("ChildBeanCollection");
    verifyArraySchemaRef(children.path("properties").path("items"), "ChildBean");

    verifyEmptyMethodRequest(root, ParentChildEndpoint.class.getName() + ".getParent");
    verifyMethodResponseRef(root, ParentChildEndpoint.class.getName() + ".getParent", "ParentBean");
    verifyEmptyMethodRequest(root, ParentChildEndpoint.class.getName() + ".listChildren");
    verifyMethodResponseRef(
        root, ParentChildEndpoint.class.getName() + ".listChildren", "ChildBeanCollection");
  }

  @Test
  public void testEndpointWithMultidimensionalArrays() throws Exception {
    String apiConfigSource = g.generateConfig(ArrayEndpoint.class).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, JsonNode.class);
    verifyArrayEndpointSchema(root);
  }

  private void verifyArrayEndpointSchema(JsonNode root) {
    JsonNode schemas = root.path("descriptor").path("schemas");

    JsonNode fooCollection = schemas.path("FooCollection");
    verifyArraySchemaRef(fooCollection.path("properties").path("items"), "Foo");

    JsonNode fooCollectionCollection = schemas.path("FooCollectionCollection");
    verifyArraySchemaRef(fooCollectionCollection.path("properties").path("items"), "Foo", 2);

    JsonNode integerCollection = schemas.path("IntegerCollection");
    verifyArraySchema(integerCollection.path("properties").path("items"), "integer");

    JsonNode baz = schemas.path("Baz");
    verifyObjectSchema(baz, "Baz", "object");
    verifyObjectPropertyRef(baz, "foo", "Foo");
    verifyArraySchemaRef(baz.path("properties").path("foos"), "Foo");

    String backendName = ArrayEndpoint.class.getName();
    verifyEmptyMethodRequest(root, backendName + ".getFoos");
    verifyMethodResponseRef(root, backendName + ".getFoos", "FooCollection");
    verifyEmptyMethodRequest(root, backendName + ".getAllFoos");
    verifyMethodResponseRef(root, backendName + ".getAllFoos", "FooCollectionCollection");
    verifyEmptyMethodRequest(root, backendName + ".getArrayedFoos");
    verifyMethodResponseRef(root, backendName + ".getArrayedFoos", "FooCollection");
    verifyEmptyMethodRequest(root, backendName + ".getAllArrayedFoosFoos");
    verifyMethodResponseRef(root, backendName + ".getAllArrayedFoos", "FooCollectionCollection");
    verifyEmptyMethodRequest(root, backendName + ".getFoosResponse");
    verifyMethodResponseRef(root, backendName + ".getFoosResponse", "CollectionResponse_Foo");
    verifyEmptyMethodRequest(root, backendName + ".getAllFoosResponse");
    verifyMethodResponseRef(
        root, backendName + ".getAllFoosResponse", "CollectionResponse_FooCollection");
    verifyEmptyMethodRequest(root, backendName + ".getAllNestedFoosResponse");
    verifyMethodResponseRef(root, backendName + ".getAllNestedFoosResponse",
        "CollectionResponse_FooCollectionCollection");
    verifyEmptyMethodRequest(root, backendName + ".getIntegers");
    verifyMethodResponseRef(root, backendName + ".getIntegers", "IntegerCollection");
    verifyEmptyMethodRequest(root, backendName + ".getObjectIntegers");
    verifyMethodResponseRef(root, backendName + ".getObjectIntegers", "IntegerCollection");
    verifyEmptyMethodRequest(root, backendName + ".getIntegersResponse");
    verifyMethodResponseRef(
        root, backendName + ".getIntegersResponse", "CollectionResponse_Integer");
    JsonNode arrayEndpoint = schemas.path("ArrayEndpoint");
    verifyArraySchemaRef(arrayEndpoint.path("properties").path("foos"), "Foo");
    verifyArraySchemaRef(arrayEndpoint.path("properties").path("allFoos"), "Foo", 2);
    verifyArraySchemaRef(arrayEndpoint.path("properties").path("arrayedFoos"), "Foo");
    verifyArraySchemaRef(arrayEndpoint.path("properties").path("allArrayedFoos"), "Foo", 2);
    verifyArraySchema(arrayEndpoint.path("properties").path("integers"), "integer");
    verifyArraySchema(arrayEndpoint.path("properties").path("objectIntegers"), "integer");
  }

  private void verifyApi(
      JsonNode root, String superConfig, String apiRoot, String apiName, String apiVersion,
      String description, String backendRoot, boolean allowCookieAuth, boolean defaultVersion) {
    verifyApi(root, superConfig, apiRoot, apiName, apiVersion, description,  backendRoot,
        allowCookieAuth, defaultVersion, null);
  }

  private void verifyApi(JsonNode root, String superConfig, String apiRoot, String apiName,
      String apiVersion, String description, String backendRoot, boolean allowCookieAuth,
      boolean defaultVersion, String[] blockedRegions) {
    assertEquals(superConfig, root.path("extends").asText());
    assertEquals(false, root.path("abstract").asBoolean());
    assertEquals(defaultVersion, root.path("defaultVersion").asBoolean());
    assertEquals(apiRoot, root.path("root").asText());
    assertEquals(apiName, root.path("name").asText());
    assertEquals(apiVersion, root.path("version").asText());
    assertEquals(description, root.path("description").asText());

    JsonNode adapter = root.path("adapter");
    assertEquals(backendRoot, adapter.path("bns").asText());

    JsonNode auth = root.path("auth");
    assertEquals(allowCookieAuth, auth.path("allowCookieAuth").asBoolean());

    if (blockedRegions != null) {
      ArrayNode blockedRegionsNode = (ArrayNode) auth.path("blockedRegions");
      for (int i = 0; i < blockedRegions.length; i++) {
        assertEquals(blockedRegions[i], blockedRegionsNode.get(i).asText());
      }
    }
  }

  private void verifyEndpoint3(JsonNode root) {
    JsonNode methods = root.path("methods");

    // myapi.listBars
    JsonNode methodListBars = methods.path("myapi.endpoint3.listBars");
    verifyMethod(methodListBars, "bar", HttpMethod.GET, Endpoint3.class.getName() + ".listBars",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodListBars.path("request"), "empty", 0);

    // myapi.getBar
    JsonNode methodGetBar = methods.path("myapi.endpoint3.getBar");
    verifyMethod(methodGetBar, "bar/{id}", HttpMethod.GET, Endpoint3.class.getName() + ".getBar",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodGetBar.path("request"), "empty", 1);
    verifyMethodRequestParameter(methodGetBar.path("request"), "id", "string", true, false);

    // myapi.insertBar
    JsonNode methodInsertBar = methods.path("myapi.endpoint3.insertBar");
    verifyMethod(methodInsertBar, "bar", HttpMethod.POST, Endpoint3.class.getName() + ".insertBar",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(
        methodInsertBar.path("request"), "autoTemplate(backendRequest)", 0);

    // myapi.updateBar
    JsonNode methodUpdateBar = methods.path("myapi.endpoint3.updateBar");
    verifyMethod(methodUpdateBar, "bar/{id}", HttpMethod.PUT,
        Endpoint3.class.getName() + ".updateBar", "autoTemplate(backendResponse)");
    verifyMethodRequest(
        methodUpdateBar.path("request"), "autoTemplate(backendRequest)", 1);
    verifyMethodRequestParameter(methodUpdateBar.path("request"), "id", "string", true, false);

    // myapi.removeBar
    JsonNode methodRemoveBar = methods.path("myapi.endpoint3.removeBar");
    verifyMethod(methodRemoveBar, "bar/{id}", HttpMethod.DELETE,
        Endpoint3.class.getName() + ".removeBar", "empty");
    verifyMethodRequest(methodRemoveBar.path("request"), "empty", 1);
    verifyMethodRequestParameter(methodRemoveBar.path("request"), "id", "string", true, false);
  }

  private void verifyEndpoint1(Class<? extends Endpoint1> serviceClass, JsonNode root) {
    verifyApi(root, "thirdParty.api", "https://myapp.appspot.com/_ah/api",
        "myapi", "v1", "API for testing", "https://myapp.appspot.com/_ah/spi", true, true,
        new String[]{"CU"});

    verifyFrontendLimits(root.path("frontendLimits"), 1, 2, 3);

    ArrayNode rules = (ArrayNode) root.path("frontendLimits").path("rules");
    verifyFrontendLimitRule(rules.get(0), "match0", 1, 2, 3, "analyticsId0");
    verifyFrontendLimitRule(rules.get(1), "match10", 11, 12, 13, "analyticsId10");

    verifyCacheControl(root.path("cacheControl"), ApiCacheControl.Type.PUBLIC, 1);

    JsonNode methods = root.path("methods");
    String serviceClassName = serviceClass.getName();

    // myapi.foos.listFoos
    JsonNode methodListFoos = methods.path("myapi.foos.list");
    verifyMethod(methodListFoos, "foos", HttpMethod.GET, serviceClassName + ".listFoos",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodListFoos.path("request"), "empty", 0);
    verifyStrings(objectMapper.convertValue(methodListFoos.path("scopes"), String[].class),
        new String[] {"s0", "s1 s2"});
    verifyStrings(objectMapper.convertValue(methodListFoos.path("audiences"), String[].class),
        new String[] {"a0", "a1"});
    verifyStrings(objectMapper.convertValue(methodListFoos.path("clientIds"), String[].class),
        new String[] {"c0", "c1"});

    // myapi.foos.get
    JsonNode methodGetFoo = methods.path("myapi.foos.get");
    verifyMethod(methodGetFoo, "foos/{id}", HttpMethod.GET, serviceClassName + ".getFoo",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodGetFoo.path("request"), "empty", 1);
    verifyMethodRequestParameter(methodGetFoo.path("request"), "id", "string", true, false);
    verifyStrings(objectMapper.convertValue(methodGetFoo.path("scopes"), String[].class),
        new String[] {"ss0", "ss1 ss2"});
    verifyStrings(objectMapper.convertValue(methodGetFoo.path("audiences"), String[].class),
        new String[] {"aa0", "aa1"});
    verifyStrings(objectMapper.convertValue(methodGetFoo.path("clientIds"), String[].class),
        new String[] {"cc0", "cc1"});

    // myapi.foos.insert
    JsonNode methodInsertFoo = methods.path("myapi.foos.insert");
    verifyMethod(methodInsertFoo, "foos", HttpMethod.POST, serviceClassName + ".insertFoo",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(
        methodInsertFoo.path("request"), "autoTemplate(backendRequest)", 0);

    // myapi.foos.update
    JsonNode methodUpdateFoo = methods.path("myapi.foos.update");
    verifyMethod(methodUpdateFoo, "foos/{id}", HttpMethod.PUT, serviceClassName + ".updateFoo",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(
        methodUpdateFoo.path("request"), "autoTemplate(backendRequest)", 1);
    verifyMethodRequestParameter(methodUpdateFoo.path("request"), "id", "string", true, false);

    // myapi.foos.remove
    JsonNode methodRemoveFoo = methods.path("myapi.foos.remove");
    verifyMethod(
        methodRemoveFoo, "foos/{id}", HttpMethod.DELETE, serviceClassName + ".removeFoo", "empty");
    verifyMethodRequest(methodRemoveFoo.path("request"), "empty", 1);
    verifyMethodRequestParameter(methodRemoveFoo.path("request"), "id", "string", true, false);

    // myapi.foos.execute0
    JsonNode methodExecute0 = methods.path("myapi.foos.execute0");
    verifyMethod(methodExecute0, "execute0", HttpMethod.POST, serviceClassName + ".execute0",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodExecute0.path("request"), "empty", 9);

    JsonNode methodExecute0Request = methodExecute0.path("request");
    verifyMethodRequestParameter(methodExecute0Request, "id", "string", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "i0", "int32", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "i1", "int32", false, false);
    verifyMethodRequestParameter(methodExecute0Request, "long0", "int64", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "long1", "int64", false, false);
    verifyMethodRequestParameter(methodExecute0Request, "b0", "boolean", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "b1", "boolean", false, false);
    verifyMethodRequestParameter(methodExecute0Request, "f", "float", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "d", "double", false, false);

    // myapi.foos.execute2
    JsonNode methodExecute2 = methods.path("myapi.foos.execute2");
    verifyMethod(methodExecute2, "execute2/{serialized}", HttpMethod.POST,
        serviceClassName + ".execute2", "empty");
    JsonNode methodExecute2Request = methodExecute2.path("request");
    verifyMethodRequestParameter(methodExecute2Request, "serialized", "string", true, false);
  }

  private void verifySubclassedOverridingEndpoint(
      Class<? extends SubclassedOverridingEndpoint> serviceClass, JsonNode root) {
    verifyApi(root, "thirdParty.api", "https://myapp.appspot.com/_ah/api",
        "myapi", "v2", "overridden description", "https://myapp.appspot.com/_ah/spi", true,
        false);

    verifyFrontendLimits(root.path("frontendLimits"), 1, 4, 3);

    ArrayNode rules = (ArrayNode) root.path("frontendLimits").path("rules");
    verifyFrontendLimitRule(rules.get(0), "match0", 1, 2, 3, "analyticsId0");
    verifyFrontendLimitRule(rules.get(1), "match10", 11, 12, 13, "analyticsId10");

    verifyCacheControl(root.path("cacheControl"), ApiCacheControl.Type.PUBLIC, 2);

    JsonNode methods = root.path("methods");
    String serviceClassName = serviceClass.getName();
    String servicePrefix = "myapi." + serviceClass.getSimpleName();

    // myapi.foos.listFoos
    JsonNode methodListFoos = methods.path("myapi.foos.list");
    verifyMethod(methodListFoos, "foos", HttpMethod.GET, serviceClassName + ".listFoos",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodListFoos.path("request"), "empty", 0);
    verifyStrings(objectMapper.convertValue(methodListFoos.path("scopes"), String[].class),
        new String[] {"s0", "s1 s2"});
    verifyStrings(objectMapper.convertValue(methodListFoos.path("audiences"), String[].class),
        new String[] {"a0", "a1"});
    verifyStrings(objectMapper.convertValue(methodListFoos.path("clientIds"), String[].class),
        new String[] {"c0", "c1"});

    // myapi.foos.get
    assertTrue(methods.path(servicePrefix + ".foos.get").isMissingNode());

    // myapi.foos.get2
    JsonNode methodGetFoo = methods.path("myapi.foos.get2");
    verifyMethod(methodGetFoo, "foos/{id}", HttpMethod.GET, serviceClassName + ".getFoo",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodGetFoo.path("request"), "empty", 1);
    verifyMethodRequestParameter(methodGetFoo.path("request"), "id", "string", true, false);
    verifyStrings(objectMapper.convertValue(methodGetFoo.path("scopes"), String[].class),
        new String[] {"ss0a", "ss1a"});
    verifyStrings(objectMapper.convertValue(methodGetFoo.path("audiences"), String[].class),
        new String[] {"aa0a", "aa1a"});
    verifyStrings(objectMapper.convertValue(methodGetFoo.path("clientIds"), String[].class),
        new String[] {"cc0a", "cc1a"});

    // myapi.foos.insert
    JsonNode methodInsertFoo = methods.path("myapi.foos.insert");
    verifyMethod(methodInsertFoo, "foos", HttpMethod.POST, serviceClassName + ".insertFoo",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(
        methodInsertFoo.path("request"), "autoTemplate(backendRequest)", 0);

    // myapi.foos.update
    JsonNode methodUpdateFoo = methods.path("myapi.foos.update");
    verifyMethod(methodUpdateFoo, "foos/{id}", HttpMethod.PUT, serviceClassName + ".updateFoo",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(
        methodUpdateFoo.path("request"), "autoTemplate(backendRequest)", 1);
    verifyMethodRequestParameter(methodUpdateFoo.path("request"), "id", "string", true, false);

    // myapi.foos.remove
    JsonNode methodRemoveFoo = methods.path("myapi.foos.remove");
    verifyMethod(methodRemoveFoo, "foos/{id}", HttpMethod.DELETE, serviceClassName + ".removeFoo",
        "empty");
    verifyMethodRequest(methodRemoveFoo.path("request"), "empty", 1);
    verifyMethodRequestParameter(methodRemoveFoo.path("request"), "id", "string", true, false);

    // myapi.foos.execute0
    JsonNode methodExecute0 = methods.path("myapi.foos.execute0");
    verifyMethod(methodExecute0, "execute0", HttpMethod.POST, serviceClassName + ".execute0",
        "autoTemplate(backendResponse)");
    verifyMethodRequest(methodExecute0.path("request"), "empty", 9);
    JsonNode methodExecute0Request = methodExecute0.path("request");
    verifyMethodRequestParameter(methodExecute0Request, "id", "string", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "i0", "int32", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "i1", "int32", false, false);
    verifyMethodRequestParameter(methodExecute0Request, "long0", "int64", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "long1", "int64", false, false);
    verifyMethodRequestParameter(methodExecute0Request, "b0", "boolean", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "b1", "boolean", false, false);
    verifyMethodRequestParameter(methodExecute0Request, "f", "float", true, false);
    verifyMethodRequestParameter(methodExecute0Request, "d", "double", false, false);

    // myapi.foos.execute2
    JsonNode methodExecute2 = methods.path("myapi.foos.execute2");
    verifyMethod(methodExecute2, "execute2/{serialized}", HttpMethod.POST,
        serviceClassName + ".execute2", "empty");
    JsonNode methodExecute2Request = methodExecute2.path("request");
    verifyMethodRequestParameter(methodExecute2Request, "serialized", "int32", true, false);
  }

  private void verifyFrontendLimits(JsonNode frontendLimits, int unregisteredUserQps,
      int unregisteredQps, int unregisteredDaily) {
    assertEquals(unregisteredUserQps, frontendLimits.path("unregisteredUserQps").asInt());
    assertEquals(unregisteredQps, frontendLimits.path("unregisteredQps").asInt());
    assertEquals(unregisteredDaily, frontendLimits.path("unregisteredDaily").asInt());
  }

  private void verifyFrontendLimitRule(
      JsonNode rule, String match, int qps, int userQps, int daily, String analyticsId) {
    assertEquals(match, rule.path("match").asText());
    assertEquals(qps, rule.path("qps").asInt());
    assertEquals(userQps, rule.path("userQps").asInt());
    assertEquals(daily, rule.path("daily").asInt());
    assertEquals(analyticsId, rule.path("analyticsId").asText());
  }

  private void verifyCacheControl(JsonNode cacheControl, String type, int maxAge) {
    assertEquals(type, cacheControl.path("type").asText());
    assertEquals(maxAge, cacheControl.path("maxAge").asInt());
  }

  private void verifyMethod(JsonNode method, String restPath, String httpMethod,
      String backendMethod, String responseBody) {
    assertFalse(method.isMissingNode());

    assertEquals(restPath, method.path("path").asText());
    assertEquals(httpMethod, method.path("httpMethod").asText());
    assertEquals(backendMethod, method.path("rosyMethod").asText());

    JsonNode response = method.path("response");
    assertEquals(responseBody, response.path("body").asText());
  }

  private void verifyMethodRequest(JsonNode request, String requestBody, int parameterCount) {
    assertEquals(requestBody, request.path("body").asText());
    if (!requestBody.equals("empty")) {
      assertEquals("resource", request.path("bodyName").asText());
    }
    JsonNode parameters = request.path("parameters");
    assertEquals(parameterCount, parameters.size());
  }

  private void verifyMethodRequestParameter(JsonNode request, String parameterName, String type,
      boolean required, boolean repeated, String... enumValues) {
    assertFalse(request.isMissingNode());

    JsonNode parameters = request.path("parameters");
    JsonNode parameter = parameters.path(parameterName);
    assertEquals(required, parameter.path("required").asBoolean());
    assertEquals(repeated, parameter.path("repeated").asBoolean());
    assertEquals(type, parameter.path("type").asText());
    if (enumValues.length > 0) {
      JsonNode enumValueNodes = parameter.path("enum");
      assertFalse(enumValueNodes.isMissingNode());
      assertEquals(enumValues.length, enumValueNodes.size());
      for (String enumValue : enumValues) {
        assertNotNull(enumValueNodes.get(enumValue));
      }
    }
  }

  private void verifyObjectSchema(JsonNode schema, String id, String type) {
    assertEquals(id, schema.path("id").asText());
    assertEquals(type, schema.path("type").asText());
  }

  private void verifyObjectPropertySchema(JsonNode schema, String name, String type) {
    assertEquals(type, schema.path("properties").path(name).path("type").asText());
  }

  private void verifyObjectPropertySchema(
      JsonNode schema, String name, String type, String format) {
    assertEquals(type, schema.path("properties").path(name).path("type").asText());
    assertEquals(format, schema.path("properties").path(name).path("format").asText());
  }

  private void verifyObjectPropertyRef(JsonNode schema, String name, String ref) {
    assertEquals(ref, schema.path("properties").path(name).path("$ref").asText());
  }

  private void verifyArraySchema(JsonNode array, String itemType) {
    verifyArraySchema(array, itemType, 1);
  }

  private void verifyArraySchema(JsonNode array, String itemType, int dimensions) {
    for (int i = 0; i < dimensions; i++) {
      assertEquals("array", array.path("type").asText());
      array = array.path("items");
    }
    assertEquals(itemType, array.path("type").asText());
  }

  private void verifyArraySchemaRef(JsonNode array, String itemType) {
    verifyArraySchemaRef(array, itemType, 1);
  }

  private void verifyArraySchemaRef(JsonNode array, String itemType, int dimensions) {
    for (int i = 0; i < dimensions; i++) {
      assertEquals("array", array.path("type").asText());
      array = array.path("items");
    }
    assertEquals(itemType, array.path("$ref").asText());
  }

  private void verifyEmptyMethodRequest(JsonNode root, String backendMethodName) {
    verifyMethodRequest(root, backendMethodName, "", "");
    verifyMethodRequestRef(root, backendMethodName, "");
  }

  private void verifyMethodRequest(
      JsonNode root, String backendMethodName, String requestType, String requestFormat) {
    JsonNode request =
        root.path("descriptor").path("methods").path(backendMethodName).path("request");
    assertEquals(requestType, request.path("type").asText());
    assertEquals(requestFormat, request.path("format").asText());
  }

  private void verifyMethodRequestRef(JsonNode root, String backendMethodName, String requestType) {
    JsonNode request =
        root.path("descriptor").path("methods").path(backendMethodName).path("request");
    assertEquals(requestType, request.path("$ref").asText());
  }

  private void verifyEmptyMethodResponse(JsonNode root, String backendMethodName) {
    verifyMethodResponse(root, backendMethodName, "", "");
    verifyMethodResponseRef(root, backendMethodName, "");
  }

  private void verifyMethodResponse(
      JsonNode root, String backendMethodName, String responseType, String responseFormat) {
    JsonNode response =
        root.path("descriptor").path("methods").path(backendMethodName).path("response");
    assertEquals(responseType, response.path("type").asText());
    assertEquals(responseFormat, response.path("format").asText());
  }

  private void verifyMethodResponseRef(
      JsonNode root, String backendMethodName, String responseType) {
    JsonNode response =
        root.path("descriptor").path("methods").path(backendMethodName).path("response");
    assertEquals(responseType, response.path("$ref").asText());
  }

  private void verifyEndpoint0Schema(JsonNode root, String className) {
    JsonNode schemas = root.path("descriptor").path("schemas");
    JsonNode foo = schemas.path("Foo");
    verifyObjectSchema(foo, "Foo", "object");
    verifyObjectPropertySchema(foo, "name", "string");
    verifyObjectPropertySchema(foo, "value", "integer");

    verifyEmptyMethodRequest(root, className + ".getFoo");
    verifyMethodResponseRef(root, className + ".getFoo", "Foo");
  }

  private void verifyEndpoint1Schema(Class<? extends Endpoint1> serviceClass, JsonNode root) {
    JsonNode schemas = root.path("descriptor").path("schemas");
    JsonNode foo = schemas.path("Foo");
    verifyObjectSchema(foo, "Foo", "object");
    verifyObjectPropertySchema(foo, "name", "string");
    verifyObjectPropertySchema(foo, "value", "integer");
    JsonNode fooCollection = schemas.path("FooCollection");
    verifyArraySchemaRef(fooCollection.path("properties").path("items"), "Foo");

    String serviceClassName = serviceClass.getName();
    verifyEmptyMethodRequest(root, serviceClassName + ".listFoos");
    verifyMethodResponseRef(root, serviceClassName + ".listFoos", "FooCollection");
    verifyEmptyMethodRequest(root, serviceClassName + ".getFoo");
    verifyMethodResponseRef(root, serviceClassName + ".getFoo", "Foo");
    verifyMethodRequestRef(root, serviceClassName + ".insertFoo", "Foo");
    verifyMethodResponseRef(root, serviceClassName + ".insertFoo", "Foo");
    verifyMethodRequestRef(root, serviceClassName + ".updateFoo", "Foo");
    verifyMethodResponseRef(root, serviceClassName + ".updateFoo", "Foo");
    verifyEmptyMethodRequest(root, serviceClassName + ".removeFoo");
    verifyEmptyMethodResponse(root, serviceClassName + ".removeFoo");
    verifyEmptyMethodRequest(root, serviceClassName + ".execute0");
    verifyMethodResponseRef(
        root, serviceClassName + ".execute0", JsonConfigWriter.ANY_SCHEMA_NAME);
    verifyMethodRequestRef(root, serviceClassName + ".execute1", "Foo");
    verifyMethodResponseRef(
        root, serviceClassName + ".execute1", JsonConfigWriter.MAP_SCHEMA_NAME);
  }

  private void verifyEndpoint3Schema(JsonNode root) {
    JsonNode schemas = root.path("descriptor").path("schemas");
    JsonNode foo = schemas.path("Bar");
    verifyObjectSchema(foo, "Bar", "object");
    verifyObjectPropertySchema(foo, "name", "string");
    verifyObjectPropertySchema(foo, "value", "integer");
    JsonNode foos = schemas.path("BarCollection");
    verifyArraySchemaRef(foos.path("properties").path("items"), "Bar");

    verifyEmptyMethodRequest(root, Endpoint3.class.getName() + ".listBars");
    verifyMethodResponseRef(root, Endpoint3.class.getName() + ".listBars", "BarCollection");
    verifyEmptyMethodRequest(root, Endpoint3.class.getName() + ".getBar");
    verifyMethodResponseRef(root, Endpoint3.class.getName() + ".getBar", "Bar");
    verifyMethodRequestRef(root, Endpoint3.class.getName() + ".insertBar", "Bar");
    verifyMethodResponseRef(root, Endpoint3.class.getName() + ".insertBar", "Bar");
    verifyMethodRequestRef(root, Endpoint3.class.getName() + ".updateBar", "Bar");
    verifyMethodResponseRef(root, Endpoint3.class.getName() + ".updateBar", "Bar");
    verifyEmptyMethodRequest(root, Endpoint3.class.getName() + ".removeBar");
    verifyEmptyMethodResponse(root, Endpoint3.class.getName() + ".removeBar");
  }

  private void verifyEndpoint5Schema(JsonNode root) {
    JsonNode schemas = root.path("descriptor").path("schemas");
    JsonNode foo = schemas.path("Foo");
    verifyObjectSchema(foo, "Foo", "object");
    verifyObjectPropertySchema(foo, "name", "string");
    verifyObjectPropertySchema(foo, "value", "integer");

    JsonNode baz = schemas.path("Baz");
    verifyObjectSchema(baz, "Baz", "object");
    verifyObjectPropertyRef(baz, "foo", "Foo");
    verifyArraySchemaRef(baz.path("properties").path("foos"), "Foo");

    verifyEmptyMethodRequest(root, Endpoint5.class.getName() + ".getBaz");
    verifyMethodResponseRef(root, Endpoint5.class.getName() + ".getBaz", "Baz");
  }

  private void verifySubclassedOverridingEndpointSchema(
      Class<? extends SubclassedOverridingEndpoint> serviceClass, JsonNode root) {
    JsonNode schemas = root.path("descriptor").path("schemas");
    JsonNode foo = schemas.path("Foo");
    verifyObjectSchema(foo, "Foo", "object");
    verifyObjectPropertySchema(foo, "name", "string");
    verifyObjectPropertySchema(foo, "value", "integer");
    JsonNode fooCollection = schemas.path("FooCollection");
    verifyArraySchemaRef(fooCollection.path("properties").path("items"), "Foo");

    String serviceClassName = serviceClass.getName();
    verifyEmptyMethodRequest(root, serviceClassName + ".listFoos");
    verifyMethodResponseRef(root, serviceClassName + ".listFoos", "FooCollection");
    verifyEmptyMethodRequest(root, serviceClassName + ".getFoo");
    verifyMethodResponseRef(root, serviceClassName + ".getFoo", "Foo");
    verifyMethodRequestRef(root, serviceClassName + ".insertFoo", "Foo");
    verifyMethodResponseRef(root, serviceClassName + ".insertFoo", "Foo");
    verifyMethodRequestRef(root, serviceClassName + ".updateFoo", "Foo");
    verifyMethodResponseRef(root, serviceClassName + ".updateFoo", "Foo");
    verifyEmptyMethodRequest(root, serviceClassName + ".removeFoo");
    verifyEmptyMethodResponse(root, serviceClassName + ".removeFoo");
    verifyEmptyMethodRequest(root, serviceClassName + ".execute0");
    verifyMethodResponseRef(
        root, serviceClassName + ".execute0", JsonConfigWriter.ANY_SCHEMA_NAME);
    verifyMethodRequestRef(root, serviceClassName + ".execute1", "Foo");
    verifyMethodResponseRef(
        root, serviceClassName + ".execute1", JsonConfigWriter.MAP_SCHEMA_NAME);
  }

  private enum Outcome {
    WON, LOST, TIE
  }

  @SuppressWarnings("unused")
  private static class Bean {
    public Date getDate() {
      return null;
    }

    public void setDate(Date date) {}
  }

  @Test
  public void testValidDateInParameter() throws Exception {
    @Api
    class DateParameter {
      @SuppressWarnings("unused")
      public void foo(@Named("date") Date date) { }
    }
    String apiConfigSource = g.generateConfig(DateParameter.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    JsonNode request = root.path("methods").path("myapi.dateParameter.foo").path("request");
    verifyMethodRequestParameter(request, "date", "datetime", true, false);
    assertTrue(root.path("descriptor").path("schemas").path("Outcome").isMissingNode());
    verifyEmptyMethodRequest(root, DateParameter.class.getName() + ".pick");
  }

  @Test
  public void testDateCollection() throws Exception {
    @Api
    class DateParameters {
      @SuppressWarnings("unused")
      public void foo(@Named("date") Date date, @Named("dates1") Collection<Date> dates1,
          @Named("dates2") Date[] dates2) { }
    }
    String apiConfigSource = g.generateConfig(DateParameters.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    JsonNode request = root.path("methods").path("myapi.dateParameters.foo").path("request");
    verifyMethodRequestParameter(request, "date", "datetime", true, false);
    verifyMethodRequestParameter(request, "dates1", "datetime", true, true);
    verifyMethodRequestParameter(request, "dates2", "datetime", true, true);
    verifyEmptyMethodRequest(root, DateParameters.class.getName() + ".foo");
  }

  @Test
  public void testInvalidDateInEndpointRequest() throws Exception {
    @Api
    class DateParameter {
      @SuppressWarnings("unused")
      public void foo(Date date) {}
    }

    try {
      g.generateConfig(DateParameter.class).get("myapi-v1.api");
      fail("Dates should not be treated as resources");
    } catch (MissingParameterNameException expected) {
      // expected
    }
  }

  private static class EnumBean {
    @SuppressWarnings("unused")
    public Outcome getOutcome() {
      return null;
    }
  }

  @Test
  public void testValidEnumInParameter() throws Exception {
    @Api
    class EnumParameter {
      @SuppressWarnings("unused")
      public void pick(@Named("outcome") Outcome outcome) { }
    }
    String apiConfigSource = g.generateConfig(EnumParameter.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    JsonNode request = root.path("methods").path("myapi.enumParameter.pick").path("request");
    verifyMethodRequestParameter(request, "outcome", "string", true, false, "WON", "LOST", "TIE");
    assertTrue(root.path("descriptor").path("schemas").path("Outcome").isMissingNode());
    verifyEmptyMethodRequest(root, EnumParameter.class.getName() + ".pick");
  }

  @Test
  public void testValidEnumInParameterAndResource() throws Exception {
    @Api
    class EnumParameter {
      @SuppressWarnings("unused")
      public void pick(@Named("outcomeParam") Outcome outcome, EnumBean date) { }
    }

    String apiConfigSource = g.generateConfig(EnumParameter.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    JsonNode outcomeSchema = root.path("descriptor").path("schemas").path("Outcome");
    assertEquals("Outcome", outcomeSchema.path("id").asText());
    assertEquals("string", outcomeSchema.path("type").asText());
    JsonNode enumConfig = outcomeSchema.path("enum");
    assertTrue(enumConfig.isArray());
    assertEquals(3, enumConfig.size());
    assertEquals(Outcome.WON.toString(), enumConfig.get(0).asText());
    assertEquals(Outcome.LOST.toString(), enumConfig.get(1).asText());
    assertEquals(Outcome.TIE.toString(), enumConfig.get(2).asText());
    JsonNode enumSchema = root.path("descriptor").path("schemas").path("EnumBean");
    assertEquals("EnumBean", enumSchema.path("id").asText());
    assertEquals("object", enumSchema.path("type").asText());
    assertNotNull(enumSchema.get("properties"));
    assertNotNull(enumSchema.path("properties").path("outcome").get("$ref"));
    assertEquals("Outcome", enumSchema.path("properties").path("outcome").path("$ref").asText());

    JsonNode methodSchema = root.path("descriptor").path("methods").path(
        EnumParameter.class.getName() + ".pick");
    assertNotNull(methodSchema.get("request"));
    assertNotNull(methodSchema.path("request").get("$ref"));
    assertEquals("EnumBean", methodSchema.path("request").get("$ref").asText());
  }

  /**
   * A {@code Bar} resource serializer with a property of type {@code Baz}.
   */
  protected static class BarResourceSerializer
      extends DefaultValueSerializer<Bar, Map<String, Object>> implements ResourceTransformer<Bar> {
    public BarResourceSerializer() {
    }
    @Override
    public ResourceSchema getResourceSchema() {
      return ResourceSchema.builderForType(Bar.class)
          .addProperty("someBaz", ResourcePropertySchema.of(TypeToken.of(Baz.class)))
          .build();
    }
  }

  @Test
  public void testSerializedPropertyInResourceSchema() throws Exception {
    class BazToDateSerializer extends DefaultValueSerializer<Baz, Date> {
    }
    @Api(transformers = {BazToDateSerializer.class, BarResourceSerializer.class})
    class BarEndpoint {
      @SuppressWarnings("unused")
      public Bar getBar() {
        return null;
      }
    }
    String apiConfigSource = g.generateConfig(BarEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode bar = root.path("descriptor").path("schemas").path("Bar");
    verifyObjectPropertySchema(bar, "someBaz", "string", "date-time");
  }

  @Test
  public void testChainedSerializer() throws Exception {
    class BarToBazSerializer extends DefaultValueSerializer<Bar, Baz> {
    }
    class BazToDateSerializer extends DefaultValueSerializer<Baz, Date> {
    }
    class Qux {
      @SuppressWarnings("unused")
      public Bar getSomeBar() {
        return null;
      }
    }
    @Api(transformers = {BazToDateSerializer.class, BarToBazSerializer.class})
    class QuxEndpoint {
      @SuppressWarnings("unused")
      public Qux getQux() {
        return null;
      }
    }
    String apiConfigSource = g.generateConfig(QuxEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode qux = root.path("descriptor").path("schemas").path("Qux");
    verifyObjectPropertySchema(qux, "someBar", "string", "date-time");
  }

  @Test
  public void testSerializedEnum() throws Exception {
    class OutcomeToIntegerSerializer extends DefaultValueSerializer<Outcome, Integer> {
    }
    @Api(transformers = {OutcomeToIntegerSerializer.class})
    class EnumParameter {
      @SuppressWarnings("unused")
      public void foo(@Named("outcome") Outcome outcome) { }
    }
    String apiConfigSource = g.generateConfig(EnumParameter.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    JsonNode request = root.path("methods").path("myapi.enumParameter.foo").path("request");
    verifyMethodRequestParameter(request, "outcome", "int32", true, false);
  }

  @Test
  public void testNonSerializedEnumShouldAlwaysBeString() throws Exception {
    class StringToIntegerSerializer extends DefaultValueSerializer<String, Integer> {
    }
    @Api(transformers = {StringToIntegerSerializer.class})
    class EnumParameter {
      @SuppressWarnings("unused")
      public void foo(@Named("outcome") Outcome outcome) { }
    }
    String apiConfigSource = g.generateConfig(EnumParameter.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    JsonNode request = root.path("methods").path("myapi.enumParameter.foo").path("request");
    verifyMethodRequestParameter(request, "outcome", "string", true, false, "WON", "LOST", "TIE");
    assertTrue(root.path("descriptor").path("schemas").path("Outcome").isMissingNode());
    verifyEmptyMethodRequest(root, EnumParameter.class.getName() + ".pick");
  }

  @Test
  public void testEnumCollection() throws Exception {
    @Api
    class EnumParameters {
      @SuppressWarnings("unused")
      public void foo(@Named("outcome") Outcome outcome,
          @Named("outcomes1") Collection<Outcome> outcomes1,
          @Named("outcomes2") Outcome[] outcomes2) { }
    }
    String apiConfigSource = g.generateConfig(EnumParameters.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    JsonNode request = root.path("methods").path("myapi.enumParameters.foo").path("request");
    verifyMethodRequestParameter(request, "outcome", "string", true, false, "WON", "LOST", "TIE");
    verifyMethodRequestParameter(request, "outcomes1", "string", true, true, "WON", "LOST", "TIE");
    verifyMethodRequestParameter(request, "outcomes2", "string", true, true, "WON", "LOST", "TIE");
    assertTrue(root.path("descriptor").path("schemas").path("Outcome").isMissingNode());
    verifyEmptyMethodRequest(root, EnumParameters.class.getName() + ".foo");
  }

  @Test
  public void testInvalidEnumInEndpointRequest() throws Exception {
    @Api
    class EnumParameter {
      @SuppressWarnings("unused")
      public void pick(Outcome outcome) {}
    }

    try {
      g.generateConfig(EnumParameter.class).get("myapi-v1.api");
      fail("Enums should not be treated as resources");
    } catch (MissingParameterNameException expected) {
      // expected
    }
  }

  @SuppressWarnings("unused")
  @Api
  private static class Endpoint {
    public void remove(@Named("id") String id) {}

    public void delete(@Named("id") String id) {}
  }

  @Test
  public void testBareRemoveAndDelete() throws Exception {
    String apiConfigSource = g.generateConfig(Endpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    // if we have no other clue, we should just use "remove" or "delete" as the REST path
    assertEquals(
        "remove/{id}", root.path("methods").path("myapi.endpoint.remove").path("path").asText());
    assertEquals(
        "delete/{id}", root.path("methods").path("myapi.endpoint.delete").path("path").asText());
  }

  @Api
  static class TestEndpoint {
    public Foo getItem(
        @Named("required") String required, @Nullable @Named("optional") String optional) {
      return null;
    }

    public List<Bar> list() {
      return null;
    }

    public CollectionResponse<Baz> listWithPagination() {
      return null;
    }

    private static class MyCollectionResponse<T> extends CollectionResponse<T> {
      protected MyCollectionResponse(Collection<T> items, String nextPageToken) {
        super(items, nextPageToken);
      }
    }

    public MyCollectionResponse<Foo> listWithMyCollectionResponse() {
      return null;
    }
  }

  @Test
  public void testOptionalParameters() throws Exception {
    String apiConfigSource = g.generateConfig(TestEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    assertEquals("foo/{required}",
        root.path("methods").path("myapi.testEndpoint.getItem").path("path").asText());
  }

  private void verifyStrings(String[] a0, String[] a1) {
    assertEquals(a0.length, a1.length);
    for (int i = 0; i < a0.length; i++) {
      assertEquals(a0[i], a1[i]);
    }
  }

  @Test
  public void testCollectionResponse() throws Exception {
    String apiConfigSource = g.generateConfig(TestEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    // regular collection response
    assertEquals("bar", root.path("methods").path("myapi.testEndpoint.list").path("path").asText());
    assertNotNull(root.path("descriptor").path("schemas").path("Bar"));
    assertNotNull(root.path("descriptor").path("schemas").path("BarCollection"));
    assertEquals("array", root.path("descriptor").path("schemas").path("BarCollection")
        .path("properties").path("items").path("type").asText());
    assertEquals("Bar", root.path("descriptor").path("schemas").path("BarCollection")
        .path("properties").path("items").path("items").path("$ref").asText());

    String backendName = TestEndpoint.class.getName();
    // specific CollectionResponse
    assertEquals("baz", root.path("methods").path("myapi.testEndpoint.listWithPagination")
        .path("path").asText());
    assertEquals("CollectionResponse_Baz", root.path("descriptor").path("methods")
        .path(backendName + ".listWithPagination").path("response").path("$ref").asText());
    assertNotNull(root.path("descriptor").path("schemas").path("Baz"));
    assertNotNull(root.path("descriptor").path("schemas").path("CollectionResponse_Baz"));
    assertEquals("array", root.path("descriptor").path("schemas").path("CollectionResponse_Baz")
        .path("properties").path("items").path("type").asText());
    assertEquals("Baz", root.path("descriptor").path("schemas").path("CollectionResponse_Baz")
        .path("properties").path("items").path("items").path("$ref").asText());
    assertEquals("string", root.path("descriptor").path("schemas").path("CollectionResponse_Baz")
        .path("properties").path("nextPageToken").path("type").asText());
    assertEquals("CollectionResponse_Baz", root.path("descriptor").path("methods")
        .path(backendName + ".listWithPagination").path("response").path("$ref").asText());

    // subclass of CollectionResponse
    assertEquals("foo", root.path("methods").path("myapi.testEndpoint.listWithMyCollectionResponse")
        .path("path").asText());
    assertEquals("MyCollectionResponse_Foo", root.path("descriptor").path("methods")
        .path(backendName + ".listWithMyCollectionResponse").path("response")
        .path("$ref").asText());
    assertNotNull(root.path("descriptor").path("schemas").path("Foo"));
    assertNotNull(root.path("descriptor").path("schemas").path("MyCollectionResponse_Foo"));
    assertEquals("array", root.path("descriptor").path("schemas").path("MyCollectionResponse_Foo")
        .path("properties").path("items").path("type").asText());
    assertEquals("Foo", root.path("descriptor").path("schemas").path("MyCollectionResponse_Foo")
        .path("properties").path("items").path("items").path("$ref").asText());
    assertEquals("string", root.path("descriptor").path("schemas").path("MyCollectionResponse_Foo")
        .path("properties").path("nextPageToken").path("type").asText());
    assertEquals("MyCollectionResponse_Foo", root.path("descriptor").path("methods")
        .path(backendName + ".listWithMyCollectionResponse").path("response")
        .path("$ref").asText());
  }

  @Test
  public void testAbstract() throws Exception {
    @Api(isAbstract = AnnotationBoolean.TRUE)
    class Test {}

    String apiConfigSource = g.generateConfig(Test.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    assertTrue(root.get("abstract").asBoolean());
  }

  @Test
  public void testRepeatedParameterName() throws Exception {
    @Api
    class Test {
      @ApiMethod(path = "path")
      public void foo(@Named("id") String id1, @Named("id") Long id2) {}
    }

    try {
      g.generateConfig(Test.class).get("myapi-v1.api");
      fail("Config generation for endpoint with two parameters named the same should have failed.");
    } catch (DuplicateParameterNameException expected) {
      // expected
    }
  }

  @Test
  public void testNullablePathParameter() throws Exception {
    @Api
    class Test {
      @ApiMethod(path = "path/{id}")
      public void foo(@Named("id") @Nullable String id) {}
    }

    try {
      g.generateConfig(Test.class);
      fail("Config generation for endpoint with nullable path parameter should have failed.");
    } catch (InvalidParameterAnnotationsException expected) {
      // expected
    }
  }

  @Api
  private static class DefaultValuedPathParameterEndpoint<T> {
    @ApiMethod(path = "path/{id}")
    public void foo(@Named("id") @DefaultValue("bar") T id) {}
  }

  @Test
  public void testDefaultValuedPathParameterBoolean() throws Exception {
    final class Test extends DefaultValuedPathParameterEndpoint<Boolean> {}

    try {
      g.generateConfig(Test.class);
      fail("Config generation for endpoint with default-valued path parameter should have failed.");
    } catch (InvalidParameterAnnotationsException expected) {
      // expected
    }
  }

  @Test
  public void testDefaultValuedPathParameterInteger() throws Exception {
    final class Test extends DefaultValuedPathParameterEndpoint<Integer> {}

    try {
      g.generateConfig(Test.class);
      fail("Config generation for endpoint with default-valued path parameter should have failed.");
    } catch (InvalidParameterAnnotationsException expected) {
      // expected
    }
  }

  @Test
  public void testDefaultValuedPathParameterLong() throws Exception {
    final class Test extends DefaultValuedPathParameterEndpoint<Long> {}

    try {
      g.generateConfig(Test.class);
      fail("Config generation for endpoint with default-valued path parameter should have failed.");
    } catch (InvalidParameterAnnotationsException expected) {
      // expected
    }
  }

  @Test
  public void testDefaultValuedPathParameterString() throws Exception {
    final class Test extends DefaultValuedPathParameterEndpoint<String> {}

    try {
      g.generateConfig(Test.class);
      fail("Config generation for endpoint with default-valued path parameter should have failed.");
    } catch (InvalidParameterAnnotationsException expected) {
      // expected
    }
  }

  @Api
  private static class DefaultValuedEndpoint<T> {
    @SuppressWarnings("unused")
    public void foo(T id) {}
  }

  @Test
  public void testValidDefaultValuedParameterBoolean() throws Exception {
    final class Test extends DefaultValuedEndpoint<Boolean> {
      @Override public void foo(@Named("id") @DefaultValue("true") Boolean id) {}
    }
    assertEquals(true, implValidTestDefaultValuedParameter(Test.class).asBoolean());
  }

  @Test
  public void testValidDefaultValuedParameterInteger() throws Exception {
    final class Test extends DefaultValuedEndpoint<Integer> {
      @Override public void foo(@Named("id") @DefaultValue("2718") Integer id) {}
    }
    assertEquals(2718, implValidTestDefaultValuedParameter(Test.class).asInt());
  }

  @Test
  public void testValidDefaultValuedParameterLong() throws Exception {
    final class Test extends DefaultValuedEndpoint<Long> {
      @Override public void foo(@Named("id") @DefaultValue("3141") Long id) {}
    }
    assertEquals(3141L, implValidTestDefaultValuedParameter(Test.class).asLong());
  }

  @Test
  public void testValidDefaultValuedParameterString() throws Exception {
    final class Test extends DefaultValuedEndpoint<String> {
      @Override public void foo(@Named("id") @DefaultValue("bar") String id) {}
    }
    assertEquals("bar", implValidTestDefaultValuedParameter(Test.class).asText());
  }

  private <T> JsonNode implValidTestDefaultValuedParameter(
      Class<? extends DefaultValuedEndpoint<T>> clazz) throws Exception {
    String apiConfigSource = g.generateConfig(clazz).get("myapi-v1.api");

    JsonNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    JsonNode methodFoo = root.path("methods").path("myapi.test.foo");
    JsonNode parameters = methodFoo.path("request").path("parameters");
    JsonNode parameter = parameters.path("id");
    JsonNode defaultValue = parameter.path("default");

    assertFalse(parameter.path("required").isMissingNode());
    assertEquals(false, parameter.path("required").asBoolean());
    assertFalse(defaultValue.isMissingNode());

    return defaultValue;
  }

  @Test
  public void testInvalidDefaultValuedParameterBoolean() throws Exception {
    implInvalidTestDefaultValuedParameter(new DefaultValuedEndpoint<Boolean>() {
      @Override public void foo(@Named("id") @DefaultValue("bar") Boolean id) {}
    }.getClass());
  }

  @Test
  public void testInvalidDefaultValuedParameterInteger() throws Exception {
    implInvalidTestDefaultValuedParameter(new DefaultValuedEndpoint<Integer>() {
      @Override public void foo(@Named("id") @DefaultValue("bar") Integer id) {}
    }.getClass());
  }

  @Test
  public void testInvalidDefaultValuedParameterLong() throws Exception {
    implInvalidTestDefaultValuedParameter(new DefaultValuedEndpoint<Long>() {
      @Override public void foo(@Named("id") @DefaultValue("bar") Long id) {}
    }.getClass());
  }

  private <T> void implInvalidTestDefaultValuedParameter(
      Class<? extends DefaultValuedEndpoint<T>> clazz) throws Exception {
    try {
      g.generateConfig(clazz);
      fail("Config generation for endpoint with bad default for given type should have failed.");
    } catch (IllegalArgumentException expected) {
      // expected
    }
  }

  @Api
  class SameRestPathDifferentType<T1, T2> {
    @SuppressWarnings("unused")
    public List<Bar> list(@Named("id") T1 id) { return null; }
    @SuppressWarnings("unused")
    public Bar get(@Named("id") T2 id) { return null; }
  }

  @Test
  public void testSameRestPathStringString() throws Exception {
    implSameRestPathDifferentTypeTest(
        new SameRestPathDifferentType<String, String>() {}.getClass());
  }

  @Test
  public void testSameRestPathIntegerLong() throws Exception {
    implSameRestPathDifferentTypeTest(
        new SameRestPathDifferentType<Long, String>() {}.getClass());
  }

  @Test
  public void testSameRestPathIntegerString() throws Exception {
    implSameRestPathDifferentTypeTest(
        new SameRestPathDifferentType<Integer, String>() {}.getClass());
  }

  @Test
  public void testSameRestPathIntegerBoolean() throws Exception {
    implSameRestPathDifferentTypeTest(
        new SameRestPathDifferentType<Integer, Boolean>() {}.getClass());
  }

  private <T1, T2> void implSameRestPathDifferentTypeTest(
      Class<? extends SameRestPathDifferentType<T1, T2>> clazz) throws Exception {
    try {
      g.generateConfig(clazz).get("myapi-v1.api");
      fail("Multiple methods with same RESTful signature");
    } catch (DuplicateRestPathException expected) {
      // expected
    }
  }

  /**
   * Test if the generated API has the expected method paths. First try this out without any
   * explicit paths.
   */
  public void testMethodPaths_repeatRestPath() throws Exception {
    @SuppressWarnings("unused")
    @Api
    class Dates {
      @ApiMethod(httpMethod = "GET", path = "dates/{id1}/{id2}")
      public Date get(@Named("id1") String id1, @Named("id2") String id2) { return null; }

      @ApiMethod(httpMethod = "GET", path = "dates/{x}/{y}")
      public List<Date> listFromXY(@Named("x") String x, @Named("y") String y) { return null; }
    }
    try {
      g.generateConfig(Dates.class).get("myapi-v1.api");
      fail("Multiple methods with same RESTful signature");
    } catch (DuplicateRestPathException expected) {
      // expected
    }
  }

  @Test
  public void testMethodPaths_apiAtCustomPath() throws Exception {
    @SuppressWarnings("unused")
    @Api(resource = "foos")
    class Foos {
      public List<Foo> list() { return null; }
      public Foo get(@Named("id") Long id) { return null; }
    }
    String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    verifyMethodPathAndHttpMethod(root, "myapi.foos.list", "foos", "GET");
    verifyMethodPathAndHttpMethod(root, "myapi.foos.get", "foos/{id}", "GET");
  }

  @Test
  public void testMethodPaths_apiAndMethodsAtCustomPaths() throws Exception {
    @Api(resource = "foo")
    class AcmeCo {
      @ApiMethod(path = "foos")
      public List<Foo> list() { return null; }
      @SuppressWarnings("unused")
      public List<Foo> listAllTheThings() { return null; }
      @ApiMethod(path = "give")
      public Foo giveMeOne() { return null; }
    }
    String apiConfigSource = g.generateConfig(AcmeCo.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    verifyMethodPathAndHttpMethod(root, "myapi.foo.list", "foos", "GET");
    verifyMethodPathAndHttpMethod(root, "myapi.foo.listAllTheThings", "foo", "GET");
    verifyMethodPathAndHttpMethod(root, "myapi.foo.giveMeOne", "give", "POST");
  }

  private static void verifyMethodPathAndHttpMethod(
      JsonNode root, String methodName, String expectedPath, String expectedHttpMethod) {
    JsonNode methodNode = root.path("methods").path(methodName);
    assertEquals(expectedPath, methodNode.path("path").asText());
    assertEquals(expectedHttpMethod, methodNode.path("httpMethod").asText());
  }

  /**
   * A class for testing inheritance in Endpoints.
   */
  abstract static class GenFoos<T> {
    public List<T> list() {
      return null;
    }
    public T get(@Named("id") long id) {
      return null;
    }
    public T insert(T object) {
      return null;
    }
    public T update(T updated) {
      return null;
    }
    public void remove(@Named("id") long id) {
      // empty
    }

    private static void verifyMethodPathsAndHttpMethods(
        Class<? extends GenFoos<?>> serviceClass, String path, JsonNode root, String resource) {
      String classPart = resource != null ? resource : serviceClass.getSimpleName();
      String servicePrefix = ApiMethodConfig.methodNameFormatter("myapi." + classPart);
      verifyMethodPathAndHttpMethod(root, servicePrefix + ".insert", path, "POST");
      verifyMethodPathAndHttpMethod(root, servicePrefix + ".update", path, "PUT");
      verifyMethodPathAndHttpMethod(root, servicePrefix + ".list", path, "GET");
      verifyMethodPathAndHttpMethod(root, servicePrefix + ".remove", path + "/{id}", "DELETE");
      verifyMethodPathAndHttpMethod(root, servicePrefix + ".get", path + "/{id}", "GET");
    }
  }

  /**
   * A class for testing inheritance in Endpoints with resource specified.
   */
  @Api(resource = "bars")
  abstract static class GenBars<T> extends GenFoos<T> {
    private static void verifyMethodPathsAndHttpMethods(
        Class<? extends GenBars<?>> serviceClass, JsonNode root) {
      GenFoos.verifyMethodPathsAndHttpMethods(serviceClass, "bars", root, "bars");
    }
  }

  static class Foo {}

  @Test
  public void testMethodPaths_inheritanceResourceSpecified() throws Exception {
    @Api(resource = "foos")
    class Foos extends GenFoos<Foo> {}
    String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    GenFoos.verifyMethodPathsAndHttpMethods(Foos.class, "foos", root, "foos");
  }

  @Test
  public void testMethodPaths_inheritanceResourceInheritedAndSpecified() throws Exception {
    @Api(resource = "foos")
    class Foos extends GenBars<Foo> {}
    String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    GenFoos.verifyMethodPathsAndHttpMethods(Foos.class, "foos", root, "foos");
  }

  @Test
  public void testMethodPaths_inheritanceResourceUnspecified() throws Exception {
    @Api
    class Foos extends GenFoos<Foo> {}
    String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    verifyMethodPathAndHttpMethod(root, "myapi.foos.insert", "foo", "POST");
    verifyMethodPathAndHttpMethod(root, "myapi.foos.update", "foo", "PUT");
    verifyMethodPathAndHttpMethod(root, "myapi.foos.list", "foo", "GET");
    verifyMethodPathAndHttpMethod(root, "myapi.foos.remove", "remove/{id}", "DELETE");
    verifyMethodPathAndHttpMethod(root, "myapi.foos.get", "foo/{id}", "GET");
  }

  @Test
  public void testMethodPaths_inheritanceResourceInheritedAndUnspecified() throws Exception {
    @Api
    class Foos extends GenBars<Foos> {}
    String apiConfigSource = g.generateConfig(Foos.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    GenBars.verifyMethodPathsAndHttpMethods(Foos.class, root);
  }

  @Test
  public void testMultipleGenericClasses() throws Exception {
    @Api
    class GenFoo extends GenBars<Foo> {}
    @Api
    @ApiClass(resource = "resource2")
    class GenBar extends GenBars<Bar> {}
    String apiConfigSource = g.generateConfig(GenFoo.class, GenBar.class).get("myapi-v1.api");
  }

  @Api
  private static class SimpleFoo<R> {
    @SuppressWarnings("unused")
    public void foo(R param) {}
  }

  @Test
  public void testRequestDoesContainMap() throws Exception {
    checkRequestIsNotEmpty(new SimpleFoo<Map<String, User>>() {}.getClass());
  }

  @Test
  public void testRequestDoesNotContainUser() throws Exception {
    checkRequestIsEmpty(new SimpleFoo<User>() {}.getClass());
  }

  @Test
  public void testRequestDoesNotContainHttpServletRequest() throws Exception {
    checkRequestIsEmpty(new SimpleFoo<HttpServletRequest>() {}.getClass());
  }

  @Test
  public void testRequestDoesNotContainServletContext() throws Exception {
    checkRequestIsEmpty(new SimpleFoo<ServletContext>() {}.getClass());
  }

  private void checkRequestIsEmpty(Class<? extends SimpleFoo<?>> clazz) throws Exception {
    checkRequest(clazz, true);
  }

  private void checkRequestIsNotEmpty(Class<? extends SimpleFoo<?>> clazz) throws Exception {
    checkRequest(clazz, false);
  }

  private void checkRequest(Class<? extends SimpleFoo<?>> clazz, boolean isEmpty) throws Exception {
    String apiConfigSource = g.generateConfig(clazz).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    String test = clazz.getName() + ".foo";
    JsonNode methodFooRequest =
        root.path("descriptor").path("methods").path(clazz.getName() + ".foo").path("request");
    assertEquals(isEmpty, methodFooRequest.isMissingNode());
  }

  @Api(transformers = {SerializerTestConvertedBeanFromApiSerializer.class})
  private static class SerializerTestEndpoint {
    @SuppressWarnings("unused")
    public SerializerTestBean getFoo() {
      return null;
    }

    @SuppressWarnings("unused")
    public SerializerTestOverridingPropertyBean getBar() {
      return null;
    }

    @SuppressWarnings("unused")
    public SerializerTestConvertedBeanFromApi getBaz() {
      return null;
    }

    @SuppressWarnings("unused")
    public void testMethodRequestBody(SerializerTestConvertedBean body) {
    }
  }

  @Api
  private static class BadMethodRequestEndpoint {
    @SuppressWarnings("unused")
    public void testBadMethodRequestBody(StringBean body) {
    }
  }

  private static class SerializerTestBean {
    @ApiResourceProperty(name = "baz")
    public String getFoo() {
      return null;
    }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public String getBar() {
      return null;
    }

    @SuppressWarnings("unused")
    public SerializerTestConvertedBean getConverted() {
      return null;
    }
  }

  @ApiTransformer(SerializerTestConvertedBeanSerializer.class)
  private static class SerializerTestConvertedBean {
    @SuppressWarnings("unused")
    public Integer getFoo() {
      return null;
    }
  }

  private static class SerializerTestConvertedBeanFromApi {
    @SuppressWarnings("unused")
    public Integer getFoo() {
      return null;
    }
  }

  private static class SerializerTestConvertedToBean {
    @SuppressWarnings("unused")
    public String getFoo() {
      return null;
    }
  }

  private static class SerializerTestConvertedBeanSerializer
      extends DefaultValueSerializer<SerializerTestConvertedBean, SerializerTestConvertedToBean> {
  }

  private static class SerializerTestConvertedBeanFromApiSerializer extends
      DefaultValueSerializer<SerializerTestConvertedBeanFromApi, SerializerTestConvertedToBean> {
  }

  private static class SerializerTestOverridingPropertyBean {
    @ApiResourceProperty(name = "bar")
    public String getFoo() {
      return null;
    }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public Integer getBar() {
      return null;
    }
  }

  @ApiTransformer(StringBeanSerializer.class)
  private static class StringBean {
    @SuppressWarnings("unused")
    public String getFoo() {
      return null;
    }
  }

  private static class StringBeanSerializer extends DefaultValueSerializer<StringBean, String> {
  }

  @Test
  public void testCustomSerialization_renamedProperty() throws Exception {
    String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode schemas = root.path("descriptor").path("schemas");
    verifyObjectPropertySchema(schemas.path("SerializerTestBean"), "baz", "string");
  }

  @Test
  public void testCustomSerialization_ignoredProperty() throws Exception {
    String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode schemas = root.path("descriptor").path("schemas");
    assertTrue(schemas.path("SerializerTestBean").path("properties").path("bar").isMissingNode());
  }

  @Test
  public void testCustomSerialization_customizedProperty() throws Exception {
    String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode schemas = root.path("descriptor").path("schemas");
    verifyObjectPropertyRef(
        schemas.path("SerializerTestBean"), "converted", "SerializerTestConvertedToBean");
  }

  @Test
  public void testCustomSerialization_customizedPropertyFromApi() throws Exception {
    String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    verifyMethodResponseRef(
        root, SerializerTestEndpoint.class.getName() + ".getBaz", "SerializerTestConvertedToBean");
  }

  @Test
  public void testCustomSerialization_overridenProperty() throws Exception {
    String apiConfigSource = g.generateConfig(SerializerTestEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode schemas = root.path("descriptor").path("schemas");
    verifyObjectPropertySchema(
        schemas.path("SerializerTestOverridingPropertyBean"), "bar", "string");
  }

  @Test
  public void testCustomSerialization_requestBody() throws Exception {
    Class<?> clazz = SerializerTestEndpoint.class;
    String apiConfigSource = g.generateConfig(clazz).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    verifyMethodRequestRef(
        root, clazz.getName() + ".testMethodRequestBody", "SerializerTestConvertedToBean");
  }

  @Test
  public void testCustomSerialization_badRequestBodyAfterSerialization() throws Exception {
    try {
      g.generateConfig(BadMethodRequestEndpoint.class).get("myapi-v1.api");
      fail("Type serialized to string should not be usable as a method request body");
    } catch (MissingParameterNameException e) {
      // expected
    }
  }

  @Api
  private static class EmptyRequestEndpoint {
    @SuppressWarnings("unused")
    public void foo() {
    }
    @SuppressWarnings("unused")
    public Void bar() {
      return null;
    }
  }

  @Test
  public void testEmptyRequestBody_void() throws Exception {
    testEmpty("foo", "request");
  }

  @Test
  public void testEmptyResponseBody_void() throws Exception {
    testEmpty("foo", "response");
  }

  @Test
  public void testEmptyRequestBody_Void() throws Exception {
    testEmpty("bar", "request");
  }

  @Test
  public void testEmptyResponseBody_Void() throws Exception {
    testEmpty("bar", "response");
  }

  private void testEmpty(String methodName, String requestOrResponse) throws Exception {
    String apiConfigSource = g.generateConfig(EmptyRequestEndpoint.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode fooNode = root.path("descriptor").path("methods").path(
        EmptyRequestEndpoint.class.getName() + "." + methodName);
    assertFalse(fooNode.isMissingNode());
    assertTrue(fooNode.path(requestOrResponse).isMissingNode());
  }

  @Api
  private static class StringRequestEndpoint {
    @SuppressWarnings("unused")
    public void foo(String body) {
    }
  }

  @Api
  private static class StringResponseEndpoint {
    @SuppressWarnings("unused")
    public String foo() {
      return null;
    }
  }

  @Test
  public void testStringRequestThrowsException() throws Exception {
    try {
      String apiConfigSource = g.generateConfig(StringRequestEndpoint.class).get("myapi-v1.api");
      fail("String request body didn't fail in config generation");
    } catch (MissingParameterNameException e) {
      // expected
    }
  }

  @Test
  public void testStringResponseThrowsException() throws Exception {
    try {
      String apiConfigSource = g.generateConfig(StringResponseEndpoint.class).get("myapi-v1.api");
      fail("String response body didn't fail in config generation");
    } catch (InvalidReturnTypeException e) {
      // expected
    }
  }

  @Test
  public void testNamespaceParameters() throws Exception {
    @Api
    class DefaultApi {}
    doTestNamespaceParameters(DefaultApi.class, null, null, null);

    @Api(namespace = @ApiNamespace(ownerDomain = "ownerdomain.com.br", ownerName = ""))
    class OwnedApi {}
    try {
      doTestNamespaceParameters(OwnedApi.class, "ownerdomain.com.br", null, null);
      fail("Forgot to specify required owner name");
    } catch (InvalidNamespaceException expected) {
      // expected
    }

    @Api(namespace = @ApiNamespace(ownerName = "Owner Name", ownerDomain = ""))
    class NamedApi {}
    try {
      doTestNamespaceParameters(NamedApi.class, null, "Owner Name", null);
      fail("Forgot to specify required owner domain");
    } catch (InvalidNamespaceException expected) {
      // expected
    }

    @Api(namespace = @ApiNamespace(packagePath = "Package path", ownerDomain = "", ownerName = ""))
    class PackagedApi {}
    try {
      doTestNamespaceParameters(PackagedApi.class, null, null, "Package path");
      fail("Forgot to specify both owner domain and owner name");
    } catch (InvalidNamespaceException expected) {
      // expected
    }

    @Api(namespace = @ApiNamespace(ownerDomain = "ownerdomain.com.br", ownerName = "Owner Name"))
    class PartiallyNamespacedApi {}
    doTestNamespaceParameters(
        PartiallyNamespacedApi.class, "ownerdomain.com.br", "Owner Name", null);

    @Api(namespace = @ApiNamespace(ownerDomain = "ownerdomain.com.br", ownerName = "Owner Name",
        packagePath = "Package path"))
    class FullyNamespacedApi {}
    doTestNamespaceParameters(
        FullyNamespacedApi.class, "ownerdomain.com.br", "Owner Name", "Package path");
  }

  private void doTestNamespaceParameters(
      Class<?> clazz, String ownerDomain, String ownerName, String packagePath) throws Exception {
    String apiConfigSource = g.generateConfig(clazz).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);

    if (ownerDomain != null) {
      assertFalse(root.path("ownerDomain").isMissingNode());
      assertEquals(ownerDomain, root.get("ownerDomain").asText());
    }
    if (ownerName != null) {
      assertFalse(root.path("ownerName").isMissingNode());
      assertEquals(ownerName, root.get("ownerName").asText());
    }
    if (packagePath != null) {
      assertFalse(root.path("packagePath").isMissingNode());
      assertEquals(packagePath, root.get("packagePath").asText());
    }
  }

  @Test
  public void testArrayRequest() throws Exception {
    @Api class StringArray {
      @SuppressWarnings("unused")
      public void foo(@Named("collection") String[] s) {}
    }
    String apiConfigSource = g.generateConfig(StringArray.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode methodNode = root.path("methods").path("myapi.stringArray.foo");
    assertFalse(methodNode.isMissingNode());
    verifyMethodRequestParameter(methodNode.get("request"), "collection", "string", true, true);
  }

  @Test
  public void testGenericArrayRequest() throws Exception {
    @Api abstract class GenericArray<T> {
      @SuppressWarnings("unused")
      public void foo(@Named("collection") T[] t) {}
    }
    // TODO: remove with JDK8, dummy to force inclusion of GenericArray to InnerClass attribute
    // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=2210448
    GenericArray<Integer> dummy = new GenericArray<Integer>() {};
    class Int32Array extends GenericArray<Integer> {}
    String apiConfigSource = g.generateConfig(Int32Array.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode methodNode = root.path("methods").path("myapi.int32Array.foo");
    assertFalse(methodNode.isMissingNode());
    verifyMethodRequestParameter(methodNode.get("request"), "collection", "int32", true, true);
  }

  @Test
  public void testCollectionRequest() throws Exception {
    @Api class BooleanCollection {
      @SuppressWarnings("unused")
      public void foo(@Named("collection") Collection<Boolean> b) {}
    }
    String apiConfigSource = g.generateConfig(BooleanCollection.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode methodNode = root.path("methods").path("myapi.booleanCollection.foo");
    assertFalse(methodNode.isMissingNode());
    verifyMethodRequestParameter(methodNode.get("request"), "collection", "boolean", true, true);
  }

  @Test
  public void testGenericCollectionRequest() throws Exception {
    @Api
    abstract class GenericCollection<T> {
      @SuppressWarnings("unused")
      public void foo(@Named("collection") Collection<T> t) {}
    }
    // TODO: remove with JDK8, dummy to force inclusion of GenericArray to InnerClass attribute
    // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=2210448
    GenericCollection<Long> dummy = new GenericCollection<Long>() {};
    class Int64Collection extends GenericCollection<Long> {}
    String apiConfigSource = g.generateConfig(Int64Collection.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode methodNode = root.path("methods").path("myapi.int64Collection.foo");
    assertFalse(methodNode.isMissingNode());
    verifyMethodRequestParameter(methodNode.get("request"), "collection", "int64", true, true);
  }

  @Test
  public void testComplexTypeArrayRequest() throws Exception {
    @Api class ComplexArray {
      @SuppressWarnings("unused")
      public void foo(@Named("collection") Bean[] beans) {}
    }
    try {
      g.generateConfig(ComplexArray.class).get("myapi-v1.api");
      fail("Non-primitive array should have failed");
    } catch (CollectionResourceException expected) {
      // expected
    }
  }

  @Test
  public void testComplexTypeCollectionRequest() throws Exception {
    @Api class ComplexCollection {
      @SuppressWarnings("unused")
      public void foo(@Named("collection") Collection<Object> dates){}
    }
    try {
      g.generateConfig(ComplexCollection.class).get("myapi-v1.api");
      fail("Non-primitive array should have failed");
    } catch (CollectionResourceException expected) {
      // expected
    }
  }

  @Test
  public void testUnnamedTypeArrayRequest() throws Exception {
    @Api class UnnamedArray {
      @SuppressWarnings("unused")
      public void foo(Long[] dates) {}
    }
    try {
      g.generateConfig(UnnamedArray.class).get("myapi-v1.api");
      fail("Non-primitive array should have failed");
    } catch (MissingParameterNameException expected) {
      // expected
    }
  }

  @Test
  public void testUnnamedTypeCollectionRequest() throws Exception {
    @Api
    class UnnamedCollection {
      @SuppressWarnings("unused")
      public void foo(Collection<String> dates){}
    }
    try {
      g.generateConfig(UnnamedCollection.class).get("myapi-v1.api");
      fail("Non-primitive array should have failed");
    } catch (MissingParameterNameException expected) {
      // expected
    }
  }

  @Test
  public void testMultipleCollectionRequests() throws Exception {
    @Api
    class MultipleCollections {
      @SuppressWarnings("unused")
      public void foo(@Named("ids") Long[] ids, @Named("authors") List<String> authors) {}
    }
    String apiConfigSource = g.generateConfig(MultipleCollections.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode methodNode = root.path("methods").path("myapi.multipleCollections.foo");
    assertFalse(methodNode.isMissingNode());
    verifyMethodRequestParameter(methodNode.get("request"), "ids", "int64", true, true);
    verifyMethodRequestParameter(methodNode.get("request"), "authors", "string", true, true);
  }

  @Test
  public void testResourceAndCollection() throws Exception {
    @Api
    class ResourceAndCollection {
      @SuppressWarnings("unused")
      public void foo(Bean resource, @Named("authors") List<String> authors) {}
    }

    String apiConfigSource = g.generateConfig(ResourceAndCollection.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode methodNode = root.path("methods").path("myapi.resourceAndCollection.foo");
    assertFalse(methodNode.isMissingNode());
    verifyMethodRequestParameter(methodNode.get("request"), "authors", "string", true, true);
    assertEquals(1, methodNode.path("request").path("parameters").size());
    verifyMethodRequestRef(root, ResourceAndCollection.class.getName() + ".foo", "Bean");
  }

  @Test
  public void testCollectionOfArrays() throws Exception {
    @Api
    class NestedCollections {
      @SuppressWarnings("unused")
      public void foo(@Named("authors") List<String[]> authors) {}
    }
    try {
      g.generateConfig(NestedCollections.class).get("myapi-v1.api");
      fail("Nested collections should fail");
    } catch (NestedCollectionException expected) {
      // expected
    }
  }

  @Test
  public void testArraysOfCollections() throws Exception {
    @Api
    class NestedCollections {
      @SuppressWarnings("unused")
      public void foo(@Named("authors") List<String>[] authors) {}
    }
    try {
      g.generateConfig(NestedCollections.class).get("myapi-v1.api");
      fail("Nested collections should fail");
    } catch (NestedCollectionException expected) {
      // expected
    }
  }

  @Test
  public void testCollectionOfCollections() throws Exception {
    @Api
    class NestedCollections {
      @SuppressWarnings("unused")
      public void foo(@Named("authors") List<List<String>> authors) {}
    }
    try {
      g.generateConfig(NestedCollections.class).get("myapi-v1.api");
      fail("Nested collections should fail");
    } catch (NestedCollectionException expected) {
      // expected
    }
  }

  @Test
  public void testArrayOfArrays() throws Exception {
    @Api
    class NestedCollections {
      @SuppressWarnings("unused")
      public void foo(@Named("authors") String[][] authors) {}
    }
    try {
      g.generateConfig(NestedCollections.class).get("myapi-v1.api");
      fail("Nested collections should fail");
    } catch (NestedCollectionException expected) {
      // expected
    }
  }

  @Test
  public void testArrayOfResources() throws Exception {
    @Api
    class ResourceCollection {
      @SuppressWarnings("unused")
      public void foo(List<Bean> beans) {}
    }
    try {
      g.generateConfig(ResourceCollection.class).get("myapi-v1.api");
      fail("Array of resources should fail");
    } catch (CollectionResourceException expected) {
      // expected
    }
  }

  @Test
  public void testArrayOfSerializedResources() throws Exception {
    @Api
    class ResourceCollection {
      @SuppressWarnings("unused")
      public void foo(@Named("beans") List<StringBean> beans) {}
    }
    String apiConfigSource = g.generateConfig(ResourceCollection.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    JsonNode methodNode = root.path("methods").path("myapi.resourceCollection.foo");
    assertFalse(methodNode.isMissingNode());
    verifyMethodRequestParameter(methodNode.get("request"), "beans", "string", true, true);
  }

  @Test
  public void testCanonicalName() throws Exception {
    @Api(name = "myapi", canonicalName = "My API")
    class CanonicalApi {
    }
    String apiConfigSource = g.generateConfig(CanonicalApi.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    assertEquals("myapi", root.path("name").asText());
    assertEquals("My API", root.path("canonicalName").asText());
  }

  @Test
  public void testTitle() throws Exception {
    @Api(name = "myapi", title = "My API Title")
    class ApiWithTitle {
    }
    String apiConfigSource = g.generateConfig(ApiWithTitle.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    assertEquals("myapi", root.path("name").asText());
    assertEquals("My API Title", root.path("title").asText());
  }

  @Test
  public void testDocumentationLink() throws Exception {
    @Api(name = "myapi", documentationLink = "http://go/documentation")
    class ApiWithDocs {
    }
    String apiConfigSource = g.generateConfig(ApiWithDocs.class).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    assertEquals("myapi", root.path("name").asText());
    assertEquals("http://go/documentation", root.path("documentation").asText());
  }

  @Test
  public void testGenericParameterTypes() throws Exception {
    @Api
    final class Test <T> {
      @SuppressWarnings("unused")
      public void setT(T t) {}
    }

    try {
      g.generateConfig(Test.class).get("myapi-v1.api");
      fail();
    } catch (GenericTypeException e) {
      // Expected.
    }
  }

  @Test
  public void testGenericParameterTypeThroughMethodCall() throws Exception {
    this.<Integer>genericParameterTypeTestImpl();
  }

  @Test
  public void testConsistentApiWideConfig() throws Exception {
    @Api(scopes = { "scopes" })
    @ApiClass(scopes = { "foo" })
    final class Test1 {}

    @Api(scopes = { "scopes" })
    @ApiClass(scopes = { "bar" })
    final class Test2 {}

    g.generateConfig(Test1.class, Test2.class);
  }

  @Test
  public void testInconsistentApiWideConfig() throws Exception {
    @Api(scopes = { "foo" })
    @ApiClass(scopes = { "scopes" })
    final class Test1 {}

    @Api(scopes = { "bar" })
    @ApiClass(scopes = { "scopes" })
    final class Test2 {}

    try {
      g.generateConfig(Test1.class, Test2.class);
      fail();
    } catch (InconsistentApiConfigurationException e) {
      // Expected exception.
    }
  }

  private <T> void genericParameterTypeTestImpl() throws Exception {
    @Api
    class Bar <T1> {
      @SuppressWarnings("unused")
      public void bar(T1 t1) {}
    }
    class Foo extends Bar<T> {}

    try {
      g.generateConfig(Foo.class).get("myapi-v1.api");
      fail();
    } catch (GenericTypeException e) {
      // Expected.
    }
  }

  @Test
  public void testAnonymousClass() throws Exception {
    @Api() 
    class Test {
      @SuppressWarnings("unused")
      public void test() {
      }
    }
    String apiConfigSource = g.generateConfig(new Test() {}.getClass()).get("myapi-v1.api");
    ObjectNode root = objectMapper.readValue(apiConfigSource, ObjectNode.class);
    assertEquals("test", root.path("methods").path("myapi.test").path("path").asText());
  }

  @Test
  public void testGenerateConfigRetainsOrder() throws Exception {
    @Api(name = "onetoday", description = "OneToday API")
    final class OneToday {
    }
    @Api(name = "onetodayadmin", description = "One Today Admin API")
    final class OneTodayAdmin {
    }
    Map<String, String> configs = g.generateConfig(OneToday.class, OneTodayAdmin.class);
    Iterator<String> iterator = configs.keySet().iterator();
    assertEquals("onetoday-v1.api", iterator.next());
    assertEquals("onetodayadmin-v1.api", iterator.next());
  }
}