use std::fs;
mod common;
use common::Environment;

#[test]
fn create_environment() -> anyhow::Result<()> {
    let e = Environment::new()?;
    assert!(e.gnupg_state().exists());
    assert!(e.git_state().exists());
    Ok(())
}

#[test]
fn make_commit() -> anyhow::Result<()> {
    let e = Environment::new()?;
    let p = e.git_state();
    fs::write(p.join("a"), "Aller Anfang ist schwer.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Initial commit.",
    ])?;
    Ok(())
}

#[test]
fn sign_commit() -> anyhow::Result<()> {
    let e = Environment::new()?;
    let p = e.git_state();
    fs::write(p.join("a"), "Aller Anfang ist schwer.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Initial commit.",
        &format!("-S{}", e.willow.fingerprint),
    ])?;
    Ok(())
}

#[test]
fn verify_commit() -> anyhow::Result<()> {
    let e = Environment::new()?;
    let p = e.git_state();
    e.sq_git(&[
        "policy",
        "authorize",
        e.willow.petname,
        &e.willow.fingerprint.to_string(),
        "--sign-commit"
    ])?;
    e.git(&["add", "openpgp-policy.toml"])?;
    e.git(&[
        "commit",
        "-m", "Initial commit.",
        &format!("-S{}", e.willow.fingerprint),
    ])?;
    let root = e.git_current_commit()?;

    fs::write(p.join("a"), "Aller Anfang ist schwer.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "First change.",
        &format!("-S{}", e.willow.fingerprint),
    ])?;

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

    fs::write(p.join("a"), "Und es bleibt schwer.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Second change.",
        &format!("-S{}", e.willow.fingerprint),
    ])?;

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

    Ok(())
}

#[test]
fn shadow_verify_commit() -> anyhow::Result<()> {
    let e = Environment::new()?;
    let p = e.git_state();

    e.sq_git(&[
        "policy",
        "authorize",
        "--policy-file", "shadow-policy.toml",
        e.willow.petname,
        &e.willow.fingerprint.to_string(),
        "--sign-commit"
    ])?;

    fs::write(p.join("a"), "Aller Anfang ist schwer.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Initial commit.",
        &format!("-S{}", e.willow.fingerprint),
    ])?;
    let root = e.git_current_commit()?;

    e.sq_git(&[
        "log",
        "--trust-root", &root,
        "--policy-file", "shadow-policy.toml",
    ])?;

    fs::write(p.join("a"), "Und es bleibt schwer.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Second change.",
        &format!("-S{}", e.willow.fingerprint),
    ])?;

    e.sq_git(&[
        "log",
        "--trust-root", &root,
        "--policy-file", "shadow-policy.toml",
    ])?;

    Ok(())
}

#[test]
fn commit_with_no_policy() -> anyhow::Result<()>
{
    // Test that we can process a commit that doesn't have a policy.
    //
    //                   E       <- Merge commit (authorized by C)
    //                /     \
    //    Policy ->   C     D    <- No policy.
    //                 \   /
    //                   B
    //                   |
    //                   A       <- Trust root.
    //

    let (e, root) = Environment::scooby_gang_bootstrap(None)?;

    e.git(&["checkout", "-b", "main"])
        .expect("can create branch");

    // Create a commit before the fork.
    e.git_commit(
        &[("pre-fork", Some(b"xxx"))],
        "pre-fork commit (willow)",
        Some(&e.willow_release)).unwrap();
    assert!(e.sq_git(&["log", "--trust-root", &root,
                       &format!("{}..", root)])
            .is_ok());

    // Create the no-policy branch.
    e.git(&["checkout", "-b", "no-policy"])
        .expect("can create no-policy branch");

    e.git(&["rm", "openpgp-policy.toml"])
        .expect("can remove openpgp-policy.toml");
    e.git_commit(
        &[("no-policy", Some(b"xxx"))],
        "no-policy commit",
        None).unwrap();
    assert!(e.sq_git(&["log", "--trust-root", &root,
                       &format!("{}..", root)])
            .is_err());

    // Add a few commits to the main branch.
    e.git(&["switch", "main"]).expect("can switch to main");

    for i in 0..3 {
        e.git_commit(
            &[(&format!("main-{}", i), Some(b"xxx"))],
            &format!("main commit #{} (willow)", i + 1),
            Some(&e.willow_release)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_ok());
    }

    // Then merge the branches.
    e.git_merge_no_ff(&[&"no-policy", "main"],
                      "Merge (willow)",
                      Some(&e.willow_release),
                      &[]).unwrap();

    eprintln!("Graph:\n{}", e.git_log_graph().unwrap());

    assert!(e.sq_git(&["log", "--trust-root", &root,
                       &format!("{}..", root)]).is_ok());

    Ok(())
}

