-module(s1ap_proxy_test).

-include_lib("eunit/include/eunit.hrl").
-include("s1gw_metrics.hrl").
-include("pfcp_mock.hrl").


-define(GlobalENBId, "001-01-0").

-define(_assertMetric(Name, Value),
        ?_assertEqual(Value, s1gw_metrics:get_current_value(Name))).

-define(_assertMetricENB(Name, Value),
        ?_assertMetric(s1gw_metrics:enb_metric(Name, ?GlobalENBId), Value)).


%% ------------------------------------------------------------------
%% setup functions
%% ------------------------------------------------------------------

-define(TC(Fun), {setup,
                  fun start/0,
                  fun stop/1,
                  Fun}).


start() ->
    pfcp_mock:mock_all(),
    exometer:start(),
    s1gw_metrics:init(),
    enb_registry:start_link(),
    gtpu_kpi:start_link(#{enable => false}),
    {ok, EnbHandle} = enb_registry:enb_register(),
    {ok, Pid} = s1ap_proxy:start_link(self()),
    s1ap_proxy:set_genb_id(Pid, ?GlobalENBId),
    ok = enb_registry:enb_unregister(EnbHandle),
    #{handler => Pid}.


stop(#{handler := Pid}) ->
    s1ap_proxy:shutdown(Pid),
    exometer:stop(),
    gtpu_kpi:shutdown(),
    enb_registry:shutdown(),
    pfcp_mock:unmock_all().


%% ------------------------------------------------------------------
%% testcase descriptions
%% ------------------------------------------------------------------

s1ap_proxy_test_() ->
    [{"S1 SETUP REQUEST/RESPONSE (unmodified)",
      ?TC(fun test_s1_setup/1)},
     {"E-RAB SETUP REQUEST/RESPONSE",
      ?TC(fun test_e_rab_setup/1)},
     {"E-RAB SETUP REQUEST (failure)",
      ?TC(fun test_e_rab_setup_req_fail/1)},
     {"E-RAB SETUP RESPONSE during RELEASE",
      ?TC(fun test_e_rab_setup_rsp_during_release/1)},
     {"E-RAB SETUP REQUEST (duplicate)",
      ?TC(fun test_e_rab_setup_dup/1)},
     {"E-RAB RELEASE COMMAND/RESPONSE",
      ?TC(fun test_e_rab_release_cmd/1)},
     {"E-RAB RELEASE INDICATION",
      ?TC(fun test_e_rab_release_ind/1)},
     {"E-RAB MODIFY REQUEST/RESPONSE (success)",
      ?TC(fun test_e_rab_modify_req_rsp/1)},
     {"E-RAB MODIFY REQUEST/RESPONSE (failure)",
      ?TC(fun test_e_rab_modify_req_rsp_fail/1)},
     {"E-RAB MODIFICATION INDICATION (modified)",
      ?TC(fun test_e_rab_modify_ind_cnf_modified/1)},
     {"E-RAB MODIFICATION INDICATION (not modified)",
      ?TC(fun test_e_rab_modify_ind_cnf_not_modified/1)},
     {"E-RAB MODIFICATION INDICATION (release)",
      ?TC(fun test_e_rab_modify_ind_cnf_release/1)},
     {"INITIAL CONTEXT SETUP REQUEST/RESPONSE",
      ?TC(fun test_initial_context_setup/1)},
     {"INITIAL CONTEXT SETUP REQUEST/RESPONSE (duplicate)",
      ?TC(fun test_initial_context_setup_dup/1)},
     {"INITIAL CONTEXT SETUP RESPONSE during RELEASE",
      ?TC(fun test_initial_context_setup_rsp_during_release/1)},
     {"HANDOVER REQUIRED/COMMAND",
      ?TC(fun test_handover_preparation/1)},
     {"HANDOVER REQUEST/REQUEST ACKNOWLEDGE",
      ?TC(fun test_handover_res_alloc/1)},
     {"UE CONTEXT RELEASE REQUEST",
      ?TC(fun test_ue_ctx_release_req/1)},
     {"UE CONTEXT RELEASE COMMAND/COMPLETE",
      ?TC(fun test_ue_ctx_release_cmd/1)},
     {"ASN.1 parsing error (drop)",
      ?TC(fun test_drop_asn1_error/1)},
     {"PDU processing error (drop)",
      ?TC(fun test_drop_proc_error/1)}].


%% ------------------------------------------------------------------
%% actual testcases
%% ------------------------------------------------------------------

test_s1_setup(#{handler := Pid}) ->
    SetupReq = s1ap_samples:s1_setup_req_pdu(),
    SetupRsp = s1ap_samples:s1_setup_rsp_pdu(),
    %% Expect the PDUs to be proxied unmodified
    [?_assertEqual({forward, SetupReq}, s1ap_proxy:process_pdu(Pid, SetupReq)),
     ?_assertEqual({forward, SetupRsp}, s1ap_proxy:process_pdu(Pid, SetupRsp)),
     %% global counters
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     %% per-eNB counters
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2)].


test_e_rab_setup(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReqIn = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    SetupReqExp = s1ap_samples:e_rab_setup_req_pdu(?ADDR_A2U, ?TEID_A2U),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRspIn = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    SetupRspExp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_C2U, ?TEID_C2U),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, s1ap_samples:s1_setup_req_pdu())),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, s1ap_samples:s1_setup_rsp_pdu())),
     ?_assertEqual({forward, SetupReqExp}, s1ap_proxy:process_pdu(Pid, SetupReqIn)),
     ?_assertEqual({forward, SetupRspExp}, s1ap_proxy:process_pdu(Pid, SetupRspIn)),
     %% global counters
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_RSP, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     %% per-eNB counters
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_REQ, 1),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_RSP, 1),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     ?_assertMatch({ok, _}, s1ap_proxy:fetch_erab(Pid, {7, 6})),
     ?_assertMatch([_], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_setup_req_fail(#{handler := Pid}) ->
    %% pfcp_peer:session_establish_req/3 responds with a reject
    PDU = pfcp_mock:pdu_rsp_reject(session_establishment_response, ?SEID_Loc),
    pfcp_mock:mock_req(session_establish_req, PDU),
    %% eNB <- [S1GW <- MME] E-RAB SETUP REQUEST
    %% eNB -- [S1GW -> MME] E-RAB SETUP RESPONSE (failure)
    SetupReqIn = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    SetupRspExp = s1ap_samples:e_rab_setup_rsp_fail_pdu(),

    [?_assertEqual({reply, SetupRspExp}, s1ap_proxy:process_pdu(Pid, SetupReqIn)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 0),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 0),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_REPLY_ALL, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_REPLY_ERAB_SETUP_RSP, 1),
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_setup_rsp_during_release(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReqIn = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    SetupReqExp = s1ap_samples:e_rab_setup_req_pdu(?ADDR_A2U, ?TEID_A2U),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRspIn = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    SetupRspExp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_C2U, ?TEID_C2U),
    %% [eNB <- MME] E-RAB RELEASE COMMAND
    ReleaseCmd = s1ap_samples:e_rab_release_cmd_pdu(),
    %% [eNB -> MME] E-RAB RELEASE RESPONSE
    ReleaseRsp = s1ap_samples:e_rab_release_rsp_pdu(),

    [%% MME orders allocation of an E-RAB
     ?_assertEqual({forward, SetupReqExp}, s1ap_proxy:process_pdu(Pid, SetupReqIn)),
     %% MME orders release of an E-RAB, even before the eNB responds
     ?_assertEqual({forward, ReleaseCmd}, s1ap_proxy:process_pdu(Pid, ReleaseCmd)),
     %% eNB confirms allocation of an E-RAB
     ?_assertEqual({forward, SetupRspExp}, s1ap_proxy:process_pdu(Pid, SetupRspIn)),
     %% eNB confirms release of an E-RAB
     ?_assertEqual({forward, ReleaseRsp}, s1ap_proxy:process_pdu(Pid, ReleaseRsp)),
     %% E-RAB have been released at this point
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid)),

     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 4),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_SETUP_RSP, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_RELEASE_CMD, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_RELEASE_RSP, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 4),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2)].


