module BSSGP_ConnHdlr {

import from General_Types all;
import from Osmocom_Types all;
import from GSM_Types all;
import from Native_Functions all;
import from Misc_Helpers all;
import from NS_Types all;
import from NS_Emulation all;
import from BSSGP_Types all;
import from BSSGP_Emulation all;
import from Osmocom_Gb_Types all;
import from SCCPasp_Types all;

import from MobileL3_CommonIE_Types all;
import from MobileL3_GMM_SM_Types all;
import from MobileL3_Types all;
import from L3_Templates all;
import from L3_Common all;
import from ITU_X213_Types all;

import from GSUP_Types all;
import from GSUP_Templates all;
import from GSUP_Emulation all;
import from IPA_Emulation all;

import from RAN_Adapter all;
import from RANAP_Constants all;
import from RANAP_PDU_Descriptions all;
import from RANAP_PDU_Contents all;
import from RANAP_IEs all;
import from RAN_Emulation all;
import from RANAP_Templates all;

import from GTPv1C_CodecPort all;
import from GTPv1U_CodecPort all;
import from GTPC_Types all;
import from GTPU_Types all;
import from GTPv1C_Templates all;
import from GTPv1U_Templates all;
import from GTP_Emulation all;

import from LLC_Types all;
import from LLC_Templates all;

import from SNDCP_Types all;

import from TELNETasp_PortType all;
import from Osmocom_VTY_Functions all;

import from MobileL3_MM_Types all;

const integer NUM_GB := 3;
type record length(NUM_GB) of BssgpCellId BssgpCellIds;

/* Emulated GGSN is at GTP_ConnHdlr.GTP[0] */
const integer GTP_GGSN_IDX := 0;
function ran2gtp_idx(integer ran_index) return integer {
	return ran_index + 1 - NUM_GB;
}

type component BSSGP_ConnHdlr extends BSSGP_Client_CT, GSUP_ConnHdlr, GTP_ConnHdlr, RAN_ConnHdlr {
	var BSSGP_ConnHdlrPars g_pars;
	timer g_Tguard;
	var LLC_Entities llc;
}

type record SGSN_ConnHdlrNetworkPars {
	boolean expect_ptmsi,
	boolean expect_auth,
	boolean expect_ciph
};

template (value) SGSN_ConnHdlrNetworkPars t_NetPars(
                template (value) boolean expect_ptmsi := true,
	        template (value) boolean expect_auth := true,
	        template (value) boolean expect_ciph := false) := {
	expect_ptmsi := expect_ptmsi,
	expect_auth := expect_auth,
	expect_ciph := expect_ciph
};

type record BSSGP_ConnHdlrPars {
	/* IMEI of the simulated ME */
	hexstring imei,
	/* IMSI of the simulated MS */
	hexstring imsi,
	/* MSISDN of the simulated MS (probably unused) */
	hexstring msisdn,
	/* P-TMSI allocated to the simulated MS */
	OCT4 p_tmsi optional,
	OCT3 p_tmsi_sig optional,
	/* TLLI of the simulated MS */
	OCT4 tlli,
	OCT4 tlli_old optional,
	RoutingAreaIdentificationV ra optional,
	BssgpCellIds bssgp_cell_id,
	/* Tracks the RNC state. If true next L3 message will be sent with InitiualUe */
	boolean rnc_send_initial_ue,
	AuthVector vec optional,
	SGSN_ConnHdlrNetworkPars net,
	float t_guard,
	/* only in IuPS / RANAP case */
	SCCP_PAR_Address sccp_addr_local optional,
	SCCP_PAR_Address sccp_addr_peer optional,
	/* Whether to expect an specific addr format in RAB Ass Req: true = X.213, false = raw IPv4, omit = don't care */
	boolean ranap_exp_itu_x213_addr_format optional,
	/* Whether to encode HNBGW addr with ITU X.213 format when sending RAB Ass Resp: */
	boolean ranap_use_itu_x213_addr_format,
	octetstring ranap_itu_x213_addr_format_padding
};

function f_new_BSSGP_ConnHdlrPars(integer imsi_suffix,
				  template (value) BssgpCellIds cell_ids,
				  template (value) SGSN_ConnHdlrNetworkPars net_pars := t_NetPars(),
				  float t_guard := 30.0) return BSSGP_ConnHdlrPars {
	var template (value) BSSGP_ConnHdlrPars pars;
	pars := {
		imei := f_gen_imei(imsi_suffix),
		imsi := f_gen_imsi(imsi_suffix),
		msisdn := f_gen_msisdn(imsi_suffix),
		p_tmsi := omit,
		p_tmsi_sig := omit,
		tlli := f_gprs_tlli_random(),
		tlli_old := omit,
		ra := omit,
		bssgp_cell_id := cell_ids,
		rnc_send_initial_ue := true,
		vec := omit,
		net := net_pars,
		t_guard := t_guard,
		sccp_addr_local := omit,
		sccp_addr_peer := omit,
		ranap_exp_itu_x213_addr_format := omit,
		ranap_use_itu_x213_addr_format := false,
		ranap_itu_x213_addr_format_padding := ''O
	}
	return valueof(pars);
}

private altstep as_Tguard() runs on BSSGP_ConnHdlr {
	[] g_Tguard.timeout {
		setverdict(fail, "Tguard timeout");
		mtc.stop;
	}
}

type function bssgp_connhdlr_void_fn(charstring id) runs on BSSGP_ConnHdlr;

/* first function called in every ConnHdlr */
function f_handler_init(bssgp_connhdlr_void_fn fn, charstring id, BSSGP_ConnHdlrPars pars)
runs on BSSGP_ConnHdlr {
	/* do some common stuff like setting up g_pars */
	g_pars := pars;

	llc := f_llc_create(false);

	/* register with BSSGP core */
	f_bssgp_client_register(g_pars.imsi, g_pars.tlli);
	/* tell GSUP dispatcher to send this IMSI to us */
	f_create_gsup_expect(hex2str(g_pars.imsi));
	/* tell GTP dispatcher to send this IMSI to us */
	f_gtp_register_imsi(g_pars.imsi, GTP_GGSN_IDX);

	g_Tguard.start(pars.t_guard);
	activate(as_Tguard());

	/* call the user-supplied test case function */
	fn.apply(id);
	f_bssgp_client_unregister(g_pars.imsi);
}

private function is_gb(integer ran_index) return boolean {
	return ran_index < NUM_GB;
}
private function is_iu(integer ran_index) return boolean {
	return ran_index >= NUM_GB;
}

function f_send_llc(template (value) PDU_LLC llc_pdu, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var octetstring llc_enc := enc_PDU_LLC(valueof(llc_pdu));
	BSSGP[ran_index].send(ts_BSSGP_UL_UD(g_pars.tlli, g_pars.bssgp_cell_id[ran_index], llc_enc));
}

function f_send_l3_gmm_llc(template (value) PDU_L3_MS_SGSN l3_mo, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var octetstring l3_enc := enc_PDU_L3_MS_SGSN(valueof(l3_mo));
	var BIT4 sapi := f_llc_sapi_by_l3_mo(valueof(l3_mo));
	var integer n_u := f_llc_get_n_u_tx(llc[bit2int(sapi)]);
	f_send_llc(ts_LLC_UI(l3_enc, sapi, '0'B, n_u), ran_index);
}

/* trigger sending of a RANAP InitialUE and wait for SCCP connection confirmation */
function f_send_l3_initial_ue(template (value) PDU_L3_MS_SGSN l3_mo) runs on BSSGP_ConnHdlr {
	log("Sending InitialUE: ", l3_mo);
	var octetstring l3_enc := enc_PDU_L3_MS_SGSN(valueof(l3_mo));
	var RANAP_PDU ranap;
	var LAI lai := {
		pLMNidentity := '62F224'O,
		lAC := '1234'O,
		iE_Extensions := omit
	};
	var RANAP_IEs.RAC rac := '00'O;
	var SAI sai := {
		pLMNidentity := lai.pLMNidentity,
		lAC := lai.lAC,
		sAC := '0000'O, /* FIXME */
		iE_Extensions := omit
	};
	var IuSignallingConnectionIdentifier sigc_id := int2bit(23, 24); /* FIXME */
	var GlobalRNC_ID grnc_id := {
		pLMNidentity := lai.pLMNidentity,
		rNC_ID := 2342 /* FIXME */
	};

	ranap := valueof(ts_RANAP_initialUE_PS(lai, rac, sai, l3_enc, sigc_id, grnc_id));
	BSSAP.send(ts_RANAP_Conn_Req(g_pars.sccp_addr_peer, g_pars.sccp_addr_local, ranap));
	alt {
	[] BSSAP.receive(tr_MSC_CONN_PRIM_CONF_IND) {}
	[] BSSAP.receive(tr_MSC_CONN_PRIM_DISC_IND) {
		setverdict(fail, "DISC.ind from SCCP");
		mtc.stop;
		}
	}
}

/* send a L3 (GMM/SM) message over whatever is the appropriate lower-layer bearer */
function f_send_l3(template (value) PDU_L3_MS_SGSN l3_mo, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	if (is_iu(ran_index)) {
		if (g_pars.rnc_send_initial_ue) {
			g_pars.rnc_send_initial_ue := false;
			f_send_l3_initial_ue(l3_mo);
		} else {
			BSSAP.send(ts_PDU_DTAP_PS_MO(l3_mo));
		}
	} else {
		f_send_l3_gmm_llc(l3_mo, ran_index);
	}
}

altstep as_mm_identity_imei(integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var MobileIdentityLV mi;
	[is_gb(ran_index)] BSSGP[ran_index].receive(tr_GMM_ID_REQ('010'B)) {
		mi := valueof(ts_MI_IMEI_LV(g_pars.imei));
		f_send_l3(ts_GMM_ID_RESP(mi), ran_index);
		repeat;
	}
	[is_iu(ran_index)] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_ID_REQ('010'B))) {
		mi := valueof(ts_MI_IMEI_LV(g_pars.imei));
		f_send_l3(ts_GMM_ID_RESP(mi), ran_index);
		repeat;
	}
}

