The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

use Test::More;
use Test::Exception;
use Try::Tiny;
use DBIx::Class::Optional::Dependencies ();
use lib qw(t/lib);
use DBICTest;

plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_mssql_ado')
  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_mssql_ado');

# Example DSN (from frew):
# dbi:ADO:PROVIDER=sqlncli10;SERVER=tcp:172.24.2.10;MARS Connection=True;Initial Catalog=CIS;UID=cis_web;PWD=...;DataTypeCompatibility=80;

my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MSSQL_ADO_${_}" } qw/DSN USER PASS/};

plan skip_all => 'Set $ENV{DBICTEST_MSSQL_ADO_DSN}, _USER and _PASS to run this test'
  unless ($dsn && $user);

DBICTest::Schema->load_classes(qw/VaryingMAX ArtistGUID/);

my %binstr = ( 'small' => join('', map { chr($_) } ( 1 .. 127 )) );
$binstr{'large'} = $binstr{'small'} x 1024;

my $maxloblen = length $binstr{'large'};

my $schema = DBICTest::Schema->connect($dsn, $user, $pass, {
  auto_savepoint => 1,
  LongReadLen => $maxloblen,
});

$schema->storage->ensure_connected;

isa_ok($schema->storage, 'DBIx::Class::Storage::DBI::ADO::Microsoft_SQL_Server');

my $ver = $schema->storage->_server_info->{normalized_dbms_version};

ok $ver, 'can introspect DBMS version';

# 2005 and greater
is $schema->storage->sql_limit_dialect, ($ver >= 9 ? 'RowNumberOver' : 'Top'),
  'correct limit dialect detected';

$schema->storage->dbh_do (sub {
    my ($storage, $dbh) = @_;
    try { local $^W = 0; $dbh->do("DROP TABLE artist") };
    $dbh->do(<<'SQL');
CREATE TABLE artist (
   artistid INT IDENTITY NOT NULL,
   name VARCHAR(100),
   rank INT NOT NULL DEFAULT '13',
   charfield CHAR(10) NULL,
   primary key(artistid)
)
SQL
});

$schema->storage->dbh_do (sub {
  my ($storage, $dbh) = @_;
  try { local $^W = 0; $dbh->do("DROP TABLE artist_guid") };
  $dbh->do(<<"SQL");
CREATE TABLE artist_guid (
 artistid UNIQUEIDENTIFIER NOT NULL,
 name VARCHAR(100),
 rank INT NULL,
 charfield CHAR(10) NULL,
 a_guid UNIQUEIDENTIFIER,
 primary key(artistid)
)
SQL
});

my $have_max = $ver >= 9; # 2005 and greater

