// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0

package org.pgpainless.cli.commands;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Date;

import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
import org.pgpainless.key.generation.type.xdh.XDHSpec;
import org.pgpainless.key.info.KeyRingInfo;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
import sop.util.UTCUtil;

public class RoundTripSignVerifyCmdTest extends CLITest {

    public RoundTripSignVerifyCmdTest() {
        super(LoggerFactory.getLogger(RoundTripSignVerifyCmdTest.class));
    }

    private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
            "Version: PGPainless\n" +
            "Comment: 9DA0 9423 C9F9 4BA4 CCA3  0951 099B 11BF 296A 373E\n" +
            "Comment: Sigmund <sigmund@pgpainless.org>\n" +
            "\n" +
            "lFgEY2vzkhYJKwYBBAHaRw8BAQdA+Z2OAFQf0k64Au7hIZfXh/ijclabddvwh7Nh\n" +
            "kedJ3ZUAAQCZy5p1cvQvRIWUopHwhnrD/oVAa1dNT/nA3cihQ5gkZBHPtCBTaWdt\n" +
            "dW5kIDxzaWdtdW5kQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJja/OSCRAJmxG/\n" +
            "KWo3PhYhBJ2glCPJ+UukzKMJUQmbEb8pajc+Ap4BApsBBRYCAwEABAsJCAcFFQoJ\n" +
            "CAsCmQEAACM9AP9APloI2waD5gXsJqzenRVU4n/VmZUvcdUyhlbpab/0HQEAlaTw\n" +
            "ZvxVyaf8EMFSJOY+LcgacHaZDHRPA1nS3bIfKwycXQRja/OSEgorBgEEAZdVAQUB\n" +
            "AQdA1WL4QKgRxbvzW91ICM6PoICSNh2QHK6j0pIdN/cqXz0DAQgHAAD/bOk3WqbF\n" +
            "QAE8xxm0w/KDZzL1N0yPcBQ5z4XKmu77FCgQ04h1BBgWCgAdBQJja/OSAp4BApsM\n" +
            "BRYCAwEABAsJCAcFFQoJCAsACgkQCZsRvylqNz6rgQEAzoG6HnPCYi2i2c6/ufuy\n" +
            "pBkLby2u1JjD0CWSbrM4dZ0A/j/pI4a9b8LcrZcuY2QwHqsXPAJp8QtOOQN6gTvN\n" +
            "WcQNnFgEY2vzkhYJKwYBBAHaRw8BAQdAsxcDCvst/GbWxQvvOpChSvmbqWeuBgm3\n" +
            "1vRoujFVFcYAAP9Ww46yfWipb8OivTSX+PvgdUhEeVgxENpsyOQLLhQP/RFziNUE\n" +
            "GBYKAH0FAmNr85ICngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/OS\n" +
            "AAoJENqfQTmGIR3GtsMBAL+b1Zo5giQKJGEyx5aGwAz3AwtGiT6QDS9FH6HyM855\n" +
            "AP4uAXDiaNxYTugqnG471jYX/hhJqIROeDGrEIkkAp+qDwAKCRAJmxG/KWo3PhOX\n" +
            "AP45LPV6I4+D3h8etdiEA2DVvNcpRA8l4WkNcq4q8H1SjwD/c/rX3FCUIWLlAHoR\n" +
            "WxCFj+gDgqDNLzwoA4iNo1VMtQc=\n" +
            "=/Np6\n" +
            "-----END PGP PRIVATE KEY BLOCK-----";

