/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.nifi.processors.pgp;

import org.apache.nifi.pgp.service.api.PGPPrivateKeyService;
import org.apache.nifi.pgp.util.PGPSecretKeyGenerator;
import org.apache.nifi.processors.pgp.attributes.CompressionAlgorithm;
import org.apache.nifi.processors.pgp.attributes.FileEncoding;
import org.apache.nifi.processors.pgp.attributes.HashAlgorithm;
import org.apache.nifi.processors.pgp.attributes.SigningStrategy;
import org.apache.nifi.pgp.service.api.KeyIdentifierConverter;
import org.apache.nifi.reporting.InitializationException;
import org.apache.nifi.util.LogMessage;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class SignContentPGPTest {
    private static final String PASSPHRASE = UUID.randomUUID().toString();

    private static final String DATA = String.class.getName();

    private static final byte[] DATA_BINARY = DATA.getBytes(StandardCharsets.UTF_8);

    private static final String SERVICE_ID = PGPPrivateKeyService.class.getName();

    private static final int SIGNATURE_VERSION = 4;

    private static PGPPrivateKey rsaPrivateKey;

    private static PGPPublicKey rsaPublicKey;

    @Mock
    private PGPPrivateKeyService privateKeyService;

    private TestRunner runner;

    @BeforeAll
    public static void setKeys() throws Exception {
        final PGPSecretKey rsaSecretKey = PGPSecretKeyGenerator.generateRsaSecretKey(PASSPHRASE.toCharArray());
        rsaPublicKey = rsaSecretKey.getPublicKey();

        final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().build(PASSPHRASE.toCharArray());
        rsaPrivateKey = rsaSecretKey.extractPrivateKey(decryptor);
    }

    @BeforeEach
    public void setRunner() throws InitializationException {
        runner = TestRunners.newTestRunner(new SignContentPGP());

        when(privateKeyService.getIdentifier()).thenReturn(SERVICE_ID);
        runner.addControllerService(SERVICE_ID, privateKeyService);
        runner.enableControllerService(privateKeyService);
        runner.setProperty(SignContentPGP.PRIVATE_KEY_SERVICE, SERVICE_ID);
    }

    @Test
    public void testFailurePrivateKeyIdParsingException() {
        runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, String.class.getSimpleName());

        runner.enqueue(DATA);
        runner.run();

        assertFailureErrorLogged();
    }

    @Test
    public void testFailureServiceException() {
        final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
        runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
        when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenThrow(new RuntimeException());

        runner.enqueue(DATA);
        runner.run();

        assertFailureErrorLogged();
    }

    @Test
    public void testFailurePrivateKeyIdNotFound() {
        final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
        runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
        when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenReturn(Optional.empty());

        runner.enqueue(DATA);
        runner.run();

        assertFailureErrorLogged();
    }

    @Test
    public void testSuccessFileEncodingAsciiHashAlgorithmSha512() throws PGPException, IOException {
        assertSuccess(FileEncoding.ASCII, HashAlgorithm.SHA512, SigningStrategy.SIGNED);
    }

    @Test
    public void testSuccessFileEncodingBinaryHashAlgorithmSha512() throws PGPException, IOException {
        assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA512, SigningStrategy.SIGNED);
    }

    @Test
    public void testSuccessFileEncodingBinaryUncompressedHashAlgorithmSha256() throws PGPException, IOException {
        runner.setProperty(SignContentPGP.COMPRESSION_ALGORITHM, CompressionAlgorithm.UNCOMPRESSED.toString());
        assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.SIGNED);
    }

    @Test
    public void testSuccessFileEncodingBinaryHashAlgorithmSha256() throws PGPException, IOException {
        assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.SIGNED);
    }

    @Test
    public void testSuccessDetachedFileEncodingBinaryHashAlgorithmSha256() throws PGPException, IOException {
        assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.DETACHED);
    }

    private void setPrivateKey() {
        final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
        runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
        when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenReturn(Optional.of(rsaPrivateKey));
    }

    private void assertSuccess(final FileEncoding fileEncoding, final HashAlgorithm hashAlgorithm, final SigningStrategy signingStrategy) throws PGPException, IOException {
        setPrivateKey();

        runner.setProperty(SignContentPGP.FILE_ENCODING, fileEncoding.toString());
        runner.setProperty(SignContentPGP.HASH_ALGORITHM, hashAlgorithm.toString());
        runner.setProperty(SignContentPGP.SIGNING_STRATEGY, signingStrategy.toString());

        runner.enqueue(DATA);
        runner.run();

        runner.assertTransferCount(SignContentPGP.SUCCESS, 1);
        final MockFlowFile flowFile = runner.getFlowFilesForRelationship(SignContentPGP.SUCCESS).iterator().next();
        assertFlowFileAttributesFound(flowFile, fileEncoding, hashAlgorithm);

        if (SigningStrategy.DETACHED == signingStrategy) {
            assertDetachedSignatureVerified(flowFile);
        } else {
            assertSignatureVerified(flowFile);
        }
    }

    private void assertFailureErrorLogged() {
        runner.assertAllFlowFilesTransferred(SignContentPGP.FAILURE);
        final Optional<LogMessage> optionalLogMessage = runner.getLogger().getErrorMessages().stream().findFirst();
        assertTrue(optionalLogMessage.isPresent());
    }

    private void assertFlowFileAttributesFound(final MockFlowFile flowFile, final FileEncoding fileEncoding, final HashAlgorithm hashAlgorithm) throws PGPException {
        flowFile.assertAttributeEquals(PGPAttributeKey.FILE_ENCODING, fileEncoding.toString());
        flowFile.assertAttributeExists(PGPAttributeKey.COMPRESS_ALGORITHM);
        flowFile.assertAttributeExists(PGPAttributeKey.COMPRESS_ALGORITHM_ID);

        final String signatureAlgorithm = PGPUtil.getSignatureName(rsaPrivateKey.getPublicKeyPacket().getAlgorithm(), hashAlgorithm.getId());
        final String keyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));

        flowFile.assertAttributeExists(PGPAttributeKey.SIGNATURE_CREATED);
        flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_ALGORITHM, signatureAlgorithm);
        flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_HASH_ALGORITHM_ID, Integer.toString(hashAlgorithm.getId()));
        flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_KEY_ALGORITHM_ID, Integer.toString(rsaPrivateKey.getPublicKeyPacket().getAlgorithm()));
        flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_KEY_ID, keyId);
        flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_TYPE_ID, Integer.toString(PGPSignature.BINARY_DOCUMENT));
        flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_VERSION, Integer.toString(SIGNATURE_VERSION));
    }

    private void assertDetachedSignatureVerified(final MockFlowFile signatureFlowFile) throws IOException, PGPException {
        final InputStream signatureContentStream = PGPUtil.getDecoderStream(signatureFlowFile.getContentStream());
        final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(signatureContentStream);

        final PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject();
        final PGPSignature signature = signatureList.iterator().next();

        signature.init(new JcaPGPContentVerifierBuilderProvider(), rsaPublicKey);

        signature.update(DATA_BINARY);

        final boolean verified = signature.verify();
        assertTrue(verified);
    }

    private void assertSignatureVerified(final MockFlowFile flowFile) throws IOException, PGPException {
        final InputStream flowFileContentStream = PGPUtil.getDecoderStream(flowFile.getContentStream());
        final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(flowFileContentStream);

        final PGPCompressedData compressedData = (PGPCompressedData) objectFactory.nextObject();
        final InputStream dataInputStream = compressedData.getDataStream();
        final PGPObjectFactory dataObjectFactory = new JcaPGPObjectFactory(dataInputStream);

        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PGPOnePassSignature onePassSignature = null;
        boolean verified = false;
        for (final Object object : dataObjectFactory) {
            if (object instanceof PGPOnePassSignatureList) {
                final PGPOnePassSignatureList onePassSignatureList = (PGPOnePassSignatureList) object;
                onePassSignature = onePassSignatureList.iterator().next();
                onePassSignature.init(new JcaPGPContentVerifierBuilderProvider(), rsaPublicKey);
            } else if (object instanceof PGPLiteralData) {
                if (onePassSignature == null) {
                    throw new IllegalStateException("One-Pass Signature not found before Literal Data");
                }

                final PGPLiteralData literalData = (PGPLiteralData) object;
                final InputStream literalInputStream = literalData.getDataStream();
                int read;
                while ((read = literalInputStream.read()) >= 0) {
                    onePassSignature.update((byte) read);
                    outputStream.write(read);
                }
            } else if (object instanceof PGPSignatureList) {
                if (onePassSignature == null) {
                    throw new IllegalStateException("One-Pass Signature not found before Signature");
                }

                final PGPSignatureList signatureList = (PGPSignatureList) object;
                final PGPSignature signature = signatureList.iterator().next();
                verified = onePassSignature.verify(signature);
            } else {
                throw new IllegalStateException(String.format("Unexpected PGP Object Found [%s]", object.getClass()));
            }
        }

        assertTrue(verified);

        final String literal = new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
        assertEquals(DATA, literal);
    }
}