altstep as_mm_identity_imsi(integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var MobileIdentityLV mi;
	[is_gb(ran_index)] BSSGP[ran_index].receive(tr_GMM_ID_REQ('001'B)) {
		mi := valueof(ts_MI_IMSI_LV(g_pars.imsi));
		f_send_l3(ts_GMM_ID_RESP(mi), ran_index);
		repeat;
	}
	[is_iu(ran_index)] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_ID_REQ('001'B))) {
		mi := valueof(ts_MI_IMSI_LV(g_pars.imsi));
		f_send_l3(ts_GMM_ID_RESP(mi), ran_index);
		repeat;
	}
}

altstep as_mm_identity(integer ran_index := 0) runs on BSSGP_ConnHdlr {
	[] as_mm_identity_imsi(ran_index);
	[] as_mm_identity_imei(ran_index);
}

/* receive a L3 (GMM/SM) message over whatever is the appropriate lower-layer bearer */
altstep as_receive_l3(template PDU_L3_SGSN_MS rx_tpl, inout PDU_L3_SGSN_MS l3_mt, integer ran_index := 0)
runs on BSSGP_ConnHdlr {
	var PDU_DTAP_PS_MT mt;
	[is_gb(ran_index)] BSSGP[ran_index].receive(rx_tpl) -> value l3_mt { }
	[is_iu(ran_index)] BSSAP.receive(tr_PDU_DTAP_PS_MT(rx_tpl)) -> value mt {
		l3_mt := mt.dtap;
	}
}

/* (copied from msc/BSC_ConnectionHandler.ttcn) */
private altstep as_ciph_utran() runs on BSSGP_ConnHdlr
{
	[g_pars.net.expect_ciph] BSSAP.receive(tr_RANAP_SecurityModeCmdEnc(uia_algs := ?,
									   uia_key := oct2bit(g_pars.vec.ik),
									   key_sts := ?,
									   uea_algs := ?,
									   uea_key := oct2bit(g_pars.vec.ck))) {
		var IntegrityProtectionAlgorithm uia_chosen := 0; /*standard_UMTS_integrity_algorithm_UIA1*/
		var EncryptionAlgorithm uea_chosen := 1; /*standard_UMTS_encryption_algorith_UEA1*/
		BSSAP.send(ts_RANAP_SecurityModeCompleteEnc(uia_chosen, uea_chosen));
		}
	[g_pars.net.expect_ciph] BSSAP.receive(tr_RANAP_SecurityModeCmdEnc(?,?,?,?,?)) {
		setverdict(fail, "Invalid SecurityModeCommand (ciphering case)");
		mtc.stop;
		}
	[not g_pars.net.expect_ciph] BSSAP.receive(tr_RANAP_SecurityModeCmd(uia_algs := ?,
									    uia_key := oct2bit(g_pars.vec.ik),
									    key_sts := ?)) {
		var IntegrityProtectionAlgorithm uia_chosen := 0; /*standard_UMTS_integrity_algorithm_UIA1;*/
		BSSAP.send(ts_RANAP_SecurityModeComplete(uia_chosen));
		}
	[not g_pars.net.expect_ciph] BSSAP.receive(tr_RANAP_SecurityModeCmd(?,?,?)) {
		setverdict(fail, "Invalid SecurityModeCommand (non-ciphering case)");
		mtc.stop;
		}
}

/* expect a GSUP Send Auth Information */
altstep as_gsup_sai(boolean umts_aka_challenge := false) runs on BSSGP_ConnHdlr
{
	[g_pars.net.expect_auth] GSUP.receive(tr_GSUP_SAI_REQ(g_pars.imsi)) {
		var GSUP_IE auth_tuple;

		if (umts_aka_challenge) {
			g_pars.vec := f_gen_auth_vec_3g();
			auth_tuple := valueof(ts_GSUP_IE_AuthTuple2G3G(g_pars.vec.rand,
							    g_pars.vec.sres,
							    g_pars.vec.kc,
							    g_pars.vec.ik,
							    g_pars.vec.ck,
							    g_pars.vec.autn,
							    g_pars.vec.res));
		} else {
			g_pars.vec := f_gen_auth_vec_2g();
			auth_tuple := valueof(ts_GSUP_IE_AuthTuple2G(g_pars.vec.rand,
								     g_pars.vec.sres,
								     g_pars.vec.kc));
		}

		GSUP.send(ts_GSUP_SAI_RES(g_pars.imsi, auth_tuple));
	}
}

/* Only used by as_gmm_auth to support same code path for Gb and Iu */
private function f_gmm_auth_as(PDU_L3_SGSN_MS l3_mt, boolean umts_aka_challenge := true, boolean force_gsm_sres := false, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var PDU_L3_MS_SGSN l3_mo;
	var BIT4 ac_ref := l3_mt.msgs.gprs_mm.authenticationAndCipheringRequest.acReferenceNumber.valueField;
	var template (value) PDU_L3_MS_SGSN auth_ciph_resp := ts_GMM_AUTH_RESP_2G(ac_ref, g_pars.vec.sres);

	if (umts_aka_challenge and not force_gsm_sres) {
		/* set UMTS response instead */
		auth_ciph_resp.msgs.gprs_mm.authenticationAndCipheringResponse.authenticationParResp := {
			valueField := substr(g_pars.vec.res, 0, 4)
		};
		auth_ciph_resp.msgs.gprs_mm.authenticationAndCipheringResponse.authenticationRespParExt := {
			elementIdentifier := '21'O,
			lengthIndicator := lengthof(g_pars.vec.res) - 4,
			valueField := substr(g_pars.vec.res, 4, lengthof(g_pars.vec.res) - 4)
		};
	}

	l3_mo := valueof(auth_ciph_resp);
	if (ispresent(l3_mt.msgs.gprs_mm.authenticationAndCipheringRequest.imeisvRequest) and
		l3_mt.msgs.gprs_mm.authenticationAndCipheringRequest.imeisvRequest.valueField == '001'B) {
		l3_mo.msgs.gprs_mm.authenticationAndCipheringResponse.imeisv :=
					valueof(ts_MI_IMEISV_TLV(g_pars.imei & '00'H));
	}
	f_send_l3(l3_mo, ran_index);
}

