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 5.010;
use utf8;
use Test::More tests => 159;
#use Test::More 'no_plan';
use App::Sqitch;
use Locale::TextDomain qw(App-Sqitch);
use Path::Class;
use Test::Dir;
use Test::File qw(file_not_exists_ok file_exists_ok);
use Test::Exception;
use Test::File::Contents;
use Test::NoWarnings;
use File::Path qw(remove_tree make_path);
use URI;
use lib 't/lib';
use MockOutput;

my $exe_ext = $^O eq 'MSWin32' ? '.exe' : '';

my $CLASS;

BEGIN {
    $CLASS = 'App::Sqitch::Command::init';
    use_ok $CLASS or die;
}

isa_ok $CLASS, 'App::Sqitch::Command', $CLASS;
chdir 't';

sub read_config($) {
    my $conf = App::Sqitch::Config->new;
    $conf->load_file(shift);
    $conf->data;
}

$ENV{SQITCH_CONFIG}        = 'nonexistent.conf';
$ENV{SQITCH_USER_CONFIG}   = 'nonexistent.user';
$ENV{SQITCH_SYSTEM_CONFIG} = 'nonexistent.sys';

##############################################################################
# Test options and configuration.
my $sqitch = App::Sqitch->new(
    options => { top_dir => dir('init.mkdir')->stringify },
);
isa_ok my $init = $CLASS->new( sqitch => $sqitch ), $CLASS, 'New init object';

can_ok $init, qw(uri options configure);
is_deeply [$init->options], [qw(
    uri=s
)], 'Options should be correct';

is_deeply $CLASS->configure({}, {}), {}, 'Default config should be empty';
is_deeply $CLASS->configure({}, { uri => 'http://example.com' }),
    { uri => URI->new('http://example.com') },
    'Should accept a URI in options';
isa_ok $CLASS->configure({}, { uri => 'http://example.com' })->{uri}, 'URI',
    'processed uri option';
isa_ok my $target = $init->default_target, 'App::Sqitch::Target', 'default target';

##############################################################################
# Test make_directories.
can_ok $init, 'make_directories';
for my $attr (map { "$_\_dir"} qw(top deploy revert verify)) {
    dir_not_exists_ok $target->$attr;
}

my $top_dir_string = $target->top_dir->stringify;
END { remove_tree $top_dir_string }

ok $init->make_directories, 'Make the directories';
for my $attr (map { "$_\_dir"} qw(top deploy revert verify)) {
    dir_exists_ok $target->$attr;
}
my $sep = dir('')->stringify;
is_deeply +MockOutput->get_info, [
    map { [__x "Created {file}", file => $target->$_ . $sep] }
    map { "$_\_dir" } qw(deploy revert verify)
], 'Each should have been sent to info';

# Do it again.
ok $init->make_directories, 'Make the directories again';
is_deeply +MockOutput->get_info, [], 'Nothing should have been sent to info';

# Delete one of them.
remove_tree $target->revert_dir->stringify;
ok $init->make_directories, 'Make the directories once more';
dir_exists_ok $target->revert_dir, 'revert dir exists again';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $target->revert_dir . $sep],
], 'Should have noted creation of revert dir';

remove_tree $top_dir_string;

# Handle errors.
FSERR: {
    # Make mkpath to insert an error.
    my $mock = Test::MockModule->new('File::Path');
    $mock->mock( mkpath => sub {
        my ($file, $p) = @_;
        ${ $p->{error} } = [{ $file => 'Permission denied yo'}];
        return;
    });

    throws_ok { $init->make_directories } 'App::Sqitch::X',
        'Should fail on permission issue';
    is $@->ident, 'init', 'Permission error should have ident "init"';
    is $@->message, __x(
        'Error creating {path}: {error}',
        path  => $target->deploy_dir,
        error => 'Permission denied yo',
    ), 'The permission error should be formatted properly';
}

##############################################################################
# Test write_config().
can_ok $init, 'write_config';