$schema->storage->dbh_do (sub {
    my ($storage, $dbh) = @_;
    try { local $^W = 0; $dbh->do("DROP TABLE varying_max_test") };
    $dbh->do("
CREATE TABLE varying_max_test (
   id INT IDENTITY NOT NULL,
" . ($have_max ? "
   varchar_max VARCHAR(MAX),
   nvarchar_max NVARCHAR(MAX),
   varbinary_max VARBINARY(MAX),
" : "
   varchar_max TEXT,
   nvarchar_max NTEXT,
   varbinary_max IMAGE,
") . "
   primary key(id)
)");
});

my $ars = $schema->resultset('Artist');

my $new = $ars->create({ name => 'foo' });
ok($new->artistid > 0, 'Auto-PK worked');

# make sure select works
my $found = $schema->resultset('Artist')->search({ name => 'foo' })->first;
is $found->artistid, $new->artistid, 'search works';

# test large column list in select
$found = $schema->resultset('Artist')->search({ name => 'foo' }, {
  select => ['artistid', 'name', map \"'foo' foo_$_", 0..50],
  as     => ['artistid', 'name', map        "foo_$_", 0..50],
})->first;
is $found->artistid, $new->artistid, 'select with big column list';
is $found->get_column('foo_50'), 'foo', 'last item in big column list';

# create a few more rows
for (1..12) {
  $schema->resultset('Artist')->create({ name => 'Artist ' . $_ });
}

# test multiple active cursors
my $rs1 = $schema->resultset('Artist')->search({}, { order_by => 'artistid' });
my $rs2 = $schema->resultset('Artist')->search({}, { order_by => 'name' });

while ($rs1->next) {
  ok try { $rs2->next }, 'multiple active cursors';
}

# test bug where ADO blows up if the first bindparam is shorter than the second
is $schema->resultset('Artist')->search({ artistid => 2 })->first->name,
  'Artist 1',
  'short bindparam';

is $schema->resultset('Artist')->search({ artistid => 13 })->first->name,
  'Artist 12',
  'longer bindparam';

# test explicit key spec
$new = $ars->create ({ name => 'bar', artistid => 66 });
is($new->artistid, 66, 'Explicit PK worked');
$new->discard_changes;
is($new->artistid, 66, 'Explicit PK assigned');

# test basic transactions
$schema->txn_do(sub {
  $ars->create({ name => 'transaction_commit' });
});
ok($ars->search({ name => 'transaction_commit' })->first,
  'transaction committed');
$ars->search({ name => 'transaction_commit' })->delete,
throws_ok {
  $schema->txn_do(sub {
    $ars->create({ name => 'transaction_rollback' });
    die 'rolling back';
  });
} qr/rolling back/, 'rollback executed';
is $ars->search({ name => 'transaction_rollback' })->first, undef,
  'transaction rolled back';

# test two-phase commit and inner transaction rollback from nested transactions
$schema->txn_do(sub {
  $ars->create({ name => 'in_outer_transaction' });
  $schema->txn_do(sub {
    $ars->create({ name => 'in_inner_transaction' });
  });
  ok($ars->search({ name => 'in_inner_transaction' })->first,
    'commit from inner transaction visible in outer transaction');
  throws_ok {
    $schema->txn_do(sub {
      $ars->create({ name => 'in_inner_transaction_rolling_back' });
      die 'rolling back inner transaction';
    });
  } qr/rolling back inner transaction/, 'inner transaction rollback executed';
});
ok($ars->search({ name => 'in_outer_transaction' })->first,
  'commit from outer transaction');
ok($ars->search({ name => 'in_inner_transaction' })->first,
  'commit from inner transaction');
is $ars->search({ name => 'in_inner_transaction_rolling_back' })->first,
  undef,
  'rollback from inner transaction';
$ars->search({ name => 'in_outer_transaction' })->delete;
$ars->search({ name => 'in_inner_transaction' })->delete;

# test populate
lives_ok (sub {
  my @pop;
  for (1..2) {
    push @pop, { name => "Artist_$_" };
  }
  $ars->populate (\@pop);
});

# test populate with explicit key
lives_ok (sub {
  my @pop;
  for (1..2) {
    push @pop, { name => "Artist_expkey_$_", artistid => 100 + $_ };
  }
  $ars->populate (\@pop);
});

# count what we did so far
is ($ars->count, 18, 'Simple count works');

# test empty insert
my $current_artistid = $ars->search({}, {
  select => [ { max => 'artistid' } ], as => ['artistid']
})->first->artistid;

my $row;
lives_ok { $row = $ars->create({}) }
  'empty insert works';

$row->discard_changes;

is $row->artistid, $current_artistid+1,
  'empty insert generated correct PK';

# test that autoinc column still works after empty insert
  $row = $ars->create({ name => 'after_empty_insert' });

  is $row->artistid, $current_artistid+2,
    'autoincrement column functional aftear empty insert';

my $rs = $schema->resultset('VaryingMAX');

foreach my $size (qw/small large/) {
  local $schema->storage->{debug} = 0 if $size eq 'large';

  my $str = $binstr{$size};
  my $row;
  lives_ok {
    $row = $rs->create({
      varchar_max => $str, nvarchar_max => $str, varbinary_max => $str
    });
  } "created $size VARXXX(MAX) LOBs";

  lives_ok {
    $row->discard_changes;
  } 're-selected just-inserted LOBs';

  cmp_ok try { $row->varchar_max },   'eq', $str, 'VARCHAR(MAX) matches';
  cmp_ok try { $row->nvarchar_max },  'eq', $str, 'NVARCHAR(MAX) matches';
  cmp_ok try { $row->varbinary_max }, 'eq', $str, 'VARBINARY(MAX) matches';
}

# test regular blobs

try { local $^W = 0; $schema->storage->dbh->do('DROP TABLE bindtype_test') };
$schema->storage->dbh->do(qq[
CREATE TABLE bindtype_test
(
  id     INT IDENTITY NOT NULL PRIMARY KEY,
  bytea  INT NULL,
  blob   IMAGE NULL,
  clob   TEXT NULL,
  a_memo NTEXT NULL
)
],{ RaiseError => 1, PrintError => 1 });

$rs = $schema->resultset('BindType');
my $id = 0;

foreach my $type (qw( blob clob a_memo )) {
  foreach my $size (qw( small large )) {
    $id++;

    lives_ok { $rs->create( { 'id' => $id, $type => $binstr{$size} } ) }
      "inserted $size $type without dying" or next;

    my $from_db = eval { $rs->find($id)->$type } || '';
    diag $@ if $@;

    ok($from_db eq $binstr{$size}, "verified inserted $size $type" )
      or do {
        my $hexdump = sub {
          join '', map sprintf('%02X', ord), split //, shift
        };
        diag 'Got: ', "\n", substr($hexdump->($from_db),0,255), '...',
          substr($hexdump->($from_db),-255);
        diag 'Size: ', length($from_db);
        diag 'Expected Size: ', length($binstr{$size});
        diag 'Expected: ', "\n",
          substr($hexdump->($binstr{$size}), 0, 255),
          "...", substr($hexdump->($binstr{$size}),-255);
      };
  }
}
# test IMAGE update
lives_ok {
  $rs->search({ id => 0 })->update({ blob => $binstr{small} });
} 'updated IMAGE to small binstr without dying';

lives_ok {
  $rs->search({ id => 0 })->update({ blob => $binstr{large} });
} 'updated IMAGE to large binstr without dying';

# test GUIDs
lives_ok {
  $row = $schema->resultset('ArtistGUID')->create({ name => 'mtfnpy' })
} 'created a row with a GUID';

ok(
  eval { $row->artistid },
  'row has GUID PK col populated',
);
diag $@ if $@;

my $guid = try { $row->artistid }||'';

ok(($guid !~ /^{.*?}\z/), 'GUID not enclosed in braces')
  or diag "GUID is: $guid";

ok(
  eval { $row->a_guid },
  'row has a GUID col with auto_nextval populated',
);
diag $@ if $@;

my $row_from_db = $schema->resultset('ArtistGUID')
  ->search({ name => 'mtfnpy' })->first;

is try { $row_from_db->artistid }, try { $row->artistid },
  'PK GUID round trip (via ->search->next)';

is try { $row_from_db->a_guid }, try { $row->a_guid },
  'NON-PK GUID round trip (via ->search->next)';

$row_from_db = try { $schema->resultset('ArtistGUID')
  ->find($row->artistid) };

is try { $row_from_db->artistid }, try { $row->artistid },
  'PK GUID round trip (via ->find)';

is try { $row_from_db->a_guid }, try { $row->a_guid },
  'NON-PK GUID round trip (via ->find)';

($row_from_db) = $schema->resultset('ArtistGUID')
  ->search({ name => 'mtfnpy' })->all;

is try { $row_from_db->artistid }, try { $row->artistid },
  'PK GUID round trip (via ->search->all)';

is try { $row_from_db->a_guid }, try { $row->a_guid },
  'NON-PK GUID round trip (via ->search->all)';

lives_ok {
  $row = $schema->resultset('ArtistGUID')->create({
      artistid => '70171270-4822-4450-81DF-921F99BA3C06',
      name => 'explicit_guid',
  });
} 'created a row with explicit PK GUID';

is try { $row->artistid }, '70171270-4822-4450-81DF-921F99BA3C06',
  'row has correct PK GUID';

lives_ok {
  $row->update({ artistid => '70171270-4822-4450-81DF-921F99BA3C07' });
} "updated row's PK GUID";

is try { $row->artistid }, '70171270-4822-4450-81DF-921F99BA3C07',
  'row has correct PK GUID';

lives_ok {
  $row->delete;
} 'deleted the row';

lives_ok {
  $schema->resultset('ArtistGUID')->populate([{
      artistid => '70171270-4822-4450-81DF-921F99BA3C06',
      name => 'explicit_guid',
  }]);
} 'created a row with explicit PK GUID via ->populate in void context';

done_testing;

# clean up our mess
END {
  local $SIG{__WARN__} = sub {};
  if (my $dbh = try { $schema->storage->_dbh }) {
    (try { $dbh->do("DROP TABLE $_") })
      for qw/artist artist_guid varying_max_test bindtype_test/;
  }

  undef $schema;
}
# vim:sw=2 sts=2