use strict;
use warnings;
use Test::More;
use Test::Git;
use Git::Repository;
use File::Temp qw( tempfile );
use constant MSWin32 => $^O eq 'MSWin32';

has_git('1.5.0.rc1');

# clean up the environment
delete @ENV{qw( GIT_DIR GIT_WORK_TREE )};
$ENV{LC_ALL}              = 'C';
$ENV{GIT_AUTHOR_NAME}     = 'Test Author';
$ENV{GIT_AUTHOR_EMAIL}    = 'test.author@example.com';
$ENV{GIT_COMMITTER_NAME}  = 'Test Committer';
$ENV{GIT_COMMITTER_EMAIL} = 'test.committer@example.com';

# a place to put a git repository
my $r;

# a fake git binary used for setting the exit status
my $exit;
eval {
    my $version = Git::Repository->version;
    ( my $fh, $exit ) = tempfile(
        DIR    => 't',
        UNLINK => 1,
      ( SUFFIX => '.bat' )x!! MSWin32,
    );
    print {$fh} MSWin32 ? << "WIN32" : << "UNIX";
\@$^X -e "shift =~ /version/ ? print qq{git version $version\\n} : exit shift" -- %1 %2
WIN32
#!$^X
shift =~ /version/ ? print "git version $version\\n"
                   : exit shift;
UNIX
    close $fh or diag "close $exit failed: $!";
    chmod 0755, $exit or diag "chmod $exit failed: $!";
};

# make sure the binary is available
if ( !-x $exit ) {
    diag "Skipping 'git exit' tests: $exit is not "
        . ( -e _ ? 'executable' : 'available' );
    $exit = '';
}

# capture all warnings
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, shift };

my @tests = (

    # empty repository
    {   test_repo => [],
        cmd       => [qw( log -1 )],
        exit      => 128,
        dollar_at => qr/^fatal: bad default revision 'HEAD' /,
    },

    # create the empty tree
    {   cmd  => [ mktree => { input => '' } ],
        exit => 0,
    },

    # create a dummy commit
    {   cmd  => [ 'commit-tree', undef, { input => "empty tree" } ],
        exit => 0,
    },

    # update master
    {   cmd  => [ 'update-ref' => 'refs/heads/master', undef ],
        exit => 0,
    },

    # failing git rm
    {   cmd  => [ rm => 'does-not-exist' ],
        exit => 128,
        dollar_at =>
            qr/^fatal: pathspec 'does-not-exist' did not match any files /,
    },

    # failing git checkout
    {   cmd      => [ checkout => 'does-not-exist' ],
        exit     => 1,
        warnings => [
            qr/^error: pathspec 'does-not-exist' did not match any file\(s\) known to git\./,
        ],
    },

    # failing git checkout (quiet)
    {   cmd  => [ checkout => 'does-not-exist', { quiet => 1 } ],
        exit => 1,
    },

    # usage messages make run() die too
    {   cmd  => [ branch => '--does-not-exist' ],
        exit => '129',
        dollar_at => Git::Repository->version_lt('1.5.4.rc0')
          ? qr/^usage: git-branch /
          : qr/^error: unknown option `does-not-exist'/
    },

    # test fatal
    {   cmd  => [ checkout => 'does-not-exist', { fatal => [1] } ],
        exit => 1,
        dollar_at =>
            qr/^error: pathspec 'does-not-exist' did not match any file\(s\) known to git\./,
    },
    {   cmd  => [ checkout => 'does-not-exist', { fatal => 1 } ],
        exit => 1,
        dollar_at =>
            qr/^error: pathspec 'does-not-exist' did not match any file\(s\) known to git\./,
    },
    {   cmd      => [ rm => 'does-not-exist', { fatal => -128 } ],
        exit     => 128,
        warnings => [
            qr/^fatal: pathspec 'does-not-exist' did not match any files /,
        ],
    },
    {   cmd  => [ rm => 'does-not-exist', { fatal => -128, quiet => 1 } ],
        exit => 128,
    },
);

# tests that depend on $exit
push @tests, (

    # test some fatal combinations
    {   cmd  => [ exit => 123, { git => $exit } ],
        exit => 123,
    },
    {   cmd  => [ exit => 124, { git => $exit, fatal => [ 1 .. 255 ] } ],
        exit => 124,
        dollar_at => qr/^fatal: unknown git error/,
    },

    # setup a repo with some 'fatal' options
    # and override them in the call to run()
    {   test_repo => [ git    => { fatal      => [ 1 .. 255 ] } ],
        cmd       => [ exit => 125, { git => $exit } ],
        exit      => 125,
        dollar_at => qr/^fatal: unknown git error/,
    },
    {   cmd  => [ exit => 126, { git => $exit, fatal => [ -130 .. -120 ] } ],
        exit => 126,
    },

)x!! $exit;

# test case where EVERY exit status is fatal
push @tests, (

    # FATALITY
    {   test_repo => [ git => { fatal => [ 0 .. 255 ] } ],
        cmd       => ['version'],
        exit      => 0,
        dollar_at => qr/^fatal: unknown git error/,
    },
    {
        cmd  => [ version => { fatal => '-0' } ],
        exit => 0,
    },
);

# more tests that depend on $exit
push @tests, (

    # "!0" is a shortcut for 1..255
    {   test_repo => [],
        cmd       => [ exit => 140, { git => $exit, fatal => '!0' } ],
        exit      => 140,
        dollar_at => qr/^fatal: unknown git error/,
    },
    {   test_repo => [ git => { fatal => '!0' } ],
        cmd       => [ exit => 141, { git => $exit } ],
        exit      => 141,
        dollar_at => qr/^fatal: unknown git error/,
    },
    {   cmd  => [ exit => 142, { git => $exit, fatal => [ -150 .. -130 ] } ],
        exit => 142,
    },

)x!! $exit;

# count the warnings we'll check
@warnings = map @{ $_->{warnings} ||= [] }, @tests;

plan tests => 3 * @tests + @warnings;

my $output = '';
for my $t (@tests) {
    @warnings = ();

    # create a new test repository if needed
    $r = test_repository( @{ $t->{test_repo} } )
        if $t->{test_repo};

    # check if the command threw errors
    my @cmd = map { (defined) ? $_ : $output } @{ $t->{cmd} };
    my $cmd = join ' ', grep !ref, @cmd;
    $output = eval { $r->run(@cmd); };
    $t->{dollar_at}
        ? like( $@, $t->{dollar_at}, "$cmd: died" )
        : is( $@, '', "$cmd: ran ok" );
    is( $? >> 8, $t->{exit}, "$cmd: exit status $t->{exit}" );

    # check warnings
    is( @warnings, @{ $t->{warnings} }, "warnings: " . @{ $t->{warnings} } );
    for my $warning ( @{ $t->{warnings} } ) {
        like( shift @warnings, $warning, '... expected warning' );
    }
    diag $_ for @warnings;
}
