package com.quorum.tessera.transaction.internal;

import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import com.quorum.tessera.data.*;
import com.quorum.tessera.enclave.*;
import com.quorum.tessera.encryption.EncryptorException;
import com.quorum.tessera.encryption.Nonce;
import com.quorum.tessera.encryption.PublicKey;
import com.quorum.tessera.transaction.*;
import com.quorum.tessera.transaction.exception.MandatoryRecipientsNotAvailableException;
import com.quorum.tessera.transaction.exception.PrivacyViolationException;
import com.quorum.tessera.transaction.exception.RecipientKeyNotFoundException;
import com.quorum.tessera.transaction.exception.TransactionNotFoundException;
import com.quorum.tessera.transaction.publish.BatchPayloadPublisher;
import com.quorum.tessera.transaction.resend.ResendManager;
import java.util.*;
import java.util.concurrent.Callable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

public class TransactionManagerTest {

  private TransactionManager transactionManager;

  private EncryptedTransactionDAO encryptedTransactionDAO;

  private EncryptedRawTransactionDAO encryptedRawTransactionDAO;

  private ResendManager resendManager;

  private Enclave enclave;

  private PayloadDigest mockDigest;

  private PrivacyHelper privacyHelper;

  private BatchPayloadPublisher batchPayloadPublisher;

  @Before
  public void onSetUp() {
    enclave = mock(Enclave.class);
    encryptedTransactionDAO = mock(EncryptedTransactionDAO.class);
    encryptedRawTransactionDAO = mock(EncryptedRawTransactionDAO.class);
    resendManager = mock(ResendManager.class);
    privacyHelper = new PrivacyHelperImpl(encryptedTransactionDAO, true);
    batchPayloadPublisher = mock(BatchPayloadPublisher.class);
    mockDigest = cipherText -> cipherText;

    transactionManager =
        new TransactionManagerImpl(
            enclave,
            encryptedTransactionDAO,
            encryptedRawTransactionDAO,
            resendManager,
            batchPayloadPublisher,
            privacyHelper,
            mockDigest);
  }

  @After
  public void onTearDown() {
    verifyNoMoreInteractions(enclave, resendManager, batchPayloadPublisher);
    verifyNoMoreInteractions(encryptedTransactionDAO);
  }

  @Test
  public void send() {

    EncodedPayload encodedPayload = mock(EncodedPayload.class);
    when(encodedPayload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());

    when(enclave.encryptPayload(any(), any(), any(), any())).thenReturn(encodedPayload);

    PublicKey sender = PublicKey.from("SENDER".getBytes());
    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());

    when(enclave.getPublicKeys()).thenReturn(Set.of(receiver));

    byte[] payload = Base64.getEncoder().encode("PAYLOAD".getBytes());

    SendRequest sendRequest = mock(SendRequest.class);
    when(sendRequest.getPayload()).thenReturn(payload);
    when(sendRequest.getSender()).thenReturn(sender);
    when(sendRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendRequest.getPrivacyMode()).thenReturn(PrivacyMode.STANDARD_PRIVATE);

    SendResponse result = transactionManager.send(sendRequest);

    assertThat(result).isNotNull();
    assertThat(result.getTransactionHash().toString()).isEqualTo("Q0lQSEVSVEVYVA==");
    assertThat(result.getManagedParties()).containsExactly(receiver);

