use anyhow::Result;

use sequoia_openpgp as openpgp;
use openpgp::{
    cert::prelude::*,
    parse::Parse,
    serialize::{
        Serialize,
        SerializeInto,
    },
};

use super::super::*;

const PLAINTEXT: &[u8] = b"plaintext";

#[test]
#[ntest::timeout(600000)]
fn general_purpose_cv25519() -> Result<()> {
    general_purpose(CipherSuite::Cv25519)
}

#[test]
#[ntest::timeout(600000)]
fn general_purpose_rsa2k() -> Result<()> {
    general_purpose(CipherSuite::RSA2k)
}

#[test]
#[ntest::timeout(600000)]
fn general_purpose_rsa3k() -> Result<()> {
    general_purpose(CipherSuite::RSA3k)
}

#[test]
#[ntest::timeout(600000)]
fn general_purpose_rsa4k() -> Result<()> {
    general_purpose(CipherSuite::RSA4k)
}

#[test]
#[ntest::timeout(600000)]
fn general_purpose_p256() -> Result<()> {
    general_purpose(CipherSuite::P256)
}

#[test]
#[ntest::timeout(600000)]
fn general_purpose_p384() -> Result<()> {
    general_purpose(CipherSuite::P384)
}

#[test]
#[ntest::timeout(600000)]
fn general_purpose_p521() -> Result<()> {
    general_purpose(CipherSuite::P521)
}

fn general_purpose(cs: CipherSuite) -> Result<()> {
    let mut experiment = make_experiment!(format!("{:?}", cs))?;
    let cert = experiment.artifact(
        "cert",
        || CertBuilder::general_purpose(
            cs, Some("Alice Lovelace <alice@lovelace.name>"))
            .set_creation_time(Experiment::now())
            .generate()
            .map(|(cert, _rev)| cert),
        |a, f| a.as_tsk().serialize(f),
        |b| Cert::from_bytes(&b))?;

    test_key(cert, experiment, true)
}

#[test]
#[ntest::timeout(600000)]
fn no_encryption_subkey() -> Result<()> {
    let mut experiment = make_experiment!()?;
    let cert = experiment.artifact(
        "cert",
        || CertBuilder::new()
            .add_userid("Alice Lovelace <alice@lovelace.name>")
            .set_creation_time(Experiment::now())
            .generate()
            .map(|(cert, _rev)| cert),
        |a, f| a.as_tsk().serialize(f),
        |b| Cert::from_bytes(&b))?;

    test_key(cert, experiment, false)
}

#[test]
#[ntest::timeout(600000)]
fn recipient_file() -> Result<()> {
    let mut experiment = make_experiment!()?;

    let cert = experiment.artifact(
        "cert",
        || CertBuilder::new()
            .add_userid("Alice Lovelace <alice@lovelace.name>")
            .set_creation_time(Experiment::now())
            .add_transport_encryption_subkey()
            .generate()
            .map(|(cert, _rev)| cert),
        |a, f| a.as_tsk().serialize(f),
        |b| Cert::from_bytes(&b))?;

    let diff = experiment.invoke(&[
        "--no-auto-key-locate",
        "--always-trust",
        "--encrypt",
        "--recipient-file", &experiment.store("cert", &cert.to_vec()?)?,
        "--output", "ciphertext",
        &experiment.store("plaintext", PLAINTEXT)?,
    ])?;
    diff.assert_success();
    diff.assert_limits(0, 0, 1);
    let ciphertexts =
        diff.with_working_dir(|p| p.get("ciphertext").cloned().ok_or_else(
            || anyhow::anyhow!("no ciphertext produced")))?;

    test_decryption(cert, experiment, ciphertexts)
}

