use std::time::Duration;

use sequoia_openpgp as openpgp;
use openpgp::parse::Parse;
use openpgp::Cert;
use openpgp::cert::amalgamation::ValidAmalgamation;
use openpgp::Result;
use openpgp::types::RevocationStatus;

use super::common::STANDARD_POLICY;
use super::common::Sq;
use super::common::time_as_string;
use super::common::UserIDArg;

#[test]
fn sq_key_expire() -> Result<()> {
    for keystore in [false, true] {
        let mut sq = Sq::new();

        let (cert, cert_path, _rev_path)
            = sq.key_generate(&[], &["alice <alice@example.org>"]);
        let fpr = cert.fingerprint().to_string();

        // Two days go by.
        sq.tick(2 * 24 * 60 * 60);

        let updated_path = sq.scratch_file("updated.pgp");
        let updated2_path = sq.scratch_file("updated2.pgp");

        if keystore {
            sq.key_import(&cert_path);
        }

        // Change the key to expire in one day.
        let mut cmd = sq.command();
        cmd.args(["key", "expire", "--expiration", "1d"]);
        if keystore {
            cmd.args(["--cert", &fpr ]);
        } else {
            cmd
                .arg("--overwrite")
                .arg("--cert-file").arg(&cert_path)
                .arg("--output").arg(&updated_path);
        }
        sq.run(cmd, true);

        let updated = if keystore {
            eprintln!("Updated certificate to expire in one day:\n{}",
                      sq.inspect(cert.key_handle()));

            sq.cert_export(cert.key_handle())
        } else {
            eprintln!("Updated certificate to expire in one day:\n{}",
                      sq.inspect(&updated_path));

            Cert::from_file(&updated_path).expect("valid cert")
        };

        // It should be alive now.
        let vc = updated.with_policy(STANDARD_POLICY, sq.now()).expect("valid");
        assert!(matches!(vc.alive(), Ok(())));

        // It should be alive in 1 day minus 1 second.
        let t = sq.now() + Duration::new(24 * 60 * 60 - 1, 0);
        eprintln!("Checking expiration status at {}", time_as_string(t.into()));
        let vc = updated.with_policy(STANDARD_POLICY, t).expect("valid");
        assert!(matches!(vc.alive(), Ok(())));

        // But in exactly one day, it should be expired.
        let t = sq.now() + Duration::new(24 * 60 * 60, 0);
        eprintln!("Checking expiration status at {}", time_as_string(t.into()));
        let vc = updated.with_policy(STANDARD_POLICY, t).expect("valid");
        assert!(matches!(vc.alive(), Err(_)));

        // 12 hours go by.  Clear the expiration time.
        sq.tick(12 * 60 * 60);

        let mut cmd = sq.command();
        cmd.args([ "key", "expire", "--expiration", "never" ]);
        if keystore {
            cmd.args([ "--cert", &fpr ]);
        } else {
            cmd.args([
                "--cert-file", &updated_path.to_string_lossy(),
                "--output", &updated2_path.to_string_lossy(),
            ]);
        }
        sq.run(cmd, true);

        let updated = if keystore {
            eprintln!("Updated certificate to expire in one day:\n{}",
                      sq.inspect(cert.key_handle()));

            sq.cert_export(cert.key_handle())
        } else {
            eprintln!("Updated certificate to expire in one day:\n{}",
                      sq.inspect(&updated2_path));

            Cert::from_file(&updated2_path).expect("valid cert")
        };

        // It should be alive now.
        let vc = updated.with_policy(STANDARD_POLICY, None)
            .expect("valid");
        assert!(matches!(vc.alive(), Ok(())));

        // It should be alive in 1 day minus 1 second.
        let vc = updated.with_policy(
            STANDARD_POLICY,
            sq.now() + Duration::new(24 * 60 * 60 - 1, 0))
            .expect("valid");
        assert!(matches!(vc.alive(), Ok(())));

        // And in exactly one day...
        let vc = updated.with_policy(
            STANDARD_POLICY,
            sq.now() + Duration::new(24 * 60 * 60, 0))
            .expect("valid");
        assert!(matches!(vc.alive(), Ok(())));
    }

    Ok(())
}

