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

use 5.10.1;
use strict;
use warnings;
use utf8;
use Test::More;


BEGIN {
  eval { require DBD::SQLite; 1 }
    or plan skip_all => 'DBD::SQLite required';
  eval { DBD::SQLite->VERSION gt '1.37' }
    or plan skip_all => 'DBD::SQLite >= 1.37 required';
  use File::Basename 'dirname';
  use Cwd;
  use lib (Cwd::abs_path(dirname(__FILE__) . '/..') . '/examples/lib');
}
use My;
use My::Group;
use My::User;

local $Params::Check::VERBOSE = 0;

#Suppress some warnings from DBIx::Simple::Class during tests.
local $SIG{__WARN__} = sub {
  if ($_[0] =~ /(generated accessors|is not such field|to build)/) {
    my ($package, $filename, $line, $subroutine) = caller(2);
    ok(1, $subroutine . ' warns OK');
  }
  else {
    warn $_[0];
  }
};


my $DSC = 'DBIx::Simple::Class';

# In memory database! No file permission troubles, no I/O slowness.
# http://use.perl.org/~tomhukins/journal/31457 ++

my $dbix = DBIx::Simple->connect('dbi:SQLite:dbname=:memory:', {sqlite_unicode => 1});

#test DBIx::Simple instantiation
like((eval { $DSC->dbix }, $@), qr/not instantiated/);
like((eval { $DSC->dbix('') }, $@), qr/not instantiated/);
isa_ok(ref($DSC->dbix($dbix)), 'DBIx::Simple');
isa_ok(ref($DSC->dbix),        'DBIx::Simple');

my $groups_table = <<"T";
CREATE TEMPORARY TABLE groups(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  group_name VARCHAR(12)
  )
T

$dbix->query($groups_table);


my $users_table = <<"T";
CREATE TEMPORARY TABLE users(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  group_id INT DEFAULT 1,
  login_name VARCHAR(12),
  login_password VARCHAR(100), 
  disabled INT DEFAULT 1
  )
T

$dbix->query($users_table);

#$DSC->DEBUG(1);
isa_ok(ref(My->dbix($dbix)), 'DBIx::Simple');
is(My->dbix, $DSC->dbix, 'same instance');
ok(!My->BUILD(), 'nothing to build');
ok(!(My->DEBUG(1) && My->BUILD()), 'nothing to build debugged');
My->DEBUG(0);
my $user;
my $password = time;
like(
  (eval { $user = My::User->new() }, $@),
  qr/Required option/,
  '"Required option" ok'
);


ok($user = My::User->new(login_password => $password));

like((eval { $user->BUILD() }, $@), qr/Call this method as/, 'BUILD() ok');
is(
  My::User->BUILD(),
  $DSC->_attributes_made->{'My::User'},
  'if (eval $code) in BUILD() ok'
);
isa_ok($user, $DSC);

#defaults
is($user->id, undef, 'id is undefined ok');
is($user->group_id, $user->CHECKS->{group_id}{default}, 'group_id default ok');
delete $user->CHECKS->{group_id}{default};
delete $user->{data}->{group_id};
is($user->group_id, $user->CHECKS->{group_id}{default}, 'group_id default ok');
is($user->login_name, undef, 'login_name is undefined ok');
is($user->login_password, $password, 'login_password is defined ok');
is($user->disabled, $user->CHECKS->{disabled}{default}, 'disabled by default ok');

#invalid
my $type_error = qr/\sis\sof\sinvalid\stype/x;
like((eval { $user->id('bar') },       $@), $type_error, "id is invalid ok");
like((eval { $user->group_id('bar') }, $@), $type_error, "group_id is invalid ok");

like((eval { $user->login_name('sakdk-') }, $@), $type_error, "login_name_error ok");
like((eval { $user->login_name('пет') }, $@),
  $type_error, 'login_name is shorter ok');
like((eval { $user->login_name('петърparvanov') }, $@),
  $type_error, 'login_name is longer ok');

like((eval { $user->login_password('тайнаtа') }, $@),
  $type_error, 'login_password is shorter ok');
like((eval { $user->login_password('тайнаtатайнаtатайнаtа') }, $@),
  $type_error, 'login_password is longer ok');

like((eval { $user->disabled('foo') }, $@), $type_error, 'disabled is invalid ok');
like((eval { $user->disabled(5) },     $@), $type_error, 'disabled is longer ok');

#valid
ok($user->login_name('петър')->login_name, 'login_name is valid');
ok($user->login_password('петър123342%$')->login_password,
  'login_password is valid');
ok($user->disabled(0), 'disabled is valid');
is($user->disabled, 0, 'disabled is valid');

#data
is($user->data->{disabled}, 0, 'disabled via data is valid');
is($user->data('disabled'), 0, 'disabled via data is valid');
is($user->data(disabled => 0, group_id => 2)->{group_id},
  2, 'disabled via data is valid');
is(ref $user->data, 'HASH', 'disabled via data is valid');

my $group;
like(
  (eval { My::Group->new() }, $@),
  qr/You can not use .+? as a column name/,
  '"You can not use \'data\' as a column name" ok'
);

delete My::Group->COLUMNS->[-1];
like(
  (eval { My::Group->BUILD() }, $@),
  qr/Illegal declaration of subroutine/,
  '"Illegal declaration of subroutine" ok'
);
delete My::Group->COLUMNS->[-1];
is_deeply(My::Group->COLUMNS, [qw(id group_name)], 'COLUMNS are valid now - ok');

like(
  (eval { My::Group->new(description => 'tralala') }, $@),
  qr/is not a valid key for/,
  '"is not a valid key for" ok'
);
My::Group->DEBUG(1);
like(
  (eval { My::Group->new->data('lala') }, $@),
  qr/Can't locate object method "lala" via package "My::Group"/,
  '"is not a valid key for" ok2'
);
ok(My::Group->can('id'),         'can id');
ok(My::Group->can('group_name'), 'can group_name');
ok($group = My::Group->new, 'My::Group->new ok');
ok($group->id(1), '$group->id(1) ok');
ok($group->data('lala' => 1), 'can not lala ok');
My::Group->DEBUG(0);
is_deeply($group->data(), {id => 1}, '"There is not such field lala" ok');

#insert
My::Group->CHECKS->{id}         = {allow => qr/^\d+$/};
My::Group->CHECKS->{group_name} = {allow => qr/^\p{IsAlnum}{3,12}$/x};
ok($group = My::Group->new(group_name => 'admin'));
is((eval { $group->save } || $@), 1, 'ok inserted group:' . $group->id);

#update
is(($group->group_name('admins')->save && $group->id),
  1, 'ok updated group:' . $group->id);
ok($group = $dbix->query('select*from groups where id=1')->object('My::Group'));
is($group->group_name, 'admins', 'group name is equal');
my $g2;
ok($g2 = My::Group->new(group_name => 'guests'));
like(
  (eval { $g2->update }, $@),
  qr/Please\sdefine\sprimary\skey/x,
  '"Please define primary key column" croaks ok'
);


is(($g2->save(group_name => 'users') && $g2->group_name),
  'users', 'new group_name "' . $g2->group_name . '" with params to save ok');

is($user->group_id($group->id)->group_id, 1, 'added user to group ok');
is($user->save,                           1, 'user inserted ok');

#update dies
$g2->{SQL_UPDATE} = 'UPDATE xx "BOOM';
like((eval { $g2->update }, $@), qr/prepare\sfailed/x, '"prepare failed" croaks ok');
delete $DSC->_attributes_made->{'My::User'};
ok(
  $user =
    $dbix->query('select*from users where login_name=?', $user->login_name)
    ->object('My::User'),
  'user retrieved from database ok'
);

