/*
 * Copyright 2000-2015 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.util.net.ssl;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Ref;
import com.intellij.testFramework.LightPlatformTestCase;
import com.intellij.testFramework.PlatformTestUtil;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.security.cert.X509Certificate;
import java.util.concurrent.Callable;

import static com.intellij.util.net.ssl.ConfirmingTrustManager.MutableTrustManager;


/**
 * @author Mikhail Golubev
 */
public class CertificateTest extends LightPlatformTestCase {
  @NonNls private static final String AUTHORITY_CN = "certificates-tests.labs.intellij.net";

  @NonNls private static final String TRUSTED_CERT_CN = "trusted.certificates-tests.labs.intellij.net";
  @NonNls private static final String EXPIRED_CERT_CN = "expired.certificates-tests.labs.intellij.net";
  @NonNls private static final String SELF_SIGNED_CERT_CN = "self-signed.certificates-tests.labs.intellij.net";

  // this is the only type of certificates, which 'Common Name' field doesn't match URL of server, where it's located
  @NonNls private static final String WRONG_HOSTNAME_CERT_CN = "illegal.certificates-tests.labs.intellij.net";
  @NonNls private static final String WRONG_HOSTNAME_CERT_URL = "https://wrong-hostname.certificates-tests.labs.intellij.net";

  // TODO: Add proper tests of client authentication, when it'll be supported (see IDEA-124209).
  // By now client certificate should be specified manually via VM options like -Djavax.net.ssl.keyStore.
  @SuppressWarnings("UnusedDeclaration") @NonNls private static final String CLIENT_AUTH_CERT_CN = "client-auth.certificates-tests.labs.intellij.net";

  //private static final Logger LOG = Logger.getInstance(CertificateTest.class);

  private CloseableHttpClient myClient;
  private MutableTrustManager myTrustManager;
  private CertificateManager myCertificateManager;
  private X509Certificate myAuthorityCertificate;


  public void testSetUp() throws Exception {
    assertTrue(myTrustManager.containsCertificate(AUTHORITY_CN));
  }

  /**
   * Test that expired certificate doesn't pass JSSE timestamp check and hence untrusted and added explicitly, although
   * issued by our test CA.
   */
  public void testExpiredCertificate() throws Exception {
    doTest(EXPIRED_CERT_CN, true);
  }

  /**
   * Test that self-signed certificate, that wasn't issued by out test CA, is untrusted and thus added explicitly.
   */
  public void testSelfSignedCertificate() throws Exception {
    doTest(SELF_SIGNED_CERT_CN, true);
  }

  /**
   * Hostname validity check (see {@link org.apache.http.conn.ssl.X509HostnameVerifier}) is disabled for now, so
   * it merely tests that even certificate with illegal CN field (i.e. it doesn't match requested URL).
   * is trusted, because issued by our test CA.
   */
  public void testWrongHostnameCertificate() throws Exception {
    // wrong hostname doesn't lead to any warning by now, thus it's treated the same as trusted certificate
    doTest(WRONG_HOSTNAME_CERT_URL, WRONG_HOSTNAME_CERT_CN, false);
  }

  /**
   * Test that certificate with correct hostname, validity terms and issued by our test CA is trusted.
   */
  public void testTrustedCertificate() throws Exception {
    doTest(TRUSTED_CERT_CN, false);
  }


  private void doTest(@NonNls String alias, boolean willBeAdded) throws Exception {
    doTest("https://" + alias, alias, willBeAdded);
  }

  private void doTest(@NotNull String url, @NotNull String alias, boolean added) throws Exception {
    CloseableHttpResponse response = myClient.execute(new HttpGet(url));
    try {
      assertEquals(response.getStatusLine().getStatusCode(), HttpStatus.SC_OK);
    }
    finally {
      response.close();
    }
    if (added) {
      assertTrue(myTrustManager.containsCertificate(alias));
      assertEquals(2, myTrustManager.getCertificates().size());
    }
    else {
      // only CA certificate
      assertEquals(1, myTrustManager.getCertificates().size());
    }
  }

  public void testDeadlockDetection() throws Exception {
    final Ref<Throwable> throwableRef = new Ref<>();

    final long interruptionTimeout = CertificateManager.DIALOG_VISIBILITY_TIMEOUT + 1000;
    // Will be interrupted after at most interruptionTimeout (6 seconds originally)
    Thread[] t = {null};
    ApplicationManager.getApplication().invokeAndWait(() -> {
      final Thread thread = new Thread(() -> {
        try {
          boolean accepted = CertificateManager.showAcceptDialog(() -> {
            // this dialog will be attempted to show only if blocking thread was forcibly interrupted after timeout
            throw new AssertionError("Deadlock was not detected in time");
          });
          // should be rejected after 5 seconds
          assertFalse("Certificate should be rejected", accepted);
        }
        catch (Throwable e) {
          throwableRef.set(e);
        }
      }, "Test EDT-blocking thread");
      thread.start();
      try {
        thread.join(interruptionTimeout);
      }
      catch (InterruptedException ignored) {
        // No one will attempt to interrupt EDT, right?
      }
      finally {
        if (thread.isAlive()) {
          thread.interrupt();
          fail("Deadlock was not detected in time");
        }
      }
      t[0] = thread;
    }, ModalityState.any());
    if (!throwableRef.isNull()) {
      throw new AssertionError(throwableRef.get());
    }
    t[0].join();
  }

  @Override
  public void setUp() throws Exception {
    super.setUp();
    myCertificateManager = CertificateManager.getInstance();
    // add CA certificate
    myTrustManager = myCertificateManager.getCustomTrustManager();
    myAuthorityCertificate = CertificateUtil.loadX509Certificate(getTestDataPath() + "certificates/ca.crt");

    myClient = HttpClientBuilder.create()
      .setSslcontext(myCertificateManager.getSslContext())
      .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
      .build();

    myTrustManager.addCertificate(myAuthorityCertificate);
  }

  @Override
  public void tearDown() throws Exception {
    try {
      assertTrue(myTrustManager.removeAllCertificates());
      assertEmpty(myTrustManager.getCertificates());
    }
    finally {
      try {
        myClient.close();
      }
      finally {
        super.tearDown();
      }
    }
  }

  private static String getTestDataPath() {
    return PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/platform/platform-tests/testData/";
  }
}
