The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Qudo::Test;
use strict;
use warnings;
use lib qw(./lib ./t/lib);
use Carp qw(croak);
use Qudo;
use YAML;
use DBI;
use Test::More;

our @SUPPORT_DRIVER = qw/Skinny/;

sub import {
    my $caller = caller(0);

    strict->import;
    warnings->import;

    for my $func (qw/run_tests run_tests_mysql run_tests_sqlite test_master teardown_dbs dsn_for/) {
        no strict 'refs'; ## no critic.
        *{$caller.'::'.$func} = \&$func;
    }
}

sub run_tests {
    my ($n, $code) = @_;

    plan tests => $n*3*scalar(@SUPPORT_DRIVER);

    for my $driver (@SUPPORT_DRIVER) {
        run_tests_mysql( $n, $driver, $code);
        run_tests_innodb($n, $driver, $code);
        run_tests_sqlite($n, $driver, $code);
    }
}

sub run_tests_innodb {
    my ($n, $driver, $code) = @_;
    run_tests_mysql($n, $driver, $code, 1);
}

sub run_tests_mysql {
    my ($n, $driver, $code, $innodb) = @_;

    SKIP: {
        local $ENV{USE_MYSQL} = 1;
        my $dbh = eval { mysql_dbh() }; ## no critic
        skip "MySQL not accessible as root on localhost", $n if $@;
        skip "InnoDB not available on localhost's MySQL", $n if $innodb && ! has_innodb($dbh);
        $code->($driver);
    }
}

sub run_tests_sqlite {
    my ($n, $driver, $code) = @_;

    SKIP: {
        my $rv = eval "use DBD::SQLite; 1"; ## no critic
        $rv = 0 if $ENV{SKIP_SQLITE};
        skip "SQLite not installed", $n if !$rv;
        $code->($driver);
    }
}

my $test_dbs;
sub test_master {
    my %opts = @_;
    my $dbs  = delete $opts{dbs} || ['default'];
    my $init = delete $opts{init};
    $init = 1 unless defined $init;

    $test_dbs = $dbs;

    if ($init) {
        setup_dbs($dbs);
    }

    my $params = +{
        databases => [
            map {{
                dsn      => dsn_for($_),
                username => 'root',
                password => '',
            }} @$dbs
        ],
        %opts,
    };

    return Qudo->new(%$params);
}

sub setup_dbs {
    my $dbs = shift;

    my $schema = load_schema();
    teardown_dbs($dbs);

    for my $db (@$dbs) {
        if ($ENV{USE_MYSQL}) {
            create_mysql_db(mysql_dbname($db));
        }

        my $dbh = DBI->connect(
            dsn_for($db),
            'root',
            '',
            { RaiseError => 1, PrintError => 0 }
        ) or die "Couldn't connect: $!\n";

        for my $sql (@{ $ENV{USE_MYSQL} ? $schema->{mysql} : $schema->{SQLite} }) {
            $sql =~ s!^\s*create\s+table\s+(\w+)!CREATE TABLE $1!i;
            $sql .= " ENGINE=INNODB\n" if $ENV{USE_MYSQL};
            $dbh->do($sql);
        }

        $dbh->disconnect;
    }
}

my $schema_data;
sub load_schema {
    $schema_data ||= YAML::Load(join "", <DATA>);
}

sub load_sql {
    my($file) = @_;
    open my $fh, '<', $file or die "Can't open $file: $!";
    my $sql = do { local $/; <$fh> };
    close $fh;
    my @sql = split /;\s*/, $sql;
    \@sql;
}

sub mysql_dbh {
    return DBI->connect(
        "DBI:mysql:mysql",
        "root",
        "",
        { RaiseError => 1 }
    ) or die "Couldn't connect to database";
}

sub dsn_for {
    my $dbname = shift;
    if ($ENV{USE_MYSQL}) {
        return 'dbi:mysql:' . mysql_dbname($dbname);
    } else {
        return 'dbi:SQLite:dbname=' . db_filename($dbname);
    }
}

sub db_filename {
    my $dbname = shift;
    'test_qudo_' . $dbname . '.db';
}

sub mysql_dbname {
    my $dbname = shift;
    'test_qudo_' . $dbname;
}

sub create_mysql_db {
    my $dbname = shift;
    mysql_dbh()->do("CREATE DATABASE $dbname");
}

sub drop_mysql_db {
    my $dbname = shift;
    mysql_dbh()->do("DROP DATABASE IF EXISTS $dbname");
}

sub teardown_dbs {
    my $dbs = shift || $test_dbs;

    for my $db (@$dbs) {
        if ($ENV{USE_MYSQL}) {
            drop_mysql_db(mysql_dbname($db));
        } else {
            my $file = db_filename($db);
            return unless -e $file;
            unlink $file or die "Can't teardown $db: $!";
        }
    }
}

sub has_innodb {
    my $dbh = shift;
    my $tmpname = "test_to_see_if_innoavail";
    $dbh->do("CREATE TABLE IF NOT EXISTS $tmpname (i int) ENGINE=INNODB")
        or return 0;
    my @row = $dbh->selectrow_array("SHOW CREATE TABLE $tmpname");
    my $row = join(' ', @row);
    my $has_it = ($row =~ /=InnoDB/i);
    $dbh->do("DROP TABLE $tmpname");
    return $has_it;
}

1;

__DATA__
SQLite:
  - |-
    CREATE TABLE func (
        id         INTEGER PRIMARY KEY AUTOINCREMENT,
        name       VARCHAR(255) NOT NULL,
        UNIQUE(name)
    )
  - |-
    CREATE TABLE job (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        func_id         INTEGER UNSIGNED NOT NULL,
        arg             MEDIUMBLOB,
        uniqkey         VARCHAR(255) NULL,
        enqueue_time    INTEGER UNSIGNED,
        grabbed_until   INTEGER UNSIGNED NOT NULL,
        run_after       INTEGER UNSIGNED NOT NULL DEFAULT 0,
        retry_cnt       INTEGER UNSIGNED NOT NULL DEFAULT 0,
        priority        INTEGER UNSIGNED NOT NULL DEFAULT 0,
        UNIQUE(func_id,uniqkey)
    )
  - |-
    CREATE TABLE exception_log (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        func_id         INTEGER UNSIGNED NOT NULL DEFAULT 0,
        exception_time  INTEGER UNSIGNED NOT NULL,
        message         MEDIUMBLOB NOT NULL,
        uniqkey         VARCHAR(255) NULL,
        arg             MEDIUMBLOB,
        retried         TINYINT(1) NOT NULL DEFAULT 0
    );
  - |-
    CREATE TABLE job_status (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        func_id         INTEGER UNSIGNED NOT NULL DEFAULT 0,
        arg             MEDIUMBLOB,
        uniqkey         VARCHAR(255) NULL,
        status          VARCHAR(10),
        job_start_time  INTEGER UNSIGNED NOT NULL,
        job_end_time    INTEGER UNSIGNED NOT NULL
    );
mysql:
  - |-
    CREATE TABLE func (
        id         INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
        name       VARCHAR(255) NOT NULL,
        UNIQUE(name)
    )
  - |-
    CREATE TABLE job (
        id              BIGINT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
        func_id         INTEGER UNSIGNED NOT NULL,
        arg             MEDIUMBLOB,
        uniqkey         VARCHAR(255) NULL,
        enqueue_time    INTEGER UNSIGNED,
        grabbed_until   INTEGER UNSIGNED NOT NULL,
        run_after       INTEGER UNSIGNED NOT NULL DEFAULT 0,
        retry_cnt       INTEGER UNSIGNED NOT NULL DEFAULT 0,
        priority        INTEGER UNSIGNED NOT NULL DEFAULT 0,
        UNIQUE(func_id, uniqkey),
        KEY priority (priority)
    )
  - |-
    CREATE TABLE exception_log (
        id              BIGINT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
        func_id         INTEGER UNSIGNED NOT NULL DEFAULT 0,
        exception_time  INTEGER UNSIGNED NOT NULL,
        message         MEDIUMBLOB NOT NULL,
        uniqkey         VARCHAR(255) NULL,
        arg             MEDIUMBLOB,
        retried         TINYINT(1) NOT NULL DEFAULT 0,
        INDEX (func_id),
        INDEX (exception_time)
    )
  - |-
    CREATE TABLE job_status (
        id              BIGINT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
        func_id         INTEGER UNSIGNED NOT NULL DEFAULT 0,
        arg             MEDIUMBLOB NOT NULL,
        uniqkey         VARCHAR(255) NULL,
        status          VARCHAR(10),
        job_start_time  INTEGER UNSIGNED NOT NULL,
        job_end_time    INTEGER UNSIGNED NOT NULL
    )
Pg:
  - |-
    CREATE TABLE func (
        id         SERIAL,
        name       VARCHAR(255) NOT NULL,
        PRIMARY KEY (id),
        UNIQUE (name)
    );
  - |-
    CREATE TABLE job (
        id              BIGSERIAL,
        func_id         INT NOT NULL,
        arg             BYTEA,
        uniqkey         VARCHAR(255) NULL,
        enqueue_time    INTEGER,
        grabbed_until   INTEGER  NOT NULL,
        run_after       INTEGER  NOT NULL DEFAULT 0,
        retry_cnt       INTEGER  NOT NULL DEFAULT 0,
        priority        INTEGER  NOT NULL DEFAULT 0,
        PRIMARY KEY (id),
        UNIQUE (func_id, uniqkey)
    );
  - |-
    CREATE TABLE exception_log (
        id              BIGSERIAL,
        func_id         INTEGER NOT NULL DEFAULT 0,
        exception_time  INTEGER NOT NULL,
        message         BYTEA,
        uniqkey         VARCHAR(255) NULL,
        arg             BYTEA,
        retried         SMALLINT,
        PRIMARY KEY (id)
    );
  - |-
    CREATE INDEX exception_log_func_id ON exception_log (func_id);
  - |-
    CREATE INDEX exception_log_exception_time ON exception_log (exception_time);
  - |-
    CREATE TABLE job_status (
        id              BIGSERIAL,
        func_id         INTEGER NOT NULL DEFAULT 0,
        arg             BYTEA,
        uniqkey         VARCHAR(255) NULL,
        status          VARCHAR(10),
        job_start_time  INTEGER NOT NULL,
        job_end_time    INTEGER NOT NULL,
        PRIMARY KEY (id)
    );