%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2012. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%

%%
%% Tests of the disconnect_cb configuration.
%%

-module(diameter_dpr_SUITE).

-export([suite/0,
         all/0,
         groups/0,
         init_per_group/2,
         end_per_group/2]).

%% testcases
-export([start/1,
         connect/1,
         remove_transport/1,
         stop_service/1,
         check/1,
         stop/1]).

%% disconnect_cb
-export([disconnect/5]).

-include("diameter.hrl").

%% ===========================================================================

-define(util, diameter_util).

-define(ADDR, {127,0,0,1}).

-define(CLIENT, "CLIENT").
-define(SERVER, "SERVER").

-define(DICT_COMMON, ?DIAMETER_DICT_COMMON).
-define(APP_ID, ?DICT_COMMON:id()).

%% Config for diameter:start_service/2.
-define(SERVICE(Host),
        [{'Origin-Host', Host},
         {'Origin-Realm', "erlang.org"},
         {'Host-IP-Address', [?ADDR]},
         {'Vendor-Id', hd(Host)},  %% match this in disconnect/5
         {'Product-Name', "OTP/diameter"},
         {'Acct-Application-Id', [?APP_ID]},
         {restrict_connections, false},
         {application, [{dictionary, ?DICT_COMMON},
                        {module, #diameter_callback{_ = false}}]}]).

%% Disconnect reasons that diameter passes as the first argument of a
%% function configured as disconnect_cb.
-define(REASONS, [transport, service, application]).

%% Valid values for Disconnect-Cause.
-define(CAUSES, [0, rebooting, 1, busy, 2, goaway]).

%% Establish one client connection for element of this list,
%% configured with disconnect/5 as disconnect_cb and returning the
%% specified value.
-define(RETURNS,
        [[close, {dpr, [{cause, invalid}]}], [ignore, close], []]
        ++ [[{dpr, [{timeout, 5000}, {cause, T}]}] || T <- ?CAUSES]).

%% ===========================================================================

suite() ->
    [{timetrap, {seconds, 60}}].

all() ->
    [{group, R} || R <- ?REASONS].

%% The group determines how transports are terminated: by remove_transport,
%% stop_service or application stop.
groups() ->
    Ts = tc(),
    [{R, [], Ts} || R <- ?REASONS].

init_per_group(Name, Config) ->
    [{group, Name} | Config].

end_per_group(_, _) ->
    ok.

tc() ->
    [start, connect, remove_transport, stop_service, check, stop].

%% ===========================================================================
%% start/stop testcases

start(_Config) ->
    ok = diameter:start(),
    ok = diameter:start_service(?SERVER, ?SERVICE(?SERVER)),
    ok = diameter:start_service(?CLIENT, ?SERVICE(?CLIENT)).

connect(Config) ->
    Pid = spawn(fun init/0),  %% process for disconnect_cb to bang
    Grp = group(Config),
    LRef = ?util:listen(?SERVER, tcp),
    Refs = [?util:connect(?CLIENT, tcp, LRef, opts(RCs, {Grp, Pid}))
            || RCs <- ?RETURNS],
    ?util:write_priv(Config, config, [Pid | Refs]).

%% Remove all the client transports only in the transport group.
remove_transport(Config) ->
    transport == group(Config)
        andalso (ok = diameter:remove_transport(?CLIENT, true)).

%% Stop the service only in the service group.
stop_service(Config) ->
    service == group(Config)
        andalso (ok = diameter:stop_service(?CLIENT)).

%% Check for callbacks and stop the service. (Not the other way around
%% for the timing reason explained below.)
check(Config) ->
    Grp = group(Config),
    [Pid | Refs] = ?util:read_priv(Config, config),
    Pid ! self(),                      %% ask for dictionary
    Dict = receive {Pid, D} -> D end,  %% get it
    check(Refs, ?RETURNS, Grp, Dict).  %% check for callbacks

stop(_Config) ->
    ok = diameter:stop().

%% Whether or not there are callbacks after diameter:stop() depends on
%% timing as long as the server runs on the same node: a server
%% transport could close the connection before the client has chance
%% to apply its callback. Therefore, just check that there haven't
%% been any callbacks yet.
check(_, _, application, Dict) ->
    [] = dict:to_list(Dict);

check([], [], _, _) ->
    ok;

check([Ref | Refs], CBs, Grp, Dict) ->
    check1(Ref, hd(CBs), Grp, Dict),
    check(Refs, tl(CBs), Grp, Dict).

check1(Ref, [ignore | RCs], Reason, Dict) ->
    check1(Ref, RCs, Reason, Dict);

check1(Ref, [_|_], Reason, Dict) ->
    {ok, Reason} = dict:find(Ref, Dict);  %% callback with expected reason

check1(Ref, [], _, Dict) ->
    error = dict:find(Ref, Dict).  %% no callback

%% ----------------------------------------

group(Config) ->
    {group, Grp} = lists:keyfind(group, 1, Config),
    Grp.

%% Configure the callback with the group name (= disconnect reason) as
%% extra argument.
opts(RCs, T) ->
    [{disconnect_cb, {?MODULE, disconnect, [T, RC]}} || RC <- RCs].

%% Match the group name with the disconnect reason to ensure the
%% callback is being called as expected.
disconnect(Reason, Ref, Peer, {Reason, Pid}, RC) ->
    io:format("disconnect: ~p ~p~n", [Ref, Reason]),
    {_, #diameter_caps{vendor_id = {$C,$S}}} = Peer,
    Pid ! {Reason, Ref},
    RC.

init() ->
    exit(recv(dict:new())).

recv(Dict) ->
    receive
        Pid when is_pid(Pid) ->
            Pid ! {self(), Dict};
        {Reason, Ref} ->
            recv(dict:store(Ref, Reason, Dict))
    end.