    verify(enclave).encryptPayload(any(), any(), any(), any());
    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class), any(Callable.class));
    verify(enclave).getForwardingKeys();
    verify(enclave).getPublicKeys();
  }

  @Test
  public void sendWithMandatoryRecipients() {
    EncodedPayload encodedPayload = mock(EncodedPayload.class);

    when(encodedPayload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());

    when(enclave.encryptPayload(any(), any(), any(), any())).thenReturn(encodedPayload);

    PublicKey sender = PublicKey.from("SENDER".getBytes());
    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());

    when(enclave.getPublicKeys()).thenReturn(Set.of(receiver));

    byte[] payload = Base64.getEncoder().encode("PAYLOAD".getBytes());

    SendRequest sendRequest = mock(SendRequest.class);
    when(sendRequest.getPayload()).thenReturn(payload);
    when(sendRequest.getSender()).thenReturn(sender);
    when(sendRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendRequest.getPrivacyMode()).thenReturn(PrivacyMode.MANDATORY_RECIPIENTS);
    when(sendRequest.getMandatoryRecipients()).thenReturn(Set.of(receiver));

    ArgumentCaptor<PrivacyMetadata> metadataArgCaptor =
        ArgumentCaptor.forClass(PrivacyMetadata.class);

    SendResponse result = transactionManager.send(sendRequest);

    assertThat(result).isNotNull();
    assertThat(result.getTransactionHash().toString()).isEqualTo("Q0lQSEVSVEVYVA==");
    assertThat(result.getManagedParties()).containsExactly(receiver);

    verify(enclave).encryptPayload(any(), any(), any(), metadataArgCaptor.capture());
    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class), any(Callable.class));
    verify(enclave).getForwardingKeys();
    verify(enclave).getPublicKeys();

    final PrivacyMetadata metadata = metadataArgCaptor.getValue();

    assertThat(metadata.getPrivacyMode()).isEqualTo(PrivacyMode.MANDATORY_RECIPIENTS);
    assertThat(metadata.getMandatoryRecipients()).containsExactly(receiver);
  }

  @Test
  public void sendAlsoWithPublishCallbackCoverage() {

    EncodedPayload encodedPayload = mock(EncodedPayload.class);

    when(encodedPayload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());

    when(enclave.encryptPayload(
            any(byte[].class), any(PublicKey.class), anyList(), any(PrivacyMetadata.class)))
        .thenReturn(encodedPayload);

    doAnswer(
            invocation -> {
              Callable callable = invocation.getArgument(1);
              callable.call();
              return mock(EncryptedTransaction.class);
            })
        .when(encryptedTransactionDAO)
        .save(any(EncryptedTransaction.class), any(Callable.class));

    PublicKey sender = PublicKey.from("SENDER".getBytes());
    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());

    byte[] payload = Base64.getEncoder().encode("PAYLOAD".getBytes());

    SendRequest sendRequest = mock(SendRequest.class);
    when(sendRequest.getPayload()).thenReturn(payload);
    when(sendRequest.getSender()).thenReturn(sender);
    when(sendRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendRequest.getPrivacyMode()).thenReturn(PrivacyMode.STANDARD_PRIVATE);

    SendResponse result = transactionManager.send(sendRequest);

    assertThat(result).isNotNull();
    assertThat(result.getTransactionHash().toString()).isEqualTo("Q0lQSEVSVEVYVA==");
    assertThat(result.getManagedParties()).isEmpty();

    verify(enclave)
        .encryptPayload(
            any(byte[].class), any(PublicKey.class), anyList(), any(PrivacyMetadata.class));
    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class), any(Callable.class));
    verify(enclave).getForwardingKeys();
    verify(enclave).getPublicKeys();
    verify(batchPayloadPublisher).publishPayload(any(), anyList());
  }

  @Test
  public void sendWithDuplicateRecipients() {
    EncodedPayload encodedPayload = mock(EncodedPayload.class);

    when(encodedPayload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());

    when(enclave.encryptPayload(any(), any(), any(), any())).thenReturn(encodedPayload);
    when(enclave.getForwardingKeys()).thenReturn(Set.of(PublicKey.from("RECEIVER".getBytes())));

    PublicKey sender = PublicKey.from("SENDER".getBytes());
    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());

    byte[] payload = Base64.getEncoder().encode("PAYLOAD".getBytes());

    SendRequest sendRequest = mock(SendRequest.class);
    when(sendRequest.getPayload()).thenReturn(payload);
    when(sendRequest.getSender()).thenReturn(sender);
    when(sendRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendRequest.getPrivacyMode()).thenReturn(PrivacyMode.STANDARD_PRIVATE);

    SendResponse result = transactionManager.send(sendRequest);

    assertThat(result).isNotNull();
    assertThat(result.getTransactionHash().toString()).isEqualTo("Q0lQSEVSVEVYVA==");
    assertThat(result.getManagedParties()).isEmpty();

    verify(enclave).encryptPayload(any(), any(), any(), any());
    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class), any(Callable.class));
    verify(enclave).getForwardingKeys();
    verify(enclave).getPublicKeys();
  }

  @Test
  public void sendSignedTransaction() {

    EncodedPayload payload = mock(EncodedPayload.class);
    MessageHash hash = new MessageHash("HASH".getBytes());

    EncryptedRawTransaction encryptedRawTransaction = mock(EncryptedRawTransaction.class);
    when(encryptedRawTransaction.getHash()).thenReturn(hash);

    when(encryptedRawTransaction.toRawTransaction()).thenReturn(mock((RawTransaction.class)));

    when(encryptedRawTransactionDAO.retrieveByHash(hash))
        .thenReturn(Optional.of(encryptedRawTransaction));

    when(payload.getCipherText()).thenReturn("ENCRYPTED_PAYLOAD".getBytes());

    when(enclave.encryptPayload(any(RawTransaction.class), any(), any())).thenReturn(payload);

    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());

    when(enclave.getPublicKeys()).thenReturn(Set.of(receiver));

    SendSignedRequest sendSignedRequest = mock(SendSignedRequest.class);
    when(sendSignedRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendSignedRequest.getSignedData()).thenReturn("HASH".getBytes());
    when(sendSignedRequest.getPrivacyMode()).thenReturn(PrivacyMode.STANDARD_PRIVATE);

    SendResponse result = transactionManager.sendSignedTransaction(sendSignedRequest);

    assertThat(result).isNotNull();
    assertThat(result.getTransactionHash()).isEqualTo(new MessageHash("HASH".getBytes()));
    assertThat(result.getManagedParties()).containsExactly(receiver);

    ArgumentCaptor<PrivacyMetadata> data = ArgumentCaptor.forClass(PrivacyMetadata.class);

    verify(enclave).encryptPayload(any(RawTransaction.class), any(), data.capture());
    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class), any(Callable.class));
    verify(encryptedRawTransactionDAO).retrieveByHash(any(MessageHash.class));
    verify(enclave).getForwardingKeys();
    verify(enclave).getPublicKeys();

    final PrivacyMetadata passingData = data.getValue();
    assertThat(passingData.getPrivacyMode()).isEqualTo(PrivacyMode.STANDARD_PRIVATE);
    assertThat(passingData.getPrivacyGroupId()).isNotPresent();
  }

  @Test
  public void sendSignedTransactionWithMandatoryRecipients() {

    EncodedPayload payload = mock(EncodedPayload.class);
    MessageHash hash = new MessageHash("HASH".getBytes());

    EncryptedRawTransaction encryptedRawTransaction = mock(EncryptedRawTransaction.class);
    when(encryptedRawTransaction.getHash()).thenReturn(hash);
    when(encryptedRawTransaction.toRawTransaction()).thenReturn(mock((RawTransaction.class)));

    when(encryptedRawTransactionDAO.retrieveByHash(eq(hash)))
        .thenReturn(Optional.of(encryptedRawTransaction));

    when(payload.getCipherText()).thenReturn("ENCRYPTED_PAYLOAD".getBytes());

    when(enclave.encryptPayload(any(RawTransaction.class), any(), any())).thenReturn(payload);

    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());

    when(enclave.getPublicKeys()).thenReturn(Set.of(receiver));

    SendSignedRequest sendSignedRequest = mock(SendSignedRequest.class);
    when(sendSignedRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendSignedRequest.getSignedData()).thenReturn("HASH".getBytes());
    when(sendSignedRequest.getPrivacyMode()).thenReturn(PrivacyMode.MANDATORY_RECIPIENTS);
    when(sendSignedRequest.getMandatoryRecipients()).thenReturn(Set.of(receiver));

    SendResponse result = transactionManager.sendSignedTransaction(sendSignedRequest);

    assertThat(result).isNotNull();
    assertThat(result.getTransactionHash()).isEqualTo(new MessageHash("HASH".getBytes()));
    assertThat(result.getManagedParties()).containsExactly(receiver);

    ArgumentCaptor<PrivacyMetadata> data = ArgumentCaptor.forClass(PrivacyMetadata.class);

    verify(enclave).encryptPayload(any(RawTransaction.class), any(), data.capture());
    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class), any(Callable.class));
    verify(encryptedRawTransactionDAO).retrieveByHash(any(MessageHash.class));
    verify(enclave).getForwardingKeys();
    verify(enclave).getPublicKeys();

    final PrivacyMetadata passingData = data.getValue();
    assertThat(passingData.getPrivacyMode()).isEqualTo(PrivacyMode.MANDATORY_RECIPIENTS);
    assertThat(passingData.getPrivacyGroupId()).isNotPresent();
    assertThat(passingData.getMandatoryRecipients()).containsExactly(receiver);
  }

  @Test
  public void sendSignedTransactionWithCallbackCoverage() {

    EncodedPayload payload = mock(EncodedPayload.class);
    MessageHash hash = new MessageHash("HASH".getBytes());

    EncryptedRawTransaction encryptedRawTransaction = mock(EncryptedRawTransaction.class);
    when(encryptedRawTransaction.getHash()).thenReturn(hash);
    when(encryptedRawTransaction.toRawTransaction()).thenReturn(mock((RawTransaction.class)));

    when(encryptedRawTransactionDAO.retrieveByHash(eq(hash)))
        .thenReturn(Optional.of(encryptedRawTransaction));

    doAnswer(
            invocation -> {
              Callable callable = invocation.getArgument(1);
              callable.call();
              return mock(EncryptedTransaction.class);
            })
        .when(encryptedTransactionDAO)
        .save(any(EncryptedTransaction.class), any(Callable.class));

    when(payload.getCipherText()).thenReturn("ENCRYPTED_PAYLOAD".getBytes());

    when(enclave.encryptPayload(any(RawTransaction.class), any(), any())).thenReturn(payload);

    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());

    SendSignedRequest sendSignedRequest = mock(SendSignedRequest.class);
    when(sendSignedRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendSignedRequest.getSignedData()).thenReturn("HASH".getBytes());
    when(sendSignedRequest.getPrivacyMode()).thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);
    when(sendSignedRequest.getAffectedContractTransactions()).thenReturn(emptySet());
    when(sendSignedRequest.getExecHash()).thenReturn("execHash".getBytes());
    when(sendSignedRequest.getPrivacyGroupId())
        .thenReturn(Optional.of(PrivacyGroup.Id.fromBytes("group".getBytes())));

    SendResponse result = transactionManager.sendSignedTransaction(sendSignedRequest);

    assertThat(result).isNotNull();
    assertThat(result.getTransactionHash()).isEqualTo(new MessageHash("HASH".getBytes()));
    assertThat(result.getManagedParties()).isEmpty();

    ArgumentCaptor<PrivacyMetadata> data = ArgumentCaptor.forClass(PrivacyMetadata.class);

    verify(enclave).encryptPayload(any(RawTransaction.class), any(), data.capture());
    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class), any(Callable.class));
    verify(encryptedRawTransactionDAO).retrieveByHash(any(MessageHash.class));
    verify(enclave).getForwardingKeys();
    verify(enclave).getPublicKeys();
    verify(batchPayloadPublisher).publishPayload(any(), anyList());

    final PrivacyMetadata passingData = data.getValue();
    assertThat(passingData.getPrivacyMode()).isEqualTo(PrivacyMode.PRIVATE_STATE_VALIDATION);
    assertThat(passingData.getAffectedContractTransactions()).isEmpty();
    assertThat(passingData.getExecHash()).isEqualTo("execHash".getBytes());
    assertThat(passingData.getPrivacyGroupId())
        .isPresent()
        .get()
        .isEqualTo(PrivacyGroup.Id.fromBytes("group".getBytes()));
  }

  @Test
  public void sendSignedTransactionWithDuplicateRecipients() {
    EncodedPayload payload = mock(EncodedPayload.class);
    MessageHash hash = new MessageHash("HASH".getBytes());

    EncryptedRawTransaction encryptedRawTransaction = mock(EncryptedRawTransaction.class);
    when(encryptedRawTransaction.getHash()).thenReturn(hash);
    when(encryptedRawTransaction.toRawTransaction()).thenReturn(mock((RawTransaction.class)));

    when(encryptedRawTransactionDAO.retrieveByHash(eq(hash)))
        .thenReturn(Optional.of(encryptedRawTransaction));

    when(payload.getCipherText()).thenReturn("ENCRYPTED_PAYLOAD".getBytes());
    when(enclave.getForwardingKeys()).thenReturn(Set.of(PublicKey.from("RECEIVER".getBytes())));
    when(enclave.encryptPayload(any(RawTransaction.class), any(), any())).thenReturn(payload);

    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());

    SendSignedRequest sendSignedRequest = mock(SendSignedRequest.class);
    when(sendSignedRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendSignedRequest.getSignedData()).thenReturn("HASH".getBytes());
    when(sendSignedRequest.getPrivacyMode()).thenReturn(PrivacyMode.STANDARD_PRIVATE);

    SendResponse result = transactionManager.sendSignedTransaction(sendSignedRequest);

    assertThat(result).isNotNull();
    assertThat(result.getTransactionHash()).isEqualTo(new MessageHash("HASH".getBytes()));
    assertThat(result.getManagedParties()).isEmpty();

    verify(enclave).encryptPayload(any(RawTransaction.class), any(), any());
    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class), any(Callable.class));
    verify(encryptedRawTransactionDAO).retrieveByHash(any(MessageHash.class));
    verify(enclave).getForwardingKeys();
    verify(enclave).getPublicKeys();
  }

  @Test
  public void sendSignedTransactionNoRawTransactionFoundException() {

    when(encryptedRawTransactionDAO.retrieveByHash(any(MessageHash.class)))
        .thenReturn(Optional.empty());

    PublicKey receiver = PublicKey.from("RECEIVER".getBytes());
    SendSignedRequest sendSignedRequest = mock(SendSignedRequest.class);
    when(sendSignedRequest.getRecipients()).thenReturn(List.of(receiver));
    when(sendSignedRequest.getSignedData()).thenReturn("HASH".getBytes());

    try {
      transactionManager.sendSignedTransaction(sendSignedRequest);
      failBecauseExceptionWasNotThrown(TransactionNotFoundException.class);
    } catch (TransactionNotFoundException ex) {
      verify(encryptedRawTransactionDAO).retrieveByHash(any(MessageHash.class));
      verify(enclave).getForwardingKeys();
    }
  }

  @Test
  public void delete() {

    MessageHash messageHash = mock(MessageHash.class);

    transactionManager.delete(messageHash);

    verify(encryptedTransactionDAO).delete(messageHash);
  }

  @Test
  public void storePayloadAsRecipient() {
    EncodedPayload payload = mock(EncodedPayload.class);

    when(payload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(encryptedTransactionDAO.retrieveByHash(any(MessageHash.class)))
        .thenReturn(Optional.empty());

    transactionManager.storePayload(payload);

    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class));
    verify(encryptedTransactionDAO).retrieveByHash(any(MessageHash.class));
    verify(enclave).getPublicKeys();
    verify(enclave).findInvalidSecurityHashes(any(EncodedPayload.class), anyList());
  }

  @Test
  public void storePayloadWhenWeAreSender() {
    final PublicKey senderKey = PublicKey.from("SENDER".getBytes());

    final EncodedPayload encodedPayload = mock(EncodedPayload.class);
    when(encodedPayload.getSenderKey()).thenReturn(senderKey);
    when(encodedPayload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(encodedPayload.getRecipientBoxes()).thenReturn(new ArrayList<>());
    when(encodedPayload.getRecipientKeys()).thenReturn(new ArrayList<>());

    when(enclave.getPublicKeys()).thenReturn(singleton(senderKey));

    transactionManager.storePayload(encodedPayload);

    verify(resendManager).acceptOwnMessage(encodedPayload);
    verify(enclave).getPublicKeys();
    verify(enclave).findInvalidSecurityHashes(any(EncodedPayload.class), anyList());
  }

  @Test
  public void storePayloadWhenWeAreSenderWithPrivateStateConsensus() {
    final PublicKey senderKey = PublicKey.from("SENDER".getBytes());

    final EncodedPayload encodedPayload = mock(EncodedPayload.class);
    when(encodedPayload.getSenderKey()).thenReturn(senderKey);
    when(encodedPayload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(encodedPayload.getCipherTextNonce()).thenReturn(null);
    when(encodedPayload.getRecipientBoxes()).thenReturn(emptyList());
    when(encodedPayload.getRecipientNonce()).thenReturn(null);
    when(encodedPayload.getRecipientKeys()).thenReturn(emptyList());
    when(encodedPayload.getPrivacyMode()).thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);
    when(encodedPayload.getAffectedContractTransactions()).thenReturn(emptyMap());
    when(encodedPayload.getExecHash()).thenReturn(new byte[0]);

    when(enclave.getPublicKeys()).thenReturn(singleton(senderKey));

    transactionManager.storePayload(encodedPayload);

    verify(resendManager).acceptOwnMessage(encodedPayload);

    verify(enclave).getPublicKeys();
    verify(enclave).findInvalidSecurityHashes(any(), any());
  }

  @Test
  public void storePayloadAsRecipientWithPrivateStateConsensus() {
    EncodedPayload payload = mock(EncodedPayload.class);

    when(payload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(payload.getPrivacyMode()).thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);

    when(encryptedTransactionDAO.retrieveByHash(any(MessageHash.class)))
        .thenReturn(Optional.empty());

    transactionManager.storePayload(payload);

    verify(encryptedTransactionDAO).save(any(EncryptedTransaction.class));
    verify(encryptedTransactionDAO).retrieveByHash(any(MessageHash.class));
    verify(enclave).getPublicKeys();
    verify(enclave).findInvalidSecurityHashes(any(), any());
  }

  @Test
  public void storePayloadAsRecipientWithAffectedContractTxsButPsvFlagMismatched() {

    final byte[] affectedContractPayload = "SOMEOTHERDATA".getBytes();
    final PublicKey senderKey = PublicKey.from("sender".getBytes());

    final EncodedPayload payload = mock(EncodedPayload.class);
    final EncryptedTransaction affectedContractTx = mock(EncryptedTransaction.class);
    final EncodedPayload affectedContractEncodedPayload = mock(EncodedPayload.class);

    Map<TxHash, SecurityHash> affectedContractTransactionHashes = new HashMap<>();
    affectedContractTransactionHashes.put(
        new TxHash(
            "bfMIqWJ/QGQhkK4USxMBxduzfgo/SIGoCros5bWYfPKUBinlAUCqLVOUAP9q+BgLlsWni1M6rnzfmaqSw2J5hQ=="),
        SecurityHash.from("securityHash".getBytes()));

    when(affectedContractTx.getPayload()).thenReturn(affectedContractEncodedPayload);
    when(affectedContractTx.getHash())
        .thenReturn(
            new MessageHash(
                new TxHash(
                        "bfMIqWJ/QGQhkK4USxMBxduzfgo/SIGoCros5bWYfPKUBinlAUCqLVOUAP9q+BgLlsWni1M6rnzfmaqSw2J5hQ==")
                    .getBytes()));
    when(payload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(payload.getPrivacyMode()).thenReturn(PrivacyMode.STANDARD_PRIVATE);
    when(affectedContractEncodedPayload.getPrivacyMode())
        .thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);
    when(payload.getAffectedContractTransactions()).thenReturn(affectedContractTransactionHashes);
    when(payload.getSenderKey()).thenReturn(senderKey);
    when(affectedContractEncodedPayload.getRecipientKeys()).thenReturn(List.of(senderKey));

    when(encryptedTransactionDAO.findByHashes(any())).thenReturn(List.of(affectedContractTx));
    when(affectedContractTx.getEncodedPayload()).thenReturn(affectedContractPayload);

    transactionManager.storePayload(payload);
    // Ignore transaction - not save
    verify(encryptedTransactionDAO).findByHashes(any());
  }

  @Test
  public void storePayloadSenderNotGenuineACOTHNotFound() {
    final byte[] input = "SOMEDATA".getBytes();
    final byte[] affectedContractPayload = "SOMEOTHERDATA".getBytes();
    final PublicKey senderKey = PublicKey.from("sender".getBytes());

    final EncodedPayload payload = mock(EncodedPayload.class);
    final EncryptedTransaction affectedContractTx = mock(EncryptedTransaction.class);
    final EncodedPayload affectedContractEncodedPayload = mock(EncodedPayload.class);

    final TxHash txHash =
        new TxHash(
            "bfMIqWJ/QGQhkK4USxMBxduzfgo/SIGoCros5bWYfPKUBinlAUCqLVOUAP9q+BgLlsWni1M6rnzfmaqSw2J5hQ==");

    Map<TxHash, SecurityHash> affectedContractTransactionHashes = new HashMap<>();
    affectedContractTransactionHashes.put(txHash, SecurityHash.from("securityHash".getBytes()));
    affectedContractTransactionHashes.put(
        new TxHash(
            "bfMIqWJ/QGQhkK4USxMBxduzfgo/SIGoCros5bWYfPKUBinlAUCqLVOUAP9q+BgLlsWni1M6rnzfmaqSr5J5hQ=="),
        SecurityHash.from("bogus".getBytes()));

    when(affectedContractTx.getEncodedPayload()).thenReturn(input);
    when(payload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(payload.getPrivacyMode()).thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);
    when(affectedContractEncodedPayload.getPrivacyMode())
        .thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);
    when(payload.getAffectedContractTransactions()).thenReturn(affectedContractTransactionHashes);
    when(payload.getSenderKey()).thenReturn(senderKey);
    when(affectedContractEncodedPayload.getRecipientKeys()).thenReturn(List.of(senderKey));

    when(encryptedTransactionDAO.findByHashes(List.of(new MessageHash(txHash.getBytes()))))
        .thenReturn(List.of(affectedContractTx));
    when(affectedContractTx.getEncodedPayload()).thenReturn(affectedContractPayload);

    transactionManager.storePayload(payload);
    // Ignore transaction - not save
    verify(encryptedTransactionDAO, times(0)).save(any(EncryptedTransaction.class));
    verify(encryptedTransactionDAO).findByHashes(any());
  }

  @Test
  public void storePayloadSenderNotInRecipientList() {
    final byte[] input = "SOMEDATA".getBytes();
    final byte[] affectedContractPayload = "SOMEOTHERDATA".getBytes();
    final PublicKey senderKey = PublicKey.from("sender".getBytes());
    final PublicKey someOtherKey = PublicKey.from("otherKey".getBytes());

    final EncodedPayload payload = mock(EncodedPayload.class);
    final EncryptedTransaction affectedContractTx = mock(EncryptedTransaction.class);
    final EncodedPayload affectedContractEncodedPayload = mock(EncodedPayload.class);

    final TxHash txHash =
        new TxHash(
            "bfMIqWJ/QGQhkK4USxMBxduzfgo/SIGoCros5bWYfPKUBinlAUCqLVOUAP9q+BgLlsWni1M6rnzfmaqSw2J5hQ==");

    Map<TxHash, SecurityHash> affectedContractTransactionHashes = new HashMap<>();
    affectedContractTransactionHashes.put(txHash, SecurityHash.from("securityHash".getBytes()));
    affectedContractTransactionHashes.put(
        new TxHash(
            "bfMIqWJ/QGQhkK4USxMBxduzfgo/SIGoCros5bWYfPKUBinlAUCqLVOUAP9q+BgLlsWni1M6rnzfmaqSr5J5hQ=="),
        SecurityHash.from("bogus".getBytes()));

    when(affectedContractTx.getEncodedPayload()).thenReturn(input);
    when(affectedContractTx.getHash()).thenReturn(new MessageHash(txHash.getBytes()));
    when(payload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(payload.getPrivacyMode()).thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);
    when(affectedContractEncodedPayload.getPrivacyMode())
        .thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);
    when(payload.getAffectedContractTransactions()).thenReturn(affectedContractTransactionHashes);
    when(payload.getSenderKey()).thenReturn(senderKey);
    when(affectedContractEncodedPayload.getRecipientKeys()).thenReturn(List.of(someOtherKey));

    when(encryptedTransactionDAO.findByHashes(any())).thenReturn(List.of(affectedContractTx));
    when(affectedContractTx.getPayload()).thenReturn(affectedContractEncodedPayload);

    transactionManager.storePayload(payload);
    // Ignore transaction - not save
    verify(encryptedTransactionDAO).findByHashes(any());
  }

  @Test
  public void storePayloadPsvWithInvalidSecurityHashes() {

    EncodedPayload payload = mock(EncodedPayload.class);
    when(payload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(payload.getPrivacyMode()).thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);

    when(enclave.findInvalidSecurityHashes(any(), any()))
        .thenReturn(singleton(new TxHash("invalidHash".getBytes())));

    assertThatExceptionOfType(PrivacyViolationException.class)
        .describedAs("There are privacy violation for psv")
        .isThrownBy(() -> transactionManager.storePayload(payload))
        .withMessageContaining("Invalid security hashes identified for PSC TX");

    verify(enclave).findInvalidSecurityHashes(any(), any());
  }

  @Test
  public void storePayloadWithInvalidSecurityHashesIgnoreIfNotPsv() {
    Map<TxHash, SecurityHash> affectedTx =
        Map.of(TxHash.from("invalidHash".getBytes()), SecurityHash.from("security".getBytes()));

    final EncodedPayload payload = mock(EncodedPayload.class);
    when(payload.getSenderKey()).thenReturn(PublicKey.from("sender".getBytes()));
    when(payload.getCipherText()).thenReturn("CIPHERTEXT".getBytes());
    when(payload.getCipherTextNonce()).thenReturn(new Nonce("nonce".getBytes()));
    when(payload.getRecipientBoxes()).thenReturn(List.of(RecipientBox.from("box1".getBytes())));
    when(payload.getRecipientNonce()).thenReturn(new Nonce("recipientNonce".getBytes()));
    when(payload.getRecipientKeys())
        .thenReturn(singletonList(PublicKey.from("recipient".getBytes())));
    when(payload.getPrivacyMode()).thenReturn(PrivacyMode.PARTY_PROTECTION);
    when(payload.getAffectedContractTransactions()).thenReturn(affectedTx);

    when(encryptedTransactionDAO.retrieveByHash(any(MessageHash.class)))
        .thenReturn(Optional.empty());

    ArgumentCaptor<EncryptedTransaction> txCaptor =
        ArgumentCaptor.forClass(EncryptedTransaction.class);
    when(enclave.findInvalidSecurityHashes(any(), any()))
        .thenReturn(singleton(new TxHash("invalidHash".getBytes())));

    transactionManager.storePayload(payload);

    verify(encryptedTransactionDAO).save(txCaptor.capture());

    EncodedPayload sanitisedPayload = txCaptor.getValue().getPayload();

    // Assert that the invalid ACOTH had been removed
    assertThat(
            sanitisedPayload
                .getAffectedContractTransactions()
                .get(TxHash.from("invalidHash".getBytes())))
        .isNull();

    verify(encryptedTransactionDAO).findByHashes(any());
    verify(encryptedTransactionDAO).retrieveByHash(any(MessageHash.class));
    verify(enclave).getPublicKeys();
    verify(enclave).findInvalidSecurityHashes(any(), any());
  }

  @Test
  public void storePayloadWithExistingRecipientAndMismatchedContents() {

    EncryptedTransaction existingDatabaseEntry = mock(EncryptedTransaction.class);
    EncodedPayload existingPayload = mock(EncodedPayload.class);
    when(existingPayload.getCipherText()).thenReturn("ct1".getBytes());
    when(existingDatabaseEntry.getPayload()).thenReturn(existingPayload);
    when(encryptedTransactionDAO.retrieveByHash(any(MessageHash.class)))
        .thenReturn(Optional.of(existingDatabaseEntry));

    EncodedPayload payloadToStore = mock(EncodedPayload.class);
    when(payloadToStore.getCipherText()).thenReturn("ct2".getBytes());

    final Throwable throwable =
        catchThrowable(() -> transactionManager.storePayload(payloadToStore));

    assertThat(throwable)
        .isInstanceOf(RuntimeException.class)
        .hasMessage("Invalid existing transaction");

    verify(encryptedTransactionDAO).retrieveByHash(any(MessageHash.class));
    verify(enclave).getPublicKeys();
    verify(enclave).findInvalidSecurityHashes(any(EncodedPayload.class), anyList());
  }

  @Test
  public void storePayloadWithExistingRecipientPSVRecipientNotFound() {
    EncryptedTransaction existingDatabaseEntry = mock(EncryptedTransaction.class);

    EncodedPayload existingPayload = mock(EncodedPayload.class);
    when(existingPayload.getCipherText()).thenReturn("ct1".getBytes());
    when(existingPayload.getPrivacyMode()).thenReturn(PrivacyMode.PRIVATE_STATE_VALIDATION);
    when(existingPayload.getExecHash()).thenReturn("execHash".getBytes());

    when(existingDatabaseEntry.getPayload()).thenReturn(existingPayload);
    when(encryptedTransactionDAO.retrieveByHash(any(MessageHash.class)))
