/*
 * Copyright 2020 ConsenSys AG.
 *
 * 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 tech.pegasys.ethsigner.tests.tls;

import static io.netty.handler.codec.http.HttpResponseStatus.BAD_GATEWAY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static tech.pegasys.ethsigner.tests.WaitUtils.waitFor;
import static tech.pegasys.ethsigner.tests.dsl.Gas.GAS_PRICE;
import static tech.pegasys.ethsigner.tests.dsl.Gas.INTRINSIC_GAS;
import static tech.pegasys.ethsigner.tests.tls.support.CertificateHelpers.populateFingerprintFile;

import tech.pegasys.ethsigner.core.config.KeyStoreOptions;
import tech.pegasys.ethsigner.core.config.tls.client.ClientTlsOptions;
import tech.pegasys.ethsigner.tests.dsl.node.NodeConfiguration;
import tech.pegasys.ethsigner.tests.dsl.node.NodeConfigurationBuilder;
import tech.pegasys.ethsigner.tests.dsl.node.NodePorts;
import tech.pegasys.ethsigner.tests.dsl.signer.Signer;
import tech.pegasys.ethsigner.tests.dsl.signer.SignerConfigurationBuilder;
import tech.pegasys.ethsigner.tests.dsl.tls.TlsCertificateDefinition;
import tech.pegasys.ethsigner.tests.tls.support.MockBalanceReporter;
import tech.pegasys.ethsigner.tests.tls.support.TlsEnabledHttpServerFactory;
import tech.pegasys.ethsigner.tests.tls.support.client.BasicClientTlsOptions;
import tech.pegasys.ethsigner.tests.tls.support.client.BasicKeyStoreOptions;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Optional;

import io.vertx.core.http.HttpServer;
import org.bouncycastle.util.Integers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.exceptions.ClientConnectionException;
import org.web3j.utils.Convert;
import org.web3j.utils.Convert.Unit;

class ClientSideTlsAcceptanceTest {

  private TlsEnabledHttpServerFactory serverFactory;
  private Signer signer;
  private static final int UNUSED_WS_PORT = 0;

  @BeforeEach
  void setup() {
    serverFactory = new TlsEnabledHttpServerFactory();
  }

  @AfterEach
  void cleanup() {
    serverFactory.shutdown();
    if (signer != null) {
      signer.shutdown();
      signer = null;
    }
  }

  private Signer createAndStartSigner(
      final TlsCertificateDefinition presentedCert,
      final TlsCertificateDefinition expectedWeb3ProviderCert,
      final int downstreamWeb3Port,
      final int listenPort,
      final Path workDir)
      throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
    final Signer signer =
        createSigner(
            presentedCert, expectedWeb3ProviderCert, downstreamWeb3Port, listenPort, workDir);
    signer.start();
    signer.awaitStartupCompletion();

    return signer;
  }

  private Signer createSigner(
      final TlsCertificateDefinition presentedCert,
      final TlsCertificateDefinition expectedWeb3ProviderCert,
      final int downstreamWeb3Port,
      final int listenPort,
      final Path workDir)
      throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {

    final Path clientPasswordFile =
        Files.writeString(workDir.resolve("clientKeystorePassword"), presentedCert.getPassword());

    final Path fingerPrintFilePath = workDir.resolve("known_servers");
    final SignerConfigurationBuilder builder = new SignerConfigurationBuilder();
    final Optional<Integer> downstreamWeb3ServerPort =
        Optional.of(Integers.valueOf(downstreamWeb3Port));

    populateFingerprintFile(
        fingerPrintFilePath, expectedWeb3ProviderCert, downstreamWeb3ServerPort);

    final KeyStoreOptions keyStoreOptions =
        new BasicKeyStoreOptions(presentedCert.getPkcs12File().toPath(), clientPasswordFile);
    final ClientTlsOptions clientTlsOptions =
        new BasicClientTlsOptions(keyStoreOptions, Optional.of(fingerPrintFilePath), true);
    builder.withDownstreamTlsOptions(clientTlsOptions);

    builder.withHttpRpcPort(listenPort);

    final NodeConfiguration nodeConfig = new NodeConfigurationBuilder().build();
    final NodePorts nodePorts = new NodePorts(downstreamWeb3Port, UNUSED_WS_PORT);

    signer = new Signer(builder.build(), nodeConfig, nodePorts);

    return signer;
  }

  @Test
  void ethSignerProvidesSpecifiedClientCertificateToDownStreamServer(@TempDir Path workDir)
      throws Exception {

    final TlsCertificateDefinition serverCert =
        TlsCertificateDefinition.loadFromResource("tls/cert1.pfx", "password");
    final TlsCertificateDefinition ethSignerCert =
        TlsCertificateDefinition.loadFromResource("tls/cert2.pfx", "password2");

    // Note: the HttpServer always responds with a JsonRpcSuccess, result=300.
    final HttpServer web3ProviderHttpServer =
        serverFactory.create(serverCert, ethSignerCert, workDir);

    signer =
        createAndStartSigner(
            ethSignerCert, serverCert, web3ProviderHttpServer.actualPort(), 0, workDir);

    assertThat(signer.accounts().balance("0x123456"))
        .isEqualTo(BigInteger.valueOf(MockBalanceReporter.REPORTED_BALANCE));
  }

  @Test
  void ethSignerDoesNotConnectToServerNotSpecifiedInTrustStore(@TempDir Path workDir)
      throws Exception {
    final TlsCertificateDefinition serverPresentedCert =
        TlsCertificateDefinition.loadFromResource("tls/cert1.pfx", "password");
    final TlsCertificateDefinition ethSignerCert =
        TlsCertificateDefinition.loadFromResource("tls/cert2.pfx", "password2");
    final TlsCertificateDefinition ethSignerExpectedServerCert =
        TlsCertificateDefinition.loadFromResource("tls/cert2.pfx", "password2");

    final HttpServer web3ProviderHttpServer =
        serverFactory.create(serverPresentedCert, ethSignerCert, workDir);

    signer =
        createAndStartSigner(
            ethSignerCert,
            ethSignerExpectedServerCert,
            web3ProviderHttpServer.actualPort(),
            0,
            workDir);

    assertThatThrownBy(() -> signer.accounts().balance("0x123456"))
        .isInstanceOf(ClientConnectionException.class)
        .hasMessageContaining(String.valueOf(BAD_GATEWAY.code()));

    // ensure submitting a transaction results in the same behaviour
    final Transaction transaction =
        Transaction.createEtherTransaction(
            signer.accounts().richBenefactor().address(),
            null,
            GAS_PRICE,
            INTRINSIC_GAS,
            "0x1b00ba00ca00bb00aa00bc00be00ac00ca00da00",
            Convert.toWei("1.75", Unit.ETHER).toBigIntegerExact());

    assertThatThrownBy(() -> signer.transactions().submit(transaction))
        .isInstanceOf(ClientConnectionException.class)
        .hasMessageContaining(String.valueOf(BAD_GATEWAY.code()));
  }

  @Test
  void missingKeyStoreForEthSignerResultsInEthSignerTerminating(@TempDir Path workDir)
      throws Exception {
    final TlsCertificateDefinition serverPresentedCert =
        TlsCertificateDefinition.loadFromResource("tls/cert1.pfx", "password");
    final TlsCertificateDefinition ethSignerCert =
        new TlsCertificateDefinition(
            workDir.resolve("Missing_keyStore").toFile(), "arbitraryPassword");

    // Ports are arbitrary as EthSigner should exit
    signer = createSigner(ethSignerCert, serverPresentedCert, 9000, 9001, workDir);
    signer.start();
    waitFor(() -> assertThat(signer.isRunning()).isFalse());
  }

  @Test
  void incorrectPasswordForDownstreamKeyStoreResultsInEthSignerTerminating(@TempDir Path workDir)
      throws Exception {
    final TlsCertificateDefinition serverPresentedCert =
        TlsCertificateDefinition.loadFromResource("tls/cert1.pfx", "password");
    final TlsCertificateDefinition ethSignerCert =
        TlsCertificateDefinition.loadFromResource("tls/cert1.pfx", "wrong_password");

    // Ports are arbitrary as EthSigner should exit
    signer = createSigner(ethSignerCert, serverPresentedCert, 9000, 9001, workDir);
    signer.start();
    waitFor(() -> assertThat(signer.isRunning()).isFalse());
  }
}