:- module(test_http,
          [ test_http/0
          ]).
:- asserta(user:file_search_path(foreign, '.')).
:- asserta(user:file_search_path(foreign, '../clib')).
:- asserta(user:file_search_path(foreign, '../sgml')).
:- asserta(user:file_search_path(foreign, '../zlib')).
:- asserta(user:file_search_path(library, '..')).
:- asserta(user:file_search_path(library, '../sgml')).
:- asserta(user:file_search_path(library, '../plunit')).
:- asserta(user:file_search_path(library, '../clib')).
:- asserta(user:file_search_path(library, '../zlib')).

:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/html_write)).
:- use_module(library(http/http_parameters)).
:- use_module(library(http/http_json)).
:- use_module(library(http/http_header)).
:- use_module(library(http/http_open)).
:- use_module(library(http/http_client)).
:- use_module(library(http/http_stream)).
:- use_module(library(plunit)).
:- use_module(library(readutil)).
:- use_module(library(debug)).
:- use_module(library(option)).
:- use_module(library(lists)).
:- use_module(library(zlib), []).               % plugin

test_http :-
    run_tests([ http_open,
                http_get,
                http_server
              ]).

run_network_tests :-
    getenv('SWIPL_PUBLIC_NETWORK_TESTS', true),
    exists_source(library(ssl)).

:- begin_tests(http_open, [condition(run_network_tests)]).

:- meta_predicate
    with_server(-, 0, +).

test(read, true) :-
    http_open('http://www.swi-prolog.org/', In,
              [status_code(Code)]),
    assertion(Code == 200),
    read_stream_to_codes(In, Codes),
    close(In),
    contains_codes("href=\"/Download.html\"", Codes).
test(redirect, true) :-
    http_open('http://www.swi-prolog.org', In, []),
    read_stream_to_codes(In, Codes),
    close(In),
    contains_codes("href=\"/Download.html\"", Codes).
test(chunked, true(Codes == Ref)) :-
    http_open('http://www.swi-prolog.org/Tests/chunked/data', In, []),
    read_stream_to_codes(In, Codes),
    close(In),
    chunked_data(Ref).

:- end_tests(http_open).

:- begin_tests(http_get, [condition(run_network_tests)]).

test(read, true) :-
    http_get('http://www.swi-prolog.org/', Data, [to(codes)]),
    contains_codes("http://www.swi-prolog.org", Data).

test(chunked, true(Data == Ref)) :-
    http_get('http://www.swi-prolog.org/Tests/chunked/data',
             Data, [to(codes)]),
    chunked_data(Ref).

:- end_tests(http_get).


:- begin_tests(http_server).

test(connection, Close == close) :-
    setup_call_cleanup(
        http_server(http_dispatch, [silent(true), port(localhost:Port)]),
        ( format(atom(URL), 'http://localhost:~w/reply-source', [Port]),
          setup_call_cleanup(
              open_null_stream(Out),
              setup_call_cleanup(
                  http_open(URL, Stream,
                            [ header(connection, Close)
                            ]),
                  copy_stream_data(Stream, Out),
                  close(Stream)),
              close(Out))
        ),
        http_stop_server(Port, [])).

test(ok_html_1, Code == 200) :-
     request('/ok/html_1', Code, Type, Content, []),
     assertion(html_content(Type, Content,
                           "world")).
test(ok_html_unicode, Code == 200) :-
     request('/ok/html_unicode', Code, Type, Content, []),
     assertion(html_content(Type, Content,
                           "world")).
test(ok_bytes_prolog, Code == 200) :-
     request('/ok/bytes/prolog', Code, Type, Content, []),
     assertion(prolog_content(Type, Content,
                              hello(world))).
test(not_found, Code == 404) :-
    request('/not-found', Code, Type, Content, []),
    assertion(html_content(Type, Content,
                           "was not found on this server")).
test(forbidden, Code == 403) :-
    request('/forbidden', Code, Type, Content, []),
    assertion(html_content(Type, Content,
                           "permission to access")).
test(method_not_allowed, Code == 405) :-
    request('/method_not_allowed', Code, Type, Content,
            [ method(delete) ]),
    assertion(html_content(Type, Content,
                           "not support method")).
test(not_modified, Code == 304) :-
    request('/not_modified', Code, _Type, Content, [redirect(false)]),
    assertion(Content == "").
test(moved, Code == 301) :-
    request('/moved', Code, Type, Content, [redirect(false)]),
    assertion(html_content(Type, Content,
                           "has moved")).
test(moved_temporary, Code == 302) :-
    request('/moved-temporary', Code, Type, Content, [redirect(false)]),
    assertion(html_content(Type, Content,
                           "is currently")).
test(see_also, Code == 303) :-
    request('/see-also', Code, Type, Content, [redirect(false)]),
    assertion(html_content(Type, Content,
                           "other document")).
test(server_error, Code == 500) :-
    request('/server-error', Code, Type, Content, []),
    assertion(html_content(Type, Content,
                           "zero_divisor")).
test(unavailable, Code == 503) :-
    request('/unavailable', Code, Type, Content, []),
    assertion(html_content(Type, Content,
                           "Shut down")).
test(resource_error, Code == 503) :-
    request('/resource-error', Code, Type, Content, []),
    assertion(html_content(Type, Content,
                           "patience")).
test(not_acceptable, Code == 406) :-
    request('/not_acceptable', Code, Type, Content, []),
    assertion(html_content(Type, Content,
                           "I reject")).
test(bad_request, Code == 400) :-
    request('/bad-request', Code, Type, Content, []),
    assertion(html_content(Type, Content,
                           "hello")).
test(created, Code == 201) :-
    request('/created', Code, Type, Content,
            [ post(json("world")),
              header(location, Location)
            ]),
    assertion(Location == '/brave-new-world'),
    assertion(html_content(Type, Content,
                           "brave-new-world")).
test(fail, Code == 500) :-
    request('/fail', Code, Type, Content, []),
    assertion(html_content(Type, Content,
                           "goal unexpectedly failed")).
test(post_echo, Content == Data) :-
    Data = "Hello world!",
    request('/post/echo', _Code, _Type, Content,
            [ post(string(text/plain, Data))
            ]).
test(post_echo_keep_alive,
     [ Content2 == Data,
       cleanup(http_close_keep_alive(_))
     ]) :-
    Data = "Hello world!",
    with_server(
        ServerURL,
        ( client(ServerURL, '/post/echo', _, _, _Content1,
                 [ post(string(text/plain, Data)),
                   connection('Keep-alive')
                 ]),
          client(ServerURL, '/post/echo', _, _, Content2,
                 [ post(string(text/plain, Data)),
                   connection('Keep-alive')
                 ])
        ),
        [ keep_alive_timeout(0.1)
        ]).
test(post_echo_keep_alive_short_read,
     [ Content2 == Data,
       cleanup(http_close_keep_alive(_))
     ]) :-
    Data = "0123456789 Hello world!",
    with_server(
        ServerURL,
        ( client(ServerURL, '/post/echo/short_read', _, _, Content1,
                 [ post(string(text/plain, Data)),
                   connection('Keep-alive')
                 ]),
          assertion(sub_string(Data, 0, 10, _, Content1)),
          client(ServerURL, '/post/echo', _, _, Content2,
                 [ post(string(text/plain, Data)),
                   connection('Keep-alive')
                 ])
        ),
        [ keep_alive_timeout(0.1)
        ]).
test(post_echo_keep_alive_short_read_spawned,
     [ Content2 == Data,
       cleanup(http_close_keep_alive(_))
     ]) :-
    Data = "0123456789 Hello world!",
    with_server(
        ServerURL,
        ( client(ServerURL, '/spawn/post/echo/short_read', _, _, Content1,
                 [ post(string(text/plain, Data)),
                   connection('Keep-alive')
                 ]),
          assertion(sub_string(Data, 0, 10, _, Content1)),
          client(ServerURL, '/post/echo', _, _, Content2,
                 [ post(string(text/plain, Data)),
                   connection('Keep-alive')
                 ])
        ),
        [ keep_alive_timeout(0.1)
        ]).

with_server(URL, Goal, Options) :-
    setup_call_cleanup(
        http_server(http_dispatch,
                    [ silent(true),
                      port(localhost:Port)
                    | Options
                    ]),
        (   format(atom(URL), 'http://localhost:~w', [Port]),
            Goal
        ),
        http_stop_server(Port, [])).

client(ServerURL, Path, Code, Type, Content, ExtraHdrs) :-
    format(atom(URL), '~w~w', [ServerURL, Path]),
    setup_call_cleanup(
        http_open(URL, Stream,
                  [ status_code(Code),
                    header(content_type, Type),
                    header(content_length, Len)
                  | ExtraHdrs
                  ]),
        read_string(Stream, CLen, Content),
        close(Stream)),
    (   Len == ''
    ->  assertion(CLen == 0)
    ;   assertion(CLen == Len)
    ).

request(Path, Code, Type, Content, ExtraHdrs) :-
    with_server(ServerURL,
                client(ServerURL, Path, Code, Type, Content, ExtraHdrs),
                []).

:- http_handler('/ok/html_1', ok_html_1, []).
:- http_handler('/ok/html_unicode', ok_html_unicode, []).
:- http_handler('/ok/bytes/prolog', ok_bytes_prolog, []).
:- http_handler('/reply-source', reply_source, []).
:- http_handler('/forbidden', forbidden, []).
:- http_handler('/method_not_allowed', method_not_allowed,
                [methods([get])]).
:- http_handler('/moved', http_redirect(moved, '/moved-to'), []).
:- http_handler('/moved-temporary', http_redirect(moved_temporary, '/moved-to'), []).
:- http_handler('/see-also', http_redirect(see_other, '/moved-to'), []).
:- http_handler('/server-error', server_error, []).
:- http_handler('/not_modified', not_modified, []).
:- http_handler('/unavailable', unavailable, []).
:- http_handler('/not_acceptable', not_acceptable, []).
:- http_handler('/resource-error', resource_error_handler, []).
:- http_handler('/bad-request', bad_request_handler, []).
:- http_handler('/created', created, []).
:- http_handler('/fail', fail, []).
:- http_handler('/post/echo', post_echo, [method(post)]).
:- http_handler('/post/echo/short_read', post_echo_short_read, [method(post)]).
:- http_handler('/spawn/post/echo/short_read', post_echo_short_read,
                [spawn, method(post)]).

ok_html_1(_Request) :-
    reply_html_page(
        title('Test'),
        h1('Hello world')).

ok_html_unicode(_Request) :-
    numlist(1000, 1050, Text),
    reply_html_page(
        title('Test'),
        [ h1('Hello world'),
          p([], '~s'-[Text])
        ]).

ok_bytes_prolog(_Request) :-
    string_codes("hello(world)", Bytes),
    throw(http_reply(bytes('application/x-prolog', Bytes))).

reply_source(Request) :-
    module_property(test_http, file(File)),
    http_reply_file(File, [unsafe(true)], Request).

forbidden(Request) :-
    option(path(Path), Request),
    throw(http_reply(forbidden(Path))).

method_not_allowed(Request) :-
    option(method(Method), Request),
    format('Content-type: text/plain~n~n'),
    format('Method ~p should not have been alowed', [Method]).

server_error(_Request) :-
    Num = 1,
    Den = 0,
    X is Num/Den,
    format('Content-type: text/plain~n~n'),
    format('X = ~q~n', [X]).

not_modified(_Request) :-
    throw(http_reply(not_modified)).

unavailable(_Request) :-
    throw(http_reply(unavailable(p(['Shut down'])))).

not_acceptable(_Request) :-
    throw(http_reply(not_acceptable(p(['I reject'])))).

resource_error_handler(_Request) :-
    resource_error(patience).

bad_request_handler(Request) :-
    http_parameters(Request, [ hello(_,[]) ]).

created(Request) :-
    http_read_json_dict(Request, _Dict),
    throw(http_reply(created('/brave-new-world'))).

fail(_Request) :-
    fail.

post_echo(Request) :-
    http_read_data(Request, Data, [to(string)]),
    format('Content-type: text/plain~n~n'),
    format('~s', [Data]).

post_echo_short_read(Request) :-
    option(input(In), Request),
    read_string(In, 10, Data),
    format('Content-type: text/plain~n~n'),
    format('~s', [Data]).

:- end_tests(http_server).


                 /*******************************
                 *             UTIL             *
                 *******************************/

html_content(Type, Content, Needle) :-
    http_parse_header_value(content_type, Type, media(text/html, _Attributes)),
    contains_codes(Needle, Content).

contains_codes(Needle, Haystack) :-
    to_string(Needle, NeedleS),
    to_string(Haystack, HaystackS),
    sub_string(HaystackS, _, _, _, NeedleS),
    !.

to_string(S, S) :-
    string(S),
    !.
to_string(A, S) :-
    atom(A),
    !,
    atom_string(A, S).
to_string(Codes, S) :-
    string_codes(S, Codes).

prolog_content(Type, Content, Match) :-
    http_parse_header_value(content_type, Type,
                            media(application/'x-prolog', _Attributes)),
    term_string(Term, Content),
    Term =@= Match.

%!  chunked_data(-String) is det.
%
%   Content of the chunked data that is sent by cgi-bin/chunked.

chunked_data(S) :-
    findall(C,
            (   between(1, 1000, X),
                C is "a" + X mod 26
            ), S0),
    append(S0, S0, S).


