/*
 * Copyright (c) 2011-2014 The original author or authors
 * ------------------------------------------------------
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution.
 *
 *     The Eclipse Public License is available at
 *     http://www.eclipse.org/legal/epl-v10.html
 *
 *     The Apache License v2.0 is available at
 *     http://www.opensource.org/licenses/apache2.0.php
 *
 * You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.ext.web;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.http.*;
import org.junit.Test;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * @author <a href="http://tfox.org">Tim Fox</a>
 */
public class RouterTest extends WebTestBase {

  @Test
  public void testSimpleRoute() throws Exception {
    router.route().handler(rc -> rc.response().end());
    testRequest(HttpMethod.GET, "/", 200, "OK");
  }

  @Test
  public void testInvalidPath() throws Exception {
    try {
      router.route("blah");
      fail();
    } catch (IllegalArgumentException e) {
      // OK
    }
    try {
      router.route().path("blah");
      fail();
    } catch (IllegalArgumentException e) {
      // OK
    }
  }

  @Test
  public void testRouteGetPath() throws Exception {
    assertEquals("/foo", router.route("/foo").getPath());
    assertEquals("/foo/:id", router.route("/foo/:id").getPath());
  }

  @Test
  public void testRouteGetPathWithParamsInHandler() throws Exception {
    router.route("/foo/:id").handler(rc -> {
      assertEquals("/foo/123", rc.normalizedPath());
      rc.response().end();
    });
    testRequest(HttpMethod.GET, "/foo/123", 200, "OK");
  }

  @Test
  public void testRoutePathAndMethod() throws Exception {
    for (HttpMethod meth : METHODS) {
      testRoutePathAndMethod(meth, true);
    }
  }

  @Test
  public void testRoutePathAndMethodBegin() throws Exception {
    for (HttpMethod meth : METHODS) {
      testRoutePathAndMethod(meth, false);
    }
  }

  private void testRoutePathAndMethod(HttpMethod method, boolean exact) throws Exception {
    String path = "/blah";
    router.clear();
    router.route(method, exact ? path : path + "*").handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    if (exact) {
      testPathExact(method, path);
    } else {
      testPathBegin(method, path);
    }
    for (HttpMethod meth : METHODS) {
      if (meth != method) {
        testRequest(meth, path, HttpResponseStatus.METHOD_NOT_ALLOWED);
      }
    }
  }

  @Test
  public void testRoutePathOnly() throws Exception {
    String path1 = "/blah";
    router.route(path1).handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    String path2 = "/quux";
    router.route(path2).handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());