fn merge(pre_fork_commits: usize, feature_commits: (bool, usize), main_commits: usize)
    -> anyhow::Result<()>
{
    // Test merging feature branches that are and are not part of the
    // authentication path.  We first add 0 or more commits, then we
    // create a feature branch with 1 or more commits, add 0 or more
    // commits to the main branch, and finally we merge the feature
    // branch into main.
    //
    //                   K       <- Post-merge commits (authorized)
    //                   |
    //                   J       <- Merge commit (authorized)
    //                /     \
    //           ->   I     F    <-
    // Main     '     |     |      `
    // commits --->   H     E    <--- Feature branch commits (maybe authorized)
    // (authed) `     |     |      '
    //           ->   G     D    <-
    //                 \   /
    //                   C       <-.
    //                   |          Pre-fork commits (authorized)
    //                   B       <-'
    //                   |
    //                   A       <- Trust root.
    //

    eprintln!("========================================================");
    eprintln!("Testing configuration: {} pre-fork commits, \
               {} {} authorized feature commits, {} main commits",
              pre_fork_commits,
              feature_commits.1,
              if feature_commits.0 {
                  ""
              } else {
                  "not "
              },
              main_commits);

    let (e, root) = Environment::scooby_gang_bootstrap(None)?;

    e.git(&["checkout", "-b", "main"])
        .expect("can create branch");

    // Create some commits before the fork.
    for i in 0..pre_fork_commits {
        // Willow adds a few commits.
        e.git_commit(
            &[(&format!("pre-fork-{}", i), Some(b"xxx"))],
            &format!("pre-fork commit #{} (willow)", i + 1),
            Some(&e.willow_release)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_ok());
    }

    // Create the feature branch.  These have the unauthorized
    // commits.
    e.git(&["checkout", "-b", "feature"])
        .expect("can create feature branch");

    let mut feature_head = "feature".to_string();
    let feature_commits_authorized = feature_commits.0;
    let feature_commit_signer = if feature_commits_authorized {
        &e.willow_release
    } else {
        &e.xander
    };
    for i in 0..feature_commits.1 {
        feature_head
            = e.git_commit(
                &[(&format!("feature-{}", i), Some(b"xxx"))],
                &format!("feature commit #{} ({})",
                         i + 1, feature_commit_signer.petname),
                Some(&feature_commit_signer)).unwrap();
        assert_eq!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                   .is_ok(),
                   feature_commits_authorized);
    }

    // Add commits to the main branch.
    e.git(&["switch", "main"]).expect("can switch to main");

    for i in 0..main_commits {
        // Willow adds a few commits.
        e.git_commit(
            &[(&format!("main-{}", i), Some(b"xxx"))],
            &format!("main commit #{} (willow)", i + 1),
            Some(&e.willow_release)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_ok());

        if i == 0 {
            e.git(&["checkout", "-b", "main-continued"])
                .expect("can create feature branch");
        }
    }

    // Merge feature branch.
    e.git_merge_no_ff(&[&feature_head, "main"],
                      "Merge (willow)",
                      Some(&e.willow_release),
                      &[]).unwrap();

    eprintln!("Graph:\n{}", e.git_log_graph().unwrap());

    assert!(e.sq_git(&["log", "--trust-root", &root,
                       &format!("{}..", root)]).is_ok());

    // Add a few more commits after the merge.
    for i in 0..2 {
        e.git_commit(
            &[(&format!("post-merge-{}", i), Some(b"xxx"))],
            &format!("post-merge commit #{} (willow)", i + 1),
            Some(&e.willow_release)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_ok());
    }

    // Make sure that we can authenticate a path that includes a
    // particular commit.
    if main_commits > 0 {
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("main-continued..")])
                .is_ok());
    }
    assert_eq!(e.sq_git(&["log", "--trust-root", &root,
                       &format!("feature..")])
               .is_ok(),
               feature_commits_authorized);

    Ok(())
}