my $write_dir = 'init.write';
make_path $write_dir;
END { remove_tree $write_dir }
chdir $write_dir;
END { chdir File::Spec->updir }
my $conf_file = $sqitch->config->local_file;

my $uri = URI->new('https://github.com/theory/sqitch/');

$sqitch = App::Sqitch->new;
ok $init = $CLASS->new(
    sqitch  => $sqitch,
), 'Another init object';
file_not_exists_ok $conf_file;
$target = $init->default_target;

# Write empty config.
ok $init->write_config, 'Write the config';
file_exists_ok $conf_file;
is_deeply read_config $conf_file, {
}, 'The configuration file should have no variables';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $conf_file]
], 'The creation should be sent to info';
my $top_dir    = File::Spec->curdir;
my $deploy_dir = File::Spec->catdir(qw(deploy));
my $revert_dir = File::Spec->catdir(qw(revert));
my $verify_dir   = File::Spec->catdir(qw(verify));
my $plan_file  = $target->top_dir->file('sqitch.plan')->cleanup->stringify;
file_contents_like $conf_file, qr{\Q[core]
	# engine = 
	# plan_file = $plan_file
	# top_dir = $top_dir
	# deploy_dir = $deploy_dir
	# revert_dir = $revert_dir
	# verify_dir = $verify_dir
	# extension = sql
}m, 'All in core section should be commented-out';
unlink $conf_file;

# Set two options.
$sqitch = App::Sqitch->new(options => { extension => 'foo' });
ok $init = $CLASS->new( sqitch => $sqitch ), 'Another init object';
$target = $init->default_target;
ok $init->write_config, 'Write the config';
file_exists_ok $conf_file;
is_deeply read_config $conf_file, {
    'core.extension' => 'foo',
}, 'The configuration should have been written with the one setting';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $conf_file]
], 'The creation should be sent to info';

file_contents_like $conf_file, qr{
	# engine = 
	# plan_file = $plan_file
	# top_dir = $top_dir
	# deploy_dir = $deploy_dir
	# revert_dir = $revert_dir
	# verify_dir = $verify_dir
}m, 'Other settings should be commented-out';

# Go again.
ok $init->write_config, 'Write the config again';
is_deeply read_config $conf_file, {
    'core.extension' => 'foo',
}, 'The configuration should be unchanged';
is_deeply +MockOutput->get_info, [
], 'Nothing should have been sent to info';

USERCONF: {
    # Delete the file and write with a user config loaded.
    unlink $conf_file;
    local $ENV{SQITCH_USER_CONFIG} = file +File::Spec->updir, 'user.conf';
    my $sqitch = App::Sqitch->new(options => { extension => 'foo' });
    ok my $init = $CLASS->new( sqitch => $sqitch),
        'Make an init object with user config';
    file_not_exists_ok $conf_file;
    ok $init->write_config, 'Write the config with a user conf';
    file_exists_ok $conf_file;
    is_deeply read_config $conf_file, {
        'core.extension' => 'foo',
    }, 'The configuration should just have core.top_dir';
    is_deeply +MockOutput->get_info, [
        [__x 'Created {file}', file => $conf_file]
    ], 'The creation should be sent to info again';
    file_contents_like $conf_file, qr{\Q
	# engine = 
	# plan_file = $plan_file
	# top_dir = $top_dir
	# deploy_dir = $deploy_dir
	# revert_dir = $revert_dir
	# verify_dir = $verify_dir
}m, 'Other settings should be commented-out';
}

