The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl

use 5.010;
use strict;
use warnings;
use Test::More 0.96;

use File::chdir;
use File::Slurp::Tiny qw(read_file write_file);
use File::Temp qw(tempdir);
use File::Which;
use Git::Bunch qw(check_bunch sync_bunch);
use IPC::System::Locale 'system', 'backtick', -log=>1;
use Probe::Perl;
use String::ShellQuote;

# XXX sync + --create_bare / --nocreate-bare
# XXX --exclude_repos_pat
# XXX --include_repos_pat
# XXX exec

for (qw(git rsync rm)) {
    plan skip_all => "$_ not available in PATH" unless which($_);
}
# due to shell quoting etc
my $pp = Probe::Perl->new;
plan skip_all => 'currently only test on Unix'
    unless $pp->os_type eq 'Unix';

my $rootdir = tempdir(CLEANUP=>1);
$CWD = $rootdir;
create_test_data($rootdir);

test_gb(
    sub     => "check_bunch",
    name    => "all repos in bunch are clean",
    args    => {source=>"src/bunch1"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        #diag explain $res;
        is( $res->[2]{repo1}[0], 200, "repo1 is clean");
        is( $res->[2]{repo2}[0], 200, "repo2 is clean");
        ok(!$res->[2]{"file1"}, "file is skipped");
        ok(!$res->[2]{".nonrepo1"}, "dotdir is skipped");
    },
);
test_gb(
    sub     => "check_bunch",
    name    => "handling / suffix in bunch name",
    args    => {source=>"src/bunch1/"},
    status  => 200,
);
test_gb(
    sub     => "check_bunch",
    name    => "bunch doesn't exist",
    args    => {source=>"src/bunch1x"},
    status  => 404,
);
test_gb(
    sub     => "check_bunch",
    name    => "using repo instead of bunch will be rejected",
    args    => {source=>"src/bunch1/repo1"},
    status  => 400,
);

mkdir "src/bunch1/nonrepo2";
test_gb(
    sub     => "check_bunch",
    name    => "skip nondot, nongit dir",
    args    => {source=>"src/bunch1"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        ok(!$res->[2]{nonrepo2}, "nonrepo2 is skipped");
    },
);
rmdir "src/bunch1/nonrepo2";

write_file("src/bunch1/repo1/d/b", "");
test_gb(
    sub     => "check_bunch",
    name    => "needs commit",
    args    => {source=>"src/bunch1"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        is  ($res->[2]{repo1}[0], 500, "repo1 is unclean (status)");
        like($res->[2]{repo1}[1], qr/needs commit/i,
             "repo1 is unclean (message)");
    },
);
system "cd src/bunch1/repo1 && git commit -am 'commit2-repo1'";
test_gb(
    sub     => "check_bunch",
    name    => "needs commit (committed)",
    args    => {source=>"src/bunch1"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        is( $res->[2]{repo1}[0], 200, "repo1 is clean again");
    },
);

write_file("src/bunch1/repo1/c", "cherry");
test_gb(
    sub     => "check_bunch",
    name    => "has untracked files",
    args    => {source=>"src/bunch1"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        is  ($res->[2]{repo1}[0], 500, "repo1 is unclean");
        like($res->[2]{repo1}[1], qr/has untracked files/i,
             "repo1 is unclean (message)");
    },
);
unlink "src/bunch1/repo1/c";
test_gb(
    sub     => "check_bunch",
    name    => "has untracked files (deleted)",
    args    => {source=>"src/bunch1"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        is( $res->[2]{repo1}[0], 200, "repo1 is clean again");
    },
);

#subtest "can't chdir to repo -> dies" => sub {
#    plan skip_all => "skipping test because user can access src/bunch1 "
#        if -x "src/bunch1";
#    test_gb(
#        sub     => "check_bunch",
#        name    => "can't chdir to repo -> dies",
#        args    => {source=>"src/bunch1"},
#        dies    => 1,
#    );
#    rmdir "src/bunch1/repo3";
#};

