module UPF_Tests {

/* Integration Tests for OsmoUPF
 * (C) 2022 by sysmocom - s.f.m.c. GmbH <info@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
 *
 * This test suite acts as a PFCP Control Plane Function to test OsmoUPF.
 */

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

import from Osmocom_CTRL_Functions all;
import from Osmocom_CTRL_Types all;
import from Osmocom_CTRL_Adapter all;

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

import from Osmocom_VTY_Functions all;
import from TELNETasp_PortType all;

import from CPF_ConnectionHandler all;

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

modulepar {
	/* IP address at which the UPF can be reached */
	charstring mp_pfcp_ip_upf := "127.0.0.1";
	charstring mp_pfcp_ip_local := "127.0.0.2";

	charstring mp_netinst_access_ip_1 := "127.0.1.1";
	charstring mp_netinst_access_ip_2 := "127.0.1.2";
	charstring mp_netinst_core_ip_1 := "127.0.2.1";
	charstring mp_netinst_core_ip_2 := "127.0.2.2";

	/* When testing with gtp mockup, actions will not show. */
	boolean mp_verify_gtp_actions := false;
}

type component test_CT extends CTRL_Adapter_CT {
	port TELNETasp_PT UPFVTY;

	/* global test case guard timer (actual timeout value is set in f_init()) */
	timer T_guard := 15.0;
}

/* global altstep for global guard timer; */
altstep as_Tguard() runs on test_CT {
	[] T_guard.timeout {
			setverdict(fail, "Timeout of T_guard");
			mtc.stop;
		}
}

private function f_get_name_val(out charstring val, charstring str, charstring name, charstring sep := ":", charstring delim := " ") return boolean {
	var charstring labl := name & sep;
	var integer namepos := f_strstr(str, labl);
	if (namepos < 0) {
		return false;
	}
	var integer valpos := namepos + lengthof(labl);
	var integer valend := f_strstr(str, delim, valpos);
	if (valend < 0) {
		valend := lengthof(str);
	}
	val := substr(str, valpos, valend - valpos);
	return true;
}

private function f_get_name_val_oct8(out OCT8 val, charstring str, charstring name) return boolean {
	var charstring token;
	if (not f_get_name_val(token, str, name, ":0x")) {
		return false;
	}
	if (lengthof(token) > 16) {
		log("token too long: ", name, " in ", str);
		return false;
	}
	var charstring padded := substr("0000000000000000", 0, 16 - lengthof(token)) & token;
	val := str2oct(padded);
	return true;
}

private function f_get_name_val_oct4(out OCT4 val, charstring str, charstring name) return boolean {
	var charstring token;
	if (not f_get_name_val(token, str, name, ":0x")) {
		return false;
	}
	if (lengthof(token) > 8) {
		log("token too long: ", name, " in ", str);
		return false;
	}
	var charstring padded := substr("00000000", 0, 8 - lengthof(token)) & token;
	val := str2oct(padded);
	return true;
}

private function f_get_name_val_int(out integer val, charstring str, charstring name) return boolean {
	var charstring token;
	if (not f_get_name_val(token, str, name)) {
		return false;
	}
	val := str2int(token);
	return true;
}

private function f_get_name_val_2int(out integer val1, out integer val2, charstring str, charstring name, charstring delim := ",") return boolean {
	var charstring token;
	if (not f_get_name_val(token, str, name)) {
		return false;
	}
	var Misc_Helpers.ro_charstring nrl := f_str_split(token, delim);
	if (lengthof(nrl) != 2) {
		return false;
	}
	val1 := str2int(nrl[0]);
	val2 := str2int(nrl[1]);
	return true;
}

/* A PFCP session as seen by the system under test, osmo-upf. up_seid is what osmo-upf sees as its local SEID
 * ("SEID-l"). cp_seid is this tester's side's SEID, which osmo-upf sees as the remote SEID. */
type record PFCP_session {
	OCT8 up_seid,
	OCT8 cp_seid,
	GTP_Action gtp
}

/* _r and _l means 'remote' and 'local', from the perspective of the osmo-upf process. */
type record GTP_Action_tunend {
	/* the PDR Id detecting packets from this side */
	integer pdr_id,
	/* IP packets arriving from this side are arriving on ip_l */
	charstring ip_l,

	/* the FAR Id forwarding packets to this side */
	integer far_id
}

/* _r and _l means 'remote' and 'local', from the perspective of the osmo-upf process. */
type record GTP_Action_tun {
	/* the PDR Id detecting packets from this side */
	integer pdr_id,
	/* GTP arriving from this side is arriving on gtp_ip_l with teid_l */
	charstring gtp_ip_l,
	OCT4 teid_l,

	/* the FAR Id forwarding packets to this side */
	integer far_id,
	/* GTP going out to this side should be sent to gtp_ip_r with teid_r */
	charstring gtp_ip_r,
	OCT4 teid_r
}

type union GTP_Action_core {
	/* For kind = "tunend", the local IP that the UE has on 'the internet' */
	GTP_Action_tunend tunend,
	/* For kind = "tunmap", the second GTP tunnel */
	GTP_Action_tun tunmap
}

/* State of what GTP functionality osmo-upf should put in place, after a PFCP request was ACKed by it.
 * _r and _l means 'remote' and 'local', from the perspective of the osmo-upf process.
 *
 * tunend:
 *  Access                                            UPF                      Core
 *  GTP-r:127.0.0.2,0x1  <-FAR-1--                     | 192.168.0.1 <-PDR-1--
 *                       --PDR-2-> GTP-l:127.0.0.1,0x2 |             --FAR-2-> (IP destination is in each GTP payload)
 *
 * tunmap:
 *  Access                                            UPF                        Core
 *  GTP-r:127.0.0.2,0x1  <-FAR-1--                     | 127.0.0.1,0x1 <-PDR-1--
 *                       --PDR-2-> GTP-l:127.0.0.1,0x2 |               --FAR-2-> GTP-r:127.0.0.3,0x2
 */
type record GTP_Action {
	/* kind = ("tunend"|"tunmap") */
	charstring kind,
	/* The access side GTP tunnel. (The 'Access' side is always GTP.) */
	GTP_Action_tun access,
	/* The core side GTP tunnel (tunmap) or local IP (tunend) */
	GTP_Action_core core,
	/* Reference to the PFCP Session that created this GTP action: PFCP session's F-SEID as seen from osmo-upf */
	charstring pfcp_peer,
	OCT8 seid_l
};

type record of GTP_Action GTP_Action_List;

private function f_parse_gtp_action(out GTP_Action ret, charstring str) return boolean {
	/* Parse a string like
	 * "GTP:tunend GTP-access-r:127.0.0.2 TEID-access-r:0x94f0001 TEID-access-l:0x1 IP-core-l:192.168.44.241 PFCP-peer:127.0.0.2 SEID-l:0x1 PDR:1,2"
	 */
	var GTP_Action a;
	if (not f_get_name_val(a.kind, str, "GTP")) {
		return false;
	}
	if (not f_get_name_val(a.access.gtp_ip_r, str, "GTP-access-r")) {
		return false;
	}
	if (not f_get_name_val_oct4(a.access.teid_r, str, "TEID-access-r")) {
		return false;
	}
	if (not f_get_name_val(a.access.gtp_ip_l, str, "GTP-access-l")) {
		return false;
	}
	if (not f_get_name_val_oct4(a.access.teid_l, str, "TEID-access-l")) {
		return false;
	}
	if (not f_get_name_val_int(a.access.pdr_id, str, "PDR-access")) {
		return false;
	}
	if (not f_get_name_val(a.pfcp_peer, str, "PFCP-peer")) {
		return false;
	}
	if (not f_get_name_val_oct8(a.seid_l, str, "SEID-l")) {
		return false;
	}
	if (a.kind == "tunend") {
		if (not f_get_name_val_int(a.core.tunend.pdr_id, str, "PDR-core")) {
			return false;
		}
		if (not f_get_name_val(a.core.tunend.ip_l, str, "IP-core-l")) {
			return false;
		}
		/* in these tests, the PDR Id and its FAR Id are always the same: PDR for incoming on Access matches its
		 * FAR that forwards to Core. */
		a.core.tunend.far_id := a.access.pdr_id;
		a.access.far_id := a.core.tunend.pdr_id;
	} else if (a.kind == "tunmap") {
		if (not f_get_name_val(a.core.tunmap.gtp_ip_r, str, "GTP-core-r")) {
			return false;
		}
		if (not f_get_name_val_oct4(a.core.tunmap.teid_r, str, "TEID-core-r")) {
			return false;
		}
		if (not f_get_name_val(a.core.tunmap.gtp_ip_l, str, "GTP-core-l")) {
			return false;
		}
		if (not f_get_name_val_oct4(a.core.tunmap.teid_l, str, "TEID-core-l")) {
			return false;
		}
		if (not f_get_name_val_int(a.core.tunmap.pdr_id, str, "PDR-core")) {
			return false;
		}
		/* in these tests, the PDR Id and its FAR Id are always the same: PDR for incoming on Access matches its
		 * FAR that forwards to Core. */
		a.core.tunmap.far_id := a.access.pdr_id;
		a.access.far_id := a.core.tunmap.pdr_id;
	}

	ret := a;
	return true;
}

private function f_vty_get_gtp_actions(TELNETasp_PT vty_pt) return GTP_Action_List {
	var charstring gtp_str := f_vty_transceive_ret(vty_pt, "show gtp");
	var Misc_Helpers.ro_charstring lines := f_str_split(gtp_str, "\n");
	var GTP_Action_List gtps := {};
	for (var integer i := 0; i < lengthof(lines); i := i + 1) {
		var charstring line := lines[i];
		var GTP_Action a;
		if (not f_parse_gtp_action(a, line)) {
			continue;
		}
		gtps := gtps & { a };
	}
	log("GTP-actions: ", gtps);
	return gtps;
}

private function f_find_gtp_action(GTP_Action_List actions, template GTP_Action find) return boolean {
	for (var integer i := 0; i < lengthof(actions); i := i + 1) {
		if (match(actions[i], find)) {
			return true;
		}
	}
	return false;
}

private function f_expect_gtp_action(GTP_Action_List actions, template GTP_Action expect) {
	if (f_find_gtp_action(actions, expect)) {
		log("VTY confirms: GTP action active: ", expect);
		setverdict(pass);
		return;
	}
	log("Expected to find ", expect, " in ", actions);
	setverdict(fail, "on VTY, a GTP action failed to show as active");
	mtc.stop;
}

private function f_expect_no_gtp_action(GTP_Action_List actions, template GTP_Action expect) {
	if (f_find_gtp_action(actions, expect)) {
		log("Expected to *not* find ", expect, " in ", actions);
		setverdict(fail, "a GTP action failed to show as inactive");
		mtc.stop;
	}
	log("VTY confirms: GTP action inactive: ", expect);
	setverdict(pass);
	return;
}

private function f_vty_expect_gtp_action(TELNETasp_PT vty_pt, template GTP_Action expect) {
	if (not mp_verify_gtp_actions) {
		/* In GTP mockup mode, GTP actions don't show on VTY. Cannot verify. */
		setverdict(pass);
		return;
	}
	var GTP_Action_List actions := f_vty_get_gtp_actions(vty_pt);
	f_expect_gtp_action(actions, expect);
}

private function f_vty_expect_no_gtp_actions(TELNETasp_PT vty_pt) {
	var GTP_Action_List actions := f_vty_get_gtp_actions(vty_pt);
	if (lengthof(actions) > 0) {
		setverdict(fail, "VTY says that there are still active GTP actions");
		mtc.stop;
	}
	setverdict(pass);
}

type record PFCP_Session_Status {
	charstring peer,
	OCT8 seid_r,
	OCT8 seid_l,
	charstring state,
	integer pdr_active_count,
	integer pdr_count,
	integer far_active_count,
	integer far_count,
	integer gtp_active_count
};

template PFCP_Session_Status PFCP_session_active := {
	peer := ?,
	seid_r := ?,
	seid_l := ?,
	state := "ESTABLISHED",
	pdr_active_count := (1..99999),
	pdr_count := (1..99999),
	far_active_count := (1..99999),
	far_count := (1..99999),
	gtp_active_count := (1..99999)
};

template PFCP_Session_Status PFCP_session_inactive := {
	peer := ?,
	seid_r := ?,
	seid_l := ?,
	state := "ESTABLISHED",
	pdr_active_count := 0,
	pdr_count := (1..99999),
	far_active_count := 0,
	far_count := (1..99999),
	gtp_active_count := 0
};

type record of PFCP_Session_Status PFCP_Session_Status_List;

private function f_parse_session_status(out PFCP_Session_Status ret, charstring str) return boolean {
	var PFCP_Session_Status st;
	if (not f_get_name_val(st.peer, str, "peer")) {
		return false;
	}
	if (not f_get_name_val_oct8(st.seid_l, str, "SEID-l")) {
		return false;
	}
	f_get_name_val_oct8(st.seid_r, str, "SEID-r");
	f_get_name_val(st.state, str, "state");

	/* parse 'PDR-active:1/2' */
	if (not f_get_name_val_2int(st.pdr_active_count, st.pdr_count, str, "PDR-active", "/")) {
		return false;
	}
	/* parse 'FAR-active:1/2' */
	if (not f_get_name_val_2int(st.far_active_count, st.far_count, str, "FAR-active", "/")) {
		return false;
	}

	f_get_name_val_int(st.gtp_active_count, str, "GTP-active");
	ret := st;
	return true;
}

private function f_vty_get_sessions(TELNETasp_PT vty_pt) return PFCP_Session_Status_List {
	var charstring sessions_str := f_vty_transceive_ret(vty_pt, "show session");
	var Misc_Helpers.ro_charstring lines := f_str_split(sessions_str, "\n");
	var PFCP_Session_Status_List sessions := {};
	for (var integer i := 0; i < lengthof(lines); i := i + 1) {
		var charstring line := lines[i];
		var PFCP_Session_Status st;
		if (not f_parse_session_status(st, line)) {
			continue;
		}
		sessions := sessions & { st };
	}
	log("Sessions: ", sessions);
	return sessions;
}

private function f_vty_get_session_status(TELNETasp_PT vty_pt, PFCP_session s, out PFCP_Session_Status ret) return boolean {
	var PFCP_Session_Status_List sessions := f_vty_get_sessions(vty_pt);
	return f_get_session_status(sessions, s, ret);
}

private function f_get_session_status(PFCP_Session_Status_List sessions, PFCP_session s, out PFCP_Session_Status ret)
return boolean {
	var PFCP_Session_Status_List matches := {};
	for (var integer i := 0; i < lengthof(sessions); i := i + 1) {
		var PFCP_Session_Status st := sessions[i];
		if (st.seid_l != s.up_seid) {
			continue;
		}
		if (st.seid_r != s.cp_seid) {
			continue;
		}
		matches := matches & { st };
	}
	if (lengthof(matches) < 1) {
		log("no session with SEID-l = ", s.up_seid);
		return false;
	}
	if (lengthof(matches) > 1) {
		log("multiple sessions have ", s, ": ", matches);
		return false;
	}
	ret := matches[0];
	return true;
}

private function f_vty_expect_session_status(TELNETasp_PT vty_pt, PFCP_session s, template PFCP_Session_Status expect_st) {
	var PFCP_Session_Status st;
	if (not f_vty_get_session_status(vty_pt, s, st)) {
		log("Session ", s, " not found in VTY session list");
		setverdict(fail, "Session not found in VTY list");
		mtc.stop;
	}
	log("Session ", s, " status: ", st);
	if (not match(st, expect_st)) {
		log("ERROR: Session ", st, " does not match ", expect_st);
		setverdict(fail, "VTY shows unexpected state of PFCP session");
		mtc.stop;
	}

	setverdict(pass);
}

private function f_vty_expect_session_active(TELNETasp_PT vty_pt, PFCP_session s)
{
	f_vty_expect_session_status(vty_pt, s, PFCP_session_active);
	f_vty_expect_gtp_action(vty_pt, s.gtp);
	setverdict(pass);
}

private function f_vty_expect_no_active_sessions(TELNETasp_PT vty_pt) {
	var PFCP_Session_Status_List stl := f_vty_get_sessions(vty_pt);
	var integer active := 0;
	for (var integer i := 0; i < lengthof(stl); i := i + 1) {
		if (match(stl[i], PFCP_session_active)) {
			log("Active session: ", stl[i]);
			active := active + 1;
		}
	}
	if (active > 0) {
		setverdict(fail, "There are still active sessions");
		mtc.stop;
	}
	setverdict(pass);
}

private function f_vty_netinst_cfg(TELNETasp_PT vty_pt, Misc_Helpers.ro_charstring netinst_cmds)
{
	f_vty_enter_config(vty_pt);
	f_vty_transceive(vty_pt, "netinst");
	for (var integer i := 0; i < lengthof(netinst_cmds); i := i + 1) {
		f_vty_transceive(vty_pt, netinst_cmds[i]);
	}
	f_vty_transceive_ret(vty_pt, "end");
}

function f_init_vty(charstring id := "foo") runs on test_CT {
	if (UPFVTY.checkstate("Mapped")) {
		/* skip initialization if already executed once */
		return;
	}
	map(self:UPFVTY, system:UPFVTY);
	f_vty_set_prompts(UPFVTY);
	f_vty_transceive(UPFVTY, "enable");
}

/* global initialization function */
function f_init(float guard_timeout := 30.0) runs on test_CT {
	var integer bssap_idx;

	T_guard.start(guard_timeout);
	activate(as_Tguard());

	f_init_vty("VirtCPF");

	/* Clear out and set up default network instance config */
	f_vty_netinst_cfg(UPFVTY,
			  { "clear",
			    "add access " & mp_netinst_access_ip_1,
			    "add access2 " & mp_netinst_access_ip_2,
			    "add core " & mp_netinst_core_ip_1,
			    "add core2 " & mp_netinst_core_ip_2
			    });
}

friend function f_shutdown_helper() runs on test_CT {
	all component.stop;
	setverdict(pass);
	mtc.stop;
}

private function f_gen_test_hdlr_pars() runs on test_CT return TestHdlrParams {
	var TestHdlrParams pars := valueof(t_def_TestHdlrPars);
	pars.remote_upf_addr := mp_pfcp_ip_upf;
	pars.local_addr := mp_pfcp_ip_local;
	pars.local_node_id := valueof(ts_PFCP_Node_ID_ipv4(f_inet_addr(mp_pfcp_ip_local)));
	return pars;
}

type function void_fn(charstring id) runs on CPF_ConnHdlr;

function f_start_handler_create(TestHdlrParams pars)
runs on test_CT return CPF_ConnHdlr {
	var charstring id := testcasename();
	var CPF_ConnHdlr vc_conn;
	vc_conn := CPF_ConnHdlr.create(id);
	return vc_conn;
}

function f_start_handler_run(CPF_ConnHdlr vc_conn, void_fn fn, TestHdlrParams pars)
runs on test_CT return CPF_ConnHdlr {
	var charstring id := testcasename();
	/* Emit a marker to appear in the SUT's own logging output */
	f_logp(UPFVTY, id & "() start");
	vc_conn.start(f_handler_init(fn, id, pars));
	return vc_conn;
}

function f_start_handler(void_fn fn, template (omit) TestHdlrParams pars_tmpl := omit)
runs on test_CT return CPF_ConnHdlr {
	var TestHdlrParams pars;
	if (isvalue(pars_tmpl)) {
		pars := valueof(pars_tmpl);
	} else {
		pars := valueof(f_gen_test_hdlr_pars());
	}
	return f_start_handler_run(f_start_handler_create(pars), fn, pars);
}

/* first function inside ConnHdlr component; sets g_pars + starts function */
private function f_handler_init(void_fn fn, charstring id, TestHdlrParams pars)
runs on CPF_ConnHdlr {
	f_CPF_ConnHdlr_init(id, pars);
	fn.apply(id);
}

/* Run a PFCP Association procedure */
private function f_assoc_setup() runs on CPF_ConnHdlr {
	PFCP.send(ts_PFCP_Assoc_Setup_Req(g_pars.local_node_id, g_recovery_timestamp));
	PFCP.receive(tr_PFCP_Assoc_Setup_Resp(cause := tr_PFCP_Cause(REQUEST_ACCEPTED)));
}

/* Release a PFCP Association */
private function f_assoc_release() runs on CPF_ConnHdlr {
	PFCP.send(ts_PFCP_Assoc_Release_Req(g_pars.local_node_id));
	PFCP.receive(tr_PFCP_Assoc_Release_Resp(cause := tr_PFCP_Cause(REQUEST_ACCEPTED)));
}

/* Collection of what a test intends to send to osmo-upf */
type record PFCP_Ruleset {
	Create_PDR_list pdr,
	Create_FAR_list far
};

/* Add to r a rule set that does GTP decapsulation (half of encapsulation/decapsulation):
 * Receive GTP on src_iface = ACCESS by a local F-TEID to be chosen by osmo-upf.
 * Dispatch GTP payload as plain IP on dest_iface = CORE. */
private function f_ruleset_add_GTP_decaps(inout PFCP_Ruleset r,
					  integer pdr_id,
					  charstring src_netinst,
					  integer far_id) {
	r.pdr := r.pdr & {
		valueof(
		ts_PFCP_Create_PDR(
			pdr_id,
			ts_PFCP_PDI(
				ACCESS,
				local_F_TEID := ts_PFCP_F_TEID_choose_v4(),
				network_instance := ts_PFCP_Network_Instance(src_netinst)),
			ts_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4),
			far_id
			)
		)
		};
	r.far := r.far & {
		valueof(
		ts_PFCP_Create_FAR(
			far_id,
			ts_PFCP_Apply_Action_FORW,
			valueof(ts_PFCP_Forwarding_Parameters(CORE))
			)
		)
		};
}

/* Add to r a rule set that does GTP encapsulation (half of encapsulation/decapsulation) */
private function f_ruleset_add_GTP_encaps(inout PFCP_Ruleset r,
					  integer pdr_id,
					  charstring ue_addr_v4 := "192.168.23.42",
					  integer far_id,
					  OCT4 remote_teid,
					  OCT4 gtp_dest_addr_v4) {
	r.pdr := r.pdr & {
		valueof(
		ts_PFCP_Create_PDR(
			pdr_id,
			ts_PFCP_PDI(
				CORE,
				ue_addr_v4 := ts_PFCP_UE_IP_Address_v4(f_inet_addr(ue_addr_v4), is_destination := true)
				),
			far_id := far_id
			)
		)
		};
	r.far := r.far & {
		valueof(
		ts_PFCP_Create_FAR(
			far_id,
			ts_PFCP_Apply_Action_FORW,
			valueof(ts_PFCP_Forwarding_Parameters(
				ACCESS,
				ts_PFCP_Outer_Header_Creation_GTP_ipv4(
					remote_teid,
					gtp_dest_addr_v4)
				))
			)
		)
		};
}

/* Add to r a rule set that forwards GTP from one tunnel to another, i.e. one direction of a tunmap */
private function f_ruleset_add_GTP_forw(inout PFCP_Ruleset r,
					integer pdr_id,
					e_PFCP_Src_Iface src_iface,
					charstring src_netinst,
					integer far_id,
					e_PFCP_Dest_Iface dest_iface,
					F_TEID dest_remote_f_teid) {
	r.pdr := r.pdr & {
		valueof(
		ts_PFCP_Create_PDR(
			pdr_id,
			ts_PFCP_PDI(
				src_iface,
				local_F_TEID := ts_PFCP_F_TEID_choose_v4(),
				network_instance := ts_PFCP_Network_Instance(src_netinst)),
			ts_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4),
			far_id
			)
		)
		};
	r.far := r.far & {
		valueof(
		ts_PFCP_Create_FAR(
			far_id,
			ts_PFCP_Apply_Action_FORW,
			valueof(ts_PFCP_Forwarding_Parameters(dest_iface,
					ts_PFCP_Outer_Header_Creation_GTP_ipv4(dest_remote_f_teid.teid,
									       dest_remote_f_teid.ipv4_address)))
			)
		)
		};
}

/* Add to r a DROP rule from src_iface to dest_iface */
private function f_ruleset_add_GTP_drop(inout PFCP_Ruleset r,
					integer pdr_id,
					e_PFCP_Src_Iface src_iface,
					charstring src_netinst,
					integer far_id,
					e_PFCP_Dest_Iface dest_iface) {
	r.pdr := r.pdr & {
		valueof(
		ts_PFCP_Create_PDR(
			pdr_id,
			ts_PFCP_PDI(
				src_iface,
				local_F_TEID := ts_PFCP_F_TEID_choose_v4(),
				network_instance := ts_PFCP_Network_Instance(src_netinst)),
			ts_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4),
			far_id
			)
		)
		};
	r.far := r.far & {
		valueof(
		ts_PFCP_Create_FAR(
			far_id,
			ts_PFCP_Apply_Action_DROP,
			fp := omit
			)
		)
		};
}

private function f_tunmap_upd_far_to_core(GTP_Action gtp) return Update_FAR
{
	return valueof(
		ts_PFCP_Update_FAR(
			gtp.core.tunmap.far_id,
			ts_PFCP_Apply_Action_FORW,
			valueof(ts_PFCP_Update_Forwarding_Parameters(
					ts_PFCP_Destination_Interface(CORE),
					ts_PFCP_Outer_Header_Creation_GTP_ipv4(gtp.core.tunmap.teid_r,
									       f_inet_addr(gtp.core.tunmap.gtp_ip_r))
					)
			       )
			)
		);
}

/* Return two PDR+FAR rulesets that involve a src=CP-Function. Such rulesets are emitted by certain third party CPF, and
 * osmo-upf should ACK the creation but ignore the rules (no-op). This function models rulesets seen in the field, so we
 * can confirm that osmo-upf ACKs and ignores. */
private function f_ruleset_noop() return PFCP_Ruleset
{
	var PFCP_Ruleset r := { {}, {} };
	var integer pdr_id := lengthof(r.pdr) + 1;
	var integer far_id := lengthof(r.far) + 1;

	r.pdr := r.pdr & {
		valueof(
		ts_PFCP_Create_PDR(
			pdr_id,
			ts_PFCP_PDI(
				CP_FUNCTION,
				local_F_TEID := ts_PFCP_F_TEID_choose_v4('17'O)),
			ts_PFCP_Outer_Header_Removal(GTP_U_UDP_IPV4),
			far_id
			)
		)
		};
	r.far := r.far & {
		valueof(
		ts_PFCP_Create_FAR(
			far_id,
			ts_PFCP_Apply_Action_FORW,
			valueof(ts_PFCP_Forwarding_Parameters(ACCESS))
			)
		)
		};

	/* And another one (sic) */
	pdr_id := lengthof(r.pdr) + 1;
	far_id := lengthof(r.far) + 1;

	r.pdr := r.pdr & {
		valueof(
		ts_PFCP_Create_PDR(
			pdr_id,
			ts_PFCP_PDI(
				CP_FUNCTION,
				local_F_TEID := ts_PFCP_F_TEID_choose_v4('2a'O)),
			far_id := far_id
			)
		)
		};
	r.far := r.far & {
		valueof(
		ts_PFCP_Create_FAR(
			far_id,
			ts_PFCP_Apply_Action_FORW,
			valueof(ts_PFCP_Forwarding_Parameters(ACCESS))
			)
		)
		};
	return r;
}

/* Return a rule set that does GTP encapsulation and decapsulation, in both directions. */
private function f_ruleset_tunend(GTP_Action gtp, charstring netinst_access := "access") return PFCP_Ruleset
{
	var PFCP_Ruleset rules := { {}, {} };
	f_ruleset_add_GTP_decaps(rules,
				 pdr_id := gtp.access.pdr_id,
				 src_netinst := netinst_access,
				 far_id := gtp.core.tunend.far_id);
	f_ruleset_add_GTP_encaps(rules,
				 gtp.core.tunend.pdr_id,
				 gtp.core.tunend.ip_l,
				 gtp.access.far_id,
				 gtp.access.teid_r,
				 f_inet_addr(gtp.access.gtp_ip_r));
	return rules;
}

/* Return a rule set that does GTP tunnel forwarding in both directions.
 * If core_gtp_known == true, place full FORW rules in both directions.
 * If core_gtp_known == false, keep the Core side as DROP: this allows testing the usual/realistic case, where upon
 * Session Establishment, the core side PGW has not yet provided the destination GTP F-TEID, which will follow later in
 * a Session Modification. (This test suite has already configured which GTP F-TEID will be used on the core side, but
 * we're omitting it from Session Establishment, until it is time to use it in f_session_mod()). */
private function f_ruleset_tunmap(GTP_Action gtp, boolean core_gtp_known := true,
				  charstring netinst_access := "access",
				  charstring netinst_core := "core") return PFCP_Ruleset
{
	var PFCP_Ruleset rules := { {}, {} };
	/* Access to Core */
	if (core_gtp_known) {
		f_ruleset_add_GTP_forw(rules,
				       pdr_id := gtp.access.pdr_id,
				       src_iface := ACCESS,
				       src_netinst := netinst_access,
				       far_id := gtp.core.tunmap.far_id,
				       dest_iface := CORE,
				       dest_remote_f_teid := valueof(ts_PFCP_F_TEID_ipv4(gtp.core.tunmap.teid_r,
											 f_inet_addr(gtp.core.tunmap.gtp_ip_r))));
	} else {
		/* The Core remote GTP will follow in a Session Modification, for now set Core->Access to DROP */
		f_ruleset_add_GTP_drop(rules,
				       pdr_id := gtp.access.pdr_id,
				       src_iface := ACCESS,
				       src_netinst := netinst_access,
				       far_id := gtp.core.tunmap.far_id,
				       dest_iface := CORE);
	}
	/* Core to Access */
	f_ruleset_add_GTP_forw(rules,
			       pdr_id := gtp.core.tunmap.pdr_id,
			       src_iface := CORE,
			       src_netinst := netinst_core,
			       far_id := gtp.access.far_id,
			       dest_iface := ACCESS,
			       dest_remote_f_teid := valueof(ts_PFCP_F_TEID_ipv4(gtp.access.teid_r,
										 f_inet_addr(gtp.access.gtp_ip_r))));
	return rules;
}

/* From a PFCP Session Establishment Response, retrieve the F_TEID returned in the Created PDR IE for the given PDR Id
 */
private function f_get_created_local_f_teid(PDU_PFCP sess_est_resp, integer pdr_id) return F_TEID
{
	for (var integer i := 0;
	     i < lengthof(sess_est_resp.message_body.pfcp_session_establishment_response.created_PDR_list);
	     i := i + 1) {
		var Created_PDR cpdr := sess_est_resp.message_body.pfcp_session_establishment_response.created_PDR_list[i];
		if (oct2int(cpdr.grouped_ie.pdr_id.rule_id) != pdr_id) {
			continue;
		}
		log("osmo-upf has chosen local F-TEID: PDR-" & int2str(pdr_id) & " = ", cpdr.grouped_ie.local_F_TEID);
		return cpdr.grouped_ie.local_F_TEID;
	}
	setverdict(fail, "PDR Id " & int2str(pdr_id) & " not found in PFCP message");
	mtc.stop;
}

/* Run a PFCP Session Establishment procedure */
private function f_session_est(inout PFCP_session s, PFCP_Ruleset rules) runs on CPF_ConnHdlr
{
	log("f_session_est: rules = ", rules);
	PFCP.send(ts_PFCP_Session_Est_Req(ts_PFCP_Node_ID_ipv4(f_inet_addr(g_pars.local_addr)),
					  ts_PFCP_F_SEID_ipv4(f_inet_addr(g_pars.local_addr), s.cp_seid),
					  rules.pdr, rules.far));

	var PDU_PFCP pfcp;
	PFCP.receive(tr_PFCP_Session_Est_Resp(seid := s.cp_seid)) -> value pfcp;
	s.up_seid := pfcp.message_body.pfcp_session_establishment_response.UP_F_SEID.seid;
	s.gtp.seid_l := s.up_seid;

	var F_TEID access_local_f_teid := f_get_created_local_f_teid(pfcp, s.gtp.access.pdr_id);
	s.gtp.access.gtp_ip_l := f_inet_ntoa(access_local_f_teid.ipv4_address);
	s.gtp.access.teid_l := access_local_f_teid.teid;

	if (ischosen(s.gtp.core.tunmap)) {
		var F_TEID core_local_f_teid := f_get_created_local_f_teid(pfcp, s.gtp.core.tunmap.pdr_id);
		s.gtp.core.tunmap.gtp_ip_l := f_inet_ntoa(core_local_f_teid.ipv4_address);
		s.gtp.core.tunmap.teid_l := core_local_f_teid.teid;
	}
	log("established PFCP session: ", s);
}

/* Run a PFCP Session Modification procedure */
private function f_session_mod(inout PFCP_session s) runs on CPF_ConnHdlr
{
	PFCP.send(ts_PFCP_Session_Mod_Req(s.up_seid, f_tunmap_upd_far_to_core(s.gtp)));
	PFCP.receive(tr_PFCP_Session_Mod_Resp(s.cp_seid));
	log("modified PFCP session: ", s);
}

private function f_create_PFCP_session_tunend() runs on CPF_ConnHdlr return PFCP_session
{
	var PFCP_session s := {
		up_seid := -,
		cp_seid := f_next_seid(),
		gtp := {
			kind := "tunend",
			access := {
				pdr_id := 1,
				/* gtp_ip_l and teid_l will be returned by Session Establishment Response, Created PDR */
				gtp_ip_l := "",
				teid_l := '00000000'O,
				far_id := 2,
				gtp_ip_r := "127.0.0.2",
				teid_r := f_next_remote_teid()
			},
			core := {
				tunend := {
					pdr_id := 2,
					ip_l := f_next_ue_addr(),
					far_id := 1
				}
			},
			pfcp_peer := g_pars.local_addr,
			seid_l := '0000000000000000'O
			/* seid_l will be returned by Session Establishment Response */
		}
	};
	return s;
}

private function f_create_PFCP_session_tunmap() runs on CPF_ConnHdlr return PFCP_session
{
	var PFCP_session s := {
		up_seid := -,
		cp_seid := f_next_seid(),
		gtp := {
			kind := "tunmap",
			access := {
				pdr_id := 1,
				/* gtp_ip_l and teid_l will be returned by Session Establishment Response, Created PDR */
				gtp_ip_l := "",
				teid_l := '00000000'O,
				far_id := 2,
				gtp_ip_r := "127.0.0.2",
				teid_r := f_next_remote_teid()
			},
			core := {
				tunmap := {
					pdr_id := 2,
					/* gtp_ip_l and teid_l will be returned by Session Establishment Response, Created PDR */
					gtp_ip_l := "",
					teid_l := '00000000'O,
					far_id := 1,
					gtp_ip_r := "127.0.0.3",
					teid_r := f_next_remote_teid()
				}
			},
			pfcp_peer := g_pars.local_addr,
			seid_l := '0000000000000000'O
			/* seid_l will be returned by Session Establishment Response */
		}
	};
	return s;
}

/* Do a PFCP Session Establishment with default values (see f_create_PFCP_session_tunend()) */
private function f_session_est_tunend() runs on CPF_ConnHdlr return PFCP_session
{
	var PFCP_session s := f_create_PFCP_session_tunend();
	f_session_est(s, f_ruleset_tunend(s.gtp));
	return s;
}

private function f_session_del(PFCP_session s) runs on CPF_ConnHdlr {
	PFCP.send(ts_PFCP_Session_Del_Req(s.up_seid));
	PFCP.receive(tr_PFCP_Session_Del_Resp(s.cp_seid));
}

private function f_tc_assoc(charstring id) runs on CPF_ConnHdlr {
	f_assoc_setup();
	f_assoc_release();
	setverdict(pass);
}

/* Verify that the CPF can send a Node-ID of the IPv4 type */
testcase TC_assoc_node_id_v4() runs on test_CT {
	var CPF_ConnHdlr vc_conn;

	f_init(guard_timeout := 5.0);
	vc_conn := f_start_handler(refers(f_tc_assoc));
	vc_conn.done;
	f_shutdown_helper();
}

/* Verify that the CPF can send a Node-ID of the FQDN type */
testcase TC_assoc_node_id_fqdn() runs on test_CT {
	var CPF_ConnHdlr vc_conn;
	var TestHdlrParams pars := f_gen_test_hdlr_pars();

	pars.local_node_id := valueof(ts_PFCP_Node_ID_fqdn("\7example\3com"));

	f_init(guard_timeout := 5.0);
	vc_conn := f_start_handler(refers(f_tc_assoc), pars);
	vc_conn.done;
	f_shutdown_helper();
}

/* Verify PFCP Session Establishment and Deletion */
private function f_tc_session_est_tunend(charstring id) runs on CPF_ConnHdlr {
	f_assoc_setup();
	var PFCP_session s := f_session_est_tunend();
	f_sleep(1.0);
	f_vty_expect_session_active(UPFVTY, s);
	f_session_del(s);
	f_vty_expect_no_active_sessions(UPFVTY);
	f_vty_expect_no_gtp_actions(UPFVTY);
	f_assoc_release();
	setverdict(pass);
}
testcase TC_session_est_tunend() runs on test_CT {
	var CPF_ConnHdlr vc_conn;

	f_init(guard_timeout := 15.0);
	vc_conn := f_start_handler(refers(f_tc_session_est_tunend));
	vc_conn.done;
	f_shutdown_helper();
}

/* Verify that releasing a PFCP Association also releases all its sessions and GTP actions. */
private function f_tc_session_term_by_assoc_rel(charstring id) runs on CPF_ConnHdlr {
	f_assoc_setup();
	var PFCP_session s := f_session_est_tunend();
	f_sleep(1.0);
	f_vty_expect_session_active(UPFVTY, s);
	f_assoc_release();
	f_vty_expect_no_active_sessions(UPFVTY);
	f_vty_expect_no_gtp_actions(UPFVTY);
	setverdict(pass);
}
testcase TC_session_term_by_assoc_rel() runs on test_CT {
	var CPF_ConnHdlr vc_conn;

	f_init(guard_timeout := 15.0);
	vc_conn := f_start_handler(refers(f_tc_session_term_by_assoc_rel));
	vc_conn.done;
	f_shutdown_helper();
}

/* Verify that PFCP Sessions with a src-interface other than ACCESS or CORE are ACKed by osmo-upf but have no effect. */
private function f_tc_session_est_noop(charstring id) runs on CPF_ConnHdlr {
	f_assoc_setup();
	var PFCP_session s := f_create_PFCP_session_tunend();
	f_session_est(s, f_ruleset_noop());

	f_sleep(1.0);
	f_vty_expect_session_status(UPFVTY, s, PFCP_session_inactive);

	f_session_del(s);
	f_vty_expect_no_active_sessions(UPFVTY);
	f_vty_expect_no_gtp_actions(UPFVTY);
	f_assoc_release();
	setverdict(pass);
}
testcase TC_session_est_noop() runs on test_CT {
	var CPF_ConnHdlr vc_conn;

	f_init(guard_timeout := 15.0);
	vc_conn := f_start_handler(refers(f_tc_session_est_noop));
	vc_conn.done;
	f_shutdown_helper();
}

/* Verify that the Network Instance IE in Create PDR chooses the right local address for a tunmap session */
private function f_tc_session_est_tunmap(charstring id) runs on CPF_ConnHdlr {
	f_assoc_setup();
	var PFCP_session s := f_create_PFCP_session_tunmap();
	f_session_est(s, f_ruleset_tunmap(s.gtp));
	f_sleep(1.0);
	f_vty_expect_session_active(UPFVTY, s);
	f_session_del(s);
	f_vty_expect_no_active_sessions(UPFVTY);
	f_vty_expect_no_gtp_actions(UPFVTY);
	f_assoc_release();
	setverdict(pass);
}
testcase TC_session_est_tunmap() runs on test_CT {
	var CPF_ConnHdlr vc_conn;

	f_init(guard_timeout := 15.0);

	vc_conn := f_start_handler(refers(f_tc_session_est_tunmap));
	vc_conn.done;
	f_shutdown_helper();
}

/* Set up a tunmap session with a partial Session Establishment, followed by a Session Modification to complete it. */
private function f_session_est_mod_tunmap(charstring netinst_access, charstring expect_gtp_ip_access,
					  charstring netinst_core, charstring expect_gtp_ip_core) runs on CPF_ConnHdlr {
	f_assoc_setup();
	var PFCP_session s := f_create_PFCP_session_tunmap();
	f_session_est(s, f_ruleset_tunmap(s.gtp, core_gtp_known := false,
					  netinst_access := netinst_access, netinst_core := netinst_core));
	/* The locally chosen GTP IP addresses where osmo-upf receives GTP traffic were chosen by netinst_access /
	 * netinst_core and are returned in s.gtp.access.gtp_ip_l / s.gtp.core.tunmap.gtp_ip_l. Verify that the netinst
	 * names have returned their matching IP addresses. */
	if (s.gtp.access.gtp_ip_l != expect_gtp_ip_access) {
		setverdict(fail, "Network Instance '" & netinst_access & "' should have yielded GTP IP " &
			   expect_gtp_ip_access & " but osmo-upf chose " & s.gtp.access.gtp_ip_l);
		mtc.stop;
	}
	if (s.gtp.core.tunmap.gtp_ip_l != expect_gtp_ip_core) {
		setverdict(fail, "Network Instance '" & netinst_core & "' should have yielded GTP IP " &
			   expect_gtp_ip_core & " but osmo-upf chose " & s.gtp.core.tunmap.gtp_ip_l);
		mtc.stop;
	}

	f_sleep(1.0);
	f_vty_expect_session_status(UPFVTY, s, PFCP_session_inactive);

	f_session_mod(s);
	f_sleep(1.0);
	f_vty_expect_session_active(UPFVTY, s);
	f_session_del(s);
	f_vty_expect_no_active_sessions(UPFVTY);
	f_vty_expect_no_gtp_actions(UPFVTY);
	f_assoc_release();
	setverdict(pass);
}
/* Run f_session_est_mod_tunmap() with the first Network Instances */
private function f_tc_session_est_mod_tunmap(charstring id) runs on CPF_ConnHdlr {
	f_session_est_mod_tunmap("access", mp_netinst_access_ip_1, "core", mp_netinst_core_ip_1);
}
/* Run f_session_est_mod_tunmap() with the second Network Instances */
private function f_tc_session_est_mod_tunmap2(charstring id) runs on CPF_ConnHdlr {
	f_session_est_mod_tunmap("access2", mp_netinst_access_ip_2, "core2", mp_netinst_core_ip_2);
}
testcase TC_session_est_mod_tunmap() runs on test_CT {
	var CPF_ConnHdlr vc_conn;

	f_init(guard_timeout := 15.0);

	vc_conn := f_start_handler(refers(f_tc_session_est_mod_tunmap));
	vc_conn.done;
	f_shutdown_helper();
}
testcase TC_session_est_mod_tunmap2() runs on test_CT {
	var CPF_ConnHdlr vc_conn;

	f_init(guard_timeout := 15.0);

	vc_conn := f_start_handler(refers(f_tc_session_est_mod_tunmap2));
	vc_conn.done;
	f_shutdown_helper();
}

control {
	execute( TC_assoc_node_id_v4() );
	execute( TC_assoc_node_id_fqdn() );
	execute( TC_session_est_tunend() );
	execute( TC_session_term_by_assoc_rel() );
	execute( TC_session_est_noop() );
	execute( TC_session_est_tunmap() );
	execute( TC_session_est_mod_tunmap() );
	execute( TC_session_est_mod_tunmap2() );
}

}