/* 5GC (55G Core) test suite in TTCN-3
 * (C) 2025 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * All rights reserved.
 * Author: Pau Espin Pedrol <pespin@sysmocom.de>
 *
 * 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 C5G_Tests {

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

import from Osmocom_Types all;
import from GSM_Types all;

import from DNS_Helpers all;

import from Milenage_Functions all;

import from NGAP_PDU_Descriptions all;
import from NGAP_IEs all;
import from NGAP_PDU_Contents all;
import from NGAP_Constants all;

import from NGAP_Types all;
import from NGAP_Pixits all;
import from NGAP_Templates all;
import from NGAP_Functions all;
import from NGAP_Emulation all;

import from NAS_CommonTypeDefs all;
import from NAS_CommonTemplates all;
import from NG_NAS_Common all;
import from NG_NAS_TypeDefs all;
import from NG_NAS_MsgContainers all;
import from NG_NAS_Templates all;

import from NG_NAS_Osmo_Templates all;
import from NG_NAS_Functions all;
import from NG_CryptoFunctions all;

import from GTPv1U_Emulation all;

import from ConnHdlr all;

modulepar {
	/* NG-C interface */
	charstring mp_5gc_ngap_ip := "127.0.0.1";
	integer mp_5gc_ngap_port := 38412;
	charstring mp_local_ngap_ip := "127.0.0.1";
	integer mp_local_ngap_port := 50000;
	GsmMcc mp_mcc := '999'H;
	GsmMnc mp_mnc := '70'H;
	HEX15n mp_imsi := '999700000000000'H;
	octetstring mp_usim_key := '762a2206fe0b4151ace403c86a11e479'O;
	octetstring mp_usim_opc := '3c6e0b8a9c15224a8228b9a98ca1531d'O;
	uint24_t mp_tac := 1;
	charstring mp_apn := "internet";
	charstring mp_local_gtpu_ip := "127.0.0.20";
	charstring mp_ping_hostname := "10.45.0.1";

	charstring mp_run_prog_log_path := "/tmp";
	charstring mp_run_prog_as_user := "osmocom";
}

template (value) RunProgParams ts_RunProgParams(integer imsi_suffix) :=
{
	run_as_user := mp_run_prog_as_user,
	log_path_prefix := mp_run_prog_log_path,
	tun_netns_name := "tun" & int2str(imsi_suffix),
	tun_dev_name := "tun" & int2str(imsi_suffix),
	ping_hostname := mp_ping_hostname
}

template (value) PDUSessionParams ts_PDUSessionParams(template (value) PDUSessionID id,
						      template (value) RunProgParams run_prog_pars,
						      template (value) OCT4 ran_gtpu_teid) :=
{
	id := id,
	run_prog_pars := run_prog_pars,
	ran_gtpu_ip := mp_local_gtpu_ip,
	ran_gtpu_teid := ran_gtpu_teid,
	cn_gtpu_ip := omit,
	cn_gtpu_teid := omit,
	qos_rules := omit,
	ue_ip := omit
};

template (value) UeParams ts_UeParams(integer idx) :=
{
	idx := idx,
	imsi := f_concat_pad(lengthof(mp_imsi), substr(mp_imsi, 0, lengthof(mp_imsi) - 6), idx),
	imeisv := f_rnd_imeisv(),
	usim_key := mp_usim_key,
	usim_opc := mp_usim_opc,
	apn := mp_apn,
	ran_id := idx,
	amf_id := omit,
	guti := omit,
	pti := '00'O,
	sess_pars := ts_PDUSessionParams(id := 1,
					 run_prog_pars := ts_RunProgParams(idx),
					 ran_gtpu_teid := int2oct(idx + 1, 4))
}

type component MTC_CT {
	/* S1 intreface of emulated ENBs */
	var NGRANParams g_ngran_pars[NUM_NGRAN];
	var NGAP_Emulation_CT vc_NGAP[NUM_NGRAN];
	port NGAP_PT NGAP_UNIT[NUM_NGRAN];
	port NGAPEM_PROC_PT NGAP_PROC[NUM_NGRAN];

	var GTPv1U_Emulation_CT vc_GTP1U;
	port GTP1UEM_PT TEID0;

	timer g_Tguard := 30.0;
}

/* send incoming unit data messages (like reset) to global NGAP_UNIT port */
private function NGapForwardUnitdataCallback(NGAP_PDU msg)
runs on NGAP_Emulation_CT return template NGAP_PDU {
	NGAP_UNIT.send(msg);
	return omit;
}