delete_test_data("bak") if Test::More->builder->is_passing;

test_gb(
    sub     => "sync_bunch",
    name    => "source bunch doesn't exist",
    args    => {source=>"src/bunch1x", target=>"sync"},
    status  => 404,
);
test_gb(
    sub     => "sync_bunch",
    name    => "using repo instead of bunch in source will be rejected",
    args    => {source=>"src/bunch1/repo1", target=>"sync"},
    status  => 400,
);
test_gb(
    sub     => "sync_bunch",
    name    => "using repo instead of bunch in target will be rejected",
    args    => {source=>"src/bunch1", target=>"src/bunch1/repo1"},
    status  => 400,
);
test_gb(
    sub     => "sync_bunch",
    name    => "main test", # also test handling / suffix in src & target
    args    => {source=>"src/bunch1/", target=>"sync/1/"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        ok((-d "sync/1"), "target directory created") or return;
        is(read_file("sync/1/file1", chomp=>1), "foo", "files copied");
        ok((-d "sync/1/.nonrepo1"), "nongit dotdir copied (exists)");
        is(read_file("sync/1/.nonrepo1/t", chomp=>1), "tea",
           "nongit dotdir copied (content)");
        for my $repo (qw(repo1 repo2)) {
            ok( (-d "sync/1/$repo"), "repo $repo copied (exists)");
            ok( (-d "sync/1/$repo/.git"),
                "repo $repo copied (.git exists)");
            is( read_file("sync/1/$repo/a", chomp=>1), "apple",
                "repo $repo copied (working copy copied)");
            like(~~backtick("cd sync/1/$repo && git log"), qr/commit1-$repo/i,
                 "repo $repo copied (git log works)");
        }
    },
);

# different length or rsync by default ignores it
write_file "src/bunch1/file1", "foobar";
write_file "src/bunch1/.nonrepo1/t", "tangerine";
# delete
unlink     "src/bunch1/repo1/a";
system  "cd src/bunch1/repo1 && git commit -am 'commit3-repo1'";
# add
write_file "src/bunch1/repo1/e", "eggplant";
system  "cd src/bunch1/repo1 && git add e && git commit -am 'commit4-repo1'";
# update
write_file "src/bunch1/repo1/d/b", "blackberry";
system  "cd src/bunch1/repo1 && git commit -am 'commit5-repo1'";
# rename
system  "cd src/bunch1/repo1 && git mv k d/ && git commit -am 'commit6-repo1'";

test_gb(
    sub     => "sync_bunch",
    name    => "update",
    args    => {source=>"src/bunch1/", target=>"sync/1/"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        is(read_file("sync/1/file1", chomp=>1), "foobar", "files updated");
        is(read_file("sync/1/.nonrepo1/t", chomp=>1), "tangerine",
           "nongit dotdir updated");
        ok(!(-e "sync/1/repo1/a"), "repo1: a deleted");
        is(read_file("sync/1/repo1/e", chomp=>1), "eggplant",
           "repo1: e added");
        is(read_file("sync/1/repo1/d/b", chomp=>1), "blackberry",
           "repo1: b updated");
        ok(!(-e "sync/1/repo1/k"), "repo1: k moved (1)");
        is(read_file("sync/1/repo1/d/k", chomp=>1), "kangkung",
           "repo1: k moved (2)");
        like(~~backtick("cd sync/1/repo1 && git log"),
             qr/commit6.+commit5.+commit4.+commit3/s,
             "repo1: commits sync-ed");
        my %status = (
            "file1"      => 200,
            ".nonrepo1"  => 200,
            "repo1"      => 200,
            "repo2"      => 304,
        );
        is($res->[2]{$_}[0], $status{$_}, "status of $_") for keys %status;
    },
);

write_file "src/bunch1/repo2/s1", "strawberry";
system  "cd src/bunch1/repo2 && git branch b2";
system  "cd src/bunch1/repo2 && git add s1 && ".
    "git commit -am 'commit3-master-repo2'";