fn merge_cross_product(pre_fork: &[usize],
                       feature: &[(bool, usize)],
                       main: &[usize])
{
    for (&pre_fork_commits, &feature_commits, &main_commits) in
        pre_fork.into_iter().flat_map(|pre| {
            feature.into_iter().flat_map(move |feature| {
                main.into_iter().map(move |main| {
                    (pre, feature, main)
                })
            })
        })
    {
        merge(pre_fork_commits, feature_commits, main_commits)
            .expect(&format!("Test pre-fork commits: {}, \
                              feature commits: {:?}, \
                              main commits: {} \
                              failed",
                             pre_fork_commits,
                             feature_commits,
                             main_commits));
    }
}

// Manually unroll:
//
//    let pre_fork = [ 0, 1, 2 ];
//    let feature = [ (false, 1), (false, 4), (true, 1), (true, 4) ];
//    let main = [ 0, 1, 4 ];
#[test] fn merge_0_1_unauthorized_0() { merge_cross_product(&[0], &[(false, 1)], &[0]) }
#[test] fn merge_0_1_unauthorized_1() { merge_cross_product(&[0], &[(false, 1)], &[1]) }
#[test] fn merge_0_1_unauthorized_4() { merge_cross_product(&[0], &[(false, 1)], &[4]) }
#[test] fn merge_0_4_unauthorized_0() { merge_cross_product(&[0], &[(false, 4)], &[0]) }
#[test] fn merge_0_4_unauthorized_1() { merge_cross_product(&[0], &[(false, 4)], &[1]) }
#[test] fn merge_0_4_unauthorized_4() { merge_cross_product(&[0], &[(false, 4)], &[4]) }

#[test] fn merge_1_1_unauthorized_0() { merge_cross_product(&[1], &[(false, 1)], &[0]) }
#[test] fn merge_1_1_unauthorized_1() { merge_cross_product(&[1], &[(false, 1)], &[1]) }
#[test] fn merge_1_1_unauthorized_4() { merge_cross_product(&[1], &[(false, 1)], &[4]) }
#[test] fn merge_1_4_unauthorized_0() { merge_cross_product(&[1], &[(false, 4)], &[0]) }
#[test] fn merge_1_4_unauthorized_1() { merge_cross_product(&[1], &[(false, 4)], &[1]) }
#[test] fn merge_1_4_unauthorized_4() { merge_cross_product(&[1], &[(false, 4)], &[4]) }

#[test] fn merge_2_1_unauthorized_0() { merge_cross_product(&[2], &[(false, 1)], &[0]) }
#[test] fn merge_2_1_unauthorized_1() { merge_cross_product(&[2], &[(false, 1)], &[1]) }
#[test] fn merge_2_1_unauthorized_4() { merge_cross_product(&[2], &[(false, 1)], &[4]) }
#[test] fn merge_2_4_unauthorized_0() { merge_cross_product(&[2], &[(false, 4)], &[0]) }
#[test] fn merge_2_4_unauthorized_1() { merge_cross_product(&[2], &[(false, 4)], &[1]) }
#[test] fn merge_2_4_unauthorized_4() { merge_cross_product(&[2], &[(false, 4)], &[4]) }

