/* OsmoS1GW (S1AP Gateway) test suite in TTCN-3
 *
 * (C) 2024 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * Author: Vadim Yanitskiy <vyanitskiy@sysmocom.de>
 *
 * All rights reserved.
 *
 * Released under the terms of GNU General Public License, Version 2 or
 * (at your option) any later version.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

module S1GW_Tests {

import from General_Types all;
import from Osmocom_Types all;
import from Native_Functions all;
import from IPL4asp_Types all;
import from Misc_Helpers all;
import from Mutex all;

import from S1AP_CodecPort all;
import from S1AP_CodecPort_CtrlFunct all;
import from S1AP_Types all;
import from S1AP_Templates all;
import from S1AP_PDU_Descriptions all;
import from S1AP_IEs all;
import from S1AP_PDU_Contents all;
import from S1AP_Constants all;

import from PFCP_Types all;
import from PFCP_Emulation all;
import from PFCP_Templates all;
import from PFCP_CodecPort all;

import from SCTP_Templates all;

import from StatsD_Types all;
import from StatsD_CodecPort all;
import from StatsD_CodecPort_CtrlFunct all;
import from StatsD_Checker all;

import from S1AP_Server all;
import from S1GW_ConnHdlr all;

modulepar {
	charstring mp_s1gw_enb_ip := "127.0.1.1";	/* eNB facing address of the S1GW */
	charstring mp_enb_bind_ip := "127.0.1.10";	/* eNB address to use locally when connecting to S1GW */
	charstring mp_s1gw_mme_ip := "127.0.2.1";	/* MME facing address of the S1GW */
	charstring mp_mme_bind_ip := "127.0.2.10";	/* MME address on which we get connections from S1GW */
	charstring mp_s1gw_upf_ip := "127.0.3.1";	/* UPF facing address of the S1GW */
	charstring mp_upf_bind_ip := "127.0.3.10";	/* UPF address on which we get connections from S1GW */

	/* Our emulated StatsD server: */
	charstring mp_local_statsd_ip := "127.0.4.10";
	integer mp_local_statsd_port := 8125;
	charstring mp_statsd_prefix := "s1gw.";

	integer mp_multi_enb_num := 42;			/* number of eNBs in _multi TCs */
}

type component test_CT extends StatsD_Checker_CT {
	timer g_Tguard;
	var MutexDispCT vc_mutex_disp;
	var S1AP_Server_CT vc_S1APSRV;
	var PFCP_Emulation_CT vc_PFCP;
	var StatsD_Checker_CT vc_STATSD;
};

private altstep as_Tguard() runs on test_CT {
	[] g_Tguard.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Tguard timeout");
	}
}

function f_init(boolean s1apsrv_start := true,
		boolean upf_start := true,
		float Tval := 20.0) runs on test_CT {
	g_Tguard.start(Tval);
	activate(as_Tguard());

	vc_mutex_disp := f_MutexDisp_start();

	f_init_statsd("StatsDSRV", vc_STATSD, mp_local_statsd_ip, mp_local_statsd_port);

	if (s1apsrv_start) {
		f_init_s1ap_srv();
	}
	if (upf_start) {
		f_init_pfcp();
		f_pfcp_assoc();
	}
}

function f_init_s1ap_srv() runs on test_CT {
	var S1APSRV_ConnParams cpars := {
		local_ip := mp_mme_bind_ip,
		local_port := 36412
	};

	vc_S1APSRV := S1AP_Server_CT.create("S1APSRV-" & testcasename()) alive;
	vc_S1APSRV.start(S1AP_Server.main(cpars));
}

function f_init_pfcp() runs on test_CT {
	var PFCP_Emulation_Cfg pfcp_cfg := {
		pfcp_bind_ip := mp_upf_bind_ip,
		pfcp_bind_port := PFCP_PORT,
		pfcp_remote_ip := mp_s1gw_upf_ip,
		pfcp_remote_port := PFCP_PORT,
		role := UPF
	};

	vc_PFCP := PFCP_Emulation_CT.create("PFCPEM-" & testcasename()) alive;
	vc_PFCP.start(PFCP_Emulation.main(pfcp_cfg));
}

/* ensure that the PFCP association is set up */
function f_pfcp_assoc() runs on test_CT {
	var verdicttype verdict;
	var ConnHdlr vc_conn;

	vc_conn := f_ConnHdlr_spawn(refers(f_ConnHdlr_pfcp_assoc_handler),
				    pars := valueof(t_ConnHdlrPars));
	vc_conn.done -> value verdict;
	if (verdict != pass) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}
}

template (value) ConnHdlrPars
t_ConnHdlrPars(integer idx := 0,
	       integer num_erabs := 1,
	       charstring statsd_prefix := mp_statsd_prefix,
	       charstring pfcp_loc_addr := mp_upf_bind_ip,
	       charstring pfcp_rem_addr := mp_s1gw_upf_ip) := {
	idx := idx,
	genb_id := ts_Global_ENB_ID(idx),
	statsd_prefix := statsd_prefix,
	pfcp_loc_addr := pfcp_loc_addr,
	pfcp_rem_addr := pfcp_rem_addr,
	mme_ue_id := 4242,
	erabs := f_gen_erab_list(idx, num_erabs)
}

private function f_gen_erab_list(integer idx, integer num_erabs)
return ERabList {
	var OCT6 seid_prefix := char2oct("TTCN-3");
	var ERabList erabs;

	for (var integer id := 0; id < num_erabs; id := id + 1) {
		var OCT2 uid := int2oct(idx, 1) & int2oct(id, 1);

		erabs[id] := {
			erab_id := id, /* sequentially assign E-RAB IDs */
			u2c := {'0001'O & uid, "127.0.0.1"},
			c2u := {'0101'O & uid, "127.0.1.1"},
			a2u := {'0202'O & uid, "127.0.2.2"},
			u2a := {'0002'O & uid, "127.0.0.2"},
			pfcp_loc_seid := seid_prefix & uid,
			pfcp_rem_seid := omit /* assigned by S1GW */
		};
	}

	return erabs;
}

function f_ConnHdlr_spawn(void_fn fn, ConnHdlrPars pars)
runs on test_CT return ConnHdlr {
	var ConnHdlr vc_conn;
	var charstring id := "ConnHdlr-" & testcasename() & "-" & int2str(pars.idx);

	vc_conn := ConnHdlr.create(id) alive;
	if (isbound(vc_STATSD) and vc_STATSD.running) {
		connect(vc_conn:STATSD_PROC, vc_STATSD:STATSD_PROC);
	}
	if (isbound(vc_S1APSRV) and vc_S1APSRV.running) {
		connect(vc_conn:S1AP_CONN, vc_S1APSRV:S1AP_CLIENT);
		connect(vc_conn:S1AP_PROC, vc_S1APSRV:S1AP_PROC);
	}
	if (isbound(vc_PFCP) and vc_PFCP.running) {
		connect(vc_conn:PFCP, vc_PFCP:CLIENT);
		connect(vc_conn:PFCP_PROC, vc_PFCP:CLIENT_PROC);
	}
	f_MutexDisp_connect(vc_mutex_disp, vc_conn);
	vc_conn.start(f_ConnHdlr_init(fn, id, pars));

	return vc_conn;
}

/* wait for all ConnHdlr in the given list to be .done() */
function f_ConnHdlrList_all_done(in ConnHdlrList vc_conns)
runs on test_CT {
	for (var integer i := 0; i < lengthof(vc_conns); i := i + 1) {
		vc_conns[i].done;
	}
}

function f_TC_setup(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_register(g_pars.genb_id);

	/* Expected values relative to snapshot: */
	var StatsDExpects statsd_exp := {
		{name := mp_statsd_prefix & "gauge.s1ap.enb.num_sctp_connections.value", mtype := "g", min := 1, max := 1},
		{name := mp_statsd_prefix & "ctr.s1ap.proxy.out_pkt.forward.unmodified.value", mtype := "c", min := 2, max := 2}
	}
	var StatsDMetrics statsd_snapshot := f_statsd_snapshot(f_statsd_keys_from_expect(statsd_exp));

	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup(g_pars.genb_id);
	f_sleep(0.5); /* keep the connection idle for some time */

	f_statsd_expect_from_snapshot(statsd_exp, wait_converge := true, snapshot := statsd_snapshot);

	f_ConnHdlr_s1ap_disconnect();

	/* Validate gauge decreases when we disconnect: */
	f_statsd_expect({{name := mp_statsd_prefix & "gauge.s1ap.enb.num_sctp_connections.value", mtype := "g", min := 0, max := 0}},
			wait_converge := true);

	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_setup() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars);
	var ConnHdlr vc_conn;

	f_init();

	vc_conn := f_ConnHdlr_spawn(refers(f_TC_setup), pars);
	vc_conn.done;
}

function f_TC_setup_multi(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_register(g_pars.genb_id);

	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup(g_pars.genb_id);
	f_sleep(0.5); /* keep the connection idle for some time */

	f_ConnHdlr_s1ap_disconnect();

	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_setup_multi() runs on test_CT {
	var ConnHdlrList vc_conns;

	f_init();

	for (var integer i := 0; i < mp_multi_enb_num; i := i + 1) {
		var ConnHdlrPars pars := valueof(t_ConnHdlrPars(i));
		vc_conns[i] := f_ConnHdlr_spawn(refers(f_TC_setup_multi), pars);
	}

	f_ConnHdlrList_all_done(vc_conns);
}


/* MME terminates connection, expect S1GW to terminate the eNB connection */
function f_TC_conn_term_by_mme(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_register(g_pars.genb_id);

	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup(g_pars.genb_id);
	f_sleep(0.5); /* keep the connection idle for some time */

	/* MME (S1AP_Server_CT) terminates connection */
	f_ConnHdlr_s1ap_close_conn(g_pars.genb_id);
	/* expect our eNB connection to be released gracefully */
	f_ConnHdlr_s1ap_expect_shutdown();

	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_conn_term_by_mme() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars);
	var ConnHdlr vc_conn;

	f_init();

	vc_conn := f_ConnHdlr_spawn(refers(f_TC_conn_term_by_mme), pars);
	vc_conn.done;
}


/* MME is not available, expect S1GW to terminate the eNB connection */
function f_TC_conn_term_mme_unavail(charstring id) runs on ConnHdlr {
	/* establish an eNB connection to the S1GW */
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	/* expect our eNB connection to be released gracefully */
	f_ConnHdlr_s1ap_expect_shutdown();
	setverdict(pass);
}
testcase TC_conn_term_mme_unavail() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars);
	var ConnHdlr vc_conn;

	f_init(s1apsrv_start := false);

	vc_conn := f_ConnHdlr_spawn(refers(f_TC_conn_term_mme_unavail), pars);
	vc_conn.done;
}

/* Test E-RAB SETUP and RELEASE.cmd procedures */
function f_TC_e_rab_setup(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_register(g_pars.genb_id);
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup(g_pars.genb_id);

	f_ConnHdlr_erab_setup_req(g_pars.erabs);
	f_ConnHdlr_erab_setup_rsp(g_pars.erabs);
	f_ConnHdlr_erab_release_cmd(g_pars.erabs);
	f_ConnHdlr_erab_release_rsp(g_pars.erabs);

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
private function f_TC_e_rab_setup_exec(integer num_enbs, integer num_erabs)
runs on test_CT {
	var ConnHdlrList vc_conns;

	f_init();

	for (var integer i := 0; i < num_enbs; i := i + 1) {
		var ConnHdlrPars pars := valueof(t_ConnHdlrPars(i, num_erabs));
		vc_conns[i] := f_ConnHdlr_spawn(refers(f_TC_e_rab_setup), pars);
	}

	f_ConnHdlrList_all_done(vc_conns);
}
/* 1 E-RAB at a time, single eNB */
testcase TC_e_rab_setup() runs on test_CT {
	f_TC_e_rab_setup_exec(num_enbs := 1, num_erabs := 1);
}
/* 1 E-RAB at a time, multiple eNB connections */
testcase TC_e_rab_setup_multi() runs on test_CT {
	f_TC_e_rab_setup_exec(num_enbs := mp_multi_enb_num, num_erabs := 1);
}
/* 3 E-RABs at a time, single eNB */
testcase TC_e_rab_setup3() runs on test_CT {
	f_TC_e_rab_setup_exec(num_enbs := 1, num_erabs := 3);
}
/* 3 E-RABs at a time, multiple eNB connections */
testcase TC_e_rab_setup3_multi() runs on test_CT {
	f_TC_e_rab_setup_exec(num_enbs := mp_multi_enb_num, num_erabs := 3);
}

/* Test E-RAB SETUP and RELEASE.ind procedures */
function f_TC_e_rab_release_ind(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_register(g_pars.genb_id);
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup(g_pars.genb_id);

	f_ConnHdlr_erab_setup_req(g_pars.erabs);
	f_ConnHdlr_erab_setup_rsp(g_pars.erabs);
	f_ConnHdlr_erab_release_ind(g_pars.erabs);

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_e_rab_release_ind() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars);
	var ConnHdlr vc_conn;

	f_init();

	vc_conn := f_ConnHdlr_spawn(refers(f_TC_e_rab_release_ind), pars);
	vc_conn.done;
}

/* Test E-RAB SETUP procedure being aborted by the S1GW due to
 * PFCP Session Establishment failure (cause=REQUEST_REJECTED). */
function f_TC_e_rab_setup_failure(charstring id) runs on ConnHdlr {
	var OCT4 addr := f_inet_addr(g_pars.pfcp_loc_addr);
	var ERab erab := g_pars.erabs[0];

	f_ConnHdlr_s1ap_register(g_pars.genb_id);
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup(g_pars.genb_id);

	/* see comments in f_ConnHdlr_erab_setup_req() */
	f_PFCPEM_subscribe_seid('0000000000000000'O);
	f_PFCPEM_subscribe_seid(erab.pfcp_loc_seid);

	log("eNB <- [S1GW <- MME]: E-RAB SETUP REQUEST");
	f_ConnHdlr_tx_erab_setup_req({erab});
	log("UPF <- S1GW: PFCP Session Establishment Request");
	var PDU_PFCP req := f_ConnHdlr_rx_session_establish_req(erab);
	/* store peer's SEID, so that it can be used in outgoing PDUs later */
	erab.pfcp_rem_seid := req.message_body.pfcp_session_establishment_request.CP_F_SEID.seid;

	log("UPF -> S1GW: PFCP Session Establishment Response (failure)");
	var template (value) PDU_PFCP resp;
	resp := ts_PFCP_Session_Est_Resp(seq_nr := req.sequence_number,
					 node_id := ts_PFCP_Node_ID_ipv4(addr),
					 seid := erab.pfcp_rem_seid,
					 f_seid := ts_PFCP_F_SEID_ipv4(addr, erab.pfcp_loc_seid),
					 created_pdr := {},
					 cause := ts_PFCP_Cause(REQUEST_REJECTED));
	PFCP.send(resp);

	/* see comments in f_ConnHdlr_erab_setup_req() */
	f_PFCPEM_unsubscribe_seid('0000000000000000'O);
	f_PFCPEM_unsubscribe_seid(erab.pfcp_loc_seid);

	/* expect E-RAB SETUP RESPONSE replied by the S1GW */
	var template (present) S1AP_PDU setup_rsp;
	var template (present) E_RABItem item;
	var S1AP_PDU s1ap_pdu;

	log("eNB -- [S1GW -> MME]: E-RAB SETUP RESPONSE (failure)");
	item := tr_E_RABItem(erab.erab_id, {transport := transport_resource_unavailable});
	setup_rsp := tr_S1AP_RABSetupRsp(g_pars.mme_ue_id, g_pars.idx,
					 rab_setup_items := omit,
					 rab_failed_items := tr_E_RABList(item));
	f_ConnHdlr_rx_s1ap_from_enb(s1ap_pdu);

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_e_rab_setup_failure() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars);
	var ConnHdlr vc_conn;

	f_init();

	vc_conn := f_ConnHdlr_spawn(refers(f_TC_e_rab_setup_failure), pars);
	vc_conn.done;
}

/* Test INITIAL CONTEXT SETUP procedure (successful case) */
function f_TC_initial_ctx_setup(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_register(g_pars.genb_id);
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup(g_pars.genb_id);

	f_ConnHdlr_initial_ctx_setup_req(g_pars.erabs);
	f_ConnHdlr_initial_ctx_setup_rsp(g_pars.erabs);

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
private function f_TC_initial_ctx_setup_exec(integer num_enbs, integer num_erabs)
runs on test_CT {
	var ConnHdlrList vc_conns;

	f_init();

	for (var integer i := 0; i < num_enbs; i := i + 1) {
		var ConnHdlrPars pars := valueof(t_ConnHdlrPars(i, num_erabs));
		vc_conns[i] := f_ConnHdlr_spawn(refers(f_TC_initial_ctx_setup), pars);
	}

	f_ConnHdlrList_all_done(vc_conns);
}
/* 1 E-RAB at a time, single eNB */
testcase TC_initial_ctx_setup() runs on test_CT {
	f_TC_initial_ctx_setup_exec(num_enbs := 1, num_erabs := 1);
}
/* 1 E-RAB at a time, multiple eNB connections */
testcase TC_initial_ctx_setup_multi() runs on test_CT {
	f_TC_initial_ctx_setup_exec(num_enbs := mp_multi_enb_num, num_erabs := 1);
}
/* 3 E-RABs at a time, single eNB */
testcase TC_initial_ctx_setup3() runs on test_CT {
	f_TC_initial_ctx_setup_exec(num_enbs := 1, num_erabs := 3);
}
/* 3 E-RABs at a time, multiple eNB connections */
testcase TC_initial_ctx_setup3_multi() runs on test_CT {
	f_TC_initial_ctx_setup_exec(num_enbs := mp_multi_enb_num, num_erabs := 3);
}

/* Test INITIAL CONTEXT SETUP procedure (failure) */
function f_TC_initial_ctx_setup_failure(charstring id) runs on ConnHdlr {
	var template (present) PDU_PFCP pfcp_pdu;
	var template (value) S1AP_PDU s1ap_pdu;
	var S1AP_PDU unused;

	f_ConnHdlr_s1ap_register(g_pars.genb_id);
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup(g_pars.genb_id);

	/* Initiate E-RAB establishment by sending INITIAL CONTEXT SETUP */
	f_ConnHdlr_initial_ctx_setup_req(g_pars.erabs);
	/* At this point, the associated PFCP session is created, but not modified yet.
	 * The E-RAB FSM in the IUT is waiting for INITIAL CONTEXT SETUP RESPONSE. */

	/* INITIAL CONTEXT SETUP FAILURE does not immediately trigger deletion of PFCP
	 * sessions.  It's forwarded as-is without any action. */
	s1ap_pdu := ts_S1AP_InitialCtxSetupFail(mme_id := g_pars.mme_ue_id,
						enb_id := g_pars.idx,
						cause := { misc := unspecified });
	f_ConnHdlr_tx_s1ap_from_enb(s1ap_pdu);
	f_ConnHdlr_rx_s1ap_from_enb(unused, s1ap_pdu);

	/* TODO: Ideally, the IUT should terminate PFCP session(s) immediately. */

	/* E-RAB FSM in the IUT terminates due to a timeout and deletes PFCP session. */
	pfcp_pdu := tr_PFCP_Session_Del_Req(g_pars.erabs[0].pfcp_loc_seid);
	f_ConnHdlr_pfcp_expect(pfcp_pdu, Tval := 8.0); /* > ERAB_T_WAIT_RELEASE_RSP */

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_initial_ctx_setup_failure() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars);
	var ConnHdlr vc_conn;

	f_init();

	vc_conn := f_ConnHdlr_spawn(refers(f_TC_initial_ctx_setup_failure), pars);
	vc_conn.done;
}

function f_TC_pfcp_heartbeat(charstring id) runs on ConnHdlr {
	var integer rts := f_PFCPEM_get_recovery_timestamp();
	var PDU_PFCP pfcp_pdu;

	/* Tx Heartbeat Request */
	PFCP.send(ts_PFCP_Heartbeat_Req(rts));

	/* Expect Heartbeat Response
	 * TODO: validate the indicated Recovery Time Stamp against
	 * the one that we received in the PFCP Association Setup. */
	pfcp_pdu := f_ConnHdlr_pfcp_expect(tr_PFCP_Heartbeat_Resp);
	setverdict(pass);
}
testcase TC_pfcp_heartbeat() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars);
	var ConnHdlr vc_conn;

	f_init();

	vc_conn := f_ConnHdlr_spawn(refers(f_TC_pfcp_heartbeat), pars);
	vc_conn.done;
}

control {
	execute( TC_setup() );
	execute( TC_setup_multi() );
	execute( TC_conn_term_by_mme() );
	execute( TC_conn_term_mme_unavail() );
	execute( TC_e_rab_setup() );
	execute( TC_e_rab_setup3() );
	execute( TC_e_rab_setup_multi() );
	execute( TC_e_rab_setup3_multi() );
	execute( TC_e_rab_release_ind() );
	execute( TC_e_rab_setup_failure() );
	execute( TC_initial_ctx_setup() );
	execute( TC_initial_ctx_setup3() );
	execute( TC_initial_ctx_setup_multi() );
	execute( TC_initial_ctx_setup3_multi() );
	execute( TC_initial_ctx_setup_failure() );
	execute( TC_pfcp_heartbeat() );
}

}