private function f_init_one_ngran(integer num := 0) runs on MTC_CT {
	var charstring id := testcasename() & "-NGAP" & int2str(num);
	var NGAPOps ops := {
		create_cb := refers(NGAP_Emulation.ExpectedCreateCallback),
		unitdata_cb := refers(NGapForwardUnitdataCallback)
	};
	var NGAP_conn_parameters pars := {
		remote_ip := mp_5gc_ngap_ip,
		remote_sctp_port := mp_5gc_ngap_port,
		local_ip := mp_local_ngap_ip,
		local_sctp_port := mp_local_ngap_port + num,
		role := NG_NAS_ROLE_UE
	};
	var PLMNIdentity plmn_id := f_enc_mcc_mnc(mp_mcc, mp_mnc);
	var NGRANParams ngran_pars := {
		global_ngran_id := valueof(m_globalRANNodeID_globalGNB_ID(m_ie_globalGnbId(plmn_id, int2bit(num, 22)))),
		cell_identity := { nR_CGI := valueof(m_nR_CGI(plmn_id, int2bit(num, 36))) },
		supported_ta_list := {
			{
				tAC := int2oct(mp_tac, 3),
				broadcastPLMNList := {
					valueof(m_ie_broadcastPLMNItem(plmn_id, { m_sliceSupportItem(m_s_NSSAI('01'O)) }))
				},
				iE_Extensions := omit
			}
		}
	};

	g_ngran_pars[num] := ngran_pars;
	vc_NGAP[num] := NGAP_Emulation_CT.create(id);
	map(vc_NGAP[num]:NGAP, system:NGAP_CODEC_PT);
	connect(vc_NGAP[num]:NGAP_PROC, self:NGAP_PROC[num]);
	connect(vc_NGAP[num]:NGAP_UNIT, self:NGAP_UNIT[num]);
	vc_NGAP[num].start(NGAP_Emulation.main(ops, pars, id));
	NGAP_UNIT[num].receive(NGAPEM_Event:{up_down:=NGAPEM_EVENT_UP});
}

private function f_init_ngap(integer imsi_suffix := 0) runs on MTC_CT {
	var integer i;
	for (i := 0; i < NUM_NGRAN; i := i+1) {
		f_init_one_ngran(i);
	}
}

private function f_init_gtp1u() runs on MTC_CT {
	var Gtp1uEmulationCfg cfg := {
		gtpu_bind_ip := omit,  /* using gtpu daemon */
		gtpu_bind_port := omit,  /* using gtpu daemon */
		use_gtpu_daemon := true
	};

	vc_GTP1U := GTPv1U_Emulation_CT.create("GTP1U_EM");
	map(vc_GTP1U:GTP1U, system:GTP1U);
	connect(vc_GTP1U:TEID0, self:TEID0);
	vc_GTP1U.start(GTPv1U_Emulation.main(cfg));
}

private function f_init(integer imsi_suffix := 0, float t_guard := 30.0) runs on MTC_CT {
	/* start guard timer and activate it as default */
	g_Tguard.start(t_guard);
	activate(as_Tguard());

	f_init_gtp1u();
	f_init_ngap(imsi_suffix);
}

/* generate parameters for a connection handler */
private function f_init_pars(integer ue_idx := 0)
runs on MTC_CT return ConnHdlrPars {
	var ConnHdlrPars pars := {
		ngran_pars := g_ngran_pars,
		ue_pars := valueof(ts_UeParams(ue_idx)),
		c5g_idx := 0,
		kset_id := valueof(cs_NAS_KeySetIdentifier_lv(tsc_NasKsi_NoKey, '0'B))
	};
	return pars;
}

/* start a connection handler with given parameters */
private function f_start_handler_with_pars(void_fn fn, ConnHdlrPars pars, integer ngap_idx := 0)
runs on MTC_CT return ConnHdlr {
	var ConnHdlr vc_conn;
	var charstring id := testcasename() & int2str(ngap_idx);

	vc_conn := ConnHdlr.create(id);
	/* NGAP part */
	connect(vc_conn:NGAP, vc_NGAP[ngap_idx]:NGAP_CLIENT);
	connect(vc_conn:NGAP_PROC, vc_NGAP[ngap_idx]:NGAP_PROC);

	/* GTPv1U */
	connect(vc_conn:GTP1U[0], vc_GTP1U:CLIENT);
	connect(vc_conn:GTP1U_PROC[0], vc_GTP1U:CLIENT_PROC);

	/* We cannot use vc_conn.start(f_init_handler(fn, id, pars)); as we cannot have
	 * a stand-alone 'derefers()' call, see https://www.eclipse.org/forums/index.php/t/1091364/ */
	vc_conn.start(f_init_handler(fn, pars));
	return vc_conn;
}

