-module(erab_fsm_test).

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

-define(U2C, {?TEID_U2C, ?ADDR_U2C}).
-define(C2U, {?TEID_C2U, ?ADDR_C2U}).
-define(A2U, {?TEID_A2U, ?ADDR_A2U}).
-define(U2A, {?TEID_U2A, ?ADDR_U2A}).

-define(U2CM, {?TEID_U2CM, ?ADDR_U2CM}).
-define(U2AM, {?TEID_U2AM, ?ADDR_U2AM}).

%% ------------------------------------------------------------------
%% setup/misc functions
%% ------------------------------------------------------------------

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


start() ->
    %% mock the PFCP peer API
    pfcp_mock:mock_all(),
    %% start an E-RAB FSM
    {ok, Pid} = erab_fsm:start_link(?MODULE),
    Pid.


stop(_Pid) ->
    pfcp_mock:unmock_all().


erab_fsm_info(Pid, Key) ->
    Info = erab_fsm:fetch_info(Pid),
    maps:get(Key, Info).


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

erab_setup_test_() ->
    [{"E-RAB setup :: success",
      ?TC(fun test_erab_setup_success/1)},
     {"E-RAB setup :: PFCP session establishment error",
      ?TC(fun test_erab_setup_pfcp_establish_error/1)},
     {"E-RAB setup :: PFCP session modification error",
      ?TC(fun test_erab_setup_pfcp_modify_error/1)}].


%% 8.2.4.2 Interactions with E-RAB Setup procedure or E-RAB Modify procedure:
%% TODO: test E-RAB MODIFY IND/CNF during E-RAB SETUP
%% TODO: test E-RAB MODIFY IND/CNF during E-RAB MODIFY
erab_modify_test_() ->
    [{"E-RAB MODIFY REQ :: ACK",
      ?TC(fun test_erab_modify_req_ack/1)},
     {"E-RAB MODIFY REQ :: NACK",
      ?TC(fun test_erab_modify_req_nack/1)},
     {"E-RAB MODIFY IND :: ACK",
      ?TC(fun test_erab_modify_ind_ack/1)},
     {"E-RAB MODIFY IND :: NACK",
      ?TC(fun test_erab_modify_ind_nack/1)}].


erab_release_test_() ->
    [{"E-RAB RELEASE CMD (in state erab_setup) :: success",
      ?TC(fun test_erab_setup_release_cmd_success/1)},
     {"E-RAB RELEASE CMD (in state erab_wait_setup_rsp) :: success",
      ?TC(fun test_erab_wait_setup_rsp_release_cmd_success/1)},
     {"E-RAB RELEASE IND (in state erab_setup) :: success",
      ?TC(fun test_erab_setup_release_ind_success/1)},
     {"E-RAB RELEASE IND (in state erab_wait_setup_rsp) :: success",
      ?TC(fun test_erab_wait_setup_rsp_release_ind_success/1)},
     {"E-RAB RELEASE IND (in state erab_wait_release_rsp) :: success",
      ?TC(fun test_erab_wait_release_rsp_release_ind_success/1)},
     {"E-RAB release :: PFCP session deletion error",
      ?TC(fun test_erab_release_pfcp_delete_error/1)},
     {"E-RAB release :: late E-RAB SETUP.rsp",
      ?TC(fun test_erab_wait_release_rsp_setup_rsp/1)}].


erab_info_test_() ->
    [{"E-RAB info :: state",
      ?TC(fun test_erab_info_state/1)},
     {"E-RAB info :: F-TEIDs",
      ?TC(fun test_erab_info_f_teid/1)}].


erab_shutdown_test_() ->
    [{"E-RAB shutdown (no session)",
      ?TC(fun test_erab_shutdown_no_session/1)},
     {"E-RAB shutdown (expect session deletion)",
      ?TC(fun test_erab_shutdown_session_del/1)}].


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

