The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
use warnings;
use strict;
use Test::More tests=>50;
use Test::Exception;
use Test::MockModule;
use File::Temp qw{tempdir};
use FindBin;

$ENV{PATH}="$FindBin::Bin/../blib/script:$ENV{PATH}";
require 'blib/script/narada-setup-cron';

sub Echo {
    my ($filename, $content) = @_;
    open my $fh, '>', $filename                         or die "open: $!";
    print {$fh} $content;
    close $fh                                           or die "close: $!";
    return;
}

# prepare sandbox
my $dst = tempdir(CLEANUP => 1);
system('blib/script/narada-new', $dst) == 0             
                                or die "system: can't create tmp proj: $?";
chdir $dst                      or die "chdir: $!"; 

# - main()
#   * too many params
#   * wrong params
#   * no params and config/crontab not readable: throw exception
#   * --clean: make sure del_cron() called
#   * no --clean and no config/crontab: make sure del_cron() called
#   * no --clean and exists config/crontab: make sure set_cron called

throws_ok { main('param-1', 'param-2') }    qr/Usage:/,
    'main: too many params';

throws_ok { main('not_existing_param') }    qr/Usage:/,
    'main: wrong param';

SKIP: {
    skip 'non-root user required', 1 if $< == 0;
    chmod 0, 'config/crontab'                               or die "chmod: $!";
    throws_ok { main() }                        qr/open\(config\/crontab\):/,
        'main: not readable';
    chmod 0644, 'config/crontab'                            or die "chmod: $!";
}

{ 
    my @log;
    my $is_log = sub { is("@log", "@{$_[0]}", $_[1]); @log=(); };
    my $m = new Test::MockModule('main');
    $m->mock('del_cron', sub { push @log, 'del_cron' });
    $m->mock('set_cron', sub { push @log, 'set_cron' });

    main('--clean');
    $is_log->(['del_cron'],
        'main: call del_cron() on --clean');

    rename 'config/crontab', 'config/crontab.orig'      or die "rename: $!";
    main();
    rename 'config/crontab.orig', 'config/crontab'      or die "rename: $!";
    $is_log->(['del_cron'],
        'main: call del_cron() on no config/crontab without --clean');

    main();
    $is_log->(['set_cron'],
        'main: call set_cron() on config/crontab without --clean');
}

# - get_project_dir()
#   * throw on \n inside directory name

Echo('pwd', "#!/bin/sh\ncat cwd");
chmod 0755, 'pwd'                                   or die "chmod: $!";

{
    local $ENV{PATH} = ".:$ENV{PATH}";

    Echo('cwd', "/a/b/c\n");
    is(get_project_dir(), '/a/b/c',
        'get_project_dir: ok');

    Echo('cwd', "/a/b\nb/c\n");
    throws_ok { get_project_dir() }     qr/must not contain \\n/,
        'get_project_dir: throw on \n inside directory name';
}

# - process()
#   * test multi line data, with several commands and comments

my $process = process(
    "# comment1\n"
  . "* * * * * echo ok\n"
  . "# comment2\n"
  . "10 6 * * * date\n"
  . "0 23-7/2,8 * * *   ( date >/tmp/date ) &"
);
my $cwd = quotemeta(get_project_dir());
$cwd =~ s{\\([/,._-])}{$1}xmsg; # unquote safe chars for readability
my $expect =
    "# comment1\n"
  . "* * * * * cd $cwd || exit; echo ok\n"
  . "# comment2\n"
  . "10 6 * * * cd $cwd || exit; date\n"
  . "0 23-7/2,8 * * *   cd $cwd || exit; ( date >/tmp/date ) &"
    ;
is($process, $expect,
    'process: ok');

# - get_markers()
#   . test regexp will match blocks composed from:
#     * start."\n".end."\n"
#     * start."\n".line1."\n".end."\n"
#     * start."\n".line1."\n".line2."\n".end."\n"
#     * start."\n".end
#   . test regexp will not match blocks composed from:
#     * junk.start."\n".end."\n"
#     * start.junk."\n".end."\n"
#     * start."\n".junk.end."\n"
#     * start."\n".end.junk."\n"

$cwd = get_project_dir();
my($re, $start, $end) = get_markers();

my $expected =
    "# ENTER Narada: $cwd"
  . "\n"
  . "# LEAVE Narada: $cwd"
  . "\n"
    ;
like($expected, $re, 'get_markers: start.\n.end.\n');

$expected =
    "# ENTER Narada: $cwd"
  . "\n"
  . "1 * * * * echo line1\n"
  . "\n"
  . "# LEAVE Narada: $cwd"
  . "\n"
    ;
like($expected, $re, 'get_markers: start.\n.line1.\n.end.\n');

$expected =
    "# ENTER Narada: $cwd"
  . "\n"
  . "1 * * * * echo line1\n"
  . "\n"
  . "1 * * * * echo line2\n"
  . "\n"
  . "# LEAVE Narada: $cwd"
  . "\n"
    ;
