### ----------------------------------------------------
### -- Cro::HTTP
### -- Licenses: Artistic-2.0
### -- Authors: Jonathan Worthington <jnthn@jnthn.net>
### -- File: t/http-router.rakutest
### ----------------------------------------------------
### -- Chunk 3 of 3
404, '404 for static works';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/css/../../../passwd');
    $source.emit($req);
    given $responses.receive -> $r {
        is $r.status, 403, '403 for static works';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/downloads/notes.rh');
    $source.emit($req);
    given $responses.receive -> $r {
        is $r.header('content-type'), 'text/plain', 'Content-type was setted correctly';
        is body-text($r), 'Notes', 'Custom extension works';
        is $r.status, 200, '200 for static works';
    }
}

{
    my $app = route {
        get -> {
            cache-control :public, :max-age(600);
            content 'text/plain', 'Response';
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my $req = Cro::HTTP::Request.new(method => 'GET', target => '/');
    $source.emit($req);
    given $responses.receive -> $r {
        is $r.has-header('cache-control'), True, 'Cache-Control header is set';
        ok $r.header('cache-control') ~~ /'max-age=600'/, 'max-age is added';
        ok $r.header('cache-control') ~~ /'public'/, 'public directive is added';
    }
}

{
    use Cro::HTTP::Client;
    use Cro::HTTP::Server;

    my $app = route {
        post -> 'destructure' {
            request-body -> (:$name!, :$surname!) {
                content 'text/plain', "Hello, $name $surname!";
            }
        }

        get -> 'test', 'uri' {
            content 'text/plain', ~request.uri;
        }
    }

    my $http-server = Cro::HTTP::Server.new(
        port => 31318,
        application => $app
    );
    $http-server.start();
    LEAVE $http-server.stop();

    my $base = "http://localhost:31318";
    given await Cro::HTTP::Client.post("$base/destructure",
                                       content-type => 'multipart/form-data',
                                       body => [name => 'John', surname => 'Doe']) -> $resp {
        is await($resp.body-text), 'Hello, John Doe!',
            'multipart/form-data is handled with destructuring';
    }
    given await Cro::HTTP::Client.post("$base/destructure",
                                       content-type => 'application/x-www-form-urlencoded',
                                       body => [name => 'John', surname => 'Doe']) -> $resp {
        is await($resp.body-text), 'Hello, John Doe!',
            'urlencoded is handled with destructuring';
    }
    my %body = name => 'John', surname => 'Doe';
    given await Cro::HTTP::Client.post("$base/destructure",
                                       content-type => 'application/json',
                                       body => %body) -> $resp {
        is await($resp.body-text), 'Hello, John Doe!',
        'json is handled with destructuring';
    }
    given await Cro::HTTP::Client.get("$base/test/uri") -> $resp {
        is await($resp.body-text), "$base/test/uri",
            'request.uri reconstructs full request URI';
    }
}

{
    my $nested = route {
        get -> {
            content 'text/plain', 'empty route (include)';
        }
        get -> 'some', 'parts' {
            content 'text/plain', 'some parts (include)';
        }
    }
    my $app = route {
        get -> 'before' {
            content 'text/plain', 'before (outer)';
        }
        include $nested;
        get -> 'after' {
            content 'text/plain', 'after (outer)';
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my %expected =
        '/' => 'empty route (include)',
        '/some/parts', 'some parts (include)',
        '/before' => 'before (outer)',
        '/after' => 'after (outer)';
    for %expected.kv -> $target, $expected {
        $source.emit(Cro::HTTP::Request.new(method => 'GET', :$target));
        given $responses.receive -> $r {
            is body-text($r), $expected, "Basic include: $expected";
        }
    }
}

{
    my class TestBody {
        has $.variant;
        has $.content;
    }

    my class TestBodyParser1 does Cro::BodyParser {
        method is-applicable(Cro::HTTP::Message $message --> Bool) {
            with $message.content-type {
                .type eq 'text' && .subtype eq 'x-test-1'
            }
            else {
                False
            }
        }
        method parse(Cro::HTTP::Message $message --> Promise) {
            $message.body-text.then({ TestBody.new(:1variant, :content(.result)) })
        }
    }

    my class TestBodyParser2 does Cro::BodyParser {
        method is-applicable(Cro::HTTP::Message $message --> Bool) {
            with $message.content-type {
                .type eq 'text' && .subtype eq 'x-test-2'
            }
            else {
                False
            }
        }
        method parse(Cro::HTTP::Message $message --> Promise) {
            $message.body-text.then({ TestBody.new(:2variant, :content(.result)) })
        }
    }

    my $nested = route {
        body-parser TestBodyParser2;
        get -> 'inner' {
            request-body -> $body {
                content 'text/plain', $body ~~ TestBody
                    ?? "$body.variant(), $body.content()"
                    !! "no match";
            }
        }
    }
    my $app = route {
        body-parser TestBodyParser1;
        include $nested;
        get -> 'outer' {
            request-body -> $body {
                content 'text/plain', $body ~~ TestBody
                    ?? "$body.variant(), $body.content()"
                    !! "no match";
            }
        }
    }

    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my %expected{Array} =
        ['/outer', 1] => ['1, blah', 'outer has outer body parser'],
        ['/outer', 2] => ['no match', 'body parser in include does not leak out'],
        ['/inner', 1] => ['1, blah', 'outer body parser visible in include'],
        ['/inner', 2] => ['2, blah', 'body parser in include works in include'];
    for %expected.kv -> [$target, $variant], [$expected, $desc] {
        my $req = Cro::HTTP::Request.new(method => 'GET', :$target);
        $req.append-header('Content-type', "text/x-test-$variant");
        $req.set-body('blah');
        $source.emit($req);
        given $responses.receive -> $r {
            is body-text($r), $expected, "include and body-parser: $desc";
        }
    }
}

{
    my $nested1 = route {
        get -> {
            content 'text/plain', 'route 1A';
        }
        get -> 'some', 'parts' {
            content 'text/plain', 'route 1B';
        }
    }
    my $nested2 = route {
        get -> {
            content 'text/plain', 'route 2A';
        }
        get -> 'more' {
            content 'text/plain', 'route 2B';
        }
    }
    my $app = route {
        include unquoted => $nested1;
        include 'quoted' => $nested2;
        include first => $nested1,
                second => $nested2;
        include 'first-quoted' => $nested1,
                second-unquoted => $nested2;
        include <many parts> => $nested1,
                <yet more parts> => $nested2;
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my %expected =
        '/unquoted' => 'route 1A',
        '/unquoted/some/parts' => 'route 1B',
        '/quoted' => 'route 2A',
        '/quoted/more' => 'route 2B',
        '/first' => 'route 1A',
        '/first/some/parts' => 'route 1B',
        '/second' => 'route 2A',
        '/second/more' => 'route 2B',
        '/first-quoted' => 'route 1A',
        '/first-quoted/some/parts' => 'route 1B',
        '/second-unquoted' => 'route 2A',
        '/second-unquoted/more' => 'route 2B',
        '/many/parts' => 'route 1A',
        '/many/parts/some/parts' => 'route 1B',
        '/yet/more/parts' => 'route 2A',
        '/yet/more/parts/more' => 'route 2B';
    for %expected.kv -> $target, $expected {
        $source.emit(Cro::HTTP::Request.new(method => 'GET', :$target));
        given $responses.receive -> $r {
            is body-text($r), $expected, "Basic include: $expected";
        }
    }
}

{
    my class TestTransform does Cro::Transform {
        has $.label is required;
        method consumes() { Cro::HTTP::Request }
        method produces() { Cro::HTTP::Response }
        method transformer(Supply $in --> Supply) {
            supply whenever $in -> $request {
                my $resp = Cro::HTTP::Response.new(:$request, :200status);
                $resp.append-header('Content-type', 'text/plain');
                $resp.set-body("delegated transform $!label");
                emit $resp;
            }
        }
    }
    my $app = route {
        delegate simple => TestTransform.new(:label<simple>);
        delegate <multi part> => TestTransform.new(:label<multi part>);
        delegate first => TestTransform.new(:label<first>),
                 <and second> => TestTransform.new(:label<and second>);
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my %expected =
        '/simple' => 'delegated transform simple',
        '/multi/part' => 'delegated transform multi part',
        '/first' => 'delegated transform first',
        '/and/second' => 'delegated transform and second';
    for %expected.kv -> $target, $expected {
        $source.emit(Cro::HTTP::Request.new(method => 'GET', :$target));
        given $responses.receive -> $r {
            is body-text($r), $expected, "Basic delegation: $expected";
        }
    }
}

{
    my class TestTransform does Cro::Transform {
        method consumes() { Cro::HTTP::Request }
        method produces() { Cro::HTTP::Response }
        method transformer(Supply $in --> Supply) {
            supply whenever $in -> $request {
                my $resp = Cro::HTTP::Response.new(:$request, :200status);
                $resp.append-header('Content-type', 'text/plain');
                $resp.set-body("{$request.target} and {$request.original-target()}");
                emit $resp;
            }
        }
    }
    my $inner = route {
        get -> 'home' {
            content 'text/plain', 'Home';
        }
    }
    my $nested = route {
        get -> {
            content 'text/plain', 'Slash';
        }
        get -> 'path' {
            content 'text/plain', 'Path';
        }
        include $inner;
    }

    my $app = route {
        delegate category => TestTransform.new;
        delegate <proxy *> => TestTransform.new;
        delegate <subsite *> => $nested;
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my %expected =
        '/category' => '/ and /category',
        '/proxy/item' => '/item and /proxy/item',
        '/proxy/item/1' => '/item/1 and /proxy/item/1',
        '/subsite' => 'Slash',
        '/subsite/path' => 'Path',
        '/subsite/home' => 'Home';
    for %expected.kv -> $target, $expected {
        $source.emit(Cro::HTTP::Request.new(method => 'GET', :$target));
        given $responses.receive -> $r {
            is body-text($r), $expected, "Delegation: $expected";
        }
    }
}

{
    my $app = route {
        get -> 'encoded/slash' {
            content 'text/plain', 'encoded slash';
        }
        get -> 'a+plus' {
            content 'text/plain', 'a plus';
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my %expected =
        '/encoded%2Fslash' => 'encoded slash',
        '/a%2Bplus' => 'a plus';
    for %expected.kv -> $target, $expected {
        $source.emit(Cro::HTTP::Request.new(method => 'GET', :$target));
        given $responses.receive -> $r {
            is body-text($r), $expected, "Can match URL segments with encoded bits ($target)";
        }
    }
}

{
    my $app = route {
        get -> :$value is query {
            content 'text/plain', "got $value";
        }
    }

    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my %expected =
        '/?value=1' => 'got 1';
    for %expected.kv -> $target, $expected {
        $source.emit(Cro::HTTP::Request.new(method => 'GET', :$target));
        given $responses.receive -> $r {
            is body-text($r), $expected, "Can pass query argument without path components ($target)";
        }
    }
}
{
    my $app = route {
        get -> *@path, :$value is query {
            content 'text/plain', "got <{@path}> and $value";
        }
    }

    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my %expected =
        '/?value=1' => 'got <> and 1',
        '/x?value=1' => 'got <x> and 1';
    for %expected.kv -> $target, $expected {
        $source.emit(Cro::HTTP::Request.new(method => 'GET', :$target));
        given $responses.receive -> $r {
            is body-text($r), $expected, "Can pass query arguments with slurpy path signature ($target)";
        }
    }
}

{
    my $app = route {
        get -> {
            static 't/samples/', :indexes(['index.xhtml']);
        }

        get -> 'index-plain' {
            static 't/samples/', :indexes(['index.html', 'index.xhtml']);
        }
        get -> 'index-extended' {
            static 't/samples/', :indexes(['index.xhtml', 'index.html']);
        }
        get -> 'no-index' {
            static 't/samples/', :indexes([]);
        }
        get -> 'index-mime-type' {
            static 't/samples/', :indexes(['index.html']), mime-types => {
                'html' => 'text/html'
            }
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my $req = Cro::HTTP::Request.new(method => 'GET', target => '/');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx{ '<HTML>Extended</HTML>' \n }, 'Get value from index';
        is $r.status, 200, 'Static sets correct status code';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/index-plain');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx{ '<HTML></HTML>' \n }, 'static indexes order check, 1';
        is $r.status, 200, 'Good status';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/index-extended');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx{ '<HTML>Extended</HTML>' \n }, 'static indexes order check, 2';
        is $r.status, 200, 'Good status';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/no-index');
    $source.emit($req);
    given $responses.receive -> $r {
        is $r.status, 404, 'No candidate served with empty indexes';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/index-mime-type');
    $source.emit($req);
    given $responses.receive -> $r {
        is $r.status, 200, 'Indexes with mime-types returns good status';
        is $r.header('Content-Type'), 'text/html', 'Indexes with mime-types returns proper content-type';
    }
}

{
    my $app = resourcey-routes();
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my $req = Cro::HTTP::Request.new(method => 'GET', target => '/index.html');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx { '<HTML></HTML>' \n }, 'Get index.html from resources';
        is $r.status, 200, 'resource sets correct status code';
        is $r.content-type, 'text/html', 'resource sets correct content-type';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/test.1');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx{ 'this is a test' \n }, 'Get folder/test.txt from resources';
        is $r.status, 200, 'Good status';
        is $r.content-type, 'text/plain', 'Good content-type';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/test.2');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx{ 'this is a test' \n }, 'Get <folder test.txt> from resources';
        is $r.status, 200, 'Good status';
        is $r.content-type, 'text/plain', 'Good content-type';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/folder-indexes');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx{ 'this is a test' \n }, 'indexes in a folder of resources';
        is $r.status, 200, 'Good status';
        is $r.content-type, 'text/plain', 'Good content-type';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/root-indexes1');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx{ '<HTML></HTML>' \n } , 'indexes in root of resources, 1';
        is $r.status, 200, 'Good status';
        is $r.content-type, 'text/html', 'Good content-type';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/root-indexes2');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx{ '<HTML></HTML>' \n }, 'indexes in root of resources, 2';
        is $r.status, 200, 'Good status';
        is $r.content-type, 'text/html', 'Good content-type';
    }

    $req = Cro::HTTP::Request.new(method => 'GET', target => '/test-plugin');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx { '<html></html>' \n }, 'The extension point for other plugins wanting to use resources works';
        is $r.content-type, 'text/html; charset=utf-8', 'Good content-type';
    }
}

{
    my $app = route {
        around {
            my $*EXCEPTION = X::AdHoc.new;
            $_();
            CATCH {
                when X::AdHoc {
                    content 'text/plain', "Rescued!";
                }
            }
        }

        get -> {
            $*EXCEPTION.throw
        }
    };
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my $req = Cro::HTTP::Request.new(method => 'GET', target => '/');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx { 'Rescued!' }, 'Around block was called';
        is $r.status, 200, 'Around block can send response';
    }
}

{
    my $mark = "";
    my $app = route {
        around {
            $mark ~= "1";
            $_();
            $mark ~= "5";
        }
        around {
            $mark ~= "2";
            $_();
            $mark ~= "4";
        }

        get -> {
            $mark ~= "3";
            content 'text/plain', "Rescued!";
        }
    };
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my $req = Cro::HTTP::Request.new(method => 'GET', target => '/');
    $source.emit($req);
    given $responses.receive -> $r {
        like body-text($r), rx { 'Rescued!' }, 'Handler works normally with around block(s)';
        is $r.status, 200, 'Handler works normally with around block(s)';
        is $mark, '12345', 'The around blocks are called in top-to-bottom order';
    }
}

{
    my $app = route {
        get -> {
            response.append-header('Content-type', 'text/plain');
            content 'text/html', 'foo';
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    $source.emit(Cro::HTTP::Request.new(:method<GET>, :target</>));
    given $responses.receive -> $r {
        is $r.header('Content-type'), 'text/html; charset=utf-8',
                'Content-type header set by content replaces any existing one';
    }
}

{
    my $app = route {
        put -> 'user',  uint32 $id {
            request-body -> (:$state!, :$comment, *%rest) {
                 # put user
            }
        }
    }
    my $source = Supplier.new;
    my $responses = $app.transformer($source.Supply).Channel;
    my $req = Cro::HTTP::Request.new(method => 'PUT', target => '/user/20',
                                     content-type => 'text/plain',
                                     body => {"state" => "HOM", "comment" => "test"});
    $source.emit($req);
    given $responses.receive -> $r {
        is $r.status, 400, 'Recieved proper status for the case where a capture cannot be unpacked for whatever reason';
    }
}

done-testing;