    testPathExact(path1);
    testPathExact(path2);
  }

  @Test
  public void testRoutePathOnlyBegin() throws Exception {
    String path1 = "/blah";
    router.route(path1 + "*").handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    String path2 = "/quux";
    router.route(path2 + "*").handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());

    testPathBegin(path1);
    testPathBegin(path2);
  }

  @Test
  public void testRoutePathWithTrailingSlashOnlyBegin() throws Exception {
    String path = "/some/path/";
    router.route(path + "*").handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    testPathBegin(path);
  }

  @Test
  public void testRoutePathBuilder() throws Exception {
    String path = "/blah";
    router.route().path(path).handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    testPathExact(path);
  }

  @Test
  public void testRoutePathBuilderBegin() throws Exception {
    String path = "/blah";
    router.route().path(path + "*").handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    testPathBegin(path);
  }

  @Test
  public void testRoutePathAndMethodBuilder() throws Exception {
    String path = "/blah";
    router.route().path(path).method(HttpMethod.GET).handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    testPathExact(HttpMethod.GET, path);
    testRequest(HttpMethod.POST, path, HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testRoutePathAndMethodBuilderBegin() throws Exception {
    String path = "/blah";
    router.route().path(path + "*").method(HttpMethod.GET).handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    testPathBegin(HttpMethod.GET, path);
    testRequest(HttpMethod.POST, path, HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testRoutePathAndMultipleMethodBuilder() throws Exception {
    String path = "/blah";
    router.route().path(path).method(HttpMethod.GET).method(HttpMethod.POST).handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    testPathExact(HttpMethod.GET, path);
    testPathExact(HttpMethod.POST, path);
    testRequest(HttpMethod.PUT, path, HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testRoutePathAndMultipleMethodBuilderBegin() throws Exception {
    String path = "/blah";
    router.route().path(path + "*").method(HttpMethod.GET).method(HttpMethod.POST).handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    testPathBegin(HttpMethod.GET, path);
    testPathBegin(HttpMethod.POST, path);
    testRequest(HttpMethod.PUT, path, HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  private void testPathBegin(String path) throws Exception {
    for (HttpMethod meth : METHODS) {
      testPathBegin(meth, path);
    }
  }

  private void testPathExact(String path) throws Exception {
    for (HttpMethod meth : METHODS) {
      testPathExact(meth, path);
    }
  }

  private void testPathBegin(HttpMethod method, String path) throws Exception {
    testRequest(method, path, 200, path);
    testRequest(method, path + "wibble", 200, path + "wibble");
    if (path.endsWith("/")) {
      testRequest(method, path.substring(0, path.length() - 1) + "wibble", 404, "Not Found");
      testRequest(method, path.substring(0, path.length() - 1) + "/wibble", 200, path.substring(0, path.length() - 1) + "/wibble");
    } else {
      testRequest(method, path + "/wibble", 200, path + "/wibble");
      testRequest(method, path + "/wibble/floob", 200, path + "/wibble/floob");
      testRequest(method, path.substring(0, path.length() - 1), 404, "Not Found");
    }
    testRequest(method, "/", 404, "Not Found");
    testRequest(method, "/" + UUID.randomUUID().toString(), 404, "Not Found");
  }

  private void testPathExact(HttpMethod method, String path) throws Exception {
    testRequest(method, path, 200, path);
    testRequest(method, path + "wibble", 404, "Not Found");
    testRequest(method, path + "/wibble", 404, "Not Found");
    testRequest(method, path + "/wibble/floob", 404, "Not Found");
    testRequest(method, path.substring(0, path.length() - 1), 404, "Not Found");
    testRequest(method, "/", 404, "Not Found");
    testRequest(method, "/" + UUID.randomUUID().toString(), 404, "Not Found");
  }

  @Test
  public void testRouteNoPath() throws Exception {
    router.route().handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    for (HttpMethod meth : METHODS) {
      testNoPath(meth);
    }
  }

  @Test
  public void testRouteNoPath2() throws Exception {
    router.route().handler(rc -> {
      rc.response().setStatusMessage(rc.request().path());
      rc.next();
    });
    router.route().handler(rc -> rc.response().setStatusCode(200).end());
    for (HttpMethod meth : METHODS) {
      testNoPath(meth);
    }
  }

  @Test
  public void testRouteNoPathWithMethod() throws Exception {
    for (HttpMethod meth : METHODS) {
      testRouteNoPathWithMethod(meth);
    }
  }

  private void testRouteNoPathWithMethod(HttpMethod meth) throws Exception {
    router.clear();
    router.route().method(meth).handler(rc -> rc.response().setStatusCode(200).setStatusMessage(rc.request().path()).end());
    testNoPath(meth);
    for (HttpMethod m : METHODS) {
      if (m != meth) {
        testRequest(m, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
      }
    }
  }

  private void testNoPath(HttpMethod method) throws Exception {
    testRequest(method, "/", 200, "/");
    testRequest(method, "/wibble", 200, "/wibble");
    String rand = "/" + UUID.randomUUID().toString();
    testRequest(method, rand, 200, rand);
  }

  @Test
  public void testChaining() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("apples");
      rc.next();
    });
    router.route(path).handler(rc -> {
      rc.response().write("oranges");
      rc.next();
    });
    router.route(path).handler(rc -> {
      rc.response().write("bananas");
      rc.response().end();
    });
    testRequest(HttpMethod.GET, path, 200, "OK", "applesorangesbananas");
  }

  @Test
  public void testAsyncChaining() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("apples");
      vertx.runOnContext(v -> rc.next());
    });
    router.route(path).handler(rc -> {
      rc.response().write("oranges");
      vertx.runOnContext(v -> rc.next());
    });
    router.route(path).handler(rc -> {
      rc.response().write("bananas");
      rc.response().end();
    });
    testRequest(HttpMethod.GET, path, 200, "OK", "applesorangesbananas");
  }

  @Test
  public void testChainingWithTimers() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("apples");
      vertx.setTimer(1, v -> rc.next());
    });
    router.route(path).handler(rc -> {
      rc.response().write("oranges");
      vertx.setTimer(1, v -> rc.next());
    });
    router.route(path).handler(rc -> {
      rc.response().write("bananas");
      rc.response().end();
    });
    testRequest(HttpMethod.GET, path, 200, "OK", "applesorangesbananas");
  }

  @Test
  public void testOrdering() throws Exception {
    String path = "/blah";
    router.route(path).order(1).handler(rc -> {
      rc.response().write("apples");
      rc.next();
    });
    router.route(path).order(2).handler(rc -> {
      rc.response().write("oranges");
      rc.response().end();
    });
    router.route(path).order(0).handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("bananas");
      rc.next();
    });
    testRequest(HttpMethod.GET, path, 200, "OK", "bananasapplesoranges");
  }

  @Test
  public void testLast() throws Exception {
    String path = "/blah";
    Route route = router.route(path);
    router.route(path).handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("oranges");
      rc.next();
    });
    router.route(path).handler(rc -> {
      rc.response().write("bananas");
      rc.next();
    });
    route.last();
    route.handler(rc -> {
      rc.response().write("apples");
      rc.response().end();
    });
    testRequest(HttpMethod.GET, path, 200, "OK", "orangesbananasapples");
  }

  @Test
  public void testDisableEnable() throws Exception {
    String path = "/blah";
    Route route1 = router.route(path).handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("apples");
      rc.next();
    });
    Route route2 = router.route(path).handler(rc -> {
      rc.response().write("oranges");
      rc.next();
    });
    Route route3 = router.route(path).handler(rc -> {
      rc.response().write("bananas");
      rc.response().end();
    });

    testRequest(HttpMethod.GET, path, 200, "OK", "applesorangesbananas");
    route2.disable();
    testRequest(HttpMethod.GET, path, 200, "OK", "applesbananas");
    route1.disable();
    route3.disable();
    testRequest(HttpMethod.GET, path, 404, "Not Found");
    route3.enable();
    route1.enable();
    testRequest(HttpMethod.GET, path, 200, "OK", "applesbananas");
    route2.enable();
    testRequest(HttpMethod.GET, path, 200, "OK", "applesorangesbananas");
  }

  @Test
  public void testRemove() throws Exception {
    String path = "/blah";
    Route route1 = router.route(path).handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("apples");
      rc.next();
    });
    Route route2 = router.route(path).handler(rc -> {
      rc.response().write("oranges");
      rc.next();
    });
    Route route3 = router.route(path).handler(rc -> {
      rc.response().write("bananas");
      rc.response().end();
    });

    testRequest(HttpMethod.GET, path, 200, "OK", "applesorangesbananas");
    route2.remove();
    testRequest(HttpMethod.GET, path, 200, "OK", "applesbananas");
    route1.remove();
    route3.remove();
    testRequest(HttpMethod.GET, path, 404, "Not Found");
  }

  @Test
  public void testClear() throws Exception {
    router.route().handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("apples");
      rc.next();
    });
    router.route().handler(rc -> {
      rc.response().write("bananas");
      rc.response().end();
    });
    testRequest(HttpMethod.GET, "/whatever", 200, "OK", "applesbananas");
    router.clear();
    router.route().handler(rc -> {
      rc.response().setChunked(true);
      rc.response().write("grapes");
      rc.response().end();
    });
    testRequest(HttpMethod.GET, "/whatever", 200, "OK", "grapes");
  }

  @Test
  public void testChangeOrderAfterActive1() throws Exception {
    String path = "/blah";
    Route route = router.route(path).handler(rc -> {
      rc.response().write("apples");
      rc.next();
    });
    try {
      route.order(23);
      fail();
    } catch (IllegalStateException e) {
      // OK
    }
  }

  @Test
  public void testChangeOrderAfterActive2() throws Exception {
    String path = "/blah";
    Route route = router.route(path).failureHandler(rc -> {
      rc.response().write("apples");
      rc.next();
    });
    try {
      route.order(23);
      fail();
    } catch (IllegalStateException e) {
      // OK
    }
  }

  @Test
  public void testNextAfterResponseEnded() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      rc.response().end();
      rc.next();  // Call next
    });
    router.route(path).handler(rc -> assertTrue(rc.response().ended()));
    testRequest(HttpMethod.GET, path, 200, "OK");
  }

  @Test
  public void testFailureHandler1() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      throw new RuntimeException("ouch!");
    }).failureHandler(frc -> frc.response().setStatusCode(555).setStatusMessage("oh dear").end());
    testRequest(HttpMethod.GET, path, 555, "oh dear");
  }

  @Test
  public void testFailureinHandlingFailure() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      throw new RuntimeException("ouch!");
    }).failureHandler(frc -> {
      throw new RuntimeException("super ouch!");
    });
    testRequest(HttpMethod.GET, path, 500, "Internal Server Error");
  }

  @Test
  public void testFailureUsingInvalidCharsInStatus() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> rc.response().setStatusMessage("Hello\nWorld!").end());
    testRequest(HttpMethod.GET, path, 500, "Internal Server Error");
  }

  @Test
  public void testFailureinHandlingFailureWithInvalidStatusMessage() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      throw new RuntimeException("ouch!");
    }).failureHandler(frc -> frc.response().setStatusMessage("Hello\nWorld").end());
    testRequest(HttpMethod.GET, path, 500, "Internal Server Error");
  }

  @Test
  public void testSetExceptionHandler() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      throw new RuntimeException("ouch!");
    });
    CountDownLatch latch = new CountDownLatch(1);
    router.errorHandler(500, ctx -> {
      Throwable t = ctx.failure();
      assertEquals("ouch!", t.getMessage());
      latch.countDown();
    });
    testRequest(HttpMethod.GET, path, 500, "Internal Server Error");
    awaitLatch(latch);
  }

  @Test
  public void testFailureHandler1CallFail() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> rc.fail(400)).failureHandler(frc -> {
      assertEquals(400, frc.statusCode());
      frc.response().setStatusCode(400).setStatusMessage("oh dear").end();
    });
    testRequest(HttpMethod.GET, path, 400, "oh dear");
  }

  @Test
  public void testFailureHandler2() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      throw new RuntimeException("ouch!");
    });
    router.route("/bl*").failureHandler(frc -> frc.response().setStatusCode(555).setStatusMessage("oh dear").end());
    testRequest(HttpMethod.GET, path, 555, "oh dear");
  }

  @Test
  public void testFailureHandler2CallFail() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> rc.fail(400));
    router.route("/bl*").failureHandler(frc -> {
      assertEquals(400, frc.statusCode());
      frc.response().setStatusCode(400).setStatusMessage("oh dear").end();
    });
    testRequest(HttpMethod.GET, path, 400, "oh dear");
  }

  @Test
  public void testDefaultFailureHandler() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      throw new RuntimeException("ouch!");
    });
    // Default failure response
    testRequest(HttpMethod.GET, path, 500, "Internal Server Error");
  }

  @Test
  public void testDefaultFailureHandlerCallFail() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> rc.fail(400));
    // Default failure response
    testRequest(HttpMethod.GET, path, 400, "Bad Request");
  }

  @Test
  public void testFailureHandlerNoMatch() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> {
      throw new RuntimeException("ouch!");
    });
    router.route("/other").failureHandler(frc -> frc.response().setStatusCode(555).setStatusMessage("oh dear").end());
    // Default failure response
    testRequest(HttpMethod.GET, path, 500, "Internal Server Error");
  }

  @Test
  public void testFailureWithThrowable() throws Exception {
    String path = "/blah";
    Throwable failure = new Throwable();
    router.route(path).handler(rc -> rc.fail(failure)).failureHandler(frc -> {
      assertEquals(-1, frc.statusCode());
      assertSame(failure, frc.failure());
      frc.response().setStatusCode(500).setStatusMessage("Internal Server Error").end();
    });
    testRequest(HttpMethod.GET, path, 500, "Internal Server Error");
  }

  @Test
  public void testFailureWithNullThrowable() throws Exception {
    String path = "/blah";
    router.route(path).handler(rc -> rc.fail(null)).failureHandler(frc -> {
      assertEquals(-1, frc.statusCode());
      assertTrue(frc.failure() instanceof NullPointerException);
      frc.response().setStatusCode(500).setStatusMessage("Internal Server Error").end();
    });
    testRequest(HttpMethod.GET, path, 500, "Internal Server Error");
  }

  @Test
  public void testPattern1() throws Exception {
    router.route("/:abc").handler(rc -> rc.response().setStatusMessage(rc.request().params().get("abc")).end());
    testPattern("/tim", "tim");
  }

  @Test
  public void testParamEscape() throws Exception {
    router.route("/demo/:abc").handler(rc -> {
      assertEquals("Hello World!", rc.request().params().get("abc"));
      rc.response().end(rc.request().params().get("abc"));
    });
    testRequest(HttpMethod.GET, "/demo/Hello%20World!", 200, "OK", "Hello World!");
  }

  @Test
  public void testParamEscape2() throws Exception {
    router.route("/demo/:abc").handler(rc -> {
      assertEquals("Hello/World!", rc.request().params().get("abc"));
      rc.response().end(rc.request().params().get("abc"));
    });
    testRequest(HttpMethod.GET, "/demo/Hello%2FWorld!", 200, "OK", "Hello/World!");
  }

  @Test
  public void testParamEscape3() throws Exception {
    router.route("/demo/:abc").handler(rc -> {
      assertEquals("http://www.google.com", rc.request().params().get("abc"));
      rc.response().end(rc.request().params().get("abc"));
    });
    testRequest(HttpMethod.GET, "/demo/http%3A%2F%2Fwww.google.com", 200, "OK", "http://www.google.com");
  }

  @Test
  public void testParamEscape4() throws Exception {
    router.route("/:var").handler(rc -> {
      assertEquals("/ping", rc.request().params().get("var"));
      rc.response().end(rc.request().params().get("var"));
    });
    testRequest(HttpMethod.GET, "/%2Fping", 200, "OK", "/ping");
  }

  @Test
  public void testPattern1WithMethod() throws Exception {
    router.route(HttpMethod.GET, "/:abc").handler(rc -> rc.response().setStatusMessage(rc.request().params().get("abc")).end());
    testPattern("/tim", "tim");
    testRequest(HttpMethod.POST, "/tim", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testPattern1WithBuilder() throws Exception {
    router.route().path("/:abc").handler(rc -> rc.response().setStatusMessage(rc.request().params().get("abc")).end());
    testPattern("/tim", "tim");
  }

  @Test
  public void testPattern2() throws Exception {
    router.route("/blah/:abc").handler(rc -> rc.response().setStatusMessage(rc.request().params().get("abc")).end());
    testPattern("/blah/tim", "tim");
  }

  @Test
  public void testPattern3() throws Exception {
    router.route("/blah/:abc/blah").handler(rc -> rc.response().setStatusMessage(rc.request().params().get("abc")).end());
    testPattern("/blah/tim/blah", "tim");
  }

  @Test
  public void testPattern4() throws Exception {
    router.route("/blah/:abc/foo").handler(rc -> rc.response().setStatusMessage(rc.request().params().get("abc")).end());
    testPattern("/blah/tim/foo", "tim");
  }

  @Test
  public void testPattern5() throws Exception {
    router.route("/blah/:abc/:def/:ghi").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("abc") + params.get("def") + params.get("ghi")).end();
    });
    testPattern("/blah/tim/julien/nick", "timjuliennick");
  }

  @Test
  public void testPattern6() throws Exception {
    router.route("/blah/:abc/:def/:ghi/blah").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("abc") + params.get("def") + params.get("ghi")).end();
    });
    testPattern("/blah/tim/julien/nick/blah", "timjuliennick");
  }

  @Test
  public void testPattern7() throws Exception {
    router.route("/blah/:abc/quux/:def/eep/:ghi").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("abc") + params.get("def") + params.get("ghi")).end();
    });
    testPattern("/blah/tim/quux/julien/eep/nick", "timjuliennick");
  }

  @Test
  public void testPercentEncoding() throws Exception {
    router.route("/blah/:percenttext").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("percenttext")).end();
    });
    testPattern("/blah/abc%25xyz", "abc%xyz");
  }

  @Test
  public void testPathParamsAreFulfilled() throws Exception {
    router.route("/blah/:abc/quux/:def/eep/:ghi").handler(rc -> {
      Map<String, String> params = rc.pathParams();
      rc.response().setStatusMessage(params.get("abc") + params.get("def") + params.get("ghi")).end();
    });
    testPattern("/blah/tim/quux/julien/eep/nick", "timjuliennick");
  }

  @Test
  public void testPathParamsDoesNotOverrideQueryParam() throws Exception {
    final String paramName = "param";
    final String pathParamValue = "pathParamValue";
    final String queryParamValue1 = "queryParamValue1";
    final String queryParamValue2 = "queryParamValue2";
    final String sep = ",";
    router.route("/blah/:" + paramName + "/test").handler(rc -> {
      Map<String, String> params = rc.pathParams();
      MultiMap queryParams = rc.request().params();
      List<String> values = queryParams.getAll(paramName);
      String qValue = values.stream().collect(Collectors.joining(sep));
      rc.response().setStatusMessage(params.get(paramName) + "|" + qValue).end();
    });
    testRequest(HttpMethod.GET,
      "/blah/" + pathParamValue + "/test?" + paramName + "=" + queryParamValue1 + "&" + paramName + "=" + queryParamValue2,
      200,
      pathParamValue + "|" + queryParamValue1 + sep + queryParamValue2);
  }

  @Test
  public void testCorrectQueryParamatersEncapsulation() throws Exception {
    final String pathParameterName = "pathParameter";
    final String pathParamValue = "awesomePath";
    final String qName = "q";
    final String qValue1 = "a";
    final String qValue2 = "b";
    final String sName = "s";
    final String sValue = "sample_value";
    final String sep = ",";
    router.route("/blah/:" + pathParameterName + "/test").handler(rc -> {
      MultiMap params = rc.queryParams();
      assertFalse(params.contains(pathParameterName));
      String qExpected = String.join(",", params.getAll("q"));
      String statusMessage = String.join("/", qExpected, params.get("s"));
      rc.response().setStatusMessage(statusMessage).end();
    });
    testRequest(HttpMethod.GET,
      "/blah/" + pathParamValue + "/test?" + qName + "=" + qValue1 + "," + qValue2 + "&" + sName + "=" + sValue, 200,
      qValue1 + "," + qValue2 + "/" + sValue);
  }

  @Test
  public void testPathParamsWithReroute() throws Exception {
    String paramName = "param";
    String firstParamValue = "fpv";
    String secondParamValue = "secondParamValue";
    router.route("/first/:" + paramName + "/route").handler(rc -> {
      assertEquals(firstParamValue, rc.pathParam(paramName));
      rc.reroute(HttpMethod.GET, "/second/" + secondParamValue + "/route");
    });
    router.route("/second/:" + paramName + "/route").handler(rc -> rc.response().setStatusMessage(rc.pathParam(paramName)).end());
    testRequest(HttpMethod.GET, "/first/" + firstParamValue + "/route", 200, secondParamValue);
  }

  private void testPattern(String pathRoot, String expected) throws Exception {
    testRequest(HttpMethod.GET, pathRoot, 200, expected);
    testRequest(HttpMethod.GET, pathRoot + "/", 404, "Not Found");
    testRequest(HttpMethod.GET, pathRoot + "/wibble", 404, "Not Found");
    testRequest(HttpMethod.GET, pathRoot + "/wibble/blibble", 404, "Not Found");
  }

  @Test
  public void testInvalidPattern() throws Exception {
    router.route("/blah/:!!!/").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("!!!")).end();
    });
    testRequest(HttpMethod.GET, "/blah/tim", 404, "Not Found"); // Because it won't match
  }

  @Test
  public void testInvalidPatternWithBuilder() throws Exception {
    router.route().path("/blah/:!!!/").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("!!!")).end();
    });
    testRequest(HttpMethod.GET, "/blah/tim", 404, "Not Found"); // Because it won't match
  }

  @Test
  public void testGroupMoreThanOne() throws Exception {
    try {
      router.route("/blah/:abc/:abc");
      fail();
    } catch (IllegalArgumentException e) {
      // OK
    }
  }

  @Test
  public void testRegex1() throws Exception {
    router.routeWithRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("param0") + params.get("param1")).end();
    });
    testPattern("/dog/cat", "dogcat");
  }

  @Test
  public void testRegex1WithBuilder() throws Exception {
    router.route().pathRegex("\\/([^\\/]+)\\/([^\\/]+)").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("param0") + params.get("param1")).end();
    });
    testPattern("/dog/cat", "dogcat");
  }

  @Test
  public void testRegex1WithMethod() throws Exception {
    router.routeWithRegex(HttpMethod.GET, "\\/([^\\/]+)\\/([^\\/]+)").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("param0") + params.get("param1")).end();
    });
    testPattern("/dog/cat", "dogcat");
    testRequest(HttpMethod.POST, "/dog/cat", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testRegex2() throws Exception {
    router.routeWithRegex("\\/([^\\/]+)\\/([^\\/]+)/blah").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("param0") + params.get("param1")).end();
    });
    testPattern("/dog/cat/blah", "dogcat");
  }

  @Test
  public void testRegex3() throws Exception {
    router.routeWithRegex(".*foo.txt").handler(rc -> rc.response().setStatusMessage("ok").end());
    testPattern("/dog/cat/foo.txt", "ok");
    testRequest(HttpMethod.POST, "/dog/cat/foo.bar", 404, "Not Found");
  }

  @Test
  public void testRegexWithNamedParams() throws Exception {
    router.routeWithRegex(HttpMethod.GET, "\\/(?<name>[^\\/]+)\\/(?<surname>[^\\/]+)").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("name") + params.get("surname")).end();
    });
    testPattern("/joe/doe", "joedoe");
  }

  @Test
  public void testRegexWithNamedParamsKeepsIndexedParams() throws Exception {
    router.routeWithRegex(HttpMethod.GET, "\\/(?<name>[^\\/]+)\\/(?<surname>[^\\/]+)").handler(rc -> {
      MultiMap params = rc.request().params();
      rc.response().setStatusMessage(params.get("param0") + params.get("param1")).end();
    });
    testPattern("/joe/doe", "joedoe");
  }

  @Test
  public void testConsumes() throws Exception {
    router.route().consumes("text/html").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/json", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "something/html", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesWithParameterKey() throws Exception {
    router.route().consumes("text/html;boo").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=ya;itWorks=4real", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo;itWorks", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=ya", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesWithParameter() throws Exception {
    router.route().consumes("text/html;boo=ya").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=ya", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesWithQuotedParameterWithComma() throws Exception {
    router.route().consumes("text/html;boo=\"yeah,right\"").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah,right\";itWorks=4real", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah,right\"", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah,right;itWorks=4real\"", 415, "Unsupported Media Type");
    // this might look wrong but since there is only 1 entry per content-type, the comma has no semantic meaning
    // therefore it is ignored
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=yeah,right", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesWithQuotedParameterWithQuotes() throws Exception {
    router.route().consumes("text/html;boo=\"yeah\\\"right\"").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah\\\"right\"", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=\"yeah,right\"", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=yeah,right", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesWithQParameterIgnored() throws Exception {
    router.route().consumes("text/html;q").consumes("text/html;q=0.1").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=yeah,right", 200, "OK");
  }

  @Test
  public void testConsumesMultiple() throws Exception {
    router.route().consumes("text/html").consumes("application/json").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/json", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "something/html", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/json", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/blah", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesVariableParameters() throws Exception {
    router.route().consumes("text/html;boo").consumes("text/html;works").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;works", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo;works", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;boo=done;it=works", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html;yes=no;right", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/book;boo", 415, "Unsupported Media Type");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/book;works=aright", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesMissingSlash() throws Exception {
    // will assume "*/json"
    router.route().consumes("json").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesSubtypeWildcard() throws Exception {
    router.route().consumes("text/*").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/json", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesTopLevelTypeWildcard() throws Exception {
    router.route().consumes("*/json").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/html", 415, "Unsupported Media Type");
  }

  @Test
  public void testConsumesAll1() throws Exception {
    router.route().consumes("*/*").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html; someparam=12", 200, "OK");
    testRequest(HttpMethod.GET, "/foo", 200, "OK");
  }

  @Test
  public void testConsumesAll2() throws Exception {
    router.route().consumes("*").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "application/json", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html; someparam=12", 200, "OK");
    testRequest(HttpMethod.GET, "/foo", 200, "OK");
  }

  @Test
  public void testConsumesCTParamsIgnored() throws Exception {
    router.route().consumes("text/html").handler(rc -> rc.response().end());
    testRequestWithContentType(HttpMethod.GET, "/foo", "text/html; someparam=12", 200, "OK");
  }

  @Test
  public void testConsumesNoContentType() throws Exception {
    router.route().consumes("text/html").handler(rc -> rc.response().end());
    testRequest(HttpMethod.GET, "/foo", HttpResponseStatus.BAD_REQUEST);
  }

  @Test
  public void testProduces() throws Exception {
    router.route().produces("text/html").handler(rc -> rc.response().end());
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/json", 406, "Not Acceptable");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "something/html", 406, "Not Acceptable");
    testRequest(HttpMethod.GET, "/foo", 200, "OK");
  }

  @Test
  public void testProducesWithParameterKey() throws Exception {
    router.route().produces("text/html;boo").handler(rc -> rc.response().end());
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html;boo;itWorks", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html;boo=ya", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html;boo", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html", 406, "Not Acceptable");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "*/*", 200, "OK");
  }

  @Test
  public void testProducesWithParameter() throws Exception {
    router.route().produces("text/html;boo=ya").handler(rc -> rc.response().end());
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html;boo=ya", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html;boo", 406, "Not Acceptable");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html", 406, "Not Acceptable");
  }

  @Test
  public void testProducesMultiple() throws Exception {
    router.route().produces("text/html").produces("application/json").handler(rc -> rc.response().end());
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "application/json", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/json", 406, "Not Acceptable");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "something/html", 406, "Not Acceptable");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/json", 406, "Not Acceptable");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "application/blah", 406, "Not Acceptable");
  }

  @Test
  public void testProducesWithQParameterIgnored() throws Exception {
    router.route().produces("text/html;q").produces("text/html;q=0.1").handler(rc -> rc.response().end());
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html;a", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html;q=2", 200, "OK");
    testRequest(HttpMethod.GET, "/foo", 200, "OK");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "*/*", 200, "OK");
  }

  @Test
  public void testProducesMissingSlash() throws Exception {
    // will assume "*/json"
    router.route().produces("application/json").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "json", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text", 406, "Not Acceptable");
  }

  @Test
  public void testProducesSubtypeWildcard() throws Exception {
    router.route().produces("text/html").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/*", 200, "text/html");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "application/*", 406, "Not Acceptable");
  }

  @Test
  public void testProducesSubtypeWildcardAcceptTextPlain() throws Exception {
    router.route().produces("text/*").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/plain", 200, "text/plain");
  }

  @Test
  public void testProducesComponentWildcardAcceptTextPlain() throws Exception {
    router.route().produces("*/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/plain", 200, "text/plain");
  }

  @Test
  public void testProducesAllWildcard() throws Exception {
    router.route().produces("*/*").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/plain", 200, "text/plain");
  }

  @Test
  public void testProducesTopLevelTypeWildcard() throws Exception {
    router.route().produces("application/json").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "*/json", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "*/html", 406, "Not Acceptable");
  }

  @Test
  public void testProducesAll1() throws Exception {
    router.route().produces("application/json").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "*/*", 200, "application/json");
  }

  @Test
  public void testProducesAll2() throws Exception {
    router.route().produces("application/json").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "*", 200, "application/json");
  }

  @Test
  public void testAcceptsMultiple1() throws Exception {
    router.route().produces("application/json").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,application/json,text/plain", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,application/json;b,text/plain", 200, "application/json");
  }

  @Test
  public void testAcceptsMultiple2() throws Exception {
    router.route().produces("application/json").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,application/*,text/plain", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html;a,application/*,text/plain", 200, "application/json");
  }

  @Test
  public void testAcceptsWithSpaces() throws Exception {
    router.route("/json").produces("application/json").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/json", "    text/html    , application/*    , text/plain; q= 0.9  ", 200, "application/json");
    router.route("/html").produces("text/html").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/html", "    text/html    , application/*    , text/plain; q= 0.9  ", 200, "text/html");
    router.route("/text").produces("text/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/text", "    text/html    , application/*    , text/plain; q= 0.9  ", 200, "text/plain");
  }

  @Test
  public void testAcceptsMultiple3() throws Exception {
    router.route().produces("application/json").produces("text/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,application/json,text/plain", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,application/json;a,text/plain", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,application/json,text/plain;a", 200, "text/plain");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,application/json;c,text/plain;a", 200, "application/json");
  }

  @Test
  public void testAcceptsMultiple4() throws Exception {
    router.route().produces("application/json").produces("text/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain,application/json", 200, "text/plain");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;a,application/json", 200, "text/plain");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain,application/json;a", 200, "application/json");
  }

  @Test
  public void testAcceptsMultiple5() throws Exception {
    router.route().produces("application/json").produces("text/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain,application/json;q=0.9", 200, "text/plain");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain,application/json;q=0.9;a", 200, "text/plain");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain,application/json;a;q=0.9", 200, "text/plain");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;a,application/json;q=0.9", 200, "text/plain");
  }

  @Test
  public void testAcceptsMultiple6() throws Exception {
    router.route().produces("application/json").produces("text/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9,application/json", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9,application/json;a", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9;a,application/json", 200, "application/json");
  }

  @Test
  public void testAcceptsMultiple7() throws Exception {
    router.route().produces("application/json").produces("text/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9,application/json;q=1.0", 200, "application/json");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9;a,application/json;q=1.0", 200, "application/json");
  }

  @Test
  public void testAcceptsMultiple8() throws Exception {
    router.route().produces("application/json").produces("text/html").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9,application/json;q=1.0", 200, "text/html");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9;b,application/json;q=1.0", 200, "text/html");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9;b,application/json;q=1.0;a", 200, "application/json");
  }

  @Test
  public void testAcceptsMultiple9() throws Exception {
    router.route().produces("application/json").produces("text/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9,application/json;q=0.8", 200, "text/plain");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9;d,application/json;q=0.8", 200, "text/plain");
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9,application/json;q=0.8;s", 200, "text/plain");
  }

  @Test
  public void testAcceptsMultipleWithParams() throws Exception {
    router.route().produces("application/json").produces("text/plain").handler(rc -> {
      rc.response().setStatusMessage(rc.getAcceptableContentType());
      rc.response().end();
    });
    testRequestWithAccepts(HttpMethod.GET, "/foo", "text/html,text/plain;q=0.9,application/json;q=0.8", 200, "text/plain");
  }

  @Test
  public void testGetPutContextData() throws Exception {
    SomeObject obj = new SomeObject();
    router.route().handler(ctx -> {
      ctx.put("foo", "bar");
      ctx.put("blah", obj);
      ctx.next();
    });
    router.route().handler(ctx -> {
      assertEquals("bar", ctx.get("foo"));
      assertEquals(obj, ctx.get("blah"));
      ctx.response().end();
    });
    testRequest(HttpMethod.GET, "/", 200, "OK");
  }

  class SomeObject {
  }

  @Test
  public void testGetRoutes() throws Exception {
    router.route("/abc").handler(rc -> {
    });
    router.route("/abc/def").handler(rc -> {
    });
    router.route("/xyz").handler(rc -> {
    });
    List<Route> routes = router.getRoutes();
    assertEquals(3, routes.size());
  }

  // Test that adding headersEndhandlers doesn't overwrite other ones
  @Test
  public void testHeadersEndHandler() throws Exception {
    router.route().handler(rc -> {
      rc.addHeadersEndHandler(v -> rc.response().putHeader("header1", "foo"));
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addHeadersEndHandler(v -> rc.response().putHeader("header2", "foo"));
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addHeadersEndHandler(v -> rc.response().putHeader("header3", "foo"));
      rc.response().end();
    });
    testRequest(HttpMethod.GET, "/", null, resp -> {
      MultiMap headers = resp.headers();
      assertTrue(headers.contains("header1"));
      assertTrue(headers.contains("header2"));
      assertTrue(headers.contains("header3"));
    }, 200, "OK", null);
  }

  @Test
  public void testHeadersEndHandlerCalledBackwards() throws Exception {

    final AtomicInteger cnt = new AtomicInteger(0);

    router.route().handler(rc -> {
      final int val = cnt.incrementAndGet();
      rc.addHeadersEndHandler(v -> assertEquals(val, cnt.getAndDecrement()));
      rc.next();
    });
    router.route().handler(rc -> {
      final int val = cnt.incrementAndGet();
      rc.addHeadersEndHandler(v -> assertEquals(val, cnt.getAndDecrement()));
      rc.next();
    });
    router.route().handler(rc -> {
      final int val = cnt.incrementAndGet();
      rc.addHeadersEndHandler(v -> assertEquals(val, cnt.getAndDecrement()));
      rc.response().end();
    });

    testRequest(HttpMethod.GET, "/", 200, "OK");
  }

  @Test
  public void testHeadersEndHandlerCalledBackwards2() throws Exception {

    final AtomicInteger cnt = new AtomicInteger(0);

    router.route().handler(rc -> {
      final int val = cnt.incrementAndGet();
      rc.addBodyEndHandler(v -> assertEquals(val, cnt.getAndDecrement()));
      rc.next();
    });
    router.route().handler(rc -> {
      final int val = cnt.incrementAndGet();
      rc.addBodyEndHandler(v -> assertEquals(val, cnt.getAndDecrement()));
      rc.next();
    });
    router.route().handler(rc -> {
      final int val = cnt.incrementAndGet();
      rc.addBodyEndHandler(v -> assertEquals(val, cnt.getAndDecrement()));
      rc.response().end();
    });

    testRequest(HttpMethod.GET, "/", 200, "OK");
  }

  @Test
  public void testHeadersEndHandlerRemoveHandler() throws Exception {
    router.route().handler(rc -> {
      rc.addHeadersEndHandler(v -> rc.response().putHeader("header1", "foo"));
      rc.next();
    });
    router.route().handler(rc -> {
      Handler<Void> handler = v -> rc.response().putHeader("header2", "foo");
      int handlerID = rc.addHeadersEndHandler(handler);
      vertx.setTimer(1, tid -> {
        assertTrue(rc.removeHeadersEndHandler(handlerID));
        assertFalse(rc.removeHeadersEndHandler(handlerID + 1));
        rc.response().end();
      });
    });

    testRequest(HttpMethod.GET, "/", null, resp -> {
      MultiMap headers = resp.headers();
      assertTrue(headers.contains("header1"));
    }, 200, "OK", null);
  }

  // Test that adding bodyEndhandlers doesn't overwrite other ones
  @Test
  public void testBodyEndHandler() throws Exception {
    AtomicInteger cnt = new AtomicInteger();
    router.route().handler(rc -> {
      rc.addBodyEndHandler(v -> cnt.incrementAndGet());
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addBodyEndHandler(v -> cnt.incrementAndGet());
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addBodyEndHandler(v -> cnt.incrementAndGet());
      rc.response().end();
    });
    testRequest(HttpMethod.GET, "/", 200, "OK");
    assertWaitUntil(() -> cnt.get() == 3);
  }

  @Test
  public void testBodyEndHandlerRemoveHandler() throws Exception {
    AtomicInteger cnt = new AtomicInteger();
    router.route().handler(rc -> {
      rc.addBodyEndHandler(v -> cnt.incrementAndGet());
      rc.next();
    });
    router.route().handler(rc -> {
      Handler<Void> handler = v -> cnt.incrementAndGet();
      int handlerID = rc.addBodyEndHandler(handler);
      vertx.setTimer(1, tid -> {
        assertTrue(rc.removeBodyEndHandler(handlerID));
        assertFalse(rc.removeBodyEndHandler(handlerID + 1));
        rc.response().end();
      });
    });

    testRequest(HttpMethod.GET, "/", 200, "OK");
    assertWaitUntil(() -> cnt.get() == 1);
  }

  // Test that adding an endHandler doesn't overwrite other ones
  @Test
  public void testEndHandler() throws Exception {
    AtomicInteger cnt = new AtomicInteger();
    router.route().handler(rc -> {
      rc.addEndHandler(v -> cnt.incrementAndGet());
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addEndHandler(v -> cnt.incrementAndGet());
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addEndHandler(v -> cnt.incrementAndGet());
      rc.response().end();
    });
    testRequest(HttpMethod.GET, "/", 200, "OK");
    assertWaitUntil(() -> cnt.get() == 3);
  }

  // Test that adding an exceptionHandler doesn't overwrite other ones
  @Test
  public void testExceptionHandler() throws Exception {
    HttpClientRequest req = client.request(HttpMethod.GET, server.actualPort(), "localhost", "/path");
    AtomicInteger cnt = new AtomicInteger();
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        if (done.failed()) {
          cnt.incrementAndGet();
        }
      });
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        if (done.failed()) {
          cnt.incrementAndGet();
        }
      });
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        if (done.failed()) {
          cnt.incrementAndGet();
        }
      });
      rc.next();
    });
    router.route().handler(rc -> {
      req.connection().close();
    });
    req.end();
    assertWaitUntil(() -> cnt.get() == 3);
  }

  // Test that adding a closeHandler doesn't overwrite other ones
  @Test
  public void testCloseHandler() throws Exception {
    HttpClientRequest req = client.request(HttpMethod.GET, server.actualPort(), "localhost", "/path");
    AtomicInteger cnt = new AtomicInteger();
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        cnt.incrementAndGet();
      });
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        cnt.incrementAndGet();
      });
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        cnt.incrementAndGet();
      });
      rc.next();
    });
    router.route().handler(rc -> {
      req.connection().close();
    });
    req.end();
    assertWaitUntil(() -> cnt.get() == 3);
  }

  // Test that the endHandler is called once for an exception
  @Test
  public void testEndHandlerCalledOnce() throws Exception {
    HttpClientRequest req = client.request(HttpMethod.GET, server.actualPort(), "localhost", "/path");
    AtomicInteger endCnt = new AtomicInteger();
    AtomicInteger excCnt = new AtomicInteger();
    AtomicInteger closeCnt = new AtomicInteger();
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        excCnt.incrementAndGet();
      });
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        endCnt.incrementAndGet();
      });
      rc.next();
    });
    router.route().handler(rc -> {
      rc.addEndHandler(done -> {
        closeCnt.incrementAndGet();
      });
      rc.next();
    });
    router.route().handler(rc -> {
      req.connection().close();
    });
    req.end();
    assertWaitUntil(() -> endCnt.get() == 1);
    assertWaitUntil(() -> excCnt.get() == 1);
    assertWaitUntil(() -> closeCnt.get() == 1);
  }

  @Test
  public void testNoRoutes() throws Exception {
    testRequest(HttpMethod.GET, "/whatever", 404, "Not Found");
  }

  @Test
  public void testGet() throws Exception {
    router.get().handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.GET, "/whatever", 200, "foo");
    testRequest(HttpMethod.POST, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testGetWithPathExact() throws Exception {
    router.get("/somepath/").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.GET, "/somepath", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/", 200, "foo");
    testRequest(HttpMethod.GET, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.POST, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.PUT, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.DELETE, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.HEAD, "/somepath/whatever", 404, "Not Found");
  }

  @Test
  public void testGetWithPathBegin() throws Exception {
    router.get("/somepath/*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.GET, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.GET, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testGetWithPathBeginShouldNotMatchPrefix() throws Exception {
    router.get("/swagger-ui/*").handler(rc -> rc.response().setStatusMessage("/swagger-ui/*").end());
    router.get("/swagger-ui").handler(rc -> rc.response().setStatusMessage("/swagger-ui").end());
    router.get("/swagger").handler(rc -> rc.response().setStatusMessage("/swagger").end());
    testRequest(HttpMethod.GET, "/swagger-ui/", 200, "/swagger-ui/*");
    testRequest(HttpMethod.GET, "/swagger-ui/whatever", 200, "/swagger-ui/*");
    testRequest(HttpMethod.GET, "/swagger", 200, "/swagger");
    testRequest(HttpMethod.GET, "/swagger/", 200, "/swagger"); // Is that expected ?
    testRequest(HttpMethod.GET, "/swagger/whatever", 404, "Not Found");
  }

  @Test
  public void testGetWithRegex() throws Exception {
    router.getWithRegex("\\/somepath\\/.*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.GET, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.GET, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testPost() throws Exception {
    router.post().handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.POST, "/whatever", 200, "foo");
    testRequest(HttpMethod.GET, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testPostWithPathExact() throws Exception {
    router.post("/somepath/").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.POST, "/somepath/", 200, "foo");
    testRequest(HttpMethod.POST, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.PUT, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.DELETE, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.HEAD, "/somepath/whatever", 404, "Not Found");
  }

  @Test
  public void testPostWithPathBegin() throws Exception {
    router.post("/somepath/*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.POST, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.POST, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testPostWithRegex() throws Exception {
    router.postWithRegex("\\/somepath\\/.*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.POST, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.POST, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testPut() throws Exception {
    router.put().handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.PUT, "/whatever", 200, "foo");
    testRequest(HttpMethod.GET, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testPutWithPathExact() throws Exception {
    router.put("/somepath/").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.PUT, "/somepath/", 200, "foo");
    testRequest(HttpMethod.PUT, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.POST, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.DELETE, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.HEAD, "/somepath/whatever", 404, "Not Found");
  }

  @Test
  public void testPutWithPathBegin() throws Exception {
    router.put("/somepath/*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.PUT, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.PUT, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testPutWithRegex() throws Exception {
    router.putWithRegex("\\/somepath\\/.*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.PUT, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.PUT, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testDelete() throws Exception {
    router.delete().handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.DELETE, "/whatever", 200, "foo");
    testRequest(HttpMethod.GET, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testDeleteWithPathExact() throws Exception {
    router.delete("/somepath/").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.DELETE, "/somepath/", 200, "foo");
    testRequest(HttpMethod.DELETE, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.POST, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.PUT, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.HEAD, "/somepath/whatever", 404, "Not Found");
  }

  @Test
  public void testDeleteWithPathBegin() throws Exception {
    router.delete("/somepath/*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.DELETE, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.DELETE, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testDeleteWithRegex() throws Exception {
    router.deleteWithRegex("\\/somepath\\/.*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.DELETE, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.DELETE, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testOptions() throws Exception {
    router.options().handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.OPTIONS, "/whatever", 200, "foo");
    testRequest(HttpMethod.GET, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testOptionsWithPathExact() throws Exception {
    router.options("/somepath/").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.OPTIONS, "/somepath/", 200, "foo");
    testRequest(HttpMethod.OPTIONS, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.POST, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.PUT, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.DELETE, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.HEAD, "/somepath/whatever", 404, "Not Found");
  }

  @Test
  public void testOptionsWithPathBegin() throws Exception {
    router.options("/somepath/*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.OPTIONS, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testOptionsWithRegex() throws Exception {
    router.optionsWithRegex("\\/somepath\\/.*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.OPTIONS, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.HEAD, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testHead() throws Exception {
    router.head().handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.HEAD, "/whatever", 200, "foo");
    testRequest(HttpMethod.GET, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testHeadWithPathExact() throws Exception {
    router.head("/somepath/").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.HEAD, "/somepath/", 200, "foo");
    testRequest(HttpMethod.HEAD, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.POST, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.PUT, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", 404, "Not Found");
    testRequest(HttpMethod.DELETE, "/somepath/whatever", 404, "Not Found");
  }

  @Test
  public void testHeadWithPathBegin() throws Exception {
    router.head("/somepath/*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.HEAD, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.HEAD, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testHeadWithRegex() throws Exception {
    router.headWithRegex("\\/somepath\\/.*").handler(rc -> rc.response().setStatusMessage("foo").end());
    testRequest(HttpMethod.HEAD, "/somepath/whatever", 200, "foo");
    testRequest(HttpMethod.HEAD, "/otherpath/whatever", 404, "Not Found");
    testRequest(HttpMethod.GET, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.POST, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.PUT, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.OPTIONS, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
    testRequest(HttpMethod.DELETE, "/somepath/whatever", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testRouteNormalized1() throws Exception {
    router.route("/foo").handler(rc -> rc.response().setStatusMessage("socks").end());
    testRequest(HttpMethod.GET, "/foo", 200, "socks");
    testRequest(HttpMethod.GET, "/foo/", 200, "socks");
    testRequest(HttpMethod.GET, "//foo/", 200, "socks");
    testRequest(HttpMethod.GET, "//foo//", 200, "socks");
    testRequest(HttpMethod.GET, "//foo/////", 200, "socks");
  }

  @Test
  public void testRouteNormalized2() throws Exception {
    router.route("/foo/").handler(rc -> rc.response().setStatusMessage("socks").end());
    // note that the final slash is significant
    testRequest(HttpMethod.GET, "/foo", 404, "Not Found");
    testRequest(HttpMethod.GET, "/foo/", 200, "socks");
    testRequest(HttpMethod.GET, "//foo/", 200, "socks");
    testRequest(HttpMethod.GET, "//foo//", 200, "socks");
    testRequest(HttpMethod.GET, "//foo/////", 200, "socks");
  }

  @Test
  public void testRouteNormalized3() throws Exception {
    router.route("/").handler(rc -> rc.response().setStatusMessage("pants").end());
    testRequest(HttpMethod.GET, "/", 200, "pants");
    testRequest(HttpMethod.GET, "//", 200, "pants");
    testRequest(HttpMethod.GET, "///", 200, "pants");
  }

  @Test
  public void testIssue170() throws Exception {
    try {
      router.route("").handler(rc -> rc.response().end());
    } catch (IllegalArgumentException e) {
      testComplete();
      return;
    }

    fail("Should fail");
  }

  @Test
  public void testIssue170b() throws Exception {
    router.route("/").handler(rc -> rc.response().end());
    testRequest(HttpMethod.GET, "/", 200, "OK");
  }

  @Test
  public void testIssue176() throws Exception {
    router.route().order(0).handler(context -> {
      context.response().headers().add("X-Here-1", "1");
      context.next();
    });
    router.route().order(0).handler(context -> {
      context.response().headers().add("X-Here-2", "2");
      context.next();
    });
    router.route().handler(context -> {
      context.response().headers().add("X-Here-3", "3");
      context.response().end();
    });

    testRequest(HttpMethod.GET, "/", null, resp -> {
      MultiMap headers = resp.headers();
      assertTrue(headers.contains("X-Here-1"));
      assertTrue(headers.contains("X-Here-2"));
      assertTrue(headers.contains("X-Here-3"));
    }, 200, "OK", null);
  }

  @Test
  public void testLocaleWithCountry() throws Exception {
    router.route().handler(rc -> {
      assertEquals(3, rc.acceptableLanguages().size());
      assertEquals("da", rc.preferredLanguage().tag());
      assertEquals("DK", rc.preferredLanguage().subtag());
      rc.response().end();
    });

    testRequest(HttpMethod.GET, "/foo", req -> req.putHeader("Accept-Language", "en-gb;q=0.8, en;q=0.7, da_DK;q=0.9"), 200, "OK", null);
    testRequest(HttpMethod.GET, "/foo", req -> req.putHeader("Accept-Language", "en-gb;q=0.8, en;q=0.7, da-DK;q=0.9"), 200, "OK", null);
  }

  @Test
  public void testLocaleSimple() throws Exception {
    router.route().handler(rc -> {
      assertEquals(3, rc.acceptableLanguages().size());
      assertEquals("da", rc.preferredLanguage().tag());
      rc.response().end();
    });

    testRequest(HttpMethod.GET, "/foo", req -> req.putHeader("Accept-Language", "da, en-gb;q=0.8, en;q=0.7"), 200, "OK", null);
  }

  @Test
  public void testLocaleWithoutQuality() throws Exception {
    router.route().handler(rc -> {
      assertEquals(1, rc.acceptableLanguages().size());
      assertEquals("en", rc.preferredLanguage().tag());
      assertEquals("GB", rc.preferredLanguage().subtag().toUpperCase());
      rc.response().end();
    });

    testRequest(HttpMethod.GET, "/foo", req -> req.putHeader("Accept-Language", "en-gb"), 200, "OK", null);
  }

  @Test
  public void testLocaleSameQuality() throws Exception {
    router.route().handler(rc -> {
      assertEquals(2, rc.acceptableLanguages().size());
      assertEquals("pt", rc.preferredLanguage().tag());
      rc.response().end();
    });

    testRequest(HttpMethod.GET, "/foo", req -> req.putHeader("Accept-Language", "pt;q=0.9, en-gb;q=0.9"), 200, "OK", null);
  }

  @Test
  public void testLocaleNoHeaderFromClient() throws Exception {
    router.route().handler(rc -> {
      assertEquals(0, rc.acceptableLanguages().size());
      rc.response().end();
    });

    testRequest(HttpMethod.GET, "/foo", 200, "OK");
  }

  @Test
  public void testUnderscoreOnRoutePath() throws Exception {
    router.route("/:account_id").handler(rc -> {
      assertEquals("foo", rc.request().params().get("account_id"));
      rc.response().end();
    });

    testRequest(HttpMethod.GET, "/foo", 200, "OK");
  }

  @Test
  public void testBadURL() throws Exception {
    router.route().handler(rc -> rc.response().end());

    testRequest(HttpMethod.GET, "/%7B%channel%%7D", 200, "OK");
  }

  @Test
  public void testDuplicateParams() throws Exception {
    router.route("/test/:p").handler(RoutingContext::next);
    router.route("/test/:p").handler(RoutingContext::next);
    router.route("/test/:p").handler(routingContext -> {
      assertEquals(1, routingContext.request().params().getAll("p").size());
      assertEquals("abc", routingContext.request().getParam("p"));
      routingContext.response().end();
    });
    testRequest(HttpMethod.GET, "/test/abc", 200, "OK");
  }

  @Test
  public void testDuplicateParams2() throws Exception {
    router.route("/test/:p").handler(RoutingContext::next);
    router.route("/test/:p").handler(ctx -> ctx.reroute("/done/abc/cde"));

    router.route("/done/:a/:p").handler(routingContext -> {
      assertEquals(1, routingContext.request().params().getAll("p").size());
      assertEquals("cde", routingContext.request().getParam("p"));
      routingContext.response().end();
    });
    testRequest(HttpMethod.GET, "/test/abc", 200, "OK");
  }

  @Test
  public void testSubRouterNPE() throws Exception {
    Router subRouter = Router.router(vertx);
    router.mountSubRouter("/", subRouter);

    testRequest(HttpMethod.GET, "foo", 404, "Not Found");
  }

  @Test
  public void testParamFirst() throws Exception {
    router.route("/:p/*").handler(context -> {
      context.response().headers().add("X-Here-1", "1");
      context.next();
    });
    router.route("/:p/test").handler(context -> {
      context.response().headers().add("X-Here-2", "2");
      context.response().end();
    });

    testRequest(HttpMethod.GET, "/abc/test", null, resp -> {
      MultiMap headers = resp.headers();
      assertTrue(headers.contains("X-Here-1"));
      assertTrue(headers.contains("X-Here-2"));
    }, 200, "OK", null);
  }

  @Test
  public void testGetWithPlusPath2() throws Exception {
    router.get("/:param1").useNormalizedPath(false).handler(rc -> {
      assertEquals("/some+path", rc.normalizedPath());
      assertEquals("some+path", rc.pathParam("param1"));
      assertEquals("some query", rc.request().getParam("q1"));
      rc.response().setStatusMessage("foo").end();
    });
    testRequest(HttpMethod.GET, "/some+path?q1=some+query", 200, "foo");
  }

  @Test
  public void testMultipleSetHandler() throws Exception {
    router.get("/path").handler(routingContext -> {
      routingContext.put("response", "handler1");
      routingContext.next();
    }).handler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "handler2");
      routingContext.next();
    }).handler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.end(routingContext.get("response") + "handler3");
    });
    testRequest(HttpMethod.GET, "/path", 200, "OK", "handler1handler2handler3");
  }

  @Test
  public void testMultipleSetFailureHandler() throws Exception {
    router.get("/path").handler(routingContext -> routingContext.fail(500)).failureHandler(routingContext -> {
      routingContext.put("response", "handler1");
      routingContext.next();
    }).failureHandler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "handler2");
      routingContext.next();
    }).failureHandler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.setStatusMessage("ERROR");
      response.setStatusCode(500);
      response.end(routingContext.get("response") + "handler3");
    });
    testRequest(HttpMethod.GET, "/path", 500, "ERROR", "handler1handler2handler3");
  }

  @Test
  public void testMultipleSetFailureHandlerCorrectOrder() throws Exception {
    router.route().failureHandler(routingContext -> {
      routingContext.put("response", "handler1");
      routingContext.next();
    });

    router.get("/path").handler(routingContext -> routingContext.fail(500)).failureHandler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "handler2");
      routingContext.next();
    }).failureHandler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.setStatusMessage("ERROR");
      response.setStatusCode(500);
      response.end(routingContext.get("response") + "handler3");
    });
    testRequest(HttpMethod.GET, "/path", 500, "ERROR", "handler1handler2handler3");
  }

  @Test
  public void testMultipleHandlersMixed() throws Exception {
    router.route().failureHandler(routingContext -> {
      routingContext.put("response", "fhandler1");
      routingContext.next();
    });

    router.get("/:param").handler(routingContext -> {
      if (routingContext.pathParam("param").equals("fail")) routingContext.fail(500);
      routingContext.put("response", "handler1");
      routingContext.next();
    }).handler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "handler2");
      routingContext.next();
    }).handler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.end(routingContext.get("response") + "handler3");
    }).failureHandler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "fhandler2");
      routingContext.next();
    }).failureHandler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.setStatusMessage("ERROR");
      response.setStatusCode(500);
      response.end(routingContext.get("response") + "fhandler3");
    });
    testRequest(HttpMethod.GET, "/path", 200, "OK", "handler1handler2handler3");
    testRequest(HttpMethod.GET, "/fail", 500, "ERROR", "fhandler1fhandler2fhandler3");
  }

  @Test
  public void testMultipleHandlersMultipleConnections() throws Exception {
    router.get("/path").handler(routingContext -> {
      routingContext.put("response", "handler1");
      routingContext.next();
    }).handler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "handler2");
      routingContext.next();
    }).handler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.end(routingContext.get("response") + "handler3");
    });
    CountDownLatch latch = new CountDownLatch(100);

    for (int i = 0; i < 100; i++) {
      vertx.executeBlocking(future -> {
        try {
          testSyncRequest("GET", "/path", 200, "OK", "handler1handler2handler3");
          future.complete();
        } catch (Exception e) {
          e.printStackTrace();
          future.fail(e);
        }
      }, asyncResult -> {
        assertFalse(asyncResult.failed());
        assertNull(asyncResult.cause());
        latch.countDown();
      });
    }
    awaitLatch(latch);
  }

  /*
  This test is for issue #729 and #740 about thread safety and errors of multiple handlers
  In this test case I try 100 connections in separated worker threads with random delays and old fashion Java sync http client.
  I've also added a timer when I call routingContext.next()
   */
  @Test
  public void testMultipleHandlersMultipleConnectionsDelayed() throws Exception {
    router.get("/path").handler(routingContext -> {
      routingContext.put("response", "handler1");
      routingContext.vertx().setTimer((int) (1 + Math.random() * 10), asyncResult -> routingContext.next());
    }).handler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "handler2");
      routingContext.vertx().setTimer((int) (1 + Math.random() * 10), asyncResult -> routingContext.next());
    }).handler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.end(routingContext.get("response") + "handler3");
    });

    CountDownLatch latch = new CountDownLatch(100);
    for (int i = 0; i < 100; i++) {
      // using executeBlocking should create multiple connections
      vertx.executeBlocking(future -> {
        try {
          Thread.sleep((int) (1 + Math.random() * 10));
          testSyncRequest("GET", "/path", 200, "OK", "handler1handler2handler3");
          future.complete();
        } catch (Exception e) {
          future.fail(e);
        }
      }, asyncResult -> {
        assertFalse(asyncResult.failed());
        assertNull(asyncResult.cause());
        latch.countDown();
      });
    }
    awaitLatch(latch);
  }

  /*
    This test is similar to test above but it mixes right and failing requests
   */
  @Test
  public void testMultipleHandlersMultipleConnectionsDelayedMixed() throws Exception {
    router.get("/:param").handler(routingContext -> {
      if (routingContext.pathParam("param").equals("fail")) {
        routingContext.fail(400);
      } else {
        routingContext.put("response", "handler1");
        routingContext.vertx().setTimer((int) (1 + Math.random() * 10), asyncResult -> routingContext.next());
      }
    }).failureHandler(routingContext -> {
      routingContext.put("response", "fhandler1");
      routingContext.vertx().setTimer((int) (1 + Math.random() * 10), asyncResult -> routingContext.next());
    }).handler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "handler2");
      routingContext.vertx().setTimer((int) (1 + Math.random() * 10), asyncResult -> routingContext.next());
    }).handler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.end(routingContext.get("response") + "handler3");
    }).failureHandler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "fhandler2");
      routingContext.vertx().setTimer((int) (1 + Math.random() * 10), asyncResult -> routingContext.next());
    }).failureHandler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.setStatusMessage("ERROR");
      response.setStatusCode(400);
      response.end(routingContext.get("response") + "fhandler3");
    });

    final int multipleConnections = 500;

    CountDownLatch latch = new CountDownLatch(multipleConnections);

    Handler<Promise<Object>> execute200Request = future -> {
      try {
        Thread.sleep((int) (1 + Math.random() * 10));
        testSyncRequest("GET", "/path", 200, "OK", "handler1handler2handler3");
        future.complete();
      } catch (InterruptedException | IOException e) {
        e.printStackTrace();
        future.fail(e);
      }
    };

    Handler<Promise<Object>> execute400Request = future -> {
      try {
        Thread.sleep((int) (1 + Math.random() * 10));
        testSyncRequest("GET", "/fail", 400, "ERROR", "fhandler1fhandler2fhandler3");
        future.complete();
      } catch (InterruptedException | IOException e) {
        e.printStackTrace();
        future.fail(e);
      }
    };

    for (int i = 0; i < multipleConnections; i++) {
      // using executeBlocking should create multiple connections
      vertx.executeBlocking((new Random().nextBoolean() ? execute200Request : execute400Request), false, objectAsyncResult -> {
        assertTrue(objectAsyncResult.succeeded());
        latch.countDown();
      });
    }
    awaitLatch(latch);
  }


  @Test
  public void testMultipleSetHandlerMultipleRouteObject() throws Exception {
    router.get("/path").handler(routingContext -> {
      routingContext.put("response", "handler1");
      routingContext.next();
    });
    router.get("/path").handler(routingContext -> {
      routingContext.put("response", routingContext.get("response") + "handler2");
      routingContext.next();
    }).handler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response.setChunked(true);
      response.end(routingContext.get("response") + "handler3");
    });
    testRequest(HttpMethod.GET, "/path", 200, "OK", "handler1handler2handler3");
  }

  @Test
  public void testSetRegexGroupsNamesMethod() throws Exception {
    List<String> groupNames = new ArrayList<>();
    groupNames.add("hello");

    Route route1 = router.getWithRegex("\\/(?<p0>[a-z]{2})");
    route1.setRegexGroupsNames(groupNames);
    route1.handler(routingContext -> routingContext
      .response()
      .setStatusCode(200)
      .setStatusMessage(routingContext.pathParam("hello"))
      .end());
    testRequest(HttpMethod.GET, "/hi", 200, "hi");

  }

  @Test
  public void testRegexGroupsNamesWithMethodOverride() throws Exception {
    List<String> groupNames = new ArrayList<>();
    groupNames.add("FirstParam");
    groupNames.add("SecondParam");

    Route route = router.getWithRegex("\\/([a-z]{2})([a-z]{2})");
    route.setRegexGroupsNames(groupNames);
    route.handler(routingContext -> routingContext
      .response()
      .setStatusCode(200)
      .setStatusMessage(routingContext.pathParam("FirstParam") + "-" + routingContext.pathParam("SecondParam"))
      .end());
    testRequest(HttpMethod.GET, "/aabb", 200, "aa-bb");
  }

  @Test
  public void testSetRegexGroupsNamesMethodWithUnorderedGroups() throws Exception {
    List<String> groupNames = new ArrayList<>();
    groupNames.add("firstParam");
    groupNames.add("secondParam");

    Route route1 = router.getWithRegex("\\/(?<p1>[a-z]{2})(?<p0>[a-z]{2})");
    route1.setRegexGroupsNames(groupNames);
    route1.handler(routingContext -> routingContext
      .response()
      .setStatusCode(200)
      .setStatusMessage(routingContext.pathParam("firstParam") + "-" + routingContext.pathParam("secondParam"))
      .end());
    testRequest(HttpMethod.GET, "/bbaa", 200, "aa-bb");

  }

  @Test
  public void testSetRegexGroupsNamesMethodWithNestedRegex() throws Exception {
    List<String> groupNames = new ArrayList<>();
    groupNames.add("firstParam");
    groupNames.add("secondParam");

    Route route1 = router.getWithRegex("\\/(?<p1>[a-z]{2}(?<p0>[a-z]{2}))");
    route1.setRegexGroupsNames(groupNames);
    route1.handler(routingContext -> routingContext
      .response()
      .setStatusCode(200)
      .setStatusMessage(routingContext.pathParam("firstParam") + "-" + routingContext.pathParam("secondParam"))
      .end());
    testRequest(HttpMethod.GET, "/bbaa", 200, "aa-bbaa");

  }

  @Test
  public void testRegexGroupsNames() throws Exception {
    router.getWithRegex("\\/(?<firstParam>[a-z]{2})(?<secondParam>[a-z]{2})").handler(routingContext -> routingContext
      .response()
      .setStatusCode(200)
      .setStatusMessage(routingContext.pathParam("firstParam") + "-" + routingContext.pathParam("secondParam"))
      .end());
    testRequest(HttpMethod.GET, "/aabb", 200, "aa-bb");
  }

  @Test
  public void testRegexGroupsNamesWithNestedGroups() throws Exception {
    router.getWithRegex("\\/(?<secondParam>[a-z]{2}(?<firstParam>[a-z]{2}))").handler(routingContext -> routingContext
      .response()
      .setStatusCode(200)
      .setStatusMessage(routingContext.pathParam("firstParam") + "-" + routingContext.pathParam("secondParam"))
      .end());
    testRequest(HttpMethod.GET, "/bbaa", 200, "aa-bbaa");
  }

  private Handler<RoutingContext> generateHandler(final int i) {
    return routingContext -> routingContext.put(Integer.toString(i), i).next();
  }

  @Test
  public void stressTestMultipleHandlers() throws Exception {
    final int HANDLERS_NUMBER = 100;
    final int REQUESTS_NUMBER = 200;

    Route r = router.get("/path");
    for (int i = 0; i < HANDLERS_NUMBER; i++) {
      r.handler(generateHandler(i));
    }
    r.handler(routingContext -> {
      StringBuilder sum = new StringBuilder();
      for (int i = 0; i < HANDLERS_NUMBER; i++) {
        sum.append((Integer) routingContext.get(Integer.toString(i)));
      }
      routingContext.response()
        .setStatusCode(200)
        .setStatusMessage("OK")
        .end(sum.toString());
    });

    CountDownLatch latch = new CountDownLatch(REQUESTS_NUMBER);
    final StringBuilder sum = new StringBuilder();
    for (int i = 0; i < HANDLERS_NUMBER; i++) {
      sum.append(i);
    }
    for (int i = 0; i < REQUESTS_NUMBER; i++) {
      // using executeBlocking should create multiple connections
      vertx.executeBlocking(future -> {
        try {
          Thread.sleep((int) (1 + Math.random() * 10));
          testSyncRequest("GET", "/path", 200, "OK", sum.toString());
          future.complete();
        } catch (Exception e) {
          future.fail(e);
        }
      }, asyncResult -> {
        assertFalse(asyncResult.failed());
        assertNull(asyncResult.cause());
        latch.countDown();
      });
    }
    awaitLatch(latch);
  }

  @Test
  public void testDecodingError() throws Exception {
    String BAD_PARAM = "[email protected]\\||$%^&*()_=-%22;;%27%22:%3C%3E/?]}{";

    router.route().handler(rc -> {
      rc.queryParams(); // Trigger decoding
      rc.next();
    });
    router.route("/path").handler(rc -> rc.response().setStatusCode(500).end());
    testRequest(HttpMethod.GET, "/path?q=" + BAD_PARAM, 400, "Bad Request");
  }

  @Test
  public void testRoutePathNoSlashBegin() throws Exception {
    String path = "?test=something";
    router.route().handler(rc -> rc.response().end());
    testRequest(HttpMethod.GET, path, 400, "Bad Request");
  }

  @Test
  public void testMultipleHandlersWithFailuresDeadlock() throws Exception {
    AtomicBoolean first = new AtomicBoolean(true);
    CountDownLatch firstHandlerLatch = new CountDownLatch(1);
    CountDownLatch secondHandlerLatch = new CountDownLatch(1);

    router.get("/path").handler(event -> {
      if (!first.compareAndSet(true, false)) {
        // Second run, block until the second handler runs
        try {
          firstHandlerLatch.countDown();
          awaitLatch(secondHandlerLatch);

          // Add a small delay so the exception handler happens first
          Thread.sleep(100);
        } catch (InterruptedException e) {
          // ignore
        }

        event.next();
      } else {
        vertx.executeBlocking(future -> {
          event.next();
          future.complete();
        }, asyncResult -> {});
      }
    });

    router.get("/path").handler(event -> {
      try {
        awaitLatch(firstHandlerLatch);
      } catch (InterruptedException e) {
        // ignore
      }
      secondHandlerLatch.countDown();
      event.fail(new NullPointerException());
    });

    CountDownLatch latch = new CountDownLatch(2);
    for (int i = 0; i < 2; i++) {
      vertx.executeBlocking(future -> {
        HttpServerRequest request = mock(HttpServerRequest.class);
        HttpServerResponse response = mock(HttpServerResponse.class);
        when(request.method()).thenReturn(HttpMethod.GET);
        when(request.scheme()).thenReturn("http");
        when(request.uri()).thenReturn("http://localhost/path");
        when(request.absoluteURI()).thenReturn("http://localhost/path");
        when(request.host()).thenReturn("localhost");
        when(request.path()).thenReturn("/path");
        when(request.response()).thenReturn(response);
        when(response.ended()).thenReturn(true);
        router.handle(request);
        future.complete();
      }, false, asyncResult -> {
        assertFalse(asyncResult.failed());
        assertNull(asyncResult.cause());
        latch.countDown();
      });
    }
    awaitLatch(latch);
  }

  @Test
  public void testCustom404ErrorHandler() throws Exception {
    // Default 404 handler
    testRequest(HttpMethod.GET, "/blah", 404, "Not Found", "<html><body><h1>Resource not found</h1></body></html>");
    router.errorHandler(404, routingContext -> routingContext
      .response()
      .setStatusMessage("Not Found")
      .setStatusCode(404)
      .end("Not Found custom error")
    );
    testRequest(HttpMethod.GET, "/blah", 404, "Not Found", "Not Found custom error");
  }

  @Test
  public void testDecodingErrorCustomHandler() throws Exception {
    String BAD_PARAM = "[email protected]\\||$%^&*()_=-%22;;%27%22:%3C%3E/?]}{";

    router.errorHandler(400, context -> context.response().setStatusCode(500).setStatusMessage("Dumb").end());

    router.route().handler(rc -> {
      rc.queryParams(); // Trigger decoding
      rc.next();
    }).handler(rc -> {
      rc.response().setStatusCode(500).end();
    });
    testRequest(HttpMethod.GET, "/path?q=" + BAD_PARAM, 500,"Dumb");
  }

  @Test
  public void testCustomErrorHandler() throws Exception {

    router.route("/path").handler(rc -> rc.fail(410));
    router.errorHandler(410, context -> context.response().setStatusCode(500).setStatusMessage("Dumb").end());

    testRequest(HttpMethod.GET, "/path", 500, "Dumb");
  }

  @Test
  public void testErrorInCustomErrorHandler() throws Exception {

    router.route("/path").handler(rc -> rc.fail(410));
    router.errorHandler(410, rc -> {
      throw new RuntimeException();
    });

    testRequest(HttpMethod.GET, "/path", 410, "Gone");
  }

  @Test
  public void testErrorHandlingResponseClosed() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    HttpClientRequest req = client.request(HttpMethod.GET, server.actualPort(), "localhost", "/path");
    router.route().handler(rc -> {
      req.connection().close();
      rc.response().closeHandler(v -> rc.next());
    });
    router.route("/path").handler(rc -> rc.response().write(""));
    router.errorHandler(500, rc -> {
      assertEquals(1, latch.getCount());
      latch.countDown();
    });
    req.end();
    latch.await();
  }

  @Test
  public void testMethodNotAllowedCustomErrorHandler() throws Exception {
    router.get("/path").handler(rc -> rc.response().end());
    router.post("/path").handler(rc -> rc.response().end());
    router.errorHandler(405, context -> context.response().setStatusCode(context.statusCode()).setStatusMessage("Dumb").end());

    testRequest(HttpMethod.PUT, "/path", 405, "Dumb");
  }

  @Test
  public void testNotAcceptableCustomErrorHandler() throws Exception {
    router.route().produces("text/html").handler(rc -> rc.response().end());
    router.errorHandler(406, context -> context.response().setStatusCode(context.statusCode()).setStatusMessage("Dumb").end());

    testRequestWithAccepts(HttpMethod.GET, "/foo", "something/html", 406, "Dumb");
  }

  @Test
  public void testUnsupportedMediaTypeCustomErrorHandler() throws Exception {
    router.route().consumes("text/html").handler(rc -> rc.response().end());
    router.errorHandler(415, context -> context.response().setStatusCode(context.statusCode()).setStatusMessage("Dumb").end());

    testRequestWithContentType(HttpMethod.GET, "/foo", "something/html", 415, "Dumb");
  }

  @Test
  public void testMethodNotAllowedStatusCode() throws Exception {
    router.get("/path").handler(rc -> rc.response().end());
    router.post("/path").handler(rc -> rc.response().end());
    router.put("/hello").handler(rc -> rc.response().end());

    testRequest(HttpMethod.PUT, "/path", HttpResponseStatus.METHOD_NOT_ALLOWED);
  }

  @Test
  public void testNotAcceptableStatusCode() throws Exception {
    router.route().produces("text/html").handler(rc -> rc.response().end());
    router.route("/hello").produces("something/html").handler(rc -> rc.response().end());

    testRequestWithAccepts(HttpMethod.GET, "/foo", "something/html", 406, HttpResponseStatus.NOT_ACCEPTABLE.reasonPhrase());
  }

  @Test
  public void testUnsupportedMediaTypeStatusCode() throws Exception {
    router.route().consumes("text/html").handler(rc -> rc.response().end());
    router.get("/hello").consumes("something/html").handler(rc -> rc.response().end());

    testRequestWithContentType(HttpMethod.GET, "/foo", "something/html", 415, HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE.reasonPhrase());
  }

  @Test
  public void testVHost() throws Exception {
    router.route().virtualHost("*.com").handler(ctx -> ctx.response().end());

    router.route().handler(ctx -> ctx.fail(500));

    testRequest(HttpMethod.GET, "/", req -> req.setAuthority("www.mysite.com"), 200, "OK", null);
  }

  @Test
  public void testVHostShouldFail() throws Exception {
    router.route().virtualHost("*.com").handler(ctx -> ctx.response().end());

    router.route().handler(ctx -> ctx.fail(500));

    testRequest(HttpMethod.GET, "/", req -> req.setAuthority("www.mysite.net"), 500, "Internal Server Error", null);
  }

  @Test
  public void testOverlappingRoutes() throws Exception {
    router.route(HttpMethod.PUT, "/foo/:param1").order(1).handler(routingContext -> {
      fail("Should not route to PUT");
    });
    router.route(HttpMethod.GET, "/foo/:param2").order(10).handler(routingContext -> {
      if (routingContext.pathParam("param1") != null) {
        fail("Should not have parameter from the other route.");
      }
      if (routingContext.pathParam("param2") == null) {
        fail("Should have parameter from the other route.");
      }
      routingContext.response().end("done");
    });

    testRequest(HttpMethod.GET, "/foo/bar", HttpResponseStatus.OK);
  }

  @Test
  public void testToString() {
    // Check we can compute toString() without infinite recursion
    assertNotNull(router.toString());
    Route route = router.route("/foo/:param1");
    assertNotNull(router.toString());
    assertNotNull(route.toString());
  }

  @Test
  public void testRouteMatching() throws Exception {
    router.route("/foo/bar/").handler(rc -> rc.response().setStatusMessage("socks").end());
    testRequest(HttpMethod.GET, "/foo/bar", 404, "Not Found");
    testRequest(HttpMethod.GET, "/foo/bar/", 200, "socks");
    testRequest(HttpMethod.GET, "/foo/bar/baz", 404, "Not Found");
    testRequest(HttpMethod.GET, "/foo/b", 404, "Not Found");
    testRequest(HttpMethod.GET, "/f", 404, "Not Found");
  }

  @Test
  public void testRouteCustomVerb() throws Exception {
    router
      .route()
      .method(HttpMethod.valueOf("MKCOL"))
      .handler(rc -> rc.response().setStatusMessage("socks").end());

    testRequest(HttpMethod.MKCOL, "/", 200, "socks");
  }
}