like($expected, $re, 'get_markers: start.\n.line1.\n.line2.\n.end.\n');

$expected =
    "# ENTER Narada: $cwd"
  . "\n"
  . "# LEAVE Narada: $cwd"
    ;
like($expected, $re, 'get_markers: start.\n.end');

$expected =
    "* 1 * * * echo ups;"
  . "# ENTER Narada: $cwd"
  . "\n"
  . "# LEAVE Narada: $cwd"
  . "\n"
    ;
unlike($expected, $re, 'get_markers: junk.start.\n.end.\n');

$expected =
    "# ENTER Narada: $cwd"
  . "# some unexpected user comment"
  . "\n"
  . "# LEAVE Narada: $cwd"
  . "\n"
    ;
unlike($expected, $re, 'get_markers: start.junk\n.end.\n');

$expected =
    "# ENTER Narada: $cwd"
  . "\n"
  . "* 1 * * * echo junk!"
  . "# LEAVE Narada: $cwd"
  . "\n"
    ;
unlike($expected, $re, 'get_markers: start.\n.junk.end.\n');

$expected =
    "# ENTER Narada: $cwd"
  . "\n"
  . "# LEAVE Narada: $cwd"
  . "* 1 * * * echo junk!"
  . "\n"
    ;
unlike($expected, $re, 'get_markers: start.\n.end.junk.\n');



# get_user_crontab(), set_user_crontab()
#   . Test MANUALLY!
# - force_last_CR()
#   . Will be tested while testing set_cron().
# - set_cron()
#   . project crontab variants:
#     * empty
#     * single line without last \n
#     * single line with last \n
#     * multi line without last \n
#     * multi line with last \n
#   . user crontab variants:
#     * empty
#     * has user data and another narada project block without last \n
#     * has user data and another narada project block with last \n
#   . operations:
#     * add
#     * update

{ 
    my $got;
    my $user_crontab;
    my $m = new Test::MockModule('main');
    $m->mock('get_user_crontab', sub { $user_crontab; });
    $m->mock('set_user_crontab', sub { $got = shift() });

    my $start = "# ENTER Narada: $cwd";
    my $end   = "# LEAVE Narada: $cwd";

    my $single_line = '* * 1 * * echo singleline';
    my $multi_line = 
        '# multiline crontab example'
      . "\n"
      . '* * 1 * * echo firstline;'
      . "\n"
      . '* * 1 * * echo secondline;'
        ;
    my $narada_project = 
        '* 1 * * * * echo user_data;' 
      . "\n"
      . '# ENTER Narada: /some/path/to/narada'
      . "\n"
      . '1 * * * * cd /some/path/to/narada || exit; echo Narada;'
      . "\n"
      . '# LEAVE Narada: /some/path/to/narada'
        ;
    my $my_narada_project = $start
              . "\n"
              . "1 * * * * echo current_narada_proj;"
              . "\n"
              . $end
              ;

    my %crontab = ('empty'                       => q{},
                   'single line without last \n' => $single_line,
                   'single line with last \n'    => $single_line."\n",
                   'multi line without last \n'  => $multi_line,
                   'multi line with last \n'     => $multi_line."\n",
                  );
    my %user_crontab = (
        'empty' => q{},
        'has user data and another narada project block without last \n'
                => $narada_project,
        'has user data and another narada project block with last \n'
                => $narada_project."\n",
                       );
   
    for my $action('add','edit'){
        for my $crontab (sort keys %crontab){
            for (sort keys %user_crontab){
                if ($action eq 'add'){
                    $user_crontab = $user_crontab{$_};
                    $expected = force_last_CR($user_crontab{$_})
                              . $start
                              . "\n"
                              . force_last_CR($crontab{$crontab})
                              . $end
                              . "\n"
                            ;
                }else{
                    $user_crontab = $my_narada_project."\n".$user_crontab{$_};
                    $expected = $start
                              . "\n"
                              . force_last_CR($crontab{$crontab})
                              . $end
                              . "\n"
                              . $user_crontab{$_}
                            ;
                }
                $got=q{};
                set_cron($crontab{$crontab});
                is ($got,
                    $expected,
                    "action: $action crontab: $crontab user crontab: $_"
                   );
            }
        }
    }
# - del_cron()
# #   . user crontab variants:
# #     * no block
# #     * empty block
# #     * block with data
    $user_crontab = $multi_line;
    del_cron();
    $expected = $multi_line;
    is($got, $expected, 'del_cron: no block');

    $user_crontab = $start."\n".$end."\n".$multi_line;
    del_cron();
    $expected = $multi_line;
    is($got, $expected, 'del_cron: empty block');

    $user_crontab = $single_line."\n".$start."\n".$multi_line."\n".$end."\n".$narada_project;
    del_cron();
    $expected = $single_line."\n".$narada_project;
    is($got, $expected, 'del_cron: block with data');
}

chdir '/';  # work around warnings in File::Temp CLEANUP handler