/// Tests changing the expiration time of a key without direct key
/// signature.
///
/// These kind of keys are generated by GnuPG.
#[test]
fn sq_key_expire_no_direct_key_sig() -> Result<()> {
    use openpgp::{
        packet::{Any, Signature},
        serialize::Serialize,
        types::SignatureType,
    };

    let mut sq = Sq::new();
    let (cert, _cert_path, _rev_path)
        = sq.key_generate(&[], &[UserIDArg::Email("alice@example.org")]);
    let fipr = cert.fingerprint().to_string();


    // Strip the direct key signature.
    let mut p = cert.as_tsk().into_packets().collect::<Vec<_>>();
    p.retain(|p| Any::<Signature>::downcast_ref(p)
             .map(|sig| sig.typ() != SignatureType::DirectKey)
             .unwrap_or(true));
    let cert = Cert::from_packets(p.into_iter())?;

    // Two days go by.
    sq.tick(2 * 24 * 60 * 60);

    let cert_path = sq.scratch_file("updated.pgp");
    cert.as_tsk().serialize(&mut std::fs::File::create(&cert_path)?)?;
    sq.key_import(&cert_path);

    // Change the key to expire in one day.
    let mut cmd = sq.command();
    cmd.args(["key", "expire", "--expiration", "1d", "--cert", &fipr]);
    sq.run(cmd, true);

    eprintln!("Updated certificate to expire in one day:\n{}",
              sq.inspect(cert.key_handle()));
    let updated = sq.cert_export(cert.key_handle());

    // It should be alive now.
    let vc = updated.with_policy(STANDARD_POLICY, sq.now()).expect("valid");
    assert!(matches!(vc.alive(), Ok(())));

    // It should be alive in 1 day minus 1 second.
    let t = sq.now() + Duration::new(24 * 60 * 60 - 1, 0);
    eprintln!("Checking expiration status at {}", time_as_string(t.into()));
    let vc = updated.with_policy(STANDARD_POLICY, t).expect("valid");
    assert!(matches!(vc.alive(), Ok(())));

    // But in exactly one day, it should be expired.
    let t = sq.now() + Duration::new(24 * 60 * 60, 0);
    eprintln!("Checking expiration status at {}", time_as_string(t.into()));
    let vc = updated.with_policy(STANDARD_POLICY, t).expect("valid");
    assert!(matches!(vc.alive(), Err(_)));

    // 12 hours go by.  Clear the expiration time.
    sq.tick(12 * 60 * 60);

    let mut cmd = sq.command();
    cmd.args(["key", "expire", "--expiration", "never", "--cert", &fipr]);
    sq.run(cmd, true);

    eprintln!("Updated certificate to expire in one day:\n{}",
              sq.inspect(cert.key_handle()));
    let updated = sq.cert_export(cert.key_handle());

    // It should be alive now.
    let vc = updated.with_policy(STANDARD_POLICY, None)
        .expect("valid");
    assert!(matches!(vc.alive(), Ok(())));

    // It should be alive in 1 day minus 1 second.
    let vc = updated.with_policy(
        STANDARD_POLICY,
        sq.now() + Duration::new(24 * 60 * 60 - 1, 0))
        .expect("valid");
    assert!(matches!(vc.alive(), Ok(())));

    // And in exactly one day...
    let vc = updated.with_policy(
        STANDARD_POLICY,
        sq.now() + Duration::new(24 * 60 * 60, 0))
        .expect("valid");
    assert!(matches!(vc.alive(), Ok(())));

    Ok(())
}

#[test]
fn unbound_userid() {
    // Make sure we can extend the expiration time of a certificate
    // that includes an unbound user ID (i.e., a user ID without a
    // self signature).

    let sq = Sq::new();

    let cert_path = sq.test_data()
        .join("keys")
        .join("unbound-userid.pgp");

    let cert = Cert::from_file(&cert_path).expect("can read");
    let vc = cert.with_policy(STANDARD_POLICY, sq.now())
        .expect("valid cert");
    // It shouldn't be expired yet.
    assert!(vc.primary_key().key_expiration_time().is_none());

    // Set it to expire in a day.
    let updated_path = sq.scratch_file("updated");
    let updated = sq.key_expire(cert_path,
                                "1d",
                                None,
                                updated_path.as_path(),
                                true)
        .expect("sq key expire should succeed");

    let vc = updated.with_policy(STANDARD_POLICY, sq.now())
        .expect("valid cert");
    let expiration = vc.primary_key().key_expiration_time();
    assert_eq!(expiration,
               Some(sq.now() + std::time::Duration::new(24 * 60 * 60, 0)));
}


#[test]
fn revoked_userid() {
    // Make sure we can extend the expiration time of a certificate
    // that includes a revoked user ID (i.e., a user ID without a self
    // signature), and make sure we DON'T make the user ID valid.

    let sq = Sq::new();

    let cert_path = sq.test_data()
        .join("keys")
        .join("retired-userid.pgp");

    let cert = Cert::from_file(&cert_path).expect("can read");
    let vc = cert.with_policy(STANDARD_POLICY, sq.now())
        .expect("valid cert");
    // It shouldn't be expired yet.
    assert!(vc.primary_key().key_expiration_time().is_none());

    let ua = vc.userids().next().expect("have a user ID");
    if let RevocationStatus::Revoked(_) = ua.revocation_status() {
    } else {
        panic!("User ID should be revoked, but isn't.");
    };

    // Set it to expire in a day.
    let updated_path = sq.scratch_file("updated");
    let updated = sq.key_expire(cert_path,
                                "1d",
                                None,
                                updated_path.as_path(),
                                true)
        .expect("sq key expire should succeed");

    let vc = updated.with_policy(STANDARD_POLICY, sq.now())
        .expect("valid cert");
    let expiration = vc.primary_key().key_expiration_time();
    assert_eq!(expiration,
               Some(sq.now() + std::time::Duration::new(24 * 60 * 60, 0)));

    let ua = vc.userids().next().expect("have a user ID");
    if let RevocationStatus::Revoked(_) = ua.revocation_status() {
    } else {
        panic!("User ID should be revoked, but isn't.");
    };
}