test_e_rab_setup_dup(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReqIn = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRspIn = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% eNB <- [S1GW <- MME] E-RAB SETUP REQUEST (duplicate)
    %% eNB -- [S1GW -> MME] E-RAB SETUP RESPONSE (failure)
    SetupRspExp = s1ap_samples:e_rab_setup_rsp_fail_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupReqIn)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupRspIn)),
     %% duplicate E-RAB SETUP REQUEST triggers a reply
     ?_assertEqual({reply, SetupRspExp}, s1ap_proxy:process_pdu(Pid, SetupReqIn)),
     ?_assertMatch({ok, _}, s1ap_proxy:fetch_erab(Pid, {7, 6})),
     ?_assertMatch([_], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_release_cmd(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReq = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRsp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% [eNB <- MME] E-RAB RELEASE COMMAND
    ReleaseCmd = s1ap_samples:e_rab_release_cmd_pdu(),
    %% [eNB -> MME] E-RAB RELEASE RESPONSE
    ReleaseRsp = s1ap_samples:e_rab_release_rsp_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupRsp)),
     ?_assertMatch([_], s1ap_proxy:fetch_erab_list(Pid)),
     ?_assertEqual({forward, ReleaseCmd}, s1ap_proxy:process_pdu(Pid, ReleaseCmd)),
     ?_assertEqual({forward, ReleaseRsp}, s1ap_proxy:process_pdu(Pid, ReleaseRsp)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_RELEASE_CMD, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_RELEASE_RSP, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_release_ind(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReq = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRsp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% [eNB -> MME] E-RAB RELEASE INDICATION
    ReleaseInd = s1ap_samples:e_rab_release_ind_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupRsp)),
     ?_assertEqual({forward, ReleaseInd}, s1ap_proxy:process_pdu(Pid, ReleaseInd)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 3),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_RELEASE_IND, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 1),
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_modify_req_rsp(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReq = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRsp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% eNB <- [S1GW <- MME] E-RAB MODIFY REQUEST (new F-TEID)
    ModifyReqIn = s1ap_samples:e_rab_modify_req_pdu(?ADDR_U2CM, ?TEID_U2CM),
    %% [eNB <- S1GW] <- MME E-RAB MODIFY REQUEST
    %% for the eNB F-TEID remains unchanged
    ModifyReqExp = s1ap_samples:e_rab_modify_req_pdu(?ADDR_A2U, ?TEID_A2U),
    %% [eNB -> MME] E-RAB MODIFY RESPONSE
    ModifyRsp = s1ap_samples:e_rab_modify_rsp_pdu(),
    %% [eNB -> MME] E-RAB RELEASE INDICATION
    ReleaseInd = s1ap_samples:e_rab_release_ind_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupRsp)),
     ?_assertEqual({forward, ModifyReqExp}, s1ap_proxy:process_pdu(Pid, ModifyReqIn)),
     ?_assertEqual({forward, ModifyRsp}, s1ap_proxy:process_pdu(Pid, ModifyRsp)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MODIFY_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MODIFY_RSP, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 1),
     ?_assertEqual({forward, ReleaseInd}, s1ap_proxy:process_pdu(Pid, ReleaseInd)),
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_modify_req_rsp_fail(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReq = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRsp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% [eNB <- MME] E-RAB MODIFY REQUEST (new F-TEID)
    ModifyReq = s1ap_samples:e_rab_modify_req_pdu(?ADDR_U2CM, ?TEID_U2CM),
    %% [eNB -> MME] E-RAB MODIFY RESPONSE (failure)
    ModifyRsp = s1ap_samples:e_rab_modify_rsp_fail_pdu(),
    %% [eNB -> MME] E-RAB RELEASE INDICATION
    ReleaseInd = s1ap_samples:e_rab_release_ind_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupRsp)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, ModifyReq)),
     ?_assertEqual({forward, ModifyRsp}, s1ap_proxy:process_pdu(Pid, ModifyRsp)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MODIFY_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MODIFY_RSP, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 1),
     ?_assertEqual({forward, ReleaseInd}, s1ap_proxy:process_pdu(Pid, ReleaseInd)),
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_modify_ind_cnf_modified(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReq = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRsp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% [eNB -> S1GW] -> MME E-RAB MODIFICATION INDICATION (new F-TEID)
    ModifyIndIn = s1ap_samples:e_rab_modify_ind_pdu(?ADDR_U2AM, ?TEID_U2AM),
    %% eNB -> [S1GW -> MME] E-RAB MODIFICATION INDICATION
    %% for the MME F-TEID remains unchanged
    ModifyIndExp = s1ap_samples:e_rab_modify_ind_pdu(?ADDR_C2U, ?TEID_C2U),
    %% [eNB <- MME] E-RAB MODIFICATION CONFIRMATION
    ModifyCnf = s1ap_samples:e_rab_modify_cnf_pdu(modified),
    %% [eNB -> MME] E-RAB RELEASE INDICATION
    ReleaseInd = s1ap_samples:e_rab_release_ind_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupRsp)),
     ?_assertEqual({forward, ModifyIndExp}, s1ap_proxy:process_pdu(Pid, ModifyIndIn)),
     ?_assertEqual({forward, ModifyCnf}, s1ap_proxy:process_pdu(Pid, ModifyCnf)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MOD_IND, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MOD_CNF, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 1),
     ?_assertEqual({forward, ReleaseInd}, s1ap_proxy:process_pdu(Pid, ReleaseInd)),
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_modify_ind_cnf_not_modified(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReq = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRsp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% [eNB -> S1GW] -> MME E-RAB MODIFICATION INDICATION (new F-TEID)
    ModifyIndIn = s1ap_samples:e_rab_modify_ind_pdu(?ADDR_U2AM, ?TEID_U2AM),
    %% eNB -> [S1GW -> MME] E-RAB MODIFICATION INDICATION
    %% for the MME F-TEID remains unchanged
    ModifyIndExp = s1ap_samples:e_rab_modify_ind_pdu(?ADDR_C2U, ?TEID_C2U),
    %% [eNB <- MME] E-RAB MODIFICATION CONFIRMATION
    ModifyCnf = s1ap_samples:e_rab_modify_cnf_pdu(not_modified),
    %% [eNB -> MME] E-RAB RELEASE INDICATION
    ReleaseInd = s1ap_samples:e_rab_release_ind_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupRsp)),
     ?_assertEqual({forward, ModifyIndExp}, s1ap_proxy:process_pdu(Pid, ModifyIndIn)),
     ?_assertEqual({forward, ModifyCnf}, s1ap_proxy:process_pdu(Pid, ModifyCnf)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MOD_IND, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MOD_CNF, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 1),
     ?_assertEqual({forward, ReleaseInd}, s1ap_proxy:process_pdu(Pid, ReleaseInd)),
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid))].


