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;

use Test::More tests => 17;
use lib 't/lib';
use CleanEnv;

use Test::Deep;
use Tie::IxHash;
use Math::BigInt;
require re;

use BSON qw/encode decode/;

my $a;
tie( my %h, 'Tie::IxHash' );
tie( my %h1, 'Tie::IxHash' );
tie( my %h2, 'Tie::IxHash' );

sub _dump_bson {
    my $bson = unpack("H*",shift);
    $bson =~ s/(..)/$1 /g;
    return $bson;
}

sub _delta_ok {
    my ($lhs, $rhs, $label) = @_;;
    local $Test::Builder::Level = $Test::Builder::Level + 1;
    ok( abs($lhs - $rhs) < 1e-6, $label );
}

# Int32
subtest int32 => sub {
    plan tests => 2;
    %h = ( a => 1, b => 2147483647, c => -2147483648 );
    my $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            26,  0,   0,   0,   16, 97, 0, 1, 0, 0, 0,   16, 98, 0,
            255, 255, 255, 127, 16, 99, 0, 0, 0, 0, 128, 0
        ],
        'Int32 encode'
    );
    is_deeply( decode($bson), \%h, 'Int32 decode' );
};

# Int64
subtest int64 => sub {
    %h = (
        a => Math::BigInt->new('2147483648'),
        b => Math::BigInt->new('9223372036854775807'),
        c => Math::BigInt->new('-9223372036854775808')
    );
    my $bson = encode( \%h );

    cmp_deeply(
        [ unpack "C*", $bson ],
        [
            38,  0,   0,   0,
            18,  97,  0,   0,   0,   0,   128, 0,   0,   0,   0,
            18,  98,  0,   255, 255, 255, 255, 255, 255, 255, 127,
            18,  99,  0,   0,   0,   0,   0,   0,   0,   0,   128,
            0
        ],
        'Int64 encode'
    );

    # is_deeply fails to compare int64 properly
    my $decoded = decode($bson);
    for my $k ( qw/a b c/ ) {
        is( $decoded->{$k}, $h{$k}, "key $k" );
    }
};

# Mixed ints
subtest mix_ints => sub {
    %h = ( a => Math::BigInt->new('2147483648'), b => 1, c => -20 );
    my $bson = encode( \%h );

    # is_deeply fails to compare int64 properly
    cmp_deeply(
        [ unpack "C*", $bson ],
        [
            30,  0, 0,  0,  18, 97,  0,   0,   0,   0,
            128, 0, 0,  0,  0,  16,  98,  0,   1,   0,
            0,   0, 16, 99, 0,  236, 255, 255, 255, 0
        ],
        'Mixints encode'
    );

    # is_deeply fails to compare int64 properly
    my $decoded = decode($bson);
    for my $k ( qw/a b c/ ) {
        is( $decoded->{$k}, $h{$k}, "key $k" );
    }
};

subtest boolean => sub {
    plan tests => 6;

    # Boolean true
    %h = ( a => BSON::Bool->true );
    my $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [ 9, 0, 0, 0, 8, 97, 0, 1, 0 ],
        'True encode'
    );
    is_deeply( decode($bson), \%h, 'True decode' );

    # Boolean false
    %h = ( a => BSON::Bool->false );
    $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [ 9, 0, 0, 0, 8, 97, 0, 0, 0 ],
        'False encode'
    );
    is_deeply( decode($bson), \%h, 'False decode' );

    # Boolean mixed
    %h = ( a => BSON::Bool->true, b => BSON::Bool->false );
    $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [ 13, 0, 0, 0, 8, 97, 0, 1, 8, 98, 0, 0, 0 ],
        'mixed encode'
    );
    is_deeply( decode($bson), \%h, 'Mixed decode' );
};

# Double
subtest double => sub {
    plan tests => 4;
    %h = ( a => 0.12345, b => -0.1234, c => 123456.789 );
    my $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            38,  0,   0,   0,   1,   97,  0,  124, 242, 176,
            80,  107, 154, 191, 63,  1,   98, 0,   243, 142,
            83,  116, 36,  151, 191, 191, 1,  99,  0,   201,
            118, 190, 159, 12,  36,  254, 64, 0
        ],
        'Double encode'
    );

    my $hash = decode( $bson );
    for my $k ( sort keys %$hash ) {
        _delta_ok( $hash->{$k}, $h{$k}, "Double decode $h{$k}" );
    }
};

