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 FindBin qw($Bin);

use Test::More tests => 208;

BEGIN
{
  use_ok('Rose::DB');
  use_ok('Rose::DB::Registry');
  use_ok('Rose::DB::Registry::Entry');
  use_ok('Rose::DB::Constants');

  require 't/test-lib.pl';

  is(Rose::DB::Constants::IN_TRANSACTION(), -1, 'Rose::DB::Constants::IN_TRANSACTION');
  Rose::DB::Constants->import('IN_TRANSACTION');

  # Default
  My::DB2->register_db(
    domain   => 'default',
    type     => 'default',
    driver   => 'Pg',
    database => 'test',
    host     => 'localhost',
    username => 'postgres',
    password => '',
  );

  # Main
  My::DB2->register_db(
    domain   => 'test',
    type     => 'default',
    driver   => 'Pg',
    database => 'test',
    host     => 'localhost',
    username => 'postgres',
    password => '',
  );

  # Aux
  My::DB2->register_db(
    domain   => 'test',
    type     => 'aux',
    driver   => 'Pg',
    database => 'test',
    host     => 'localhost',
    username => 'postgres',
    password => '',
  );

  # Generic
  My::DB2->register_db(
    domain   => 'test',
    type     => 'generic',
    driver   => 'NoneSuch',
    database => 'test',
    host     => 'localhost',
    username => 'someuser',
    password => '',
  );

  # Alias
  My::DB2->alias_db(source => { domain => 'test',  type => 'aux'  },
                     alias  => { domain => 'atest', type => 'aaux' });

  package MyPgClass;
  @MyPgClass::ISA = qw(Rose::DB::Pg);
  sub format_date { die "boo!" }
}

my $sqlite_ok = have_db('sqlite_admin');

is_deeply(scalar My::DB2->registry->registered_domains, 
          [ qw(atest catalog_test default test) ], 'registered_domains()');

is_deeply(scalar My::DB2->registry->registered_types('test'), 
          [ qw(aux default generic informix informix_admin mysql mysql_admin
               oracle oracle_admin pg pg_admin pg_with_schema),
               ($sqlite_ok ? qw(sqlite sqlite_admin) : ()) ],
          'registered_types()');

# Lame arbitrary test of one dump attr
my $dump = My::DB2->registry->dump;
is($dump->{'test'}{'aux'}{'username'}, 'postgres', 'dump() 1');

is(IN_TRANSACTION, -1, 'IN_TRANSACTION');

is(My::DB2->default_keyword_function_calls, 0, 'default_keyword_function_calls 1');
My::DB2->default_keyword_function_calls(1);
is(My::DB2->default_keyword_function_calls, 1, 'default_keyword_function_calls 2');

my $db = My::DB2->new;

is($db->keyword_function_calls, 1, 'keyword_function_calls 1');

$db = My::DB2->new;

is($db->keyword_function_calls, 1, 'keyword_function_calls 2');

is(My::DB2->default_domain, 'test', 'default_domain() 1');
is(My::DB2->default_type, 'default', 'default_type() 1');

ok(My::DB2->db_exists('default'), 'db_exists() 1');
ok(!My::DB2->db_exists('defaultx'), 'db_exists() 2');

ok(My::DB2->db_exists(type => 'default'), 'db_exists() 3');
ok(!My::DB2->db_exists(type => 'defaultx'), 'db_exists() 4');

ok(My::DB2->db_exists(type => 'default', domain => 'test'), 'db_exists() 3');
ok(!My::DB2->db_exists(type => 'defaultx', domain => 'testx'), 'db_exists() 4');

ok(!My::DB2->db_exists(type => 'defaultx', domain => 'test'), 'db_exists() 3');

My::DB2->error('foo');

is(My::DB2->error, 'foo', 'error() 2');

$db->error('bar');

is(My::DB2->error, 'bar', 'error() 3');
is($db->error, 'bar', 'error() 4');

eval { $db = My::DB2->new };
ok(!$@, 'Valid type and domain');

My::DB2->default_domain('foo');

is(My::DB2->default_domain, 'foo', 'default_domain() 2');

eval { $db = My::DB2->new };
ok($@, 'Invalid domain');

My::DB2->default_domain('test');
My::DB2->default_type('bar');

is(My::DB2->default_type, 'bar', 'default_type() 2');

eval { $db = My::DB2->new };
ok($@, 'Invalid type');

