%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2010-2011. 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%
%%

%%
%%----------------------------------------------------------------------
%% Purpose: Verify the tcp transport component of the Diameter application
%%----------------------------------------------------------------------
%% 
-module(diameter_tcp_test).

-export([
	 init_per_testcase/2, fin_per_testcase/2, 

	 all/0,
	 groups/0, 
	 init_per_suite/1, end_per_suite/1, 
	 suite_init/1, suite_fin/1, 
	 init_per_group/2, end_per_group/2, 

	 start_and_stop_transport_plain/1,
	 start_and_listen/1, 
	 simple_connect/1,
	 simple_send_and_recv/1

	]).

-export([t/0, t/1]).

%% diameter_peer (internal) callback API
-export([up/1, up/3, recv/2]).

-include("diameter_test_lib.hrl").
-include_lib("diameter/include/diameter.hrl").
%% -include_lib("diameter/src/tcp/diameter_tcp.hrl").


t()     -> diameter_test_server:t(?MODULE).
t(Case) -> diameter_test_server:t({?MODULE, Case}).


%% Test server callbacks
init_per_testcase(Case, Config) ->
    diameter_test_server:init_per_testcase(Case, Config).

fin_per_testcase(Case, Config) ->
    diameter_test_server:fin_per_testcase(Case, Config).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

all() ->
    [
     {group, start}, 
     {group, simple}
    ].

groups() ->
    [
     {start,  [], [start_and_stop_transport_plain, start_and_listen]}, 
     {simple, [], [simple_connect, simple_send_and_recv]}
    ].

init_per_group(_GroupName, Config) ->
    Config.

end_per_group(_GroupName, Config) ->
    Config.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

suite_init(X) -> init_per_suite(X).

init_per_suite(suite) -> [];
init_per_suite(doc) -> [];
init_per_suite(Config) when is_list(Config) ->
    Config.


suite_fin(X) -> end_per_suite(X).

end_per_suite(suite) -> [];
end_per_suite(doc) -> [];
end_per_suite(Config) when is_list(Config) ->
    Config.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 
%% Test case(s) 
%% 


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 
%% Plain start and stop of TCP transport
%% 

start_and_stop_transport_plain(suite) ->
    [];
start_and_stop_transport_plain(doc) ->
    [];
start_and_stop_transport_plain(Config) when is_list(Config) ->

    ?SKIP(not_yet_implemented),

    %% This has been changed *a lot* since it was written...

    process_flag(trap_exit, true),
    Transport = ensure_transport_started(),
    TcpTransport = ensure_tcp_transport_started(),
    ensure_tcp_transport_stopped(TcpTransport),
    ensure_transport_stopped(Transport),
    i("done"),
    ok.



%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 
%% Start TCP transport and then create a listen socket
%% 

start_and_listen(suite) ->
    [];
start_and_listen(doc) ->
    [];
start_and_listen(Config) when is_list(Config) ->

    ?SKIP(not_yet_implemented),

    %% This has been changed *a lot* since it was written...

    process_flag(trap_exit, true),
    Transport = ensure_transport_started(),
    TcpTransport = ensure_tcp_transport_started(),

    case listen([{port, 0}]) of
	{ok, Acceptor} when is_pid(Acceptor) ->
	    Ref = erlang:monitor(process, Acceptor),
	    [{Acceptor, Info}] = diameter_tcp:which_listeners(),
	    case lists:keysearch(socket, 1, Info) of
		{value, {_, Listen}} ->
		    i("Listen socket: ~p"
		      "~n   Opts:  ~p"
		      "~n   Stats: ~p"
		      "~n   Name:  ~p", 
		      [Listen, 
		       ok(inet:getopts(Listen, [keepalive, delay_send])),
		       ok(inet:getstat(Listen)),
		       ok(inet:sockname(Listen))
		      ]),
		    ok;
		_ ->
		    ?FAIL({bad_listener_info, Acceptor, Info})
	    end,
	    Crash = simulate_crash, 
	    exit(Acceptor, Crash), 
	    receive
		{'DOWN', Ref, process, Acceptor, Crash} ->
		    ?SLEEP(1000),
		    case diameter_tcp:which_listeners() of
			[{NewAcceptor, _NewInfo}] ->
			    diameter_tcp_accept:stop(NewAcceptor),
			    ?SLEEP(1000),
			    case diameter_tcp:which_listeners() of
				[] ->
				    ok;
				UnexpectedListeners ->
				    ?FAIL({unexpected_listeners, empty, UnexpectedListeners})
			    end;
			UnexpectedListeners ->
			    ?FAIL({unexpected_listeners, non_empty, UnexpectedListeners})
		    end
	    after 5000 ->
		    ?FAIL({failed_killing, Acceptor})
	    end;
	Error ->
	    ?FAIL({failed_creating_acceptor, Error})
    end,
    ensure_tcp_transport_stopped(TcpTransport),
    ensure_transport_stopped(Transport),
    i("done"),
    ok.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 
%% TCP transport connecting
%% 

simple_connect(suite) ->
    [];
simple_connect(doc) ->
    [];
simple_connect(Config) when is_list(Config) ->

    ?SKIP(not_yet_implemented),

    %% This has been changed *a lot* since it was written...

    process_flag(trap_exit, true),
    Transport = ensure_transport_started(),
    TcpTransport = ensure_tcp_transport_started(),
    {_Acceptor, Port} = ensure_tcp_listener(),
    
    {ok, Hostname} = inet:gethostname(),
    
    i("try connect"),
    Opts = [{host, Hostname}, {port, Port}, {module, ?MODULE}], 
    Conn = case connect(Opts) of
	       {ok, C} ->
		   C;
	       Error ->
		   ?FAIL({failed_connecting, Error})
	   end,
    i("connected: ~p", [Conn]),
    
    %% Up for connect
    receive
	{diameter, {up, Host, Port}} ->
	    i("Received expected connect up (~p:~p)", [Host, Port]),
	    ok
    after 5000 ->
	    ?FAIL(connect_up_confirmation_timeout)
    end,

    %% Up for accept
    receive
	{diameter, {up, _ConnPid}} ->
	    i("Received expected accept up"),
	    ok
    after 5000 ->
	    ?FAIL(acceptor_up_confirmation_timeout)
    end,

    i("try disconnect"),
    diameter_tcp:disconnect(Conn),
    ensure_tcp_transport_stopped(TcpTransport),
    ensure_transport_stopped(Transport),
    i("done"),
    ok.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 
%% Plain start and stop of TCP transport
%% 

simple_send_and_recv(suite) ->
    [];
simple_send_and_recv(doc) ->
    [];
simple_send_and_recv(Config) when is_list(Config) ->

    ?SKIP(not_yet_implemented),

    %% This has been changed *a lot* since it was written...

    process_flag(trap_exit, true),
    %% --------------------------------------------------
    %% Start the TCP transport sub-system
    %% 

    Transport = ensure_transport_started(),
    TcpTransport = ensure_tcp_transport_started(),

    {_Acceptor, Port} = ensure_tcp_listener(),
    
    {ok, Hostname} = inet:gethostname(),
    
    i("try connect"),
    Opts = [{host, Hostname}, {port, Port}, {module, ?MODULE}], 
    Conn = case connect(Opts) of
	       {ok, C1} ->
		   C1;
	       Error ->
		   ?FAIL({failed_connecting, Error})
	   end,
    i("connected: ~p", [Conn]),
    
    %% Up for connect
    receive
	{diameter, {up, Host, Port}} ->
	    i("Received expected connect up (~p:~p)", [Host, Port]),
	    ok
    after 5000 ->
	    ?FAIL(connect_up_confirmation_timeout)
    end,

    %% Up for accept
    APid = 
	receive
	    {diameter, {up, C2}} ->
		i("Received expected accept up"),
		C2
	after 5000 ->
		?FAIL(acceptor_up_confirmation_timeout)
	end,
 
    %% --------------------------------------------------
    %% Start some stuff needed for the codec to run
    %% 

    i("start persistent table"),
    {ok, _Pers}    = diameter_persistent_table:start_link(),

    i("start session"),
    {ok, _Session} = diameter_session:start_link(),

    i("try decode a (DWR) message"),
    Base = diameter_gen_base_rfc3588,
    DWR  = ['DWR', 
	    {'Origin-Host',     Hostname}, 
	    {'Origin-Realm',    "whatever-realm"}, 
	    {'Origin-State-Id', [10]}],

    #diameter_packet{msg = Msg} = diameter_codec:encode(Base, DWR),


    %% --------------------------------------------------
    %% Now try to send the message
    %% 
    %% This is not the codec-test suite, so we dont really care what we 
    %% send, as long as it encoded/decodes correctly in the transport
    %% 

    i("try send from connect side"),
    ok = diameter_tcp:send_message(Conn, Msg),

    %% Wait for data on Accept side
    APkt = 
	receive
	    {diameter, {recv, A}} ->
		i("[accept] Received expected data message: ~p", [A]),
		A
	after 5000 ->
		?FAIL(acceptor_up_confirmation_timeout)
	end,
    
    %% Send the same message back, just to have something to send...
    i("try send (\"reply\") from accept side"),
    ok = diameter_tcp:send_message(APid, APkt),

    %% Wait for data on Connect side
    receive
	{diameter, {recv, B}} ->
	    i("[connect] Received expected data message: ~p", [B]),
	    ok
    after 5000 ->
	    ?FAIL(acceptor_up_confirmation_timeout)
    end,  

    i("we are done - now close shop"),
    diameter_session:stop(),
    diameter_persistent_table:stop(),

    ensure_tcp_transport_stopped(TcpTransport),
    ensure_transport_stopped(Transport),
    i("done"),
    ok.



%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

ensure_transport_started() ->
%%     i("start diameter transport (top) supervisor"),
    case diameter_transport_sup:start_link() of
	{ok, TransportSup} ->
	    TransportSup;
	Error ->
	    ?FAIL({failed_starting_transport_sup, Error})
    end.

ensure_transport_stopped(Pid) when is_pid(Pid) ->
%%     i("stop diameter transport (top) supervisor"),
    Stop = fun(P) -> exit(P, kill) end,
    ensure_stopped(Pid, Stop, failed_stopping_transport_sup).

ensure_tcp_transport_started() ->
%%     i("start diameter TCP transport"),
    case diameter_tcp:start_transport() of
	{ok, TcpTransport} when is_pid(TcpTransport) ->
	    TcpTransport;
	Error ->
	    ?FAIL({failed_starting_transport, Error})
    end.

ensure_tcp_transport_stopped(Pid) when is_pid(Pid) ->
%%     i("stop diameter TCP transport supervisor"),
    Stop = fun(P) -> diameter_tcp:stop_transport(P) end,
    ensure_stopped(Pid, Stop, failed_stopping_tcp_transport).


ensure_tcp_listener() ->
%%     i("create diameter TCP transport listen socket"),
    case listen([{port, 0}]) of
	{ok, Acceptor} ->
	    [{Acceptor, Info}] = diameter_tcp:which_listeners(),
	    case lists:keysearch(socket, 1, Info) of
		{value, {_, Listen}} ->
		    {ok, Port} = inet:port(Listen),
		    {Acceptor, Port};
		_ ->
		    ?FAIL({failed_retrieving_listen_socket, Info})
	    end;
	Error ->
	    ?FAIL({failed_creating_listen_socket, Error})
    end.


ensure_stopped(Pid, Stop, ReasonTag) when is_pid(Pid) ->
%%     i("ensure_stopped -> create monitor to ~p", [Pid]),
    Ref = erlang:monitor(process, Pid),
%%     i("ensure_stopped -> try stop"),
    Stop(Pid), 
%%     i("ensure_stopped -> await DOWN message"),
    receive
	{'DOWN', Ref, process, Pid, _} ->
%% 	    i("ensure_stopped -> received DOWN message"),
	    ok
    after 5000 ->
%% 	    i("ensure_stopped -> timeout"),
	    ?FAIL({ReasonTag, Pid})
    end.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

listen(Opts) ->
    diameter_tcp:listen([{module, ?MODULE} | Opts]).

connect(Opts) ->
    diameter_tcp:connect([{module, ?MODULE} | Opts]).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

up(Pid, Host, Port) ->
    Pid ! {diameter, {up, Host, Port}},
    ok.

up(Pid) ->
    Pid ! {diameter, {up, self()}},
    ok.

recv(Pid, Pkt) ->
    Pid ! {diameter, {recv, Pkt}}.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

i(F) ->
    i(F, []).

i(F, A) ->
    io:format(F ++ "~n", A).


ok({ok, Whatever}) ->
    Whatever;
ok(Crap) ->
    Crap.