# String
subtest string => sub {
    plan tests => 5;
    %h = ( a => 'bar', b => 'foo' );
    my $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            27, 0, 0,  0, 2, 97, 0, 4, 0,   0,   0,   98, 97, 114,
            0,  2, 98, 0, 4, 0,  0, 0, 102, 111, 111, 0,  0
        ],
        'String encode'
    );

    my $hash = decode( $bson );
    is_deeply( $hash, \%h, 'String decode' );

    # String object
    %h = (
        a => BSON::String->new(123456),
        b => BSON::String->new(-11.99)
    );
    $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            33, 0,  0,  0,  2,  97, 0,  7,  0, 0, 0, 49,
            50, 51, 52, 53, 54, 0,  2,  98, 0, 7, 0, 0,
            0,  45, 49, 49, 46, 57, 57, 0,  0
        ],
        'String object encode'
    );

    $hash = decode( $bson );
    is( $hash->{a}, 123456, 'String object decode' );
    is( $hash->{b}, -11.99, 'String object decode 2' );
};

# Array
subtest array => sub {
    plan tests => 2;
    %h = ( a => [ 1, 2, 3, 'a', 'b', 'c' ] );
    my $bson = encode(\%h);
    is_deeply(
        [ unpack "C*", $bson ],
        [
            61, 0,  0,  0,  4, 97, 0, 53, 0, 0,  0,  16, 48, 0, 1, 0,
            0,  0,  16, 49, 0, 2,  0, 0,  0, 16, 50, 0,  3,  0, 0, 0,
            2,  51, 0,  2,  0, 0,  0, 97, 0, 2,  52, 0,  2,  0, 0, 0,
            98, 0,  2,  53, 0, 2,  0, 0,  0, 99, 0,  0,  0
        ],
        'Array encode'
    );
    is_deeply( decode($bson), \%h, 'Array decode' );
};

# Null
subtest null => sub {
    plan tests => 2;
    my $h = { a => undef };
    my $bson = encode( $h );
    is_deeply(
        [ unpack "C*", $bson ],
        [ 8, 0, 0, 0, 10, 97, 0, 0 ],
        'Null encode'
    );

    is_deeply( decode($bson), $h, 'Null decode' );
};

# Hash
subtest hash => sub {
    plan tests => 4;
    tie( %h2, 'Tie::IxHash', b => 1, c => 'bar' );
    %h = ( a => \%h2 );
    my $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            31, 0,  0,  0,   3, 97, 0, 23, 0, 0, 0, 16,
            98, 0,  1,  0,   0, 0,  2, 99, 0, 4, 0, 0,
            0,  98, 97, 114, 0, 0,  0
        ],
        'Hash 1 encode'
    );
    is_deeply( decode($bson), \%h, 'Hash 1 decode' );

    # Hash 2
    tie( %h1, 'Tie::IxHash', a => [ 1, 2, 3 ], b => 'foo' );
    tie( %h2, 'Tie::IxHash', a => \%h1, b => [ 1, 2, 3 ] );
    %h = ( a => \%h2, b => \%h1 );
    $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            138, 0,  0, 0,  3,  97, 0,   82,  0,   0, 0,  3,  97,  0,
            45,  0,  0, 0,  4,  97, 0,   26,  0,   0, 0,  16, 48,  0,
            1,   0,  0, 0,  16, 49, 0,   2,   0,   0, 0,  16, 50,  0,
            3,   0,  0, 0,  0,  2,  98,  0,   4,   0, 0,  0,  102, 111,
            111, 0,  0, 4,  98, 0,  26,  0,   0,   0, 16, 48, 0,   1,
            0,   0,  0, 16, 49, 0,  2,   0,   0,   0, 16, 50, 0,   3,
            0,   0,  0, 0,  0,  3,  98,  0,   45,  0, 0,  0,  4,   97,
            0,   26, 0, 0,  0,  16, 48,  0,   1,   0, 0,  0,  16,  49,
            0,   2,  0, 0,  0,  16, 50,  0,   3,   0, 0,  0,  0,   2,
            98,  0,  4, 0,  0,  0,  102, 111, 111, 0, 0,  0
        ],
        'Hash 2 encode'
    );
    is_deeply( decode($bson), \%h, 'Hash 2 decode' );
};

