%% 
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2015. 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%
%% 

-module(snmp_test_mgr).

%%----------------------------------------------------------------------
%% This module implements a simple SNMP manager for Erlang.
%%----------------------------------------------------------------------

%% c(snmp_test_mgr).
%% snmp_test_mgr:start().
%% snmp_test_mgr:g([[sysContact,0]]).

%% snmp_test_mgr:start([{engine_id, "mbjk's engine"}, v3, {agent, "clip"}, {mibs, ["../mibs/SNMPv2-MIB"]}]).

%% snmp_test_mgr:start([{engine_id, "agentEngine"}, {user, "iwl_test"}, {dir, "mgr_conf"}, {sec_level, authPriv}, v3, {agent, "clip"}]).

%% User interface
-export([start_link/1, start/1, stop/0, 
	 d/0, discovery/0, 
	 g/1, s/1, gn/1, gn/0, r/0, gb/3, rpl/1,
	 send_bytes/1,
	 expect/2,expect/3,expect/4,expect/6,get_response/2, 
	 receive_response/0,
	 purify_oid/1, 
	 oid_to_name/1, name_to_oid/1]).

%% Internal exports
-export([get_oid_from_varbind/1, 
	 var_and_value_to_varbind/2, flatten_oid/2, make_vb/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).

-include_lib("snmp/include/snmp_types.hrl").
-include_lib("snmp/include/STANDARD-MIB.hrl").

-record(state,{dbg         = true,
	       quiet,
	       parent,
	       timeout     = 3500,
	       print_traps = true,
	       mini_mib,
	       packet_server, 
	       last_sent_pdu, 
	       last_received_pdu}).

-define(SERVER, ?MODULE).
-define(PACK_SERV, snmp_test_mgr_misc).

start_link(Options) ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, {Options, self()}, []).

start(Options) ->
    gen_server:start({local, ?SERVER}, ?MODULE, {Options, self()}, []).

stop() ->
    call(stop).

d() ->
    discovery().

discovery() ->
    call(discovery).

g(Oids) ->
    cast({get, Oids}).

%% VarsAndValues is: {PlainOid, o|s|i, Value} (unknown mibs) | {Oid, Value} 
s(VarsAndValues) ->
    cast({set, VarsAndValues}).

gn(Oids) when is_list(Oids) ->
    cast({get_next, Oids});
gn(N) when is_integer(N) ->
    cast({iter_get_next, N}).
gn() ->
    cast(iter_get_next).

r() ->
    cast(resend_pdu).

gb(NonRepeaters, MaxRepetitions, Oids) ->
    cast({bulk, {NonRepeaters, MaxRepetitions, Oids}}).

rpl(RespPdu) ->
    cast({response, RespPdu}).

send_bytes(Bytes) ->
    cast({send_bytes, Bytes}).

purify_oid(Oid) ->
    call({purify_oid, Oid}, 5000).

oid_to_name(Oid) ->
    call({oid_to_name, Oid}, 5000).

name_to_oid(Name) ->
    call({name_to_oid, Name}, 5000).


%%----------------------------------------------------------------------
%% Purpose: For writing test sequences
%% Args: Y=any (varbinds) | trap | timeout | VarBinds | ErrStatus
%% Returns: ok|{error, Id, Reason}
%%----------------------------------------------------------------------
expect(Id,Y) -> echo_errors(expect_impl(Id,Y)).
expect(Id,v2trap,VBs) -> echo_errors(expect_impl(Id,v2trap,VBs));
expect(Id,report,VBs) -> echo_errors(expect_impl(Id,report,VBs));
expect(Id,{inform, Reply},VBs) ->
    echo_errors(expect_impl(Id,{inform,Reply},VBs)).
expect(Id,Err,Idx,VBs) -> echo_errors(expect_impl(Id,Err,Idx,VBs)).
expect(Id,trap, Enterp, Generic, Specific, ExpectedVarbinds) ->
    echo_errors(expect_impl(Id,trap,Enterp,Generic,
			    Specific,ExpectedVarbinds)).

%%-----------------------------------------------------------------
%% Purpose: For writing test sequences
%%-----------------------------------------------------------------
get_response(Id, Vars) -> echo_errors(get_response_impl(Id, Vars)).

%%----------------------------------------------------------------------
%% Receives a response from the agent.
%% Returns: a PDU or {error, Reason}.
%% It doesn't receive traps though.
%%----------------------------------------------------------------------
receive_response() ->
    receive_response(get_timeout()).

receive_response(Timeout) ->
    d("await response within ~w ms",[Timeout]),
    receive
	{snmp_pdu, PDU} when is_record(PDU, pdu) ->
	    d("received PDU: ~n\t~p",[PDU]),
	    PDU
    after Timeout ->
	    d("response timeout",[]),
	    {error, timeout}
    end.


get_timeout() ->
    case get(receive_response_timeout) of
	Int when is_integer(Int) and (Int > 0) ->
	    Int;
	_ ->
	    get_timeout(os:type())
    end.

get_timeout(_)       -> 10000. % Trying to improve test results % 3500.

%%----------------------------------------------------------------------
%% Receives a trap from the agent.
%% Returns: TrapPdu|{error, Reason}
%%----------------------------------------------------------------------
receive_trap(Timeout) ->
    d("await trap within ~w ms",[Timeout]),
    receive
	{snmp_pdu, PDU} when is_record(PDU, trappdu) ->
	    d("received trap-PDU: ~n\t~p",[PDU]),
	    PDU
    after Timeout ->
	    d("trap timeout",[]),
	    {error, timeout}
    end.

%%----------------------------------------------------------------------
%% Options: List of
%%  {agent_udp, UDPPort},  {agent, Agent}
%%  Optional: 
%%  {community, String ("public" is default}, quiet,
%%  {mibs, List of Filenames}, {trap_udp, UDPPort (default 5000)},
%%----------------------------------------------------------------------
init({Options, CallerPid}) ->
    put(sname,     mgr),
    put(verbosity, debug), 
    random:seed(erlang:phash2([node()]),
                erlang:monotonic_time(),
                erlang:unique_integer()),
    case (catch is_options_ok(Options)) of
	true ->
	    put(debug, get_value(debug, Options, false)),
	    d("init -> (~p) extract options",[self()]),
 	    PacksDbg    = get_value(packet_server_debug, Options, false),
	    io:format("[~w] ~p -> PacksDbg: ~p~n", [?MODULE, self(), PacksDbg]),
	    RecBufSz    = get_value(recbuf,            Options, 1024),
	    io:format("[~w] ~p -> RecBufSz: ~p~n", [?MODULE, self(), RecBufSz]),
	    Mibs        = get_value(mibs,              Options, []),
	    io:format("[~w] ~p -> Mibs: ~p~n", [?MODULE, self(), Mibs]),
	    Udp         = get_value(agent_udp,         Options, 4000),
	    io:format("[~w] ~p -> Udp: ~p~n", [?MODULE, self(), Udp]),
	    User        = get_value(user,              Options, "initial"),
	    io:format("[~w] ~p -> User: ~p~n", [?MODULE, self(), User]),
	    EngineId    = get_value(engine_id,         Options, "agentEngine"),
	    io:format("[~w] ~p -> EngineId: ~p~n", [?MODULE, self(), EngineId]),
	    CtxEngineId = get_value(context_engine_id, Options, EngineId),
	    io:format("[~w] ~p -> CtxEngineId: ~p~n", [?MODULE, self(), CtxEngineId]),
	    TrapUdp     = get_value(trap_udp,          Options, 5000),
	    io:format("[~w] ~p -> TrapUdp: ~p~n", [?MODULE, self(), TrapUdp]),
	    Dir         = get_value(dir,               Options, "."),
	    io:format("[~w] ~p -> Dir: ~p~n", [?MODULE, self(), Dir]),
	    SecLevel    = get_value(sec_level,         Options, noAuthNoPriv),
	    io:format("[~w] ~p -> SecLevel: ~p~n", [?MODULE, self(), SecLevel]),
	    MiniMIB     = snmp_mini_mib:create(Mibs),
	    io:format("[~w] ~p -> MiniMIB: ~p~n", [?MODULE, self(), MiniMIB]),
	    Version     = case lists:member(v2, Options) of
			      true -> 'version-2';
			      false -> 
				  case lists:member(v3, Options) of
				      true -> 'version-3';
				      false -> 'version-1'
				  end
			  end,
	    io:format("[~w] ~p -> Version: ~p~n", [?MODULE, self(), Version]),
	    Com = case Version of
		      'version-3' ->
			  get_value(context, Options, "");
		      _ ->
			  get_value(community, Options, "public")
		  end,
	    io:format("[~w] ~p -> Com: ~p~n", [?MODULE, self(), Com]),
	    VsnHdrD = 
		{Com, User, EngineId, CtxEngineId, mk_seclevel(SecLevel)},
	    io:format("[~w] ~p -> VsnHdrD: ~p~n", [?MODULE, self(), VsnHdrD]),
	    IpFamily = get_value(ipfamily, Options, inet),
	    io:format("[~w] ~p -> IpFamily: ~p~n", [?MODULE, self(), IpFamily]),
	    AgIp = case snmp_misc:assq(agent, Options) of
		       {value, Tuple4} when is_tuple(Tuple4) andalso 
					    (size(Tuple4) =:= 4) ->
			   Tuple4;
		       {value, Host} when is_list(Host) ->
			   {ok, Ip} = snmp_misc:ip(Host, IpFamily),
			   Ip
		   end,
	    io:format("[~w] ~p -> AgIp: ~p~n", [?MODULE, self(), AgIp]),
	    Quiet = lists:member(quiet, Options),
	    io:format("[~w] ~p -> Quiet: ~p~n", [?MODULE, self(), Quiet]),
	    PackServ =
		start_packet_server(
		  Quiet, Options, CallerPid, AgIp, Udp, TrapUdp,
		  VsnHdrD, Version, Dir, RecBufSz, PacksDbg, IpFamily),
	    d("init -> packet server: ~p",[PackServ]),
	    State = #state{parent        = CallerPid,
			   quiet         = Quiet,
			   mini_mib      = MiniMIB, 
			   packet_server = PackServ},
	    d("init -> done",[]),
	    {ok, State};
	
	{error, Reason} -> 
	    {stop,Reason}
    end.

start_packet_server(false, _Options, _CallerPid, AgIp, Udp, TrapUdp, 
		    VsnHdrD, Version, Dir, RecBufSz, PacksDbg, IpFamily) ->
    d("start_packet_server -> entry", []),
    ?PACK_SERV:start_link_packet(
       {msg, self()}, AgIp, Udp, TrapUdp,
       VsnHdrD, Version, Dir, RecBufSz, PacksDbg, IpFamily);
start_packet_server(true, Options, CallerPid, AgIp, Udp, TrapUdp, 
		    VsnHdrD, Version, Dir, RecBufSz, PacksDbg, IpFamily) ->
    Type =  get_value(receive_type, Options, pdu),
    d("start_packet_server -> entry with"
      "~n   CallerPid: ~p"
      "~n   when"
      "~n   Type:      ~p",[CallerPid, Type]),
    ?PACK_SERV:start_link_packet(
       {Type, CallerPid}, AgIp, Udp, TrapUdp,
       VsnHdrD, Version, Dir, RecBufSz, PacksDbg, IpFamily).

is_options_ok([{mibs,List}|Opts]) when is_list(List) ->
    is_options_ok(Opts);
is_options_ok([quiet|Opts])  ->
    is_options_ok(Opts);
is_options_ok([{agent,_}|Opts]) ->
    is_options_ok(Opts);
is_options_ok([{ipfamily,IpFamily}|Opts])
  when IpFamily =:= inet;
       IpFamily =:= inet6 ->
    is_options_ok(Opts);
is_options_ok([{agent_udp,Int}|Opts]) when is_integer(Int) ->
    is_options_ok(Opts);
is_options_ok([{trap_udp,Int}|Opts]) when is_integer(Int) ->
    is_options_ok(Opts);
is_options_ok([{community,List}|Opts]) when is_list(List) ->
    is_options_ok(Opts);
is_options_ok([{dir,List}|Opts]) when is_list(List) ->
    is_options_ok(Opts);
is_options_ok([{sec_level,noAuthNoPriv}|Opts]) ->
    is_options_ok(Opts);
is_options_ok([{sec_level,authNoPriv}|Opts]) ->
    is_options_ok(Opts);
is_options_ok([{sec_level,authPriv}|Opts]) ->
    is_options_ok(Opts);
is_options_ok([{context,List}|Opts]) when is_list(List) ->
    is_options_ok(Opts);
is_options_ok([{user,List}|Opts]) when is_list(List) ->
    is_options_ok(Opts);
is_options_ok([{engine_id,List}|Opts]) when is_list(List) ->
    is_options_ok(Opts);
is_options_ok([{context_engine_id,List}|Opts]) when is_list(List) ->
    is_options_ok(Opts);
is_options_ok([v1|Opts]) ->
    is_options_ok(Opts);
is_options_ok([v2|Opts]) ->
    is_options_ok(Opts);
is_options_ok([v3|Opts]) ->
    is_options_ok(Opts);
is_options_ok([{debug,Bool}|Opts]) ->
    case is_bool(Bool) of
	ok ->
	    is_options_ok(Opts);
	error ->
	    {error, {bad_option, debug, Bool}}
    end;
is_options_ok([{packet_server_debug,Bool}|Opts]) ->
    case is_bool(Bool) of
	ok ->
	    is_options_ok(Opts);
	error ->
	    {error, {bad_option, packet_server_debug, Bool}}
    end;
is_options_ok([{recbuf,Sz}|Opts]) when (0 < Sz) and (Sz =< 65535) ->
    is_options_ok(Opts);
is_options_ok([InvOpt|_]) ->
    {error,{invalid_option,InvOpt}};
is_options_ok([]) -> true.

is_bool(true)  -> ok;
is_bool(false) -> ok;
is_bool(_)     -> error.

mk_seclevel(noAuthNoPriv) -> 0;
mk_seclevel(authNoPriv) -> 1;
mk_seclevel(authPriv) -> 3.
    

handle_call({purify_oid, Oid}, _From, #state{mini_mib = MiniMib} = State) ->
    d("handle_call -> purify_oid for ~p",[Oid]),
    Reply = (catch purify_oid(Oid, MiniMib)),
    {reply, Reply, State};

handle_call({find_pure_oid, XOid}, _From, State) ->
    d("handle_call -> find_pure_oid for ~p",[XOid]),
    {reply, catch flatten_oid(XOid, State#state.mini_mib), State};

handle_call({oid_to_name, Oid}, _From, State) ->
    d("handle_call -> oid_to_name for Oid: ~p",[Oid]),
    Reply = 
	case lists:keysearch(Oid, 1, State#state.mini_mib) of
	    {value, {_Oid, Name, _Type}} ->
		{ok, Name};
	    false ->
		{error, {no_such_oid, Oid}}
	end,
    {reply, Reply, State};

handle_call({name_to_oid, Name}, _From, State) ->
    d("handle_call -> name_to_oid for Name: ~p",[Name]),
    Reply = 
	case lists:keysearch(Name, 2, State#state.mini_mib) of
	    {value, {Oid, _Name, _Type}} ->
		{ok, Oid};
	    false ->
		{error, {no_such_name, Name}}
	end,
    {reply, Reply, State};

handle_call(stop, _From, #state{mini_mib = MiniMIB} = State) ->
    d("handle_call -> stop request",[]),
    snmp_mini_mib:delete(MiniMIB),
    {stop, normal, ok, State#state{mini_mib = undefined}};

handle_call(discovery, _From, State) ->
    d("handle_call -> discovery",[]),
    {Reply, NewState} = execute_discovery(State),
    {reply, Reply, NewState}.
    

handle_cast({get, Oids}, State) ->
    d("handle_cast -> get request for ~p", [Oids]),
    {noreply, execute_request(get, Oids, State)};

handle_cast({set, VariablesAndValues}, State) ->
    d("handle_cast -> set request for ~p", [VariablesAndValues]),
    {noreply, execute_request(set, VariablesAndValues, State)};

handle_cast({get_next, Oids}, State) ->
    d("handle_cast -> get-next request for ~p", [Oids]),
    {noreply, execute_request(get_next, Oids, State)};

handle_cast(iter_get_next, State)
  when is_record(State#state.last_received_pdu, pdu) ->
    d("handle_cast -> iter_get_next request", []),
    PrevPDU = State#state.last_received_pdu,
    Oids    = [get_oid_from_varbind(Vb) || Vb <- PrevPDU#pdu.varbinds], 
    {noreply, execute_request(get_next, Oids, State)};

handle_cast(iter_get_next, State) ->
    ?PACK_SERV:error("[Iterated get-next] No Response PDU to "
		     "start iterating from.", []),
    {noreply, State};

handle_cast({iter_get_next, N}, State) ->
    d("handle_cast -> iter_get_next(~p) request",[N]),
    if
	is_record(State#state.last_received_pdu, pdu) ->
	    PDU = get_next_iter_impl(N, State#state.last_received_pdu,
				     State#state.mini_mib,
				     State#state.packet_server),
	    {noreply, State#state{last_received_pdu = PDU}};
	true ->
	    ?PACK_SERV:error("[Iterated get-next] No Response PDU to "
				"start iterating from.", []),
	    {noreply, State}
    end;

handle_cast(resend_pdu, #state{last_sent_pdu = PDU} = State) ->
    d("handle_cast -> resend_pdu request when"
      "~n   PDU = ~p", [PDU]),
    send_pdu(PDU#pdu{request_id = make_request_id()},
	     State#state.mini_mib,
	     State#state.packet_server),
    {noreply, State};

handle_cast({bulk, Args}, State) ->
    d("handle_bulk -> bulk request for ~p", [Args]),
    {noreply, execute_request(bulk, Args, State)};

handle_cast({response, RespPdu}, State) ->
    d("handle_cast -> response request with ~p", [RespPdu]),
    ?PACK_SERV:send_pdu(RespPdu, State#state.packet_server),
    {noreply, State};

handle_cast({send_bytes, Bytes}, State) ->
    d("handle_cast -> send-bytes request for ~p bytes", [sizeOf(Bytes)]),
    ?PACK_SERV:send_bytes(Bytes, State#state.packet_server),
    {noreply, State};

handle_cast(Msg, State) ->
    d("handle_cast -> unknown message: "
      "~n   ~p", [Msg]),
    {noreply, State}.


handle_info({snmp_msg, Msg, Ip, Udp}, State) ->
    io:format("* Got PDU: ~s", [?PACK_SERV:format_hdr(Msg)]),
    PDU = ?PACK_SERV:get_pdu(Msg),
    echo_pdu(PDU, State#state.mini_mib),
    case PDU#pdu.type of
	'inform-request' ->
	    %% Generate a response...
	    RespPDU = PDU#pdu{type = 'get-response',
			      error_status = noError,
			      error_index = 0},
	    RespMsg = ?PACK_SERV:set_pdu(Msg, RespPDU),
	    ?PACK_SERV:send_msg(RespMsg, State#state.packet_server, Ip, Udp);
	_Else ->
	    ok
    end,
    {noreply, State#state{last_received_pdu = PDU}};

handle_info(Info, State) ->
    d("handle_info -> unknown info: "
      "~n   ~p", [Info]),
    {noreply, State}.


terminate(Reason, State) ->
    d("terminate -> with Reason: ~n\t~p",[Reason]),
    ?PACK_SERV:stop(State#state.packet_server).


%%----------------------------------------------------------------------
%% Returns: A new State
%%----------------------------------------------------------------------
execute_discovery(State) ->
    Pdu   = make_discovery_pdu(),
    Reply = ?PACK_SERV:send_discovery_pdu(Pdu, State#state.packet_server),
    {Reply, State#state{last_sent_pdu = Pdu}}.


execute_request(Operation, Data, State) ->
    case (catch make_pdu(Operation, Data, State#state.mini_mib)) of
	{error, {Format, Data2}} ->
	    report_error(State, Format, Data2),
	    State;
	{error, _Reason} -> 
	    State;
	PDU when is_record(PDU, pdu) ->
	    send_pdu(PDU, State#state.mini_mib, State#state.packet_server),
	    State#state{last_sent_pdu = PDU}
    end.
    
report_error(#state{quiet = true, parent = Pid}, Format, Args) ->
    Reason = lists:flatten(io_lib:format(Format, Args)),
    Pid ! {oid_error, Reason};
report_error(_, Format, Args) ->
    ?PACK_SERV:error(Format, Args).


get_oid_from_varbind(#varbind{oid = Oid}) -> Oid.

send_pdu(PDU, _MiniMIB, PackServ) ->
    ?PACK_SERV:send_pdu(PDU, PackServ).

%%----------------------------------------------------------------------
%% Purpose: Unnesting of oids like [myTable, 3, 4, "hej", 45] to
%%          [1,2,3,3,4,104,101,106,45]
%%----------------------------------------------------------------------

purify_oid([A|T], MiniMib) when is_atom(A) ->
    Oid2 = 
	case snmp_mini_mib:oid(MiniMib, A) of
	    false ->
		throw({error, {unknown_aliasname, A}});
	    Oid ->
		lists:flatten([Oid|T])
	end,
    {ok, verify_pure_oid(Oid2)};
purify_oid(L, _) when is_list(L) ->
    {ok, verify_pure_oid(lists:flatten(L))};
purify_oid(X, _) ->
    {error, {invalid_oid, X}}.
    
verify_pure_oid([]) -> 
    [];
verify_pure_oid([H | T]) when is_integer(H) and (H >= 0) ->
    [H | verify_pure_oid(T)];
verify_pure_oid([H | _]) ->
    throw({error, {not_pure_oid, H}}).
    
flatten_oid(XOid, DB)  ->
    Oid = case XOid of
	       [A|T] when is_atom(A) -> 
		   [remove_atom(A, DB)|T];
	       L when is_list(L) -> 
		   XOid;
	       Shit -> 
		   throw({error,
			  {"Invalid oid, not a list of integers: ~w", [Shit]}})
	   end,
    check_is_pure_oid(lists:flatten(Oid)).

remove_atom(AliasName, DB) when is_atom(AliasName) ->
    case snmp_mini_mib:oid(DB, AliasName) of
	false ->
	    throw({error, {"Unknown aliasname in oid: ~w", [AliasName]}});
	Oid -> 
	    Oid
    end;
remove_atom(X, _DB) -> 
    X.

%%----------------------------------------------------------------------
%% Throws if not a list of integers
%%----------------------------------------------------------------------
check_is_pure_oid([]) -> [];
check_is_pure_oid([X | T]) when is_integer(X) and (X >= 0) ->
    [X | check_is_pure_oid(T)];
check_is_pure_oid([X | _T]) ->
    throw({error, {"Invalid oid, it contains a non-integer: ~w", [X]}}).

get_next_iter_impl(0, PrevPDU, _MiniMIB, _PackServ) -> 
    PrevPDU;
get_next_iter_impl(N, PrevPDU, MiniMIB, PackServ) ->
    Oids = [get_oid_from_varbind(Vb) || Vb <- PrevPDU#pdu.varbinds],
    PDU  = make_pdu(get_next, Oids, MiniMIB),
    send_pdu(PDU, MiniMIB, PackServ),
    case receive_response() of
	{error, timeout} ->
	    io:format("(timeout)~n"),
	    get_next_iter_impl(N, PrevPDU, MiniMIB, PackServ);
	{error, _Reason} ->
	    PrevPDU;
	RPDU when is_record(RPDU, pdu) ->
	    io:format("(~w)", [N]),
	    echo_pdu(RPDU, MiniMIB),
	    get_next_iter_impl(N-1, RPDU, MiniMIB, PackServ)
    end.
	
%%--------------------------------------------------
%% Used to resend a PDU. Takes the old PDU and
%% generates a fresh one (with a new requestID).
%%--------------------------------------------------

make_pdu(set, VarsAndValues, MiniMIB) ->
    VBs = [var_and_value_to_varbind(VAV, MiniMIB) || VAV <- VarsAndValues],
    make_pdu_impl(set, VBs);
make_pdu(bulk, {NonRepeaters, MaxRepetitions, Oids}, MiniMIB) ->
    Foids = [flatten_oid(Oid, MiniMIB) || Oid <- Oids], 
    #pdu{type = 'get-bulk-request',
	 request_id   = make_request_id(),
	 error_status = NonRepeaters, 
	 error_index  = MaxRepetitions,
	 varbinds     = [make_vb(Oid) || Oid <- Foids]};
make_pdu(Operation, Oids, MiniMIB) ->
    Foids = [flatten_oid(Oid, MiniMIB) || Oid <- Oids], 
    make_pdu_impl(Operation, Foids).

make_pdu_impl(get, Oids) ->
    #pdu{type         = 'get-request',
	 request_id   = make_request_id(),
	 error_status = noError, 
	 error_index  = 0,
	 varbinds     = [make_vb(Oid) || Oid <- Oids]};

make_pdu_impl(get_next, Oids) ->
    #pdu{type         = 'get-next-request', 
	 request_id   = make_request_id(), 
	 error_status = noError, 
	 error_index  = 0,
	 varbinds     = [make_vb(Oid) || Oid <- Oids]};

make_pdu_impl(set, Varbinds) ->
    #pdu{type         = 'set-request', 
	 request_id   = make_request_id(),
	 error_status = noError, 
	 error_index  = 0, 
	 varbinds     = Varbinds}.

make_discovery_pdu() ->
    make_pdu_impl(get, []).

var_and_value_to_varbind({Oid, Type, Value}, MiniMIB) ->
    Oid2 = flatten_oid(Oid, MiniMIB), 
    #varbind{oid          = Oid2, 
	     variabletype = char_to_type(Type), 
	     value        = Value};
var_and_value_to_varbind({XOid, Value}, MiniMIB) ->
    Oid = flatten_oid(XOid, MiniMIB), 
    #varbind{oid          = Oid, 
	     variabletype = snmp_mini_mib:type(MiniMIB, Oid),
	     value        = Value}.

char_to_type(o) ->
    'OBJECT IDENTIFIER';
char_to_type(i) ->
    'INTEGER';
char_to_type(u) ->
    'Unsigned32';
char_to_type(g) -> % Gauge, Gauge32
    'Unsigned32';
char_to_type(s) ->
    'OCTET STRING'.

make_vb(Oid) ->
    #varbind{oid = Oid, variabletype = 'NULL', value = 'NULL'}.

make_request_id() ->
    %% random:uniform(16#FFFFFFF-1).
    snmp_test_mgr_counter_server:increment(mgr_request_id, 1, 1, 2147483647).

echo_pdu(PDU, MiniMIB) ->
    io:format("~s", [snmp_misc:format_pdu(PDU, MiniMIB)]).


%%----------------------------------------------------------------------
%% Test Sequence
%%----------------------------------------------------------------------
echo_errors({error, Id, {ExpectedFormat, ExpectedData}, {Format, Data}})->
    io:format("* Unexpected Behaviour * Id: ~w.~n"
	      "  Expected: " ++ ExpectedFormat ++ "~n"
	      "  Got:      " ++ Format ++ "~n", 
	      [Id] ++ ExpectedData ++ Data),
    {error, Id, {ExpectedFormat, ExpectedData}, {Format, Data}};
echo_errors(ok) -> ok;
echo_errors({ok, Val}) -> {ok, Val}.

get_response_impl(Id, Vars) ->
    case receive_response() of
	#pdu{type         = 'get-response', 
	     error_status = noError, 
	     error_index  = 0,
	     varbinds     = VBs} ->
	    match_vars(Id, find_pure_oids2(Vars), VBs, []);

	#pdu{type         = Type2, 
	     request_id   = ReqId, 
	     error_status = Err2, 
	     error_index  = Index2} ->
	    {error, 
	     Id, 
	     {"Type: ~w, ErrStat: ~w, Idx: ~w, RequestId: ~w",
	      ['get-response', noError, 0, ReqId]},
	     {"Type: ~w, ErrStat: ~w, Idx: ~w", 
	      [Type2, Err2, Index2]}};

	{error, Reason} -> 
	    format_reason(Id, Reason)
    end.

    

%%----------------------------------------------------------------------
%% Returns: ok | {error, Id, {ExpectedFormat, ExpectedData}, {Format, Data}}
%%----------------------------------------------------------------------
expect_impl(Id, any) -> 
    io:format("expect_impl(~w, any) -> entry ~n", [Id]),
    case receive_response() of
	PDU when is_record(PDU, pdu) -> ok;
	{error, Reason} -> format_reason(Id, Reason)
    end;

expect_impl(Id, return) -> 
    io:format("expect_impl(~w, return) -> entry ~n", [Id]),
    case receive_response() of
	PDU when is_record(PDU, pdu) -> {ok, PDU};
	{error, Reason} -> format_reason(Id, Reason)
    end;

expect_impl(Id, trap) -> 
    io:format("expect_impl(~w, trap) -> entry ~n", [Id]),
    case receive_trap(3500) of
	PDU when is_record(PDU, trappdu) -> ok;
	{error, Reason} -> format_reason(Id, Reason)
    end;

expect_impl(Id, timeout) -> 
    io:format("expect_impl(~w, timeout) -> entry ~n", [Id]),
    receive
	X -> 
	    io:format("expect_impl(~w, timeout) -> "
		      "received unexpected message: ~n~p~n", [Id, X]),
	    {error, Id, {"Timeout", []}, {"Message ~w",  [X]}}
    after 3500 ->
	    ok
    end;

expect_impl(Id, Err) when is_atom(Err) ->
    io:format("expect_impl(~w, ~w) -> entry ~n", [Id, Err]),
    case receive_response() of
	#pdu{error_status = Err} -> 
	    ok;

	#pdu{request_id   = ReqId, 
	     error_status = OtherErr} ->
	    io:format("expect_impl(~w, ~w) -> "
		      "received pdu (~w) with unexpected error-status: "
		      "~n~p~n", [Id, Err, ReqId, OtherErr]),
	    {error, Id, {"ErrorStatus: ~w, RequestId: ~w", [Err,ReqId]},
	     {"ErrorStatus: ~w", [OtherErr]}};

	{error, Reason} -> 
	    format_reason(Id, Reason)
    end;

expect_impl(Id, ExpectedVarbinds) when is_list(ExpectedVarbinds) ->
    io:format("expect_impl(~w) -> entry with"
	      "~n   ExpectedVarbinds: ~p~n", [Id, ExpectedVarbinds]),
    case receive_response() of
	#pdu{type         = 'get-response', 
	     error_status = noError, 
	     error_index  = 0,
	     varbinds     = VBs} ->
	    io:format("expect_impl(~w) -> received pdu with"
		      "~n   VBs: ~p~n", [Id, VBs]),
	    check_vars(Id, find_pure_oids(ExpectedVarbinds), VBs);

	#pdu{type         = Type2, 
	     request_id   = ReqId, 
	     error_status = Err2, 
	     error_index  = Index2} ->
	    io:format("expect_impl(~w) -> received unexpected pdu with"
		      "~n   Type2:  ~p"
		      "~n   ReqId:  ~p"
		      "~n   Err2:   ~p"
		      "~n   Index2: ~p"
		      "~n", [Id, Type2, ReqId, Err2, Index2]),
	    {error, Id, {"Type: ~w, ErrStat: ~w, Idx: ~w, RequestId: ~w", 
			 ['get-response', noError, 0, ReqId]},
	     {"Type: ~w, ErrStat: ~w, Idx: ~w", [Type2, Err2, Index2]}};

	{error, Reason} -> 
	    format_reason(Id, Reason)
    end.

expect_impl(Id, v2trap, ExpectedVarbinds) when is_list(ExpectedVarbinds) ->
    io:format("expect_impl(~w, v2trap) -> entry with"
	      "~n   ExpectedVarbinds: ~p~n", [Id, ExpectedVarbinds]),
    case receive_response() of
	#pdu{type         = 'snmpv2-trap', 
	     error_status = noError, 
	     error_index  = 0,
	     varbinds     = VBs} ->
	    io:format("expect_impl(~w, v2trap) -> received pdu with"
		      "~n   VBs: ~p~n", [Id, VBs]),
	    check_vars(Id, find_pure_oids(ExpectedVarbinds), VBs);

	#pdu{type         = Type2, 
	     request_id   = ReqId, 
	     error_status = Err2, 
	     error_index  = Index2} ->
	    io:format("expect_impl(~w, v2trap) -> received unexpected pdu with"
		      "~n   Type2:  ~p"
		      "~n   ReqId:  ~p"
		      "~n   Err2:   ~p"
		      "~n   Index2: ~p"
		      "~n", [Id, Type2, ReqId, Err2, Index2]),
	    {error, Id, {"Type: ~w, ErrStat: ~w, Idx: ~w, RequestId: ~w", 
			 ['snmpv2-trap', noError, 0, ReqId]},
	     {"Type: ~w, ErrStat: ~w, Idx: ~w", [Type2, Err2, Index2]}};

	{error, Reason} -> 
	    format_reason(Id, Reason)
    end;

expect_impl(Id, report, ExpectedVarbinds) when is_list(ExpectedVarbinds) ->
    io:format("expect_impl(~w, report) -> entry with"
	      "~n   ExpectedVarbinds: ~p~n", [Id, ExpectedVarbinds]),
    case receive_response() of
	#pdu{type         = 'report', 
	     error_status = noError, 
	     error_index  = 0,
	     varbinds     = VBs} ->
	    io:format("expect_impl(~w, report) -> received pdu with"
		      "~n   VBs: ~p~n", [Id, VBs]),
	    check_vars(Id, find_pure_oids(ExpectedVarbinds), VBs);

	#pdu{type         = Type2, 
	     request_id   = ReqId, 
	     error_status = Err2, 
	     error_index  = Index2} ->
	    io:format("expect_impl(~w, report) -> received unexpected pdu with"
		      "~n   Type2:  ~p"
		      "~n   ReqId:  ~p"
		      "~n   Err2:   ~p"
		      "~n   Index2: ~p"
		      "~n", [Id, Type2, ReqId, Err2, Index2]),
	    {error, Id, {"Type: ~w, ErrStat: ~w, Idx: ~w, RequestId: ~w", 
			 [report, noError, 0, ReqId]},
	     {"Type: ~w, ErrStat: ~w, Idx: ~w", [Type2, Err2, Index2]}};

	{error, Reason} -> 
	    format_reason(Id, Reason)
    end;

expect_impl(Id, {inform, Reply}, ExpectedVarbinds) 
  when is_list(ExpectedVarbinds) ->
    io:format("expect_impl(~w, inform) -> entry with"
	      "~n   Reply:            ~p"
	      "~n   ExpectedVarbinds: ~p"
	      "~n", [Id, Reply, ExpectedVarbinds]),
    Resp = receive_response(),
    case Resp of
	#pdu{type         = 'inform-request', 
	     error_status = noError, 
	     error_index  = 0,
	     varbinds     = VBs} ->
	    io:format("expect_impl(~w, inform) -> received pdu with"
		      "~n   VBs: ~p~n", [Id, VBs]),
	    case check_vars(Id, find_pure_oids(ExpectedVarbinds), VBs) of
		ok when (Reply == true) ->
		    io:format("expect_impl(~w, inform) -> send ok response"
			      "~n", [Id]),
		    RespPDU = Resp#pdu{type = 'get-response',
				       error_status = noError,
				       error_index = 0},
		    ?MODULE:rpl(RespPDU),
		    ok;
		ok when (element(1, Reply) == error) ->
		    io:format("expect_impl(~w, inform) -> send error response"
			      "~n", [Id]),
		    {error, Status, Index} = Reply,
		    RespPDU = Resp#pdu{type = 'get-response',
				       error_status = Status,
				       error_index = Index},
		    ?MODULE:rpl(RespPDU),
		    ok;
		ok when (Reply == false) ->
		    io:format("expect_impl(~w, inform) -> no response sent"
			      "~n", [Id]),
		    ok;
		Else ->
		    io:format("expect_impl(~w, inform) -> "
			      "~n   Else: ~p"
			      "~n", [Id, Else]),
		    Else
	    end;

	#pdu{type         = Type2, 
	     request_id   = ReqId, 
	     error_status = Err2, 
	     error_index  = Index2} ->
	    io:format("expect_impl(~w, inform) -> received unexpected pdu with"
		      "~n   Type2:  ~p"
		      "~n   ReqId:  ~p"
		      "~n   Err2:   ~p"
		      "~n   Index2: ~p"
		      "~n", [Id, Type2, ReqId, Err2, Index2]),
	    {error, Id, {"Type: ~w, ErrStat: ~w, Idx: ~w, RequestId: ~w", 
			 ['inform-request', noError, 0, ReqId]},
	     {"Type: ~w, ErrStat: ~w, Idx: ~w", [Type2, Err2, Index2]}};

	{error, Reason} -> 
	    io:format("expect_impl(~w, inform) -> receive failed"
		      "~n   Reason: ~p"
		      "~n", [Id, Reason]),
	    format_reason(Id, Reason)
    end.

expect_impl(Id, Err, Index, any) ->
    io:format("expect_impl(~w, any) -> entry with"
	      "~n   Err:   ~p"
	      "~n   Index: ~p"
	      "~n", [Id, Err, Index]),
    case receive_response() of
	#pdu{type         = 'get-response', 
	     error_status = Err, 
	     error_index  = Index} -> 
	    io:format("expect_impl(~w, any) -> received expected pdu"
		      "~n", [Id]),
	    ok;

	#pdu{type = 'get-response', error_status = Err} when (Index == any) -> 
	    io:format("expect_impl(~w, any) -> received expected pdu (any)"
		      "~n", [Id]),
	    ok;

	#pdu{type         = 'get-response', 
	     request_id   = ReqId, 
	     error_status = Err, 
	     error_index  = Idx} when is_list(Index) ->
	    io:format("expect_impl(~w, any) -> received pdu: "
		      "~n   ReqId: ~p"
		      "~n   Err:   ~p"
		      "~n   Idx:   ~p"
		      "~n", [Id, ReqId, Err, Idx]),
	    case lists:member(Idx, Index) of
		true -> 
		    ok;
		false ->
		    {error, Id, {"ErrStat: ~w, Idx: ~w, RequestId: ~w", 
				 [Err, Index, ReqId]},
		     {"ErrStat: ~w, Idx: ~w", [Err, Idx]}}
	    end;

	#pdu{type         = Type2, 
	     request_id   = ReqId, 
	     error_status = Err2, 
	     error_index  = Index2} ->
	    io:format("expect_impl(~w, any) -> received unexpected pdu: "
		      "~n   Type2:  ~p"
		      "~n   ReqId:  ~p"
		      "~n   Err2:   ~p"
		      "~n   Index2: ~p"
		      "~n", [Id, Type2, ReqId, Err2, Index2]),
	    {error, Id, {"Type: ~w, ErrStat: ~w, Idx: ~w, RequestId: ~w", 
			 ['get-response', Err, Index, ReqId]},
	     {"Type: ~w, ErrStat: ~w, Idx: ~w", [Type2, Err2, Index2]}};

	{error, Reason} -> 
	    format_reason(Id, Reason)
    end;

expect_impl(Id, Err, Index, ExpectedVarbinds) ->
    io:format("expect_impl(~w) -> entry with"
	      "~n   Err:              ~p"
	      "~n   Index:            ~p"
	      "~n   ExpectedVarbinds: ~p"
	      "~n", [Id, Err, Index, ExpectedVarbinds]),
    PureVBs = find_pure_oids(ExpectedVarbinds),
    case receive_response() of
	#pdu{type         = 'get-response', 
	     error_status = Err, 
	     error_index  = Index,
	     varbinds     = VBs} ->
	    check_vars(Id, PureVBs, VBs);

	#pdu{type         = 'get-response', 
	     error_status = Err, 
	     varbinds     = VBs} when (Index == any) ->
	    check_vars(Id, PureVBs, VBs);

	#pdu{type         = 'get-response', 
	     request_id   = ReqId, 
	     error_status = Err, 
	     error_index  = Idx,
	     varbinds     = VBs} when is_list(Index) ->
	    case lists:member(Idx, Index) of
		true ->
		    check_vars(Id, PureVBs, VBs);
		false ->
		    {error,Id,
		     {"ErrStat: ~w, Idx: ~w, Varbinds: ~w, RequestId: ~w",
		      [Err,Index,PureVBs,ReqId]},
		     {"ErrStat: ~w, Idx: ~w, Varbinds: ~w",
		      [Err,Idx,VBs]}}
	    end;

	#pdu{type         = Type2, 
	     request_id   = ReqId, 
	     error_status = Err2, 
	     error_index  = Index2, 
	     varbinds     = VBs} ->
	    {error,Id,
	     {"Type: ~w, ErrStat: ~w, Idx: ~w, Varbinds: ~w, RequestId: ~w",
	      ['get-response',Err,Index,PureVBs,ReqId]},
	     {"Type: ~w, ErrStat: ~w Idx: ~w Varbinds: ~w",
	      [Type2,Err2,Index2,VBs]}};

	{error, Reason} -> 
	    format_reason(Id, Reason)
    end.

expect_impl(Id, trap, Enterp, Generic, Specific, ExpectedVarbinds) ->
    PureE = find_pure_oid(Enterp),
    case receive_trap(3500) of
	#trappdu{enterprise    = PureE, 
		 generic_trap  = Generic,
		 specific_trap = Specific, 
		 varbinds      = VBs} ->
	    check_vars(Id, find_pure_oids(ExpectedVarbinds), VBs);

	#trappdu{enterprise    = Ent2, 
		 generic_trap  = G2,
		 specific_trap = Spec2, 
		 varbinds      = VBs} ->
	    {error, Id,
	     {"Enterprise: ~w, Generic: ~w, Specific: ~w, Varbinds: ~w",
	      [PureE, Generic, Specific, ExpectedVarbinds]},
	     {"Enterprise: ~w, Generic: ~w, Specific: ~w, Varbinds: ~w",
	      [Ent2, G2, Spec2, VBs]}};

	{error, Reason} -> 
	    format_reason(Id, Reason)
    end.

format_reason(Id, Reason) ->
    {error, Id, {"?", []}, {"~w", [Reason]}}.

%%----------------------------------------------------------------------
%% Args: Id, ExpectedVarbinds, GotVarbinds
%% Returns: ok
%% Fails: if not ok
%%----------------------------------------------------------------------
check_vars(_Id,[], []) -> 
    ok;
check_vars(Id,Vars, []) ->
    {error, Id, {"More Varbinds (~w)", [Vars]}, {"Too few", []}};
check_vars(Id,[], Varbinds) ->
    {error,Id, {"Fewer Varbinds", []}, {"Too many (~w)", [Varbinds]}};
check_vars(Id,[{_XOid, any} | Vars], [#varbind{oid = _Oid} |Vbs]) ->
    check_vars(Id,Vars, Vbs);
check_vars(Id,[{Oid, Val} | Vars], [#varbind{oid = Oid, value = Val} |Vbs]) ->
    check_vars(Id,Vars, Vbs);
check_vars(Id,[{Oid, Val} | _], [#varbind{oid = Oid, value = Val2} |_]) ->
    {error, Id, {" Varbind: ~w = ~w", [Oid, Val]}, {"Value: ~w", [Val2]}};
check_vars(Id,[{Oid, _Val} | _], [#varbind{oid = Oid2, value = _Val2} |_]) ->
    {error, Id, {"Oid: ~w", [Oid]}, {"Oid: ~w", [Oid2]}}.

match_vars(Id, [Oid|T], [#varbind{oid = Oid, value = Value} | Vbs], Res) ->
    match_vars(Id, T, Vbs, [Value | Res]);
match_vars(_Id, [], [], Res) ->
    {ok, lists:reverse(Res)};
match_vars(Id, [Oid | _], [#varbind{oid = Oid2}], _Res) ->
    {error, Id, {" Oid: ~w", [Oid]}, {"Oid2: ~w", [Oid2]}};
match_vars(Id, Vars, [], _Res) ->
    {error, Id, {"More Varbinds (~w)", [Vars]}, {"Too few", []}};
match_vars(Id, [], Varbinds, _Res) ->
    {error,Id, {"Fewer Varbinds", []}, {"Too many (~w)", [Varbinds]}}.

    

find_pure_oids([]) -> [];
find_pure_oids([{XOid, Q}|T]) ->
    [{find_pure_oid(XOid), Q} | find_pure_oids(T)].

find_pure_oids2([]) -> [];
find_pure_oids2([XOid|T]) ->
    [find_pure_oid(XOid) | find_pure_oids2(T)].


%%----------------------------------------------------------------------
%% Returns: Oid
%% Fails: malformed oids
%%----------------------------------------------------------------------
find_pure_oid(XOid) ->
    case gen_server:call(?MODULE, {find_pure_oid, XOid}, infinity) of
	{error, {Format, Data}} ->
	    ok = io:format(Format, Data),
	    exit(malformed_oid);
	Oid when is_list(Oid) -> Oid
    end.

get_value(Opt, Opts, Default) ->
    case snmp_misc:assq(Opt,Opts) of
	{value, C} -> C;
	false -> Default
    end.


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

call(Req) ->
    call(Req, infinity).

call(Req, To) ->
    gen_server:call(?SERVER, Req, To).

cast(Msg) ->
    gen_server:cast(?SERVER, Msg).


%%----------------------------------------------------------------------
%% Debug
%%----------------------------------------------------------------------

sizeOf(L) when is_list(L) ->
    length(lists:flatten(L));
sizeOf(B) when is_binary(B) ->
    size(B).

d(F,A) -> d(get(debug),F,A).

d(true,F,A) ->
    io:format("*** [~s] MGR_DBG *** " ++ F ++ "~n",
	      [formated_timestamp()|A]);
d(_,_F,_A) -> 
    ok.


formated_timestamp() ->
    snmp_test_lib:formated_timestamp().