public class TestUtils {

  public static final String USER = "******";
  public static final String ADMIN = "admin";
  public static final String TEXT_HTML_CONTENT_TYPE_WITH_UTF_8_CHARSET = "text/html; charset=UTF-8";
  public static final String TEXT_HTML_CONTENT_TYPE_WITH_ISO_8859_1_CHARSET =
      "text/html; charset=ISO-8859-1";
  private static final File TMP_DIR =
      new File(
          System.getProperty("java.io.tmpdir"),
          "ahc-tests-" + UUID.randomUUID().toString().substring(0, 8));
  public static final byte[] PATTERN_BYTES =
      "FooBarBazQixFooBarBazQixFooBarBazQixFooBarBazQixFooBarBazQixFooBarBazQix"
          .getBytes(Charset.forName("UTF-16"));
  public static final File LARGE_IMAGE_FILE;
  public static byte[] LARGE_IMAGE_BYTES;
  public static final File SIMPLE_TEXT_FILE;
  public static final String SIMPLE_TEXT_FILE_STRING;
  private static final LoginService LOGIN_SERVICE =
      new HashLoginService("MyRealm", "src/test/resources/realm.properties");

  static {
    try {
      TMP_DIR.mkdirs();
      TMP_DIR.deleteOnExit();
      LARGE_IMAGE_FILE = new File(TestUtils.class.getClassLoader().getResource("300k.png").toURI());
      LARGE_IMAGE_BYTES = FileUtils.readFileToByteArray(LARGE_IMAGE_FILE);
      SIMPLE_TEXT_FILE =
          new File(TestUtils.class.getClassLoader().getResource("SimpleTextFile.txt").toURI());
      SIMPLE_TEXT_FILE_STRING = FileUtils.readFileToString(SIMPLE_TEXT_FILE, UTF_8);
    } catch (Exception e) {
      throw new ExceptionInInitializerError(e);
    }
  }

  public static synchronized int findFreePort() throws IOException {
    try (ServerSocket socket = new ServerSocket(0)) {
      return socket.getLocalPort();
    }
  }

  public static File createTempFile(int approxSize) throws IOException {
    long repeats = approxSize / TestUtils.PATTERN_BYTES.length + 1;
    File tmpFile = File.createTempFile("tmpfile-", ".data", TMP_DIR);
    tmpFile.deleteOnExit();
    try (FileOutputStream out = new FileOutputStream(tmpFile)) {
      for (int i = 0; i < repeats; i++) {
        out.write(PATTERN_BYTES);
      }

      long expectedFileSize = PATTERN_BYTES.length * repeats;
      assertEquals(tmpFile.length(), expectedFileSize, "Invalid file length");

      return tmpFile;
    }
  }

  public static Server newJettyHttpServer(int port) {
    Server server = new Server();
    addHttpConnector(server, port);
    return server;
  }

  public static void addHttpConnector(Server server, int port) {
    ServerConnector connector = new ServerConnector(server);
    connector.setPort(port);

    server.addConnector(connector);
  }

  public static Server newJettyHttpsServer(int port) throws URISyntaxException {
    Server server = new Server();
    addHttpsConnector(server, port);
    return server;
  }

  public static void addHttpsConnector(Server server, int port) throws URISyntaxException {
    ClassLoader cl = TestUtils.class.getClassLoader();

    URL keystoreUrl = cl.getResource("ssltest-keystore.jks");
    String keyStoreFile = new File(keystoreUrl.toURI()).getAbsolutePath();
    SslContextFactory sslContextFactory = new SslContextFactory(keyStoreFile);
    sslContextFactory.setKeyStorePassword("changeit");

    String trustStoreFile =
        new File(cl.getResource("ssltest-cacerts.jks").toURI()).getAbsolutePath();
    sslContextFactory.setTrustStorePath(trustStoreFile);
    sslContextFactory.setTrustStorePassword("changeit");

    HttpConfiguration httpsConfig = new HttpConfiguration();
    httpsConfig.setSecureScheme("https");
    httpsConfig.setSecurePort(port);
    httpsConfig.addCustomizer(new SecureRequestCustomizer());

    ServerConnector connector =
        new ServerConnector(
            server,
            new SslConnectionFactory(sslContextFactory, "http/1.1"),
            new HttpConnectionFactory(httpsConfig));
    connector.setPort(port);

    server.addConnector(connector);
  }

  public static void addBasicAuthHandler(Server server, Handler handler) {
    addAuthHandler(server, Constraint.__BASIC_AUTH, new BasicAuthenticator(), handler);
  }

