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

use Test::More qw(no_plan);

use Web::Simple::DispatchParser;

my $dp = Web::Simple::DispatchParser->new;

{
   my $get = $dp->parse_dispatch_specification('GET');

   is_deeply(
     [ $get->({ REQUEST_METHOD => 'GET' }) ],
     [ {} ],
     'GET matches'
   );

   is_deeply(
     [ $get->({ REQUEST_METHOD => 'POST' }) ],
     [],
     'POST does not match'
   );
}

ok(
  !eval { $dp->parse_dispatch_specification('GET POST'); 1; },
  "Don't yet allow two methods"
);

{
   my $html = $dp->parse_dispatch_specification('.html');

   is_deeply(
     [ $html->({ PATH_INFO => '/foo/bar.html' }) ],
     [ { PATH_INFO => '/foo/bar' } ],
     '.html matches'
   );

   is_deeply(
     [ $html->({ PATH_INFO => '/foo/bar.xml' }) ],
     [],
     '.xml does not match .html'
   );
}

{
   my $any_ext = $dp->parse_dispatch_specification('.*');

   is_deeply(
     [ $any_ext->({ PATH_INFO => '/foo/bar.html' }) ],
     [ { PATH_INFO => '/foo/bar' }, 'html' ],
     '.html matches .* and extension returned'
   );

   is_deeply(
     [ $any_ext->({ PATH_INFO => '/foo/bar' }) ],
     [],
     'no extension does not match .*'
   );
}

{
   my $slash = $dp->parse_dispatch_specification('/');

   is_deeply(
     [ $slash->({ PATH_INFO => '/' }) ],
     [ {} ],
     '/ matches /'
   );

   is_deeply(
     [ $slash->({ PATH_INFO => '/foo' }) ],
     [ ],
     '/foo does not match /'
   );
}

{
   my $post = $dp->parse_dispatch_specification('/post/*');

   is_deeply(
     [ $post->({ PATH_INFO => '/post/one' }) ],
     [ {}, 'one' ],
     '/post/one parses out one'
   );

   is_deeply(
     [ $post->({ PATH_INFO => '/post/one/' }) ],
     [],
     '/post/one/ does not match'
   );
}

{
   my $combi = $dp->parse_dispatch_specification('GET+/post/*');

   is_deeply(
     [ $combi->({ PATH_INFO => '/post/one', REQUEST_METHOD => 'GET' }) ],
     [ {}, 'one' ],
     '/post/one parses out one'
   );

   is_deeply(
     [ $combi->({ PATH_INFO => '/post/one/', REQUEST_METHOD => 'GET' }) ],
     [],
     '/post/one/ does not match'
   );

   is_deeply(
     [ $combi->({ PATH_INFO => '/post/one', REQUEST_METHOD => 'POST' }) ],
     [],
     'POST /post/one does not match'
   );
}

{
   my $or = $dp->parse_dispatch_specification('GET|POST');

   foreach my $meth (qw(GET POST)) {

     is_deeply(
       [ $or->({ REQUEST_METHOD => $meth }) ],
       [ {} ],
       'GET|POST matches method '.$meth
     );
   }

   is_deeply(
     [ $or->({ REQUEST_METHOD => 'PUT' }) ],
     [],
     'GET|POST does not match PUT'
   );
}

{
   my $or = $dp->parse_dispatch_specification('GET|POST|DELETE');

   foreach my $meth (qw(GET POST DELETE)) {

     is_deeply(
       [ $or->({ REQUEST_METHOD => $meth }) ],
       [ {} ],
       'GET|POST|DELETE matches method '.$meth
     );
   }

   is_deeply(
     [ $or->({ REQUEST_METHOD => 'PUT' }) ],
     [],
     'GET|POST|DELETE does not match PUT'
   );
}

{
   my $nest = $dp->parse_dispatch_specification('(GET+/foo)|POST');

   is_deeply(
     [ $nest->({ PATH_INFO => '/foo', REQUEST_METHOD => 'GET' }) ],
     [ {} ],
     '(GET+/foo)|POST matches GET /foo'
   );

   is_deeply(
     [ $nest->({ PATH_INFO => '/bar', REQUEST_METHOD => 'GET' }) ],
     [],
     '(GET+/foo)|POST does not match GET /bar'
   );

   is_deeply(
     [ $nest->({ PATH_INFO => '/bar', REQUEST_METHOD => 'POST' }) ],
     [ {} ],
     '(GET+/foo)|POST matches POST /bar'
   );

   is_deeply(
     [ $nest->({ PATH_INFO => '/foo', REQUEST_METHOD => 'PUT' }) ],
     [],
     '(GET+/foo)|POST does not match PUT /foo'
   );
}

{
  local $@;
  ok(
    !eval { $dp->parse_dispatch_specification('/foo+(GET'); 1 },
    'Death with missing closing )'
  );
  my $err = q{
    /foo+(GET
         ^
  };
  (s/^\n//s,s/\n  $//s,s/^    //mg) for $err;
  like(
    $@,
    qr{\Q$err\E},
    "Error $@ matches\n${err}\n"
  );
}

{
   my $not = $dp->parse_dispatch_specification('!.html+.*');

   is_deeply(
     [ $not->({ PATH_INFO => '/foo.xml' }) ],
     [ { PATH_INFO => '/foo' }, 'xml' ],
     '!.html+.* matches /foo.xml'
   );

   is_deeply(
     [ $not->({ PATH_INFO => '/foo.html' }) ],
     [],
     '!.html+.* does not match /foo.html'
   );

   is_deeply(
     [ $not->({ PATH_INFO => '/foo' }) ],
     [],
     '!.html+.* does not match /foo'
   );
}

{
   my $sub = $dp->parse_dispatch_specification('/foo/*/...');

   is_deeply(
     [ $sub->({ PATH_INFO => '/foo/1/bar' }) ],
     [ { PATH_INFO => '/bar' }, 1 ],
     '/foo/*/... matches /foo/1/bar and strips to /bar'
   );

   is_deeply(
     [ $sub->({ PATH_INFO => '/foo/1/' }) ],
     [ { PATH_INFO => '/' }, 1 ],
     '/foo/*/... matches /foo/1/bar and strips to /'
   );

   is_deeply(
     [ $sub->({ PATH_INFO => '/foo/1' }) ],
     [],
     '/foo/*/... does not match /foo/1 (no trailing /)'
   );
}

#
# query string
#

my $q = 'foo=FOO&bar=BAR1&baz=one+two&quux=QUUX1&quux=QUUX2'
    .'&bar=BAR2&quux=QUUX3&evil=%2F';

my %all_single = (
    foo => 'FOO',
    bar => 'BAR2',
    baz => 'one two',
    quux => 'QUUX3',
    evil => '/',
);

my %all_multi = (
    foo => [ 'FOO' ],
    bar => [ qw(BAR1 BAR2) ],
    baz => [ 'one two' ],
    quux => [ qw(QUUX1 QUUX2 QUUX3) ],
    evil => [ '/' ],
);

foreach my $lose ('?foo=','?:foo=','?@foo=','?:@foo=') {
    my $foo = $dp->parse_dispatch_specification($lose);

    is_deeply(
        [ $foo->({ QUERY_STRING => '' }) ],
        [],
        "${lose} fails with no query"
    );

    is_deeply(
        [ $foo->({ QUERY_STRING => 'bar=baz' }) ],
        [],
        "${lose} fails with query missing foo key"
    );
}

foreach my $win (
    [ '?foo=' => 'FOO' ],
    [ '?:foo=' => { foo => 'FOO' } ],
    [ '?spoo~' => undef ],
    [ '?:spoo~' => {} ],
    [ '?@spoo~' => [] ],
    [ '?:@spoo~' => { spoo => [] } ],
    [ '?bar=' => 'BAR2' ],
    [ '?:bar=' => { bar => 'BAR2' } ],
    [ '?@bar=' => [ qw(BAR1 BAR2) ] ],
    [ '?:@bar=' => { bar => [ qw(BAR1 BAR2) ] } ],
    [ '?foo=&@bar=' => 'FOO', [ qw(BAR1 BAR2) ] ],
    [ '?foo=&:@bar=' => 'FOO', { bar => [ qw(BAR1 BAR2) ] } ],
    [ '?:foo=&:@bar=' => { foo => 'FOO', bar => [ qw(BAR1 BAR2) ] } ],
    [ '?:baz=&:evil=' => { baz => 'one two', evil => '/' } ],
    [ '?*' => \%all_single ],
    [ '?@*' => \%all_multi ],
    [ '?foo=&@*' => 'FOO', do { my %h = %all_multi; delete $h{foo}; \%h } ],
    [ '?:foo=&@*' => { %all_multi, foo => 'FOO' } ],
    [ '?:@bar=&*' => { %all_single, bar => [ qw(BAR1 BAR2) ] } ],
) {
    my ($spec, @res) = @$win;
    my $match = $dp->parse_dispatch_specification($spec);
    #use Data::Dump::Streamer; warn Dump($match);
    is_deeply(
        [ $match->({ QUERY_STRING => $q }) ],
        [ {}, @res ],
        "${spec} matches correctly"
    );
}

#
# /path/info/ + query string
#

foreach my $lose2 ('/foo/bar/+?foo=','/foo/bar/+?:foo=','/foo/bar/+?@foo=','/foo/bar/+?:@foo=') {
    my $foo = $dp->parse_dispatch_specification($lose2);

    is_deeply(
        [ $foo->({ PATH_INFO => '/foo/bar/', QUERY_STRING => '' }) ],
        [ ],
        "${lose2} fails with no query"
    );

    is_deeply(
        [ $foo->({ PATH_INFO => '/foo/bar/', QUERY_STRING => 'bar=baz' }) ],
        [ ],
        "${lose2} fails with query missing foo key"
    );
}

foreach my $win2 (
    [ '/foo/bar/+?foo=' => 'FOO' ],
    [ '/foo/bar/+?:foo=' => { foo => 'FOO' } ],
    [ '/foo/bar/+?spoo~' => undef ],
    [ '/foo/bar/+?:spoo~' => {} ],
    [ '/foo/bar/+?@spoo~' => [] ],
    [ '/foo/bar/+?:@spoo~' => { spoo => [] } ],
    [ '/foo/bar/+?bar=' => 'BAR2' ],
    [ '/foo/bar/+?:bar=' => { bar => 'BAR2' } ],
    [ '/foo/bar/+?@bar=' => [ qw(BAR1 BAR2) ] ],
    [ '/foo/bar/+?:@bar=' => { bar => [ qw(BAR1 BAR2) ] } ],
    [ '/foo/bar/+?foo=&@bar=' => 'FOO', [ qw(BAR1 BAR2) ] ],
    [ '/foo/bar/+?foo=&:@bar=' => 'FOO', { bar => [ qw(BAR1 BAR2) ] } ],
    [ '/foo/bar/+?:foo=&:@bar=' => { foo => 'FOO', bar => [ qw(BAR1 BAR2) ] } ],
    [ '/foo/bar/+?:baz=&:evil=' => { baz => 'one two', evil => '/' } ],
    [ '/foo/bar/+?*' => \%all_single ],
    [ '/foo/bar/+?@*' => \%all_multi ],
    [ '/foo/bar/+?foo=&@*' => 'FOO', do { my %h = %all_multi; delete $h{foo}; \%h } ],
    [ '/foo/bar/+?:foo=&@*' => { %all_multi, foo => 'FOO' } ],
    [ '/foo/bar/+?:@bar=&*' => { %all_single, bar => [ qw(BAR1 BAR2) ] } ],
) {
    my ($spec, @res) = @$win2;
    my $match = $dp->parse_dispatch_specification($spec);
    # use Data::Dump::Streamer; warn Dump($match);
    is_deeply(
        [ $match->({ PATH_INFO => '/foo/bar/', QUERY_STRING => $q }) ],
        [ {}, @res ],
        "${spec} matches correctly"
    );
}

#
# /path/info + query string
#

foreach my $lose3 ('/foo/bar+?foo=','/foo/bar+?:foo=','/foo/bar+?@foo=','/foo/bar+?:@foo=') {
    my $foo = $dp->parse_dispatch_specification($lose3);

    is_deeply(
        [ $foo->({ PATH_INFO => '/foo/bar', QUERY_STRING => '' }) ],
        [ ],
        "${lose3} fails with no query"
    );

    is_deeply(
        [ $foo->({ PATH_INFO => '/foo/bar', QUERY_STRING => 'bar=baz' }) ],
        [ ],
        "${lose3} fails with query missing foo key"
    );
}

foreach my $win3 (
    [ '/foo/bar+?foo=' => 'FOO' ],
    [ '/foo/bar+?:foo=' => { foo => 'FOO' } ],
    [ '/foo/bar+?spoo~' => undef ],
    [ '/foo/bar+?:spoo~' => {} ],
    [ '/foo/bar+?@spoo~' => [] ],
    [ '/foo/bar+?:@spoo~' => { spoo => [] } ],
    [ '/foo/bar+?bar=' => 'BAR2' ],
    [ '/foo/bar+?:bar=' => { bar => 'BAR2' } ],
    [ '/foo/bar+?@bar=' => [ qw(BAR1 BAR2) ] ],
    [ '/foo/bar+?:@bar=' => { bar => [ qw(BAR1 BAR2) ] } ],
    [ '/foo/bar+?foo=&@bar=' => 'FOO', [ qw(BAR1 BAR2) ] ],
    [ '/foo/bar+?foo=&:@bar=' => 'FOO', { bar => [ qw(BAR1 BAR2) ] } ],
    [ '/foo/bar+?:foo=&:@bar=' => { foo => 'FOO', bar => [ qw(BAR1 BAR2) ] } ],
    [ '/foo/bar+?:baz=&:evil=' => { baz => 'one two', evil => '/' } ],
    [ '/foo/bar+?*' => \%all_single ],
    [ '/foo/bar+?@*' => \%all_multi ],
    [ '/foo/bar+?foo=&@*' => 'FOO', do { my %h = %all_multi; delete $h{foo}; \%h } ],
    [ '/foo/bar+?:foo=&@*' => { %all_multi, foo => 'FOO' } ],
    [ '/foo/bar+?:@bar=&*' => { %all_single, bar => [ qw(BAR1 BAR2) ] } ],
) {
    my ($spec, @res) = @$win3;
    my $match = $dp->parse_dispatch_specification($spec);
    # use Data::Dump::Streamer; warn Dump($match);
    is_deeply(
        [ $match->({ PATH_INFO => '/foo/bar', QUERY_STRING => $q }) ],
        [ {}, @res ],
        "${spec} matches correctly"
    );
}