use std::borrow::Cow;
use std::fs;
use std::io::Write;
use std::path::PathBuf;

mod common;
use common::Environment;

/// Initializes a "local" respository and a "remote" respository, and
/// adds the remote repository as a remote named "origin".
///
/// Returns the envirnment, the root, and the location of the external
/// policy file (if enabled).
///
/// If `use_external_policy` is true, the update hook is configured to
/// use the external policy otherwise it is configured to use the
/// internal policy.
///
/// If `create_interanl_policy` is false, no policy is added to the
/// initial (root) commit.  The initial commit is still signed with
/// the willow release key.
fn create_environment(use_external_policy: bool,
                      create_internal_policy: bool,
                      signer: Option<&str>)
    -> anyhow::Result<(Environment, String, Option<PathBuf>)>
{
    let (e, root) = if create_internal_policy {
        Environment::scooby_gang_bootstrap(signer)?
    } else {
        let e = Environment::new()?;
        e.init_repo(None)?;
        let root = e.git_commit(&[("initial-commit", Some(b"xxx"))],
                                "Initial commit (no signing policy)",
                                Some(&e.willow_release))
            .expect("can commit");
        (e, root)
    };

    // Create a bare repository under our scratch directory.
    let repo = e.scratch_state().join("repo");
    e.git(&["init", "--bare", &repo.display().to_string()])?;

    // Add as origin.
    e.git(&["remote", "add", "origin", &repo.display().to_string()])?;

    e.git(&["checkout", "-b", "main"])?;
    e.git(&["push", "origin", "main"])?;

    // Create the external policy file.
    let external_policy_file = if use_external_policy {
        let external_policy_file = e.scratch_state().join("policy.toml");
        e.scooby_gang_bootstrap_policy(&external_policy_file)?;

        Some(external_policy_file)
    } else {
        None
    };

    // Set us as update hook.
    let update_hook = repo.join("hooks").join("update");
    let mut update = fs::File::create(&update_hook)?;
    writeln!(update, "#!/bin/sh")?;
    writeln!(update)?;
    writeln!(update,
             "{} update-hook --trust-root={} {}{} \"$@\"",
             Environment::sq_git_path()?.display(),
             root,
             if external_policy_file.is_some() {
                 "--policy-file="
             } else {
                 ""
             },
             if let Some(file) = external_policy_file.as_ref() {
                 Cow::Owned(file.display().to_string())
             } else {
                 Cow::Borrowed("")
             })?;

    eprintln!("Update hook {}:\n{}",
              update_hook.display(),
              String::from_utf8_lossy(
                  &std::fs::read(&update_hook)
                      .expect("Can read update hook file")));

    #[cfg(unix)]
    {
        // Make file executable.
        use std::os::unix::fs::PermissionsExt;

        let metadata = update.metadata()?;
        let mut permissions = metadata.permissions();
        permissions.set_mode(0o755);
        update.set_permissions(permissions)?;
    }

    Ok((e, root, external_policy_file))
}

#[test]
fn update_hook_internal_policy() -> anyhow::Result<()> {
    // Test that the update hook works using an internal policy.
    let (e, _root, _) = create_environment(false, true, None)?;
    let p = e.git_state();

    // Bookmark.
    e.git(&["checkout", "-b", "test-base"])?;

    // Willow's code-signing key can change the source code, as she
    // has the sign-commit right.
    e.git(&["checkout", "-b", "test-willow"])?;
    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.git(&["push", "origin", "test-willow"])?;

    // Reset.
    e.git(&["checkout", "test-base"])?;
    e.git(&["clean", "-fdx"])?;

    // Her release key also has that right, because she needs it in
    // order to give it to new users.
    e.git(&["checkout", "-b", "test-willow-release"])?;
    fs::write(p.join("a"), "Aller Anfang ist schwer.  -- Schiller")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Someone is not quite correct on the internet.",
        &format!("-S{}", e.willow_release.fingerprint),
    ])?;

    e.git(&["push", "origin", "test-willow-release"])?;

    // Reset.
    e.git(&["checkout", "test-base"])?;
    e.git(&["clean", "-fdx"])?;

    // Buffy's cert was not yet added, so she may not sign commits.
    e.git(&["checkout", "-b", "test-buffy"])?;
    fs::write(p.join("a"),
              "Aller Anfang ist schwer, unless you are super strong!1")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "Well, actually...",
        &format!("-S{}", e.buffy.fingerprint),
    ])?;

    if let Ok(output) = e.git(&["push", "origin", "test-buffy"]) {
        eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
    }
    assert!(e.git(&["push", "origin", "test-buffy"]).is_err());

    Ok(())
}