SYSTEMCONF: {
    # Delete the file and write with a system config loaded.
    unlink $conf_file;
    local $ENV{SQITCH_SYSTEM_CONFIG} = file +File::Spec->updir, 'sqitch.conf';
    my $sqitch = App::Sqitch->new(options => { extension => 'foo' });
    ok my $init = $CLASS->new( sqitch => $sqitch),
        'Make an init object with system config';
    ok $target = $init->default_target, 'Get target';
    file_not_exists_ok $conf_file;
    ok $init->write_config, 'Write the config with a system conf';
    file_exists_ok $conf_file;
    is_deeply read_config $conf_file, {
        'core.extension' => 'foo',
        'core.engine' => 'pg',
    }, 'The configuration should have local and system config' or diag $conf_file->slurp;
    is_deeply +MockOutput->get_info, [
        [__x 'Created {file}', file => $conf_file]
    ], 'The creation should be sent to info again';

    my $deploy_dir = File::Spec->catdir(qw(migrations deploy));
    my $revert_dir = File::Spec->catdir(qw(migrations revert));
    my $verify_dir = File::Spec->catdir(qw(migrations verify));
    my $plan_file  = $target->top_dir->file('sqitch.plan')->stringify;

    file_contents_like $conf_file, qr{\Q
	# plan_file = $plan_file
	# top_dir = migrations
	# deploy_dir = $deploy_dir
	# revert_dir = $revert_dir
	# verify_dir = $verify_dir
}m, 'Other settings should be commented-out';
}

##############################################################################
# Now get it to write a bunch of other stuff.
unlink $conf_file;
$sqitch = App::Sqitch->new(
    options => {
        plan_file  => 'my.plan',
        deploy_dir => dir('dep')->stringify,
        revert_dir => dir('rev')->stringify,
        verify_dir => dir('tst')->stringify,
        extension  => 'ddl',
        engine    => 'sqlite',
    },
);

ok $init = $CLASS->new( sqitch  => $sqitch ),
    'Create new init with sqitch non-default attributes';
ok $init->write_config, 'Write the config with core attrs';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $conf_file]
], 'The creation should be sent to info once more';

is_deeply read_config $conf_file, {
    'core.plan_file'  => 'my.plan',
    'core.deploy_dir' => 'dep',
    'core.revert_dir' => 'rev',
    'core.verify_dir' => 'tst',
    'core.extension'  => 'ddl',
    'core.engine'     => 'sqlite',
}, 'The configuration should have been written with all the core values';

##############################################################################
# Now get it to write core.sqlite stuff.
unlink $conf_file;
$sqitch = App::Sqitch->new(
    options => {
        engine => 'sqlite',
        client => '/to/sqlite3',
    },
);

ok $init = $CLASS->new( sqitch => $sqitch ),
    'Create new init with sqitch with non-default engine attributes';
ok $init->write_config, 'Write the config with engine attrs';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $conf_file]
], 'The creation should be sent to info yet again';

is_deeply read_config $conf_file, {
    'core.engine'         => 'sqlite',
    'engine.sqlite.client'  => '/to/sqlite3',
}, 'The configuration should have been written with sqlite values';

file_contents_like $conf_file, qr{^\tclient = /to/sqlite3\n}m,
    'Client should be included';
file_contents_like $conf_file, qr/^\t# registry = sqitch\n/m,
    'registry_uri should be included in a comment';

# Try it with no options.
unlink $conf_file;
$sqitch = App::Sqitch->new(options => { engine => 'sqlite' });
ok $init = $CLASS->new( sqitch => $sqitch ),
    'Create new init with sqitch with default engine attributes';
ok $init->write_config, 'Write the config with engine attrs';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $conf_file]
], 'The creation should be sent to info again again';
is_deeply read_config $conf_file, {
    'core.engine' => 'sqlite',
}, 'The configuration should have been written with only the engine var';

file_contents_like $conf_file, qr{^\Q# [engine "sqlite"]
	# target = db:sqlite:
	# registry = sqitch
	# client = sqlite3$exe_ext
}m, 'Engine section should be present but commented-out';