fn test_key(cert: Cert, mut experiment: Experiment, expect_success: bool)
            -> Result<()>
{
    experiment.section("Importing cert...");
    let diff = experiment.invoke(&[
        "--import",
        &experiment.store("cert", &cert.to_vec()?)?,
    ])?;
    diff.assert_success();
    diff.assert_limits(0, 0, 0);

    let diff = experiment.invoke(&[
        "--no-auto-key-locate",
        "--always-trust",
        "--encrypt",
        "--recipient", "<alice@lovelace.name>",
        "--output", "ciphertext",
        &experiment.store("plaintext", PLAINTEXT)?,
    ])?;
    if expect_success {
        diff.assert_success();
        diff.assert_limits(0, 0, 0);
        let ciphertexts =
            diff.with_working_dir(|p| p.get("ciphertext").cloned().ok_or_else(
                || anyhow::anyhow!("no ciphertext produced")))?;

        test_decryption(cert, experiment, ciphertexts)?;
    } else {
        diff.assert_failure();
        diff.assert_limits(0, 0, 0);
        assert!(diff.with_working_dir(
            |p| Ok(p.get("ciphertext").is_some()))?
                .iter().all(|&exists| exists == false));
    }

    Ok(())
}

fn test_decryption(cert: Cert,
                   mut experiment: Experiment,
                   ciphertexts: Vec<Vec<u8>>)
                   -> Result<()> {
    experiment.section("Importing key...");
    let diff = experiment.invoke(&[
        "--import",
        &experiment.store("key", &cert.as_tsk().to_vec()?)?,
    ])?;
    diff.assert_success();
    diff.assert_limits(0, 0, 78);

    test_decryption_with(
        &mut experiment,
        &ciphertexts,
        &[],
        |diff| {
            diff.assert_success();
            diff.assert_limits(0, 1, 0);
            diff.with_working_dir(|p| {
                assert_eq!(p.get("plaintext").expect("no output"), PLAINTEXT);
                Ok(())
            })?;
            Ok(())
        })
}

fn test_decryption_with(experiment: &mut Experiment,
                        ciphertexts: &[Vec<u8>],
                        extra_args: &[&str],
                        predicate: impl Fn(Diff) -> Result<()>)
                        -> Result<()>
{
    for ciphertext in ciphertexts {
        let ciphertext_file =
            experiment.store("ciphertext", &ciphertext)?;
        let mut args = extra_args.to_vec();
        args.extend_from_slice(&[
            "--decrypt",
            "--output", "plaintext",
            &ciphertext_file,
        ]);
        let diff = experiment.invoke(&args)?;
        predicate(diff)?;
    }

    Ok(())
}

#[test]
#[ntest::timeout(600000)]
fn fingerprint_recipient() -> Result<()> {
    let mut experiment = make_experiment!()?;

    let cert = experiment.artifact(
        "cert",
        || CertBuilder::new()
            .add_userid("Alice Lovelace <alice@lovelace.name>")
            .set_creation_time(Experiment::now())
            .add_transport_encryption_subkey()
            .generate()
            .map(|(cert, _rev)| cert),
        |a, f| a.as_tsk().serialize(f),
        |b| Cert::from_bytes(&b))?;

    experiment.section("Importing cert...");
    let diff = experiment.invoke(&[
        "--import",
        &experiment.store("cert", &cert.to_vec()?)?,
    ])?;
    diff.assert_success();

    let fp = cert.fingerprint().to_string();
    let diff = experiment.invoke(&[
        "--no-auto-key-locate",
        "--always-trust",
        "--encrypt",
        "--recipient", &fp,
        "--output", "ciphertext",
        &experiment.store("plaintext", PLAINTEXT)?,
    ])?;
    diff.assert_success();
    diff.assert_limits(0, 0, 0);
    let ciphertexts =
        diff.with_working_dir(|p| p.get("ciphertext").cloned().ok_or_else(
            || anyhow::anyhow!("no ciphertext produced")))?;

    test_decryption(cert, experiment, ciphertexts)
}

#[test]
#[ntest::timeout(600000)]
fn throw_keyids() -> Result<()> {
    let mut experiment = make_experiment!()?;

    let cert = experiment.artifact(
        "cert",
        || CertBuilder::new()
            .add_userid("Alice Lovelace <alice@lovelace.name>")
            .set_creation_time(Experiment::now())
            .add_transport_encryption_subkey()
            .generate()
            .map(|(cert, _rev)| cert),
        |a, f| a.as_tsk().serialize(f),
        |b| Cert::from_bytes(&b))?;

    experiment.section("Importing cert...");
    let diff = experiment.invoke(&[
        "--import",
        &experiment.store("cert", &cert.to_vec()?)?,
    ])?;
    diff.assert_success();

    let fp = cert.fingerprint().to_string();
    let diff = experiment.invoke(&[
        "--no-auto-key-locate",
        "--always-trust",
        "--encrypt",
        "--throw-keyids",
        "--recipient", &fp,
        "--output", "ciphertext",
        &experiment.store("plaintext", PLAINTEXT)?,
    ])?;
    diff.assert_success();
    diff.assert_limits(0, 0, 0);
    let ciphertexts =
        diff.with_working_dir(|p| p.get("ciphertext").cloned().ok_or_else(
            || anyhow::anyhow!("no ciphertext produced")))?;

    experiment.section("Importing key...");
    let diff = experiment.invoke(&[
        "--import",
        &experiment.store("key", &cert.as_tsk().to_vec()?)?,
    ])?;
    diff.assert_success();
    diff.assert_limits(0, 0, 78);

    test_decryption_with(
        &mut experiment,
        &ciphertexts,
        &["--skip-hidden-recipients"],
        skip_anonymous_recipient_predicate)?;

    test_decryption_with(
        &mut experiment,
        &ciphertexts,
        &[],
        anonymous_recipient_predicate)?;

    Ok(())
}

#[test]
#[ntest::timeout(600000)]
fn hidden_recipient() -> Result<()> {
    let mut experiment = make_experiment!()?;

    let cert = experiment.artifact(
        "cert",
        || CertBuilder::new()
            .add_userid("Alice Lovelace <alice@lovelace.name>")
            .set_creation_time(Experiment::now())
            .add_transport_encryption_subkey()
            .generate()
            .map(|(cert, _rev)| cert),
        |a, f| a.as_tsk().serialize(f),
        |b| Cert::from_bytes(&b))?;

    experiment.section("Importing cert...");
    let diff = experiment.invoke(&[
        "--import",
        &experiment.store("cert", &cert.to_vec()?)?,
    ])?;
    diff.assert_success();

    let fp = cert.fingerprint().to_string();
    let diff = experiment.invoke(&[
        "--no-auto-key-locate",
        "--always-trust",
        "--encrypt",
        "--hidden-recipient", &fp,
        "--output", "ciphertext",
        &experiment.store("plaintext", PLAINTEXT)?,
    ])?;
    diff.assert_success();
    diff.assert_limits(0, 0, 0);
    let ciphertexts =
        diff.with_working_dir(|p| p.get("ciphertext").cloned().ok_or_else(
            || anyhow::anyhow!("no ciphertext produced")))?;

    experiment.section("Importing key...");
    let diff = experiment.invoke(&[
        "--import",
        &experiment.store("key", &cert.as_tsk().to_vec()?)?,
    ])?;
    diff.assert_success();
    diff.assert_limits(0, 0, 78);

    test_decryption_with(
        &mut experiment,
        &ciphertexts,
        &["--skip-hidden-recipients"],
        skip_anonymous_recipient_predicate)?;

    test_decryption_with(
        &mut experiment,
        &ciphertexts,
        &[],
        anonymous_recipient_predicate)?;

    Ok(())
}

/// Checks successful decryption of a message using anonymous
/// recipients.
fn anonymous_recipient_predicate(diff: Diff) -> Result<()> {
    diff.assert_success();
    diff.assert_limits(
        0,
        "gpg: selecting card failed: No such device\n".len(),
        "[GNUPG:] CARDCTRL 4\n".len());
    diff.with_working_dir(|p| {
        assert_eq!(p.get("plaintext").expect("no output"), PLAINTEXT);
        Ok(())
    })?;

    Ok(())
 }

/// Checks unsuccessful decryption of a message using anonymous
/// recipients.
fn skip_anonymous_recipient_predicate(diff: Diff) -> Result<()> {
    diff.assert_failure();
    diff.assert_limits(
        0,
        0,
        0);

    Ok(())
 }
