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

use strict;
use warnings;

my %CONFIG;
use Test::More;

BEGIN {
   # This test attempts to talk to a real Cassandra database, and will
   # create its own keyspace to work in.
   #
   # It is disabled by default, but to enable it create a t/local.yaml file
   # containing the host, keyspace, and optionally username, password and
   # service port number to connect to.
   -e "t/local.yaml" or plan skip_all => "No t/local.yaml config";
   require YAML;
   %CONFIG = %{ YAML::LoadFile( "t/local.yaml" ) };
   defined $CONFIG{host} or plan skip_all => "t/local.yaml does not define a host";
   defined $CONFIG{keyspace} or plan skip_all => "t/local.yaml does not define a keyspace";
}

use Test::HexString;

use IO::Async::Test;
use IO::Async::Loop;

use Net::Async::CassandraCQL;
use Protocol::CassandraCQL qw( CONSISTENCY_ONE );

my $loop = IO::Async::Loop->new();
testing_loop( $loop );

my $cass = Net::Async::CassandraCQL->new(
   username => $CONFIG{username},
   password => $CONFIG{password},
   keyspace => $CONFIG{keyspace},
   default_consistency => CONSISTENCY_ONE,
);
$loop->add( $cass );

my $f = $cass->connect(
   host     => $CONFIG{host},
   service  => $CONFIG{port},
);
wait_for { $f->is_ready };
$f->get;

$cass->query( "CREATE TABLE tbl1 (key varchar PRIMARY KEY, t1 varchar, i1 int)" )->get;
my $table = 1;
END { $table and $cass->query( "DROP TABLE tbl1" )->await }

pass( "CREATE TABLE" );

$cass->query( "INSERT INTO tbl1 (key, t1) VALUES ('the-key', 'the-value')" )->get;
pass( "INSERT INTO tbl" );

# SELECT as ->query
{
   my ( $type, $result ) = $cass->query( "SELECT key, t1 FROM tbl1" )->get;

   is( $type, "rows", "SELECT query result type is rows" );

   is( $result->columns, 2, '$result has 2 columns' );
   is( $result->rows, 1, '$result has 1 row' );
   is( scalar $result->column_name(0), "$CONFIG{keyspace}.tbl1.key", 'column_name 0' );
   is( scalar $result->column_name(1), "$CONFIG{keyspace}.tbl1.t1",  'column_name 1' );
   is( $result->column_type(0)->name, "VARCHAR", 'column_type 0 name' );
   is( $result->column_type(1)->name, "VARCHAR", 'column_type 1 name' );

   is( $result->find_column( "key" ), 0, 'find_column key' );

   is_deeply( $result->row_array(0),
              [ "the-key", "the-value" ],
              'row_array(0)' );

   is_deeply( $result->row_hash(0),
              { key => "the-key", t1 => "the-value" },
              'row_hash(0)' );
}

# INSERT as ->prepare / ->execute
{
   my $query = $cass->prepare( "INSERT INTO tbl1 (key, i1) VALUES (?, ?)" )->get;

   ok( length $query->id, '$query->id is set for prepared INSERT' );

   is( $query->columns, 2, '$query has 2 columns for prepared INSERT' );
   is( scalar $query->column_name(0), "$CONFIG{keyspace}.tbl1.key", 'column_name 0' );
   is( scalar $query->column_name(1), "$CONFIG{keyspace}.tbl1.i1",  'column_name 1' );
   is( $query->column_type(0)->name, "VARCHAR", 'column_type 0 name' );
   is( $query->column_type(1)->name, "INT",     'column_type 1 name' );

   # ARRAY
   $query->execute( [ "another-key", 123456789 ] )->get;

   # HASH
   $query->execute( { key => "second-key", i1 => 987654321 } )->get;
}