#[test] fn merge_0_1_authorized_0() { merge_cross_product(&[0], &[(true, 1)], &[0]) }
#[test] fn merge_0_1_authorized_1() { merge_cross_product(&[0], &[(true, 1)], &[1]) }
#[test] fn merge_0_1_authorized_4() { merge_cross_product(&[0], &[(true, 1)], &[4]) }
#[test] fn merge_0_4_authorized_0() { merge_cross_product(&[0], &[(true, 4)], &[0]) }
#[test] fn merge_0_4_authorized_1() { merge_cross_product(&[0], &[(true, 4)], &[1]) }
#[test] fn merge_0_4_authorized_4() { merge_cross_product(&[0], &[(true, 4)], &[4]) }

#[test] fn merge_1_1_authorized_0() { merge_cross_product(&[1], &[(true, 1)], &[0]) }
#[test] fn merge_1_1_authorized_1() { merge_cross_product(&[1], &[(true, 1)], &[1]) }
#[test] fn merge_1_1_authorized_4() { merge_cross_product(&[1], &[(true, 1)], &[4]) }
#[test] fn merge_1_4_authorized_0() { merge_cross_product(&[1], &[(true, 4)], &[0]) }
#[test] fn merge_1_4_authorized_1() { merge_cross_product(&[1], &[(true, 4)], &[1]) }
#[test] fn merge_1_4_authorized_4() { merge_cross_product(&[1], &[(true, 4)], &[4]) }

#[test] fn merge_2_1_authorized_0() { merge_cross_product(&[2], &[(true, 1)], &[0]) }
#[test] fn merge_2_1_authorized_1() { merge_cross_product(&[2], &[(true, 1)], &[1]) }
#[test] fn merge_2_1_authorized_4() { merge_cross_product(&[2], &[(true, 1)], &[4]) }
#[test] fn merge_2_4_authorized_0() { merge_cross_product(&[2], &[(true, 4)], &[0]) }
#[test] fn merge_2_4_authorized_1() { merge_cross_product(&[2], &[(true, 4)], &[1]) }
#[test] fn merge_2_4_authorized_4() { merge_cross_product(&[2], &[(true, 4)], &[4]) }