  public static void addDigestAuthHandler(Server server, Handler handler) {
    addAuthHandler(server, Constraint.__DIGEST_AUTH, new DigestAuthenticator(), handler);
  }

  private static void addAuthHandler(
      Server server, String auth, LoginAuthenticator authenticator, Handler handler) {

    server.addBean(LOGIN_SERVICE);

    Constraint constraint = new Constraint();
    constraint.setName(auth);
    constraint.setRoles(new String[] {USER, ADMIN});
    constraint.setAuthenticate(true);

    ConstraintMapping mapping = new ConstraintMapping();
    mapping.setConstraint(constraint);
    mapping.setPathSpec("/*");

    Set<String> knownRoles = new HashSet<>();
    knownRoles.add(USER);
    knownRoles.add(ADMIN);

    List<ConstraintMapping> cm = new ArrayList<>();
    cm.add(mapping);

    ConstraintSecurityHandler security = new ConstraintSecurityHandler();
    security.setConstraintMappings(cm, knownRoles);
    security.setAuthenticator(authenticator);
    security.setLoginService(LOGIN_SERVICE);
    security.setHandler(handler);
    server.setHandler(security);
  }

  private static KeyManager[] createKeyManagers() throws GeneralSecurityException, IOException {
    InputStream keyStoreStream =
        Thread.currentThread().getContextClassLoader().getResourceAsStream("ssltest-cacerts.jks");
    char[] keyStorePassword = "******".toCharArray();
    KeyStore ks = KeyStore.getInstance("JKS");
    ks.load(keyStoreStream, keyStorePassword);
    assert (ks.size() > 0);

    // Set up key manager factory to use our key store
    char[] certificatePassword = "******".toCharArray();
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(ks, certificatePassword);

    // Initialize the SSLContext to work with our key managers.
    return kmf.getKeyManagers();
  }

  private static TrustManager[] createTrustManagers() throws GeneralSecurityException, IOException {
    InputStream keyStoreStream =
        Thread.currentThread().getContextClassLoader().getResourceAsStream("ssltest-keystore.jks");
    char[] keyStorePassword = "******".toCharArray();
    KeyStore ks = KeyStore.getInstance("JKS");
    ks.load(keyStoreStream, keyStorePassword);
    assert (ks.size() > 0);

    TrustManagerFactory tmf =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ks);
    return tmf.getTrustManagers();
  }

  public static SSLContext createSSLContext(AtomicBoolean trust) {
    try {
      KeyManager[] keyManagers = createKeyManagers();
      TrustManager[] trustManagers =
          new TrustManager[] {
            dummyTrustManager(trust, (X509TrustManager) createTrustManagers()[0])
          };
      SecureRandom secureRandom = new SecureRandom();

      SSLContext sslContext = SSLContext.getInstance("TLS");
      sslContext.init(keyManagers, trustManagers, secureRandom);

      return sslContext;
    } catch (Exception e) {
      throw new Error("Failed to initialize the server-side SSLContext", e);
    }
  }

  public static class DummyTrustManager implements X509TrustManager {

    private final X509TrustManager tm;
    private final AtomicBoolean trust;

    public DummyTrustManager(final AtomicBoolean trust, final X509TrustManager tm) {
      this.trust = trust;
      this.tm = tm;
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType)
        throws CertificateException {
      tm.checkClientTrusted(chain, authType);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
        throws CertificateException {
      if (!trust.get()) {
        throw new CertificateException("Server certificate not trusted.");
      }
      tm.checkServerTrusted(chain, authType);
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
      return tm.getAcceptedIssuers();
    }
  }

  private static TrustManager dummyTrustManager(
      final AtomicBoolean trust, final X509TrustManager tm) {
    return new DummyTrustManager(trust, tm);
  }

  public static File getClasspathFile(String file) throws FileNotFoundException {
    ClassLoader cl = null;
    try {
      cl = Thread.currentThread().getContextClassLoader();
    } catch (Throwable ex) {
    }
    if (cl == null) {
      cl = TestUtils.class.getClassLoader();
    }
    URL resourceUrl = cl.getResource(file);

    try {
      return new File(new URI(resourceUrl.toString()).getSchemeSpecificPart());
    } catch (URISyntaxException e) {
      throw new FileNotFoundException(file);
    }
  }
}
  /** A builder for {@link Realm} */
  public static class RealmBuilder {

    private static final ThreadLocal<MessageDigest> DIGEST_TL =
        new ThreadLocal<MessageDigest>() {
          @Override
          protected MessageDigest initialValue() {
            try {
              return MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
              throw new RuntimeException(e);
            }
          }
        };

    private String principal;
    private String password;
    private AuthScheme scheme;
    private String realmName;
    private String nonce;
    private String algorithm;
    private String response;
    private String opaque;
    private String qop;
    private String nc = DEFAULT_NC;
    private String cnonce;
    private Uri uri;
    private String methodName = "GET";
    private boolean usePreemptive;
    private String ntlmDomain = System.getProperty("http.auth.ntlm.domain");
    private Charset charset = UTF_8;
    private String ntlmHost = "localhost";
    private boolean useAbsoluteURI = false;
    private boolean omitQuery;

    public RealmBuilder ntlmDomain(String ntlmDomain) {
      this.ntlmDomain = ntlmDomain;
      return this;
    }

    public RealmBuilder ntlmHost(String host) {
      this.ntlmHost = host;
      return this;
    }

    public RealmBuilder principal(String principal) {
      this.principal = principal;
      return this;
    }

    public RealmBuilder password(String password) {
      this.password = password;
      return this;
    }

    public RealmBuilder scheme(AuthScheme scheme) {
      this.scheme = scheme;
      return this;
    }

    public RealmBuilder realmName(String realmName) {
      this.realmName = realmName;
      return this;
    }

    public RealmBuilder nonce(String nonce) {
      this.nonce = nonce;
      return this;
    }

    public RealmBuilder algorithm(String algorithm) {
      this.algorithm = algorithm;
      return this;
    }

    public RealmBuilder response(String response) {
      this.response = response;
      return this;
    }

    public RealmBuilder opaque(String opaque) {
      this.opaque = opaque;
      return this;
    }

    public RealmBuilder qop(String qop) {
      if (isNonEmpty(qop)) {
        this.qop = qop;
      }
      return this;
    }

    public RealmBuilder nc(String nc) {
      this.nc = nc;
      return this;
    }

    public RealmBuilder uri(Uri uri) {
      this.uri = uri;
      return this;
    }

    public RealmBuilder methodName(String methodName) {
      this.methodName = methodName;
      return this;
    }

    public RealmBuilder usePreemptiveAuth(boolean usePreemptiveAuth) {
      this.usePreemptive = usePreemptiveAuth;
      return this;
    }

    public RealmBuilder useAbsoluteURI(boolean useAbsoluteURI) {
      this.useAbsoluteURI = useAbsoluteURI;
      return this;
    }

    public RealmBuilder omitQuery(boolean omitQuery) {
      this.omitQuery = omitQuery;
      return this;
    }

    public RealmBuilder charset(Charset charset) {
      this.charset = charset;
      return this;
    }

    private String parseRawQop(String rawQop) {
      String[] rawServerSupportedQops = rawQop.split(",");
      String[] serverSupportedQops = new String[rawServerSupportedQops.length];
      for (int i = 0; i < rawServerSupportedQops.length; i++) {
        serverSupportedQops[i] = rawServerSupportedQops[i].trim();
      }

      // prefer auth over auth-int
      for (String rawServerSupportedQop : serverSupportedQops) {
        if (rawServerSupportedQop.equals("auth")) return rawServerSupportedQop;
      }

      for (String rawServerSupportedQop : serverSupportedQops) {
        if (rawServerSupportedQop.equals("auth-int")) return rawServerSupportedQop;
      }

      return null;
    }

    public RealmBuilder parseWWWAuthenticateHeader(String headerLine) {
      realmName(match(headerLine, "realm")) //
          .nonce(match(headerLine, "nonce")) //
          .opaque(match(headerLine, "opaque")) //
          .scheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC);
      String algorithm = match(headerLine, "algorithm");
      if (isNonEmpty(algorithm)) {
        algorithm(algorithm);
      }

      // FIXME qop is different with proxy?
      String rawQop = match(headerLine, "qop");
      if (rawQop != null) {
        qop(parseRawQop(rawQop));
      }

      return this;
    }

    public RealmBuilder parseProxyAuthenticateHeader(String headerLine) {
      realmName(match(headerLine, "realm")) //
          .nonce(match(headerLine, "nonce")) //
          .opaque(match(headerLine, "opaque")) //
          .scheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC);
      String algorithm = match(headerLine, "algorithm");
      if (isNonEmpty(algorithm)) {
        algorithm(algorithm);
      }
      // FIXME qop is different with proxy?
      qop(match(headerLine, "qop"));

      return this;
    }

    private void newCnonce(MessageDigest md) {
      byte[] b = new byte[8];
      ThreadLocalRandom.current().nextBytes(b);
      b = md.digest(b);
      cnonce = toHexString(b);
    }

    /** TODO: A Pattern/Matcher may be better. */
    private String match(String headerLine, String token) {
      if (headerLine == null) {
        return null;
      }

      int match = headerLine.indexOf(token);
      if (match <= 0) return null;

      // = to skip
      match += token.length() + 1;
      int trailingComa = headerLine.indexOf(",", match);
      String value =
          headerLine.substring(match, trailingComa > 0 ? trailingComa : headerLine.length());
      value =
          value.length() > 0 && value.charAt(value.length() - 1) == '"'
              ? value.substring(0, value.length() - 1)
              : value;
      return value.charAt(0) == '"' ? value.substring(1) : value;
    }

    private byte[] md5FromRecycledStringBuilder(StringBuilder sb, MessageDigest md) {
      md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
      sb.setLength(0);
      return md.digest();
    }

    private byte[] secretDigest(StringBuilder sb, MessageDigest md) {

      sb.append(principal).append(':').append(realmName).append(':').append(password);
      byte[] ha1 = md5FromRecycledStringBuilder(sb, md);

      if (algorithm == null || algorithm.equals("MD5")) {
        return ha1;
      } else if ("MD5-sess".equals(algorithm)) {
        appendBase16(sb, ha1);
        sb.append(':').append(nonce).append(':').append(cnonce);
        return md5FromRecycledStringBuilder(sb, md);
      }

      throw new UnsupportedOperationException("Digest algorithm not supported: " + algorithm);
    }

    private byte[] dataDigest(StringBuilder sb, String digestUri, MessageDigest md) {

      sb.append(methodName).append(':').append(digestUri);
      if ("auth-int".equals(qop)) {
        sb.append(':').append(EMPTY_ENTITY_MD5);

      } else if (qop != null && !qop.equals("auth")) {
        throw new UnsupportedOperationException("Digest qop not supported: " + qop);
      }

      return md5FromRecycledStringBuilder(sb, md);
    }

    private void appendDataBase(StringBuilder sb) {
      sb.append(':').append(nonce).append(':');
      if ("auth".equals(qop) || "auth-int".equals(qop)) {
        sb.append(nc).append(':').append(cnonce).append(':').append(qop).append(':');
      }
    }

    private void newResponse(MessageDigest md) {
      // BEWARE: compute first as it used the cached StringBuilder
      String digestUri = AuthenticatorUtils.computeRealmURI(uri, useAbsoluteURI, omitQuery);

      StringBuilder sb = StringUtils.stringBuilder();

      // WARNING: DON'T MOVE, BUFFER IS RECYCLED!!!!
      byte[] secretDigest = secretDigest(sb, md);
      byte[] dataDigest = dataDigest(sb, digestUri, md);

      appendBase16(sb, secretDigest);
      appendDataBase(sb);
      appendBase16(sb, dataDigest);

      byte[] responseDigest = md5FromRecycledStringBuilder(sb, md);
      response = toHexString(responseDigest);
    }

    private static String toHexString(byte[] data) {
      StringBuilder buffer = StringUtils.stringBuilder();
      for (int i = 0; i < data.length; i++) {
        buffer.append(Integer.toHexString((data[i] & 0xf0) >>> 4));
        buffer.append(Integer.toHexString(data[i] & 0x0f));
      }
      return buffer.toString();
    }

    private static void appendBase16(StringBuilder buf, byte[] bytes) {
      int base = 16;
      for (byte b : bytes) {
        int bi = 0xff & b;
        int c = '0' + (bi / base) % base;
        if (c > '9') c = 'a' + (c - '0' - 10);
        buf.append((char) c);
        c = '0' + bi % base;
        if (c > '9') c = 'a' + (c - '0' - 10);
        buf.append((char) c);
      }
    }

    /**
     * Build a {@link Realm}
     *
     * @return a {@link Realm}
     */
    public Realm build() {

      // Avoid generating
      if (isNonEmpty(nonce)) {
        MessageDigest md = DIGEST_TL.get();
        newCnonce(md);
        newResponse(md);
      }

      return new Realm(
          scheme,
          principal,
          password,
          realmName,
          nonce,
          algorithm,
          response,
          qop,
          nc,
          cnonce,
          uri,
          methodName,
          usePreemptive,
          ntlmDomain,
          charset,
          ntlmHost,
          opaque,
          useAbsoluteURI,
          omitQuery);
    }
  }