is(Rose::DB->driver_class('Pg'), 'Rose::DB::Pg', 'driver_class() 1');
is(My::DB2->driver_class('xxx'), undef, 'driver_class() 2');

My::DB2->driver_class(Pg => 'MyPgClass');
is(My::DB2->driver_class('Pg'), 'MyPgClass', 'driver_class() 3');

$db = My::DB2->new(type => 'aux', database => 'xyzzy');

is($db->database, 'xyzzy', 'override on new() 1');

$db = My::DB2->new(type => 'aux', dsn => 'dbi:Pg:host=foo;database=bar');

is($db->dsn, 'dbi:Pg:host=foo;database=bar', 'override on new() 2');

$db = My::DB2->new('aux');

ok($db->isa('MyPgClass'), 'new() single arg');

is($db->error('foo'), 'foo', 'subclass 1');
is($db->error, 'foo', 'subclass 2');

eval { $db->format_date('123') };
ok($@ =~ /^boo!/, 'driver_class() 4');

is(My::DB2->default_connect_option('AutoCommit'), 1, "default_connect_option('AutoCommit')");
is(My::DB2->default_connect_option('RaiseError'), 1, "default_connect_option('RaiseError')");
is(My::DB2->default_connect_option('PrintError'), 1, "default_connect_option('PrintError')");
is(My::DB2->default_connect_option('ChopBlanks'), 1, "default_connect_option('ChopBlanks')");
is(My::DB2->default_connect_option('Warn'), 0, "default_connect_option('Warn')");

my $options = My::DB2->default_connect_options;

is(ref $options, 'HASH', 'default_connect_options() 1');
is(join(',', sort keys %$options), 'AutoCommit,ChopBlanks,PrintError,RaiseError,Warn',
  'default_connect_options() 2');

My::DB2->default_connect_options(a => 1, b => 2);

is(My::DB2->default_connect_option('a'), 1, "default_connect_option('a')");
is(My::DB2->default_connect_option('b'), 2, "default_connect_option('b')");

My::DB2->default_connect_options({ c => 3, d => 4 });

is(My::DB2->default_connect_option('c'), 3, "default_connect_option('c') 1");
is(My::DB2->default_connect_option('d'), 4, "default_connect_option('d') 1");

my $keys = join(',', sort keys %{$db->default_connect_options});

$db->default_connect_options(zzz => 'bar');

my $keys2 = join(',', sort keys %{$db->default_connect_options});

is($keys2, "$keys,zzz", 'default_connect_options() 1');

$db->default_connect_options({ zzz => 'bar' });

$keys2 = join(',', sort keys %{$db->default_connect_options});

is($keys2, 'zzz', 'default_connect_options() 2');

$keys = join(',', sort keys %{$db->connect_options});

$db->connect_options(zzzz => 'bar');

$keys2 = join(',', sort keys %{$db->connect_options});

is($keys2, "$keys,zzzz", 'connect_option() 1');

$db->connect_options({ zzzz => 'bar' });

$keys2 = join(',', sort keys %{$db->connect_options});

is($keys2, 'zzzz', 'connect_option() 2');

$db->dsn('dbi:Pg:dbname=dbfoo;host=hfoo;port=pfoo');

#ok(!defined($db->database) || $db->database eq 'dbfoo', 'dsn() 1');
#ok(!defined($db->host) || $db->host eq 'hfoo', 'dsn() 2');
#ok(!defined($db->port) || $db->port eq 'port', 'dsn() 3');

eval { $db->dsn('dbi:mysql:dbname=dbfoo;host=hfoo;port=pfoo') };

ok($@ || $DBI::VERSION <  1.43, 'dsn() driver change');

$db = My::DB2->new(domain  => 'test', type  => 'aux');
my $adb = My::DB2->new(domain  => 'atest', type  => 'aaux');

is($db->class, 'My::DB2', 'class() 1');

foreach my $attr (qw(domain type driver database username password 
                     connect_options post_connect_sql))
{
  is($db->username, $adb->username, "alias $attr()");
}

My::DB2->modify_db(domain   => 'test', 
                    type     => 'aux', 
                    username => 'blargh',
                    connect_options => { Foo => 1 });

$db->init_db_info(refresh => 1);
$adb->init_db_info(refresh => 1);

is($db->username, $adb->username, "alias username() mod");
is($db->connect_options->{'Foo'}, $adb->connect_options->{'Foo'}, "alias connect_options() mod");

$db = My::DB2->new('generic');