system  "cd src/bunch1/repo2 && git checkout b2";
write_file "src/bunch1/repo2/s2", "spearmint";
system  "cd src/bunch1/repo2 && git add s2 && ".
    "git commit -am 'commit4-b2-repo2'";

test_gb(
    sub     => "sync_bunch",
    name    => "multiple branches",
    args    => {source=>"src/bunch1/", target=>"sync/1/"},
    status  => 200,
    test_res => sub {
        my ($res) = @_;
        system "cd sync/1/repo2 && git checkout master";
        is(read_file("sync/1/repo2/s1", chomp=>1),
           "strawberry", "branch master: s1 added");
        ok(!(-e "sync/1/repo2/s2"), "branch master: s2 not added");

        system "cd sync/1/repo2 && git checkout b2";
        is(read_file("sync/1/repo2/s2", chomp=>1),
           "spearmint", "branch b2: s2 added");
        ok(!(-e "sync/1/repo2/s1"), "branch b2: s2 not added");
    },
);

TODO: {
    local $TODO = "todo";
    fail("arg: delete_branch");
    fail("arg: repos (skips nonrepo as well as repo)");
    fail("sync tags");
}

# TODO: test options exclude_files=>1, exclude_non_git_dirs=>1 on sync

delete_test_data("sync") if Test::More->builder->is_passing;

DONE_TESTING:
done_testing();
if (Test::More->builder->is_passing) {
    diag "all tests successful, deleting test data dir";
    $CWD = "/";
} else {
    # don't delete test data dir if there are errors
    diag "there are failing tests, not deleting test data dir $rootdir";
}

sub test_gb {
    my (%args) = @_;
    my $sub = $args{sub};
    my $sub_args = $args{args};

    subtest "$sub: $args{name}" => sub {

        my $res;
        my $eval_err;
        if ($sub =~ /^(check|sync)_bunch$/) {
            no strict 'refs';
            eval { $res = $sub->(%$sub_args) }; $eval_err = $@;
        } else {
            die "Unknown sub to test: $sub";
        }

        if ($args{dies}) {
            ok($eval_err, "dies");
        }
        if ($args{status}) {
            is($res->[0], $args{status}, "status $args{status}")
                or diag explain($res);
        }
        if ($args{test_res}) {
            $args{test_res}->($res);
        }

    };
}

sub create_test_data {
    die unless $rootdir;
    local $CWD = $rootdir;
    use autodie;

    mkdir      "src";
    mkdir      "src/bunch1";
    mkdir      "src/bunch1/.nonrepo1";
    write_file "src/bunch1/.nonrepo1/t", "tea";
    write_file "src/bunch1/file1", "foo";

    mkdir      "src/bunch1/repo1";
    write_file "src/bunch1/repo1/a", "apple";
    mkdir      "src/bunch1/repo1/d";
    write_file "src/bunch1/repo1/d/b", "banana";
    write_file "src/bunch1/repo1/k", "kangkung";
    $CWD     = "src/bunch1/repo1";
    system     "git init";
    # doesn't matter, what's needed is config --global?
    #system     'git config user.name nobody';
    #system     'git config user.email nobody@example.org';
    system     "git add .";
    system     "git commit -am 'commit1-repo1'";
    $CWD     = "../../..";

    mkdir      "src/bunch1/repo2";
    write_file "src/bunch1/repo2/a", "avocado";
    mkdir      "src/bunch1/repo2/d";
    write_file "src/bunch1/repo2/d/b", "blueberry";
    $CWD     = "src/bunch1/repo2";
    system     "git init";
    system     "git add .";
    system     "git commit -am 'commit1-repo2'";
    write_file   "a", "apple";
    system     "git commit -am 'commit2-repo2'";
    $CWD     = "../../..";
}

sub delete_test_data {
    die unless $rootdir;
    my @dirs = @_ ? @_ : ("src", "sync", "bak");
    system "rm -rf ".join(" ", map {shell_quote("$rootdir/$_")} @dirs);
}