#[test]
fn update_hook_external_policy() -> anyhow::Result<()> {
    // Check that the update hook can use an external policy.
    //
    // The repository does NOT contain a policy.

    let (e, _root, _policy_file) = create_environment(true, false, None)?;

    // Willow is allowed by the external policy.  We can push.
    e.git(&["checkout", "-b", "willow"])?;
    e.git_commit(&[("xxx", Some(b"2"))],
                 "commit-1",
                 Some(&e.willow_release))
        .expect("can commit");

    assert!(e.git(&["push", "origin", "willow"]).is_ok());

    // Xander is not allowed by the external policy.  We won't be able
    // to push.
    e.git(&["checkout", "-b", "xander"])?;
    e.git_commit(&[("xxx", Some(b"3"))],
                 "commit-2",
                 Some(&e.xander))
        .expect("can commit");

    assert!(e.git(&["push", "origin", "xander"]).is_err());

    Ok(())
}

#[test]
fn update_hook_ignore_internal_policy() -> anyhow::Result<()> {
    // Check that the update hook can use an external policy.
    //
    // The repository contains a policy, but it has different
    // permissions.

    let (e, _root, external_policy_file)
        = create_environment(true, true, Some("Riley Finn"))?;
    let external_policy_file = external_policy_file
        .expect("have external policy");

    // Change the external policy, which the update hook does NOT use.

    // Make riley the project manager.
    std::fs::remove_file(&external_policy_file).expect("Can remove file");
    e.sq_git(
        &[
            "policy",
            "authorize",
            "--policy-file", &external_policy_file.display().to_string(),
            e.riley.petname,
            &e.riley.fingerprint.to_string(),
            "--sign-commit",
            "--sign-tag",
            "--sign-archive",
            "--add-user",
            "--retire-user",
            "--audit",
        ])?;

    // Xander is not allowed by the external policy or the internal
    // policy.  We won't be able to push.
    e.git(&["checkout", "-b", "xander", "main"])?;
    e.git_commit(&[("xxx", Some(b"xander"))],
                 "xander commit",
                 Some(&e.xander))
        .expect("can commit");

    e.git_log_graph().expect("can display graph");

    assert!(e.git(&["push", "origin", "xander"]).is_err());

    // Willow is not allowed by the external policy, but she is
    // allowed by the internal one.  We shouldn't be able to push
    // since the update hook uses the external policy.
    e.git(&["checkout", "-b", "willow", "main"])?;
    e.git_commit(&[("xxx", Some(b"willow"))],
                 "willow commit",
                 Some(&e.willow_release))
        .expect("can commit");

    e.git_log_graph().expect("can display graph");

    assert!(e.git(&["push", "origin", "willow"]).is_err());

    // Riley is allowed by the external policy, and is not allowed by
    // the internal policy.  This should work.
    e.git(&["checkout", "-b", "riley", "main"])?;
    e.git_commit(&[("xxx", Some(b"riley"))],
                 "riley commit",
                 Some(&e.riley))
        .expect("can commit");

    e.git_log_graph().expect("can display graph");

    assert!(e.git(&["push", "origin", "riley"]).is_ok());

    Ok(())
}

#[test]
fn rebase() -> anyhow::Result<()> {
    let (e, _root, _policy_file) = create_environment(false, true, None)?;
    let p = e.git_state();

    // Bookmark.
    e.git(&["checkout", "-b", "test-base"])?;

    // There are two threads of development.  Let's start the first
    // one.
    e.git(&["checkout", "-b", "feature-one"])?;
    fs::write(p.join("a"), "Aller Anfang ist schwer.")?;
    e.git(&["add", "a"])?;
    e.git(&[
        "commit",
        "-m", "First change of the first feature.",
        &format!("-S{}", e.willow.fingerprint),
    ])?;

    e.git(&["push", "origin", "feature-one"])?;

    // Reset.
    e.git(&["checkout", "test-base"])?;
    e.git(&["clean", "-fdx"])?;

    // There are two threads of development.  Let's start the second
    // one.
    e.git(&["checkout", "-b", "feature-two"])?;
    fs::write(p.join("b"), "And now for something completely different.")?;
    e.git(&["add", "b"])?;
    e.git(&[
        "commit",
        "-m", "First change of the second feature.",
        &format!("-S{}", e.willow.fingerprint),
    ])?;

    e.git(&["push", "origin", "feature-two"])?;

    // Now we rebase feature-two on top of feature-one and push it to
    // update the remote feature-two branch.
    e.git(&[
        "rebase",
        "feature-one",
        &format!("-S{}", e.willow.fingerprint),
    ])?;
    e.git(&["push", "origin", "--force", "feature-two"])?;

    Ok(())
}
