-module(mochiweb_test_util).
-export([with_server/3, client_request/4, sock_fun/2,
         read_server_headers/1, drain_reply/3, ssl_client_opts/1]).
-include("mochiweb_test_util.hrl").
-include_lib("eunit/include/eunit.hrl").

ssl_cert_opts() ->
    {ok, Cwd} = file:get_cwd(),
    CertDir = filename:join(
        case filename:basename(Cwd) of
            %% rebar2 compatibility
            ".eunit" -> [".."];
            _ -> []
        end ++ ["support", "test-materials"]),
    CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
    KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
    [{certfile, CertFile}, {keyfile, KeyFile}].

with_server(Transport, ServerFun, ClientFun) ->
    ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}],
    ServerOpts = case Transport of
        plain ->
            ServerOpts0;
        ssl ->
            ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
    end,
    {ok, Server} = mochiweb_http:start_link(ServerOpts),
    Port = mochiweb_socket_server:get(Server, port),
    Res = (catch ClientFun(Transport, Port)),
    mochiweb_http:stop(Server),
    Res.

ssl_client_opts(Opts) ->
  [{server_name_indication, disable}, {verify, verify_none} | Opts].

sock_fun(Transport, Port) ->
    Opts = [binary, {active, false}, {packet, http}],
    case Transport of
        plain ->
            {ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts),
            fun (recv) ->
                    gen_tcp:recv(Socket, 0);
                ({recv, Length}) ->
                    gen_tcp:recv(Socket, Length);
                ({send, Data}) ->
                    gen_tcp:send(Socket, Data);
                ({setopts, L}) ->
                    inet:setopts(Socket, L);
                (get) ->
                    Socket
            end;
        ssl ->
            {ok, Socket} = ssl:connect("127.0.0.1", Port, ssl_client_opts(Opts)),
            fun (recv) ->
                    ssl:recv(Socket, 0);
                ({recv, Length}) ->
                    ssl:recv(Socket, Length);
                ({send, Data}) ->
                    ssl:send(Socket, Data);
                ({setopts, L}) ->
                    ssl:setopts(Socket, L);
                (get) ->
                    {ssl, Socket}
            end
    end.

client_request(Transport, Port, Method, TestReqs) ->
    client_request(sock_fun(Transport, Port), Method, TestReqs).

body({ chunked, ChunkedBody }) ->
    ChunkedBody;
body(Body) ->
    Body.

client_request(SockFun, _Method, []) ->
    {the_end, {error, closed}} = {the_end, SockFun(recv)},
    ok;
client_request(SockFun, Method,
               [#treq{path=Path, body=Body, xreply=ExReply, xheaders=ExHeaders} | Rest]) ->
    Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n",
               client_headers(Body, Rest =:= []),
               "\r\n",
               body(Body)],
    ok = SockFun({setopts, [{packet, http}]}),
    ok = SockFun({send, Request}),
    case Method of
        'GET' ->
            {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv);
        'POST' ->
            {ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv);
        'CONNECT' ->
            {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv)
    end,
    Headers = read_server_headers(SockFun),
    ?assertMatch("MochiWeb" ++ _, mochiweb_headers:get_value("Server", Headers)),
    ?assert(mochiweb_headers:get_value("Date", Headers) =/= undefined),
    ?assert(mochiweb_headers:get_value("Content-Type", Headers) =/= undefined),
    ContentLength = list_to_integer(mochiweb_headers:get_value("Content-Length", Headers)),
    EHeaders = mochiweb_headers:make(ExHeaders),
    lists:foreach(
      fun (K) ->
              ?assertEqual(mochiweb_headers:get_value(K, EHeaders),
                           mochiweb_headers:get_value(K, Headers))
      end,
      %% Assumes implementation details of the headers
      gb_trees:keys(EHeaders)),
    {payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)},
    client_request(SockFun, Method, Rest).

read_server_headers(SockFun) ->
    ok = SockFun({setopts, [{packet, httph}]}),
    Headers = read_server_headers(SockFun, mochiweb_headers:empty()),
    ok = SockFun({setopts, [{packet, raw}]}),
    Headers.

read_server_headers(SockFun, Headers) ->
    case SockFun(recv) of
        {ok, http_eoh} ->
            Headers;
        {ok, {http_header, _, Header, _, Value}} ->
            read_server_headers(
              SockFun,
              mochiweb_headers:insert(Header, Value, Headers))
    end.

body_length_headers(<<>>) ->
    "";
body_length_headers({ chunked, _ }) ->
    "Transfer-Encoding: chunked\r\n";
body_length_headers(Body) ->
    ["Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"].

client_headers(Body, IsLastRequest) ->
    ["Host: localhost\r\n",
     case Body of
        <<>> ->
            "";
        _ ->
            ["Content-Type: application/octet-stream\r\n" | body_length_headers(Body)]
     end,
     case IsLastRequest of
         true ->
             "Connection: close\r\n";
         false ->
             ""
     end].

drain_reply(_SockFun, 0, Acc) ->
    Acc;
drain_reply(SockFun, Length, Acc) ->
    Sz = erlang:min(Length, 1024),
    {ok, B} = SockFun({recv, Sz}),
    drain_reply(SockFun, Length - Sz, <<Acc/bytes, B/bytes>>).