# Now build it with other config.
USERCONF: {
    # Delete the file and write with a user config loaded.
    unlink $conf_file;
    local $ENV{SQITCH_USER_CONFIG} = file +File::Spec->updir, 'user.conf';
    my $sqitch = App::Sqitch->new(options => { engine => 'sqlite' });
    ok my $init = $CLASS->new( sqitch => $sqitch ),
        'Make an init with sqlite and user config';
    file_not_exists_ok $conf_file;
    ok $init->write_config, 'Write the config with sqlite config';
    is_deeply +MockOutput->get_info, [
        [__x 'Created {file}', file => $conf_file]
    ], 'The creation should be sent to info once more';

    is_deeply read_config $conf_file, {
        'core.engine'         => 'sqlite',
    }, 'New config should have been written with sqlite values';

    file_contents_like $conf_file, qr{^\t\Q# client = /opt/local/bin/sqlite3\E\n}m,
        'Configured client should be included in a comment';
    file_contents_like $conf_file, qr/^\t# target = db:sqlite:my\.db\n/m,
        'Configured target should be included in a comment';
    file_contents_like $conf_file, qr/^\t# registry = meta\n/m,
        'Configured registry should be included in a comment';
}

##############################################################################
# Now get it to write engine.pg stuff.
unlink $conf_file;
$sqitch = App::Sqitch->new(
    options => {
        engine     => 'pg',
        client      => '/to/psql',
    },
);

ok $init = $CLASS->new( sqitch => $sqitch ),
    'Create new init with sqitch with more non-default engine attributes';
ok $init->write_config, 'Write the config with more engine attrs';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $conf_file]
], 'The creation should be sent to info one more time';

is_deeply read_config $conf_file, {
    'core.engine'    => 'pg',
    'engine.pg.client' => '/to/psql',
}, 'The configuration should have been written with client values' or diag $conf_file->slurp;

file_contents_like $conf_file, qr/^\t# registry = sqitch\n/m,
    'registry should be included in a comment';

# Try it with no config or options.
unlink $conf_file;
$sqitch = App::Sqitch->new(options => { engine => 'pg' });
ok $init = $CLASS->new( sqitch => $sqitch ),
    'Create new init with sqitch with default engine attributes';
ok $init->write_config, 'Write the config with engine attrs';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $conf_file]
], 'The creation should be sent to info again again again';
is_deeply read_config $conf_file, {
    'core.engine' => 'pg',
}, 'The configuration should have been written with only the engine var' or diag $conf_file->slurp;

file_contents_like $conf_file, qr{^\Q# [engine "pg"]
	# target = db:pg:
	# registry = sqitch
	# client = psql$exe_ext
}m, 'Engine section should be present but commented-out' or diag $conf_file->slurp;

USERCONF: {
    # Delete the file and write with a user config loaded.
    unlink $conf_file;
    local $ENV{SQITCH_USER_CONFIG} = file +File::Spec->updir, 'user.conf';
    my $sqitch = App::Sqitch->new(options => { engine => 'pg' });
    ok my $init = $CLASS->new( sqitch  => $sqitch ),
        'Make an init with pg and user config';
    file_not_exists_ok $conf_file;
    ok $init->write_config, 'Write the config with pg config';
    is_deeply +MockOutput->get_info, [
        [__x 'Created {file}', file => $conf_file]
    ], 'The pg config creation should be sent to info';

    is_deeply read_config $conf_file, {
        'core.engine'      => 'pg',
    }, 'The configuration should have been written with pg options' or diag $conf_file->slurp;

    file_contents_like $conf_file, qr/^\t# registry = meta\n/m,
        'Configured registry should be in a comment';
    file_contents_like $conf_file,
        qr{^\t# target = db:pg://postgres\@localhost/thingies\n}m,
        'Configured target should be in a comment';
}

##############################################################################
# Test write_plan().
can_ok $init, 'write_plan';
$target = $init->default_target;
$plan_file = $target->plan_file;
file_not_exists_ok $plan_file, 'Plan file should not yet exist';
ok $init->write_plan( 'nada' ), 'Write the plan file';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $plan_file]
], 'The plan creation should be sent to info';
file_exists_ok $plan_file, 'Plan file should now exist';
file_contents_is $plan_file,
    '%syntax-version=' . App::Sqitch::Plan::SYNTAX_VERSION() . "\n" .
    '%project=nada' . "\n\n",
 'The contents should be correct';