# Regex
subtest regex => sub {
    plan tests => 9;

    my @sp = BSON::PP::_split_re(qr/\w/i);
    is_deeply(\@sp, ['\w', 'i']);

    my $re1_str = q!"(?:[^"\\\]++|\\\.)*+"!;
    my @re1_bytes = (
        34,  40, 63, 58, 91, 94, 34, 92, 92, 93, 43, 43,
        124, 92, 92, 46, 41, 42, 43, 34, 0,  0
    );
    my $re2_str = q!"(?>(?:(?>[^"\\\]+)|\\\.)*)"!;
    my @re2_bytes = (
        34, 40, 63, 62, 40, 63, 58,  40, 63, 62, 91, 94,
        34, 92, 92, 93, 43, 41, 124, 92, 92, 46, 41, 42,
        41, 34, 0,  0
    );

    my @expected_bytes;
    if ($] >= 5.01) {
        # first regex works only on perl >= 5.10
        %h = eval { ( a => qr/$re1_str/, b => qr/$re2_str/ ) };
        die "Can't eval regexes: $@" if $@;
        @expected_bytes = (
            61, 0,  0, 0,
            11, 97, 0, @re1_bytes,
            11, 98, 0, @re2_bytes,
            0
        );
    } else {
        %h = (a => qr/$re2_str/, b => qr/$re2_str/);
        @expected_bytes = (
            67, 0,  0, 0,
            11, 97, 0, @re2_bytes,
            11, 98, 0, @re2_bytes,
            0
        );
    }

    my $bson = encode( \%h );
    is_deeply(
        [ unpack "C*", $bson ],
        \@expected_bytes,
        'Regex encode'
    );
    my $hash = decode( $bson );
    is(ref $hash->{a}, 'BSON::Regex');
    is(ref $hash->{b}, 'BSON::Regex');

    SKIP: {
        skip "Comparing regexes is fragile before 5.10", 1 if $] lt 5.010;
        $hash->{$_} = $hash->{$_}->try_compile for qw/a b/;
        for (qw/a b/) {
            is_deeply(
                [ re::regexp_pattern( $hash->{$_} ) ],
                [ re::regexp_pattern( $h{$_} ) ],
                "Regex decode of key $_",
            );
        }
    }

    #<<<
    %h = ( a => qr/(?:(?:[+-]?)(?:(?=[0123456789]|[.])(?:[0123456789]*)(?:(?:[.])(?:[0123456789]{0,}))?)(?:(?:[E])(?:(?:[+-]?)(?:[0123456789]+))|))/i );
    #>>>
    $bson = encode(\%h);
    is_deeply(
        [ unpack "C*", $bson ],
        [
            139, 0,  0,  0,  11,  97,  0,  40,  63,  58, 40, 63,
            58,  91,  43, 45,  93,  63, 41, 40,
            63,  58, 40, 63, 61,  91,  48, 49,  50,  51, 52, 53,
            54,  55, 56, 57, 93,  124, 91, 46,  93,  41, 40, 63,
            58,  91, 48, 49, 50,  51,  52, 53,  54,  55, 56, 57,
            93,  42, 41, 40, 63,  58,  40, 63,  58,  91, 46, 93,
            41,  40, 63, 58, 91,  48,  49, 50,  51,  52, 53, 54,
            55,  56, 57, 93, 123, 48,  44, 125, 41,  41, 63, 41,
            40,  63, 58, 40, 63,  58,  91, 69,  93,  41, 40, 63,
            58,  40, 63, 58, 91,  43,  45, 93,  63,  41, 40, 63,
            58,  91, 48, 49, 50,  51,  52, 53,  54,  55, 56, 57,
            93,  43, 41, 41, 124, 41,  41, 0,   105, 0,  0
        ],
        'real num regex'
    );
    $hash = decode( $bson );
    is(ref $hash->{a}, 'BSON::Regex');
    SKIP: {
        skip "Comparing regexes is fragile before 5.10", 2 if $] lt 5.010;
        $hash->{a} = $hash->{a}->try_compile;
        # after try_compile, "i" flags are put into the regex, so we must
        # do the same with the original
        my ($p,$f) = re::regexp_pattern($h{a});
        $h{a} = qr{(?$f:$p)};
        is_deeply(
            [ re::regexp_pattern( $hash->{a} ) ],
            [ re::regexp_pattern( $h{a} ) ],
            "Regex decode of key a",
        );
    }
};

# Datetime
subtest datetime => sub {
    eval { require DateTime };
    plan skip_all => "Needs DateTime" unless $INC{"DateTime.pm"};
    plan tests => 6;

    my $dt = DateTime->new(
        year      => 1974,
        month     => 10,
        day       => 15,
        hour      => 22,
        minute    => 50,
        second    => 8,
        time_zone => 'UTC'
    );
    my $h = { a => BSON::Time->new( $dt->epoch ) };
    my $bson = encode( $h );
    #<<<
    is_deeply(
        [ unpack "C*", $bson ],
        [ 16, 0, 0, 0, 9, 97, 0, 0, 149, 210, 46, 35, 0, 0, 0, 0 ],
        'encode 1974'
    );
    #>>>
    is_deeply( decode($bson), $h, 'decode 1974' );

    $dt = DateTime->new(
        year      => 1964,
        month     => 10,
        day       => 15,
        hour      => 22,
        minute    => 50,
        second    => 8,
        time_zone => 'UTC'
    );
    $h = { a => BSON::Time->new( $dt->epoch ) };
    $bson = encode( $h );
    #<<<
    is_deeply(
        [ unpack "C*", $bson ],
        [16, 0, 0, 0, 9, 97, 0, 0, 37, 154, 183, 217, 255, 255, 255, 0],
        'encode 1964'
    );
    #>>>
    is_deeply( decode($bson), $h, 'decode 1964' );

    $dt = DateTime->new(
        year      => 2028,
        month     => 10,
        day       => 15,
        hour      => 22,
        minute    => 50,
        second    => 8,
        time_zone => 'UTC'
    );
    $h = { a => BSON::Time->new( $dt->epoch ) };
    $bson = encode( $h );
    #<<<
    is_deeply(
        [ unpack "C*", $bson ],
        [16, 0, 0, 0, 9, 97, 0, 0, 229, 74, 246, 175, 1, 0, 0, 0],
        'encode 2028'
    );
    #>>>
    is_deeply( decode($bson), $h, 'decode 2028' );
};

subtest min_max_key => sub {
    plan tests => 4;
    my $bson = encode( { a => BSON::MinKey->new } );
    #<<<
    is_deeply(
        [ unpack "C*", $bson ],
        [8, 0, 0, 0, 255, 97, 0, 0],
        'MinKey encode'
    );
    #>>>
    isa_ok( decode($bson)->{a}, 'BSON::MinKey', 'MinKey decode' );

    $bson = encode( { a => BSON::MaxKey->new } );
    #<<<
    is_deeply(
        [ unpack "C*", $bson ],
        [8, 0, 0, 0, 127, 97, 0, 0],
        'MaxKey'
    );
    #>>>
    isa_ok( decode($bson)->{a}, 'BSON::MaxKey', 'MaxKey decode' );
};


subtest binary => sub {
    plan tests => 8;
    my $bin = BSON::Binary->new( [ 1, 2, 3, 4, 5 ] );
    my $bson = encode( { a => $bin } );
    #<<<
    is_deeply(
        [ unpack "C*", $bson ],
        [18, 0, 0, 0, 5, 97, 0, 5, 0, 0, 0, 0, 1, 2, 3, 4, 5, 0],
        'Binary 1 encode'
    ) or diag _dump_bson($bson);
    #>>>
    my $hash = decode($bson);
    isa_ok( $hash->{a}, 'BSON::Bytes' );
    is( $hash->{a}->type, $bin->type, 'compare type' );
    is_deeply( $hash->{a}->data, pack("C*",@{$bin->data}), 'compare data' );

    $bin = BSON::Binary->new( "5366a937375901366effb80511b39919", 5 );
    $bson = encode( { a => $bin } );
    $a = [ unpack "C*", encode( { a => $bin } ) ];
    is_deeply(
        [ unpack "C*", $bson ],
        [
            45, 0,  0,  0,  5,  97,  0,   32,  0,  0,  0,  5,
            53, 51, 54, 54, 97, 57,  51,  55,  51, 55, 53, 57,
            48, 49, 51, 54, 54, 101, 102, 102, 98, 56, 48, 53,
            49, 49, 98, 51, 57, 57,  49,  57,  0
        ],
        'Binary 2 encode'
    );
    $hash = decode($bson);
    isa_ok( $hash->{a}, 'BSON::Bytes' );
    is( $hash->{a}->type, $bin->type, 'compare type' );
    is_deeply( $hash->{a}->data, pack("C*",@{$bin->data}), 'compare data' );
};

# ObjectId
subtest objectid => sub {
    plan tests => 4;
    my $oid = BSON::ObjectId->new('4e2766e6e1b8325d02000028');
    my $h = { _id => $oid };
    my $bson = encode( $h );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            22,  0,   0,   0,  7,  95, 105, 100, 0,  78, 39, 102,
            230, 225, 184, 50, 93, 2,  0,   0,   40, 0
        ],
        'ObjectId encode'
    );

    my $hash = decode($bson);
    isa_ok( $hash->{_id}, 'BSON::OID', 'OID created' );
    is_deeply( $hash, $h, 'OID decode' );
    is("$h->{_id}", "$hash->{_id}", 'Match');
};


subtest code => sub {
    plan tests => 8;
    my $code = BSON::Code->new("function a(b,c){return b>c?c:b}", {});
    my $bson = encode( { a => $code } );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            53,  0,  0,  0,   15,  97,  0,   45,  0,   0,   0,   32,
            0,   0,  0,  102, 117, 110, 99,  116, 105, 111, 110, 32,
            97,  40, 98, 44,  99,  41,  123, 114, 101, 116, 117, 114,
            110, 32, 98, 62,  99,  63,  99,  58,  98,  125, 0,   5,
            0,   0,  0,  0,   0
        ],
        'Code with empty scope encode'
    );

    my $hash = decode( $bson );
    isa_ok( $hash->{a}, 'BSON::Code' );
    is( $hash->{a}->code, $code->code );
    is_deeply( $hash->{a}->scope, $code->scope );

    %h = ( a => 'foo', b => 'bar', c => 45 );
    $code = BSON::Code->new("function a(b,c){alert('OMG!')}", \%h);
    $bson = encode( { a => $code } );
    is_deeply(
        [ unpack "C*", $bson ],
        [
            81, 0,  0,  0,   15,  97,  0,   73,  0,   0,   0,   31,
            0,  0,  0,  102, 117, 110, 99,  116, 105, 111, 110, 32,
            97, 40, 98, 44,  99,  41,  123, 97,  108, 101, 114, 116,
            40, 39, 79, 77,  71,  33,  39,  41,  125, 0,   34,  0,
            0,  0,  2,  97,  0,   4,   0,   0,   0,   102, 111, 111,
            0,  2,  98, 0,   4,   0,   0,   0,   98,  97,  114, 0,
            16, 99, 0,  45,  0,   0,   0,   0,   0
        ],
        'Code'
    );

    $hash = decode( $bson );
    isa_ok( $hash->{a}, 'BSON::Code' );
    is( $hash->{a}->code, $code->code );
    is_deeply( $hash->{a}->scope, $code->scope );
};

subtest timestamp => sub {
    plan tests => 4;
    my $ts = BSON::Timestamp->new( 0x1234, 0x5678 );
    my $bson = encode( { a => $ts } );
    is_deeply(
        [ unpack "C*", $bson ],
        [ 16, 0, 0, 0, 17, 97, 0, 120, 86, 0, 0, 52, 18, 0, 0, 0 ],
        'timestamp encode'
    );

    my $hash = decode( $bson );
    isa_ok( $hash->{a}, 'BSON::Timestamp' );
    is( $hash->{a}->increment, $ts->increment, 'timestamp increment' );
    is( $hash->{a}->seconds, $ts->seconds, 'timestamp seconds' );
};

subtest options => sub {
    plan tests => 2;

    # ixhash
    my $hash = { a => 1, b => 2 };
    my $bson = encode($hash);
    my $h1   = decode($bson);
    my $h2   = decode( $bson, ixhash => 1 );
    is( ref tied %$h1, '',            'regular hash' );
    is( ref tied %$h2, 'Tie::IxHash', 'Tie::IxHash' );
};