#[cfg(test)]
mod integration {
    use std::path;

    use assert_cmd::Command;
    use predicates::prelude::*;

    use sequoia_openpgp as openpgp;

    use openpgp::Cert;
    use openpgp::Packet;
    use openpgp::parse::Parse;

    fn dir() -> path::PathBuf {
        path::Path::new("tests").join("data").join("keyring-lint")
    }

    const FROZEN_TIME: &str = "20220101";

    /// Returns an assert_cmd::Command for sq with the console detached.
    #[cfg(unix)]
    fn sq() -> Command {
        use std::os::fd::AsRawFd;
        use std::os::unix::process::CommandExt;
        use libc::{TIOCNOTTY, ioctl};

        let mut c =
            std::process::Command::new(assert_cmd::cargo::cargo_bin("sq"));
        unsafe {
            c.pre_exec(|| {
                // Best-effort, ignores errors.
                if let Ok(h) = std::fs::File::open("/dev/tty") {
                    ioctl(h.as_raw_fd(), TIOCNOTTY);
                } else {
                    ioctl(std::io::stdin().as_raw_fd(), TIOCNOTTY);
                }
                Ok(())
            });
        }

        c.into()
    }

    /// Returns an assert_cmd::Command for sq with the console detached.
    #[cfg(windows)]
    fn sq() -> Command {
        use std::os::windows::process::CommandExt;

        let mut c =
            std::process::Command::new(assert_cmd::cargo::cargo_bin("sq"));

        const DETACHED_PROCESS: u32 = 0x00000008;
        c.creation_flags(DETACHED_PROCESS);

        c.into()
    }

    // passwords: one '-p' option per element.
    // required_fixes: the number of fixes (= new top-level signatures) needed.
    // expected_fixes: the number of them that we can create.
    fn t(base: &str, prv: Option<&str>, passwords: &[&str],
         required_fixes: usize, expected_fixes: usize)
    {
        assert!(required_fixes >= expected_fixes);

        let dir = dir();
        let mut suffixes = vec![ "pub" ];
        if let Some(prv) = prv {
            suffixes.push(prv);
        }

        for suffix in suffixes.iter() {
            // Lint it.
            let filename = &format!("{}-{}.pgp", base, suffix);
            eprintln!("Linting {}", filename);
            sq()
                .current_dir(&dir)
                .arg("--no-cert-store")
                .arg("keyring").arg("lint")
                .arg("--time").arg(FROZEN_TIME)
                .arg(filename)
                .assert()
                .code(if required_fixes > 0 { 2 } else { 0 });


            // Fix it.
            let filename = &format!("{}-{}.pgp", base, suffix);
            eprint!("Fixing {}", filename);
            if passwords.len() > 0 {
                eprint!(" (passwords: ");
                for (i, p) in passwords.iter().enumerate() {
                    if i > 0 {
                        eprint!(", ");
                    }
                    eprint!("{:?}", p)
                }
                eprint!(")");
            }
            eprintln!(".");

            let expected_fixes = if suffix == &"pub" {
                // We only have public key material: we won't be able
                // to fix anything.
                0
            } else {
                expected_fixes
            };

            let mut cmd = sq();
            let mut cmd = cmd.current_dir(&dir)
                .args(&[
                    "--no-cert-store",
                    "keyring", "lint",
                    "--time", FROZEN_TIME,
                    "--fix", &format!("{}-{}.pgp", base, suffix)
                ]);
            for p in passwords.iter() {
                cmd = cmd.arg("-p").arg(p)
            }
            cmd.assert()
                 // If not everything can be fixed, then --fix's exit code is 3.
                .code(if expected_fixes == required_fixes { 0 } else { 3 })
                .stdout(predicate::function(|output: &[u8]| -> bool {
                    if expected_fixes == 0 {
                        // If there are no fixes, nothing is printed.
                        output == b""
                    } else {
                        // We got a certificate on stdout.  Pass it
                        // through the linter.
                        Command::cargo_bin("sq").unwrap()
                            .current_dir(&dir)
                            .arg("--no-cert-store")
                            .arg("keyring").arg("lint")
                            .arg("--time").arg(FROZEN_TIME)
                            .arg("-")
                            .write_stdin(output)
                            .assert()
                            .code(
                                if expected_fixes == required_fixes {
                                    // Everything should have been fixed.
                                    0
                                } else {
                                    // There are still issues.
                                    2
                                });

                        // Check that the number of new signatures equals
                        // the number of expected new signatures.
                        let orig_sigs: isize =
                            Cert::from_file(dir.clone().join(filename)).unwrap()
                            .into_packets()
                            .map(|p| {
                                if let Packet::Signature(_) = p {
                                    1
                                } else {
                                    0
                                }
                            })
                            .sum();

                        let fixed_sigs: isize = Cert::from_bytes(output)
                            .map(|cert| {
                                cert.into_packets()
                                    .map(|p| {
                                        match p {
                                            Packet::Signature(_) => 1,
                                            Packet::SecretKey(_)
                                                | Packet::SecretSubkey(_) =>
                                                panic!("Secret key material \
                                                        should not be exported!"),
                                            _ => 0,
                                        }
                                    })
                                    .sum()
                            })
                            .map_err(|err| {
                                eprintln!("Parsing fixed certificate: {}", err);
                                0
                            })
                            .unwrap();

                        let fixes = fixed_sigs - orig_sigs;
                        if expected_fixes as isize != fixes {
                            eprintln!("Expected {} fixes, \
                                       found {} additional signatures",
                                      expected_fixes, fixes);
                            false
                        } else {
                            true
                        }
                    }
                }));
        }
    }

    #[test]
    fn known_good() {
        t("gnupg-rsa-normal", Some("priv"), &[], 0, 0);
        t("gnupg-ecc-normal", Some("priv"), &[], 0, 0);
    }

    #[test]
    fn userid_certification() {
        // User ID: SHA256
        // User ID: SHA1
        // Enc Subkey: SHA256
        t("sha1-userid", Some("priv"), &[], 1, 1);
    }

    #[test]
    fn revoked_userid_certification() {
        // A revoked User ID shouldn't be updated.

        // User ID: SHA256
        // User ID: SHA1 (revoked)
        // Enc Subkey: SHA256
        t("sha1-userid-revoked", Some("priv"), &[], 0, 0);
    }

    #[test]
    fn signing_subkey_binding_signature() {
        // User ID: SHA256
        // Enc Subkey: SHA256
        // Sig Subkey: SHA1
        t("sha1-signing-subkey", Some("priv"), &[], 1, 1);
    }

    #[test]
    fn encryption_subkey_binding_signature() {
        // User ID: SHA256
        // Enc Subkey: SHA256
        // Enc Subkey: SHA1
        t("sha1-encryption-subkey", Some("priv"), &[], 1, 1);
    }

    #[test]
    fn subkey_backsig() {
        // User ID: SHA256
        // Enc Subkey: SHA256
        // Sig Subkey: SHA256, backsig: SHA1
        t("sha1-backsig-signing-subkey", Some("priv"), &[], 1, 1);
    }

    #[test]
    fn all_bad() {
        // User ID: SHA1
        // Enc Subkey: SHA1
        t("only-sha1", Some("priv"), &[], 2, 2);

        // We don't fix MD5 signatures.
        //
        // User ID: MD5
        // Enc Subkey: MD5
        t("only-md5", Some("priv"), &[], 2, 0);
    }

    #[test]
    fn passwords() {
        // User ID: SHA1
        // Enc Subkey: SHA1

        // Wrong password.
        t("all-sha1-password-Foobar", Some("priv"), &["foobar"], 2, 0);
        // Right password.
        t("all-sha1-password-Foobar", Some("priv"), &["Foobar"], 2, 2);

        // Try multiple passwords.
        t("all-sha1-password-Foobar", Some("priv"), &["Foobar", "bar"], 2, 2);
        t("all-sha1-password-Foobar", Some("priv"), &["bar", "Foobar"], 2, 2);
    }

    #[test]
    fn multiple_passwords() {
        // The primary is encrypted with foo and the signing subkey
        // with bar.  We need to provide both, because the signing
        // subkey needs its backsig updated.

        // User ID: SHA256
        // Enc Subkey: SHA256
        // Enc Subkey: SHA1
        // Sig Subkey: SHA1

        // We only have the password for the signing subkey: we can't
        // update anything.
        t("multiple-passwords", Some("priv"), &["bar", "Foobar"], 2, 0);
        // We only have the password for the primary key: we can't
        // update the backsig.
        t("multiple-passwords", Some("priv"), &["foo", "Foobar"], 2, 1);
        // We have all passwords: we can fix everything.
        t("multiple-passwords", Some("priv"), &["bar", "Foobar", "foo"], 2, 2);
    }

    #[test]
    fn offline_subkeys() {
        // The User ID, the encryption subkey, and the signing subkey
        // all need new signatures.  With just the primary key, we are
        // able to create two of the three required signatures.

        // User ID: SHA1
        // Enc Subkey: SHA1
        // Sig Subkey: SHA1

        // We can't update the backsig.
        t("sha1-offline-subkeys", Some("offline"), &[], 3, 2);
        // We can fix everything.
        t("sha1-offline-subkeys", Some("priv"), &[], 3, 3);
    }

    #[test]
    fn sha1_authentication_subkey() {
        // User ID: SHA1
        // Enc Subkey: SHA1
        // Auth Subkey: SHA1
        t("sha1-authentication-subkey", Some("priv"), &[], 3, 3);
    }

    #[test]
    fn authentication_subkey() {
        // An authentication subkey doesn't require a backsig.  Make
        // sure we don't flag a missing backsig as an error.

        // User ID: SHA512
        // Enc Subkey: SHA512
        // Auth Subkey: SHA512
        t("authentication-subkey", Some("priv"), &[], 0, 0);
    }

    #[test]
    fn sha1_userid_sha256_subkeys() {
        // The User ID is protected with a SHA-1 signature, but two
        // subkeys are protected with SHA256.  Make sure the subkeys
        // don't get new binding signatures.

        // User ID: SHA1
        // Enc Subkey: SHA1
        // Sig Subkey: SHA256
        // Enc Subkey: SHA256
        t("sha1-userid-sha256-subkeys", Some("priv"), &[], 2, 2);
    }

    #[test]
    fn no_backsig() {
        // If a key doesn't have a backsig and needs one, it won't be
        // detected as an issue, because it is not valid under
        // SHA1+SP.  That's okay.

        // User ID: SHA512
        // Sig Subkey: SHA512, no backsig.
        t("no-backsig", Some("priv"), &[], 0, 0);
    }

    #[test]
    fn sha512_self_sig_sha1_revocation() {
        // Under the standard policy, SHA1 revocations are considered
        // bad.  We assume that SP+SHA-1 is strictly more liberal than
        // SP (i.e., it accepts at least everything that SP accepts).

        // User ID: SHA512, SHA-1 revocation.
        t("sha512-self-sig-sha1-revocation", None, &[], 0, 0);
    }

    #[test]
    fn revoked_certificate() {
        // The certificate is only valid under SP+SHA1, and the
        // revocation certificate uses SHA1.  There is no need to
        // upgrade the certificate or the revocation certificate.

        // User ID: SHA1
        // Enc Subkey: SHA1
        // Revocation: SHA1
        t("sha1-cert-sha1-revocation", Some("priv"), &[], 0, 0);

        // The certificate is only valid under SP+SHA1, and the
        // revocation certificate uses SHA256.  There is no need to
        // upgrade the certificate or the revocation certificate.

        // User ID: SHA1
        // Enc Subkey: SHA1
        // Revocation: SHA256
        t("sha1-cert-sha256-revocation", Some("priv"), &[], 0, 0);

        // The certificate is valid under SP (the signatures use
        // SHA512), but there are two revocation certificates that use
        // SHA1.  Make sure we upgrade them.

        // User ID: SHA512
        // Enc Subkey: SHA512
        // Revocation: SHA1
        // Revocation: SHA1
        t("sha512-cert-sha1-revocation", Some("priv"), &[], 2, 2);

        // The certificate is valid under SP (the signatures use
        // SHA256), and it is revoked using a SHA256 revocation
        // certificate, which is also valid under SP.  It also has a
        // SHA-1 protected signing subkey.  Because the certificate is
        // revoked and the revocation certificate uses SHA256, we
        // don't need to fix the SHA-1 signature.  Make sure we don't.

        // User ID: SHA256
        // Enc Subkey: SHA256
        // Sig Subkey: SHA1
        // Revocation: SHA256
        t("sha256-cert-sha256-revocation", Some("priv"), &[], 0, 0);
    }

    #[test]
    fn expired_certificates() {
        // User ID: SHA256 (expired)
        // Enc Subkey: SHA256
        t("sha256-expired", Some("priv"), &[], 0, 0);

        // User ID: SHA1 (expired)
        // Enc Subkey: SHA1
        t("sha1-expired", Some("priv"), &[], 0, 0);

        // User ID: SHA256 (old, expired), SHA1 (new, live)
        // Enc Subkey: SHA256
        t("sha256-expired-sha1-live", Some("priv"), &[], 1, 1);
    }

    #[test]
    fn list_keys() {
        Command::cargo_bin("sq").unwrap()
            .current_dir(&dir())
            .args(&[
                "--no-cert-store",
                "keyring", "lint",
                "--time", FROZEN_TIME,
                "--list-keys",
                // 94F19D3CB5656E0BC3977C09A8AC5ACC2FB87104
                "sha1-userid-pub.pgp",
                // 55EF7181C288067AE189FF12F5A5CD01D8070917
                "gnupg-rsa-normal-pub.pgp"
            ])
            .assert()
            // If there are issues, the exit code is 2.
            .code(2)
            .stdout(predicate::eq("94F19D3CB5656E0BC3977C09A8AC5ACC2FB87104\n"));
    }

    #[test]
    fn signature() {
        Command::cargo_bin("sq").unwrap()
            .current_dir(&dir())
            .args(&[
                "--no-cert-store",
                "keyring", "lint",
                "--time", FROZEN_TIME,
                "msg.sig",
            ])
            .assert()
            // If there are issues, the exit code is 1.
            .code(1);
    }
}