ok($db->isa('Rose::DB::Generic'), 'generic class');

is($db->dsn, 'dbi:NoneSuch:dbname=test;host=localhost', 'generic dsn');

ok(!$db->has_dbh, 'has_dbh() 1');

#
# Registry tests
#

my $reg = My::DB2->registry;

ok($reg->isa('Rose::DB::Registry'), 'registry');

my $entry = $reg->entry(domain => 'test', type => 'aux');

ok($entry->isa('Rose::DB::Registry::Entry'), 'registry entry 1');

foreach my $param (qw(autocommit database domain driver dsn host password port
                      print_error raise_error handle_error server_time_zone schema
                      type username connect_options pre_disconnect_sql 
                      post_connect_sql))
{
  eval { $entry->$param() };

  ok(!$@, "entry $param()");
}

my $host     = $entry->host;
my $database = $entry->database;

My::DB2->modify_db(domain => 'test', type => 'aux', host => 'foo', database => 'bar');

is($entry->host, 'foo', 'entry modify_db() 1');
is($entry->database, 'bar', 'entry modify_db() 2');

is($entry->connect_option('RaiseError') || 0, 0, 'entry connect_option() 1');
$entry->connect_option('RaiseError' => 1);
is($entry->connect_option('RaiseError'), 1, 'entry connect_option() 2');

$entry->pre_disconnect_sql(qw(sql1 sql2));
my $sql = $entry->pre_disconnect_sql;
ok(@$sql == 2 && $sql->[0] eq 'sql1' && $sql->[1] eq 'sql2', 'entry pre_disconnect_sql() 1');

$entry->post_connect_sql(qw(sql3 sql4));
$sql = $entry->post_connect_sql;
ok(@$sql == 2 && $sql->[0] eq 'sql3' && $sql->[1] eq 'sql4', 'entry post_connect_sql() 1');

$entry->raise_error(0);
is($entry->connect_option('RaiseError'), 0, 'entry raise_error() 1');

$entry->print_error(1);
is($entry->connect_option('PrintError'), 1, 'entry print_error() 1');

$entry->autocommit(1);
is($entry->connect_option('AutoCommit'), 1, 'entry autocommit() 1');

my $handler = sub { 123 };
$entry->handle_error($handler);
is($entry->connect_option('HandleError'), $handler, 'entry handle_error() 1');

{
  package MyTest::DB;
  our @ISA = qw(My::DB2);
  MyTest::DB->use_private_registry;
  MyTest::DB->default_type('dt');
  MyTest::DB->default_domain('dd');
  MyTest::DB->register_db(driver => 'sqlite');
}

$db = MyTest::DB->new;

is($db->type, 'dt', 'default type 1');
is($db->domain, 'dd', 'default domain 1');

{
  package MyTest::DB2;
  our @ISA = qw(My::DB2);
  MyTest::DB2->default_type('xdt');
  MyTest::DB2->default_domain('xdd');

  MyTest::DB2->register_db(driver => 'sqlite');
}

$db = MyTest::DB2->new;

is($db->type, 'xdt', 'default type 2');
is($db->domain, 'xdd', 'default domain 2');

my @Intervals =
(
  '+0::'               => '',
  '-0:1:'              => '-00:01:00',
  '2:'                 => '02:00:00',
  '1 D'                => '1 day',
  '-1 d 2 s'           => '-1 days +00:00:02',
  '-1 y 3 h -57 M 4 s' => '-1 years +02:03:04',
  '-1 y 2 mons  3 d'   => '-10 mons +3 days',
  '-1 y 2 mons -3 d'   => '-10 mons -3 days',

  '5 h -208 m -495 s' => '01:23:45',
  '-208 m -495 s'     => '-03:36:15',
  '5 h 208 m 495 s'   => '08:36:15',

  ':'         => undef,
  '::'        => undef,
  '123:456:'  => undef,
  '1:-2:3'    => undef,
  '1:2:-3'    => undef,
  '1 h 1:1:1' => undef,
  '1 d 2 d'   => undef,
  '1: 2:'     => undef,
  '1 s 2:'    => undef,

  '1 ys 2 h 3 m 4 s'  => undef,
  '1 y s 2 h 3 m 4 s' => undef,
  '1 ago'             => undef,
  '1s ago'            => undef,
  '1 s agos'          => undef,
  '1 m ago ago 1 s'   => undef,
  '1 m ago1 s'        => undef,
  '1 m1 s'            => undef,

  '1 mil 2 c 3 dec 4 y 5 mon 1 w -1 d 7 h 8 m 9 s'             => '1234 years 5 mons 6 days 07:08:09',
  '-1 mil -2 c -3 dec -4 y -5 mon -1 w 1 d -7 h -8 m -9 s'     => '-1234 years -5 mons -6 days -07:08:09',
  '-1 mil -2 c -3 dec -4 y -5 mon -1 w 1 d -7 h -8 m -9 s ago' => '1234 years 5 mons 6 days 07:08:09',

  '1 mils 2 cents 3 decs 4 years 5 mons 1 weeks -1 days 7 hours 8 mins 9 secs' => '1234 years 5 mons 6 days 07:08:09',

  '1 millenniums 2 centuries 3 decades 4 years 5 months 1 weeks -1 days 7 hours 8 minutes 9 seconds' =>
      '1234 years 5 mons 6 days 07:08:09',

  '1 mil -1 d ago'     => '-1000 years +1 day',
  '1 mil ago -1 d ago' => '-1000 years +1 day',
);