    private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
            "Version: PGPainless\n" +
            "Comment: 9DA0 9423 C9F9 4BA4 CCA3  0951 099B 11BF 296A 373E\n" +
            "Comment: Sigmund <sigmund@pgpainless.org>\n" +
            "\n" +
            "mDMEY2vzkhYJKwYBBAHaRw8BAQdA+Z2OAFQf0k64Au7hIZfXh/ijclabddvwh7Nh\n" +
            "kedJ3ZW0IFNpZ211bmQgPHNpZ211bmRAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEF\n" +
            "AmNr85IJEAmbEb8pajc+FiEEnaCUI8n5S6TMowlRCZsRvylqNz4CngECmwEFFgID\n" +
            "AQAECwkIBwUVCgkICwKZAQAAIz0A/0A+WgjbBoPmBewmrN6dFVTif9WZlS9x1TKG\n" +
            "Vulpv/QdAQCVpPBm/FXJp/wQwVIk5j4tyBpwdpkMdE8DWdLdsh8rDLg4BGNr85IS\n" +
            "CisGAQQBl1UBBQEBB0DVYvhAqBHFu/Nb3UgIzo+ggJI2HZAcrqPSkh039ypfPQMB\n" +
            "CAeIdQQYFgoAHQUCY2vzkgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEAmbEb8p\n" +
            "ajc+q4EBAM6Buh5zwmItotnOv7n7sqQZC28trtSYw9Alkm6zOHWdAP4/6SOGvW/C\n" +
            "3K2XLmNkMB6rFzwCafELTjkDeoE7zVnEDbgzBGNr85IWCSsGAQQB2kcPAQEHQLMX\n" +
            "Awr7Lfxm1sUL7zqQoUr5m6lnrgYJt9b0aLoxVRXGiNUEGBYKAH0FAmNr85ICngEC\n" +
            "mwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/OSAAoJENqfQTmGIR3GtsMB\n" +
            "AL+b1Zo5giQKJGEyx5aGwAz3AwtGiT6QDS9FH6HyM855AP4uAXDiaNxYTugqnG47\n" +
            "1jYX/hhJqIROeDGrEIkkAp+qDwAKCRAJmxG/KWo3PhOXAP45LPV6I4+D3h8etdiE\n" +
            "A2DVvNcpRA8l4WkNcq4q8H1SjwD/c/rX3FCUIWLlAHoRWxCFj+gDgqDNLzwoA4iN\n" +
            "o1VMtQc=\n" +
            "=KuJ4\n" +
            "-----END PGP PUBLIC KEY BLOCK-----";
    private static final String PLAINTEXT = "Hello, World!\n";
    private static final String BINARY_SIG = "-----BEGIN PGP SIGNATURE-----\n" +
            "Version: PGPainless\n" +
            "\n" +
            "iHUEABYKACcFAmNr9BgJENqfQTmGIR3GFiEEREwQqwEe+EJMg/Cp2p9BOYYhHcYA\n" +
            "AKocAP48P2C3TU33T3Zy73clw0eBa1oW9pwxTGuFxhgOBzmoSwEArj0781GlpTB0\n" +
            "Vnr2PjPYEqzB+ZuOzOnGhsVGob4c3Ao=\n" +
            "=VWAZ\n" +
            "-----END PGP SIGNATURE-----";
    private static final String BINARY_SIG_VERIFICATION =
            "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:binary\n";
    private static final String TEXT_SIG = "-----BEGIN PGP SIGNATURE-----\n" +
            "Version: PGPainless\n" +
            "\n" +
            "iHUEARYKACcFAmNr9E4JENqfQTmGIR3GFiEEREwQqwEe+EJMg/Cp2p9BOYYhHcYA\n" +
            "AG+CAQD1B3GAAlyxahSiGhvJv7YAI1m6qGcI7dIXcV7FkAFPSgEAlZ0UpCC8oGR+\n" +
            "hi/mQlex4z0hDWSA4abAjclPTJ+qkAI=\n" +
            "=s5xn\n" +
            "-----END PGP SIGNATURE-----";
    private static final String TEXT_SIG_VERIFICATION =
            "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:text\n";
    private static final Date TEXT_SIG_CREATION;

    static {
        try {
            TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z");
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void createArmoredSignature() throws IOException {
        File keyFile = writeFile("key.asc", KEY);
        pipeStringToStdin(PLAINTEXT);
        ByteArrayOutputStream out = pipeStdoutToStream();
        assertSuccess(executeCommand("sign", "--as", "text", keyFile.getAbsolutePath()));
        assertTrue(out.toString().startsWith("-----BEGIN PGP SIGNATURE-----\n"));
    }

    @Test
    public void createUnarmoredSignature() throws IOException {
        File keyFile = writeFile("key.asc", KEY);
        pipeStringToStdin(PLAINTEXT);
        ByteArrayOutputStream out = pipeStdoutToStream();
        assertSuccess(executeCommand("sign", "--no-armor", keyFile.getAbsolutePath()));
        assertFalse(out.toString().startsWith("-----BEGIN PGP SIGNATURE-----\n"));
    }

    @Test
    public void unarmorArmoredSigAndVerify() throws IOException {
        File certFile = writeFile("cert.asc", CERT);

        pipeStringToStdin(BINARY_SIG);
        File unarmoredSigFile = pipeStdoutToFile("sig.pgp");
        assertSuccess(executeCommand("dearmor"));

        pipeStringToStdin(PLAINTEXT);
        ByteArrayOutputStream out = pipeStdoutToStream();
        assertSuccess(executeCommand("verify", unarmoredSigFile.getAbsolutePath(), certFile.getAbsolutePath()));

        assertEquals(BINARY_SIG_VERIFICATION, out.toString());
    }

    @Test
    public void testNotBefore() throws IOException {
        File cert = writeFile("cert.asc", CERT);
        File sigFile = writeFile("sig.asc", TEXT_SIG);
        Date plus1Minute = new Date(TEXT_SIG_CREATION.getTime() + 1000 * 60);

        pipeStringToStdin(PLAINTEXT);
        ByteArrayOutputStream out = pipeStdoutToStream();
        int exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(),
                "--not-before", UTCUtil.formatUTCDate(plus1Minute));

        assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode);
        assertEquals(0, out.size());

        Date minus1Minute = new Date(TEXT_SIG_CREATION.getTime() - 1000 * 60);
        pipeStringToStdin(PLAINTEXT);
        out = pipeStdoutToStream();
        exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(),
                "--not-before", UTCUtil.formatUTCDate(minus1Minute));

