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

use strict;
use warnings;
use utf8;
use Test::More tests => 91;
#use Test::More 'no_plan';
use App::Sqitch;
use Locale::TextDomain qw(App-Sqitch);
use Path::Class;
use Test::Exception;
use Test::Dir;
use Test::File qw(file_not_exists_ok file_exists_ok);
use Test::File::Contents 0.05;
use File::Path qw(make_path remove_tree);
use Test::NoWarnings 0.083;
use lib 't/lib';
use MockOutput;

my $CLASS = 'App::Sqitch::Command::add';

ok my $sqitch = App::Sqitch->new(
    top_dir => Path::Class::Dir->new('sql'),
), 'Load a sqitch sqitch object';
my $config = $sqitch->config;

sub dep($$) {
    my $dep = App::Sqitch::Plan::Depend->new(
        %{ App::Sqitch::Plan::Depend->parse( $_[1] ) },
        plan      => $sqitch->plan,
        conflicts => $_[0],
    );
    $dep->project;
    return $dep;
}

isa_ok my $add = App::Sqitch::Command->load({
    sqitch  => $sqitch,
    command => 'add',
    config  => $config,
}), $CLASS, 'add command';

can_ok $CLASS, qw(
    options
    requires
    conflicts
    variables
    template_directory
    with_deploy
    with_revert
    with_verify
    deploy_template
    revert_template
    verify_template
    configure
    execute
    _find
    _slurp
    _add
);

is_deeply [$CLASS->options], [qw(
    requires|r=s@
    conflicts|c=s@
    note|n=s@
    set|s=s%
    template-directory=s
    deploy-template=s
    revert-template=s
    verify-template|test-template=s
    deploy!
    revert!
    verify|test!
)], 'Options should be set up';

sub contents_of ($) {
    my $file = shift;
    open my $fh, "<:encoding(UTF-8)", $file or die "cannot open $file: $!";
    local $/;
    return <$fh>;
}

##############################################################################
# Test configure().
is_deeply $CLASS->configure($config, {}), {
    requires  => [],
    conflicts => [],
    note      => [],
}, 'Should have default configuration with no config or opts';

is_deeply $CLASS->configure($config, {
    requires  => [qw(foo bar)],
    conflicts => ['baz'],
    note      => [qw(hellow there)],
}), {
    requires  => [qw(foo bar)],
    conflicts => ['baz'],
    note      => [qw(hellow there)],
}, 'Should have get requires and conflicts options';

is_deeply $CLASS->configure($config, { template_directory => 't' }), {
    requires  => [],
    conflicts => [],
    note      => [],
    template_directory => Path::Class::dir('t'),
}, 'Should set up template directory option';

throws_ok {
    $CLASS->configure($config, { template_directory => '__nonexistent__' });
} 'App::Sqitch::X', 'Should die if --template-directory does not exist';
is $@->ident, 'add', 'Missing directory ident should be "add"';
is $@->message, __x(
    'Directory "{dir}" does not exist',
    dir => '__nonexistent__',
), 'Missing directory error message should be correct';

throws_ok {
    $CLASS->configure($config, { template_directory => 'README.md' });
} 'App::Sqitch::X', 'Should die if --template-directory does is not a dir';
is $@->ident, 'add', 'In alid directory ident should be "add"';
is $@->message, __x(
    '"{dir}" is not a directory',
    dir => 'README.md',
), 'Invalid directory error message should be correct';

is_deeply $CLASS->configure($config, {
    deploy => 1,
    revert => 1,
    verify => 0,
    deploy_template => 'templates/deploy.tmpl',
    revert_template => 'templates/revert.tmpl',
    verify_template => 'templates/verify.tmpl',
}), {
    requires  => [],
    conflicts => [],
    note      => [],
    with_deploy => 1,
    with_revert => 1,
    with_verify => 0,
    deploy_template => Path::Class::file('templates/deploy.tmpl'),
    revert_template => Path::Class::file('templates/revert.tmpl'),
    verify_template => Path::Class::file('templates/verify.tmpl'),
}, 'Should have get template options';

# Test variable configuration.
CONFIG: {
    local $ENV{SQITCH_CONFIG} = File::Spec->catfile(qw(t add_change.conf));
    my $config = App::Sqitch::Config->new;
    my $dir = dir 't';
    is_deeply $CLASS->configure($config, {}), {
        template_directory => $dir,
        requires  => [],
        conflicts => [],
        note      => [],
    }, 'Variables should by default not be loaded from config';

    is_deeply $CLASS->configure($config, {set => { yo => 'dawg' }}), {
        template_directory => $dir,
        requires  => [],
        conflicts => [],
        note      => [],
        variables => {
            foo => 'bar',
            baz => [qw(hi there you)],
            yo  => 'dawg',
        },
    }, '--set should be merged with config variables';

    is_deeply $CLASS->configure($config, {set => { foo => 'ick' }}), {
        template_directory => $dir,
        requires  => [],
        conflicts => [],
        note      => [],
        variables => {
            foo => 'ick',
            baz => [qw(hi there you)],
        },
    }, '--set should be override config variables';
}

