use std::fs;

use sequoia_openpgp::types::ReasonForRevocation;

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

#[test]
fn revoked_certificate() -> anyhow::Result<()> {
    // Consider:
    //
    // Alice is authorized to add commits at time t0.  Then she
    // rotates her signing subkey.

    let e = Environment::new()?;
    let p = e.git_state();

    let (alice, alice_pgp) = e.gen("alice", None, None);
    let alice_fpr = &alice.fingerprint().to_string();

    // Alice adds herself as the project maintainer.
    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice_pgp,
        "--project-maintainer"
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Initial commit.",
    ])?;
    let root = e.git_current_commit()?;

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

    // Check that she can add commits.
    fs::write(p.join("2"), "2.")?;
    e.git(&["add", "2"])?;
    e.git(&[
        "commit",
        "-m", "Alice adds a commit.",
        &format!("-S{}", alice_fpr),
    ])?;
    let _c2 = e.git_current_commit()?;

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

    // Alice rotates her signing subkey.
    let alice_rotated = rotate_subkeys(&alice);
    assert!(alice_rotated.keys().count() > alice.keys().count());
    e.import(&alice_rotated).expect("can import");

    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert", &alice.fingerprint().to_string(),
        "--sign-commit"
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Adding rotated key.",
        &format!("-S{}", alice_fpr),
    ])?;
    let _c3 = e.git_current_commit()?;

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

    e.check_export("alice", None, &[ &alice_rotated ]);


    // Check that she can still add commits.
    fs::write(p.join("4"), "4.")?;
    e.git(&["add", "4"])?;
    e.git(&[
        "commit",
        "-m", "Alice adds a commit.",
        &format!("-S{}", alice_fpr),
    ])?;
    let _c4 = e.git_current_commit()?;

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


    // Alice revokes her certificate.
    let alice_revoked
        = revoke_cert(&alice_rotated, ReasonForRevocation::KeyRetired);

    let alice_revoked_pgp
        = e.serialize_cert("alice-revoked", &alice_revoked);

    e.sq_git(&[
        "policy",
        "authorize",
        "alice",
        "--cert-file", &alice_revoked_pgp,
        "--sign-commit"
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Adding revoked key.",
        &format!("-S{}", alice_fpr),
    ])?;
    let _c5 = e.git_current_commit()?;

    e.check_export("alice", None, &[ &alice_revoked ]);

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


    // Alice shouldn't be able to add any more commits.
    fs::write(p.join("6"), "6.")?;
    e.git(&["add", "6"])?;
    e.git(&[
        "commit",
        "-m", "Alice adds a commit with her revoked key.",
        &format!("-S{}", alice_fpr),
    ])?;
    let _c6 = e.git_current_commit()?;

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

    Ok(())
}

//#[test]
//fn hard_revocation_bypass_branched_policy() -> anyhow::Result<()> {
//    // An attacker whose key has been hard-revoked on one branch can
//    // bypass the revocation by merging with strategy "ours", keeping
//    // the old policy.  sha512sum() returns [] for all inputs, so
//    // only the first policy seen during BFS is scanned for hard
//    // revocations — the rest are silently skipped.
//
//    let e = Environment::new()?;
//    let p = e.git_state();
//
//    let (alice, alice_pgp) = e.gen("alice", None, None);
//    let alice_fpr = &alice.fingerprint().to_string();
//
//    let (bob, bob_pgp) = e.gen("bob", None, None);
//    let bob_fpr = &bob.fingerprint().to_string();
//
//    // Alice bootstraps the project, authorizes herself and Bob.
//    e.sq_git(&[
//        "policy", "authorize", "alice",
//        "--cert-file", &alice_pgp,
//        "--project-maintainer",
//    ])?;
//    e.sq_git(&[
//        "policy", "authorize", "bob",
//        "--cert-file", &bob_pgp,
//        "--sign-commit",
//    ])?;
//    e.git(&["add", "openpgp-policy.toml"])?;
//    e.git(&["commit", "-m", "Initial commit."])?;
//    let root = e.git_current_commit()?;
//
//    // Bob makes a legitimate commit.
//    fs::write(p.join("file1"), "content")?;
//    e.git(&["add", "file1"])?;
//    e.git(&[
//        "commit", "-m", "Bob adds a file.",
//        &format!("-S{}", bob_fpr),
//    ])?;
//
//    // Branch here — before the hard revocation.
//    e.git(&["branch", "attacker-branch"])?;
//
//    // On main: hard-revoke Bob's certificate (KeyCompromised).
//    let bob_revoked = revoke_cert(&bob, ReasonForRevocation::KeyCompromised);
//    let bob_revoked_pgp = e.serialize_cert("bob-revoked", &bob_revoked);
//    e.sq_git(&[
//        "policy", "authorize", "bob",
//        "--cert-file", &bob_revoked_pgp,
//        "--sign-commit",
//    ])?;
//    e.git(&["add", "openpgp-policy.toml"])?;
//    e.git(&[
//        "commit", "-m", "Hard-revoke Bob (KeyCompromised).",
//        &format!("-S{}", alice_fpr),
//    ])?;
//
//    // Sanity: main verifies fine.
////    e.sq_git(&["log", "--trust-root", &root])?;
//
//    // Switch to attacker-branch (policy has no revocation).
//    e.git(&["checkout", "attacker-branch"])?;
//
//    // Attacker signs a new commit with the compromised key.
//    fs::write(p.join("malicious"), "payload")?;
//    e.git(&["add", "malicious"])?;
//    e.git(&[
//        "commit", "-m", "Attacker commit.",
//        &format!("-S{}", bob_fpr),
//    ])?;
//
//    // Merge main with strategy "ours": brings in main's history
//    // but keeps our tree — policy stays without the revocation.
//    e.git(&[
//        "merge", "main", "-s", "ours",
//        "-m", "Merge.",
//        &format!("-S{}", bob_fpr),
//    ])?;
//
//    // This MUST fail: Bob's key is hard-revoked on main.
//    // Hard revocations apply retroactively across all branches.
//    //
//    // BUG: sha512sum() returns [] for every policy, so only the
//    // merge commit's policy (no revocation) is scanned. Main's
//    // policy (with the hard revocation) is skipped. Verification
//    // incorrectly passes.
//    assert!(
//        e.sq_git(&["log", "--trust-root", &root]).is_err(),
//        "Bob's hard-revoked key must not pass verification"
//    );
//
//    Ok(())
//}
#[test]
fn hard_revocation_bypass_branched_policy() -> anyhow::Result<()> {
    // An attacker whose key has been hard-revoked on one branch can
    // bypass the revocation by merging with strategy "ours", keeping
    // the old policy.  sha512sum() returns [] for all inputs, so
    // only the first policy seen during BFS is scanned for hard
    // revocations — the rest are silently skipped.

    let e = Environment::new()?;
    let p = e.git_state();

    let (alice, alice_pgp) = e.gen("alice", None, None);
    let alice_fpr = alice.fingerprint().to_string();
    let (bob, bob_pgp) = e.gen("bob", None, None);
    let bob_fpr = bob.fingerprint().to_string();

    // Alice sets up policy: she's maintainer, Bob can sign commits.
    e.sq_git(&[
        "policy", "authorize", "alice",
        "--cert-file", &alice_pgp,
        "--project-maintainer",
    ])?;
    e.sq_git(&[
        "policy", "authorize", "bob",
        "--cert-file", &bob_pgp,
        "--sign-commit",
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&["commit", "-m", "Initial commit.", &format!("-S{}", alice_fpr)])?;
    let root = e.git_current_commit()?;

    // Sanity check: initial commit verifies.
    e.sq_git(&["log", "--trust-root", &root])?;

    // Bob signs a commit - should be valid.
    fs::write(p.join("file1"), "content1")?;
    e.git(&["add", "file1"])?;
    e.git(&["commit", "-m", "Bob's first commit.", &format!("-S{}", bob_fpr)])?;
    e.sq_git(&["log", "--trust-root", &root])?;

    // Create feature branch at this point (before revocation).
    e.git(&["checkout", "-b", "feature"])?;

    // Switch back to master to add the hard revocation.
    e.git(&["switch", "master"])?;

    // Hard-revoke Bob's certificate (KeyCompromised = hard revocation).
    let bob_revoked = revoke_cert(&bob, ReasonForRevocation::KeyCompromised);
    let bob_revoked_pgp = e.serialize_cert("bob-revoked", &bob_revoked);

    // Update the policy with the revoked certificate.
    e.sq_git(&[
        "policy", "authorize", "bob",
        "--cert-file", &bob_revoked_pgp,
        "--sign-commit",
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&["commit", "-m", "Revoke Bob's key (compromised).",
            &format!("-S{}", alice_fpr)])?;

    // Sanity: on master, Bob's earlier commit should now fail (hard
    // revocation is retroactive). This works even with the bug
    // because this is the first policy on this branch.
    e.sq_git(&["log", "--trust-root", &root])
        .expect_err("Bob's pre-revocation commit should fail on main \
                     (hard revocation is retroactive)");

    // Switch to the feature branch. Policy here does NOT have the
    // revocation.
    e.git(&["switch", "feature"])?;
    e.sq_git(&["log", "--trust-root", &root])
        .expect("Traversal does not see commit with Bob's hard revocation");

    // Bob signs another commit on the feature branch.
    fs::write(p.join("feature-file"), "feature content")?;
    e.git(&["add", "feature-file"])?;
    e.git(&["commit", "-m", "Bob's feature commit.",
            &format!("-S{}", bob_fpr)])?;

    // Merge master into feature using --strategy=ours.
    // Brings master's history as parent but keeps feature's tree
    // (which has the pre-revocation policy).
    e.git(&["merge", "-m", "Merge main into feature (ours strategy).",
            "--strategy=ours", "--no-edit",
            &format!("-S{}", alice_fpr),
            "master"])?;

    // THE KEY ASSERTION:
    // sq-git log should REJECT this merge because Bob's key is
    // hard-revoked on the master branch, and hard revocations are
    // retroactive (they apply to all commits, even past ones).
    //
    // With the sha512sum bug: this PASSES (incorrectly) because
    // all policies hash to the same empty vec, so the policy with
    // the revocation is never scanned.
    //
    // With the fix: this FAILS (correctly) because the distinct
    // policies are scanned and the revocation is detected.
    let result = e.sq_git(&["log", "--trust-root", &root]);
    result.expect_err(
        "Merge should be rejected: Bob's key is hard-revoked \
         (KeyCompromised) and hard revocations are retroactive. \
         If this passes, the sha512sum deduplication bug is hiding \
         the revocation.");

    Ok(())
}