if (eval { My::User->dbix->abstract }) {
  is_deeply(My::User->select(id => $user->id, disabled => 0)->data,
    $user->data, 'select works!');
  is_deeply(My::User->select(id => $user->id)->data, undef, 'wrong select works!');
}
is_deeply(
  My::User->query('select * from users where id=? and disabled=?', $user->id, 0)->data,
  $user->data,
  'select works!'
);
is_deeply(
  My::User->query('select * from users where id=? and disabled=?', $user->id, 1)->data,
  undef,
  'wrong select works!'
);
isa_ok(My->query('select * from users where id=? and disabled=?', $user->id, 1),
  'HASH', 'My->query isa HASH');

#test column=>method collision
my $collision_table = <<"T";
CREATE TEMPORARY TABLE collision(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  data TEXT
  )
T

$dbix->query($collision_table);

my $coll;
isa_ok($coll = My::Collision->new(data => 'some text'),
  'My::Collision', '"column=>alias" ok');

#use it
is_deeply($coll->column_data('bar')->data, {data => 'bar'}, 'alias() sets ok');
is($coll->column_data, 'bar', 'alias() gets ok');
is_deeply($coll->data('data'), 'bar', 'data() gets ok');
is_deeply($coll->data('data' => 'foo'), {data => 'foo'}, 'data() sets ok');
is($coll->save, 1, 'alias() inserts ok');
$coll = My::Collision->query('select * from collision where id=1');
is_deeply($coll->data, {data => 'foo', id => 1}, 'alias() query ok');
if (eval { My::Collision->dbix->abstract }) {
  $coll = My::Collision->select(id => 1);
  is_deeply($coll->data, {data => 'foo', id => 1}, 'alias() select ok');
}
ok($coll->column_data('barababa')->save, 'alias() updates ok');
is(
  $coll->column_data,
  My::Collision->query('select * from collision where id=1')->column_data,
  'alias() updates ok2'
);

#select
#test getting by primary key
My::Collision->new(data => 'second id')->save;
is(My::Collision->select_by_pk(2)->id, 2, 'select_by_pk ok');
is(My::Collision->select_by_pk(2)->id, 2, 'select_by_pk ok from $SQL_CACHE');
delete $DSC->_SQL_CACHE->{'My::Collision'}{SELECT_BY_PK};
is(My::Collision->find(2)->id, 2, 'find ok');
is(My::Collision->find(2)->id, 2, 'find ok from $SQL_CACHE');

#testing SQL
my $site_group = My::Group->new(group_name => 'SiteUsers');
is($site_group->save, 3, ' group ' . $site_group->group_name . ' created ok');
is(My::Group->create(group_name => 'OtherUsers')->id, 4, 'create() ok');
my $SCLASS = 'My::SiteUser';
$SCLASS->CHECKS->{group_id}{default} = $site_group->id;
$SCLASS->WHERE->{group_id} = $site_group->id;
isa_ok($SCLASS->SQL(FOO => 'SELECT * FROM foo'), 'HASH', 'SQL(FOO=>...) is setting ok');
is(
  $SCLASS->SQL(FOO => 'SELECT * FROM foo') && $SCLASS->SQL('FOO'),
  'SELECT * FROM foo',
  'SQL(FOO=>...) is setting ok2'
);
isa_ok($SCLASS->SQL(), 'HASH', 'SQL() is getting ok');
like($SCLASS->SQL('SELECT'),       qr/FROM\s+users/x,     'SQL(SELECT) is getting ok');
like(My::Collision->SQL('SELECT'), qr/FROM\s+collision/x, 'SQL(SELEC) is getting ok2');
like(
  $SCLASS->SQL('GUEST_USER'),
  qr/SELECT \* FROM users/,
  'SQL(GUEST_USER) is getting ok'
);

like(
  (eval { $DSC->SQL('SELECT') } || $@),
  qr/fields for your class/,
  '$DSC->SQL(SELECT) croaks ok'
);
$SCLASS->new(login_name => 'guest', login_password => time . 'QW')
  ->group_id($site_group->id)->save;