##############################################################################
# Test attributes.
is_deeply $add->requires, [], 'Requires should be an arrayref';
is_deeply $add->conflicts, [], 'Conflicts should be an arrayref';
is_deeply $add->note, [], 'Notes should be an arrayref';
is_deeply $add->variables, {}, 'Varibles should be a hashref';
is $add->template_directory, undef, 'Default dir should be undef';

MOCKCONFIG: {
    my $config_mock = Test::MockModule->new('App::Sqitch::Config');
    $config_mock->mock(system_dir => Path::Class::dir('nonexistent'));
    $config_mock->mock(user_dir => Path::Class::dir('nonexistent'));
    for my $script (qw(deploy revert verify)) {
        my $with = "with_$script";
        ok $add->$with, "$with should be true by default";
        my $tmpl = "$script\_template";
        throws_ok { $add->$tmpl } 'App::Sqitch::X', "Should die on $tmpl";
        is $@->ident, 'add', 'Should be an "add" exception';
        is $@->message, __x(
            'Cannot find {script} template',
            script => $script,
        ), "Should get $tmpl failure note";;
    }
}

# Point to a valid template directory.
ok $add = $CLASS->new(
    sqitch => $sqitch,
    template_directory => Path::Class::dir(qw(etc templates))
), 'Create add with template_directory';

for my $script (qw(deploy revert verify)) {
    my $tmpl = "$script\_template";
    is $add->$tmpl, Path::Class::file('etc', 'templates', "$script.tmpl"),
        "Should find $script in templates directory";
}

##############################################################################
# Test find().
is $add->_find('deploy'), Path::Class::file(qw(etc templates deploy.tmpl)),
    '_find should work with template_directory';

ok $add = $CLASS->new(sqitch => $sqitch),
    'Create add with no template directory';

MOCKCONFIG: {
    my $config_mock = Test::MockModule->new('App::Sqitch::Config');
    $config_mock->mock(system_dir => Path::Class::dir('nonexistent'));
    $config_mock->mock(user_dir => Path::Class::dir('etc'));
    is $add->_find('deploy'), Path::Class::file(qw(etc templates deploy.tmpl)),
        '_find should work with user_dir from Config';

    $config_mock->mock(user_dir => Path::Class::dir('nonexistent'));
    throws_ok { $add->_find('verify') } 'App::Sqitch::X',
        "Should die trying to find template";
    is $@->ident, 'add', 'Should be an "add" exception';
    is $@->message, __x(
        'Cannot find {script} template',
        script => 'verify',
    ), "Should get unfound verify template note";

    $config_mock->mock(system_dir => Path::Class::dir('etc'));
    is $add->_find('deploy'), Path::Class::file(qw(etc templates deploy.tmpl)),
        '_find should work with system_dir from Config';
}

##############################################################################
# Test _slurp().
my $tmpl = Path::Class::file(qw(etc templates deploy.tmpl));
is $ { $add->_slurp($tmpl)}, contents_of $tmpl,
    '_slurp() should load a reference to file contents';

##############################################################################
# Test _add().
make_path 'sql';
my $fn = $sqitch->plan_file;
open my $fh, '>', $fn or die "Cannot open $fn: $!";
say $fh "%project=add\n\n";
close $fh or die "Error closing $fn: $!";
END { remove_tree 'sql' };
my $out = file 'sql', 'sqitch_change_test.sql';
file_not_exists_ok $out;
ok $add->_add('sqitch_change_test', $out, $tmpl),
    'Write out a script';
file_exists_ok $out;
file_contents_is $out, <<EOF, 'The template should have been evaluated';
-- Deploy sqitch_change_test

BEGIN;

-- XXX Add DDLs here.

COMMIT;
EOF
is_deeply +MockOutput->get_info, [[__x 'Created {file}', file => $out ]],
    'Info should show $out created';

# Try with requires and conflicts.
ok $add =  $CLASS->new(
    sqitch    => $sqitch,
    requires  => [qw(foo bar)],
    conflicts => ['baz'],
), 'Create add cmd with requires and conflicts';

$out = file 'sql', 'another_change_test.sql';
ok $add->_add('another_change_test', $out, $tmpl),
    'Write out a script with requires and conflicts';
is_deeply +MockOutput->get_info, [[__x 'Created {file}', file => $out ]],
    'Info should show $out created';
file_contents_is $out, <<EOF, 'The template should have been evaluated with requires and conflicts';
-- Deploy another_change_test
-- requires: foo
-- requires: bar
-- conflicts: baz

BEGIN;

-- XXX Add DDLs here.

COMMIT;
EOF
unlink $out;

##############################################################################
# Test execute.
ok $add = $CLASS->new(
    sqitch => $sqitch,
    template_directory => Path::Class::dir(qw(etc templates))
), 'Create another add with template_directory';

# Override request_note().
my $change_mocker = Test::MockModule->new('App::Sqitch::Plan::Change');
my %request_params;
$change_mocker->mock(request_note => sub {
    shift;
    %request_params = @_;
});

my $deploy_file = file qw(sql deploy widgets_table.sql);
my $revert_file = file qw(sql revert widgets_table.sql);
my $verify_file = file qw(sql verify   widgets_table.sql);

my $plan = $sqitch->plan;
is $plan->get('widgets_table'), undef, 'Should not have "widgets_table" in plan';
dir_not_exists_ok +File::Spec->catdir('sql', $_) for qw(deploy revert verify);
ok $add->execute('widgets_table'), 'Add change "widgets_table"';
isa_ok my $change = $plan->get('widgets_table'), 'App::Sqitch::Plan::Change',
    'Added change';
is $change->name, 'widgets_table', 'Change name should be set';
is_deeply [$change->requires],  [], 'It should have no requires';
is_deeply [$change->conflicts], [], 'It should have no conflicts';
is_deeply \%request_params, {
    for => __ 'add',
    scripts => [$change->deploy_file, $change->revert_file, $change->verify_file],
}, 'It should have prompted for a note';

file_exists_ok $_ for ($deploy_file, $revert_file, $verify_file);
file_contents_like +File::Spec->catfile(qw(sql deploy widgets_table.sql)),
    qr/^-- Deploy widgets_table/, 'Deploy script should look right';
file_contents_like +File::Spec->catfile(qw(sql revert widgets_table.sql)),
    qr/^-- Revert widgets_table/, 'Revert script should look right';
file_contents_like +File::Spec->catfile(qw(sql verify widgets_table.sql)),
    qr/^-- Verify widgets_table/, 'Verify script should look right';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $deploy_file],
    [__x 'Created {file}', file => $revert_file],
    [__x 'Created {file}', file => $verify_file],
    [__x 'Added "{change}" to {file}',
        change => 'widgets_table',
        file   => $sqitch->plan_file,
    ],
], 'Info should have reported file creation';

# Relod the plan file to make sure change is written to it.
$plan->load;
isa_ok $change = $plan->get('widgets_table'), 'App::Sqitch::Plan::Change',
    'Added change in reloaded plan';

# Make sure conflicts are avoided and conflicts and requires are respected.
ok $add = $CLASS->new(
    sqitch             => $sqitch,
    requires           => ['widgets_table'],
    conflicts          => [qw(dr_evil joker)],
    note               => [qw(hello there)],
    with_verify        => 0,
    template_directory => Path::Class::dir(qw(etc templates))
), 'Create another add with template_directory and no verify script';

$deploy_file = file qw(sql deploy foo_table.sql);
$revert_file = file qw(sql revert foo_table.sql);
$verify_file = file qw(sql ferify foo_table.sql);
$deploy_file->touch;

file_exists_ok $deploy_file;
file_not_exists_ok $_ for ($revert_file, $verify_file);
is $plan->get('foo_table'), undef, 'Should not have "foo_table" in plan';
ok $add->execute('foo_table'), 'Add change "foo_table"';
file_exists_ok $_ for ($deploy_file, $revert_file);
file_not_exists_ok $verify_file;
isa_ok $change = $plan->get('foo_table'), 'App::Sqitch::Plan::Change',
    '"foo_table" change';
is_deeply \%request_params, {
    for => __ 'add',
    scripts => [$change->deploy_file, $change->revert_file],
}, 'It should have prompted for a note';

is $change->name, 'foo_table', 'Change name should be set to "foo_table"';
is_deeply [$change->requires],  [dep 0, 'widgets_table'], 'It should have requires';
is_deeply [$change->conflicts], [map { dep 1, $_ } qw(dr_evil joker)], 'It should have conflicts';
is        $change->note, "hello\n\nthere", 'It should have a comment';

is_deeply +MockOutput->get_info, [
    [__x 'Skipped {file}: already exists', file => $deploy_file],
    [__x 'Created {file}', file => $revert_file],
    [__x 'Added "{change}" to {file}',
        change => 'foo_table [widgets_table !dr_evil !joker]',
        file   => $sqitch->plan_file,
    ],
], 'Info should report skipping file and include dependencies';