use std::fs;

use sequoia_openpgp as openpgp;
use openpgp::cert::KeyBuilder;
use openpgp::policy::StandardPolicy;
use openpgp::types::KeyFlags;

mod common;
use common::Environment;
use common::rotate_subkeys;

const P: &StandardPolicy = &StandardPolicy::new();

#[test]
fn keyring_update_add_certificate() -> anyhow::Result<()> {
    // Consider: Alice is the project maintainer.  She gives Bob
    // commit rights.  Bob modifies Alice's keyring to include another
    // certificate that he presumably controls.  That's not allowed.
    // Make sure we reject it.
    let e = Environment::new()?;
    let p = e.git_state();

    let (alice, alice_pgp) = e.gen("alice", None, None);
    let (bob, bob_pgp) = e.gen("bob", None, None);
    let (alice_bob, alice_bob_pgp) = e.gen("alice-bob", None, None);

    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice_pgp,
        "--project-maintainer" // All capabilities.
    ])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Alice makes herself a release manager.",
        &format!("-S{}", alice.fingerprint()),
    ])?;
    let root = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    e.sq_git(&[
        "policy",
        "authorize",
        "bob",
        "--cert-file", &bob_pgp,
        "--committer" // Committer.
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ &bob ]);
    e.git(&[
        "commit",
        "-m", "Alice makes Bob a committer.",
        &format!("-S{}", alice.fingerprint()),
    ])?;
    let _c2 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    fs::write(p.join("a"), "aaa.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Bob makes a commit.",
        &format!("-S{}", bob.fingerprint()),
    ])?;
    let c3 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    // Bob tries to add a certificate to Alice's keyring.  This should
    // fail.
    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice_bob_pgp,
        "--project-maintainer" // Committer
    ])?;
    e.check_export("alice", None, &[ &alice, &alice_bob ]);
    e.check_export("bob", None, &[ &bob ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Bob adds a certificate to Alice's keyring.",
        &format!("-S{}", bob.fingerprint()),
    ])?;

    e.git(&["log"])?;
    assert!(e.sq_git(&["log", "--trust-root", &root]).is_err());


    // Reset to c3.  Make sure Alice can add a certificate to an
    // existing entity.
    e.git(&["reset", "--soft", &c3])?;
    e.check_export("alice", None, &[ &alice, &alice_bob ]);
    e.check_export("bob", None, &[ &bob ]);
    assert_eq!(c3, e.git_current_commit()?);

    e.git(&[
        "commit",
        "-m", "Alice adds the certificate to her keyring.",
        &format!("-S{}", alice.fingerprint()),
    ])?;

    e.git(&["log"])?;
    assert!(e.sq_git(&["log", "--trust-root", &root]).is_ok());


    // And make sure the new certificate can add commits.
    fs::write(p.join("b"), "bbb.")?;
    e.git(&["add", "b"])?;
    e.git(&[
        "commit",
        "-m", "Alice makes a commit signed with the new certificate.",
        &format!("-S{}", alice_bob.fingerprint()),
    ])?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;

    Ok(())
}

#[test]
fn keyring_update_remove_certificate() -> anyhow::Result<()> {
    // Consider:
    //
    // Alice is the project maintainer.  She gives Bob commit rights.
    // Bob modifies Alice's keyring to remove her certificate.
    // That's not allowed.  Make sure we catch it.
    let e = Environment::new()?;
    let p = e.git_state();

    let (alice1, alice1_pgp) = e.gen("alice1", None, None);
    let (alice2, alice2_pgp) = e.gen("alice2", None, None);
    let (bob, bob_pgp) = e.gen("bob", None, None);

    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice1_pgp,
        "--project-maintainer" // All capabilities.
    ])?;
    e.check_export("alice", None, &[ &alice1 ]);
    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice2_pgp,
        "--project-maintainer" // All capabilities.
    ])?;
    e.check_export("alice", None, &[ &alice1, &alice2 ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Alice makes herself a release manager.",
        &format!("-S{}", alice1.fingerprint()),
    ])?;
    let root = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    e.sq_git(&[
        "policy",
        "authorize",
        "bob",
        "--cert-file", &bob_pgp,
        "--committer" // Committer
    ])?;
    e.check_export("alice", None, &[ &alice1, &alice2 ]);
    e.check_export("bob", None, &[ &bob ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Alice makes Bob a committer.",
        &format!("-S{}", alice2.fingerprint()),
    ])?;
    let _c2 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    fs::write(p.join("a"), "aaa.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Bob makes a commit.",
        &format!("-S{}", bob.fingerprint()),
    ])?;
    let c3 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    std::fs::remove_file(p.join("openpgp-policy.toml")).expect("can remove");
    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice1_pgp,
        "--project-maintainer" // All capabilities.
    ])?;
    e.sq_git(&[
        "policy",
        "authorize",
        "bob",
        "--cert-file", &bob_pgp,
        "--committer" // Committer
    ])?;
    e.check_export("alice", None, &[ &alice1 ]);
    e.check_export("bob", None, &[ &bob ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Bob removes one of Alice's certificates.",
        &format!("-S{}", bob.fingerprint()),
    ])?;

    e.git(&["log"])?;
    assert!(e.sq_git(&["log", "--trust-root", &root]).is_err());

    // Reset to c3.  Make sure alice can remove the certificate.
    e.git(&["reset", "--soft", &c3])?;
    e.check_export("alice", None, &[ &alice1 ]);
    e.check_export("bob", None, &[ &bob ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Alice removes one of her certificates.",
        &format!("-S{}", alice2.fingerprint()),
    ])?;

    Ok(())
}

#[test]
fn keyring_update_add_packet() -> anyhow::Result<()> {
    // It's always safe to add to an existing certificate.
    let e = Environment::new()?;
    let p = e.git_state();

    let (alice, alice_pgp) = e.gen("alice", None, None);
    let (bob, bob_pgp) = e.gen("bob", None, None);

    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice_pgp,
        "--project-maintainer" // All capabilities.
    ])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Alice makes herself a release manager.",
        &format!("-S{}", alice.fingerprint()),
    ])?;
    let root = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    e.sq_git(&[
        "policy",
        "authorize",
        "bob",
        "--cert-file", &bob_pgp,
        "--committer" // Committer.
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ &bob ]);
    e.git(&[
        "commit",
        "-m", "Alice makes Bob a committer.",
        &format!("-S{}", alice.fingerprint()),
    ])?;
    let _c2 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    fs::write(p.join("a"), "aaa.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Bob makes a commit.",
        &format!("-S{}", bob.fingerprint()),
    ])?;
    let _c3 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    // Bob updates his certificate.
    let bob2 = rotate_subkeys(&bob);
    assert!(bob.keys().count() < bob2.keys().count());
    let bob2_pgp = e.serialize_cert("bob2", &bob2);
    e.import(&bob2).expect("can import");

    e.sq_git(&[
        "policy",
        "authorize",
        "bob",
        "--cert-file", &bob2_pgp,
        "--committer" // Committer
    ])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ &bob2 ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Bob updates his certificate.",
        &format!("-S{}", bob.fingerprint()),
    ])?;

    e.git(&["log"])?;
    assert!(e.sq_git(&["log", "--trust-root", &root]).is_ok());

    // Bob updates Alice's certificate.
    let alice_vc = alice.with_policy(P, None).expect("valid cert");
    let alice2 = KeyBuilder::new(KeyFlags::signing())
        .subkey(alice_vc).unwrap()
        .attach_cert().unwrap();
    assert!(alice.keys().count() < alice2.keys().count());
    let alice2_pgp = e.serialize_cert("alice2", &alice2);
    e.import(&alice2).expect("can import");

    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice2_pgp,
    ])?;
    e.check_export("alice", None, &[ &alice2 ]);
    e.check_export("bob", None, &[ &bob2 ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Bob updates Alice's certificate.",
        &format!("-S{}", bob.fingerprint()),
    ])?;

    e.git(&["log"])?;
    assert!(e.sq_git(&["log", "--trust-root", &root]).is_ok());

    Ok(())
}

#[test]
fn keyring_update_remove_packet() -> anyhow::Result<()> {
    // Removing a packet requires authorization.
    let e = Environment::new()?;
    let p = e.git_state();

    let (alice, alice_pgp) = e.gen("alice", None, None);
    let (bob, bob_pgp) = e.gen("bob", None, None);

    let bob_vc = bob.with_policy(P, None).expect("valid cert");
    let bob2 = KeyBuilder::new(KeyFlags::signing())
        .subkey(bob_vc).unwrap()
        .attach_cert().unwrap();
    assert!(bob.keys().count() < bob2.keys().count());
    let bob2_pgp = e.serialize_cert("bob2", &bob2);
    e.import(&bob2).expect("can import");

    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice_pgp,
        "--project-maintainer" // All capabilities.
    ])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Alice makes herself a release manager.",
        &format!("-S{}", alice.fingerprint()),
    ])?;
    let root = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    e.sq_git(&[
        "policy",
        "authorize",
        "bob",
        "--cert-file", &bob2_pgp,
        "--committer" // Committer.
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ &bob2 ]);
    e.git(&[
        "commit",
        "-m", "Alice makes Bob a committer.",
        &format!("-S{}", alice.fingerprint()),
    ])?;
    let _c2 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    fs::write(p.join("a"), "aaa.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Bob makes a commit.",
        &format!("-S{}", bob.fingerprint()),
    ])?;
    let c3 = e.git_current_commit()?;

    e.git(&["log"])?;
    e.sq_git(&["log", "--trust-root", &root])?;


    // Bob tries to strip a User ID from his certificate.  This should
    // fail.
    std::fs::remove_file(p.join("openpgp-policy.toml")).expect("can remove");
    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice_pgp,
        "--project-maintainer" // All capabilities.
    ])?;
    e.sq_git(&[
        "policy",
        "authorize",
        "bob",
        "--cert-file", &bob_pgp,
        "--committer" // Committer
    ])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ &bob ]);

    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Bob strips a user ID from his certificate.",
        &format!("-S{}", bob.fingerprint()),
    ])?;

    e.git(&["log"])?;
    assert!(e.sq_git(&["log", "--trust-root", &root]).is_err());


    // Reset to c3.  Make sure alice can remove the user ID.
    e.git(&["reset", "--soft", &c3])?;
    e.check_export("alice", None, &[ &alice ]);
    e.check_export("bob", None, &[ &bob ]);
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Alice strips a user ID from Bob's certificate.",
        &format!("-S{}", alice.fingerprint()),
    ])?;

    Ok(())
}
