%% @doc A module to be used where gen_tcp is expected.
%%
%% A "fake socket" is used in test where we need to mock socket communication.
%% It is a pid maintaining a list of expected send and recv events.
-module(mock_tcp).

%% gen_tcp interface functions.
-export([send/2, recv/2, recv/3]).

%% Functions to setup the mock_tcp.
-export([create/1, close/1]).

%% @doc Creates a mock_tcp process with a buffer of expected recv/2,3 and send/2
%% calls. The pid of the mock_tcp process is returned.
-spec create([{recv, binary()} | {send, binary()}]) -> pid().
create(ExpectedEvents) ->
    spawn_link(fun () -> loop(ExpectedEvents) end).

%% @doc Receives NumBytes bytes from mock_tcp Pid. This function can be used
%% as a replacement for gen_tcp:recv/2 in unit tests. If there not enough data
%% in the mock_tcp's buffer, an error is raised.
recv(Pid, NumBytes) ->
    Pid ! {recv, NumBytes, self()},
    receive
        {ok, Data} -> {ok, Data};
        error -> error({unexpected_recv, NumBytes})
    after 100 ->
        error(noreply)
    end.

recv(Pid, NumBytes, _Timeout) ->
    recv(Pid, NumBytes).

%% @doc Sends data to a mock_tcp. This can be used as replacement for
%% gen_tcp:send/2 in unit tests. If the data sent is not what the mock_tcp
%% expected, an error is raised.
send(Pid, Data) ->
    Pid ! {send, iolist_to_binary(Data), self()},
    receive
        ok -> ok;
        error -> error({unexpected_send, Data})
    after 100 ->
        error(noreply)
    end.

%% Stops the mock_tcp process. If the mock_tcp's buffer is not empty,
%% an error is raised.
close(Pid) ->
    Pid ! {done, self()},
    receive
        ok -> ok;
        {remains, Remains} -> error({unexpected_close, Remains})
    after 100 ->
        error(noreply)
    end.

%% Used by create/1.
loop(AllEvents = [{Func, Data} | Events]) ->
    receive
        {recv, NumBytes, FromPid} when Func == recv, NumBytes == size(Data) ->
            FromPid ! {ok, Data},
            loop(Events);
        {recv, NumBytes, FromPid} when Func == recv, NumBytes < size(Data) ->
            <<Data1:NumBytes/binary, Rest/binary>> = Data,
            FromPid ! {ok, Data1},
            loop([{recv, Rest} | Events]);
        {send, Bytes, FromPid} when Func == send, Bytes == Data ->
            FromPid ! ok,
            loop(Events);
        {send, Bytes, FromPid} when Func == send, size(Bytes) < size(Data) ->
            Size = size(Bytes),
            case Data of
                <<Bytes:Size/binary, Rest/binary>> ->
                    FromPid ! ok,
                    loop([{send, Rest} | Events]);
                _ ->
                    FromPid ! error
            end;
        {_, _, FromPid} ->
            FromPid ! error;
        {done, FromPid} ->
            FromPid ! {remains, AllEvents}
    end;
loop([]) ->
    receive
        {done, FromPid} -> FromPid ! ok;
        {_, _, FromPid} -> FromPid ! error
    end.

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

%% Tests for the mock_tcp functions.
bad_recv_test() ->
    Pid = create([{recv, <<"foobar">>}]),
    ?assertError(_, recv(Pid, 10)).

success_test() ->
    Pid = create([{recv, <<"foobar">>}, {send, <<"baz">>}]),
    %?assertError({unexpected_close, _}, close(Pid)),
    ?assertEqual({ok, <<"foo">>}, recv(Pid, 3)),
    ?assertEqual({ok, <<"bar">>}, recv(Pid, 3)),
    ?assertEqual(ok, send(Pid, <<"baz">>)),
    ?assertEqual(ok, close(Pid)),
    %% The process will exit after close. Another recv will raise noreply.
    ?assertError(noreply, recv(Pid, 3)).
-endif.