# SELECT as ->prepare / ->execute
{
   my $query = $cass->prepare( "SELECT i1 FROM tbl1 WHERE key = ?" )->get;

   ok( length $query->id, '$query->id is set for prepared SELECT' );

   is( $query->columns, 1, '$query has 1 column for prepared SELECT' );

   my ( $type, $result ) = $query->execute( [ "another-key" ] )->get;

   is( $type, "rows", 'SELECT prepare/execute result type is rows' );

   is_deeply( $result->row_array(0),
              [ 123456789 ],
              'row_array(0) for SELECT prepare/execute' );
}

# Now test we have the right (de)serialisation form for all the numeric types
{
   $cass->query( <<'EOCQL' )->get;
CREATE TABLE numbers (
   key text PRIMARY KEY,
   i int, bi bigint, vi varint,
   flt float, dbl double, d decimal)
EOCQL
   my $table = 1;
   END { $table and $cass->query( "DROP TABLE numbers" )->await }

   my %ints = (
      zero => 0,
      one  => 1,
      two  => 2,
      ten  => 10,
      minus_one => -1,
      million => 1_000_000,
   );

   my $getn_stmt = $cass->prepare( "SELECT * FROM numbers WHERE key = ?" )->get;

   foreach my $name ( keys %ints ) {
      my $n = $ints{$name};
      $cass->query( "INSERT INTO numbers (key, i, bi, vi, flt, dbl, d) VALUES ('$name', $n, $n, $n, $n, $n, $n)" )->get;
   }

   foreach my $name ( keys %ints ) {
      my $n = $ints{$name};
      my ( undef, $result ) = $getn_stmt->execute( { key => $name } )->get;

      is_deeply( $result->row_hash(0),
                 { key => $name, map { +$_ => $n } qw( i bi vi flt dbl d ) },
                 "Number deserialisation for $name" );
   }

   my %reals = (
      tenth => 0.1,
      half  => 0.5,
      e     => exp(1),
   );

   foreach my $name ( keys %reals ) {
      my $n = $reals{$name};
      $cass->query( "INSERT INTO numbers (key, flt, dbl, d) VALUES ('$name', $n, $n, $n);" )->get;
   }

   foreach my $name ( keys %reals ) {
      my $n = $reals{$name};
      my ( undef, $result ) = $getn_stmt->execute( { key => $name } )->get;

      # floats aren't digit-wise exact
      my $row = $result->row_hash(0);
      is( sprintf( "%.5g", delete $row->{flt} ), sprintf( "%.5g", $n ),
          "Float deserialisation for $name" );

      is_deeply( $row,
                 { key => $name,
                   dbl => $n, d => $n,
                   i => undef, bi => undef, vi => undef },
                 "Number deserialisation for $name" );
   }
}

# Now test the collections
{
   $cass->query( <<'EOCQL' )->get;
CREATE TABLE collections (
   key text PRIMARY KEY,
   a_set set<text>,
   a_list list<int>,
   a_map map<text,text>)
EOCQL
   my $table = 1;
   END { $table and $cass->query( "DROP TABLE collections" )->await }

   $cass->query( "INSERT INTO collections (key, a_set) VALUES ('letters', {'a', 'b', 'c'})" )->get;
   $cass->query( "INSERT INTO collections (key, a_list) VALUES ('numbers', [1, 2, 3])" )->get;
   $cass->query( "INSERT INTO collections (key, a_map) VALUES ('upcase', {'x':'X', 'y':'Y'})" )->get;

   my ( undef, $result ) = $cass->query( "SELECT * FROM collections;" )->get;

   my $map = $result->rowmap_hash( "key" );

   is_deeply( $map->{letters}{a_set},  [qw( a b c )],    'SET serialisation' );
   is_deeply( $map->{numbers}{a_list}, [ 1, 2, 3 ],      'LIST serialisation' );
   is_deeply( $map->{upcase}{a_map},   {x=>"X", y=>"Y"}, 'MAP serialisation' );
}

done_testing;