/*
 * Copyright (C) 2016 Google Inc.
 *
 * 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 okhttp3;

import org.junit.Test;

import static okhttp3.CipherSuite.TLS_KRB5_WITH_DES_CBC_MD5;
import static okhttp3.CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5;
import static okhttp3.CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256;
import static okhttp3.CipherSuite.forJavaName;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;

public class CipherSuiteTest {
  @Test public void nullCipherName() {
    try {
      forJavaName(null);
      fail("Should have thrown");
    } catch (NullPointerException expected) {
    }
  }

  @Test public void hashCode_usesIdentityHashCode_legacyCase() {
    CipherSuite cs = TLS_RSA_EXPORT_WITH_RC4_40_MD5; // This one's javaName starts with "SSL_".
    assertEquals(cs.toString(), System.identityHashCode(cs), cs.hashCode());
  }

  @Test public void hashCode_usesIdentityHashCode_regularCase() {
    CipherSuite cs = TLS_RSA_WITH_AES_128_CBC_SHA256; // This one's javaName matches the identifier.
    assertEquals(cs.toString(), System.identityHashCode(cs), cs.hashCode());
  }

  @Test public void instancesAreInterned() {
    assertSame(forJavaName("TestCipherSuite"), forJavaName("TestCipherSuite"));
    assertSame(TLS_KRB5_WITH_DES_CBC_MD5,
        forJavaName(TLS_KRB5_WITH_DES_CBC_MD5.javaName()));
  }

  /**
   * Tests that interned CipherSuite instances remain the case across garbage collections, even if
   * the String used to construct them is no longer strongly referenced outside of the CipherSuite.
   */
  @SuppressWarnings("RedundantStringConstructorCall")
  @Test public void instancesAreInterned_survivesGarbageCollection() {
    // We're not holding onto a reference to this String instance outside of the CipherSuite...
    CipherSuite cs = forJavaName(new String("FakeCipherSuite_instancesAreInterned"));
    System.gc(); // Unless cs references the String instance, it may now be garbage collected.
    assertSame(cs, forJavaName(new String(cs.javaName())));
  }

  @Test public void equals() {
    assertEquals(forJavaName("cipher"), forJavaName("cipher"));
    assertNotEquals(forJavaName("cipherA"), forJavaName("cipherB"));
    assertEquals(forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5"), TLS_RSA_EXPORT_WITH_RC4_40_MD5);
    assertNotEquals(TLS_RSA_EXPORT_WITH_RC4_40_MD5, TLS_RSA_WITH_AES_128_CBC_SHA256);
  }

  @Test public void forJavaName_acceptsArbitraryStrings() {
    // Shouldn't throw.
    forJavaName("example CipherSuite name that is not in the whitelist");
  }

  @Test public void javaName_examples() {
    assertEquals("SSL_RSA_EXPORT_WITH_RC4_40_MD5", TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName());
    assertEquals("TLS_RSA_WITH_AES_128_CBC_SHA256", TLS_RSA_WITH_AES_128_CBC_SHA256.javaName());
    assertEquals("TestCipherSuite", forJavaName("TestCipherSuite").javaName());
  }

  @Test public void javaName_equalsToString() {
    assertEquals(TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName,
        TLS_RSA_EXPORT_WITH_RC4_40_MD5.toString());
    assertEquals(TLS_RSA_WITH_AES_128_CBC_SHA256.javaName,
        TLS_RSA_WITH_AES_128_CBC_SHA256.toString());
  }

  /**
   * On the Oracle JVM some older cipher suites have the "SSL_" prefix and others have the "TLS_"
   * prefix. On the IBM JVM all cipher suites have the "SSL_" prefix.
   *
   * <p>Prior to OkHttp 3.3.1 we accepted either form and consider them equivalent. And since OkHttp
   * 3.7.0 this is also true. But OkHttp 3.3.1 through 3.6.0 treated these as different.
   */
  @Test public void forJavaName_fromLegacyEnumName() {
    // These would have been considered equal in OkHttp 3.3.1, but now aren't.
    assertEquals(
        forJavaName("TLS_RSA_EXPORT_WITH_RC4_40_MD5"),
        forJavaName("SSL_RSA_EXPORT_WITH_RC4_40_MD5"));
    assertEquals(
        forJavaName("TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA"),
        forJavaName("SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA"));
    assertEquals(
        forJavaName("TLS_FAKE_NEW_CIPHER"),
        forJavaName("SSL_FAKE_NEW_CIPHER"));
  }

  @Test public void applyIntersectionRetainsSslPrefixes() throws Exception {
    FakeSslSocket socket = new FakeSslSocket();
    socket.setEnabledProtocols(new String[] { "TLSv1" });
    socket.setSupportedCipherSuites(new String[] { "SSL_A", "SSL_B", "SSL_C", "SSL_D", "SSL_E" });
    socket.setEnabledCipherSuites(new String[] { "SSL_A", "SSL_B", "SSL_C" });

    ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
        .tlsVersions(TlsVersion.TLS_1_0)
        .cipherSuites("TLS_A", "TLS_C", "TLS_E")
        .build();
    connectionSpec.apply(socket, false);

    assertArrayEquals(new String[] { "SSL_A", "SSL_C" }, socket.enabledCipherSuites);
  }

  @Test public void applyIntersectionRetainsTlsPrefixes() throws Exception {
    FakeSslSocket socket = new FakeSslSocket();
    socket.setEnabledProtocols(new String[] { "TLSv1" });
    socket.setSupportedCipherSuites(new String[] { "TLS_A", "TLS_B", "TLS_C", "TLS_D", "TLS_E" });
    socket.setEnabledCipherSuites(new String[] { "TLS_A", "TLS_B", "TLS_C" });

    ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
        .tlsVersions(TlsVersion.TLS_1_0)
        .cipherSuites("SSL_A", "SSL_C", "SSL_E")
        .build();
    connectionSpec.apply(socket, false);

    assertArrayEquals(new String[] { "TLS_A", "TLS_C" }, socket.enabledCipherSuites);
  }

  @Test public void applyIntersectionAddsSslScsvForFallback() throws Exception {
    FakeSslSocket socket = new FakeSslSocket();
    socket.setEnabledProtocols(new String[] { "TLSv1" });
    socket.setSupportedCipherSuites(new String[] { "SSL_A", "SSL_FALLBACK_SCSV" });
    socket.setEnabledCipherSuites(new String[] { "SSL_A" });

    ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
        .tlsVersions(TlsVersion.TLS_1_0)
        .cipherSuites("SSL_A")
        .build();
    connectionSpec.apply(socket, true);

    assertArrayEquals(new String[] { "SSL_A", "SSL_FALLBACK_SCSV" }, socket.enabledCipherSuites);
  }

  @Test public void applyIntersectionAddsTlsScsvForFallback() throws Exception {
    FakeSslSocket socket = new FakeSslSocket();
    socket.setEnabledProtocols(new String[] { "TLSv1" });
    socket.setSupportedCipherSuites(new String[] { "TLS_A", "TLS_FALLBACK_SCSV" });
    socket.setEnabledCipherSuites(new String[] { "TLS_A" });

    ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
        .tlsVersions(TlsVersion.TLS_1_0)
        .cipherSuites("TLS_A")
        .build();
    connectionSpec.apply(socket, true);

    assertArrayEquals(new String[] { "TLS_A", "TLS_FALLBACK_SCSV" }, socket.enabledCipherSuites);
  }

  @Test public void applyIntersectionToProtocolVersion() throws Exception {
    FakeSslSocket socket = new FakeSslSocket();
    socket.setEnabledProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" });
    socket.setSupportedCipherSuites(new String[] { "TLS_A" });
    socket.setEnabledCipherSuites(new String[] { "TLS_A" });

    ConnectionSpec connectionSpec = new ConnectionSpec.Builder(true)
        .tlsVersions(TlsVersion.TLS_1_1, TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
        .cipherSuites("TLS_A")
        .build();
    connectionSpec.apply(socket, false);

    assertArrayEquals(new String[] { "TLSv1.1", "TLSv1.2" }, socket.enabledProtocols);
  }

  static final class FakeSslSocket extends DelegatingSSLSocket {
    private String[] enabledProtocols;
    private String[] supportedCipherSuites;
    private String[] enabledCipherSuites;

    FakeSslSocket() {
      super(null);
    }

    @Override public String[] getEnabledProtocols() {
      return enabledProtocols;
    }

    @Override public void setEnabledProtocols(String[] enabledProtocols) {
      this.enabledProtocols = enabledProtocols;
    }

    @Override public String[] getSupportedCipherSuites() {
      return supportedCipherSuites;
    }

    public void setSupportedCipherSuites(String[] supportedCipherSuites) {
      this.supportedCipherSuites = supportedCipherSuites;
    }

    @Override public String[] getEnabledCipherSuites() {
      return enabledCipherSuites;
    }

    @Override public void setEnabledCipherSuites(String[] enabledCipherSuites) {
      this.enabledCipherSuites = enabledCipherSuites;
    }
  }
}