my $i = 0;

while($i < @Intervals)
{
  my($val, $formatted) = ($Intervals[$i++], $Intervals[$i++]);

  is($db->format_interval($db->parse_interval($val)), $formatted, "parse_interval ($val)");
}

MyTest::DB2->max_interval_characters(1);

eval { $db->format_interval($db->parse_interval('1 day ago')) };
ok($@, 'max_interval_characters 1');

ok(My::DB2->max_interval_characters != MyTest::DB2->max_interval_characters, 'max_interval_characters 2');

$db->keyword_function_calls(1);
is($db->parse_interval('foo()'), 'foo()', 'parse_interval (foo())');
$db->keyword_function_calls(0);

MyTest::DB2->max_interval_characters(255);

my $d = $db->parse_interval('1 year 0.000003 seconds');

is($d->nanoseconds, 3000, 'nanoseconds 1');

is($db->format_interval($d), '1 year 00:00:00.000003000', 'nanoseconds 2');

# Time vaues

my $tc;

ok($tc = $db->parse_time('12:34:56.123456789'), 'parse time 12:34:56.123456789');
is($tc->as_string, '12:34:56.123456789', 'check time 12:34:56.123456789');
is($db->format_time($tc), '12:34:56.123456789', 'format time 12:34:56.123456789');

ok($tc = $db->parse_time('12:34:56.123456789 pm'), 'parse time 12:34:56.123456789 pm');
is($tc->as_string, '12:34:56.123456789', 'check time 12:34:56.123456789 pm');
is($db->format_time($tc), '12:34:56.123456789', 'format time 12:34:56.123456789 pm');

ok($tc = $db->parse_time('12:34:56. A.m.'), 'parse time 12:34:56. A.m.');
is($tc->as_string, '00:34:56', 'check time 12:34:56 am');
is($db->format_time($tc), '00:34:56', 'format time 12:34:56 am');

ok($tc = $db->parse_time('12:34:56 pm'), 'parse time 12:34:56 pm');
is($tc->as_string, '12:34:56', 'check time 12:34:56 pm');
is($db->format_time($tc), '12:34:56', 'format time 12:34:56 pm');

ok($tc = $db->parse_time('2:34:56 pm'), 'parse time 2:34:56 pm');
is($tc->as_string, '14:34:56', 'check time 14:34:56 pm');
is($db->format_time($tc), '14:34:56', 'format time 14:34:56 pm');

ok($tc = $db->parse_time('2:34 pm'), 'parse time 2:34 pm');
is($tc->as_string, '14:34:00', 'check time 2:34 pm');
is($db->format_time($tc), '14:34:00', 'format time 2:34 pm');

ok($tc = $db->parse_time('2 pm'), 'parse time 2 pm');
is($tc->as_string, '14:00:00', 'check time 2 pm');
is($db->format_time($tc), '14:00:00', 'format time 2 pm');

ok($tc = $db->parse_time('3pm'), 'parse time 3pm');
is($tc->as_string, '15:00:00', 'check time 3pm');
is($db->format_time($tc), '15:00:00', 'format time 3pm');

ok($tc = $db->parse_time('4 p.M.'), 'parse time 4 p.M.');
is($tc->as_string, '16:00:00', 'check time 4 p.M.');
is($db->format_time($tc), '16:00:00', 'format time 4 p.M.');