test_e_rab_modify_ind_cnf_release(#{handler := Pid}) ->
    %% [eNB <- MME] E-RAB SETUP REQUEST
    SetupReq = s1ap_samples:e_rab_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] E-RAB SETUP RESPONSE
    SetupRsp = s1ap_samples:e_rab_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% [eNB -> S1GW] -> MME E-RAB MODIFICATION INDICATION (new F-TEID)
    ModifyIndIn = s1ap_samples:e_rab_modify_ind_pdu(?ADDR_U2AM, ?TEID_U2AM),
    %% eNB -> [S1GW -> MME] E-RAB MODIFICATION INDICATION
    %% for the MME F-TEID remains unchanged
    ModifyIndExp = s1ap_samples:e_rab_modify_ind_pdu(?ADDR_C2U, ?TEID_C2U),
    %% [eNB <- MME] E-RAB MODIFICATION CONFIRMATION
    ModifyCnf = s1ap_samples:e_rab_modify_cnf_pdu(release),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, SetupRsp)),
     ?_assertEqual({forward, ModifyIndExp}, s1ap_proxy:process_pdu(Pid, ModifyIndIn)),
     ?_assertEqual({forward, ModifyCnf}, s1ap_proxy:process_pdu(Pid, ModifyCnf)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MOD_IND, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ERAB_MOD_CNF, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 1),
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid))].


