use 5.008001;
use strict;
use warnings;
use utf8;

use Test::More 0.96;

binmode( Test::More->builder->$_, ":utf8" )
  for qw/output failure_output todo_output/;

use lib 't/lib';
use lib 't/pvtlib';
use CleanEnv;
use TestUtils;

use BSON;
use BSON::Types ':all';

sub _BSON { BSON->new(@_) }

subtest "error_callback" => sub {
    my $bad = "\x05\x00\x00\x00\x01";
    my @errs;
    my $obj = _BSON( error_callback => sub { push @errs, [@_] } );
    $obj->decode_one($bad);
    is( 0+ @errs, 1, "error_callback ran" );
    # 'error reading' is from BSON::XS
    like( $errs[0][0], qr/error reading|not null terminated/i, "error_callback arg 0" );
    is( ${ $errs[0][1] }, $bad,         "error_callback arg 1" );
    is( $errs[0][2],      'decode_one', "error_callback arg 2" );
};

subtest "invalid_char" => sub {
    my $obj = _BSON( invalid_chars => '.' );
    eval { $obj->encode_one( { "example.com" => 1 } ) };
    like(
        $@,
        qr/key 'example\.com' has invalid character\(s\) '\.'/,
        "invalid char throws exception"
    );

    $obj = _BSON( invalid_chars => '.$' );
    eval { $obj->encode_one( { "example.c\$om" => 1 } ) };
    like(
        $@,
        qr/key 'example\.c\$om' has invalid character\(s\) '\.\$'/,
        "multi-invalid chars throws exception"
    );
};

subtest "max_length" => sub {
    my $obj = _BSON( max_length => 20 );

    my $hash = { "example.com" => "a" x 100 };
    my $encoded = _BSON->encode_one($hash);

    eval { $obj->encode_one($hash) };
    like(
        $@,
        qr/encode_one.*Document exceeds maximum size 20/,
        "max_length exceeded during encode_one"
    );

    eval { $obj->decode_one($encoded) };
    like(
        $@,
        qr/decode_one.*Document exceeds maximum size 20/,
        "max_length exceeded during decode_one"
    );
};

subtest "op-char" => sub {
    my $obj = _BSON( op_char => '-' );

    my $hash   = { -inc   => { x => 1 } };
    my $expect = { '$inc' => { x => 1 } };
    my $got    = $obj->decode_one( $obj->encode_one($hash) );

    is_deeply( $got, $expect, "op-char converts to '\$'" )
      or diag explain $got;
};

subtest "prefer_numeric" => sub {
    my $hash = { x => "42" };

    my $pn0 = _BSON( prefer_numeric => 0 );
    my $pn1 = _BSON( prefer_numeric => 1 );
    my $dec = _BSON( wrap_numbers   => 1, wrap_strings => 1 );

    is( ref( $dec->decode_one( $pn1->encode_one($hash) )->{x} ),
        'BSON::Int32', 'prefer_numeric => 1' );
    is( ref( $dec->decode_one( $pn0->encode_one($hash) )->{x} ),
        'BSON::String', 'prefer_numeric => 0' );
};

subtest "first_key" => sub {
    my @doc = ( x => 42, y => 23, z => { a => 1, b => 2 } );

    my $obj = _BSON( ordered => 1 );

    my $got =
      $obj->decode_one(
        $obj->encode_one( bson_doc(@doc), { first_key => 'y', first_value => 32 } ) );

    my ( $k, $v ) = each %$got;

    is( $k, 'y', "first_key put first" );
    is( $v, 32,  "first_value overrode existing value" );
    ok( !exists $got->{z}{_id}, "first_key doesn't propagate" );

    # empty doc with first_key
    $got =
      $obj->decode_one(
        $obj->encode_one( bson_doc(), { first_key => 'y', first_value => 32 } ) );
    ( $k, $v ) = each %$got;

    is( $k, 'y', "first_key put first" );
    is( $v, 32,  "first_value overrode existing value" );
};

subtest "dt_type" => sub {
    my $now = time;

    # undef
    {
        my $obj = _BSON( dt_type => undef );
        my $bson = $obj->encode_one( { A => bson_time() } );
        my $hash = $obj->decode_one($bson);
        is( ref( $hash->{A} ), 'BSON::Time', "dt_type = undef" );
    }

    # BSON::Time
    {
        my $obj = _BSON( dt_type => "BSON::Time" );
        my $bson = $obj->encode_one( { A => bson_time() } );
        my $hash = $obj->decode_one($bson);
        is( ref( $hash->{A} ), 'BSON::Time', "dt_type = BSON::Time" );
    }

    # DateTime
    SKIP: {
        eval { require DateTime };
        skip( "DateTime not installed", 1 )
          unless $INC{'DateTime.pm'};

        my $obj = _BSON( dt_type => "DateTime" );
        my $bson = $obj->encode_one( { A => bson_time() } );
        my $hash = $obj->decode_one($bson);
        is( ref( $hash->{A} ), 'DateTime', "dt_type = DateTime" );
    }

    # DateTime::Tiny
    SKIP: {
        eval { require DateTime::Tiny };
        skip( "DateTime::Tiny not installed", 1 )
          unless $INC{'DateTime/Tiny.pm'};

        my $obj = _BSON( dt_type => "DateTime::Tiny" );
        my $bson = $obj->encode_one( { A => bson_time() } );
        my $hash = $obj->decode_one($bson);
        is( ref( $hash->{A} ), 'DateTime::Tiny', "dt_type = DateTime::Tiny" );
    }

    # Time::Moment
    SKIP: {
        eval { require Time::Moment };
        skip( "Time::Moment not installed", 1 )
          unless $INC{'Time/Moment.pm'};

        my $obj = _BSON( dt_type => "Time::Moment" );
        my $bson = $obj->encode_one( { A => bson_time() } );
        my $hash = $obj->decode_one($bson);
        is( ref( $hash->{A} ), 'Time::Moment', "dt_type = Time::Moment" );
    }

    # Mango::BSON::Time
    SKIP: {
        eval { require Mango::BSON::Time };
        skip( "Mango::BSON::Time not installed", 1 )
          unless $INC{'Mango/BSON/Time.pm'};

        my $obj = _BSON( dt_type => "Mango::BSON::Time" );
        my $bson = $obj->encode_one( { A => bson_time() } );
        my $hash = $obj->decode_one($bson);
        is( ref( $hash->{A} ), 'Mango::BSON::Time', "dt_type = Mango::BSON::Time" );
    }

    # unknown
    {
        my $obj = _BSON( dt_type => 'BOGUS' );
        my $bson = $obj->encode_one( { A => bson_time() } );
        eval { $obj->decode_one($bson) };
        like( $@, qr/unsupported dt_type/i, "dt_type = BOGUS" );
    }

};

done_testing;

#
# This file is part of BSON
#
# This software is Copyright (c) 2018 by Stefan G. and MongoDB, Inc.
#
# This is free software, licensed under:
#
#   The Apache License, Version 2.0, January 2004
#
#
# vim: set ts=4 sts=4 sw=4 et tw=75:

