%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2002-2020. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%
%%

%%
%%----------------------------------------------------------------------
%% Purpose: Verify the application specifics of the Megaco application
%%----------------------------------------------------------------------

-module(megaco_mib_SUITE).

-export([
	 suite/0, all/0, groups/0,
         init_per_suite/1, end_per_suite/1,
         init_per_group/2, end_per_group/2,
         init_per_testcase/2, end_per_testcase/2,

         plain/1,
         connect/1,
         traffic/1,

         mg/3,
         mgc/3,

         handle_connect/3,
         handle_disconnect/4,
         handle_syntax_error/4,
         handle_message_error/4,
         handle_trans_request/4,
         handle_trans_long_request/4,
         handle_trans_reply/5,
         handle_trans_ack/5
        ]).

-include_lib("megaco/include/megaco.hrl").
-include_lib("megaco/include/megaco_message_v1.hrl").
-include("megaco_test_lib.hrl").

-define(TEST_VERBOSITY, info). % silence | info | debug
-define(MGC_VERBOSITY,  debug).
-define(MG_VERBOSITY,   debug).

-define(LOAD_COUNTER_START, 100).
-define(A4444, ["11111111", "00000000", "00000000"]).

-record(mgc, {parent  = undefined,
	      tcp_sup = undefined,
	      udp_sup = undefined,
	      mid     = undefined,
	      mg      = []}).
-record(mg, {parent       = undefined,
	     mid          = undefined,
	     conn_handle  = undefined,
	     state        = initiated,
	     load_counter = 0}).


%%======================================================================
%% Common Test interface functions
%%======================================================================

suite() -> 
    [{ct_hooks, [ts_install_cth]}].

all() -> 
    [
     plain,
     connect,
     traffic
    ].

groups() -> 
    [].



%%
%% -----
%%

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

    ?ANNOUNCE_SUITE_INIT(),

    p("init_per_suite -> entry with"
      "~n      Config: ~p"
      "~n      Nodes:  ~p", [Config0, erlang:nodes()]),

    case ?LIB:init_per_suite(Config0) of
        {skip, _} = SKIP ->
            SKIP;

        Config1 when is_list(Config1) ->

            %% We need a (local) monitor on this node also
            megaco_test_sys_monitor:start(),

            p("init_per_suite -> end when"
              "~n      Config: ~p"
              "~n      Nodes:  ~p", [Config1, erlang:nodes()]),

            Config1
    end.

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

    p("end_per_suite -> entry with"
      "~n      Config: ~p"
      "~n      Nodes:  ~p", [Config0, erlang:nodes()]),

    megaco_test_sys_monitor:stop(),
    Config1 = ?LIB:end_per_suite(Config0),

    p("end_per_suite -> end when"
      "~n      Nodes:  ~p", [erlang:nodes()]),

    Config1.


%%
%% -----
%%

init_per_group(_GroupName, Config) ->
    Config.

end_per_group(_GroupName, Config) ->
    Config.



%%
%% -----
%%

init_per_testcase(Case, Config) ->
    process_flag(trap_exit, true),

    progress("init_per_testcase -> ~w", [Case]),

    megaco_test_global_sys_monitor:reset_events(),

    case Case of
	traffic ->
	    Conf0 = lists:keydelete(tc_timeout, 1, Config),
	    Conf  = [{tc_timeout, timer:minutes(5)}|Conf0],
	    megaco_test_lib:init_per_testcase(Case, Conf);
	_ ->
	    megaco_test_lib:init_per_testcase(Case, Config)
    end.

end_per_testcase(Case, Config) ->
    process_flag(trap_exit, false),

    progress("end_per_testcase -> ~w", [Case]),

    p("system events during test: "
      "~n   ~p", [megaco_test_global_sys_monitor:events()]),

    megaco_test_lib:end_per_testcase(Case, Config).



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

plain(suite) ->
    [];
plain(doc) ->
    ["Test case for the basic statistics counter handling. "];
plain(Config) when is_list(Config) ->
    io:format("create test table 1~n", []),
    Tab1 = megaco_test_cnt1,
    megaco_stats:init(Tab1),

    io:format("~ncreate test table 2~n", []),
    Tab2 = megaco_test_cnt2,
    megaco_stats:init(Tab2, [kalle, hobbe]),

    io:format("~ntable 1 increments~n", []),
    H1 = #megaco_conn_handle{local_mid  = {deviceName, "a"},
			     remote_mid = {deviceName, "b"}},
    H2 = #megaco_conn_handle{local_mid  = {deviceName, "a"},
			     remote_mid = {deviceName, "c"}},
    1 = megaco_stats:inc(Tab1, H1, sune),
    2 = megaco_stats:inc(Tab1, H2, sune, 2),
    3 = megaco_stats:inc(Tab1, H1, gurka, 3),
    4 = megaco_stats:inc(Tab1, H2, sune, 2),
    4 = megaco_stats:inc(Tab1, H1, gurka),

    io:format("~ntable 2 increments~n", []),
    H3 = #megaco_conn_handle{local_mid  = {deviceName, "e"},
			     remote_mid = {deviceName, "c"}},
    H4 = #megaco_conn_handle{local_mid  = {deviceName, "e"},
			     remote_mid = {deviceName, "d"}},
    1 = megaco_stats:inc(Tab2, H3, tomat),
    4 = megaco_stats:inc(Tab2, H3, tomat, 3),
    5 = megaco_stats:inc(Tab2, H4, paprika, 5),

    io:format("~ntable 2 global increments~n", []),
    1 = megaco_stats:inc(Tab2, kalle),
    1 = megaco_stats:inc(Tab2, hobbe),
    2 = megaco_stats:inc(Tab2, hobbe),
    2 = megaco_stats:inc(Tab2, kalle),
    3 = megaco_stats:inc(Tab2, kalle),
    4 = megaco_stats:inc(Tab2, hobbe, 2),
    
    io:format("~ntable 1 stats~n", []),
    {ok, Stats1} = megaco_stats:get_stats(Tab1),
    io:format("Stats1 = ~p~n", [Stats1]),
    {value, {H1, H1Stats}} = lists:keysearch(H1, 1, Stats1),
    Stats1_2 = lists:keydelete(H1, 1, Stats1),
    {value, {H2, H2Stats}} = lists:keysearch(H2, 1, Stats1_2), 
    Stats1_3 = lists:keydelete(H2, 1, Stats1_2),
    [] = Stats1_3,
    io:format("H1Stats = ~p~n", [H1Stats]),
    io:format("H2Stats = ~p~n", [H2Stats]),

    {value, {sune, 1}}  = lists:keysearch(sune, 1, H1Stats),
    H1Stats_2 = lists:keydelete(sune, 1, H1Stats),
    {value, {gurka, 4}} = lists:keysearch(gurka, 1, H1Stats_2),
    H1Stats_3 = lists:keydelete(gurka, 1, H1Stats_2),
    [] = H1Stats_3,

    {value, {sune, 4}} = lists:keysearch(sune, 1, H2Stats),
    H2Stats_2 = lists:keydelete(sune, 1, H2Stats),
    [] = H2Stats_2,
    

    %% --
    io:format("~ntable 2 stats~n", []),
    {ok, Stats2} = megaco_stats:get_stats(Tab2),
    io:format("Stats2 = ~p~n", [Stats2]),
    {ok, 3} = megaco_stats:get_stats(Tab2, kalle),
    {ok, 4} = megaco_stats:get_stats(Tab2, hobbe),
     

    %% --
    io:format("~ntable 1 reset stats for ~p~n", [H1]),
    megaco_stats:reset_stats(Tab1, H1),
    {ok, Stats1_4} = megaco_stats:get_stats(Tab1),
    io:format("Stats1_4 = ~p~n", [Stats1_4]),
    {ok, 0} = megaco_stats:get_stats(Tab1, H1, sune),
    {ok, 0} = megaco_stats:get_stats(Tab1, H1, gurka),
    

    %% --
    io:format("~ntable 2 reset stats for kalle and ~p~n", [H4]),
    megaco_stats:reset_stats(Tab2, kalle),
    megaco_stats:reset_stats(Tab2, H4),
    {ok, Stats2_2} = megaco_stats:get_stats(Tab2),
    io:format("Stats2_2 = ~p~n", [Stats2_2]),
    {ok, 0} = megaco_stats:get_stats(Tab2, kalle),
    {ok, 4} = megaco_stats:get_stats(Tab2, hobbe),
    {ok, 4} = megaco_stats:get_stats(Tab2, H3, tomat),
    {ok, 0} = megaco_stats:get_stats(Tab2, H4, paprika),
    {ok, Stats4_4} = megaco_stats:get_stats(Tab2, H4),
    io:format("Stats4_4 = ~p~n", [Stats4_4]),

    %% --
    io:format("~ntable 2 stats for nonexisting counters~n", []),
    {error, _} = megaco_stats:get_stats(Tab2, kalla),
    {error, _} = megaco_stats:get_stats(Tab2, H3, paprika),
    ok.


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

connect(suite) ->
    [];
connect(doc) ->
    [];
connect(Config) when is_list(Config) ->
    Pre = fun() ->
                  progress("start nodes"),
                  MgcNode = make_node_name(mgc),
                  Mg1Node = make_node_name(mg1),
                  Mg2Node = make_node_name(mg2),
                  d("connect -> Nodes: "
                    "~n   MgcNode: ~p"
                    "~n   Mg1Node: ~p"
                    "~n   Mg2Node: ~p", [MgcNode, Mg1Node, Mg2Node]),
                  Nodes = [MgcNode, Mg1Node, Mg2Node],
                  ok = ?START_NODES(Nodes, true),
                  Nodes
          end,
    Case = fun do_connect/1,
    Post = fun(Nodes) ->
                   progress("stop nodes"),
                   d("stop nodes"),
                   ?STOP_NODES(lists:reverse(Nodes))
           end,
    try_tc(connect, Pre, Case, Post).

do_connect([MgcNode, Mg1Node, Mg2Node]) ->
    %% Start the MGC and MGs

    ET = [{text, tcp}, {text, udp}, {binary, tcp}, {binary, udp}],
    progress("start MGC (on ~p)", [MgcNode]),
    {ok, Mgc} = 
	start_mgc(MgcNode, {deviceName, "ctrl"}, ET, ?MGC_VERBOSITY),

    progress("start MG1 (on ~p) using tcp", [Mg1Node]),
    {ok, Mg1} = 
	start_mg(Mg1Node,  {deviceName, "mg1"}, text,   tcp, ?MG_VERBOSITY),

    progress("start MG2 (on ~p) using udp", [Mg2Node]),
    {ok, Mg2} = 
	start_mg(Mg2Node,  {deviceName, "mg2"}, binary, udp, ?MG_VERBOSITY),

    %% Collect the initial statistics (should be zero if anything)
    progress("collect initial MG1 stats"),
    {ok, Mg1Stats0} = get_stats(Mg1, 1),
    d("connect  -> stats for Mg1: ~n~p", [Mg1Stats0]),
    progress("collect initial MG2 stats"),
    {ok, Mg2Stats0} = get_stats(Mg2, 1),
    d("connect  -> stats for Mg2: ~n~p", [Mg2Stats0]),
    progress("collect initial MGC stats"),
    {ok, MgcStats0} = get_stats(Mgc, 1),
    d("connect  -> stats for Mgc: ~n~p", [MgcStats0]),

    %% Ask Mg1 to do a service change
    progress("perform MG1 service change"),
    {ok, Res1} = service_change(Mg1),
    d("connect -> (Mg1) service change result: ~p", [Res1]),

    %% Collect the statistics
    progress("collect MG1 statistics (after service change)"),
    {ok, Mg1Stats1} = get_stats(Mg1, 1),
    d("connect  -> stats for Mg1: ~n~p", [Mg1Stats1]),
    progress("collect MGC statistics (after MG1 service change)"),
    {ok, MgcStats1} = get_stats(Mgc, 1),
    d("connect  -> stats (1) for Mgc: ~n~p", [MgcStats1]),
    {ok, MgcStats2} = get_stats(Mgc, 2),
    d("connect  -> stats (2) for Mgc: ~n~p", [MgcStats2]),

    %% Ask Mg2 to do a service change
    progress("perform MG2 service change"),
    {ok, Res2} = service_change(Mg2),
    d("connect -> (Mg2) service change result: ~p", [Res2]),

    %% Collect the statistics
    progress("collect MG2 statistics (after service change)"),
    {ok, Mg2Stats1} = get_stats(Mg2, 1),
    d("connect  -> stats for Mg1: ~n~p", [Mg2Stats1]),
    progress("collect MGC statistics (after MG2 service change)"),
    {ok, MgcStats3} = get_stats(Mgc, 1),
    d("connect  -> stats (1) for Mgc: ~n~p", [MgcStats3]),
    {ok, MgcStats4} = get_stats(Mgc, 2),
    d("connect  -> stats (2) for Mgc: ~n~p", [MgcStats4]),

    %% Tell Mg1 to stop
    progress("stop MG1"),
    stop(Mg1),

    %% Collect the statistics
    progress("collect MGC statistics (after MG1 stop)"),
    {ok, MgcStats5} = get_stats(Mgc, 1),
    d("connect  -> stats (1) for Mgc: ~n~p", [MgcStats5]),
    {ok, MgcStats6} = get_stats(Mgc, 2),
    d("connect  -> stats (2) for Mgc: ~n~p", [MgcStats6]),

    %% Tell Mg2 to stop
    progress("stop MG2"),
    stop(Mg2),

    %% Collect the statistics
    progress("collect MGC statistics (after MG2 stop)"),
    {ok, MgcStats7} = get_stats(Mgc, 1),
    d("connect  -> stats (1) for Mgc: ~n~p", [MgcStats7]),
    {ok, MgcStats8} = get_stats(Mgc, 2),
    d("connect  -> stats (2) for Mgc: ~n~p", [MgcStats8]),

    %% Tell Mgc to stop
    progress("stop MGC"),
    stop(Mgc),

    i("connect -> done", []),
    ok.



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

traffic(suite) ->
    [];
traffic(doc) ->
    [];
traffic(Config) when is_list(Config) ->
    Pre = fun() ->
                  progress("start nodes"),
                  MgcNode = make_node_name(mgc),
                  Mg1Node = make_node_name(mg1),
                  Mg2Node = make_node_name(mg2),
                  Mg3Node = make_node_name(mg3),
                  Mg4Node = make_node_name(mg4),
                  Nodes = [MgcNode, Mg1Node, Mg2Node, Mg3Node, Mg4Node],
                  d("traffic -> Nodes: "
                    "~n   MgcNode: ~p"
                    "~n   Mg1Node: ~p"
                    "~n   Mg2Node: ~p"
                    "~n   Mg3Node: ~p"
                    "~n   Mg4Node: ~p", 
                    [MgcNode, Mg1Node, Mg2Node, Mg3Node, Mg4Node]),
                  ok = ?START_NODES(Nodes, true),
                  Nodes
          end,
    Case = fun do_traffic/1,
    Post = fun(Nodes) ->
                   progress("stop nodes"),
                   d("stop nodes"),
                   ?STOP_NODES(lists:reverse(Nodes))
           end,
    try_tc(traffic, Pre, Case, Post).

do_traffic([MgcNode, Mg1Node, Mg2Node, Mg3Node, Mg4Node]) ->
    %% Start the MGC and MGs
    i("start the MGC"),    
    progress("start MGC (on ~p)", [MgcNode]),
    ET = [{text,tcp}, {text,udp}, {binary,tcp}, {binary,udp}],
    {ok, Mgc} = 
	start_mgc(MgcNode, {deviceName, "ctrl"}, ET, ?MGC_VERBOSITY),

    i("traffic -> start and connect the MGs"),    
    progress("start and connect MGs"),
    MgConf0 = [{Mg1Node, "mg1", text,   tcp},
	       {Mg2Node, "mg2", text,   udp},
	       {Mg3Node, "mg3", binary, tcp},
	       {Mg4Node, "mg4", binary, udp}],
    MgConf = traffic_connect_mg(MgConf0, []),

    %% Collect and check the MGs statistics
    i("traffic -> collect and check the MGs stats"),    
    progress("collect and verify MGs (initial) stats"),
    traffic_verify_mg_stats(MgConf, 1, 1),

    %% Collect and check the MGC statistics
    i("traffic -> collect and check the MGC (initial) stats"),    
    progress("collect and verify MGC stats"),
    {ok, MgcStats1} = get_stats(Mgc, 1),
    d("traffic  -> stats (1) for Mgc: ~n~p~n", [MgcStats1]),
    traffic_verify_mgc_stats(Mgc, 1, 1),


    ?SLEEP(1000),


    %% And apply some load 
    i("traffic -> apply traffic load (1)"),
    progress("apply some load (1)"),
    ok = traffic_apply_load(MgConf),

    %% Await completion of load part and the collect traffic
    i("traffic -> await load (1) competion"),
    progress("await load (1) completion"),
    ok = traffic_await_load_complete(MgConf),


    ?SLEEP(1000),


    i("traffic -> collect and check the MGs statistics"),
    progress("collect and verify MGs (after load 1) stats"),
    traffic_verify_mg_stats(MgConf, 
			    1 + ?LOAD_COUNTER_START, 
			    1 + ?LOAD_COUNTER_START),

    i("traffic -> collect and check the MGC statistics"),
    progress("collect and verify MGC (after load 1) stats"),
    {ok, MgcStats3} = get_stats(Mgc, 1),
    d("traffic  -> stats (1) for Mgc: ~n~p~n", [MgcStats3]),
    traffic_verify_mgc_stats(Mgc, 
			     1 + ?LOAD_COUNTER_START, 
			     1 + ?LOAD_COUNTER_START),


    ?SLEEP(1000),


    %% Reset counters
    i("traffic -> reset the MGs statistics"),
    progress("reset MGs stats"),
    traffic_reset_mg_stats(MgConf),
    i("traffic -> collect and check the MGs statistics"),
    progress("collect and verify MGs (after reset) stats"),
    traffic_verify_mg_stats(MgConf, 0, 0),

    i("traffic -> reset the MGC statistics"),
    progress("reset MGC stats"),
    traffic_reset_mgc_stats(Mgc),
    i("traffic -> collect and check the MGC statistics"),
    progress("collect and verify MGC (after reset) stats"),
    traffic_verify_mgc_stats(Mgc, 0, 0),


    ?SLEEP(1000),


    %% And apply some load 
    i("traffic -> apply traffic load (2)"),
    progress("apply some load (2)"),
    ok = traffic_apply_load(MgConf),

    %% Await completion of load part and the collect traffic
    i("traffic -> await load (2) competion"),
    progress("await load (2) completion"),
    ok = traffic_await_load_complete(MgConf),


    ?SLEEP(1000),


    i("traffic -> collect and check the MGs statistics"),
    progress("collect and verify MGs (after load 2) stats"),
    traffic_verify_mg_stats(MgConf, 
			    ?LOAD_COUNTER_START, 
			    ?LOAD_COUNTER_START),

    i("traffic -> collect and check the MGC statistics"),
    progress("collect and verify MGC (after load 2) stats"),
    traffic_verify_mgc_stats(Mgc, 
			     ?LOAD_COUNTER_START, 
			     ?LOAD_COUNTER_START),


    ?SLEEP(1000),


    %% Tell MGs to stop
    i("traffic -> stop the MGs"),
    progress("stop MGs"),
    traffic_stop_mg(MgConf),


    ?SLEEP(1000),


    %% Collect the statistics
    i("traffic -> collect the MGC statistics"),
    progress("collect and verify MGC (after MGs stop) stats"),
    {ok, MgcStats7} = get_stats(Mgc, 1),
    d("traffic -> stats (1) for Mgc: ~n~p~n", [MgcStats7]),
    {ok, MgcStats8} = get_stats(Mgc, 2),
    d("traffic -> stats (2) for Mgc: ~n~p~n", [MgcStats8]),

    %% Tell Mgc to stop
    i("traffic -> stop the MGC"),
    progress("stop MGC"),
    stop(Mgc),

    i("traffic -> done", []),
    ok.


traffic_verify_mgc_stats(Pid, Out, In) 
  when is_pid(Pid) andalso is_integer(Out) andalso is_integer(In) ->
    d("traffic_verify_mgc_stats -> entry with"
      "~n   Out:   ~p"
      "~n   In:    ~p", [Out, In]),
    {ok, Stats} = get_stats(Pid, 2),
    d("traffic_verify_mgc_stats -> stats (2) for Mgc: ~n~p~n", [Stats]),
    traffic_verify_mgc_stats(Stats, Out, In);
    
traffic_verify_mgc_stats(Stats, Out, In) when is_list(Stats) ->
    d("traffic_verify_mgc_stats -> checking stats"),
    Gen   = traffic_verify_get_stats(gen, Stats),
    Trans = traffic_verify_get_stats(trans, Stats),
    traffic_verify_mgc_stats_gen(Gen),
    traffic_verify_mgc_stats_trans(Trans, Out, In).

traffic_verify_mgc_stats_gen([]) ->
    d("traffic_verify_mgc_stats_gen -> done"),
    ok;
traffic_verify_mgc_stats_gen([{medGwyGatewayNumErrors, 0}|Stats]) ->
    traffic_verify_mgc_stats_gen(Stats);
traffic_verify_mgc_stats_gen([{medGwyGatewayNumErrors, Val}|_]) ->
    exit({global_error_counter, Val, mgc});
traffic_verify_mgc_stats_gen([{Handle, Counters}|Stats]) ->
    N = {mgc, Handle, Handle},
    traffic_verify_counter(N, medGwyGatewayNumErrors, Counters, 0),
    traffic_verify_mgc_stats_gen(Stats).


traffic_verify_mgc_stats_trans([], _Out, _In) ->    
    ok;
traffic_verify_mgc_stats_trans([{Mod, Stats}|MgcStats], Out, In) ->    
    d("traffic_verify_mgc_stats_trans -> entry with"
      "~n   Mod:   ~p"
      "~n   Stats: ~p", [Mod, Stats]),
    traffic_verify_mgc_stats_trans(Mod, Stats, Out, In),
    traffic_verify_mgc_stats_trans(MgcStats, Out, In).

traffic_verify_mgc_stats_trans(_Mod, [], _Out, _In) ->
    ok;
traffic_verify_mgc_stats_trans(Mod, [{Handle,Counters}|Stats], Out, In) ->
    N = {mgc, Mod, Handle},
    traffic_verify_counter(N, medGwyGatewayNumErrors, Counters, 0),
    traffic_verify_counter(N, medGwyGatewayNumOutMessages, Counters, Out),
    traffic_verify_counter(N, medGwyGatewayNumInMessages, Counters, In),
    traffic_verify_mgc_stats_trans(Mod, Stats, Out, In).
    

traffic_verify_mg_stats(MgConf, Out, In) 
  when is_integer(Out) andalso is_integer(In) ->
    d("traffic_verify_mg_stats -> entry with"
      "~n   Out:   ~p"
      "~n   In:    ~p", [Out, In]),
    Stats = traffic_get_mg_stats(MgConf, []),
    d("traffic_verify_mg_stats -> stats for MGs: ~n~p", [Stats]),
    traffic_verify_mg_stats1(Stats, Out, In).
    
traffic_verify_mg_stats1([], _, _) ->
    ok;
traffic_verify_mg_stats1([{Name, Stats}|MgStats], Out, In) ->
    d("traffic_verify_mg_stats1 -> entry with"
      "~n   Name:  ~s"
      "~n   Stats: ~p", [Name, Stats]),
    Gen   = traffic_verify_get_stats(gen, Stats),
    Trans = traffic_verify_get_stats(trans, Stats),
    traffic_verify_mg_stats_gen(Name, Gen),
    traffic_verify_mg_stats_trans(Name, Trans, Out, In),
    traffic_verify_mg_stats1(MgStats, Out, In).

traffic_verify_mg_stats_gen(Mg, []) ->
    d("traffic_verify_mg_stats_gen -> ~s checked out OK",[Mg]),
    ok;
traffic_verify_mg_stats_gen(Mg, [{medGwyGatewayNumErrors, 0}|Stats]) ->
    traffic_verify_mg_stats_gen(Mg, Stats);
traffic_verify_mg_stats_gen(Mg, [{medGwyGatewayNumErrors, Val}|_]) ->
    exit({global_error_counter, Val, Mg});
traffic_verify_mg_stats_gen(Mg, [{_Handle, Counters}|Stats]) ->
    traffic_verify_counter(Mg, medGwyGatewayNumErrors, Counters, 0),
    traffic_verify_mg_stats_gen(Mg, Stats).

traffic_verify_mg_stats_trans(Mg, Counters, Out, In) ->
    traffic_verify_counter(Mg, medGwyGatewayNumErrors,      Counters, 0),
    traffic_verify_counter(Mg, medGwyGatewayNumOutMessages, Counters, Out),
    traffic_verify_counter(Mg, medGwyGatewayNumInMessages,  Counters, In).


traffic_verify_get_stats(S, Stats) ->
    case lists:keysearch(S, 1, Stats) of
	{value, {S, Val}} ->
	    Val;
	false ->
	    exit({not_found, S, Stats})
    end.
    
traffic_verify_counter(Name, Counter, Counters, Expected) ->
    case lists:keysearch(Counter, 1, Counters) of
	{value, {Counter, Expected}} ->
            i("counter ~w verified for ~p", [Counter, Name]),
	    ok;
	{value, {Counter, Val}} ->
            i("counter ~w *not* verified for ~p: "
              "~n   Expected: ~w"
              "~n   Actual:   ~w", [Counter, Name, Expected, Val]),
	    exit({illegal_counter_value, Counter, Val, Expected, Name});
	false ->
            i("counter ~w *not* found for ~p", [Counter, Name]),
	    exit({not_found, Counter, Counters, Name, Expected})
    end.
    

traffic_connect_mg([], Acc) ->
    lists:reverse(Acc);
traffic_connect_mg([{Node, Name, Coding, Trans}|Mg], Acc) ->
    Pid = traffic_connect_mg(Node, Name, Coding, Trans),
    traffic_connect_mg(Mg, [{Name, Pid}|Acc]).

traffic_connect_mg(Node, Name, Coding, Trans) ->
    progress("start (and connect) ~s (on ~p) using ~p", [Name, Node, Trans]),
    Mid = {deviceName, Name}, 
    {ok, Pid} = start_mg(Node, Mid, Coding, Trans, ?MG_VERBOSITY),

    %% Ask the MGs to do a service change
    progress("perform ~s service change", [Name]),
    {ok, Res} = service_change(Pid),
    d("traffic_connect_mg -> (~s) service change result: ~p", [Name, Res]),
    Pid.


traffic_stop_mg(MGs) ->
    [stop(Pid) || {_Name, Pid} <- MGs].


traffic_get_mg_stats([], Acc) ->
    lists:reverse(Acc);
traffic_get_mg_stats([{Name, Pid}|Mgs], Acc) ->
    {ok, Stats} = get_stats(Pid, 1),
    d("traffic_get_mg_stats -> stats for ~s: "
      "~n   ~p"
      "~n", [Name, Stats]),
    traffic_get_mg_stats(Mgs, [{Name, Stats}|Acc]).


traffic_apply_load([]) ->
    ok;
traffic_apply_load([{_,MG}|MGs]) ->
    MG ! {apply_load, self(), ?LOAD_COUNTER_START},
    receive
	{apply_load_ack, MG} ->
	    traffic_apply_load(MGs);
	{'EXIT', MG, Reason} ->
	    exit({mg_exit, MG, Reason})
    after 10000 ->
	    exit({apply_load_ack_timeout, MG})
    end.


traffic_reset_mg_stats([]) ->
    ok;
traffic_reset_mg_stats([{Name, Pid}|MGs]) ->
    d("traffic_reset_mg_stats -> resetting ~s", [Name]),
    traffic_reset_stats(Pid),
    traffic_reset_mg_stats(MGs).

traffic_reset_mgc_stats(Mgc) ->
    d("traffic_reset_mgc_stats -> resetting ~p", [Mgc]),
    traffic_reset_stats(Mgc).
    
traffic_reset_stats(Pid) ->
    Pid ! {reset_stats, self()},
    receive
	{reset_stats_ack, Pid} ->
	    ok;
	{'EXIT', Pid, Reason} ->
	    exit({client_exit, Pid, Reason})
    after 10000 ->
	    exit({reset_stats_ack_timeout, Pid})
    end.

    
traffic_await_load_complete([]) ->
    ok;
traffic_await_load_complete(MGs0) ->
    receive
	{load_complete, Pid} ->
	    d("received load_complete from ~p", [Pid]),
	    MGs1 = lists:keydelete(Pid, 2, MGs0),
	    traffic_await_load_complete(lists:delete(Pid, MGs1));
	{'EXIT', Pid, Reason} ->
	    i("exit signal from ~p: ~p", [Pid, Reason]),
	    case lists:keymember(Pid, 2, MGs0) of
		true ->
		    exit({mg_exit, Pid, Reason});
		false ->
		    MGs1 = lists:keydelete(Pid, 2, MGs0),
		    traffic_await_load_complete(lists:delete(Pid, MGs1))
	    end
    end.
	    

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

make_node_name(Name) ->
    case string:tokens(atom_to_list(node()), [$@]) of
	[_,Host] ->
	    list_to_atom(lists:concat([atom_to_list(Name) ++ "@" ++ Host]));
	_ ->
	    exit("Test node must be started with '-sname'")
     end.


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

start_mgc(Node, Mid, ET, Verbosity) ->
    d("start mgc[~p]: ~p", [Node, Mid]),
    RI = {receive_info, mk_recv_info(ET)},
    Config = [{local_mid, Mid}, RI],
    Pid = spawn_link(Node, ?MODULE, mgc, [self(), Verbosity, Config]),
    await_started(Pid).

mk_recv_info(ET) ->
    mk_recv_info(ET, []).

mk_recv_info([], Acc) ->
    Acc;
mk_recv_info([{text,tcp}|ET], Acc) ->
    RI = [{encoding_module, megaco_pretty_text_encoder},
	  {encoding_config, []},
	  {transport_module, megaco_tcp},
	  {port, 2944}],
    mk_recv_info(ET, [RI|Acc]);
mk_recv_info([{text,udp}|ET], Acc) ->
    RI = [{encoding_module, megaco_pretty_text_encoder},
	  {encoding_config, []},
	  {transport_module, megaco_udp},
	  {port, 2944}],
    mk_recv_info(ET, [RI|Acc]);
mk_recv_info([{binary,tcp}|ET], Acc) ->
    RI = [{encoding_module, megaco_ber_encoder},
	  {encoding_config, []},
	  {transport_module, megaco_tcp},
	  {port, 2945}],
    mk_recv_info(ET, [RI|Acc]);
mk_recv_info([{binary,udp}|ET], Acc) ->
    RI = [{encoding_module, megaco_ber_encoder},
	  {encoding_config, []},
	  {transport_module, megaco_udp},
	  {port, 2945}],
    mk_recv_info(ET, [RI|Acc]);
mk_recv_info([ET|_], _) ->
    throw({error, {invaalid_encoding_transport, ET}}).

mgc(Parent, Verbosity, Config) ->
    process_flag(trap_exit, true),
    put(verbosity, Verbosity),
    put(sname,   "MGC"),
    i("mgc -> starting"),
    {Mid, TcpSup, UdpSup} = mgc_init(Config),
    notify_started(Parent),
    S = #mgc{parent = Parent, 
	     tcp_sup = TcpSup, udp_sup = UdpSup, mid = Mid},
    i("mgc -> started"),
    mgc_loop(S).

mgc_init(Config) ->
    d("mgc_init -> entry"),
    Mid = get_conf(local_mid, Config),
    RI  = get_conf(receive_info, Config),
    i("mgc_init -> start megaco"),
    application:start(megaco),
    d("mgc_init -> start megaco user"),
    megaco:start_user(Mid, []),
    d("mgc_init -> update user info (user_mod)"),
    megaco:update_user_info(Mid, user_mod,  ?MODULE),
    d("mgc_init -> update user info (user_args)"),
    megaco:update_user_info(Mid, user_args, [self()]),
    d("mgc_init -> get user info (receive_handle)"),
    RH = megaco:user_info(Mid,receive_handle),
    d("mgc_init -> parse receive info"),
    ListenTo = mgc_parse_receive_info(RI, RH),
    i("mgc_init -> start transport(s) with:"
      "~n      ListenTo: ~p", [ListenTo]),
    {Tcp, Udp} = mgc_start_transports(ListenTo),
    i("mgc_init -> transport(s) started:"
      "~n      Tcp: ~p"
      "~n      Udp: ~p", [Tcp, Udp]),
    {Mid, Tcp, Udp}.
    

mgc_loop(S) ->
    d("mgc_loop -> await request"),
    receive
	{stop, Parent} when S#mgc.parent == Parent ->
	    i("mgc_loop -> stopping", []),
  	    Mid = S#mgc.mid,
	    (catch mgc_close_conns(Mid)),
	    megaco:stop_user(Mid),
	    application:stop(megaco),
	    i("mgc_loop -> stopped", []),
	    Parent ! {stopped, self()},
	    exit(normal);



	%% Reset stats
	{reset_stats, Parent} when S#mgc.parent == Parent ->
	    i("mgc_loop -> got request to reset stats counters"),
	    mgc_reset_stats(S#mgc.mid),
	    Parent ! {reset_stats_ack, self()},
	    mgc_loop(S);


	%% Give me statistics
	{statistics, 1, Parent} when S#mgc.parent == Parent ->
	    i("mgc_loop -> got request for statistics 1"),
	    {ok, Gen} = megaco:get_stats(),
	    GetTrans = 
		fun(CH) ->
			Reason = {statistics, CH}, 
			Pid = megaco:conn_info(CH, control_pid),
			SendMod = megaco:conn_info(CH, send_mod),
			SendHandle = megaco:conn_info(CH, send_handle),
			{ok, Stats} = 
			    case SendMod of
				megaco_tcp -> megaco_tcp:get_stats(SendHandle);
				megaco_udp -> megaco_udp:get_stats(SendHandle);
				SendMod    -> exit(Pid, Reason)
			    end,
			{SendHandle, Stats}
		end,
	    Mid = S#mgc.mid,
	    Trans = 
		lists:map(GetTrans, megaco:user_info(Mid, connections)),
	    Parent ! {statistics, 1, [{gen, Gen}, {trans, Trans}], self()},
	    mgc_loop(S);


	{statistics, 2, Parent} when S#mgc.parent == Parent ->
	    i("mgc_loop -> got request for statistics 2"),
	    {ok, Gen} = megaco:get_stats(),
	    #mgc{tcp_sup = TcpSup, udp_sup = UdpSup} = S,
	    TcpStats = get_trans_stats(TcpSup, megaco_tcp),
	    UdpStats = get_trans_stats(UdpSup, megaco_udp),
	    Parent ! {statistics, 2, [{gen, Gen}, {trans, [TcpStats, UdpStats]}], self()},
	    mgc_loop(S);


	%% Megaco callback messages
	{request, Request, From} ->
	    d("mgc_loop -> received megaco request: ~n~p~n   From: ~p", 
	      [Request, From]),
	    Reply = mgc_handle_request(Request),
	    d("mgc_loop -> send request reply: ~n~p", [Reply]),
	    From ! {reply, Reply, self()},
	    mgc_loop(S);


	{'EXIT', Pid, Reason} ->
	    error_msg("MGC received unexpected exit signal from ~p:~n~p", 
		      [Pid, Reason]),
	    mgc_loop(S);


	Invalid ->
	    i("mgc_loop -> received invalid request: ~p", [Invalid]),
	    mgc_loop(S)
    end.


mgc_reset_stats(Mid) ->
    megaco:reset_stats(),
    mgc_reset_trans_stats(megaco:user_info(Mid, connections), []).

mgc_reset_trans_stats([], _Reset) ->
    ok;
mgc_reset_trans_stats([CH|CHs], Reset) ->
    SendMod = megaco:conn_info(CH, send_mod),
    case lists:member(SendMod, Reset) of
	true ->
	    mgc_reset_trans_stats(CHs, Reset);
	false ->
	    SendMod:reset_stats(),
	    mgc_reset_trans_stats(CHs, [SendMod|Reset])
    end.
	    

mgc_close_conns(Mid) ->
    Reason = {self(), ignore},
    Disco  = fun(CH) ->
		     (catch mgc_close_conn(CH, Reason))
	     end,
    lists:map(Disco, megaco:user_info(Mid, connections)).

mgc_close_conn(CH, Reason) ->
    d("close connection to ~p", [CH#megaco_conn_handle.remote_mid]),
    Pid        = megaco:conn_info(CH, control_pid),
    SendMod    = megaco:conn_info(CH, send_mod),
    SendHandle = megaco:conn_info(CH, send_handle),
    megaco:disconnect(CH, Reason),
    case SendMod of
	megaco_tcp -> megaco_tcp:close(SendHandle);
	megaco_udp -> megaco_udp:close(SendHandle);
	SendMod    -> exit(Pid, Reason)
    end.
    
get_trans_stats(P, SendMod) when is_pid(P) ->
    case (catch SendMod:get_stats()) of
	{ok, Stats} ->
	    {SendMod, Stats};
	Else ->
	    {SendMod, Else}
    end;
get_trans_stats(_P, SendMod) ->
    {SendMod, undefined}.

mgc_parse_receive_info([], _RH) ->
    throw({error, no_receive_info});
mgc_parse_receive_info(RI, RH) ->
    mgc_parse_receive_info(RI, RH, []).

mgc_parse_receive_info([], _RH, ListenTo) ->
    ListenTo;
mgc_parse_receive_info([RI|RIs], RH, ListenTo) ->
    d("mgc_parse_receive_info -> parse receive info"),
    RH1 = mgc_parse_receive_info1(RI, RH),
    case (catch mgc_parse_receive_info1(RI, RH)) of
	{error, Reason} ->
	    i("failed parsing receive info: ~p~n~p", [RI, Reason]),
	    exit({failed_parsing_recv_info, RI, Reason});
	RH1 ->
	    mgc_parse_receive_info(RIs, RH, [RH1|ListenTo])
    end.
    
mgc_parse_receive_info1(RI, RH) ->
    d("mgc_parse_receive_info1 -> get encoding module"),
    EM = get_encoding_module(RI),
    d("mgc_parse_receive_info1 -> get encoding config"),
    EC = get_encoding_config(RI, EM),
    d("mgc_parse_receive_info1 -> get transport module"),
    TM = get_transport_module(RI),
    d("mgc_parse_receive_info1 -> get transport port"),
    TP = get_transport_port(RI),
    RH1 = RH#megaco_receive_handle{send_mod        = TM,
				   encoding_mod    = EM,
				   encoding_config = EC},
    {TP, RH1}.


mgc_start_transports([]) ->
    throw({error, no_transport});
mgc_start_transports(ListenTo) ->
    mgc_start_transports(ListenTo, undefined, undefined).
    

mgc_start_transports([], TcpSup, UdpSup) ->
    {TcpSup, UdpSup};
mgc_start_transports([{Port, RH}|ListenTo], TcpSup, UdpSup) 
  when RH#megaco_receive_handle.send_mod == megaco_tcp ->
    TcpSup1 = mgc_start_tcp(RH, Port, TcpSup),
    mgc_start_transports(ListenTo, TcpSup1, UdpSup);
mgc_start_transports([{Port, RH}|ListenTo], TcpSup, UdpSup) 
  when RH#megaco_receive_handle.send_mod == megaco_udp ->
    UdpSup1 = mgc_start_udp(RH, Port, UdpSup),
    mgc_start_transports(ListenTo, TcpSup, UdpSup1);
mgc_start_transports([{_Port, RH}|_ListenTo], _TcpSup, _UdpSup) ->
    throw({error, {bad_send_mod, RH#megaco_receive_handle.send_mod}}).


mgc_start_tcp(RH, Port, undefined) ->
    i("start tcp transport"),
    case megaco_tcp:start_transport() of
	{ok, Sup} ->
	    mgc_start_tcp(RH, Port, Sup);
	Else ->
	    throw({error, {failed_starting_tcp_transport, Else}})
    end;
mgc_start_tcp(RH, Port, Sup) when is_pid(Sup) ->
    i("tcp listen on ~p", [Port]),
    Opts = [{port,           Port}, 
	    {receive_handle, RH}, 
	    {tcp_options,    [{nodelay, true}]}],
    mgc_tcp_create_listen(Sup, Opts, 3).

mgc_tcp_create_listen(Sup, Opts, N) ->
    mgc_tcp_create_listen(Sup, Opts, N, 1, undefined).

mgc_tcp_create_listen(_Sup, _Opts, N, N, InitialReason) ->
    d("failed creating mgc tcp listen socket after ~p tries: ~p", 
      [N, InitialReason]),
    throw({error, {failed_starting_tcp_listen, InitialReason}});
mgc_tcp_create_listen(Sup, Opts, MaxN, N, _InitialReason) 
  when is_integer(N) andalso is_integer(MaxN) andalso (MaxN > N) ->
    d("try create mgc tcp listen socket [~w]", [N]),
    case megaco_tcp:listen(Sup, Opts) of
	ok ->
	    Sup;
	{error, {could_not_start_listener, {gen_tcp_listen, eaddrinuse} = Reason}} ->
	    ?SLEEP(N * 200),
	    mgc_tcp_create_listen(Sup, Opts, MaxN, N + 1, Reason);
	{error, Reason} ->
	    throw({error, {failed_starting_tcp_listen, Reason}});
	Else ->
	    throw({error, {failed_starting_tcp_listen, Else}})
    end.


mgc_start_udp(RH, Port, undefined) ->
    i("start udp transport"),
    case megaco_udp:start_transport() of
	{ok, Sup} ->
	    mgc_start_udp(RH, Port, Sup);
	Else ->
	    throw({error, {failed_starting_udp_transport, Else}})
    end;
mgc_start_udp(RH, Port, Sup) ->
    i("open udp ~p", [Port]),
    Opts = [{port, Port}, {receive_handle, RH}],
    case megaco_udp:open(Sup, Opts) of
	{ok, _SendHandle, _ControlPid} ->
	    Sup;
	Else ->
	    exit({error, {failed_starting_udp_listen, Else}})
    end.



%% -----------------------
%% Handle megaco callbacks
%%

mgc_handle_request({handle_connect, _CH, _PV}) ->
    ok;
mgc_handle_request({handle_disconnect, CH, _PV, R}) ->
    megaco:cancel(CH, R), % Cancel the outstanding messages
    ok;
mgc_handle_request({handle_syntax_error, _RH, _PV, _ED}) ->
    reply;
mgc_handle_request({handle_message_error, _CH, _PV, _ED}) ->
    no_reply;
mgc_handle_request({handle_trans_request, CH, PV, ARs}) ->
    ED =  #'ErrorDescriptor'{errorCode = ?megaco_not_implemented,
                             errorText = "Only single service change on null context handled"},
    case ARs of
        [AR] ->
            ContextId = AR#'ActionRequest'.contextId,
            case AR#'ActionRequest'.commandRequests of
                [CR] when ContextId == ?megaco_null_context_id ->
                    case CR#'CommandRequest'.command of
                        {serviceChangeReq, Req} ->
                            Rep    = mgc_service_change(CH, PV, Req),
			    CmdRep = [{serviceChangeReply, Rep}],
                            {discard_ack,
                             [#'ActionReply'{contextId    = ContextId,
                                             commandReply = CmdRep}]};
                        _ ->
                            {discard_ack, ED}
                    end;
                _ ->
                    {discard_ack, ED}
            end;
        _ ->
            {discard_ack, ED}
    end;
mgc_handle_request({handle_trans_long_request, _CH, _PV, _RD}) ->
    ED = #'ErrorDescriptor'{errorCode = ?megaco_not_implemented,
                            errorText = "Long transaction requests not handled"},
    {discard_ack,  ED};
mgc_handle_request({handle_trans_reply, _CH, _PV, _AR, _RD}) ->
    ok;
mgc_handle_request({handle_trans_ack, _CH, _PV, _AS, _AD}) ->
    ok.

mgc_service_change(CH, _PV, SCR) ->
    SCP = SCR#'ServiceChangeRequest'.serviceChangeParms,
    #'ServiceChangeParm'{serviceChangeAddress = Address,
                         serviceChangeProfile = Profile,
                         serviceChangeReason  = [_Reason]} = SCP,
    TermId = SCR#'ServiceChangeRequest'.terminationID,
    if
        TermId == [?megaco_root_termination_id] ->
            MyMid = CH#megaco_conn_handle.local_mid,
            Res = {serviceChangeResParms,
                   #'ServiceChangeResParm'{serviceChangeMgcId   = MyMid,
                                           serviceChangeAddress = Address,
                                           serviceChangeProfile = Profile}},
            #'ServiceChangeReply'{terminationID       = TermId,
                                  serviceChangeResult = Res};
        true ->
            Res = {errorDescriptor,
                   #'ErrorDescriptor'{errorCode = ?megaco_not_implemented,
                                      errorText = "Only handled for root"}},
            
             #'ServiceChangeReply'{terminationID       = TermId,
                                   serviceChangeResult = Res}
    end.

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

start_mg(Node, Mid, Encoding, Transport, Verbosity) ->
    d("start mg[~p]: ~p", [Node, Mid]),
    RI1 = 
	case Encoding of 
	    text ->
		[{encoding_module, megaco_pretty_text_encoder},
		 {encoding_config, []},
		 {port,2944}];
	    binary ->
		[{encoding_module, megaco_ber_encoder},
		 {encoding_config, []},
		 {port,2945}]
	end,
    RI2 = 
	case Transport of
	    tcp ->
		[{transport_module, megaco_tcp}];
	    udp ->
		[{transport_module, megaco_udp}]
	end,
    RI = {receive_info, RI1 ++ RI2},
    Config = [{local_mid, Mid}, RI],
    Pid = spawn_link(Node, ?MODULE, mg, [self(), Verbosity, Config]),
    await_started(Pid).


mg(Parent, Verbosity, Config) ->
    process_flag(trap_exit, true),
    put(verbosity, Verbosity),
    put(sname,   "MG"),
    i("mg -> starting"),
    {Mid, ConnHandle} = mg_init(Config),
    notify_started(Parent),
    S = #mg{parent = Parent, mid = Mid, conn_handle = ConnHandle},
    i("mg -> started"),
    mg_loop(S).

mg_init(Config) ->
    d("mg_init -> entry"),
    Mid = get_conf(local_mid, Config),
    RI  = get_conf(receive_info, Config),
    i("mg_init -> start megaco"),
    application:start(megaco),
    d("mg_init -> start megaco user"),
    megaco:start_user(Mid, []),
    d("mg_init -> update user info (user_mod)"),
    megaco:update_user_info(Mid, user_mod,  ?MODULE),
    d("mg_init -> update user info (user_args)"),
    megaco:update_user_info(Mid, user_args, [self()]),
    d("mg_init -> get user info (receive_handle)"),
    RH = megaco:user_info(Mid,receive_handle),
    d("mg_init -> parse receive info"),
    {MgcPort,RH1} = mg_parse_receive_info(RI, RH),
    i("mg_init -> start transport(s)"),
    ConnHandle = mg_start_transport(MgcPort, RH1),
    {Mid, ConnHandle}.

mg_loop(#mg{state = State} = S) ->
    d("mg_loop(~p) -> await request", [State]),
    receive
	{stop, Parent} when S#mg.parent == Parent ->
	    i("mg_loop(~p) -> stopping", [State]),
	    mg_close_conn(S#mg.conn_handle),
	    megaco:stop_user(S#mg.mid),
	    application:stop(megaco),
	    i("mg_loop(~p) -> stopped", [State]),
	    Parent ! {stopped, self()},
	    exit(normal);


	{reset_stats, Parent} when S#mg.parent == Parent ->
	    i("mg_loop(~p) -> got request to reset stats counters", [State]),
	    %% mg_reset_stats(S#mgc.conn_handle),
	    mg_reset_stats(S#mg.conn_handle),
	    Parent ! {reset_stats_ack, self()},
	    mg_loop(S);



	%% Give me statistics
	{statistics, 1, Parent} when S#mg.parent == Parent ->
	    i("mg_loop(~p) -> got request for statistics 1", [State]),
	    {ok, Gen}   = megaco:get_stats(),
	    CH          = S#mg.conn_handle,
	    Reason      = {statistics, CH}, 
	    Pid         = megaco:conn_info(CH, control_pid),
	    SendMod     = megaco:conn_info(CH, send_mod),
	    SendHandle  = megaco:conn_info(CH, send_handle),
	    {ok, Trans} = 
		case SendMod of
		    megaco_tcp -> megaco_tcp:get_stats(SendHandle);
		    megaco_udp -> megaco_udp:get_stats(SendHandle);
		    SendMod    -> exit(Pid, Reason)
		end,
	    Parent ! {statistics, 1, [{gen, Gen}, {trans, Trans}], self()},
	    mg_loop(S); 


	%% Do a service change
	{service_change, Parent} when S#mg.parent == Parent, 
				      State == initiated ->
	    i("mg_loop(~p) -> received request to perform service change when:"
              "~n      Conn Handle: ~p"
              "~n      Conn Data:   ~p", 
	      [State,
               S#mg.conn_handle,
               megaco:conn_info(S#mg.conn_handle, conn_data)]),
	    Res = mg_service_change(S#mg.conn_handle),
	    d("mg_loop(~p) -> service change request send result: ~p",
              [State, Res]),
	    mg_loop(S#mg{state = connecting}); 


	%% Apply some load
	{apply_load, Parent, Times} when S#mg.parent == Parent ->
	    i("mg_loop(~p) -> received apply_load request", [State]),
	    apply_load_timer(),
	    Parent ! {apply_load_ack, self()},
	    mg_loop(S#mg{load_counter = Times - 1});


	apply_load_timeout ->
	    d("mg_loop(~p) -> received apply_load timeout [~p]", 
	      [State, S#mg.load_counter]),
	    mg_apply_load(S),
	    mg_loop(S);


	%% Megaco callback messages
	{request, Request, From} ->
	    d("mg_loop(~p) -> received megaco request: ~n~p~n   From: ~p", 
	      [State, Request, From]),
	    {Reply, NewS} = mg_handle_request(Request, S),
	    d("mg_loop(~p) -> send request reply: ~n~p", 
	      [NewS#mg.state, Reply]),
	    From ! {reply, Reply, self()},
	    mg_loop(NewS);


	{'EXIT', Pid, Reason} ->
	    error_msg("MG ~p received unexpected exit signal from ~p:~n~p", 
		      [S#mg.mid, Pid, Reason]),
	    mg_loop(S);


	Invalid ->
	    i("mg_loop(~p) -> received invalid request: ~p", [State, Invalid]),
	    mg_loop(S)

    end.


mg_reset_stats(CH) ->
    megaco:reset_stats(),
    case (catch megaco:conn_info(CH, send_mod)) of
	{error, Reason} ->
	    error_msg("unexpected result when retrieving send module for "
		      "own connection ~p: ~p. "
		      "~nexiting...", [CH, Reason]),
	    exit({invalid_connection, CH, Reason});
	{'EXIT', Reason} ->
	    error_msg("exit signal when retrieving send module for "
		      "own connection ~p: ~p. "
		      "~nexiting...", [CH, Reason]),
	    exit({invalid_connection, CH, Reason});
	SendMod when is_atom(SendMod) ->
	    SendMod:reset_stats()
    end.


mg_close_conn(CH) ->
    Reason = {self(), ignore},
    Pid        = megaco:conn_info(CH, control_pid),
    SendMod    = megaco:conn_info(CH, send_mod),
    SendHandle = megaco:conn_info(CH, send_handle),
    megaco:disconnect(CH, Reason),
    case SendMod of
	megaco_tcp -> megaco_tcp:close(SendHandle);
	megaco_udp -> megaco_udp:close(SendHandle);
	SendMod    -> exit(Pid, Reason)
    end.


mg_parse_receive_info(RI, RH) ->
    d("mg_parse_receive_info -> get encoding module"),
    EM = get_encoding_module(RI),
    d("mg_parse_receive_info -> get encoding config"),
    EC = get_encoding_config(RI, EM),
    d("mg_parse_receive_info -> get transport module"),
    TM = get_transport_module(RI),
    d("mg_parse_receive_info -> get transport port"),
    TP = get_transport_port(RI),
    RH1 = RH#megaco_receive_handle{send_mod        = TM,
				   encoding_mod    = EM,
				   encoding_config = EC},
    {TP, RH1}.


mg_start_transport(MgcPort,
		   #megaco_receive_handle{send_mod = megaco_tcp} = RH) ->
    mg_start_tcp(MgcPort,RH);
mg_start_transport(MgcPort,
		   #megaco_receive_handle{send_mod = megaco_udp} = RH) ->
    mg_start_udp(MgcPort,RH);
mg_start_transport(_, #megaco_receive_handle{send_mod = Mod}) ->
    throw({error, {bad_send_mod, Mod}}).


mg_start_tcp(MgcPort, RH) ->
    i("start tcp transport"),
    case megaco_tcp:start_transport() of
	{ok, Sup} ->
	    {ok, LocalHost} = inet:gethostname(),
	    Opts = [{host,           LocalHost},
		    {port,           MgcPort}, 
		    {receive_handle, RH}, 
		    {tcp_options,    [{nodelay, true}]}],
            i("tcp transport started: attempt (tcp) connect to MGC at:"
              "~n   ~p", [LocalHost]),
	    case megaco_tcp:connect(Sup, Opts) of
		{ok, SendHandle, ControlPid} ->
                    PrelMgcMid = preliminary_mid,
                    i("tcp transport (tcp) connected: attempt (megaco) connect:"),
		    {ok, ConnHandle} = 
			megaco:connect(RH, PrelMgcMid, 
				       SendHandle, ControlPid),
		    ConnHandle;
		{error, Reason} ->
		    {error, {megaco_tcp_connect, Reason}}
	    end;
        {error, Reason} ->
            {error, {megaco_tcp_start_transport, Reason}}
    end.


mg_start_udp(MgcPort, RH) ->
    i("start udp transport"),
    case megaco_udp:start_transport() of
	{ok, Sup} ->
            %% Some linux (Ubuntu) has "crap" in their /etc/hosts, that 
            %% causes problem for us in this case (UDP). So we can't use
            %% local host. Try instead to "figure out" tha actual address...
            LocalAddr = which_local_addr(),
	    Opts = [{port, 0}, {receive_handle, RH}],
            i("udp transport started: attempt (udp) open"),
	    case megaco_udp:open(Sup, Opts) of
		{ok, Handle, ControlPid} ->
                    MgcMid = preliminary_mid,
                    i("udp transport open: "
                      "now create send handle with MGC address: "
                      "~n   ~p", [LocalAddr]),
                    SendHandle = megaco_udp:create_send_handle(Handle, 
							       LocalAddr, 
							       MgcPort),
                    i("udp transport: attempt (megaco) connect to:"
                      "~n   ~p", [SendHandle]),
		    {ok, ConnHandle} = 
			megaco:connect(RH, MgcMid, 
				       SendHandle, ControlPid),
		    ConnHandle;
		{error, Reason} ->
                    {error, {megaco_udp_open, Reason}}
	    end;
        {error, Reason} ->
            {error, {megaco_udp_start_transport, Reason}}
    end.


mg_service_change(ConnHandle) ->
    mg_service_change(ConnHandle, restart, ?megaco_cold_boot).

mg_service_change(ConnHandle, Method, Reason) ->
    SCP = #'ServiceChangeParm'{serviceChangeMethod = Method,
                               serviceChangeReason = [Reason]},
    TermId = [?megaco_root_termination_id],
    SCR = #'ServiceChangeRequest'{terminationID = TermId,
                                  serviceChangeParms = SCP},
    CR = #'CommandRequest'{command = {serviceChangeReq, SCR}},
    AR = #'ActionRequest'{contextId = ?megaco_null_context_id,
                          commandRequests = [CR]},
    megaco:cast(ConnHandle, [AR], []).


mg_notify_request(CH) ->
    TimeStamp = cre_timeNotation("19990729", "22000000"),
    Event     = cre_observedEvent("al/of",TimeStamp),
    Desc      = cre_observedEventsDesc(2222,[Event]),
    NotifyReq = cre_notifyReq([#megaco_term_id{id = ?A4444}],Desc),
    CmdReq    = cre_commandReq({notifyReq, NotifyReq}),
    ActReq    = cre_actionReq(?megaco_null_context_id, [CmdReq]),
    megaco:cast(CH, [ActReq], [{reply_data, Desc}]).
    
mg_apply_load(#mg{conn_handle = CH}) ->
    mg_notify_request(CH).


cre_actionReq(Cid, Cmds) ->
    #'ActionRequest'{contextId = Cid,
		     commandRequests = Cmds}.

cre_commandReq(Cmd) ->
    #'CommandRequest'{command = Cmd}.

cre_notifyReq(Tid, EvsDesc) ->
    #'NotifyRequest'{terminationID = Tid, observedEventsDescriptor = EvsDesc}.

cre_observedEventsDesc(Id, EvList) ->
    #'ObservedEventsDescriptor'{requestId = Id, observedEventLst = EvList}.

cre_observedEvent(Name, Not) ->
    #'ObservedEvent'{eventName = Name, timeNotation = Not}.

cre_timeNotation(D,T) ->
    #'TimeNotation'{date = D, time = T}.

%% -----------------------
%% Handle megaco callbacks
%%

mg_handle_request({handle_connect, CH, _PV}, 
		  #mg{state = connecting} = S) ->
    {ok, S#mg{conn_handle = CH}};

mg_handle_request({handle_disconnect, CH, _PV, _R}, S) ->
    {ok, S#mg{conn_handle = CH}};

mg_handle_request({handle_syntax_error, _RH, _PV, _ED}, S) ->
    {reply, S};

mg_handle_request({handle_message_error, CH, _PV, _ED}, S) ->
    {no_reply, S#mg{conn_handle = CH}};

mg_handle_request({handle_trans_request, CH, _PV, _AR}, S) ->
    ED =  #'ErrorDescriptor'{errorCode = ?megaco_not_implemented,
                             errorText = "Transaction requests not handled"},
    {{discard_ack, ED}, S#mg{conn_handle = CH}};

mg_handle_request({handle_trans_long_request, CH, _PV, _RD}, S) ->
    ED = #'ErrorDescriptor'{errorCode = ?megaco_not_implemented,
                            errorText = "Long transaction requests not handled"},
    {{discard_ack,  ED}, S#mg{conn_handle = CH}};

mg_handle_request({handle_trans_reply, CH, _PV, _AR, _RD}, 
		  #mg{parent = Pid, state = connecting} = S) ->
    %% Should really check this...
    Pid ! {service_change_reply, ok, self()},
    {ok, S#mg{conn_handle = CH, state = connected}};
mg_handle_request({handle_trans_reply, _CH, _PV, {error, ED}, RD}, 
		  #mg{parent = Pid, load_counter = 0} = S) 
  when is_record(ED, 'ErrorDescriptor') andalso 
       is_record(RD, 'ObservedEventsDescriptor') ->
    Pid ! {load_complete, self()},
    {ok, S};
mg_handle_request({handle_trans_reply, _CH, _PV, {error, ED}, RD}, 
		  #mg{load_counter = N} = S) 
  when is_record(ED, 'ErrorDescriptor') andalso 
       is_record(RD, 'ObservedEventsDescriptor') ->
    apply_load_timer(),
    {ok, S#mg{load_counter = N-1}};

mg_handle_request({handle_trans_ack, _CH, _PV, _AS, _AD}, S) ->
    {ok, S}.

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

await_started(Pid) ->
    receive
	{started, Pid} ->
	    d("await_started ~p: ok", [Pid]),
	    {ok, Pid};
	{'EXIT', Pid, Reason} ->
	    i("await_started ~p: received exit signal: ~p", [Pid, Reason]),
	    exit({failed_starting, Pid, Reason})
    after 10000 ->
	    i("await_started ~p: timeout", [Pid]),
	    exit({error, timeout})
    end.

stop(Pid) ->
    d("stop ~p", [Pid]),
    Pid ! {stop, self()},
    receive 
	{stopped, Pid} ->
	    d("stop -> received stopped from ~p", [Pid]),
	    ok;
	{'EXIT', Pid, Reason} ->
	    i("stop ~p: received exit signal: ~p", [Pid, Reason]),
	    exit({failed_stopping, Pid, Reason})
    after 10000 ->
	    exit({error, timeout})
    end.

get_stats(Pid, No) ->
    d("get_stats ~p", [Pid]),
    Pid ! {statistics, No, self()},
    receive 
	{statistics, No, Stats, Pid} ->
	    {ok, Stats};
	{'EXIT', Pid, Reason} ->
	    i("get_stats ~p: received exit signal: ~p", [Pid, Reason]),
	    exit({failed_getting_stats, Pid, Reason})
    after 10000 ->
	    exit({error, timeout})
    end.

service_change(Pid) ->
    d("service_change ~p", [Pid]),
    Pid ! {service_change, self()},
    receive 
	{service_change_reply, Res, Pid} ->
	    {ok, Res};
	{'EXIT', Pid, Reason} ->
	    i("service_change ~p: received exit signal: ~p", [Pid, Reason]),
	    exit({failed_service_change, Pid, Reason})
    after 10000 ->
	    exit({error, timeout})
    end.


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

notify_started(Parent) ->
    Parent ! {started, self()}.


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

%% The megaco user callback interface 

handle_connect(CH, PV, Pid) ->
%     i("handle_connect -> entry with"
%       "~n    CH:  ~p"
%       "~n    PV:  ~p"
%       "~n    Pid: ~p", [CH, PV, Pid]),
    case CH#megaco_conn_handle.remote_mid of
        preliminary_mid ->
	    %% Avoids deadlock
	    ok;
	_ ->
	    Reply = request(Pid, {handle_connect, CH, PV}),
% 	    d("handle_connect -> Reply:~n~p", [Reply]),
	    Reply
    end.

handle_disconnect(_CH, _PV, 
		  {user_disconnect, {Pid, ignore}}, 
		  Pid) ->
%     i("handle_disconnect(ignore) -> entry with"
%       "~n   CH: ~p"
%       "~n   PV: ~p", [CH, PV]),
    %% Avoids deadlock
    ok;
handle_disconnect(CH, PV, R, Pid) ->
%     i("handle_disconnect -> entry with"
%       "~n   CH: ~p"
%       "~n   PV: ~p"
%       "~n   R:  ~p", [CH, PV, R]),
    request(Pid, {handle_disconnect, CH, PV, R}).

handle_syntax_error(ReceiveHandle, ProtocolVersion, ErrorDescriptor, Pid) ->
%     i("handle_syntax_error -> entry with"
%       "~n   ReceiveHandle:   ~p"
%       "~n   ProtocolVersion: ~p"
%       "~n   ErrorDescriptor: ~p", 
%       [ReceiveHandle, ProtocolVersion, ErrorDescriptor]),
    Req = {handle_syntax_error, ReceiveHandle, ProtocolVersion, 
	   ErrorDescriptor},
    request(Pid, Req).
    
handle_message_error(ConnHandle, ProtocolVersion, ErrorDescriptor, Pid) ->
%     i("handle_message_error -> entry with"
%       "~n   ConnHandle:      ~p"
%       "~n   ProtocolVersion: ~p"
%       "~n   ErrorDescriptor: ~p", 
%       [ConnHandle, ProtocolVersion, ErrorDescriptor]),
    Req = {handle_message_error, ConnHandle, ProtocolVersion, ErrorDescriptor},
    request(Pid, Req).

handle_trans_request(CH, PV, AR, Pid) ->
%     i("handle_trans_request -> entry with" 
%       "~n   CH:  ~p"
%       "~n   PV:  ~p"
%       "~n   AR:  ~p"
%       "~n   Pid: ~p", [CH, PV, AR, Pid]),
    Reply = request(Pid, {handle_trans_request, CH, PV, AR}),
%     i("handle_trans_request -> Reply:~n~p", [Reply]),
    Reply.

handle_trans_long_request(ConnHandle, ProtocolVersion, ReqData, Pid) ->
%     i("handle_trans_long_request -> entry with"
%       "~n   ConnHandle:      ~p"
%       "~n   ProtocolVersion: ~p"
%       "~n   ReqData:         ~p", [ConnHandle, ProtocolVersion, ReqData]),
    Req = {handle_trans_long_request, ConnHandle, ProtocolVersion, ReqData},
    request(Pid, Req).

handle_trans_reply(ConnHandle, ProtocolVersion, ActualReply, ReplyData, Pid) ->
%     i("handle_trans_reply -> entry with"
%       "~n   ConnHandle:      ~p"
%       "~n   ProtocolVersion: ~p"
%       "~n   ActualReply:     ~p"
%       "~n   ReplyData:       ~p", 
%       [ConnHandle, ProtocolVersion, ActualReply, ReplyData]),
    Req = {handle_trans_reply, ConnHandle, ProtocolVersion, 
	   ActualReply, ReplyData},
    request(Pid, Req).

handle_trans_ack(ConnHandle, ProtocolVersion, AckStatus, AckData, Pid) ->
%     i("handle_trans_ack -> entry with"
%       "~n   ConnHandle:      ~p"
%       "~n   ProtocolVersion: ~p"
%       "~n   AckStatus:       ~p"
%       "~n   AckData:         ~p", 
%       [ConnHandle, ProtocolVersion, AckStatus, AckData]),
    Req = {handle_trans_ack, ConnHandle, ProtocolVersion, AckStatus, AckData},
    request(Pid, Req).


request(Pid, Request) ->
    Pid ! {request, Request, self()},
    receive
	{reply, Reply, Pid} ->
	    Reply
    end.


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

error_msg(F,A) -> error_logger:error_msg(F ++ "~n",A).


get_encoding_module(RI) ->
    case (catch get_conf(encoding_module, RI)) of
	{error, _} ->
	    undefined;
	Val ->
	    Val
    end.

get_encoding_config(RI, EM) ->
    case text_codec(EM) of
	true ->
	    case megaco:system_info(text_config) of
		[Conf] when is_list(Conf) ->
		    Conf;
		_ ->
		    []
	    end;

	false ->
	    get_conf(encoding_config, RI)
    end.

text_codec(megaco_compact_text_encoder) ->
    true;
text_codec(megaco_pretty_text_encoder) ->
    true;
text_codec(_) ->
    false.


get_transport_module(RI) ->
    get_conf(transport_module, RI).

get_transport_port(RI) ->
    get_conf(port, RI).


get_conf(Key, Config) ->
    case lists:keysearch(Key, 1, Config) of
	{value, {Key, Val}} ->
	    Val;
	_ ->
	    exit({error, {not_found, Key, Config}})
    end.


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

%% Tries to find a valid local address...
which_local_addr() ->
    case inet:getifaddrs() of
        {ok, IFs} ->
            which_local_addr(IFs);
        {error, Reason} ->
            i("Failed get local address: "
              "~n   ~p", [Reason]),
            ?SKIP({failed_get_local_addr, Reason})
    end.

%% We explicitly skip some interfaces that we know is not "valid" 
%% (docker stuff)
which_local_addr([]) ->
    ?SKIP(failed_get_local_addr);
which_local_addr([{"br-" ++ _ = _IfName, _IfOpts}|IFs]) ->
    which_local_addr(IFs);
which_local_addr([{"docker" ++ _ = _IfName, _IfOpts}|IFs]) ->
    which_local_addr(IFs);
which_local_addr([{_IfName, IfOpts}|IFs]) ->
    case which_local_addr2(IfOpts) of
        {ok, Addr} ->
            Addr;
        error ->
            which_local_addr(IFs)
    end.

which_local_addr2(IfOpts) ->
    case if_is_running(IfOpts) of
        true ->
            which_local_addr3(IfOpts);
        false ->
            error
    end.

if_is_running(If) ->
    lists:keymember(flags, 1, If) andalso
        begin
            {value, {flags, Flags}} = lists:keysearch(flags, 1, If),
            (not lists:member(loopback, Flags)) andalso lists:member(running, Flags)
        end.
    
which_local_addr3([]) ->
    error;
which_local_addr3([{addr, Addr}|_]) 
  when (size(Addr) =:= 4) andalso (element(1, Addr) =/= 127) ->
    {ok, Addr};
which_local_addr3([_|T]) ->
    which_local_addr3(T).


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

try_tc(TCName, Pre, Case, Post) ->
    try_tc(TCName, "TEST", ?TEST_VERBOSITY, Pre, Case, Post).

try_tc(TCName, Name, Verbosity, Pre, Case, Post) ->
    ?TRY_TC(TCName, Name, Verbosity, Pre, Case, Post).



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

%% p(F) ->
%%     p(F, []).

p(F, A) ->
    p(get(sname), F, A).

p(S, F, A) when is_list(S) ->
    io:format("*** [~s] ~p ~s ***" 
	      "~n   " ++ F ++ "~n", 
	      [?FTS(), self(), S | A]);
p(_S, F, A) ->
    io:format("*** [~s] ~p *** "
	      "~n   " ++ F ++ "~n", 
	      [?FTS(), self() | A]).


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

i(F, A) ->
    print(info, get(verbosity), "", F, A).


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

d(F, A) ->
    print(debug, get(verbosity), "DBG: ", F, A).


printable(_, debug)   -> true;
printable(info, info) -> true;
printable(_,_)        -> false.

print(Severity, Verbosity, P, F, A) ->
    print(printable(Severity,Verbosity), P, F, A).

print(true, P, F, A) ->
    io:format("~s~p:~s:~s: " ++ F ++ "~n", [P, self(), get(sname), ?FTS() | A]);
print(_, _, _, _) ->
    ok.


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

progress(F, A) ->
    io:format("~s " ++ F ++ "~n", [?FTS()|A]),
    io:format(user, "~s " ++ F ++ "~n", [?FTS()|A]).


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

random() ->
    10 * rand:uniform(50).

apply_load_timer() ->
    erlang:send_after(random(), self(), apply_load_timeout).