test_initial_context_setup(#{handler := Pid}) ->
    %% [eNB <- MME] INITIAL CONTEXT SETUP REQUEST
    InitCtxSetupReqIn = s1ap_samples:initial_context_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    InitCtxSetupReqExp = s1ap_samples:initial_context_setup_req_pdu(?ADDR_A2U, ?TEID_A2U),
    %% [eNB -> MME] INITIAL CONTEXT SETUP RESPONSE
    InitCtxSetupRspIn = s1ap_samples:initial_context_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    InitCtxSetupRspExp = s1ap_samples:initial_context_setup_rsp_pdu(?ADDR_C2U, ?TEID_C2U),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, s1ap_samples:s1_setup_req_pdu())),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, s1ap_samples:s1_setup_rsp_pdu())),
     ?_assertEqual({forward, InitCtxSetupReqExp}, s1ap_proxy:process_pdu(Pid, InitCtxSetupReqIn)),
     ?_assertEqual({forward, InitCtxSetupRspExp}, s1ap_proxy:process_pdu(Pid, InitCtxSetupRspIn)),
     %% global counters
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_INIT_CTX_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_INIT_CTX_RSP, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     %% per-eNB counters
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_INIT_CTX_REQ, 1),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_INIT_CTX_RSP, 1),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     ?_assertMatch({ok, _}, s1ap_proxy:fetch_erab(Pid, {1, 5})),
     ?_assertMatch([_], s1ap_proxy:fetch_erab_list(Pid))].