#[test]
fn via_commit() -> anyhow::Result<()>
{
    // There are multiple authenticated paths from the trust root to
    // the target.  We want to know whether there is an authenticated
    // path via a particular commit.  This is tricky, because the
    // commit we are interested in is only on one of the authenticated
    // paths and there is an unauthenticated path.
    //
    // Consider the following, where the middle branch has an
    // unauthenticated commit:
    //
    //    target
    //      o                   \
    //      |                     Post-merge
    //      o                   /
    //      |
    //      o
    //    ' | `
    //  /   |   \
    // o   bad   o       5
    // |    |    |
    // |    |    o
    // |    |    | `
    // o    o    o  |    4
    // |    |    |  |
    // |    o    |  |
    // |  / | \  |  |
    // a '  C  ` b  |    3
    // |  --+----+-'
    // A '  o    B       2
    // |    |    |
    // o    o    o       1
    //  \   |   /
    //    ` o '                 \
    //      |                    \
    //      o                      Pre-fork
    //      |                    /
    //      o                   /
    //      |
    //  Trust root
    //
    // We want to make sure that we can authenticate from the trust
    // root to the target via (e.g.) A and B, but not C.
    //
    // The fact that a and b each have a parent along the
    // unauthenticated path complicates things, because we will
    // normally stop exploring the path when we reach the bad commit.

    let (e, root) = Environment::scooby_gang_bootstrap(None)?;

    e.git(&["checkout", "-b", "main"])
        .expect("can create branch");

    // Create some commits before the fork.
    let mut pre_fork_commits = Vec::new();
    for i in 0..3 {
        // Willow adds a few commits.
        let commit = e.git_commit(
            &[(&format!("pre-fork-{}", i), Some(b"xxx"))],
            &format!("pre-fork commit #{} (willow)", i + 1),
            Some(&e.willow_release)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_ok());

        pre_fork_commits.push(commit);
    }

    // Create the first good branch.
    e.git(&["switch", "main"]).expect("can switch to main");

    e.git(&["checkout", "-b", "good-a"])
        .expect("can create good-a branch");

    let mut good_a = Vec::new();
    for i in 0..5 {
        // Willow adds a few commits.
        let commit = e.git_commit(
            &[(&format!("good-a-{}", i), Some(b"xxx"))],
            &format!("good-a commit #{} (willow)", i + 1),
            Some(&e.willow_release)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_ok());

        good_a.push(commit);
    }

    // Create the second good branch.
    e.git(&["switch", "main"]).expect("can switch to main");

    e.git(&["checkout", "-b", "good-b"])
        .expect("can create good-b branch");

    let mut good_b = Vec::new();
    for i in 0..5 {
        // Willow adds a few commits.
        let commit = e.git_commit(
            &[(&format!("good-b-{}", i), Some(b"xxx"))],
            &format!("good-b commit #{} (willow)", i + 1),
            Some(&e.willow_release)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_ok());

        if i == 3 {
            e.git_merge_no_ff(
                &[
                    &good_a[0],
                    &commit,
                ],
                "b merge: good a #1, good b #4 merge (willow)",
                Some(&e.willow_release),
                &[]).unwrap();
        }

        good_b.push(commit);
    }

    // Create the bad branch with the unauthorized commits.
    e.git(&["switch", "main"]).expect("can switch to main");

    e.git(&["checkout", "-b", "bad"])
        .expect("can create bad branch");

    let mut bad = Vec::new();
    for i in 0..5 {
        let commit = e.git_commit(
                &[(&format!("bad-{}", i), Some(b"xxx"))],
                &format!("feature commit #{} (xander)", i + 1),
                Some(&e.xander)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_err());

        if i == 2 {
            e.git_merge_no_ff(
                &[
                    &good_a[2],
                    &good_b[2],
                    &commit,
                ],
                "bad merge: good a #3, good b #3, bad #3 merge (willow)",
                Some(&e.willow_release),
                &[]).unwrap();
        }

        bad.push(commit);
    }

    // Merge the tips of the three branches.
    e.git_merge_no_ff(
        &[
            &good_a.last().expect("have one"),
            &good_b.last().expect("have one"),
            &bad.last().expect("have one"),
        ],
        "Merge (willow)",
        Some(&e.willow_release),
        &[]).unwrap();

    eprintln!("Graph:\n{}", e.git_log_graph().unwrap());

    assert!(e.sq_git(&["log", "--trust-root", &root,
                       &format!("{}..", root)]).is_ok());

    // Add a few more commits after the merge.
    let mut post_merge = Vec::new();
    for i in 0..2 {
        let commit = e.git_commit(
            &[(&format!("post-merge-{}", i), Some(b"xxx"))],
            &format!("post-merge commit #{} (willow)", i + 1),
            Some(&e.willow_release)).unwrap();
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", root)])
                .is_ok());
        post_merge.push(commit);
    }

    // Make sure that we can authenticate a path that includes a
    // particular commit.
    for (i, commit) in pre_fork_commits.iter().enumerate() {
        eprintln!("Checking via pre-fork-{}", i + 1);
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", commit)])
                .is_ok());
    }
    for (i, commit) in good_a.iter().enumerate() {
        eprintln!("Checking via good-a-{}", i + 1);
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", commit)])
                .is_ok());
    }
    for (i, commit) in good_b.iter().enumerate() {
        eprintln!("Checking via good-b-{}", i + 1);
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", commit)])
                .is_ok());
    }
    // And that we fail when it must go via a commit that isn't on an
    // authenticated commit.
    for (i, commit) in bad.iter().enumerate() {
        eprintln!("Checking via bad-{}", i + 1);
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", commit)])
                .is_err());
    }
    for (i, commit) in post_merge.iter().enumerate() {
        eprintln!("Checking via post-merge-{}", i + 1);
        assert!(e.sq_git(&["log", "--trust-root", &root,
                           &format!("{}..", commit)])
                .is_ok());
    }

    Ok(())
}
