/*
 * Copyright 2020 NEM
 *
 * 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 io.nem.symbol.core.crypto;

import io.nem.symbol.core.utils.ConvertUtils;
import io.nem.symbol.sdk.infrastructure.RandomUtils;
import org.hamcrest.MatcherAssert;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNot;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public abstract class BlockCipherTest {

    @Test
    void encryptedDataCanBeDecrypted() {
        // Arrange:
        final CryptoEngine engine = this.getCryptoEngine();
        final KeyPair kp = KeyPair.random(engine);
        final BlockCipher blockCipher = this.getBlockCipher(kp, kp);
        final byte[] input = RandomUtils.generateRandomBytes();

        // Act:
        final byte[] encryptedBytes = blockCipher.encrypt(input);
        final byte[] decryptedBytes = blockCipher.decrypt(encryptedBytes);

        // Assert:
        MatcherAssert.assertThat(encryptedBytes, IsNot.not(IsEqual.equalTo(decryptedBytes)));
        MatcherAssert.assertThat(decryptedBytes, IsEqual.equalTo(input));
    }

    @Test
    void dataCanBeEncryptedWithSenderPrivateKeyAndRecipientPublicKey() {
        // Arrange:
        final CryptoEngine engine = this.getCryptoEngine();
        final KeyPair skp = KeyPair.random(engine);
        final KeyPair rkp = KeyPair.random(engine);
        final BlockCipher blockCipher =
            this.getBlockCipher(skp, KeyPair.onlyPublic(rkp.getPublicKey(), engine));
        final byte[] input = RandomUtils.generateRandomBytes();

        // Act:
        final byte[] encryptedBytes = blockCipher.encrypt(input);

        // Assert:
        MatcherAssert.assertThat(encryptedBytes, IsNot.not(IsEqual.equalTo(input)));
    }

    @Test
    void dataCanBeDecryptedWithSenderPublicKeyAndRecipientPrivateKey() {
        // Arrange:
        final CryptoEngine engine = this.getCryptoEngine();
        final KeyPair skp = KeyPair.random(engine);
        final KeyPair rkp = KeyPair.random(engine);
        final BlockCipher blockCipher1 =
            this.getBlockCipher(skp, KeyPair.onlyPublic(rkp.getPublicKey(), engine));
        final BlockCipher blockCipher2 =
            this.getBlockCipher(KeyPair.onlyPublic(skp.getPublicKey(), engine), rkp);
        final byte[] input = RandomUtils.generateRandomBytes();

        // Act:
        final byte[] encryptedBytes = blockCipher1.encrypt(input);
        final byte[] decryptedBytes = blockCipher2.decrypt(encryptedBytes);

        // Assert:
        Assertions.assertEquals(ConvertUtils.toHex(decryptedBytes), ConvertUtils.toHex(input));
    }

    @Test
    void dataCanBeDecryptedWithSenderPrivateKeyAndRecipientPublicKey() {
        // Arrange:
        final CryptoEngine engine = this.getCryptoEngine();
        final KeyPair skp = KeyPair.random(engine);
        final KeyPair rkp = KeyPair.random(engine);
        final BlockCipher blockCipher1 =
            this.getBlockCipher(skp, KeyPair.onlyPublic(rkp.getPublicKey(), engine));
        final BlockCipher blockCipher2 =
            this.getBlockCipher(KeyPair.onlyPublic(rkp.getPublicKey(), engine), skp);
        final byte[] input = "Some text".getBytes();

        // Act:
        final byte[] encryptedBytes = blockCipher1.encrypt(input);
        final byte[] decryptedBytes = blockCipher2.decrypt(encryptedBytes);

        // Assert:
        MatcherAssert.assertThat(ConvertUtils.toHex(decryptedBytes),
            IsEqual.equalTo(ConvertUtils.toHex(input)));
    }

    @Test
    void dataEncryptedWithPrivateKeyCanOnlyBeDecryptedByMatchingPublicKey(
    ) {
        // Arrange:
        final CryptoEngine engine = this.getCryptoEngine();
        final BlockCipher blockCipher1 =
            this.getBlockCipher(KeyPair.random(engine), KeyPair.random(engine));
        final BlockCipher blockCipher2 =
            this.getBlockCipher(KeyPair.random(engine), KeyPair.random(engine));
        final byte[] input = RandomUtils.generateRandomBytes();

        // Act:
        final byte[] encryptedBytes1 = blockCipher1.encrypt(input);
        final byte[] encryptedBytes2 = blockCipher2.encrypt(input);

        // Assert:
        MatcherAssert.assertThat(blockCipher1.decrypt(encryptedBytes1), IsEqual.equalTo(input));
        MatcherAssert
            .assertThat(blockCipher1.decrypt(encryptedBytes2), IsNot.not(IsEqual.equalTo(input)));
        MatcherAssert
            .assertThat(blockCipher2.decrypt(encryptedBytes1), IsNot.not(IsEqual.equalTo(input)));
        MatcherAssert.assertThat(blockCipher2.decrypt(encryptedBytes2), IsEqual.equalTo(input));
    }

    protected BlockCipher getBlockCipher(
        final KeyPair senderKeyPair, final KeyPair recipientKeyPair) {
        return this.getCryptoEngine()
            .createBlockCipher(senderKeyPair, recipientKeyPair);
    }

    protected abstract CryptoEngine getCryptoEngine();
}