/* altstep for the global guard timer */
private altstep as_Tguard()runs on MTC_CT {
	[] g_Tguard.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Tguard timeout");
	}
}

private function f_init_handler(void_fn fn, ConnHdlrPars pars) runs on ConnHdlr {
	/* make parameters available via component variable */
	g_pars := pars;
	fn.apply();
}

private function f_ngap_setup(integer idx := 0, template (omit) NGAP_IEs.Cause cause := omit) runs on MTC_CT {
	var template (present) NGAP_IEs.Cause exp_cause := ?;
	var boolean exp_fail := false;
	timer T := 5.0;
	var template (value) NGAP_PDU tx_pdu;
	var template (present) NGAP_PDU exp_pdu;
	var NGAP_PDU rx_pdu;

	tx_pdu := m_ngap_initMsg(m_n2_NGSetupRequest(g_ngran_pars[idx].global_ngran_id,
						     g_ngran_pars[idx].supported_ta_list,
						     v32));

	if (not istemplatekind(cause, "omit")) {
		exp_fail := true;
		exp_cause := cause;
	}

	if (exp_fail) {
		exp_pdu := mw_ngap_unsuccMsg((mw_n2_NGSetupFailure(exp_cause),
					      f_mw_n2_NGSetupFailure(exp_cause, p_timeToWait := ?)));
	} else {
		exp_pdu := mw_ngap_succMsg(mw_n2_NGSetupResponse);
	}

	NGAP_UNIT[idx].send(tx_pdu);
	T.start;
	alt {
	[] NGAP_UNIT[idx].receive(exp_pdu) {
		setverdict(pass);
		}
	[] NGAP_UNIT[idx].receive(NGAP_PDU:?) -> value rx_pdu {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Received unexpected NGAP ", rx_pdu, " vs exp ", exp_pdu));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Timeout waiting for NGAP Setup result");
		}
	}
}

/* NG Setup procedure to 5GC using a Global gNB ID containing unknown/foreign PLMN.
 * Related: https://github.com/open5gs/open5gs/issues/3544 */
testcase TC_ng_setup_unknown_global_gnb_id_plmn() runs on MTC_CT {
	f_init();
	g_ngran_pars[0].global_ngran_id.globalGNB_ID.pLMNIdentity := '62F224'O;
	f_ngap_setup(0);
}

/* Unsuccessful NG Setup procedure to AMF (wrong PLMN) */
testcase TC_ng_setup_wrong_tac() runs on MTC_CT {
	f_init();
	g_ngran_pars[0].supported_ta_list[0].broadcastPLMNList[0].pLMNIdentity := '62F224'O;
	f_ngap_setup(0, {misc:=unknown_PLMN_or_SNPN});
}

/* NG Setup procedure to 5GC using a correct Global gNB ID. */
testcase TC_ng_setup() runs on MTC_CT {
	f_init();
	f_ngap_setup(0);
}

private function f_TC_register() runs on ConnHdlr {
	f_register();
	f_deregister();
}
testcase TC_ng_register() runs on MTC_CT {
	f_init();
	f_ngap_setup(0);

	var ConnHdlrPars pars := f_init_pars(ue_idx := 0);
	var ConnHdlr vc_conn;
	vc_conn := f_start_handler_with_pars(refers(f_TC_register), pars);
	vc_conn.done;
}

private function f_TC_periodic_registration_updating() runs on ConnHdlr {
	f_register();
	f_periodic_register_update();
	f_deregister();
}
testcase TC_periodic_registration_updating() runs on MTC_CT {
	f_init();
	f_ngap_setup(0);

	var ConnHdlrPars pars := f_init_pars(ue_idx := 0);
	var ConnHdlr vc_conn;
	vc_conn := f_start_handler_with_pars(refers(f_TC_periodic_registration_updating), pars);
	vc_conn.done;
}