my $guest = $SCLASS->query($SCLASS->SQL('SELECT') . ' AND id=?', 1);
$guest = $SCLASS->select_by_pk(1);
like(
  (eval { $guest->SQL('SELECT') } || $@),
  qr/This is a class method/,
  '$guest->SQL(SELECT) croaks ok'
);

is(
  $SCLASS->SQL('SELECT_BY_PK'),
  $DSC->_SQL_CACHE->{$SCLASS}{SELECT_BY_PK},
  'SQL(SELECT_BY_PK) is getting ok'
);

like(
  $SCLASS->SQL('SELECT'),
  qr/WHERE\s(disabled='0'\sAND\sgroup_id='3'|group_id='3'\sAND\sdisabled='0')/x,
  'SELECT generated ok'
);

for (3 .. 5) {
  my $user = $SCLASS->new(login_name => "user$_", login_password => time . $_ . 'a');
  is($user->save, $_, 'User with id:' . $user->id . ' saved ok');
  is($user->group_id, $site_group->id, 'User has group_id:' . $site_group->id . ' ok');
}

#test objects scalar and list contexts
my $site_users =
  $dbix->query('SELECT * FROM users WHERE group_id=?', $site_group->id)
  ->objects($SCLASS);
my @site_users =
  $dbix->query('SELECT * FROM users WHERE group_id=?', $site_group->id)
  ->objects($SCLASS);
is_deeply($site_users, \@site_users, 'new_from_dbix_simple() wantarray ok');

#test query context awareness
my $site_user = $SCLASS->query('SELECT * FROM users WHERE group_id=?', $site_group->id);
@site_users = $SCLASS->query('SELECT * FROM users WHERE group_id=?', $site_group->id);

is_deeply($site_user, $site_users[0], 'query() wantarray ok');


#LIMIT
like(
  (eval { $DSC->SQL('_LIMIT') } || $@),
  qr/Named query '_LIMIT' can not be used directly/,
  '$DSC->SQL(_LIMIT) croaks ok'
);
$site_users = $dbix->query(
  'SELECT * FROM users WHERE group_id=? ORDER BY id ASC ' . $SCLASS->SQL_LIMIT(2),
  $site_group->id)->objects($SCLASS);
is(scalar @$site_users, 2, 'LIMIT limits ok');
$site_users = $dbix->query(
  'SELECT * FROM users WHERE group_id=? ORDER BY id ASC ' . $SCLASS->SQL_LIMIT(2, 2),
  $site_group->id)->objects($SCLASS);
is(scalar @$site_users, 2, 'OFFSET offsets ok');
is_deeply($site_users, [$site_users[-2], $site_users[-1]], 'OFFSET really offsets ok');


#QUOTE_IDENTIFIERS
is_deeply(
  $SCLASS->_UNQUOTED,
  { 'WHERE' => {
      'disabled' => 0,
      'group_id' => 3
    },
    'COLUMNS' => ['id', 'group_id', 'login_name', 'login_password', 'disabled'],
    'TABLE'   => 'users'
  },
  '_UNQUOTED ok'
);

my $my_groups_table = <<"T";
CREATE TEMPORARY TABLE "my groups"(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  "group" VARCHAR(12),
  "is' enabled" INT DEFAULT 0
  )
T

$dbix->query($my_groups_table);

#now dbix is instantiated and we can call BUILD in the package it self
require My::Groups;

is(My::Groups->TABLE, '"my groups"', 'table IDENTIFIER quoted ok');
is(eval { My::Groups->new('is\' enabled' => 1, group => 'name1')->insert }
    || $@ => 1 => 'quoteD_identifier inserts ok');
is(eval { My::Groups->new('is\' enabled' => 1, group => 'name2')->save } || $@,
  2, 'quoteD_identifier inserts ok2');