        assertSuccess(exitCode);
        assertEquals(TEXT_SIG_VERIFICATION, out.toString());
    }

    @Test
    public void testNotAfter() throws IOException {
        File cert = writeFile("cert.asc", CERT);
        File sigFile = writeFile("sig.asc", TEXT_SIG);

        Date minus1Minute = new Date(TEXT_SIG_CREATION.getTime() - 1000 * 60);
        pipeStringToStdin(PLAINTEXT);
        ByteArrayOutputStream out = pipeStdoutToStream();
        int exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(),
                "--not-after", UTCUtil.formatUTCDate(minus1Minute));

        assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode);
        assertEquals(0, out.size());

        Date plus1Minute = new Date(TEXT_SIG_CREATION.getTime() + 1000 * 60);
        pipeStringToStdin(PLAINTEXT);
        out = pipeStdoutToStream();
        exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(),
                "--not-after", UTCUtil.formatUTCDate(plus1Minute));

        assertSuccess(exitCode);
        assertEquals(TEXT_SIG_VERIFICATION, out.toString());
    }

    @Test
    public void testSignWithIncapableKey()
            throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
        PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
                .addUserId("Cannot Sign <cannot@sign.key>")
                .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER))
                .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
                .build();
        File keyFile = writeFile("key.pgp", secretKeys.getEncoded());

        pipeStringToStdin("Hello, World!\n");
        ByteArrayOutputStream out = pipeStdoutToStream();
        int exitCode = executeCommand("sign", keyFile.getAbsolutePath());

        assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode);
        assertEquals(0, out.size());
    }

    @Test
    public void testSignatureCreationAndVerification()
            throws IOException {
        // Create key and cert
        File aliceKeyFile = pipeStdoutToFile("alice.key");
        assertSuccess(executeCommand("generate-key", "Alice <alice@pgpainless.org>"));
        File aliceCertFile = pipeStdoutToFile("alice.cert");
        pipeFileToStdin(aliceKeyFile);
        assertSuccess(executeCommand("extract-cert"));

        File micalgOut = nonExistentFile("micalg");
        String msg = "If privacy is outlawed, only outlaws will have privacy.\n";
        File dataFile = writeFile("data", msg);

        // sign data
        File sigFile = pipeStdoutToFile("sig.asc");
        pipeFileToStdin(dataFile);
        assertSuccess(executeCommand("sign",
                "--armor",
                "--as", "binary",
                "--micalg-out", micalgOut.getAbsolutePath(),
                aliceKeyFile.getAbsolutePath()));

        // verify test data signature
        pipeFileToStdin(dataFile);
        ByteArrayOutputStream verificationsOut = pipeStdoutToStream();
        assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath()));

        // Test verification output

        PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(readBytesFromFile(aliceCertFile));
        KeyRingInfo info = PGPainless.inspectKeyRing(cert);

        // [date] [signing-key-fp] [primary-key-fp] signed by [key.pub]
        String verification = verificationsOut.toString();
        String[] split = verification.split(" ");
        OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(cert);
        OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(info.getSigningSubkeys().get(0));
        assertEquals(signingKeyFingerprint.toString(), split[1].trim(), verification);
        assertEquals(primaryKeyFingerprint.toString(), split[2].trim());

        // Test micalg output
        String content = readStringFromFile(micalgOut);
        assertEquals("pgp-sha512", content);
    }

    private static final String PROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
            "Version: PGPainless\n" +
            "Comment: 738E EAB2 503D 322D 613A  C42A B18E 8BF8 884F C050\n" +
            "Comment: Axel <axel@pgpainless.org>\n" +
            "\n" +
            "lIYEY2v6aRYJKwYBBAHaRw8BAQdA3PXtH19zYpVQ9zTU3zlY+iXUptelAO3z4vK/\n" +
            "M2FkmrP+CQMCYgVa6K+InVJguITSDIA+HQ6vhOZ5Dbanqx7GFbJbJLD2fWrxhTSr\n" +
            "BUWGaUWTqN647auD/kUI8phH1cedVL6CzVR+YWvaWj9zZHr/CYXLobQaQXhlbCA8\n" +
            "YXhlbEBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCY2v6aQkQsY6L+IhPwFAWIQRz\n" +
            "juqyUD0yLWE6xCqxjov4iE/AUAKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAACq\n" +
            "zgEAkxB+dUI7Jjcg5zRvT1EfE9DKCI1qTsxOAU/ZXLcSXLkBAJtWRsyptetZvjzB\n" +
            "Ze2A7ArOl4q+IvKvun/d783YyRMInIsEY2v6aRIKKwYBBAGXVQEFAQEHQPFmlZ+o\n" +
            "jCGEo2X0474vJfRG7blctuZXmCbC0sLO7MgzAwEIB/4JAwJiBVror4idUmDFhBq4\n" +
            "lEhJxjCVc6aSD6+EWRT3YdplqCmNdynnrPombUFst6LfJFzns3H3d0rCeXHfQr93\n" +
            "GrHTLkHfW8G3x0PJJPiqFkBviHUEGBYKAB0FAmNr+mkCngECmwwFFgIDAQAECwkI\n" +
            "BwUVCgkICwAKCRCxjov4iE/AUNC2AP9WDx4lHt9oYFLSrM8vMLRFI31U8TkYrtCe\n" +
            "pYICE76cIAEA5+wEbtE5vQrLxOqIRueVVdzwK9kTeMvSIQfc9PNoyQKchgRja/pp\n" +
            "FgkrBgEEAdpHDwEBB0CyAEVlCUbFr3dBBG3MQ84hjCPfYqSx9kYsTN8j5Og6uP4J\n" +
            "AwJiBVror4idUmCIFuAYXia0YpEhEpB/Lrn/D6/WAUPEgZjNLMvJzL//EmhkWfEa\n" +
            "OfQz/fslj1erWNjLKNiW5C/TvGapDfjbn596AkNlcd1JiNUEGBYKAH0FAmNr+mkC\n" +
            "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/ppAAoJELRgil1uCuQj\n" +
            "VUYBAJecbedwwqWQITVqucEBIraTRoc6ZGkN8jytDp8z9CsBAQDrb/W/J/kze6ln\n" +
            "nRyJSriWF3SjcKOGIRkUslmdJEPPCQAKCRCxjov4iE/AUAvbAQDBBgQFG8acTT5L\n" +
            "cyIi1Ix9/XBG7G23SSs6l7Beap8M+wEAmK13NYuq7Mv/mct8iIKZbBFH9aAiY+nX\n" +
            "3Uct4Q5f0w0=\n" +
            "=K65R\n" +
            "-----END PGP PRIVATE KEY BLOCK-----";
    private static final String PASSPHRASE = "orange";
    private static final String SIGNING_KEY = "9846F3606EE875FB77EC8808B4608A5D6E0AE423 738EEAB2503D322D613AC42AB18E8BF8884FC050";

    @Test
    public void signWithProtectedKey_missingPassphraseFails() throws IOException {
        File key = writeFile("key.asc", PROTECTED_KEY);
        pipeStringToStdin(PLAINTEXT);
        ByteArrayOutputStream out = pipeStdoutToStream();
        int exitCode = executeCommand("sign", key.getAbsolutePath());
        assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode);
        assertEquals(0, out.size());
    }

    @Test
    public void signWithProtectedKey_wrongPassphraseFails() throws IOException {
        File password = writeFile("password", "blue");
        File key = writeFile("key.asc", PROTECTED_KEY);
        pipeStringToStdin(PLAINTEXT);
        ByteArrayOutputStream out = pipeStdoutToStream();
        int exitCode = executeCommand("sign", key.getAbsolutePath(),
                "--with-key-password", password.getAbsolutePath());
        assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode);
        assertEquals(0, out.size());
    }

    @Test
    public void signWithProtectedKey() throws IOException {
        File password = writeFile("password", PASSPHRASE);
        File key = writeFile("key.asc", PROTECTED_KEY);
        pipeStringToStdin(PROTECTED_KEY);
        File cert = pipeStdoutToFile("cert.asc");
        assertSuccess(executeCommand("extract-cert"));

        pipeStringToStdin(PLAINTEXT);
        File sigFile = pipeStdoutToFile("sig.asc");
        assertSuccess(executeCommand("sign", key.getAbsolutePath(),
                "--with-key-password", password.getAbsolutePath()));

        pipeStringToStdin(PLAINTEXT);
        ByteArrayOutputStream verificationOut = pipeStdoutToStream();
        assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath()));
        assertTrue(verificationOut.toString().contains(SIGNING_KEY));
    }

}