test_initial_context_setup_dup(#{handler := Pid}) ->
    %% [eNB <- MME] INITIAL CONTEXT SETUP REQUEST
    InitCtxSetupReqIn = s1ap_samples:initial_context_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] INITIAL CONTEXT SETUP RESPONSE
    InitCtxSetupRspIn = s1ap_samples:initial_context_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, InitCtxSetupReqIn)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, InitCtxSetupRspIn)),
     %% duplicate INITIAL CONTEXT SETUP REQUEST results in the PDU being dropped
     ?_assertEqual({drop, InitCtxSetupReqIn}, s1ap_proxy:process_pdu(Pid, InitCtxSetupReqIn)),
     ?_assertMatch({ok, _}, s1ap_proxy:fetch_erab(Pid, {1, 5})),
     ?_assertMatch([_], s1ap_proxy:fetch_erab_list(Pid))].


test_initial_context_setup_rsp_during_release(#{handler := Pid}) ->
    %% [eNB <- MME] INITIAL CONTEXT SETUP REQUEST
    InitCtxSetupReq = s1ap_samples:initial_context_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    InitCtxSetupReqExp = s1ap_samples:initial_context_setup_req_pdu(?ADDR_A2U, ?TEID_A2U),
    %% [eNB -> MME] INITIAL CONTEXT SETUP RESPONSE
    InitCtxSetupRsp = s1ap_samples:initial_context_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    InitCtxSetupRspExp = s1ap_samples:initial_context_setup_rsp_pdu(?ADDR_C2U, ?TEID_C2U),
    %% [eNB -> MME] UE CONTEXT RELEASE REQUEST
    UeCtxReleaseReq = s1ap_samples:ue_ctx_release_req_pdu(),
    %% [eNB <- MME] UE CONTEXT RELEASE COMMAND
    UeCtxReleaseCmd = s1ap_samples:ue_ctx_release_cmd_pdu(),
    %% [eNB -> MME] UE CONTEXT RELEASE COMPLETE
    UeCtxReleaseCompl = s1ap_samples:ue_ctx_release_compl_pdu(),

    [%% MME orders creation of the UE context
     ?_assertMatch({forward, InitCtxSetupReqExp}, s1ap_proxy:process_pdu(Pid, InitCtxSetupReq)),
     %% eNB requests the UE context release before even responding to the first PDU
     ?_assertEqual({forward, UeCtxReleaseReq}, s1ap_proxy:process_pdu(Pid, UeCtxReleaseReq)),
     %% MME orders release of the UE context, as requested
     ?_assertEqual({forward, UeCtxReleaseCmd}, s1ap_proxy:process_pdu(Pid, UeCtxReleaseCmd)),
     %% eNB confirms creation of the UE context
     ?_assertEqual({forward, InitCtxSetupRspExp}, s1ap_proxy:process_pdu(Pid, InitCtxSetupRsp)),
     %% eNB confirms release of the UE context
     ?_assertEqual({forward, UeCtxReleaseCompl}, s1ap_proxy:process_pdu(Pid, UeCtxReleaseCompl)),
     %% the UE context has been released, so no E-RAB FSMs shall remain
     ?_assertEqual([], s1ap_proxy:fetch_erab_list(Pid)),

     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 5),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_INIT_CTX_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_INIT_CTX_RSP, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_CMD, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_COMPL, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 5),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 3)].