isa_ok(eval { $g2 = My::Groups->find(2) }
    || $@ => 'My::Groups' => 'quoteD_identifier finds ok');
is_deeply(
  $g2->data,
  { 'group'        => 'name2',
    'is\' enabled' => 1,
    'id'           => 2
  },
  'quoteD_identifier data ok'
);

is(eval { $g2->group('name_second')->update } || $@, 1,
  'quoteD_identifier updates ok2');

is_deeply(
  $g2->data,
  { 'group'        => 'name_second',
    'is\' enabled' => 1,
    'id'           => 2
  },
  'quoteD_identifier data after update ok'
);
is_deeply(
  My::Groups->find(2)->data,
  { 'group'        => 'name_second',
    'is\' enabled' => 1,
    'id'           => 2
  },
  'quoteD_identifier updated data found ok'
);

#this will make it die since identifiers are already quoted and become double quoted
delete $DSC->_attributes_made->{'My::Groups'};
like(
  eval { My::Groups->find(2) } || $@,
  qr/'"""my\sgroups"""'/x,
  'quoteD already identifier  ok'
);

like(eval { My::Groups->query(My::Groups->SQL('SELECT') . ' and id=?', 2) } || $@,
  qr/'"""my\sgroups"""'/x, 'quoteD already identifier  ok2');
if (eval { My::Groups->dbix->abstract }) {
  like(eval { My::Groups->select(id => 2) } || $@,
    qr/'"""my\sgroups"""'/x, 'quoteD already identifier  ok3');

}

done_testing();


__END__

#Benchmarks
use Benchmark qw(:all);
for (1 .. 1000) {
  my $user =
    My::User->new(login_name => "user$_", login_password => time . $_ . 'a')->save();
}

#We are about 3 times faster when selecting than :RowObject.
timethese(
  10000,
  { 'My::User' => sub {
      my $u = My::User->query('SELECT * FROM users WHERE id=?', 22);
      my $a = $u->login_name . $u->login_password;
      #$u->login_name('aladin');
      #$u->login_password('akjskajdksa12');
    },
    ':RowObject' => sub {
      my $u = $dbix->query('SELECT * FROM users WHERE id=?', 22)->object(':RowObject');
      my $a = $u->login_name . $u->login_password;
      #$u->login_name('aladin');
      #$u->login_password('akjskajdksa12');
    },
  }
);

#We are faster than $dbix->insert and faster than $dbix->query when used with (??)
my $i = 0;
timethese(
  10000,
  { 'My::User->insert' => sub {
      My::User->new(login_name => "user" . $i++, login_password => time . 'a')
        ->insert();
    },
    '$dbix->insert' => sub {
      $dbix->insert(
        'users',
        { login_name     => "user" . $i++,
          login_password => time . 'a'
        }
      );
    },
    '$dbix->query' => sub {
      $dbix->query(
        'INSERT into users(login_name,login_password)VALUES(??)',
        "user" . $i++,
        time . 'a'
      );
    },
  }
);


use Benchmark qw(:all);

#We are faster than $dbix->update
my $uu = My::User->query('SELECT id,login_name,group_id FROM users WHERE id=?', 2);
my $du = $dbix->query('SELECT id,login_name,group_id FROM users WHERE id=?', 2)->hash;
my $dq = $dbix->query('SELECT id,login_name,group_id FROM users WHERE id=?', 2)->hash;

my $i = 0;
timethese(
  10000,
  { 'My::User->update' => sub {

      $uu->data(login_name => 'pepi1', group_id => 2);
      $uu->update;
    },
    '$dbix->update' => sub {
      $dbix->update('users', {login_name => 'pepi1', group_id => 2}, {id => $du->{id}});
    },
    '$dbix->query' => sub {
      $dbix->query('UPDATE users SET login_name=?, group_id=? WHERE id=? ',
        'pepi1', 2, $dq->{id});
    },
  }
);