/* 3GPP TS 23.502 4.2.6 AN Release */
private function f_TC_ue_context_release_no_pdu_session() runs on ConnHdlr {
	f_register();
	f_ue_context_release();
}
testcase TC_ue_context_release_no_pdu_session() runs on MTC_CT {
	f_init();
	f_ngap_setup(0);

	var ConnHdlrPars pars := f_init_pars(ue_idx := 0);
	var ConnHdlr vc_conn;
	vc_conn := f_start_handler_with_pars(refers(f_TC_ue_context_release_no_pdu_session), pars);
	vc_conn.done;
}
private function f_TC_ue_context_release_with_pdu_session() runs on ConnHdlr {
	f_register();
	f_pdu_sess_establish(false);
	f_sleep(1.0);
	f_ue_context_release();
}
testcase TC_ue_context_release_with_pdu_session() runs on MTC_CT {
	f_init();
	f_ngap_setup(0);

	var ConnHdlrPars pars := f_init_pars(ue_idx := 0);
	var ConnHdlr vc_conn;
	vc_conn := f_start_handler_with_pars(refers(f_TC_ue_context_release_with_pdu_session), pars);
	vc_conn.done;
}

private function f_TC_pdu_sess_modification() runs on ConnHdlr {
	f_register();
	f_pdu_sess_establish(false);
	f_sleep(1.0);
	f_pdu_sess_modify();
	f_pdu_sess_release();
	f_deregister();
}
testcase TC_pdu_sess_modification() runs on MTC_CT {
	f_init();
	f_ngap_setup(0);

	var ConnHdlrPars pars := f_init_pars(ue_idx := 0);
	var ConnHdlr vc_conn;
	vc_conn := f_start_handler_with_pars(refers(f_TC_pdu_sess_modification), pars);
	vc_conn.done;
}

private function f_TC_register_ping4() runs on ConnHdlr {
	f_register();
	f_pdu_sess_establish();
	f_sleep(1.0);
	f_ping4(g_pars.ue_pars.sess_pars.run_prog_pars.ping_hostname);
	f_pdu_sess_release();
	f_deregister();
}
testcase TC_ng_register_ping4() runs on MTC_CT {
	f_init();
	f_ngap_setup(0);

	var ConnHdlrPars pars := f_init_pars(ue_idx := 0);
	var ConnHdlr vc_conn;
	vc_conn := f_start_handler_with_pars(refers(f_TC_register_ping4), pars);
	vc_conn.done;
}
testcase TC_ng_register_ping4_256() runs on MTC_CT {
	var ConnHdlr vc_conn[256];
	var integer i;

	f_init();
	f_ngap_setup(0);

	for (i := 0; i < sizeof(vc_conn); i := i + 1) {
		var ConnHdlrPars pars := f_init_pars(ue_idx := i);
		vc_conn[i] := f_start_handler_with_pars(refers(f_TC_register_ping4), pars);
	}

	for (i := 0; i < sizeof(vc_conn); i := i + 1) {
		vc_conn[i].done;
	}
}


control {
	execute( TC_ng_setup() );
	execute( TC_ng_setup_unknown_global_gnb_id_plmn() );
	execute( TC_ng_setup_wrong_tac() );
	execute( TC_ng_register() );
	execute( TC_periodic_registration_updating() );
	execute( TC_ue_context_release_no_pdu_session() );
	execute( TC_ue_context_release_with_pdu_session() );
	/* Remove "is nightly" check after open5gs > 2.7.6 is released with
	 * https://github.com/open5gs/open5gs/commit/59391d8d5ce23835456d4cebbd23c52a18d8a276 */
	if (Misc_Helpers.f_osmo_repo_is("nightly")) {
		execute( TC_pdu_sess_modification() );
	}
	execute( TC_ng_register_ping4() );
	execute( TC_ng_register_ping4_256() );
}

/* TODO:
 * - Emergency call (PDU Sess Establish "Request Type"="Emergency Request")
 * - Handover from/to EPS vs 5GC
 * - Handover from/to 3GPP vs non-3GPP (PDU Sess Establish "Request Type"="Existing PDU Session" and RAT Type change)
 * - PDU Session Type IPv6
 * - PDU Session Type IPv4v6
 * - VoLTE:
 * -- " If the UE requested P-CSCF discovery then the message shall also include the P-CSCF IP address(es) as determined by the SMF and as described in clause 5.16.3.4 of TS 23.50"
 * - Fill in PCO during PDU Sess Establish Req.
 * - "If the PDU Session being established was requested to be an always-on PDU Session, the SMF shall indicate whether the request is accepted by including an Always-on PDU Session Granted indication in the PDU Session Establishment Accept message"
 */

}