test_handover_preparation(#{handler := Pid}) ->
    %% [eNB -> MME] HANDOVER REQUIRED
    HandoverRqd = s1ap_samples:handover_required_pdu(),
    %% [MME -> eNB] HANDOVER COMMAND
    HandoverCmd = s1ap_samples:handover_command_pdu(),

    [?_assertMatch({forward, HandoverRqd}, s1ap_proxy:process_pdu(Pid, HandoverRqd)),
     ?_assertMatch({forward, HandoverCmd}, s1ap_proxy:process_pdu(Pid, HandoverCmd)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_HANDOVER_CMD, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 0),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     ?_assertMatch([], s1ap_proxy:fetch_erab_list(Pid))].

test_handover_res_alloc(#{handler := Pid}) ->
    %% [MME -> eNB] HANDOVER REQUEST
    HandoverReqIn = s1ap_samples:handover_request_pdu(?ADDR_U2C, ?TEID_U2C),
    HandoverReqExp = s1ap_samples:handover_request_pdu(?ADDR_A2U, ?TEID_A2U),
    %% [eNB -> MME] HANDOVER REQUEST ACKNOWLEDGE
    HandoverAckIn = s1ap_samples:handover_request_ack_pdu(?ADDR_U2A, ?TEID_U2A),
    HandoverAckExp = s1ap_samples:handover_request_ack_pdu(?ADDR_C2U, ?TEID_C2U),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, s1ap_samples:s1_setup_req_pdu())),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, s1ap_samples:s1_setup_rsp_pdu())),
     ?_assertEqual({forward, HandoverReqExp}, s1ap_proxy:process_pdu(Pid, HandoverReqIn)),
     ?_assertEqual({forward, HandoverAckExp}, s1ap_proxy:process_pdu(Pid, HandoverAckIn)),
     %% global counters
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_HANDOVER_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_HANDOVER_REQ_ACK, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     %% per-eNB counters
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_HANDOVER_REQ, 1),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_HANDOVER_REQ_ACK, 1),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     ?_assertMatch([_], s1ap_proxy:fetch_erab_list(Pid))].


test_ue_ctx_release_req(#{handler := Pid}) ->
    %% [eNB <- MME] INITIAL CONTEXT SETUP REQUEST
    InitCtxSetupReq = s1ap_samples:initial_context_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] INITIAL CONTEXT SETUP RESPONSE
    InitCtxSetupRsp = s1ap_samples:initial_context_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% [eNB -> MME] UE CONTEXT RELEASE REQUEST
    UeCtxReleaseReq = s1ap_samples:ue_ctx_release_req_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, InitCtxSetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, InitCtxSetupRsp)),
     ?_assertEqual({forward, UeCtxReleaseReq}, s1ap_proxy:process_pdu(Pid, UeCtxReleaseReq)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_REQ, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 1),
     %% The E-RAB FSM is expected to remain alive and unchanged,
     %% since this procedure itself does not terminate the UE contect.
     ?_assertMatch([_], s1ap_proxy:fetch_erab_list(Pid))].


test_ue_ctx_release_cmd(#{handler := Pid}) ->
    %% [eNB <- MME] INITIAL CONTEXT SETUP REQUEST
    InitCtxSetupReq = s1ap_samples:initial_context_setup_req_pdu(?ADDR_U2C, ?TEID_U2C),
    %% [eNB -> MME] INITIAL CONTEXT SETUP RESPONSE
    InitCtxSetupRsp = s1ap_samples:initial_context_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),
    %% [eNB <- MME] UE CONTEXT RELEASE COMMAND
    UeCtxReleaseCmd = s1ap_samples:ue_ctx_release_cmd_pdu(),
    %% [eNB -> MME] UE CONTEXT RELEASE COMPLETE
    UeCtxReleaseCompl = s1ap_samples:ue_ctx_release_compl_pdu(),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, InitCtxSetupReq)),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, InitCtxSetupRsp)),
     ?_assertEqual({forward, UeCtxReleaseCmd}, s1ap_proxy:process_pdu(Pid, UeCtxReleaseCmd)),
     ?_assertEqual({forward, UeCtxReleaseCompl}, s1ap_proxy:process_pdu(Pid, UeCtxReleaseCompl)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_CMD, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_RELEASE_CTX_COMPL, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_ALL, 2 + 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_PROC, 2),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_OUT_PKT_FWD_UNMODIFIED, 2),
     ?_assertMatch([], s1ap_proxy:fetch_erab_list(Pid))].


test_drop_asn1_error(#{handler := Pid}) ->
    PDU = << 16#de, 16#ad, 16#be, 16#ef >>,
    [?_assertEqual({drop, PDU}, s1ap_proxy:process_pdu(Pid, PDU)),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_DROP_ALL, 1)].


test_drop_proc_error(#{handler := Pid}) ->
    %% [eNB -> MME] INITIAL CONTEXT SETUP RESPONSE
    InitCtxSetupRsp = s1ap_samples:initial_context_setup_rsp_pdu(?ADDR_U2A, ?TEID_U2A),

    [?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, s1ap_samples:s1_setup_req_pdu())),
     ?_assertMatch({forward, _}, s1ap_proxy:process_pdu(Pid, s1ap_samples:s1_setup_rsp_pdu())),
     %% INITIAL CONTEXT SETUP RESPONSE without prior request
     %% "Failed to process INITIAL CONTEXT SETUP RESPONSE: erab_not_registered"
     ?_assertMatch({drop, _}, s1ap_proxy:process_pdu(Pid, InitCtxSetupRsp)),
     %% global metrics
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, 1),
     ?_assertMetric(?S1GW_CTR_S1AP_PROXY_IN_PKT_DROP_ALL, 1),
     %% per-eNB metrics
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_ALL, 2 + 1),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_PROC_ERROR, 1),
     ?_assertMetricENB(?S1GW_CTR_S1AP_PROXY_IN_PKT_DROP_ALL, 1)].


%% vim:set ts=4 sw=4 et:
