/* OsmoS1GW (S1AP Gateway) test suite in TTCN-3
 *
 * (C) 2024-2026 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 HTTP_Adapter all;
import from HTTPmsg_Types all;

import from S1AP_Server all;
import from S1GW_ConnHdlr all;
import from S1GW_UEMux all;

import from S1GW_REST_Types all;
import from S1GW_REST_Functions 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_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 */
	integer mp_multi_ue_num := 128;			/* number of UEs in _multi TCs */

	charstring mp_rest_host := "127.0.0.1";
	integer mp_rest_port := 8080;
}

type component test_CT extends StatsD_Checker_CT, http_CT {
	timer g_Tguard;
	var MutexDispCT vc_mutex_disp;
	var S1AP_ServerList vc_S1APSRVs := {};
	var PFCP_Emulation_CT vc_PFCP;
	var StatsD_Checker_CT vc_STATSD;
	var PfcpAssocInfo g_pfcp_assoc;
};

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,
		integer num_mmes := 1,
		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(num_mmes);
	}
	f_init_rest();
	if (upf_start) {
		f_init_pfcp();
		f_pfcp_assoc();
	}
}

/* compute the IP address for pool MME server [idx]: mp_mme_bind_ip + idx */
private function f_mme_ip(integer idx) return charstring {
	return f_inet_ntoa(int2oct(oct2int(f_inet_addr(mp_mme_bind_ip)) + idx, 4));
}

function f_init_s1ap_srv(integer num_mmes := 1) runs on test_CT {
	for (var integer i := 0; i < num_mmes; i := i + 1) {
		var S1APSRV_ConnParams cpars := {
			local_ip := f_mme_ip(i),
			local_port := 36412
		};
		vc_S1APSRVs[i] := S1AP_Server_CT.create("S1APSRV-" & int2str(i) & "-" & testcasename()) alive;
		vc_S1APSRVs[i].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,
		answer_heartbeat_req := true
	};

	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;

	g_pfcp_assoc := f_REST_PfcpAssocState();
	if (g_pfcp_assoc.state != connected) {
		log("Waiting for IUT to associate over PFCP");
		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__);
		}
		g_pfcp_assoc := f_REST_PfcpAssocState();
	}
}

function f_init_rest() runs on test_CT {
	f_http_init(valueof(t_HTTP_Adapter_Params));
}

template (value) HTTP_Adapter_Params t_HTTP_Adapter_Params := {
	http_host := mp_rest_host,
	http_port := mp_rest_port,
	use_ssl := false
}

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),
	pool_srvs := omit,
	pool_behaviors := omit
}

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"},
			u2cm := {'0011'O & uid, "127.127.0.1"},
			u2am := {'0022'O & uid, "127.127.0.2"},
			pfcp_loc_seid := seid_prefix & uid,
			pfcp_rem_seid := omit /* assigned by S1GW */
		};
	}

	return erabs;
}

function f_ConnHdlr_connect(ConnHdlr vc_conn)
runs on test_CT {
	if (isbound(vc_STATSD) and vc_STATSD.running) {
		connect(vc_conn:STATSD_PROC, vc_STATSD:STATSD_PROC);
	}
	for (var integer i := 0; i < lengthof(vc_S1APSRVs); i := i + 1) {
		if (vc_S1APSRVs[i].running) {
			connect(vc_conn:S1AP_CONN, vc_S1APSRVs[i]:S1AP_CLIENT);
			connect(vc_conn:S1AP_PROC, vc_S1APSRVs[i]: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);
}

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;
	f_ConnHdlr_connect(vc_conn);

	/* Init the HTTP client (M2M/REST interface) */
	vc_conn.start(f_http_init(valueof(t_HTTP_Adapter_Params)));
	vc_conn.done;

	/* run the actual testcase logic */
	vc_conn.start(f_ConnHdlr_init(fn, id, pars));

	return vc_conn;
}

function f_UEMux_spawn(void_fn2 fn, ConnHdlrPars pars)
runs on test_CT return UEMux_CT {
	var UEMux_CT vc_conn;
	var charstring id := "UEMux-" & testcasename() & "-" & int2str(pars.idx);

	vc_conn := UEMux_CT.create(id) alive;
	f_ConnHdlr_connect(vc_conn);
	vc_conn.start(f_UEMux_init(fn, id, pars));

	return vc_conn;
}

function f_TC_UEMux_main(charstring id) runs on UEMux_CT {
	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);

	S1GW_UEMux.main();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}

function f_UEMuxUE_create(in UEMux_CT vc_uemux,
			  in ConnHdlrPars pars,
			  MME_UE_S1AP_ID mme_ue_id,
			  ENB_UE_S1AP_ID enb_ue_id)
runs on test_CT return UEMuxUE {
	var charstring id := "UEMuxUE-" & testcasename();
	var UEMuxUE vc_ue;

	vc_ue := UEMuxUE.create(id) alive;
	connect(vc_ue:UEMUX_CONN, vc_uemux:UEMUX_CONN);
	connect(vc_ue:UEMUX_PROC, vc_uemux:UEMUX_PROC);

	/* pre-initialize the component */
	vc_ue.start(f_UEMuxUE_init(pars, mme_ue_id, enb_ue_id));
	vc_ue.done;

	return vc_ue;
}

function f_UEMuxUE_init(in ConnHdlrPars pars,
			MME_UE_S1AP_ID mme_ue_id,
			ENB_UE_S1AP_ID enb_ue_id)
runs on UEMuxUE {
	g_mme_ue_id := mme_ue_id;
	g_enb_ue_id := enb_ue_id;

	g_ies.tai := {
		pLMNidentity := pars.genb_id.pLMNidentity,
		tAC := int2oct(12345, 2), /* XXX: hard-coded */
		iE_Extensions := omit
	};
	g_ies.eutran_cgi := {
		pLMNidentity := pars.genb_id.pLMNidentity,
		cell_ID := int2bit(pars.idx, 28),
		iE_Extensions := omit
	};
}

/* 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;
	}
}

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

function f_TC_exec(void_fn fn,
		   integer num_enbs := 1,
		   integer num_erabs := 1)
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(fn, pars);
	}

	f_ConnHdlrList_all_done(vc_conns);
}

/* execute a single-eNB pool test: spins up num_mmes S1AP servers, builds ConnHdlrPars
 * with pool_srvs/pool_behaviors set, and runs fn inside a ConnHdlr */
function f_TC_exec_pool(void_fn fn,
			integer num_mmes,
			S1APSRV_SetupBehaviorList behaviors,
			float Tval := 60.0)
runs on test_CT {
	var ConnHdlrPars pars;
	var ConnHdlr vc_conn;

	f_init(num_mmes := num_mmes, Tval := Tval);

	pars := valueof(t_ConnHdlrPars);
	pars.pool_srvs := vc_S1APSRVs;
	pars.pool_behaviors := behaviors;

	vc_conn := f_ConnHdlr_spawn(fn, pars);
	vc_conn.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 {
	f_TC_exec(refers(f_TC_setup));
}

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 {
	f_TC_exec(refers(f_TC_setup_multi), mp_multi_enb_num);
}


/* 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 */

	/* stop tracking eNB connection (teardown expected) */
	f_ConnHdlr_conn_track_disable();

	/* 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_();
	f_ConnHdlr_erab_setup_rsp_();
	f_ConnHdlr_erab_release_cmd_();
	f_ConnHdlr_erab_release_rsp_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
/* 1 E-RAB at a time, single eNB */
testcase TC_e_rab_setup() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_setup));
}
/* 1 E-RAB at a time, multiple eNB connections */
testcase TC_e_rab_setup_multi() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_setup), mp_multi_enb_num);
}
/* 3 E-RABs at a time, single eNB */
testcase TC_e_rab_setup3() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_setup), 1, 3);
}
/* 3 E-RABs at a time, multiple eNB connections */
testcase TC_e_rab_setup3_multi() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_setup), mp_multi_enb_num, 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_();
	f_ConnHdlr_erab_setup_rsp_();
	f_ConnHdlr_erab_release_ind_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_e_rab_release_ind() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_release_ind));
}

/* Test E-RAB SETUP failure: all E-RABs in the request fail because the first
 * PFCP Session Establishment is rejected.  The S1GW processes E-RABs sequentially,
 * so only E-RAB 0 triggers a PFCP Session Establishment Request; the remaining
 * E-RABs (if any) are abandoned.  The S1GW must list all E-RABs as failed. */
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 erab0 := 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(erab0.pfcp_loc_seid);

	log("eNB <- [S1GW <- MME]: E-RAB SETUP REQUEST");
	f_ConnHdlr_tx_erab_setup_req(g_pars.erabs, g_pars.mme_ue_id, g_pars.idx);
	log("UPF <- S1GW: PFCP Session Establishment Request (E-RAB 0)");
	var PDU_PFCP req := f_ConnHdlr_rx_session_establish_req(erab0);
	erab0.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 := erab0.pfcp_rem_seid,
					 f_seid := ts_PFCP_F_SEID_ipv4(addr, erab0.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(erab0.pfcp_loc_seid);

	log("eNB -- [S1GW -> MME]: E-RAB SETUP RESPONSE (all E-RABs failed)");
	var template (present) E_RABList failed_items;
	var template (present) S1AP_PDU setup_rsp;
	var S1AP_PDU s1ap_pdu;

	failed_items := f_tr_E_RABList(g_pars.erabs,
				       cause := {transport := transport_resource_unavailable});
	setup_rsp := tr_S1AP_RABSetupRsp(g_pars.mme_ue_id, g_pars.idx,
					 rab_setup_items := omit,
					 rab_failed_items := failed_items);
	f_ConnHdlr_rx_s1ap_from_enb(s1ap_pdu, setup_rsp);

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_e_rab_setup_failure() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_setup_failure), num_erabs := 1);
}
testcase TC_e_rab_setup3_failure() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_setup_failure), num_erabs := 3);
}

/* Test E-RAB MODIFY Req/Rsp procedure */
function f_TC_e_rab_modify_req_rsp(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_();
	f_ConnHdlr_erab_setup_rsp_();

	/* MME orders eNB to modify all 4 E-RABs */
	f_ConnHdlr_erab_modify_req(g_pars.erabs);
	/* eNB modifies E-RABs 1 and 3, but not E-RAB 0 and 2 */
	f_ConnHdlr_erab_modify_rsp(erabs_modified := {1, 3},
				   erabs_failed := {0, 2});

	f_ConnHdlr_erab_release_cmd_();
	f_ConnHdlr_erab_release_rsp_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
/* 4 E-RABs at a time, single eNB */
testcase TC_e_rab_modify_req_rsp() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_modify_req_rsp), 1, 4);
}
/* 4 E-RABs at a time, multiple eNB connections */
testcase TC_e_rab_modify_req_rsp_multi() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_modify_req_rsp), mp_multi_enb_num, 4);
}

/* Test E-RAB MODIFICATION Ind/Cnf procedure */
function f_TC_e_rab_modify_ind_cnf(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_();
	f_ConnHdlr_erab_setup_rsp_();

	/* eNB wants to modify all E-RABs but E-RAB 4 */
	f_ConnHdlr_erab_modify_ind(erabs_modified := {0, 1, 2, 3},
				   erabs_not_modified := {4});
	/* MME modifies E-RABs 1 and 3, but not E-RAB 0 and 2 */
	f_ConnHdlr_erab_modify_cnf(erabs_modified := {1, 3},
				   erabs_not_modified := {0, 2});

	/* For the sake of fun, test forwarding of an empty confirmation,
	 * which is expected to be forwarded unmodified */
	f_ConnHdlr_erab_modify_cnf();

	f_ConnHdlr_erab_release_cmd_();
	f_ConnHdlr_erab_release_rsp_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
/* 5 E-RABs at a time, single eNB */
testcase TC_e_rab_modify_ind_cnf() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_modify_ind_cnf), 1, 5);
}
/* 5 E-RABs at a time, multiple eNB connections */
testcase TC_e_rab_modify_ind_cnf_multi() runs on test_CT {
	f_TC_exec(refers(f_TC_e_rab_modify_ind_cnf), mp_multi_enb_num, 5);
}

/* 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_();
	f_ConnHdlr_initial_ctx_setup_rsp_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
/* 1 E-RAB at a time, single eNB */
testcase TC_initial_ctx_setup() runs on test_CT {
	f_TC_exec(refers(f_TC_initial_ctx_setup));
}
/* 1 E-RAB at a time, multiple eNB connections */
testcase TC_initial_ctx_setup_multi() runs on test_CT {
	f_TC_exec(refers(f_TC_initial_ctx_setup), mp_multi_enb_num);
}
/* 3 E-RABs at a time, single eNB */
testcase TC_initial_ctx_setup3() runs on test_CT {
	f_TC_exec(refers(f_TC_initial_ctx_setup), 1, 3);
}
/* 3 E-RABs at a time, multiple eNB connections */
testcase TC_initial_ctx_setup3_multi() runs on test_CT {
	f_TC_exec(refers(f_TC_initial_ctx_setup), mp_multi_enb_num, 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_();
	/* 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 {
	f_TC_exec(refers(f_TC_initial_ctx_setup_failure));
}

/* Test UE CONTEXT RELEASE REQUEST procedure (eNB initiated) */
function f_TC_ue_ctx_release_req(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_();
	f_ConnHdlr_initial_ctx_setup_rsp_();
	f_ConnHdlr_ue_ctx_release_req_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_ue_ctx_release_req() runs on test_CT {
	f_TC_exec(refers(f_TC_ue_ctx_release_req), num_erabs := 1);
}
testcase TC_ue_ctx_release_req3() runs on test_CT {
	f_TC_exec(refers(f_TC_ue_ctx_release_req), num_erabs := 3);
}

/* Test UE CONTEXT RELEASE COMMAND/COMPLETE procedure (MME initiated) */
function f_TC_ue_ctx_release_cmd_compl(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_();
	f_ConnHdlr_initial_ctx_setup_rsp_();

	f_ConnHdlr_ue_ctx_release_cmd_();
	f_ConnHdlr_ue_ctx_release_compl_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_ue_ctx_release_cmd_compl() runs on test_CT {
	f_TC_exec(refers(f_TC_ue_ctx_release_cmd_compl), num_erabs := 1);
}
testcase TC_ue_ctx_release_cmd_compl3() runs on test_CT {
	f_TC_exec(refers(f_TC_ue_ctx_release_cmd_compl), num_erabs := 3);
}

/* Test multiple UEs concurrently doing the following:
 *   eNB -> MME: INITIAL UE MESSAGE
 *   eNB <- MME: DOWNLINK NAS TRANSPORT
 *   eNB -> MME: UPLINK NAS TRANSPORT
 *   eNB <- MME: DOWNLINK NAS TRANSPORT
 *   eNB <- MME: UE CONTEXT RELEASE COMMAND
 *   eNB -> MME: UE CONTEXT RELEASE COMPLETE
 * All S1AP PDUs are expected to be forwarded as-is. */
function f_TC_uemux_uldl_nas_release()
runs on UEMuxUE {
	var template (value) S1AP_PDU pdu;
	var float Tval := 5.0;

	f_UEMuxUE_subscribe_for_mme_ue_id(g_mme_ue_id);
	f_UEMuxUE_subscribe_for_enb_ue_id(g_enb_ue_id);

	/* eNB -> MME: INITIAL UE MESSAGE */
	pdu := ts_S1AP_InitialUE(g_enb_ue_id, ''O, g_ies.tai, g_ies.eutran_cgi, mo_Signalling);
	f_UEMuxUE_trx_s1ap_from_enb(pdu, pdu, Tval);
	/* eNB <- MME: DOWNLINK NAS TRANSPORT */
	pdu := ts_S1AP_DlNasTransport(g_mme_ue_id, g_enb_ue_id, ''O);
	f_UEMuxUE_trx_s1ap_from_mme(pdu, pdu, Tval);
	/* eNB -> MME: UPLINK NAS TRANSPORT */
	pdu := ts_S1AP_UlNasTransport(g_mme_ue_id, g_enb_ue_id, ''O, g_ies.eutran_cgi, g_ies.tai);
	f_UEMuxUE_trx_s1ap_from_enb(pdu, pdu, Tval);
	/* eNB <- MME: DOWNLINK NAS TRANSPORT */
	pdu := ts_S1AP_DlNasTransport(g_mme_ue_id, g_enb_ue_id, ''O);
	f_UEMuxUE_trx_s1ap_from_mme(pdu, pdu, Tval);

	var UE_S1AP_IDs ue_ids := {
		uE_S1AP_ID_pair := {
			mME_UE_S1AP_ID := g_mme_ue_id,
			eNB_UE_S1AP_ID := g_enb_ue_id,
			iE_Extensions := omit
		}
	};

	/* eNB <- MME: UE CONTEXT RELEASE COMMAND */
	pdu := ts_S1AP_UeContextReleaseCmd(mme_ids := ue_ids,
					   cause := {nas := normal_release});
	f_UEMuxUE_trx_s1ap_from_mme(pdu, pdu, Tval);
	/* eNB -> MME: UE CONTEXT RELEASE COMPLETE */
	pdu := ts_S1AP_UeContextReleaseCompl(g_mme_ue_id, g_enb_ue_id);
	f_UEMuxUE_trx_s1ap_from_enb(pdu, pdu, Tval);

	f_UEMuxUE_unsubscribe();
}
testcase TC_uemux_uldl_nas_release() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars);
	var UEMuxUEList vc_ues;
	var UEMux_CT vc_uemux;

	f_init();

	vc_uemux := f_UEMux_spawn(refers(f_TC_UEMux_main), pars);

	for (var integer i := 0; i < mp_multi_ue_num; i := i + 1) {
		vc_ues[i] := f_UEMuxUE_create(vc_uemux, pars, i, i);
		vc_ues[i].start(f_TC_uemux_uldl_nas_release());
	}

	f_UEMuxUEList_all_done(vc_ues);
}

function f_TC_uemux_e_rab_setup_release(in ERabList erabs,
					boolean release_cmd := false,
					boolean release_ind := false)
runs on UEMuxUE {
	f_UEMuxUE_subscribe_for_mme_ue_id(g_mme_ue_id);
	f_UEMuxUE_subscribe_for_enb_ue_id(g_enb_ue_id);

	f_rnd_sleep(0.5);
	f_UEMux_erab_setup(erabs);

	if (release_cmd) {
		f_rnd_sleep(0.5);
		f_UEMux_erab_release_cmd(erabs);
	}
	if (release_ind) {
		f_rnd_sleep(0.5);
		f_UEMux_erab_release_ind(erabs);
	}

	f_UEMuxUE_unsubscribe();
}
testcase TC_uemux_e_rab_setup() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars(num_erabs := mp_multi_ue_num));
	var UEMuxUEList vc_ues;
	var UEMux_CT vc_uemux;

	f_init();

	vc_uemux := f_UEMux_spawn(refers(f_TC_UEMux_main), pars);

	for (var integer i := 0; i < mp_multi_ue_num; i := i + 1) {
		var ERabList erabs := { pars.erabs[i] };

		vc_ues[i] := f_UEMuxUE_create(vc_uemux, pars, i, i);
		vc_ues[i].start(f_TC_uemux_e_rab_setup_release(erabs));
	}

	f_UEMuxUEList_all_done(vc_ues);
}
testcase TC_uemux_e_rab_release_cmd() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars(num_erabs := mp_multi_ue_num));
	var UEMuxUEList vc_ues;
	var UEMux_CT vc_uemux;

	f_init();

	vc_uemux := f_UEMux_spawn(refers(f_TC_UEMux_main), pars);

	for (var integer i := 0; i < mp_multi_ue_num; i := i + 1) {
		var ERabList erabs := { pars.erabs[i] };

		vc_ues[i] := f_UEMuxUE_create(vc_uemux, pars, i, i);
		vc_ues[i].start(f_TC_uemux_e_rab_setup_release(erabs, release_cmd := true));
	}

	f_UEMuxUEList_all_done(vc_ues);
}
testcase TC_uemux_e_rab_release_ind() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars(num_erabs := mp_multi_ue_num));
	var UEMuxUEList vc_ues;
	var UEMux_CT vc_uemux;

	f_init();

	vc_uemux := f_UEMux_spawn(refers(f_TC_UEMux_main), pars);

	for (var integer i := 0; i < mp_multi_ue_num; i := i + 1) {
		var ERabList erabs := { pars.erabs[i] };

		vc_ues[i] := f_UEMuxUE_create(vc_uemux, pars, i, i);
		vc_ues[i].start(f_TC_uemux_e_rab_setup_release(erabs, release_ind := true));
	}

	f_UEMuxUEList_all_done(vc_ues);
}

function f_TC_uemux_initial_ctx_setup_release(in ERabList erabs,
					      boolean release_req := false,
					      boolean release_cmd := false)
runs on UEMuxUE {
	f_UEMuxUE_subscribe_for_mme_ue_id(g_mme_ue_id);
	f_UEMuxUE_subscribe_for_enb_ue_id(g_enb_ue_id);

	f_rnd_sleep(0.5);
	f_UEMux_initial_ctx_setup(erabs);

	if (release_req) {
		f_rnd_sleep(0.5);
		f_UEMux_ue_ctx_release_req(erabs);
	}
	if (release_cmd) {
		f_rnd_sleep(0.5);
		f_UEMux_ue_ctx_release_cmd(erabs);
	}

	f_UEMuxUE_unsubscribe();
}
testcase TC_uemux_initial_ctx_setup() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars(num_erabs := mp_multi_ue_num));
	var UEMuxUEList vc_ues;
	var UEMux_CT vc_uemux;

	f_init();

	vc_uemux := f_UEMux_spawn(refers(f_TC_UEMux_main), pars);

	for (var integer i := 0; i < mp_multi_ue_num; i := i + 1) {
		var ERabList erabs := { pars.erabs[i] };

		vc_ues[i] := f_UEMuxUE_create(vc_uemux, pars, i, i);
		vc_ues[i].start(f_TC_uemux_initial_ctx_setup_release(erabs));
	}

	f_UEMuxUEList_all_done(vc_ues);
}
testcase TC_uemux_ue_ctx_release_req() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars(num_erabs := mp_multi_ue_num));
	var UEMuxUEList vc_ues;
	var UEMux_CT vc_uemux;

	f_init();

	vc_uemux := f_UEMux_spawn(refers(f_TC_UEMux_main), pars);

	for (var integer i := 0; i < mp_multi_ue_num; i := i + 1) {
		var ERabList erabs := { pars.erabs[i] };

		vc_ues[i] := f_UEMuxUE_create(vc_uemux, pars, i, i);
		vc_ues[i].start(f_TC_uemux_initial_ctx_setup_release(erabs, release_req := true));
	}

	f_UEMuxUEList_all_done(vc_ues);
}
testcase TC_uemux_ue_ctx_release_cmd() runs on test_CT {
	var ConnHdlrPars pars := valueof(t_ConnHdlrPars(num_erabs := mp_multi_ue_num));
	var UEMuxUEList vc_ues;
	var UEMux_CT vc_uemux;

	f_init();

	vc_uemux := f_UEMux_spawn(refers(f_TC_UEMux_main), pars);

	for (var integer i := 0; i < mp_multi_ue_num; i := i + 1) {
		var ERabList erabs := { pars.erabs[i] };

		vc_ues[i] := f_UEMuxUE_create(vc_uemux, pars, i, i);
		vc_ues[i].start(f_TC_uemux_initial_ctx_setup_release(erabs, release_cmd := true));
	}

	f_UEMuxUEList_all_done(vc_ues);
}

/* Test Handover Preparation procedures (no change in E-RAB) */
function f_TC_handover_preparation(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_();
	f_ConnHdlr_erab_setup_rsp_();
	f_ConnHdlr_handover_rqd_();
	f_ConnHdlr_handover_cmd_();
	f_ConnHdlr_erab_release_cmd_();
	f_ConnHdlr_erab_release_rsp_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_handover_preparation() runs on test_CT {
	f_TC_exec(refers(f_TC_handover_preparation));
}

/* Test Handover Preparation procedures with release */
function f_TC_handover_preparation_release(charstring id) runs on ConnHdlr {
	var ERabIdxList erabs_forward := {0, 2};
	var ERabIdxList erabs_release := {1, 3};
	var ERabList erabs_left;

	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_();
	f_ConnHdlr_erab_setup_rsp_();
	f_ConnHdlr_handover_rqd_();
	f_ConnHdlr_handover_cmd(erabs_forward,
				erabs_release,
				g_pars.mme_ue_id,
				g_pars.idx);

	if (lengthof(erabs_forward) > 0) {
		erabs_left := f_ERabList_compose(erabs_forward);
		f_ConnHdlr_erab_release_cmd(erabs_left, g_pars.mme_ue_id, g_pars.idx);
		f_ConnHdlr_erab_release_rsp(erabs_left, g_pars.mme_ue_id, g_pars.idx);
	}

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_handover_preparation_release() runs on test_CT {
	f_TC_exec(refers(f_TC_handover_preparation_release), 1, 4);
}

/* Test Handover Resource Allocation procedures */
function f_TC_handover_res_alloc(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_handover_req_();
	f_ConnHdlr_handover_ack_();
	f_ConnHdlr_erab_release_cmd_();
	f_ConnHdlr_erab_release_rsp_();

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
testcase TC_handover_res_alloc() runs on test_CT {
	f_TC_exec(refers(f_TC_handover_res_alloc));
}

/* Test Handover Resource Allocation procedures with falures */
function f_TC_handover_res_alloc_fail(charstring id) runs on ConnHdlr {
	var ERabIdxList erabs_admitted := {0, 2, 4};
	var ERabIdxList erabs_failed := {1, 3, 5};
	var ERabList erabs_left;

	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_handover_req_();
	f_ConnHdlr_handover_ack(erabs_admitted,
				erabs_failed,
				g_pars.mme_ue_id,
				g_pars.idx);

	if (lengthof(erabs_admitted) > 0) {
		erabs_left := f_ERabList_compose(erabs_admitted);
		f_ConnHdlr_erab_release_cmd(erabs_left, g_pars.mme_ue_id, g_pars.idx);
		f_ConnHdlr_erab_release_rsp(erabs_left, g_pars.mme_ue_id, g_pars.idx);
	}

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister(g_pars.genb_id);
}
/* 6 E-RABs at a time, single eNB */
testcase TC_handover_res_alloc_fail() runs on test_CT {
	f_TC_exec(refers(f_TC_handover_res_alloc_fail), 1, 6);
}

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 {
	f_TC_exec(refers(f_TC_pfcp_heartbeat));
}

/* MME pool test: eNB connects, the first MME rejects, the second one accepts */
function f_TC_mme_pool_reject_fallback(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup_pool(g_pars.pool_srvs, g_pars.pool_behaviors);

	f_sleep(0.5); /* keep the connection idle for some time */

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister_from(g_pars.genb_id, g_s1ap_server);
}
testcase TC_mme_pool_reject_fallback() runs on test_CT {
	f_TC_exec_pool(refers(f_TC_mme_pool_reject_fallback), 2,
		       { S1APSRV_SETUP_REJECT, S1APSRV_SETUP_ACCEPT });
}

/* MME pool test: eNB connects, the first MME times out, the second one accepts */
function f_TC_mme_pool_timeout_fallback(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup_pool(g_pars.pool_srvs, g_pars.pool_behaviors);

	f_sleep(0.5); /* keep the connection idle for some time */

	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister_from(g_pars.genb_id, g_s1ap_server);
}
testcase TC_mme_pool_timeout_fallback() runs on test_CT {
	f_TC_exec_pool(refers(f_TC_mme_pool_timeout_fallback), 2,
		       { S1APSRV_SETUP_TIMEOUT, S1APSRV_SETUP_ACCEPT });
}

/* MME pool test: eNB connects, all MMEs reject, S1GW sends S1SetupFailure to eNB */
function f_TC_mme_pool_all_reject(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);

	/* conn_track would fire on CONN_DOWN events during the reject loop */
	f_ConnHdlr_conn_track_disable();

	/* pre-register with all pool servers */
	for (var integer i := 0; i < lengthof(g_pars.pool_srvs); i := i + 1) {
		f_ConnHdlr_s1ap_register_to(g_pars.genb_id, g_pars.pool_srvs[i]);
	}

	/* send S1SetupReq once from the eNB; S1GW will forward it to each MME it tries */
	f_ConnHdlr_tx_s1ap_from_enb(ts_S1AP_SetupReq(g_pars.genb_id, c_SupportedTAs, v32));

	/* S1GW will try each MME and get rejected */
	for (var integer i := 0; i < lengthof(g_pars.pool_behaviors); i := i + 1) {
		var S1AP_Server_CT vc_srv := g_pars.pool_srvs[i];
		var S1AP_PDU pdu;
		timer T;

		g_s1ap_server := vc_srv;
		T.start(10.0);
		alt {
		[] S1AP_CONN.receive(S1APSRV_Event:S1APSRV_EVENT_CONN_UP) from vc_srv { repeat; }
		[] S1AP_CONN.receive(tr_S1AP_SetupReq) from vc_srv -> value pdu { T.stop; }
		[] T.timeout {
			setverdict(fail, "Timeout waiting for S1SetupReq on pool server ", i);
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
			}
		}
		f_ConnHdlr_tx_s1ap_from_mme(ts_S1AP_SetupFail({ radioNetwork := unspecified }));

		T.start(10.0);
		alt {
		[] S1AP_CONN.receive(S1APSRV_Event:S1APSRV_EVENT_CONN_DOWN) from vc_srv { T.stop; }
		[] T.timeout {
			setverdict(fail, "Timeout waiting for CONN_DOWN from pool server ", i);
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
			}
		}
		f_ConnHdlr_s1ap_unregister_from(g_pars.genb_id, vc_srv);
	}

	/* S1GW has exhausted all MMEs: it sends S1SetupFailure to eNB and then closes */
	var S1AP_PDU pdu;
	f_ConnHdlr_rx_s1ap_from_mme(pdu, tr_S1AP_SetupFail(?));
	f_ConnHdlr_s1ap_expect_shutdown();
	setverdict(pass);
}
testcase TC_mme_pool_all_reject() runs on test_CT {
	f_TC_exec_pool(refers(f_TC_mme_pool_all_reject), 3,
		       { S1APSRV_SETUP_REJECT, S1APSRV_SETUP_REJECT, S1APSRV_SETUP_REJECT });
}

/* MME pool test: eNB connects but disconnects before sending S1SetupReq
 * (S1GW is in wait_s1setup_req); S1GW must handle the disconnect cleanly
 * and must not attempt to connect to any MME. */
function f_TC_mme_pool_enb_disc_wait_s1setup_req(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);

	/* disconnect before S1SetupReq: S1GW is in wait_s1setup_req,
	 * no MME connection is ever attempted */
	f_ConnHdlr_s1ap_close();
	setverdict(pass);
}
testcase TC_mme_pool_enb_disc_wait_s1setup_req() runs on test_CT {
	f_TC_exec_pool(refers(f_TC_mme_pool_enb_disc_wait_s1setup_req), 1,
		       { S1APSRV_SETUP_ACCEPT });
}

/* MME pool test: eNB connects, sends S1SetupReq, S1GW forwards it to an MME
 * (S1GW is in wait_s1setup_rsp), then eNB disconnects before the response arrives.
 * S1GW must detect the eNB disconnect and close the MME connection. */
function f_TC_mme_pool_enb_disc_wait_s1setup_rsp(charstring id) runs on ConnHdlr {
	var S1AP_Server_CT vc_srv := g_pars.pool_srvs[0];
	var S1AP_PDU pdu;
	timer T;

	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_conn_track_disable();

	/* pre-register with the pool server, then trigger MME selection */
	f_ConnHdlr_s1ap_register_to(g_pars.genb_id, vc_srv);
	f_ConnHdlr_tx_s1ap_from_enb(ts_S1AP_SetupReq(g_pars.genb_id, c_SupportedTAs, v32));

	/* wait until S1GW has forwarded SetupReq to the MME (now in wait_s1setup_rsp) */
	g_s1ap_server := vc_srv;
	T.start(10.0);
	alt {
	[] S1AP_CONN.receive(S1APSRV_Event:S1APSRV_EVENT_CONN_UP) from vc_srv { repeat; }
	[] S1AP_CONN.receive(tr_S1AP_SetupReq) from vc_srv -> value pdu { T.stop; }
	[] T.timeout {
		setverdict(fail, "Timeout waiting for S1SetupReq on pool server");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
		}
	}

	/* eNB disconnects impatient; S1GW must close the MME connection in response */
	f_ConnHdlr_s1ap_close();

	T.start(10.0);
	alt {
	[] S1AP_CONN.receive(S1APSRV_Event:S1APSRV_EVENT_CONN_DOWN) from vc_srv {
		T.stop;
		setverdict(pass);
		}
	[] T.timeout {
		setverdict(fail, "S1GW did not close MME connection after eNB disconnect");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
		}
	}

	f_ConnHdlr_s1ap_unregister_from(g_pars.genb_id, vc_srv);
}
testcase TC_mme_pool_enb_disc_wait_s1setup_rsp() runs on test_CT {
	f_TC_exec_pool(refers(f_TC_mme_pool_enb_disc_wait_s1setup_rsp), 1,
		       { S1APSRV_SETUP_ACCEPT });
}


/* Find an MME entry in the list by name; return its index, or -1 if not found */
private function f_REST_mme_find(MmeList mmes, charstring name) return integer {
	for (var integer i := 0; i < lengthof(mmes); i := i + 1) {
		if (mmes[i].name == name) {
			return i;
		}
	}
	return -1;
}

/* Restore the MME pool to its original state: delete all current entries and
 * re-add the three static pool entries (mme0/mme1/mme2) in their original order. */
private function f_REST_mme_pool_restore() runs on test_CT {
	var MmeList mmes := f_REST_MmeList();
	for (var integer i := 0; i < lengthof(mmes); i := i + 1) {
		f_REST_MmeDelete({ name := "name:" & mmes[i].name });
	}
	for (var integer i := 0; i < 3; i := i + 1) {
		f_REST_MmeAdd(valueof(ts_MmeItem(name := "mme" & int2str(i),
						 raddr := f_mme_ip(i),
						 laddr := "127.0.2.1")));
	}
}

/* REST test: verify the configured MME pool is correctly reflected via REST */
testcase TC_rest_mme_list() runs on test_CT {
	var template (present) MmeList tr_mmes;
	var MmeList mmes;

	f_init(s1apsrv_start := false, upf_start := false);

	/* this resembles the 'mme_pool' section in osmo-s1gw.config */
	for (var integer i := 0; i < 3; i := i + 1) {
		tr_mmes[i] := tr_MmeItem(name := "mme" & int2str(i),
					 raddr := f_mme_ip(i),
					 laddr := "127.0.2.1",
					 rport := 36412,
					 tac_list := { });
	}

	mmes := f_REST_MmeList();
	if (not match(mmes, tr_mmes)) {
		setverdict(fail, "Got unexpected MmeList: ", mmes);
	}

	setverdict(pass);
}

/* REST test: add an MME to the pool, verify it appears, then delete it */
testcase TC_rest_mme_add_del() runs on test_CT {
	var MmeList mmes;
	var MmeItem mme;
	var integer idx;

	var template (value) MmeItem ts_mme_test := ts_MmeItem(name := "mme-test",
							       raddr := f_mme_ip(9));
	var template (present) MmeItem tr_mme_test := tr_MmeItem(name := "mme-test",
								 raddr := f_mme_ip(9),
								 laddr := "any",
								 rport := 36412,
								 tac_list := { });

	f_init(s1apsrv_start := false, upf_start := false);

	/* add the new entry and verify it appears in the list */
	if (not f_REST_MmeAdd(valueof(ts_mme_test))) {
		setverdict(fail, "Failed to add mme-test via REST");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}
	mmes := f_REST_MmeList();
	if (lengthof(mmes) != 4) {
		setverdict(fail, "Expected 4 MMEs after add, got ", lengthof(mmes));
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}
	idx := f_REST_mme_find(mmes, "mme-test");
	if (idx == -1) {
		setverdict(fail, "mme-test not found in MmeList after add");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}
	if (not match(mmes[idx], tr_mme_test)) {
		setverdict(fail, "mme-test does not match: ", mmes[idx]);
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}

	/* also verify via individual GET */
	mme := f_REST_MmeInfo({ name := "name:mme-test" });
	if (not match(mme, tr_mme_test)) {
		setverdict(fail, "mme-test does not match: ", mme);
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}

	/* delete the entry and verify it's gone */
	if (not f_REST_MmeDelete({ name := "name:mme-test" })) {
		setverdict(fail, "Failed to delete mme-test via REST");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}
	mmes := f_REST_MmeList();
	if (lengthof(mmes) != 3) {
		setverdict(fail, "Expected 3 MMEs after delete, got ", lengthof(mmes));
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}
	if (f_REST_mme_find(mmes, "mme-test") != -1) {
		setverdict(fail, "mme-test still present in MmeList after delete");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}

	setverdict(pass);
}

/* ConnHdlr body for TC_rest_mme_del_fallback: connect and complete S1 setup via pool */
private function f_TC_rest_pool_connect_accept(charstring id) runs on ConnHdlr {
	f_ConnHdlr_s1ap_connect(mp_enb_bind_ip, mp_s1gw_enb_ip);
	f_ConnHdlr_s1ap_setup_pool(g_pars.pool_srvs, g_pars.pool_behaviors);
	f_sleep(0.5);
	f_ConnHdlr_s1ap_disconnect();
	f_ConnHdlr_s1ap_unregister_from(g_pars.genb_id, g_s1ap_server);
}

/* REST test: delete an MME from the pool at runtime; eNB must fall back to the
 * next available MME without attempting to connect to the deleted entry. */
testcase TC_rest_mme_del_fallback() runs on test_CT {
	var ConnHdlrPars pars;
	var ConnHdlr vc_conn;
	var MmeList mmes;

	/* start 2 S1AP servers: server[0]=mme0@127.0.2.10, server[1]=mme1@127.0.2.11 */
	f_init(num_mmes := 2);

	/* delete mme0: effective pool becomes [mme1, mme2] */
	if (not f_REST_MmeDelete({ name := "name:mme0" })) {
		setverdict(fail, "Failed to delete mme0 via REST");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}
	mmes := f_REST_MmeList();
	if (lengthof(mmes) != 2) {
		setverdict(fail, "Expected 2 MMEs after deleting mme0, got ", lengthof(mmes));
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}
	if (f_REST_mme_find(mmes, "mme0") != -1) {
		setverdict(fail, "mme0 still present in MmeList after delete");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}

	/* eNB connects: S1GW must select mme1 (server[1]@127.0.2.11) directly */
	pars := valueof(t_ConnHdlrPars);
	pars.pool_srvs := { vc_S1APSRVs[1] };
	pars.pool_behaviors := { S1APSRV_SETUP_ACCEPT };
	vc_conn := f_ConnHdlr_spawn(refers(f_TC_rest_pool_connect_accept), pars);
	vc_conn.done;

	/* restore original pool state (mme0/mme1/mme2 in order) for subsequent tests */
	f_REST_mme_pool_restore();
	mmes := f_REST_MmeList();
	if (lengthof(mmes) != 3) {
		setverdict(fail, "Expected 3 MMEs after pool restore, got ", lengthof(mmes));
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
	}

	setverdict(pass);
}

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_e_rab_setup3_failure() );
	execute( TC_e_rab_modify_req_rsp() );
	execute( TC_e_rab_modify_req_rsp_multi() );
	execute( TC_e_rab_modify_ind_cnf() );
	execute( TC_e_rab_modify_ind_cnf_multi() );
	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_ue_ctx_release_req() );
	execute( TC_ue_ctx_release_req3() );
	execute( TC_ue_ctx_release_cmd_compl() );
	execute( TC_ue_ctx_release_cmd_compl3() );
	execute( TC_uemux_uldl_nas_release() );
	execute( TC_uemux_e_rab_setup() );
	execute( TC_uemux_e_rab_release_cmd() );
	execute( TC_uemux_e_rab_release_ind() );
	execute( TC_uemux_initial_ctx_setup() );
	execute( TC_uemux_ue_ctx_release_req() );
	execute( TC_uemux_ue_ctx_release_cmd() );
	execute( TC_handover_preparation() );
	execute( TC_handover_preparation_release() );
	execute( TC_handover_res_alloc() );
	execute( TC_handover_res_alloc_fail() );
	execute( TC_pfcp_heartbeat() );
	execute( TC_mme_pool_reject_fallback() );
	execute( TC_mme_pool_timeout_fallback() );
	execute( TC_mme_pool_all_reject() );
	execute( TC_mme_pool_enb_disc_wait_s1setup_req() );
	execute( TC_mme_pool_enb_disc_wait_s1setup_rsp() );

	/* REST interface TCs */
	execute( TC_rest_mme_list() );
	execute( TC_rest_mme_add_del() );
	execute( TC_rest_mme_del_fallback() );
}

}