/* Handles GMM Auth and supplies auth tuples via GSUP if expect_sai is true. */
altstep as_gmm_auth(boolean umts_aka_challenge := false, boolean force_gsm_sres := false, integer ran_index := 0,
					boolean expect_sai := false) runs on BSSGP_ConnHdlr {
	var PDU_DTAP_PS_MT mt;
	var PDU_L3_SGSN_MS l3_mt;

	/* Ignoring autn for now */
	[is_gb(ran_index)] BSSGP[ran_index].receive(tr_GMM_AUTH_REQ(g_pars.vec.rand)) -> value l3_mt {
		f_gmm_auth_as(l3_mt, umts_aka_challenge, force_gsm_sres, ran_index);
	}
	[is_iu(ran_index)] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_AUTH_REQ(g_pars.vec.rand))) -> value mt {
		l3_mt := mt.dtap;
		f_gmm_auth_as(l3_mt, umts_aka_challenge, force_gsm_sres, ran_index);
	}
	[expect_sai] as_gsup_sai(umts_aka_challenge := umts_aka_challenge) { repeat; };
}

/* perform GMM authentication (if expected).
 * Note, for umts_aka_challenge to work, the revisionLevelIndicatior needs to
 * be 1 to mark R99 capability, in the GMM Attach Request, see f_gmm_attach().
 *
 * Requires the order to be:
 * SAI Req
 * SAI Resp
 * Auth Req
 * Auth Resp
 * CommonId
 * (out of order: GMM ID Req)
 */
function f_gmm_auth (boolean umts_aka_challenge := false, boolean force_gsm_sres := false, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var PDU_L3_MS_SGSN l3_mo;
	var PDU_L3_SGSN_MS l3_mt;
	var default di := activate(as_mm_identity(ran_index));
	if (g_pars.net.expect_auth) {
		var GSUP_IE auth_tuple;
		var template AuthenticationParameterAUTNTLV autn;

		if (umts_aka_challenge) {
			g_pars.vec := f_gen_auth_vec_3g();
			autn := {
				elementIdentifier := '28'O,
				lengthIndicator := lengthof(g_pars.vec.autn),
				autnValue := g_pars.vec.autn
				};

			auth_tuple := valueof(ts_GSUP_IE_AuthTuple2G3G(g_pars.vec.rand,
								       g_pars.vec.sres,
								       g_pars.vec.kc,
								       g_pars.vec.ik,
								       g_pars.vec.ck,
								       g_pars.vec.autn,
								       g_pars.vec.res));
			log("GSUP sends 2G and 3G auth tuples", auth_tuple);
		} else {
			g_pars.vec := f_gen_auth_vec_2g();
			autn := omit;
			auth_tuple := valueof(ts_GSUP_IE_AuthTuple2G(g_pars.vec.rand,
								     g_pars.vec.sres,
								     g_pars.vec.kc));
			log("GSUP sends only 2G auth tuple", auth_tuple);
		}

		GSUP.receive(tr_GSUP_SAI_REQ(g_pars.imsi));
		GSUP.send(ts_GSUP_SAI_RES(g_pars.imsi, auth_tuple));

		var template PDU_L3_SGSN_MS auth_ciph_req := tr_GMM_AUTH_REQ(g_pars.vec.rand);
		auth_ciph_req.msgs.gprs_mm.authenticationAndCipheringRequest.authenticationParameterAUTN := autn;
		as_receive_l3(auth_ciph_req, l3_mt, ran_index);
		var BIT4 ac_ref := l3_mt.msgs.gprs_mm.authenticationAndCipheringRequest.acReferenceNumber.valueField;
		var template (value) PDU_L3_MS_SGSN auth_ciph_resp := ts_GMM_AUTH_RESP_2G(ac_ref, g_pars.vec.sres);

		if (umts_aka_challenge and not force_gsm_sres) {
			/* set UMTS response instead */
			auth_ciph_resp.msgs.gprs_mm.authenticationAndCipheringResponse.authenticationParResp := {
				valueField := substr(g_pars.vec.res, 0, 4)
			};
			auth_ciph_resp.msgs.gprs_mm.authenticationAndCipheringResponse.authenticationRespParExt := {
				elementIdentifier := '21'O,
				lengthIndicator := lengthof(g_pars.vec.res) - 4,
				valueField := substr(g_pars.vec.res, 4, lengthof(g_pars.vec.res) - 4)
			};
		}

		l3_mo := valueof(auth_ciph_resp);
		if (ispresent(l3_mt.msgs.gprs_mm.authenticationAndCipheringRequest.imeisvRequest) and
		    l3_mt.msgs.gprs_mm.authenticationAndCipheringRequest.imeisvRequest.valueField == '001'B) {
			l3_mo.msgs.gprs_mm.authenticationAndCipheringResponse.imeisv :=
						valueof(ts_MI_IMEISV_TLV(g_pars.imei & '00'H));
		}
		f_send_l3(l3_mo, ran_index);

		/* Security Mode Command + Complete on Iu case */
		if (is_iu(ran_index)) {
			as_ciph_utran();
			BSSAP.receive(tr_RANAP_CommonId(imsi_hex2oct(g_pars.imsi)));
		}
	} else {
		/* wait for identity procedure */
		f_sleep(1.0);
	}

	deactivate(di);
}

function f_upd_ptmsi_and_tlli(OCT4 p_tmsi, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	g_pars.p_tmsi := p_tmsi;
	/* update TLLI */
	g_pars.tlli_old := g_pars.tlli;
	g_pars.tlli := g_pars.p_tmsi or4b 'c0000000'O;
	if (is_gb(ran_index)) {
		f_bssgp_client_llgmm_assign(g_pars.tlli_old, g_pars.tlli, BSSGP_PROC[ran_index]);
	}
}

function f_process_attach_accept(PDU_GMM_AttachAccept aa, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	/* mandatory IE */
	var hexstring aa_plmn := f_RAI_to_plmn_hexstr(aa.routingAreaIdentification);
	/* we cannot use ran_index here, as it would overflow the cell_id object, since ran_idx > NUM_GB
	 * indicates an Iu RAN connection.  All cells are expected to run the same MCC/MNC anyway... */
	if (not (g_pars.bssgp_cell_id[0].ra_id.lai.mcc_mnc == aa_plmn)) {
		  setverdict(fail, "mismatching PLMN in Attach Accept: " & hex2str(aa_plmn)
				   & "; expected " & hex2str(g_pars.bssgp_cell_id[ran_index].ra_id.lai.mcc_mnc));
		  mtc.stop;
	}
	g_pars.ra := aa.routingAreaIdentification;
	if (ispresent(aa.allocatedPTMSI)) {
		if (not g_pars.net.expect_ptmsi) {
			setverdict(fail, "unexpected P-TMSI allocation");
			mtc.stop;
		}
		f_upd_ptmsi_and_tlli(aa.allocatedPTMSI.mobileIdentityLV.mobileIdentityV.oddEvenInd_identity.tmsi_ptmsi.octets,
				     ran_index);
	}
	if (ispresent(aa.msIdentity)) {
		setverdict(fail, "unexpected TMSI allocation in non-combined attach");
		mtc.stop;
	}
	/* P-TMSI.sig */
	if (ispresent(aa.ptmsiSignature)) {
		g_pars.p_tmsi_sig := aa.ptmsiSignature.valueField;
	}
	/* updateTimer */
	// aa.readyTimer
	/* T3302, T3319, T3323, T3312_ext, T3324 */
}

function f_process_rau_accept(PDU_GMM_RoutingAreaUpdateAccept ra, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	/* mandatory IE */
	g_pars.ra := ra.routingAreaId;
	if (ispresent(ra.allocatedPTMSI)) {
		if (not g_pars.net.expect_ptmsi) {
			setverdict(fail, "unexpected P-TMSI allocation");
			mtc.stop;
		}
		f_upd_ptmsi_and_tlli(ra.allocatedPTMSI.mobileIdentityLV.mobileIdentityV.oddEvenInd_identity.tmsi_ptmsi.octets,
				     ran_index);
	}
	if (ispresent(ra.msIdentity)) {
		setverdict(fail, "unexpected TMSI allocation in non-combined attach");
		mtc.stop;
	}
	/* P-TMSI.sig */
	if (ispresent(ra.ptmsiSignature)) {
		g_pars.p_tmsi_sig := ra.ptmsiSignature.valueField;
	}
	/* updateTimer */
	// aa.readyTimer
	/* T3302, T3319, T3323, T3312_ext, T3324 */
}

function f_random_RAI(HEX0_3n mcc := '262'H, HEX0_3n mnc := '42'H) return RoutingAreaIdentificationV {
	return f_RAI(mcc, mnc, f_rnd_octstring(2), f_rnd_octstring(1));
}

/* return a MobileIdentityLV: P-TMSI if we have one, IMSI otherwise */
function f_mi_get_lv() runs on BSSGP_ConnHdlr return MobileIdentityLV {
	if (ispresent(g_pars.p_tmsi)) {
		return valueof(ts_MI_TMSI_LV(g_pars.p_tmsi));
	} else {
		return valueof(ts_MI_IMSI_LV(g_pars.imsi));
	}
}

altstep as_gmm_gsup_lu_isd() runs on BSSGP_ConnHdlr {
	[] GSUP.receive(tr_GSUP_UL_REQ(g_pars.imsi)) {
		var GSUP_PDU gsup := valueof(ts_GSUP_ISD_REQ(g_pars.imsi, g_pars.msisdn));
		gsup.ies := gsup.ies & { valueof(ts_GSUP_IE_PdpInfo('00'O, char2oct("*"), ts_EuaIPv4Dyn, ''O)) };
		GSUP.send(gsup);
		GSUP.receive(tr_GSUP_ISD_RES(g_pars.imsi));
		GSUP.send(ts_GSUP_UL_RES(g_pars.imsi));
	}
}

function f_gmm_attach(boolean umts_aka_challenge, boolean force_gsm_sres, integer ran_index := 0,
			     template (omit) RoutingAreaIdentificationV old_ra := omit,
			     boolean allow_id_imei_req := false) runs on BSSGP_ConnHdlr {
	var RoutingAreaIdentificationV old_ra_val;
	var template (value) PDU_L3_MS_SGSN attach_req;
	var PDU_L3_SGSN_MS l3_mt;

	if (istemplatekind(old_ra, "omit")) {
		old_ra_val := f_random_RAI();
	} else {
		old_ra_val := valueof(old_ra);
	}

	attach_req := ts_GMM_ATTACH_REQ(f_mi_get_lv(), old_ra_val, false, false, omit, omit);
	/* indicate R99 capability of the MS to enable UMTS AKA in presence of
	 * 3G auth vectors */
	attach_req.msgs.gprs_mm.attachRequest.msNetworkCapability.msNetworkCapabilityV.revisionLevelIndicatior := '1'B;
	/* The thing is, if the solSACapability is 'omit', then the
	 * revisionLevelIndicatior is at the wrong place! */
	attach_req.msgs.gprs_mm.attachRequest.msNetworkCapability.msNetworkCapabilityV.solSACapability := '0'B;

	f_send_l3(attach_req, ran_index);
	f_gmm_auth(umts_aka_challenge, force_gsm_sres, ran_index);
	/* Expect SGSN to perform LU with HLR */
	as_gmm_gsup_lu_isd();

	alt {
		[allow_id_imei_req] as_mm_identity_imei(ran_index) { repeat; }
		[] as_receive_l3(tr_GMM_ATTACH_ACCEPT('001'B, ?, ?), l3_mt, ran_index) {
			f_process_attach_accept(l3_mt.msgs.gprs_mm.attachAccept, ran_index)
			}
	}

	/* FIXME: Extract P-TMSI, if any. Only send Complete if necessary */
	f_send_l3(ts_GMM_ATTACH_COMPL, ran_index);

	/* IuPS case: Expect Iu Release */
	if (is_iu(ran_index)) {
		as_iu_release_compl_disc_ext();
	}

	/* Race condition
	 * It has shown, that GMM_ATTACH_COMPL might take some time to arrive at the SGSN through the layers.
	 * In TC hlr_location_cancel_request_update, the GMM_ATTACH_COMPL came 2ms too late, so that the Location Cancel Request
	 * arrived before it. This results in a test case failure.
	 * Delay execution by 50 ms
	 */
	f_sleep(0.05);
}

function f_bssgp_suspend(integer ran_idx := 0) runs on BSSGP_ConnHdlr return OCT1 {
	timer T := 5.0;
	var PDU_BSSGP rx_pdu;
	BSSGP_GLOBAL[ran_idx].send(ts_BSSGP_SUSPEND(g_pars.tlli, g_pars.bssgp_cell_id[ran_idx].ra_id));
	T.start;
	alt {
	[] BSSGP_GLOBAL[ran_idx].receive(tr_BSSGP_SUSPEND_ACK(g_pars.tlli, g_pars.bssgp_cell_id[ran_idx].ra_id, ?)) -> value rx_pdu {
		return rx_pdu.pDU_BSSGP_SUSPEND_ACK.suspend_Reference_Number.suspend_Reference_Number_value;
		}
	[] BSSGP_GLOBAL[ran_idx].receive(tr_BSSGP_SUSPEND_NACK(g_pars.tlli, g_pars.bssgp_cell_id[ran_idx].ra_id, ?)) -> value rx_pdu {
		setverdict(fail, "SUSPEND-NACK in response to SUSPEND for TLLI ", g_pars.tlli);
		mtc.stop;
		}
	[] T.timeout {
		setverdict(fail, "No SUSPEND-ACK in response to SUSPEND for TLLI ", g_pars.tlli);
		mtc.stop;
		}
	}
	return '00'O;
}

function f_bssgp_resume(OCT1 susp_ref, integer ran_idx := 0) runs on BSSGP_ConnHdlr {
	timer T := 5.0;
	BSSGP_GLOBAL[ran_idx].send(ts_BSSGP_RESUME(g_pars.tlli, g_pars.bssgp_cell_id[ran_idx].ra_id, susp_ref));
	T.start;
	alt {
	[] BSSGP_GLOBAL[ran_idx].receive(tr_BSSGP_RESUME_ACK(g_pars.tlli, g_pars.bssgp_cell_id[ran_idx].ra_id));
	[] BSSGP_GLOBAL[ran_idx].receive(tr_BSSGP_RESUME_NACK(g_pars.tlli, g_pars.bssgp_cell_id[ran_idx].ra_id,
?)) {
		setverdict(fail, "RESUME-NACK in response to RESUME for TLLI ", g_pars.tlli);
		mtc.stop;
		}
	[] T.timeout {
		setverdict(fail, "No RESUME-ACK in response to SUSPEND for TLLI ", g_pars.tlli);
		mtc.stop;
		}
	}
}

altstep as_routing_area_update_gb(integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var PDU_L3_SGSN_MS l3_mt;

	[] BSSGP[ran_index].receive(tr_GMM_RAU_ACCEPT) -> value l3_mt {
		f_process_rau_accept(l3_mt.msgs.gprs_mm.routingAreaUpdateAccept, ran_index);
		f_send_l3(ts_GMM_RAU_COMPL, ran_index);
		setverdict(pass);
		}
	[] BSSGP[ran_index].receive(tr_GMM_RAU_REJECT) {
		setverdict(fail, "Unexpected RAU Reject");
		mtc.stop;
	}
}
altstep as_routing_area_update_iu(integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var PDU_DTAP_PS_MT mt;

	[] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_RAU_ACCEPT)) -> value mt {
		f_process_rau_accept(mt.dtap.msgs.gprs_mm.routingAreaUpdateAccept, ran_index);
		f_send_l3(ts_GMM_RAU_COMPL, ran_index);
		setverdict(pass);
		}
	[] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_RAU_REJECT)) {
		setverdict(fail, "Unexpected RAU Reject");
		mtc.stop;
		}
	[] BSSAP.receive(tr_RANAP_SecurityModeCmd(uia_algs := ?,
						  uia_key := oct2bit(g_pars.vec.ik),
						  key_sts := ?)) {
		var IntegrityProtectionAlgorithm uia_chosen := 0; /* 0 = standard_UMTS_integrity_algorithm_UIA1 */
		BSSAP.send(ts_RANAP_SecurityModeComplete(uia_chosen));
		BSSAP.receive(tr_RANAP_CommonId(imsi_hex2oct(g_pars.imsi)))
		repeat;
	}
}
altstep as_routing_area_update(integer ran_index := 0) runs on BSSGP_ConnHdlr {
	[is_gb(ran_index)] as_routing_area_update_gb(ran_index);
	[is_iu(ran_index)] as_routing_area_update_iu(ran_index);
}

function f_routing_area_update(RoutingAreaIdentificationV old_ra,
			       GprsUpdateType upd_type := GPRS_UPD_T_RA,
			       integer ran_index := 0,
			       float Tval := 2.0) runs on BSSGP_ConnHdlr {
	var template (omit) OCT4 p_tmsi := omit;
	timer T := Tval;

	if (is_iu(ran_index)) {
		p_tmsi := g_pars.p_tmsi;
	}

	f_send_l3(ts_GMM_RAU_REQ(f_mi_get_lv(), upd_type, old_ra, p_tmsi := p_tmsi), ran_index);

	T.start;
	alt {
	[] as_routing_area_update(ran_index) { setverdict(pass); }
	[is_gb(ran_index)] BSSGP[ran_index].receive { repeat; }
	[is_iu(ran_index)] BSSAP.receive { repeat; }
	[] T.timeout {
		setverdict(fail, "Timeout completing the RAU procedure");
		mtc.stop;
		}
	}
}

/* Iu only
 *
 * Handle a service request for a UE in PMM_IDLE or PMM_CONNECTED depending on exp_service_acc
 * Depending on the PMM state:
 * a) PMM_IDLE: The network will do a SecurityModeCommand on Iu, which the UE will treat as an implicit Service Accept
 * b) PMM_CONNECTED: The Iu connection is already secured, do an explicit Service Accept
 *
 * NOTE: The old osmo-sgsn will always respond with a ServiceAccept even when the spec is clear this is not needed.
 */
altstep as_service_request(boolean exp_service_acc := true, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var PDU_DTAP_PS_MT mt;

	[exp_service_acc] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_SERVICE_ACC)) -> value mt {
		setverdict(pass);
		}
	[not exp_service_acc] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_SERVICE_ACC)) -> value mt {
		setverdict(fail, "Unexpected Service Accept");
		mtc.stop;
		}
	[] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_SERVICE_REJ)) {
		setverdict(fail, "Unexpected Service Reject");
		mtc.stop;
		}
	/* 24.008 4.7.13.3: a SecurityModeCommand is an implicit Service Accept if UE was in PMM-IDLE */
	[] BSSAP.receive(tr_RANAP_SecurityModeCmd(uia_algs := ?,
						  uia_key := oct2bit(g_pars.vec.ik),
						  key_sts := ?)) {
		var IntegrityProtectionAlgorithm uia_chosen := 0; /* 0 = standard_UMTS_integrity_algorithm_UIA1 */
		BSSAP.send(ts_RANAP_SecurityModeComplete(uia_chosen));
		if (not exp_service_acc) {
			/* Because we stop processing early, we need to consume the CommonID */
			BSSAP.receive(tr_RANAP_CommonId(imsi_hex2oct(g_pars.imsi)));
			setverdict(pass);
		} else {
			/* This repeat would be wrong if you follow the spec correct. Because:
			 * a) the UE is in PMM Idle and in this case the exp_service_acc would be true
			 * b) the UE is in PMM Connected and in this case the Iu Connection should be already secure and this would fail.
			 * The old osmo-sgsn is doing for UE in PMM Idle both a Security Command and a Service Accept, after the VLR change, the
			 * osmo-sgsn will follow the spec correct.
			 */
			repeat;
		}
	}
}

function f_service_request(inout PdpActPars apars,
			   ServiceType service_type := SERVICE_TYPE_Signalling,
			   template (value) OCT2 pdp_status := '0000'O,
			   boolean exp_ggsn_pdp_del := false,
			   boolean expect_auth := false,
			   integer ran_index := 0,
			   float Tval := 5.0) runs on BSSGP_ConnHdlr {
	timer T := Tval;

	if (not is_iu(ran_index)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					"GMM Service Request called on non-Iu RAN index");
	}


	f_send_l3(ts_GMM_SERVICE_REQ(service_type,
				     p_tmsi := g_pars.p_tmsi,
				     pdp_status := pdp_status),
		  ran_index);

	T.start;

	if (exp_ggsn_pdp_del) {
		alt {
		[] as_ggsn_gtp_ctx_del_req(apars) { setverdict(pass); }
		[] T.timeout {
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
				"Timeout completing the DeletePDPCtx procedure");
			}
		}
	}

	if (expect_auth) {
		f_gmm_auth(umts_aka_challenge := true, ran_index := ran_index);
	}

	alt {
	[] as_service_request(exp_service_acc := true, ran_index := ran_index) { setverdict(pass); }
	[not expect_auth] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_GMM_AUTH_REQ)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
			"Service Request: Unexpected GMM Auth Req");
		}
	[not expect_auth] GSUP.receive(tr_GSUP_SAI_REQ(*)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
			"Service Request: Unexpected GSUP SAI Req");
		}
	[] BSSAP.receive { repeat; }
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
			"Timeout completing the Service Request procedure");
		}
	}
}

/* expect a IuReleaseCommand; Confirm that; expect SCCP-level N-DISCONNET.ind */
altstep as_iu_release_compl_disc_ext() runs on BSSGP_ConnHdlr {
	[] as_iu_release_compl_disc() {
		/* Next message will be an InitialUE since conn was torn down */
		g_pars.rnc_send_initial_ue := true;
	}
}

function f_iu_release_req(template (value) Cause cause) runs on BSSGP_ConnHdlr {
	timer T := 5.0;
	BSSAP.send(ts_RANAP_IuReleaseRequest(cause));
	T.start;
	alt {
	[] as_iu_release_compl_disc_ext() { T.stop; };
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
			"Timeout completing the Iu Release procedure");
	}
	}
}

/* Validate received IP address + TEID from SGSN is the one we
 * did set up from the GGSN, since the SGSN is expected to do
 * Direct Tunnel: */
private function f_ranap_rab_ass_req_validate_tli(RANAP_PDU rab_ass_req, PdpActPars apars)
runs on BSSGP_ConnHdlr {
	var TransportLayerInformation tli := f_ranap_rab_ass_req_extract_tli(rab_ass_req);
	var template (present) TransportLayerAddress exp_tla_x213 := decmatch tr_NSAP_Address_IANA_BIN_IPv4(apars.ggsn_ip_u);
	var template (present) TransportLayerAddress exp_tla_raw := oct2bit(apars.ggsn_ip_u);
	var template (present) TransportLayerAddress exp_tla;
	var template (present) TransportLayerInformation exp_tli;
	if (ispresent(g_pars.ranap_exp_itu_x213_addr_format)) {
		if (g_pars.ranap_exp_itu_x213_addr_format) {
			exp_tla := exp_tla_x213;
		} else {
			exp_tla := exp_tla_raw;
		}
	} else { /* Accept any of the known formats: */
		exp_tla := (exp_tla_x213, exp_tla_raw);
	}
	exp_tli := tr_TLI_ps(exp_tla, apars.ggsn_tei_u);
	if (not match(tli, exp_tli)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
			log2str("Rx RAB Ass Req with TLI ", tli, " vs exp ", exp_tli));
	}
}

altstep as_ranap_rab_ass_req(inout PdpActPars apars) runs on BSSGP_ConnHdlr {
	var RANAP_PDU ranap;
	[] BSSAP.receive(tr_RANAP_RabAssReq(?)) -> value ranap {
		var RAB_ID rab_id := f_ranap_rab_ass_req_extract_rab_id(ranap);

		f_ranap_rab_ass_req_validate_tli(ranap, apars);

		var template (value) RAB_SetupOrModifiedList l;
		var bitstring tla_bits;
		if (g_pars.ranap_use_itu_x213_addr_format) {
			tla_bits := oct2bit(enc_NSAP_Address(valueof(ts_NSAP_Address_IANA_BIN_IPv4(apars.rnc_ip_u, g_pars.ranap_itu_x213_addr_format_padding))));
		} else {
			tla_bits := oct2bit(apars.rnc_ip_u);
		}
		l := ts_RAB_SMdL_ps(rab_id, tla_bits, apars.rnc_tei_u);
		BSSAP.send(ts_RANAP_RabAssResp(l));
	}
}

type record PdpActPars {
	BIT3			tid,				/* L3 Transaction ID */
	BIT4			nsapi,				/* SNDCP NSAPI */
	BIT4			sapi,				/* LLC SAPI */
	QoSV			qos,				/* QoS parameters */
	PDPAddressV		addr,				/* IP address */
	octetstring		apn optional,			/* APN name */
	ProtocolConfigOptionsV	pco optional,			/* protoco config opts */
	OCT1			exp_rej_cause optional,		/* expected SM reject cause */
	GTP_Cause		gtp_resp_cause,			/* GTP response cause */
	OCT4			chg_id,				/* GTP Charging Identifier */

	OCT4			ggsn_tei_c,			/* GGSN TEI Control*/
	OCT4			ggsn_tei_u,			/* GGSN TEI User */
	octetstring		ggsn_ip_c,			/* GGSN IP Control */
	octetstring		ggsn_ip_u,			/* GGSN IP User */
	OCT1			ggsn_restart_ctr,		/* GGSN Restart Counter */
	boolean			direct_tunnel_requested,	/* Did SGSN request Direct Tunnel to the GGSN ?*/

	OCT4			rnc_tei_u,			/* SGSN TEI User */
	octetstring		rnc_ip_u,			/* SGSN IP USer */

	OCT4			sgsn_tei_c optional,		/* SGSN TEI Control */
	OCT4			sgsn_tei_u optional,		/* SGSN TEI User */
	octetstring		sgsn_ip_c optional,		/* SGSN IP Control */
	octetstring		sgsn_ip_u optional		/* SGSN IP USer */
};


function f_process_gtp_ctx_act_req(inout PdpActPars apars, PDU_GTPC gtpc) runs on BSSGP_ConnHdlr {
	var GTPC_PDUs gtpc_rx := gtpc.gtpc_pdu;
	apars.sgsn_tei_c := gtpc_rx.createPDPContextRequest.teidControlPlane.teidControlPlane;
	apars.sgsn_tei_u := gtpc_rx.createPDPContextRequest.teidDataI.teidDataI;
	apars.sgsn_ip_c := gtpc_rx.createPDPContextRequest.sgsn_addr_signalling.addressf;
	apars.sgsn_ip_u := gtpc_rx.createPDPContextRequest.sgsn_addr_traffic.addressf;
	f_gtp_register_teid(apars.ggsn_tei_c, GTP_GGSN_IDX);
	f_gtp_register_teid(apars.ggsn_tei_u, GTP_GGSN_IDX);
}
altstep as_ggsn_gtp_ctx_act_req(inout PdpActPars apars, boolean send_recovery := false) runs on BSSGP_ConnHdlr {
	var Gtp1cUnitdata g_ud;
	var template Recovery_gtpc recovery := omit;

	[] GTP[GTP_GGSN_IDX].receive(tr_GTPC_MsgType(?, createPDPContextRequest, ?)) -> value g_ud {
		f_process_gtp_ctx_act_req(apars, g_ud.gtpc);
		if (send_recovery) {
			recovery := ts_Recovery(apars.ggsn_restart_ctr);
		}
		var integer seq_nr := oct2int(g_ud.gtpc.opt_part.sequenceNumber);
		GTP[GTP_GGSN_IDX].send(ts_GTPC_CreatePdpResp(g_ud.peer, seq_nr,
						apars.sgsn_tei_c, apars.gtp_resp_cause,
						apars.ggsn_tei_c, apars.ggsn_tei_u,
						apars.nsapi,
						apars.ggsn_ip_c, apars.ggsn_ip_u, apars.chg_id,
						omit, recovery));
	}
}

function f_process_gtp_ctx_upd_req(inout PdpActPars apars,
				   PDU_GTPC gtpc,
				   boolean exp_dir_tun := true,
				   integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var UpdatePDPContextRequestSGSN upd := gtpc.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestSGSN;
	var boolean dti := ispresent(upd.directTunnelFlags) and upd.directTunnelFlags.dTI == '1'B;
	if (dti != exp_dir_tun) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
				log2str("Rx UpdatePDPCtxReq with Direct Tunnel Flags ",
					upd.directTunnelFlags, " vs exp DTI=", dti));
	}

	if (ispresent(upd.teidControlPlane.teidControlPlane)) {
		apars.sgsn_tei_c := upd.teidControlPlane.teidControlPlane;
	}
	apars.sgsn_ip_c := upd.sgsn_addr_controlPlane.addressf;
	if (dti) {
		if (apars.rnc_ip_u != upd.sgsn_addr_traffic.addressf or
		    apars.rnc_tei_u != upd.teidDataI.teidDataI) {
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
				log2str("Rx UpdatePDPCtxReq with DTI=1 and wrong RNC IP+TEID: ",
					upd.sgsn_addr_traffic.addressf, ":", upd.teidDataI.teidDataI,
					" vs exp ", apars.rnc_ip_u, ":", apars.rnc_tei_u));
		}
		f_gtp_register_teid(apars.rnc_tei_u, ran2gtp_idx(ran_index));
	} else {
		apars.sgsn_ip_u := upd.sgsn_addr_traffic.addressf;
		apars.sgsn_tei_u := upd.teidDataI.teidDataI;
	}
	apars.direct_tunnel_requested := dti;
}
altstep as_ggsn_gtp_ctx_upd_req(inout PdpActPars apars,
				boolean send_recovery := false,
				boolean exp_dir_tun := true,
				integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var Gtp1cUnitdata g_ud;

	[] GTP[GTP_GGSN_IDX].receive(tr_GTPC_MsgType(?, updatePDPContextRequest, ?)) -> value g_ud {
		f_process_gtp_ctx_upd_req(apars, g_ud.gtpc,
					  exp_dir_tun := exp_dir_tun,
					  ran_index := ran_index);
		var integer seq_nr := oct2int(g_ud.gtpc.opt_part.sequenceNumber);
		GTP[GTP_GGSN_IDX].send(ts_GTPC_UpdatePdpRespGGSN(g_ud.peer, seq_nr,
						apars.sgsn_tei_c, apars.gtp_resp_cause,
						apars.ggsn_tei_c, apars.ggsn_tei_u,
						apars.ggsn_ip_c, apars.ggsn_ip_u, apars.chg_id,
						omit, omit));
	}
}

altstep as_ggsn_gtp_ctx_del_req(inout PdpActPars apars) runs on BSSGP_ConnHdlr {
	var Gtp1cUnitdata g_ud;

	[] GTP[GTP_GGSN_IDX].receive(tr_GTPC_MsgType(?, deletePDPContextRequest, apars.ggsn_tei_c)) -> value g_ud {
		var integer seq_nr := oct2int(g_ud.gtpc.opt_part.sequenceNumber);
		GTP[GTP_GGSN_IDX].send(ts_GTPC_DeletePdpResp(g_ud.peer, seq_nr,
							     apars.sgsn_tei_c,
							     GTP_CAUSE_REQUEST_ACCEPTED));
	}
}

altstep as_pdp_ctx_act_gb(inout PdpActPars apars, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var boolean exp_rej := ispresent(apars.exp_rej_cause);

	[exp_rej] BSSGP[ran_index].receive(tr_SM_ACT_PDP_REJ(apars.tid, apars.exp_rej_cause)) {
		setverdict(pass);
		}
	[exp_rej] BSSGP[ran_index].receive(tr_SM_ACT_PDP_ACCEPT) {
		setverdict(fail, "Unexpected PDP CTX ACT ACC");
		mtc.stop;
		}
	[not exp_rej] BSSGP[ran_index].receive(tr_SM_ACT_PDP_REJ(apars.tid, ?)) {
		setverdict(fail, "Unexpected PDP CTX ACT FAIL");
		mtc.stop;
		}
	[not exp_rej] BSSGP[ran_index].receive(tr_SM_ACT_PDP_REJ(apars.tid, ?)) {
		setverdict(fail, "Unexpected PDP CTX ACT FAIL");
		mtc.stop;
		}
	[not exp_rej] BSSGP[ran_index].receive(tr_SM_ACT_PDP_ACCEPT(apars.tid, apars.sapi)) {
		setverdict(pass);
		}
	[] as_xid(apars, ran_index);
}
altstep as_pdp_ctx_act_iu(inout PdpActPars apars, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	var boolean exp_rej := ispresent(apars.exp_rej_cause);

	[exp_rej] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_SM_ACT_PDP_REJ(apars.tid, apars.exp_rej_cause))) {
		setverdict(pass);
		}
	[exp_rej] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_SM_ACT_PDP_ACCEPT)) {
		setverdict(fail, "Unexpected PDP CTX ACT ACC");
		mtc.stop;
		}
	[not exp_rej] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_SM_ACT_PDP_REJ(apars.tid, ?))) {
		setverdict(fail, "Unexpected PDP CTX ACT FAIL");
		mtc.stop;
		}
	[not exp_rej] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_SM_ACT_PDP_REJ(apars.tid, ?))) {
		setverdict(fail, "Unexpected PDP CTX ACT FAIL");
		mtc.stop;
		}
	[not exp_rej] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_SM_ACT_PDP_ACCEPT(apars.tid, apars.sapi))) {
		setverdict(pass);
		}
}
altstep as_pdp_ctx_act(inout PdpActPars apars, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	[is_gb(ran_index)] as_pdp_ctx_act_gb(apars, ran_index);
	[is_iu(ran_index)] as_pdp_ctx_act_iu(apars, ran_index);
}

function f_pdp_tx_ctx_act(inout PdpActPars apars, integer ran_index := 0)
runs on BSSGP_ConnHdlr {
	f_send_l3(ts_SM_ACT_PDP_REQ(apars.tid, apars.nsapi, apars.sapi, apars.qos, apars.addr,
				    apars.apn, apars.pco), ran_index);
}

function f_pdp_ctx_act(inout PdpActPars apars, boolean send_recovery := false, integer ran_index := 0, float Tval := 5.0)
runs on BSSGP_ConnHdlr {
	timer T := Tval;

	f_pdp_tx_ctx_act(apars, ran_index);
	as_ggsn_gtp_ctx_act_req(apars, send_recovery := send_recovery);

	T.start;
	if (is_iu(ran_index)) {
		alt {
		[] as_ranap_rab_ass_req(apars) {
			as_ggsn_gtp_ctx_upd_req(apars,
					        send_recovery := send_recovery,
						ran_index := ran_index);
			}
		[] T.timeout {
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
				"Timeout waiting for RANAP RAB AssReq");
			}
		}
	}

	alt {
	[] as_pdp_ctx_act(apars, ran_index) { T.stop; setverdict(pass); }
	[is_gb(ran_index)] BSSGP[ran_index].receive { repeat; }
	[is_iu(ran_index)] BSSAP.receive { repeat; }
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
			"Timeout completing the PDP ACT");
		}
	}
}

function f_pdp_ctx_deact_mo(inout PdpActPars apars, OCT1 cause, integer ran_index := 0)
runs on BSSGP_ConnHdlr {
	var boolean exp_rej := ispresent(apars.exp_rej_cause);
	var Gtp1cUnitdata g_ud;

	f_send_l3(ts_SM_DEACT_PDP_REQ_MO(apars.tid, cause, false, omit), ran_index);
	if (is_gb(ran_index)) {
		BSSGP[ran_index].clear;
	}
	as_ggsn_gtp_ctx_del_req(apars);
	alt {
	[is_gb(ran_index)] BSSGP[ran_index].receive(tr_SM_DEACT_PDP_ACCEPT_MT(apars.tid));
	[is_gb(ran_index)] as_xid(apars, ran_index);
	[is_iu(ran_index)] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_SM_DEACT_PDP_ACCEPT_MT(apars.tid)));
	}
	setverdict(pass);
}

altstep as_pdp_ctx_deact_mt(inout PdpActPars apars, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	[is_gb(ran_index)] BSSGP[ran_index].receive(tr_SM_DEACT_PDP_REQ_MT(apars.tid, ?, true)) {
		f_send_l3(ts_SM_DEACT_PDP_ACCEPT_MO(apars.tid), ran_index);
		}
	[is_iu(ran_index)] BSSAP.receive(tr_PDU_DTAP_PS_MT(tr_SM_DEACT_PDP_REQ_MT(apars.tid, ?, true))) {
		f_send_l3(ts_SM_DEACT_PDP_ACCEPT_MO(apars.tid), ran_index);
		}
}

function f_pdp_ctx_deact_mt(inout PdpActPars apars, boolean error_ind := false, integer ran_index := 0)
runs on BSSGP_ConnHdlr {
	var Gtp1cUnitdata g_ud;
	var integer seq_nr := 23;

	BSSGP[ran_index].clear;
	if (error_ind) {
		var Gtp1uPeer peer := valueof(ts_GtpPeerU(apars.sgsn_ip_c));
		GTP[GTP_GGSN_IDX].send(ts_GTPU_ErrorIndication(peer, 0 /* seq */, apars.ggsn_tei_u, apars.ggsn_ip_u));
	} else {
		var Gtp1cPeer peer := valueof(ts_GtpPeerC(apars.sgsn_ip_c));
		GTP[GTP_GGSN_IDX].send(ts_GTPC_DeletePDP(peer, seq_nr, apars.sgsn_tei_c, apars.nsapi, '1'B));
	}

	timer T := 5.0;
	T.start;

	alt {
	[] as_pdp_ctx_deact_mt(apars, ran_index := ran_index);
	[not error_ind] GTP[GTP_GGSN_IDX].receive(tr_GTPC_MsgType(?, deletePDPContextResponse, apars.ggsn_tei_c)) {
		repeat;
		}
	[] T.timeout {
		setverdict(fail, "Waiting for SM_DEACT_PDP_REQ_MT");
		}
	}
}


/* Table 10.5.156/3GPP TS 24.008 */
template (value) QoSV t_QosDefault := {
	reliabilityClass := '011'B, /* unacknowledged GTP+LLC, acknowledged RLC */
	delayClass := '100'B,	/* best effort */
	spare1 := '00'B,
	precedenceClass := '010'B, /* normal */
	spare2 := '0'B,
	peakThroughput := '0000'B, /* subscribed */
	meanThroughput := '00000'B, /* subscribed */
	spare3 := '000'B,
	deliverErroneusSDU := omit,
	deliveryOrder := omit,
	trafficClass := omit,
	maxSDUSize := omit,
	maxBitrateUplink := omit,
	maxBitrateDownlink := omit,
	sduErrorRatio := omit,
	residualBER := omit,
	trafficHandlingPriority := omit,
	transferDelay := omit,
	guaranteedBitRateUplink := omit,
	guaranteedBitRateDownlink := omit,
	sourceStatisticsDescriptor := omit,
	signallingIndication := omit,
	spare4 := omit,
	maxBitrateDownlinkExt := omit,
	guaranteedBitRateDownlinkExt := omit,
	maxBitrateUplinkExt := omit,
	guaranteedBitRateUplinkExt := omit,
	maxBitrateDownlinkExt2 := omit,
	guaranteedBitRateDownlinkExt2 := omit,
	maxBitrateUplinkExt2 := omit,
	guaranteedBitRateUplinkExt2 := omit
}

/* 10.5.6.4 / 3GPP TS 24.008 */
template (value) PDPAddressV t_AddrIPv4dyn := {
	pdpTypeOrg := '0001'B, /* IETF */
	spare := '0000'B,
	pdpTypeNum := '21'O, /* IPv4 */
	addressInfo := omit
}
template (value) PDPAddressV t_AddrIPv6dyn := {
	pdpTypeOrg := '0001'B, /* IETF */
	spare := '0000'B,
	pdpTypeNum := '53'O, /* IPv6 */
	addressInfo := omit
}

template (value) PdpActPars t_PdpActPars(charstring ggsn_ip,
					 charstring rnc_ip := "127.0.0.1") := {
	tid := '000'B,
	nsapi := '0101'B, /* < 5 are reserved */
	sapi := '0011'B, /* 3/5/9/11 */
	qos := t_QosDefault,
	addr := t_AddrIPv4dyn,
	apn := omit,
	pco := omit,
	exp_rej_cause := omit,
	gtp_resp_cause := GTP_CAUSE_REQUEST_ACCEPTED,
	chg_id := f_rnd_octstring(4),

	/* FIXME: make below dynamic !! */
	ggsn_tei_c := f_rnd_octstring(4),
	ggsn_tei_u := f_rnd_octstring(4),
	ggsn_ip_c := f_inet_addr(ggsn_ip),
	ggsn_ip_u := f_inet_addr(ggsn_ip),
	ggsn_restart_ctr := int2oct(2, 1),
	direct_tunnel_requested := false,

	rnc_tei_u := f_rnd_octstring(4),
	rnc_ip_u := f_inet_addr(rnc_ip),

	sgsn_tei_c := omit,
	sgsn_tei_u := omit,
	sgsn_ip_c := omit,
	sgsn_ip_u := omit
}

template (value) Gtp1uPeer ts_GtpPeerU(octetstring ip) := {
	connId := 1,
	remName := f_inet_ntoa(ip),
	remPort := GTP1U_PORT
}

template (value) Gtp1cPeer ts_GtpPeerC(octetstring ip) := {
	connId := 1,
	remName := f_inet_ntoa(ip),
	remPort := GTP1C_PORT
}

/* Send data from GGSN to SGSN/RNC (depending on apars.f_ggsn_gtpu_send) */
function f_ggsn_gtpu_send(inout PdpActPars apars, octetstring payload) runs on BSSGP_ConnHdlr {
	var Gtp1uPeer peer;
	var OCT4 rem_teid;
	if (apars.direct_tunnel_requested) {
		peer := valueof(ts_GtpPeerU(apars.rnc_ip_u));
		rem_teid := apars.rnc_tei_u;
	} else {
		peer := valueof(ts_GtpPeerU(apars.sgsn_ip_u));
		rem_teid := apars.sgsn_tei_u;
	}
	GTP[GTP_GGSN_IDX].send(ts_GTP1U_GPDU(peer, omit /*opt_part*/, rem_teid, payload));
}

altstep as_xid(PdpActPars apars, integer ran_index := 0) runs on BSSGP_ConnHdlr {
	[] BSSGP[ran_index].receive(tr_LLC_XID_MT_CMD(?, apars.sapi)) {
		repeat;
	}
}

template PDU_SN tr_SN_UD(template BIT4 nsapi, template octetstring payload) := {
	pDU_SN_UNITDATA := {
		nsapi := nsapi,
		moreBit := ?,
		snPduType := '1'B,
		firstSegmentIndicator := ?,
		spareBit := ?,
		pcomp := ?,
		dcomp := ?,
		npduNumber := ?,
		segmentNumber := ?,
		npduNumberContinued := ?,
		dataSegmentSnUnitdataPdu := payload
	}
}

/* simple case: single segment, no compression */
template (value) PDU_SN ts_SN_UD(BIT4 nsapi, octetstring payload) := {
	pDU_SN_UNITDATA := {
		nsapi := nsapi,
		moreBit := '0'B,
		snPduType := '1'B,
		firstSegmentIndicator := '1'B,
		spareBit := '0'B,
		pcomp := '0000'B,
		dcomp := '0000'B,
		npduNumber := '0000'B,
		segmentNumber := '0000'B,
		npduNumberContinued := '00'O,
		dataSegmentSnUnitdataPdu := payload
	}
}

/* Transceive given 'payload' as MT message from GTP -> OsmoSGSN -> {Gb,Iu} */
function f_gtpu_xceive_mt(inout PdpActPars apars, octetstring payload, integer ran_index := 0, boolean expect_fwd := true)
runs on BSSGP_ConnHdlr {
	timer T := 5.0;
	/* Send PDU via GTP from our simulated GGSN to the SGSN */
	f_ggsn_gtpu_send(apars, payload);
	T.start;

	alt {
	/* Expect PDU via BSSGP/LLC on simulated PCU from SGSN: */
	[is_gb(ran_index)] as_xid(apars, ran_index);
	//[] BSSGP[ran_index].receive(tr_BD_SNDCP(apars.sapi, tr_SN_UD(apars.nsapi, payload)));
	[is_gb(ran_index) and expect_fwd] BSSGP[ran_index].receive(tr_SN_UD(apars.nsapi, payload));
	[is_gb(ran_index) and not expect_fwd] BSSGP[ran_index].receive(tr_SN_UD(apars.nsapi, payload)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "GTP-U forwarded to BSSGP but not expected");
		}
	/* Expect PDU directly from RNC (Direct Tunnel): */
	[is_iu(ran_index) and expect_fwd] GTP[ran2gtp_idx(ran_index)].receive(tr_GTPU_GPDU(valueof(ts_GtpPeerU(apars.ggsn_ip_u)), apars.rnc_tei_u, payload));
	[is_iu(ran_index) and not expect_fwd] GTP[ran2gtp_idx(ran_index)].receive(tr_GTPU_GPDU(valueof(ts_GtpPeerU(apars.ggsn_ip_u)), apars.rnc_tei_u, payload)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "GTP-U forwarded to BSSGP but not expected")
		}
	[expect_fwd] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Timeout waiting for GTP-U to appear on Gb/Iu");
		}
	[not expect_fwd] T.timeout {}
	}
}

/* Transceive given 'payload' as MO message from {Gb,Iu} -> OsmoSGSN -> GTP */
function f_gtpu_xceive_mo(inout PdpActPars apars, octetstring payload, integer ran_index := 0, uint9_t n_u := 0)
runs on BSSGP_ConnHdlr {
	var Gtp1uPeer rx_peer;

	if (is_gb(ran_index)) {
		/* Send PDU via SNDCP/LLC/BSSGP/NS via simulated MS/PCU to the SGSN: */
		rx_peer := valueof(ts_GtpPeerU(apars.sgsn_ip_u));
		var PDU_SN sndcp := valueof(ts_SN_UD(apars.nsapi, payload));
		BSSGP[ran_index].send(ts_LLC_UI(enc_PDU_SN(sndcp), apars.sapi, '0'B, n_u));
	} else if (is_iu(ran_index)) {
		/* Send PDU via GTPv1U from simulated MS/RNC directly to the simulated GGSN: */
		rx_peer := valueof(ts_GtpPeerU(apars.rnc_ip_u));
		GTP[ran2gtp_idx(ran_index)].send(ts_GTP1U_GPDU(valueof(ts_GtpPeerU(apars.ggsn_ip_u)), omit /*opt_part*/, apars.ggsn_tei_u, payload));
	}
	/* Expect PDU via GTP from RNC/SGSN on simulated GGSN */
	alt {
	[] GTP[GTP_GGSN_IDX].receive(tr_GTPU_GPDU(rx_peer, apars.ggsn_tei_u, payload));
	}
}



}