test_erab_setup_success(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertEqual(ok, erab_fsm:shutdown(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_setup_pfcp_establish_error(Pid) ->
    %% pfcp_peer:session_delete_req/1 shall not be called
    ok = pfcp_mock:unmock_req(session_delete_req, 1),
    %% 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),
    unlink(Pid), %% we expect the FSM to terminate abnormally
    Error = {unexp_pdu, session_establish},
    [?_assertEqual({error, Error}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_setup_pfcp_modify_error(Pid) ->
    %% pfcp_peer:session_modify_req/3 responds with a reject
    PDU = pfcp_mock:pdu_rsp_reject(session_modification_response, ?SEID_Loc),
    pfcp_mock:mock_req(session_modify_req, PDU),
    unlink(Pid), %% we expect the FSM to terminate abnormally
    Error = {unexp_pdu, session_modify},
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({error, Error}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_modify_req(Pid, Res) ->
    U2C = case Res of
        ack -> ?U2CM;
        nack -> ?U2C
    end,
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     %% E-RAB MODIFY Req indicates the new (modified) F-TEID to be used
     %% in the UPF -> Core direction.  The FSM logic indicates the old
     %% (unmodified) F-TEID to be used in the UPF <- Access direction.
     ?_assertEqual({ok, ?A2U}, erab_fsm:erab_modify_req(Pid, ?U2CM)),
     %% Before getting the Rsp, the U2C F-TEID remains unchanged
     ?_assertEqual(?U2C, erab_fsm_info(Pid, f_teid_u2c)),
     %% After getting the Rsp (ACK), the new (modified) U2C F-TEID is used
     %% After getting the Rsp (NACK), the U2C F-TEID remains unchanged
     ?_assertEqual(ok, erab_fsm:erab_modify_rsp(Pid, Res)),
     ?_assertEqual(U2C, erab_fsm_info(Pid, f_teid_u2c)),
     ?_assertEqual(ok, erab_fsm:shutdown(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_modify_req_ack(Pid) ->
    test_erab_modify_req(Pid, ack).


test_erab_modify_req_nack(Pid) ->
    test_erab_modify_req(Pid, nack).


test_erab_modify_ind(Pid, Res) ->
    U2A = case Res of
        ack -> ?U2AM;
        nack -> ?U2A
    end,
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     %% E-RAB MODIFY Ind indicates the new (modified) F-TEID to be used
     %% in the UPF -> Access direction.  The FSM logic indicates the old
     %% (unmodified) F-TEID to be used in the UPF <- Core direction.
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_modify_ind(Pid, ?U2AM)),
     %% Before getting the Rsp, the U2C F-TEID remains unchanged
     ?_assertEqual(?U2A, erab_fsm_info(Pid, f_teid_u2a)),
     %% After getting the Cnf (ACK), the new (modified) U2A F-TEID is used
     %% After getting the Cnf (NACK), the U2A F-TEID remains unchanged
     ?_assertEqual(ok, erab_fsm:erab_modify_cnf(Pid, Res)),
     ?_assertEqual(U2A, erab_fsm_info(Pid, f_teid_u2a)),
     ?_assertEqual(ok, erab_fsm:shutdown(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_modify_ind_ack(Pid) ->
    test_erab_modify_ind(Pid, ack).


test_erab_modify_ind_nack(Pid) ->
    test_erab_modify_ind(Pid, nack).


%% test E-RAB RELEASE.{cmd,rsp} received in state erab_setup
test_erab_setup_release_cmd_success(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertEqual(ok, erab_fsm:erab_release_cmd(Pid)),
     ?_assertEqual(ok, erab_fsm:erab_release_rsp(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


%% test E-RAB RELEASE.{cmd,rsp} received in state erab_wait_setup_rsp
test_erab_wait_setup_rsp_release_cmd_success(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual(ok, erab_fsm:erab_release_cmd(Pid)),
     ?_assertEqual(ok, erab_fsm:erab_release_rsp(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


%% test E-RAB RELEASE.ind received in state erab_setup
test_erab_setup_release_ind_success(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertEqual(ok, erab_fsm:erab_release_ind(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


%% test E-RAB RELEASE.ind received in state erab_wait_setup_rsp
test_erab_wait_setup_rsp_release_ind_success(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual(ok, erab_fsm:erab_release_ind(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


%% test E-RAB RELEASE.ind received in state erab_wait_release_rsp
test_erab_wait_release_rsp_release_ind_success(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertEqual(ok, erab_fsm:erab_release_cmd(Pid)),
     ?_assertEqual(ok, erab_fsm:erab_release_ind(Pid)), %% ignored
     ?_assertEqual(ok, erab_fsm:erab_release_rsp(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_release_pfcp_delete_error(Pid) ->
    %% pfcp_peer:session_delete_req/1 responds with a reject
    PDU = pfcp_mock:pdu_rsp_reject(session_deletion_response, ?SEID_Loc),
    pfcp_mock:mock_req(session_delete_req, PDU),
    unlink(Pid), %% we expect the FSM to terminate abnormally
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     %% erab_fsm:erab_release_cmd/1 is non-blocking,
     %% so we get ok before the error happens
     ?_assertEqual(ok, erab_fsm:erab_release_cmd(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


%% test E-RAB SETUP.rsp received in state erab_wait_release_rsp
test_erab_wait_release_rsp_setup_rsp(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual(ok, erab_fsm:erab_release_cmd(Pid)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertEqual(ok, erab_fsm:erab_release_rsp(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_info_state(Pid) ->
    [?_assertEqual(erab_wait_setup_req, erab_fsm_info(Pid, state)),
     ?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual(erab_wait_setup_rsp, erab_fsm_info(Pid, state)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertEqual(erab_setup, erab_fsm_info(Pid, state)),
     ?_assertEqual(ok, erab_fsm:erab_release_cmd(Pid)),
     ?_assertEqual(erab_wait_release_rsp, erab_fsm_info(Pid, state)),
     ?_assertEqual(ok, erab_fsm:erab_release_rsp(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_info_f_teid(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual(?U2C, erab_fsm_info(Pid, f_teid_u2c)),
     ?_assertEqual(?A2U, erab_fsm_info(Pid, f_teid_a2u)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertEqual(?U2A, erab_fsm_info(Pid, f_teid_u2a)),
     ?_assertEqual(?C2U, erab_fsm_info(Pid, f_teid_c2u)),
     ?_assertEqual(ok, erab_fsm:erab_release_cmd(Pid)),
     ?_assertEqual(ok, erab_fsm:erab_release_rsp(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_shutdown_no_session(Pid) ->
    %% pfcp_peer:session_delete_req/1 shall not be called
    ok = pfcp_mock:unmock_req(session_delete_req, 1),
    [?_assertEqual(ok, erab_fsm:shutdown(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].


test_erab_shutdown_session_del(Pid) ->
    [?_assertEqual({ok, ?A2U}, erab_fsm:erab_setup_req(Pid, ?U2C)),
     ?_assertEqual({ok, ?C2U}, erab_fsm:erab_setup_rsp(Pid, ?U2A)),
     ?_assertEqual(ok, erab_fsm:shutdown(Pid)),
     ?_assertNot(erlang:is_process_alive(Pid))].

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