### ----------------------------------------------------
### -- Cro::HTTP
### -- Licenses: Artistic-2.0
### -- Authors: Jonathan Worthington <jnthn@jnthn.net>
### -- File: t/http-router.rakutest
### ----------------------------------------------------
### -- Chunk 1 of 3
use Cro;
use Cro::HTTP::Request;
use Cro::HTTP::Router;
use Test;
use lib 't/TestModule';
use TestModule;

sub body-text(Cro::HTTP::Response $r) {
    $r.body-byte-stream.list.map(*.decode('utf-8')).join
}

{
    my $app = route -> { }
    ok $app ~~ Cro::Transform, 'Route block with no routes gives back a Cro::Transform';
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    $source.emit(Cro::HTTP::Request.new(:method<GET>, :target</>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Empty route set gives a response';
        is $r.status, '404', 'Status code from empty route set is 404';
    }
}

throws-like { request }, X::Cro::HTTP::Router::OnlyInHandler, what => 'request',
    'Can only use request term inside of a handler';
throws-like { response }, X::Cro::HTTP::Router::OnlyInHandler, what => 'response',
    'Can only use response term inside of a handler';
throws-like { created 'foo' }, X::Cro::HTTP::Router::OnlyInHandler, what => 'created',
    'Can only use created term inside of a handler';
throws-like { not-found }, X::Cro::HTTP::Router::OnlyInHandler, what => 'not-found',
    'Can only use not-found term inside of a handler';
throws-like { forbidden }, X::Cro::HTTP::Router::OnlyInHandler, what => 'forbidden',
    'Can only use forbidden term inside of a handler';
throws-like { redirect 'foo' }, X::Cro::HTTP::Router::OnlyInHandler, what => 'redirected',
    'Can only use redirect term inside of a handler';
throws-like { conflict }, X::Cro::HTTP::Router::OnlyInHandler, what => 'conflict',
    'Can only use conflict term inside of a handler';
throws-like { i'm-a-teapot }, X::Cro::HTTP::Router::OnlyInHandler, what => "i'm-a-teapot",
    "Can only use i'm-a-teapot term inside of a handler";
throws-like { bad-request }, X::Cro::HTTP::Router::OnlyInHandler, what => 'bad-request',
    'Can only use bad-request term inside of a handler';

{
    my $app = route {
        get -> {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body('Hello, world'.encode('ascii'));
        }

        get -> 'about' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body('We are the awesome'.encode('ascii'));
        }

        get -> 'company', 'careers' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body('No jobs, kthxbai'.encode('ascii'));
        }
    }
    ok $app ~~ Cro::Transform, 'Route block with routes gives back a Cro::Transform';
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;

    $source.emit(Cro::HTTP::Request.new(:method<GET>, :target</>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Route set routes / correctly';
        is $r.status, 200, 'Got 200 response';
        is $r.header('Content-type'), 'text/html', 'Got expected header';
        is-deeply body-text($r), 'Hello, world', 'Got expected body';
    }

    $source.emit(Cro::HTTP::Request.new(:method<GET>, :target</about>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Route set routes /about correctly';
        is $r.status, 200, 'Got 200 response';
        is $r.header('Content-type'), 'text/html', 'Got expected header';
        is-deeply body-text($r), 'We are the awesome', 'Got expected body';
    }

    $source.emit(Cro::HTTP::Request.new(:method<GET>, :target</company/careers>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Route set routes /company/careers correctly';
        is $r.status, 200, 'Got 200 response';
        is $r.header('Content-type'), 'text/html', 'Got expected header';
        is-deeply body-text($r), 'No jobs, kthxbai', 'Got expected body';
    }

    $source.emit(Cro::HTTP::Request.new(:method<GET>, :target</wat>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'No matching route gets a HTTP response';
        is $r.status, '404', 'Status code when no matching route is 404';
    }
}

{
    my $app = route {
        get -> 'product' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body('A GET request'.encode('ascii'));
        }

        post -> 'product' {
            response.status = 201;
            response.append-header('Content-type', 'text/html');
            response.set-body('A POST request'.encode('ascii'));
        }

        put -> 'product' {
            response.status = 204;
        }

        delete -> 'product' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body('A DELETE request'.encode('ascii'));
        }
        patch -> 'product' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body('A PATCH request'.encode('ascii'));
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;

    $source.emit(Cro::HTTP::Request.new(:method<GET>, :target</product>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Route set routes GET';
        is $r.status, 200, 'Got 200 response';
        is $r.header('Content-type'), 'text/html', 'Got expected header';
        is-deeply body-text($r), 'A GET request', 'Got expected body';
    }

    $source.emit(Cro::HTTP::Request.new(:method<POST>, :target</product>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Route set routes POST';
        is $r.status, 201, 'Got 201 response';
        is $r.header('Content-type'), 'text/html', 'Got expected header';
        is-deeply body-text($r), 'A POST request', 'Got expected body';
    }

    $source.emit(Cro::HTTP::Request.new(:method<PUT>, :target</product>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Route set routes PUT';
        is $r.status, 204, 'Got 204 response';
    }

    $source.emit(Cro::HTTP::Request.new(:method<DELETE>, :target</product>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Route set routes DELETE';
        is $r.status, 200, 'Got 200 response';
        is $r.header('Content-type'), 'text/html', 'Got expected header';
        is-deeply body-text($r), 'A DELETE request', 'Got expected body';
    }

    $source.emit(Cro::HTTP::Request.new(:method<PATCH>, :target</product>));
    given $responses.receive -> $r {
        ok $r ~~ Cro::HTTP::Response, 'Route set routes PATCH';
        is $r.status, 200, 'Got 200 response';
        is $r.header('Content-type'), 'text/html', 'Got expected header';
        is-deeply body-text($r), 'A PATCH request', 'Got expected body';
    }
}

{
    my $app = route {
        get -> 'product', $uuid {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("product $uuid".encode('ascii'));
        }

        get -> 'category', Any $uuid {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("category $uuid".encode('ascii'));
        }

        get -> 'user', Str $uuid {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("user $uuid".encode('ascii'));
        }

        get -> 'user', 'posts' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("user posts".encode('ascii'));
        }

        get -> 'product', Str $uuid, 'reviews' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reviews $uuid".encode('ascii'));
        }

        get -> 'product', Str $uuid, 'reviews', Int $page {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reviews $uuid page $page".encode('ascii'));
        }

        get -> 'tree', *@path {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body(@path.join(",").encode('ascii'));
        }

        get -> 'orders', 'history', Int $page? {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("order history ({$page // 'no page'})".encode('ascii'));
        }

        get -> 'category', 'tree', $leval-a = 'none-a', $level-b = 'none-b' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("category tree $leval-a $level-b".encode('ascii'));
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;

    my @cases =
        '/product/123.456', 'product 123.456',
            'Mu variable at end handled correctly',
        '/category/124.556', 'category 124.556',
            'Any variable at end handled correctly',
        '/user/128.856', 'user 128.856',
            'Str variable at end handled correctly',
        '/user/posts', 'user posts',
            'Longest literal prefix wins',
        '/product/123.456/reviews', 'reviews 123.456',
            'Str variable in middle of literals handled correctly',
        '/product/123.456/reviews/21', 'reviews 123.456 page 21',
            'Having both Str and Int variables handled correctly',
        '/product/123.456/reviews/-21', 'reviews 123.456 page -21',
            'Int may have a sign',
        '/tree', '',
            'Slurpy handled correctly (empty case)',
        '/tree/foo', 'foo',
            'Slurpy handled correctly (one segment case)',
        '/tree/bar/baz', 'bar,baz',
            'Slurpy handled correctly (two segment case)',
        '/tree/gee/wizz/fizz', 'gee,wizz,fizz',
            'Slurpy handled correctly (three segment case)',
        '/orders/history', 'order history (no page)',
            'Optional segment handled correctly (no argument)',
        '/orders/history/2', 'order history (2)',
            'Optional segment handled correctly (argument)',
        '/category/tree', 'category tree none-a none-b',
            'Two optional segments handled correctly (none passed)',
        '/category/tree/foo', 'category tree foo none-b',
            'Two optional segments handled correctly (one passed)',
        '/category/tree/bar/baz', 'category tree bar baz',
            'Two optional segments handled correctly (two passed)';
    for @cases -> $target, $expected-output, $desc {
        $source.emit(Cro::HTTP::Request.new(:method<GET>, :$target));
        given $responses.receive -> $r {
            is-deeply body-text($r), $expected-output, $desc;
        }
    }
}

{
    my $app = route {
        get -> 'search', :$min-price is query = 0, :$max-price is query = Inf {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("search $min-price .. $max-price".encode('ascii'));
        }

        get -> 'headertest1', :$x-custom1 is header = 'x', :$x-custom3 is header = 'x' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("headertest1 $x-custom1 $x-custom3".encode('ascii'));
        }

        get -> 'headertest2', :$x-custom2 is header = 'x', :$x-custom1 is header = 'x' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("headertest2 $x-custom1 $x-custom2".encode('ascii'));
        }

        get -> 'headertest3', :$X-CUSTOM1 is header = 'x', :$X-cusTom2 is header = 'x' {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("headertest3 $X-CUSTOM1 $X-cusTom2".encode('ascii'));
        }

        get -> 'reqquery', :$field1! is query {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reqquery field1".encode('ascii'));
        }

        get -> 'reqquery', :$field2! is query {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reqquery field2".encode('ascii'));
        }

        get -> 'reqquery-list', :@params {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            my $res = @params.map({ "$_.key()=$_.value()" }).join(',');
            response.set-body("reqquery with $res".encode('ascii'));
        }

        get -> 'reqquery-hash', :%params {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reqquery with %params<field1> and %params<field2>".encode('ascii'));
        }

        get -> 'reqheader', :$unknown! is header {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reqheader unknown".encode('ascii'));
        }

        get -> 'reqheader', :$x-custom1! is header {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reqheader x-custom1".encode('ascii'));
        }

        get -> 'reqheader-list', :@headers is header {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            my @sorted-headers = @headers.sort({$^a.name cmp $^b.name});
            response.set-body(@sorted-headers.map({ "$_.name() = $_.value()" }).join(",").encode('ascii'));
        }

        get -> 'reqheader-hash', :%headers is header {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reqheader with a %headers<X-Custom1> and %headers<X-Custom2>".encode('ascii'));
        }

        get -> 'reqintarg', Int :$page! is query {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("reqintarg $page".encode('ascii'));
        }

        get -> 'optintarg', Int :$page is query = 1 {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("optintarg $page".encode('ascii'));
        }

        get -> 'optintarg_uint', UInt :$page {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("optuintarg $page".encode('ascii'));
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;

    my @cases =
        '/search', 'search 0 .. Inf',
            'Two query string parameters, neither passed (explicit is query)',
        '/search?min-price=50', 'search 50 .. Inf',
            'Two query string parameters, first passed (explicit is query)',
        '/search?max-price=100', 'search 0 .. 100',
            'Two query string parameters, second passed (explicit is query)',
        '/search?max-price=60&min-price=20', 'search 20 .. 60',
            'Two query string parameters, both passed (explicit is query)',
        '/headertest1', 'headertest1 c1 x',
            'Two header parameters, one for a non-present header',
        '/headertest2', 'headertest2 c1 c2',
            'Two header parameters, both present',
        '/headertest3', 'headertest3 c1 c2',
            'Header parameters are case-insensitive',
        '/reqquery?field1=x', 'reqquery field1',
            'Required query parameter selects correct route (1)',
        '/reqquery?field2=x', 'reqquery field2',
            'Required query parameter selects correct route (2)',
        '/reqquery?field1=x&field2=x', 'reqquery field1',
            'First winning route with required query items wins',
        '/reqquery-list?field1=x&field2=y', 'reqquery with field1=x,field2=y',
            'Route with named array of query parameters works',
        '/reqquery-hash?field1=x&field2=y', 'reqquery with x and y',
            'Route with named array of headers works',
        '/reqheader', 'reqheader x-custom1',
            'Correct route picked when there are required headers',
        '/reqheader-list', 'X-Custom1 = c1,X-Custom2 = c2',
            'Route with named array of headers works',
        '/reqheader-hash', 'reqheader with a c1 and c2',
            'Route with named hash of headers works',
        '/reqintarg?page=42', 'reqintarg 42',
            'Route with required Int named arg for query parameter works',
        '/optintarg?page=100', 'optintarg 100',
            'Route with optional Int named arg for query parameter works when passed',
        '/optintarg', 'optintarg 1',
            'Route with optional Int named arg for query parameter works when not passed',
        '/optintarg_uint?page=1', 'optuintarg 1',
            'Route with optional UInt named arg for query parameter works when passed',
        '/optintarg_uint?page=-1', '',
            'Route with optional UInt named arg for query parameter doesn\'t match negative values';
    for @cases -> $target, $expected-output, $desc {
        my $req = Cro::HTTP::Request.new(:method<GET>, :$target);
        $req.append-header('X-Custom1', 'c1');
        $req.append-header('X-Custom2', 'c2');
        $source.emit($req);
        given $responses.receive -> $r {
            is-deeply body-text($r), $expected-output, $desc;
        }
    }
}

{
    my $app = route {
        get -> 'id_int8', int8 $id {
            content 'text/plain', "Correct, int8, $id";
        }
        get -> 'id_int8', int8 $id {
            content 'text/plain', "Correct, int8, $id";
        }
        get -> 'id_uint8', uint8 $id {
            content 'text/plain', "Correct, uint8, $id";
        }
        get -> 'id_int16', int16 $id {
            content 'text/plain', "Correct, int16, $id";
        }
        get -> 'id_uint16', uint16 $id {
            content 'text/plain', "Correct, uint16, $id";
        }
        get -> 'id_int32', int32 $id {
            content 'text/plain', "Correct, int32, $id";
        }
        get -> 'id_uint32', uint32 $id {
            content 'text/plain', "Correct, uint32, $id";
        }
        get -> 'id_int64', int64 $id {
            content 'text/plain', "Correct, int64, $id";
        }
        get -> 'id_uint64', uint64 $id {
            content 'text/plain', "Correct, uint64, $id";
        }
    }

    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my @cases =
        # int8
        '/id_int8/1', 'Correct, int8, 1', 'Route for positional int8 works',
        '/id_int8/-129', '', 'Lower border for positional int8 route works',
        '/id_int8/128', '',  'Upper border for positional int8 route works',
        # uint8
        '/id_uint8/1', 'Correct, uint8, 1', 'Route for positional uint8 works',
        '/id_uint8/-1', '', 'Lower border for positional uint8 route works',
        '/id_uint8/256', '',  'Upper border for positional uint8 route works',
        # int16
        '/id_int16/1', 'Correct, int16, 1', 'Route for positional int16 works',
        '/id_int16/-32769', '', 'Lower border for positional int16 route works',
        '/id_int16/32768', '',  'Upper border for positional int16 route works',
        # uint16
        '/id_uint16/1', 'Correct, uint16, 1', 'Route for positional uint16 works',
        '/id_uint16/-1', '', 'Lower border for positional uint16 route works',
        '/id_uint16/65536', '',  'Upper border for positional uint16 route works',
        # int32
        '/id_int32/1', 'Correct, int32, 1', 'Route for positional int32 works',
        '/id_int32/-2147483649', '', 'Lower border for positional int32 route works',
        '/id_int32/2147483648', '',  'Upper border for positional int32 route works',
        # uint32
        '/id_uint32/1', 'Correct, uint32, 1', 'Route for positional  uint32 works',
        '/id_uint32/-1', '', 'Lower border for positional uint32 route works',
        '/id_uint32/4294967296', '',  'Upper border for positional uint32 route works',
        # int64
        '/id_int64/1', 'Correct, int64, 1', 'Route for positional int64 works',
        '/id_int64/-9223372036854775809', '', 'Lower border for positional int64 route works',
        '/id_int64/9223372036854775808', '',  'Upper border for positional int64 route works',
        # uint64
        '/id_uint64/1', 'Correct, uint64, 1', 'Route for positional uint64 works',
        '/id_uint64/-1', '', 'Lower border for positional uint64 route works',
        '/id_uint64/18446744073709551616', '',  'Upper border for positional uint64 route works';

    for @cases -> $target, $expected-output, $desc {
        my $req = Cro::HTTP::Request.new(:method<GET>, :$target);
        $source.emit($req);
        given $responses.receive -> $r {
            if $expected-output eq '' {
                is $r.status, 404;
            }
            is-deeply body-text($r), $expected-output, $desc;
        }
    }
}

{
    my $app = route {
        get -> 'id_int8', int8 :$id {
            content 'text/plain', "Correct, int8, $id";
        }
        get -> 'id_uint8', uint8 :$id {
            content 'text/plain', "Correct, uint8, $id";
        }
        get -> 'id_int16', int16 :$id {
            content 'text/plain', "Correct, int16, $id";
        }
        get -> 'id_uint16', uint16 :$id {
            content 'text/plain', "Correct, uint16, $id";
        }
        get -> 'id_int32', int32 :$id {
            content 'text/plain', "Correct, int32, $id";
        }
        get -> 'id_uint32', uint32 :$id {
            content 'text/plain', "Correct, uint32, $id";
        }
        get -> 'id_int64', int64 :$id {
            content 'text/plain', "Correct, int64, $id";
        }
        get -> 'id_uint64', uint64 :$id {
            content 'text/plain', "Correct, uint64, $id";
        }
    }

    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my @cases =
        # int8
        '/id_int8', 'Correct, int8, 0', 'Route with optional named int8 works',
        '/id_int8?id=1', 'Correct, int8, 1', 'Route for named int8 works',
        '/id_int8?id=-129', '', 'Lower border for named int8 route works',
        '/id_int8?id=128', '',  'Upper border for named int8 route works',
        # uint8
        '/id_uint8', 'Correct, uint8, 0', 'Route with optional named uint8 works',
        '/id_uint8?id=1', 'Correct, uint8, 1', 'Route for named uint8 works',
        '/id_uint8?id=-1', '', 'Lower border for named uint8 route works',
        '/id_uint8?id=256', '',  'Upper border for named uint8 route works',
        # int16
        '/id_int16', 'Correct, int16, 0', 'Route with optional named int16 works',
        '/id_int16?id=1', 'Correct, int16, 1', 'Route for named int16 works',
        '/id_int16?id=-32769', '', 'Lower border for named int16 route works',
        '/id_int16?id=32768', '',  'Upper border for named int16 route works',
        # uint16
        '/id_uint16', 'Correct, uint16, 0', 'Route with optional named uint16 works',
        '/id_uint16?id=1', 'Correct, uint16, 1', 'Route for named uint16 works',
        '/id_uint16?id=-1', '', 'Lower border for named uint16 route works',
        '/id_uint16?id=65536', '',  'Upper border for named uint16 route works',
        # int32
        '/id_int32', 'Correct, int32, 0', 'Route with optional named int32 works',
        '/id_int32?id=1', 'Correct, int32, 1', 'Route for named int32 works',
        '/id_int32?id=-2147483649', '', 'Lower border for named int32 route works',
        '/id_int32?id=2147483648', '',  'Upper border for named int32 route works',
        # uint32
        '/id_uint32', 'Correct, uint32, 0', 'Route with optional named uint32 works',
        '/id_uint32?id=1', 'Correct, uint32, 1', 'Route for named uint32 works',
        '/id_uint32?id=-1', '', 'Lower border for named uint32 route works',
        '/id_uint32?id=4294967296', '',  'Upper border for named uint32 route works',
        # int64
        '/id_int64', 'Correct, int64, 0', 'Route with optional named int64 works',
        '/id_int64?id=1', 'Correct, int64, 1', 'Route for named int64 works',
        '/id_int64?id=-9223372036854775809', '', 'Lower border for named int64 route works',
        '/id_int64?id=9223372036854775808', '',  'Upper border for named int64 route works',
        # uint64
        '/id_uint64', 'Correct, uint64, 0', 'Route with optional named uint64 works',
        '/id_uint64?id=1', 'Correct, uint64, 1', 'Route for named uint64 works',
        '/id_uint64?id=-1', '', 'Lower border for named uint64 route works',
        '/id_uint64?id=18446744073709551616', '',  'Upper border for named uint64 route works';

    for @cases -> $target, $expected-output, $desc {
        my $req = Cro::HTTP::Request.new(:method<GET>, :$target);
        $source.emit($req);
        given $responses.receive -> $r {
            if $expected-output eq '' {
                is $r.status, 400;
            }
            is-deeply body-text($r), $expected-output, $desc;
        }
    }
}

{
    my subset UUIDv4 of Str where /^
        <[0..9a..f]> ** 12
        4 <[0..9a..f]> ** 3
        <[89ab]> <[0..9a..f]> ** 15
        $/;
    my subset Percent of Int where 1..100;
    my $app = route {
        get -> 'product', UUIDv4 $id {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("product $id".encode('ascii'));
        }

        get -> 'chart', Percent $complete {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("percent $complete".encode('ascii'));
        }

        get -> 'tag', $tag where /^\w+$/ {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("tag $tag".encode('ascii'));
        }

        get -> 'advent', Int $day where * <= 24 {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("advent $day".encode('ascii'));
        }

        get -> 'content', *@path where *[*-1].ends-with('.html') {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("it is html".encode('ascii'));
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;

    my @good-cases =
        '/product/673c748325a3411d871ccf969751f0de', 'product 673c748325a3411d871ccf969751f0de',
            'Segment constrained by Str-base subset type matches when it should',
        '/chart/50', 'percent 50',
            'Segment constrained by Int-base subset type matches when it should',
        '/tag/pizza', 'tag pizza',
            'Segment constrained by where clause matches when it should',
        '/advent/13', 'advent 13',
            'Segment of type Int constrained by where clause matches when it should',
        '/content/foo/bar.html', 'it is html',
            'Slurpy segment with where clause matches when it could';
    for @good-cases -> $target, $expected-output, $desc {
        my $req = Cro::HTTP::Request.new(:method<GET>, :$target);
        $source.emit($req);
        given $responses.receive -> $r {
            is-deeply body-text($r), $expected-output, $desc;
        }
    }

    my @bad-cases =
        '/product/not-a-uuid', 404, 'Non-matching segment gives 404 error (subset, Str)',
        '/percent/1000', 404, 'Non-matching segment gives 404 error (subset, Int)',
        '/tag/not-valid', 404, 'Non-matching segment gives 404 error (where, Str)',
        '/advent/25', 404, 'Non-matching segment gives 404 error (where, Int)',
        '/content/foo/bar.jpg', 404, 'Non-matching where clause on slurpy gives 404 error';
    for @bad-cases -> $target, $expected-status, $desc {
        my $req = Cro::HTTP::Request.new(:method<GET>, :$target);
        $source.emit($req);
        given $responses.receive -> $r {
            is $r.status, $expected-status, $desc;
        }
    }
}

{
    my subset UUIDv4 of Str where /^
        <[0..9a..f]> ** 12
        4 <[0..9a..f]> ** 3
        <[89ab]> <[0..9a..f]> ** 15
        $/;
    my subset Percent of Int where 1..100;
    my $app = route {
        get -> 'orders', UUIDv4 :$id! is query {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("order $id".encode('ascii'));
        }

        my constant DEFAULT_SESSION = '6419c8383038446bb3e95d0302dd4942';
        get -> 'home', UUIDv4 :$session is query = DEFAULT_SESSION {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("home $session".encode('ascii'));
        }

        get -> 'chart', Percent :$percent! is query {
            response.status = 200;
            response.append-header('Content-type', 'text/html');
            response.set-body("chart $percent