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

use strict;
use warnings;

use Test::More;
use Test::Deep qw(cmp_details deep_diag bag);
use Data::Dump qw(pp);
use Test::Exception;
use Elastic::Model::SearchBuilder;

my $a = Elastic::Model::SearchBuilder->new;

test_filters(
    'SCALAR',

    'V',
    'v',
    { term => { _all => 'v' } },

    '\\V',
    \'v',
    'v',

);

test_filters(
    'KEY-VALUE PAIRS',

    'K: V',
    { k    => 'v' },
    { term => { k => 'v' } },

    'K: UNDEF',
    { k       => undef },
    { missing => { field => 'k' } },

    'K: \\V',
    { k => \'v' },
    { k => 'v' },

    'K: []',
    { k       => [] },
    { missing => { field => 'k' } },

    'K: [V]',
    { k    => ['v'] },
    { term => { k => 'v' } },

    'K: [V,V]',
    { k => [ 'v', 'v' ] },
    { terms => { k => [ 'v', 'v' ] } },

    'K: [UNDEF]',
    { k       => [undef] },
    { missing => { field => 'k' } },

    'K: [V,UNDEF]',
    { k  => [ 'v', undef ] },
    { or => [
            { term    => { k     => 'v' } },
            { missing => { field => 'k' } },
        ]
    },

    'K: [-and,V,UNDEF]',
    { k => [ '-and', 'v', undef ] },
    {   and =>
            bag( { missing => { field => 'k' } }, { term => { k => 'v' } }, )
    },

);

for my $op (qw(= term terms)) {
    test_filters(
        "FIELD OPERATOR: $op",

        "K: $op V",
        { k    => { $op => 'v' } },
        { term => { k   => 'v' } },

        "K: $op UNDEF",
        { k       => { $op   => undef } },
        { missing => { field => 'k' } },

        "K: $op [V]",
        { k    => { $op => ['v'] } },
        { term => { k   => 'v' } },

        "K: $op [V,V]",
        { k     => { $op => [ 'v', 'v' ] } },
        { terms => { k   => [ 'v', 'v' ] } },

        "K: $op [UNDEF]",
        { k       => { $op   => [undef] } },
        { missing => { field => 'k' } },

        "K: $op [V,UNDEF]",
        { k => { $op => [ 'v', undef ] } },
        {   or => [
                { term => { k => 'v' } }, { missing => { field => 'k' } },
            ]
        },

        'K: = [-and,V,UNDEF]',
        { k => { $op => [ '-and', 'v', undef ] } },
        {   or => [
                { term    => { k     => '-and' } },
                { term    => { k     => 'v' } },
                { missing => { field => 'k' } },
            ]
        },

        'K: {VV,ex}',
        { k => { $op => { value => [ 1, 2 ], execution => 'bool' } } },
        {   terms => {
                k         => [ 1, 2 ],
                execution => 'bool'
            }
        },

        'K: {V,ex}',
        { k    => { $op => { value => [1], execution => 'bool' } } },
        { term => { k   => 1 } },
    );
}

for my $op (qw(!= <> not_term not_terms)) {
    test_filters(
        "FIELD OPERATOR: $op",

        "K: $op V",
        { k => { $op => 'v' } },
        { not => { filter => { term => { k => 'v' } } } },

        "K: $op UNDEF",
        { k => { $op => undef } },
        { not => { filter => { missing => { field => 'k' } } } },

        "K: $op [V]",
        { k => { $op => ['v'] } },
        { not => { filter => { term => { k => 'v' } } } },

        "K: $op [V,V]",
        { k => { $op => [ 'v', 'v' ] } },
        { not => { filter => { terms => { k => [ 'v', 'v' ] } } } },

        "K: $op [UNDEF]",
        { k => { $op => [undef] } },
        { not => { filter => { missing => { field => 'k' } } } },

        "K: $op [V,UNDEF]",
        { k => { $op => [ 'v', undef ] } },
        {   not => {
                filter => {
                    or => [
                        { term    => { k     => 'v' } },
                        { missing => { field => 'k' } },
                    ]
                }
            }
        },

        'K: = [-and,V,UNDEF]',
        { k => { $op => [ '-and', 'v', undef ] } },
        {   not => {
                filter => {
                    or => [
                        { term    => { k     => '-and' } },
                        { term    => { k     => 'v' } },
                        { missing => { field => 'k' } },
                    ]
                }
            }
        },

    );
}

for my $op (qw(^ prefix)) {
    test_filters(
        "FIELD OPERATOR: $op",

        "K: $op V",
        { k      => { $op => 'v' } },
        { prefix => { k   => 'v' } },

        "K: $op UNDEF",
        { k => { $op => undef } },
        qr/ARRAYREF, SCALAR/,

        "K: $op [V]",
        { k      => { $op => ['v'] } },
        { prefix => { k   => 'v' } },

        "K: $op [V,V]",
        { k => { $op => [ 'v', 'v' ] } },
        { or => [ { prefix => { k => 'v' } }, { prefix => { k => 'v' } } ] },

        "K: $op [UNDEF]",
        { k => { $op => [undef] } },
        qr/ARRAYREF, SCALAR/,

        "K: $op [V,UNDEF]",
        { k => { $op => [ 'v', undef ] } },
        qr/ARRAYREF, SCALAR/,

        'K: = [-and,V,UNDEF]',
        { k => { $op => [ '-and', 'v', undef ] } },
        qr/ARRAYREF, SCALAR/,
    );
}

test_filters(
    "FIELD OPERATOR: not_prefix",

    "K: not_prefix V",
    { k   => { not_prefix => 'v' } },
    { not => { filter     => { prefix => { k => 'v' } } } },

    "K: not_prefix UNDEF",
    { k => { not_prefix => undef } },
    qr/ARRAYREF, SCALAR/,

    "K: not_prefix [V]",
    { k   => { not_prefix => ['v'] } },
    { not => { filter     => { prefix => { k => 'v' } } } },

    "K: not_prefix [V,V]",
    { k => { not_prefix => [ 'v', 'v' ] } },
    {   not => {
            filter => {
                or => [
                    { prefix => { k => 'v' } }, { prefix => { k => 'v' } }
                ]
            }
        }
    },

    "K: not_prefix [UNDEF]",
    { k => { not_prefix => [undef] } },
    qr/ARRAYREF, SCALAR/,

    "K: not_prefix [V,UNDEF]",
    { k => { not_prefix => [ 'v', undef ] } },
    qr/ARRAYREF, SCALAR/,

    'K: = [-and,V,UNDEF]',
    { k => { not_prefix => [ '-and', 'v', undef ] } },
    qr/ARRAYREF, SCALAR/,
);

my %range_map = (
    '<'  => 'lt',
    '<=' => 'lte',
    '>'  => 'gt',
    '>=' => 'gte'
);

for my $op (qw(< <= >= > gt gte lt lte)) {
    my ( $type, $es_op );

    if ( $es_op = $range_map{$op} ) {
        $type = 'numeric_range';
    }
    else {
        $type  = 'range';
        $es_op = $op;
    }

    test_filters(
        "FIELD OPERATOR: $op",

        "K: $op V",
        { k => { $op => 'v' } },
        { $type => { k => { $es_op => 'v' } } },

        "K: $op UNDEF",
        { $type => { $op => undef } },
        qr/SCALAR/,

        "K: $op [V]",
        { k => { $op => ['v'] } },
        qr/SCALAR/,

        "K: $op [V,V]",
        { k => { $op => [ 'v', 'v' ] } },
        qr/SCALAR/,

        "K: $op [UNDEF]",
        { k => { $op => [undef] } },
        qr/SCALAR/,

        "K: $op [V,UNDEF]",
        { k => { $op => [ 'v', undef ] } },
        qr/SCALAR/,

        'K: = [-and,V,UNDEF]',
        { k => { $op => [ '-and', 'v', undef ] } },
        qr/SCALAR/,

        'K[$op 5],K[$op 10]',
        { k => [ -and => { '>' => 5 }, { '>' => 10 } ] },
        qr/Duplicate/,
    );
}

test_filters(
    "COMBINED RANGE OPERATORS",

    "K: gt gte lt lte < <= > >= V",
    {   k => {
            gt   => 'v',
            gte  => 'v',
            lt   => 'v',
            lte  => 'v',
            '>'  => 'V',
            '>=' => 'V',
            '<'  => 'V',
            '<=' => 'V'
        }
    },
    {   and => bag( {
                numeric_range =>
                    { k => { gt => 'V', gte => 'V', lt => 'V', lte => 'V' } }
            },
            {   range =>
                    { k => { gt => 'v', gte => 'v', lt => 'v', lte => 'v' } }
            },
        )
    },

    "K: [gt gte lt lte < <= > >=] V",
    {   k => [
            { gt   => 'v' },
            { gte  => 'v' },
            { lt   => 'v' },
            { lte  => 'v' },
            { '>'  => 'V' },
            { '>=' => 'V' },
            { '<'  => 'V' },
            { '<=' => 'V' }
        ]
    },
    {   or => [
            { range         => { k => { gt  => "v" } } },
            { range         => { k => { gte => "v" } } },
            { range         => { k => { lt  => "v" } } },
            { range         => { k => { lte => "v" } } },
            { numeric_range => { k => { gt  => "V" } } },
            { numeric_range => { k => { gte => "V" } } },
            { numeric_range => { k => { lt  => "V" } } },
            { numeric_range => { k => { lte => "V" } } },
        ],
    },

);

test_filters(
    "FIELD OPERATORS: missing/exists",

    "K: exists 1",
    { k      => { exists => 1 } },
    { exists => { field  => 'k' } },

    "K: exists 0",
    { k       => { exists => 0 } },
    { missing => { field  => 'k' } },

    "K: exists UNDEF",
    { k       => { exists => undef } },
    { missing => { field  => 'k' } },

    "K: missing 1",
    { k       => { missing => 1 } },
    { missing => { field   => 'k' } },

    "K: missing 0",
    { k      => { missing => 0 } },
    { exists => { field   => 'k' } },

    "K: missing UNDEF",
    { k      => { missing => undef } },
    { exists => { field   => 'k' } },

    "K: not_missing HASH",
    { k => { missing => { null_value => 1, existence => 1 } } },
    { missing => { field => 'k', null_value => 1, existence => 1 } },

    "K: not_exists 1",
    { k       => { not_exists => 1 } },
    { missing => { field      => 'k' } },

    "K: not_exists 0",
    { k      => { not_exists => 0 } },
    { exists => { field      => 'k' } },

    "K: not_exists UNDEF",
    { k      => { not_exists => undef } },
    { exists => { field      => 'k' } },

    "K: not_missing 1",
    { k   => { not_missing => 1 } },
    { not => { filter      => { missing => { field => 'k' } } } },

    "K: not_missing 0",
    { k   => { not_missing => 0 } },
    { not => { filter      => { exists => { field => 'k' } } } },

    "K: not_missing UNDEF",
    { k   => { not_missing => undef } },
    { not => { filter      => { exists => { field => 'k' } } } },

    "K: not_missing HASH",
    { k => { not_missing => { null_value => 1, existence => 1 } } },
    {   not => {
            filter => {
                missing => { field => 'k', null_value => 1, existence => 1 }
            }
        }
    },

);

test_filters(
          "FIELD OPERATORS: geo_distance, geo_distance_range, "
        . "geo_bounding_box, geo_polygon",

    'K: geo_distance %V',
    {   k => {
            geo_distance => {
                location      => 'LAT,LON',
                distance      => '10km',
                normalize     => 0,
                optimize_bbox => 'indexed',
            }
        }
    },
    {   geo_distance => {
            k             => 'LAT,LON',
            distance      => '10km',
            normalize     => 0,
            optimize_bbox => 'indexed'
        }
    },

    'K: geo_distance FOO',
    { k => { geo_distance => 'FOO' } },
    qr/hashref/,

    'K: geo_distance_range %V',
    {   k => {
            geo_distance_range => {
                location      => 'LAT,LON',
                'gt'          => '10km',
                'lt'          => '10km',
                normalize     => 0,
                optimize_bbox => 'indexed',
            },
        }
    },
    {   geo_distance_range => {
            k             => 'LAT,LON',
            gt            => '10km',
            lt            => '10km',
            normalize     => 0,
            optimize_bbox => 'indexed',
        }
    },

    'K: geo_distance_range FOO',
    { k => { geo_distance => 'FOO' } },
    qr/hashref/,

    'K: geo_bbox %V',
    {   k => {
            geo_bbox => {
                top_left     => 'LAT,LON',
                bottom_right => 'LAT,LON',
                normalize    => 0,
                type         => 'indexed',
            },
        }
    },
    {   geo_bounding_box => {
            k => {
                bottom_right => 'LAT,LON',
                top_left     => 'LAT,LON',
                normalize    => 0,
                type         => 'indexed',
            }
        }
    },

    'K: geo_bbox FOO',
    { k => { geo_bbox => 'FOO' } },
    qr/hashref/,

    'K: geo_bounding_box %V',
    {   k => {
            geo_bounding_box => {
                top_left     => 'LAT,LON',
                bottom_right => 'LAT,LON',
                normalize    => 0,
                type         => 'indexed',
            },
        }
    },
    {   geo_bounding_box => {
            k => {
                bottom_right => 'LAT,LON',
                top_left     => 'LAT,LON',
                normalize    => 0,
                type         => 'indexed',
            }
        }
    },

    'K: geo_bounding_box FOO',
    { k => { geo_bounding_box => 'FOO' } },
    qr/hashref/,

    'K: geo_polygon @V',
    { k => { geo_polygon => [ 'LAT,LON', 'LAT,LON' ] } },
    { geo_polygon => { k => { points => [ 'LAT,LON', 'LAT,LON' ] } } },

    'K: geo_polygon {}',
    {   k => {
            geo_polygon =>
                { points => [ 'LAT,LON', 'LAT,LON' ], normalize => 0 }
        }
    },
    {   geo_polygon =>
            { k => { points => [ 'LAT,LON', 'LAT,LON' ], normalize => 0 } }
    },

    'K: geo_polygon FOO',
    { k => { geo_polygon => 'FOO' } },
    qr/ARRAYREF/,

);

test_filters(
          "FIELD OPERATORS: not_geo_distance, not_geo_distance_range, "
        . "not_geo_bounding_box, not_geo_polygon",

    'K: not_geo_distance %V',
    {   k => {
            not_geo_distance => {
                location      => 'LAT,LON',
                distance      => '10km',
                normalize     => 0,
                optimize_bbox => 'indexed',
            }
        }
    },
    {   not => {
            filter => {
                geo_distance => {
                    k             => 'LAT,LON',
                    distance      => '10km',
                    normalize     => 0,
                    optimize_bbox => 'indexed'
                }
            }
        }
    },

    'K: not_geo_distance_range %V',
    {   k => {
            not_geo_distance_range => {
                location      => 'LAT,LON',
                'gt'          => '10km',
                'lt'          => '10km',
                normalize     => 0,
                optimize_bbox => 'indexed'
            },
        }
    },
    {   not => {
            filter => {
                geo_distance_range => {
                    k             => 'LAT,LON',
                    gt            => '10km',
                    lt            => '10km',
                    normalize     => 0,
                    optimize_bbox => 'indexed'
                }
            }
        }
    },

    'K: not_geo_bounding_box %V',
    {   k => {
            not_geo_bounding_box => {
                top_left     => 'LAT,LON',
                bottom_right => 'LAT,LON',
                normalize    => 0,
                type         => 'indexed',
            },
        }
    },
    {   not => {
            filter => {
                geo_bounding_box => {
                    k => {
                        bottom_right => 'LAT,LON',
                        top_left     => 'LAT,LON',
                        normalize    => 0,
                        type         => 'indexed',
                    }
                }
            }
        }
    },

    'K: not_geo_polygon @V',
    { k => { not_geo_polygon => [ 'LAT,LON', 'LAT,LON' ] } },
    {   not => {
            filter => {
                geo_polygon => { k => { points => [ 'LAT,LON', 'LAT,LON' ] } }
            }
        }
    },

    'K: not_geo_polygon {}',
    {   k => {
            not_geo_polygon =>
                { points => [ 'LAT,LON', 'LAT,LON' ], normalize => 0 }
        }
    },
    {   not => {
            filter => {
                geo_polygon => {
                    k => {
                        points    => [ 'LAT,LON', 'LAT,LON' ],
                        normalize => 0
                    }
                }
            }
        }
    },

);

done_testing();

#===================================
sub test_filters {
#===================================
    note "\n" . shift();
    while (@_) {
        my $name = shift;
        my $in   = shift;
        my $out  = shift;
        if ( ref $out eq 'Regexp' ) {
            throws_ok { $a->filter($in) } $out, $name;
            next;
        }

        my $got = $a->filter($in);
        my $expect = { filter => $out };
        my ( $ok, $stack ) = cmp_details( $got, $expect );

        if ($ok) {
            pass $name;
            next;
        }

        fail($name);

        note("Got:");
        note( pp($got) );
        note("Expected:");
        note( pp($expect) );

        diag( deep_diag($stack) );

    }
}