ok($tc = $db->parse_time('24:00:00'), 'parse time 24:00:00');
is($tc->as_string, '24:00:00', 'check time 24:00:00');
is($db->format_time($tc), '24:00:00', 'format time 24:00:00');

ok($tc = $db->parse_time('24:00:00 PM'), 'parse time 24:00:00 PM');
is($tc->as_string, '24:00:00', 'check time 24:00:00 PM');
is($db->format_time($tc), '24:00:00', 'format time 24:00:00 PM');

ok($tc = $db->parse_time('24:00'), 'parse time 24:00');
is($tc->as_string, '24:00:00', 'check time 24:00');
is($db->format_time($tc), '24:00:00', 'format time 24:00');

ok(!defined $db->parse_time('24:00:00.000000001'), 'parse time fail 24:00:00.000000001');
ok(!defined $db->parse_time('24:00:01'), 'parse time fail 24:00:01');
ok(!defined $db->parse_time('24:01'), 'parse time fail 24:01');

if(have_db('sqlite'))
{
  My::DB2->register_db
  (
    domain => 'handel',
    type   => 'default',
    driver => 'SQLite',
  );

  $db = My::DB2->new
  (
    domain => 'handel',
    type   => 'default',
    dsn    => "dbi:SQLite:dbname=$Bin/sqlite.db",
  );

  my $dbh = $db->dbh;

  is($db->dsn, "dbi:SQLite:dbname=$Bin/sqlite.db", 'dsn preservation 1');

  $db = My::DB2->new
  (
    domain   => 'handel',
    type     => 'default',
    database => "$Bin/sqlitex.db",
  );

  $dbh = $db->dbh;

  is($db->dsn, "dbi:SQLite:dbname=$Bin/sqlitex.db", 'dsn preservation 2');

  unlink("$Bin/sqlite.db");
  unlink("$Bin/sqlitex.db");
}
else
{
  ok(1, 'skipping - dsn preservation requires sqlite 1');
  ok(1, 'skipping - dsn preservation requires sqlite 2');
}

#
# Registry entry tests
#

my @entry;

$i = 1;

foreach my $attr (sort(Rose::DB::Registry::Entry::_attrs(type => 'scalar')))
{
  push(@entry, $attr => ($attr eq 'driver' || $attr eq 'dbi_driver' ? 'sqlite' :  $i++));
}

foreach my $attr (sort(Rose::DB::Registry::Entry::_attrs(type => 'boolean')))
{
  push(@entry, $attr => $i++ % 2);
}

foreach my $attr (sort(Rose::DB::Registry::Entry::_attrs(type => 'hash')))
{
  push(@entry, $attr => { $i++ => $i++ });
}

foreach my $attr (sort(Rose::DB::Registry::Entry::_attrs(type => 'array')))
{
  push(@entry, $attr => [ $i++ ]);
}

$entry = Rose::DB::Registry::Entry->new(@entry);

$dump = $entry->dump;

is_deeply($dump, { @entry }, 'dump entry');

if(have_db('mysql'))
{
  my %mysql_entry = map { $_ => $dump->{$_} } grep { /^mysql_/ } keys %$dump;

  My::DB2->register_db(
    domain   => 'abc',
    type     => 'def',
    driver   => 'mysql',
    database => 'test',
    %mysql_entry);

  my $db = My::DB2->new(domain => 'abc', type => 'def');

  foreach my $attr (grep { /^mysql_/ } keys %$dump)
  {
    is($db->$attr(), $dump->{$attr}, "entry attr - $attr");
  }
}
else
{
  my $count = grep { /^mysql_/ } keys %$dump;
  SKIP: { skip('mysql entry tests', $count) }
}

if(have_db('sqlite'))
{
  {
      package My::DBX;
  
      use base 'My::DB2';
  
      My::DBX->register_db(
          driver => 'SQLite',
      );  
  
      My::DBX->default_connect_options( { RaiseError => 0, } );
  }
  
  my $db1 = My::DBX->new;
  ok(!$db1->dbh->{RaiseError}, 'RaiseError false');
  
  my $db2 = My::DBX->new(raise_error => 1);
  ok($db2->dbh->{RaiseError}, 'RaiseError true');
  
  my $db3 = My::DBX->new;
  ok(!$db3->dbh->{RaiseError}, 'RaiseError false');
}
else
{
  SKIP: { skip('connect option tests that require DBD::SQLite', 3) }
}