# Write more to the plan.
my $fh = $plan_file->open('>:utf8_strict') or die "Cannot open $plan_file: $!\n";
$fh->say('# testing 1, 2, 3');
$fh->close;

# Try writing again.
ok $init->write_plan( 'foofoo' ), 'Write the plan file again';
file_contents_like $plan_file, qr/testing 1, 2, 3/,
    'The file should not be overwritten';

# Make sure a URI gets written, if present.
$plan_file->remove;
$sqitch = App::Sqitch->new(options => { top_dir => dir('plan.dir')->stringify });
END { remove_tree dir('plan.dir')->stringify };
ok $init = $CLASS->new(
    sqitch => $sqitch,
    uri    => $uri,
), 'Create new init with sqitch with project and URI';
$target = $init->default_target;
$plan_file = $target->plan_file;
ok $init->write_plan( 'howdy' ), 'Write the plan file again';
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $plan_file->dir . $sep],
    [__x 'Created {file}', file => $plan_file]
], 'The plan creation should be sent to info againq';
file_exists_ok $plan_file, 'Plan file should again exist';
file_contents_is $plan_file,
    '%syntax-version=' . App::Sqitch::Plan::SYNTAX_VERSION() . "\n" .
    '%project=howdy' . "\n" .
    '%uri=' . $uri->canonical . "\n\n",
    'The plan should include the project and uri pragmas';

##############################################################################
# Test _validate_project().
can_ok $init, '_validate_project';
NOPROJ: {
    # Test handling of no command.
    my $mock = Test::MockModule->new($CLASS);
    my @args;
    $mock->mock(usage => sub { @args = @_; die 'USAGE' });
    throws_ok { $CLASS->_validate_project }
        qr/USAGE/, 'No project should yield usage';
    is_deeply \@args, [$CLASS], 'No args should be passed to usage';
}

# Test invalid project names.
my @bad_names = (
    '^foo',     # No leading punctuation
    'foo^',     # No trailing punctuation
    'foo^6',    # No trailing punctuation+digit
    'foo^666',  # No trailing punctuation+digits
    '%hi',      # No leading punctuation
    'hi!',      # No trailing punctuation
    'foo@bar',  # No @ allowed at all
    'foo:bar',  # No : allowed at all
    '+foo',     # No leading +
    '-foo',     # No leading -
    '@foo',     # No leading @
);
for my $bad (@bad_names) {
    throws_ok { $init->_validate_project($bad) } 'App::Sqitch::X',
        qq{Should get error for invalid project name "$bad"};
    is $@->ident, 'init', qq{Bad project "$bad" ident should be "init"};
    is $@->message, __x(
        qq{invalid project name "{project}": project names must not }
        . 'begin with punctuation, contain "@", ":", "#", or blanks, or end in '
        . 'punctuation or digits following punctuation',
        project => $bad
    ), qq{Bad project "$bad" error message should be correct};
}

##############################################################################
# Bring it all together, yo.
$conf_file->remove;
$plan_file->remove;
ok $init->execute('foofoo'), 'Execute!';

# Should have directories.
for my $attr (map { "$_\_dir"} qw(top deploy revert verify)) {
    dir_exists_ok $target->$attr;
}

# Should have config and plan.
file_exists_ok $conf_file;
file_exists_ok $plan_file;

# Should have the output.
my @dir_messages = map {
    [__x 'Created {file}', file => $target->$_ . $sep]
} map { "$_\_dir" } qw(deploy revert verify);
is_deeply +MockOutput->get_info, [
    [__x 'Created {file}', file => $conf_file],
    [__x 'Created {file}', file => $plan_file],
    @dir_messages,
], 'Should have status messages';

file_contents_is $plan_file,
    '%syntax-version=' . App::Sqitch::Plan::SYNTAX_VERSION() . "\n" .
    '%project=foofoo' . "\n" .
    '%uri=' . $uri->canonical . "\n\n",
    'The plan should have the --project name';