module SIP_Tests {

/* osmo-sip-connector test suite in TTCN-3
 * (C) 2018-2019 Harald Welte <laforge@gnumonks.org>
 * 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
 */

import from General_Types all;
import from Osmocom_Types all;

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

import from TELNETasp_PortType all;
import from Osmocom_VTY_Functions all;

import from MNCC_Emulation all;
import from MNCC_Types all;

import from SDP_Types all;

import from SIP_Emulation all;
import from SIPmsg_Types all;
import from SIP_Templates all;

modulepar {
	charstring mp_local_host := "127.0.0.2";
	charstring mp_osmosip_host := "127.0.0.1";
	integer mp_osmosip_port_ctrl := -1; /* RFU */
	charstring mp_mncc := "/tmp/mncc";
}

type component test_CT extends CTRL_Adapter_CT {
	var MNCC_Emulation_CT vc_MNCC;
	var SIP_Emulation_CT vc_SIP;

	port TELNETasp_PT SIPVTY;
}

type component ConnHdlr extends SIP_ConnHdlr, MNCC_ConnHdlr {
	var ConnHdlrPars g_pars;
	timer g_Tguard;
}

type record ConnHdlrPars {
	float t_guard,
	CallPars g_cp optional
}

type record CallPars {
	boolean is_mo,
	charstring calling,
	charstring called,

	uint32_t mncc_call_id optional,

	CallParsComputed comp optional
}

type record CallParsComputed {
	CallidString sip_call_id,
	SipAddr sip_url_ext,
	SipAddr sip_url_gsm,
	charstring sip_body,
	integer sip_seq_nr
}

private template (value) CallPars t_CallPars(boolean is_mo) := {
	is_mo := is_mo,
	calling := "12345",
	called := "98766",
	mncc_call_id := omit,
	comp := omit
}

private function f_CallPars_compute(inout CallPars cp) {
	if (cp.is_mo) {
		cp.comp.sip_url_ext := valueof(ts_SipAddr(cp.called, mp_local_host, 5060));
		cp.comp.sip_url_gsm := valueof(ts_SipAddr(cp.calling, mp_osmosip_host, 5060));
		cp.mncc_call_id := f_rnd_int(429496725);
	} else {
		cp.comp.sip_url_ext := valueof(ts_SipAddr(cp.calling, mp_local_host, 5060));
		cp.comp.sip_url_gsm := valueof(ts_SipAddr(cp.called, mp_osmosip_host, 5060));
		cp.comp.sip_call_id := hex2str(f_rnd_hexstring(15));
	}
	cp.comp.sip_seq_nr := f_rnd_int(4294967295);
	cp.comp.sip_body := "";
}


function f_init_mncc(charstring id) runs on test_CT {
	id := id & "-MNCC";
	var MnccOps ops := {
		create_cb := refers(MNCC_Emulation.ExpectedCreateCallback),
		unitdata_cb := refers(MNCC_Emulation.DummyUnitdataCallback)
	};

	vc_MNCC := MNCC_Emulation_CT.create(id);
	map(vc_MNCC:MNCC, system:MNCC_CODEC_PT);
	vc_MNCC.start(MNCC_Emulation.main(ops, id, mp_mncc, true));
}

function f_init() runs on test_CT {
	//f_ipa_ctrl_start(mp_osmosip_host, mp_osmosip_port_ctrl);
	f_init_mncc("SIP_Test");
	log("end of f_init_mncc");
	f_init_sip(vc_SIP, "SIP_Test");
	log("end of f_init_sip");

	map(self:SIPVTY, system:SIPVTY);
	f_vty_set_prompts(SIPVTY);
	f_vty_transceive(SIPVTY, "enable");
	log("end of f_init");
}

type function void_fn(charstring id) runs on ConnHdlr;

function f_start_handler(void_fn fn, ConnHdlrPars pars)
runs on test_CT return ConnHdlr {
	var ConnHdlr vc_conn;
	var charstring id := testcasename();

	vc_conn := ConnHdlr.create(id);

	connect(vc_conn:SIP, vc_SIP:CLIENT);
	connect(vc_conn:SIP_PROC, vc_SIP:CLIENT_PROC);

	connect(vc_conn:MNCC, vc_MNCC:MNCC_CLIENT);
	connect(vc_conn:MNCC_PROC, vc_MNCC:MNCC_PROC);

	vc_conn.start(f_handler_init(fn, id, pars));
	return vc_conn;
}

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

private function f_handler_init(void_fn fn, charstring id, ConnHdlrPars pars)
runs on ConnHdlr {
	g_pars := pars;
	g_Tguard.start(pars.t_guard);
	activate(as_Tguard());

	/* call the user-supied test case function */
	fn.apply(id);
}


template (value) ConnHdlrPars t_Pars := {
	t_guard := 30.0,
	g_cp := omit
}

/* Establish a mobile terminated call described in 'cp' */
function f_establish_mt(inout CallPars cp) runs on ConnHdlr {
	var template SipAddr sip_addr_gsm := tr_SipAddr_from_val(cp.comp.sip_url_gsm);
	var template SipAddr sip_addr_ext := tr_SipAddr_from_val(cp.comp.sip_url_ext);
	var PDU_SIP_Request sip_req;
	var MNCC_PDU mncc;

	/* Ask MNCC_Emulation to "expect" a call to the given called number */
	f_create_mncc_expect(cp.called);

	/* OSC <- SIP: A party sends SIP invite for a MT-call into OSC */
	SIP.send(ts_SIP_INVITE(cp.comp.sip_call_id, cp.comp.sip_url_ext, cp.comp.sip_url_gsm,
				cp.comp.sip_seq_nr, cp.comp.sip_body));
	/* MSC <- OSC: OSC generates MNCC_SETUP_REQ from INVITE */
	MNCC.receive(tr_MNCC_SETUP_req) -> value mncc {
		cp.mncc_call_id := mncc.u.signal.callref;
		}
	/* OSC -> SIP */
	SIP.receive(tr_SIP_Response(cp.comp.sip_call_id, sip_addr_ext, sip_addr_gsm, *,
				    "INVITE", 100, ?, "Trying", *));

	/* MSC -> OSC: After MS sends CALL CONF in response to SETUP */
	MNCC.send(ts_MNCC_CALL_CONF_ind(cp.mncc_call_id));
	/* MSC <- OSC: OSC asks MSC to create RTP socket */
	MNCC.receive(tr_MNCC_RTP_CREATE(cp.mncc_call_id));
	MNCC.send(ts_MNCC_RTP_CREATE(cp.mncc_call_id));

	/* MSC -> OSC: After MS is ringing and sent CC ALERTING */
	MNCC.send(ts_MNCC_ALERT_ind(cp.mncc_call_id));
	SIP.clear;
	SIP.receive(tr_SIP_Response(cp.comp.sip_call_id, sip_addr_ext, sip_addr_gsm, *,
				    "INVITE", 180, ?, "Ringing", *));

	/* MSC -> OSC: After MT user has picked up and sent CC CONNECT */
	MNCC.send(ts_MNCC_SETUP_CNF(cp.mncc_call_id));

	SIP.clear;
	interleave {
	/* MSC <- OSC: OSC asks MSC to connect its RTP stream to remote end */
	[] MNCC.receive(tr_MNCC_RTP_CONNECT(cp.mncc_call_id)) {}
	/* OSC -> SIP: OSC confirms call establishment to SIP side */
	[] SIP.receive(tr_SIP_Response(cp.comp.sip_call_id, sip_addr_ext, sip_addr_gsm, ?,
					"INVITE", 200, ?, "OK", ?)) {}
	}
	/* OSC <- SIP: SIP world acknowledges "200 OK" */
	SIP.send(ts_SIP_ACK(cp.comp.sip_call_id, cp.comp.sip_url_ext, cp.comp.sip_url_gsm,
			    cp.comp.sip_seq_nr, omit));
	/* MSC <- OSC: OSC sends SETUP COMPL to MNCC (which triggers CC CONNECT ACK */
	MNCC.receive(tr_MNCC_SETUP_COMPL_req(cp.mncc_call_id));
}

/* Establish a mobile originated call described in 'cp' */
function f_establish_mo(inout CallPars cp) runs on ConnHdlr {
	var MNCC_number dst := valueof(ts_MNCC_number(cp.called, GSM48_TON_UNKNOWN));
	var MNCC_number src := valueof(ts_MNCC_number(cp.calling, GSM48_TON_UNKNOWN));
	var template SipAddr sip_addr_gsm := tr_SipAddr_from_val(cp.comp.sip_url_gsm);
	var template SipAddr sip_addr_ext := tr_SipAddr_from_val(cp.comp.sip_url_ext);
	var PDU_SIP_Request sip_req;
	var integer seq_nr;

	f_create_sip_expect(cp.comp.sip_url_ext.addr.nameAddr.addrSpec);

	/* MSC -> OSC: MSC sends SETUP.ind after CC SETUP was received from MS */
	MNCC.send(ts_MNCC_SETUP_ind(cp.mncc_call_id, dst, src, "262420123456789"));
	/* MSC <- OSC: Create GSM side RTP socket */
	MNCC.receive(tr_MNCC_RTP_CREATE(cp.mncc_call_id)) {
		var MNCC_PDU mncc := valueof(ts_MNCC_RTP_CREATE(cp.mncc_call_id));
		mncc.u.rtp.payload_msg_type := oct2int('0300'O);
		MNCC.send(mncc);	/* FIXME: port/ip */
		}
	/* OSC -> SIP: Send INVITE with GSM side IP/Port in SDP */
	SIP.receive(tr_SIP_INVITE(?, sip_addr_gsm, sip_addr_ext, ?, ?)) -> value sip_req {
		cp.comp.sip_url_gsm.params := sip_req.msgHeader.fromField.fromParams;
		cp.comp.sip_call_id := sip_req.msgHeader.callId.callid;
		seq_nr := sip_req.msgHeader.cSeq.seqNumber;
		}
	/* OSC <- SIP: Notify call is proceeding */
	SIP.send(ts_SIP_Response(cp.comp.sip_call_id, cp.comp.sip_url_gsm, cp.comp.sip_url_ext,
				 "INVITE", 100, seq_nr, "Trying", sip_req.msgHeader.via));
	/* MSC <- OSC: "100 Trying" translated to MNCC_CALL_PROC_REQ */
	MNCC.receive(tr_MNCC_CALL_PROC_req(cp.mncc_call_id));

	/* OSC <- SIP: SIP-terminated user is ringing now */
	SIP.send(ts_SIP_Response(cp.comp.sip_call_id, cp.comp.sip_url_gsm, cp.comp.sip_url_ext,
				 "INVITE", 180, seq_nr, "Ringing", sip_req.msgHeader.via));

	/* MSC <- OSC: "180 Ringing" translated to MNCC_ALERT_REQ */
	MNCC.receive(tr_MNCC_ALERT_req(cp.mncc_call_id)) {}

	/* OSC <- SIP: SIP-terminated user has accepted the call */
	SIP.send(ts_SIP_Response(cp.comp.sip_call_id, cp.comp.sip_url_gsm, cp.comp.sip_url_ext,
				 "INVITE", 200, seq_nr, "OK", sip_req.msgHeader.via,
				 cp.comp.sip_body));
	MNCC.receive(tr_MNCC_RTP_CONNECT(cp.mncc_call_id));
	/* MSC <- OSC: "200 OK" translated to MNCC_SETUP_RSP */
	MNCC.receive(tr_MNCC_SETUP_rsp(cp.mncc_call_id));

	/* MSC -> OSC: CC CONNECT ACK was received from MS */
	MNCC.send(ts_MNCC_SETUP_COMPL_ind(cp.mncc_call_id));
	/* OSC -> SIP: Acknowledge the call */
	SIP.receive(tr_SIP_ACK(cp.comp.sip_call_id, sip_addr_gsm, sip_addr_ext, ?, omit));
}

/* Release call from the mobile side */
function f_release_mobile(inout CallPars cp) runs on ConnHdlr {
	var template SipAddr sip_addr_gsm := tr_SipAddr_from_val(cp.comp.sip_url_gsm);
	var template SipAddr sip_addr_ext := tr_SipAddr_from_val(cp.comp.sip_url_ext);
	var PDU_SIP_Request sip_req;
	SIP.clear;
	/* MSC -> OSC: Simulate a CC DISCONNET from the MT user */
	MNCC.send(ts_MNCC_DISC_ind(cp.mncc_call_id, ts_MNCC_cause(0)));
	/* OSC -> SIP: Expect BYE from OSC to SIP side */
	SIP.receive(tr_SIP_BYE(cp.comp.sip_call_id, sip_addr_gsm, sip_addr_ext, ?, *)) -> value sip_req {
		cp.comp.sip_url_gsm.params := sip_req.msgHeader.fromField.fromParams;
	}
	/* OSC <- SIP: Acknowledge the BYE */
	SIP.send(ts_SIP_Response(cp.comp.sip_call_id, cp.comp.sip_url_gsm, cp.comp.sip_url_ext,
					 "BYE", 200, sip_req.msgHeader.cSeq.seqNumber, "OK",
					 sip_req.msgHeader.via));
	/* MSC <- OSC: Send REL_REQ to MSC, triggers CC RELEASE REQ to MS */
	MNCC.receive(tr_MNCC_REL_req(cp.mncc_call_id)); // CAUSE?
	/* MSC -> OSC: MS has responded with CC CLEAR COMPL, triggers MNCC_REL_CNF */
	MNCC.send(ts_MNCC_REL_cnf(cp.mncc_call_id, ts_MNCC_cause(0)));
}

/* Release call from the SIP side */
function f_release_sip(inout CallPars cp) runs on ConnHdlr {
	var template SipAddr sip_addr_gsm := tr_SipAddr_from_val(cp.comp.sip_url_gsm);
	var template SipAddr sip_addr_ext := tr_SipAddr_from_val(cp.comp.sip_url_ext);
	/* OSC <- SIP: SIP-side sends a BYE to OSC */
	SIP.send(ts_SIP_BYE(cp.comp.sip_call_id, cp.comp.sip_url_ext, cp.comp.sip_url_gsm,
			    cp.comp.sip_seq_nr, omit));
	/* MSC <- OSC: Expect OSC to cause MNCC Disconnect Request */
	MNCC.receive(tr_MNCC_DISC_req(cp.mncc_call_id));
	/* MSC -> OSC: Indicate GSM side release */
	MNCC.send(ts_MNCC_REL_ind(cp.mncc_call_id, ts_MNCC_cause(0)));
	/* OSC -> SIP: Confirmation to SIP side */
	SIP.receive(tr_SIP_Response(cp.comp.sip_call_id, sip_addr_ext, sip_addr_gsm, *,
				    "BYE", 200, cp.comp.sip_seq_nr, "OK", omit));
}

/* Successful MT Call, which is subsequently released by GSM side */
private function f_TC_mt_success_rel_gsm(charstring id) runs on ConnHdlr {
	var CallPars cp := valueof(t_CallPars(false));
	f_CallPars_compute(cp);
	cp.comp.sip_body := "v=0\r\no=Osmocom 0 0 IN IP4 0.0.0.0\r\ns=GSM Call\r\nc=IN IP4 0.0.0.0\r\nt=0 0\r\nm=audio 0 RTP/AVP 0\r\na=rtpmap:0 GSM/8000\r\n";
	f_sleep(3.0)

	f_establish_mt(cp);
	/* now call is fully established */
	f_sleep(2.0);
	f_release_mobile(cp);
	setverdict(pass);
}
testcase TC_mt_success_rel_gsm() runs on test_CT {
	var ConnHdlrPars pars;
	var ConnHdlr vc_conn;
	f_init();
	pars := valueof(t_Pars);
	vc_conn := f_start_handler(refers(f_TC_mt_success_rel_gsm), pars);
	vc_conn.done;
}

/* Successful MT Call, which is subsequently released by SIP side */
private function f_TC_mt_success_rel_sip(charstring id) runs on ConnHdlr {
	var CallPars cp := valueof(t_CallPars(false));
	f_CallPars_compute(cp);
	cp.comp.sip_body := "v=0\r\no=Osmocom 0 0 IN IP4 0.0.0.0\r\ns=GSM Call\r\nc=IN IP4 0.0.0.0\r\nt=0 0\r\nm=audio 0 RTP/AVP 0\r\na=rtpmap:0 GSM/8000\r\n";
	f_sleep(3.0)

	f_establish_mt(cp);
	/* now call is fully established */
	f_sleep(2.0);
	f_release_sip(cp);
	setverdict(pass);
}
testcase TC_mt_success_rel_sip() runs on test_CT {
	var ConnHdlrPars pars;
	var ConnHdlr vc_conn;
	f_init();
	pars := valueof(t_Pars);
	vc_conn := f_start_handler(refers(f_TC_mt_success_rel_sip), pars);
	vc_conn.done;
}


/* Successful MO Call, which is subsequently released by GSM side */
private function f_TC_mo_success_rel_gsm(charstring id) runs on ConnHdlr {
	var CallPars cp := valueof(t_CallPars(true));
	f_CallPars_compute(cp);
	cp.comp.sip_body := "v=0\r\no=Osmocom 0 0 IN IP4 0.0.0.0\r\ns=GSM Call\r\nc=IN IP4 0.0.0.0\r\nt=0 0\r\nm=audio 0 RTP/AVP 0\r\na=rtpmap:0 GSM/8000\r\n";
	f_sleep(3.0)

	f_establish_mo(cp);
	/* now call is fully established */
	f_sleep(2.0);
	f_release_mobile(cp);
	setverdict(pass);
}
testcase TC_mo_success_rel_gsm() runs on test_CT {
	var ConnHdlrPars pars;
	var ConnHdlr vc_conn;
	f_init();
	pars := valueof(t_Pars);
	vc_conn := f_start_handler(refers(f_TC_mo_success_rel_gsm), pars);
	vc_conn.done;
}

/* Successful MO Call, which is subsequently released by SIP side */
private function f_TC_mo_success_rel_sip(charstring id) runs on ConnHdlr {
	var CallPars cp := valueof(t_CallPars(true));
	f_CallPars_compute(cp);
	cp.comp.sip_body := "v=0\r\no=Osmocom 0 0 IN IP4 0.0.0.0\r\ns=GSM Call\r\nc=IN IP4 0.0.0.0\r\nt=0 0\r\nm=audio 0 RTP/AVP 0\r\na=rtpmap:0 GSM/8000\r\n";
	f_sleep(3.0)

	f_establish_mo(cp);
	/* now call is fully established */
	f_sleep(2.0);
	f_release_sip(cp);
	setverdict(pass);
}
testcase TC_mo_success_rel_sip() runs on test_CT {
	var ConnHdlrPars pars;
	var ConnHdlr vc_conn;
	f_init();
	pars := valueof(t_Pars);
	vc_conn := f_start_handler(refers(f_TC_mo_success_rel_sip), pars);
	vc_conn.done;
}

/* SETUP followed by DISC results in lingering B-leg (OS#3518)*/
private function f_TC_mo_setup_disc_late_rtp(charstring id) runs on ConnHdlr {
	var CallPars cp := valueof(t_CallPars(true));
	f_CallPars_compute(cp);
	cp.comp.sip_body := "v=0\r\no=Osmocom 0 0 IN IP4 0.0.0.0\r\ns=GSM Call\r\nc=IN IP4 0.0.0.0\r\nt=0 0\r\nm=audio 0 RTP/AVP 0\r\na=rtpmap:0 GSM/8000\r\n";
	f_sleep(3.0);

	var MNCC_number dst := valueof(ts_MNCC_number(cp.called, GSM48_TON_UNKNOWN));
	var MNCC_number src := valueof(ts_MNCC_number(cp.calling, GSM48_TON_UNKNOWN));
	var template SipAddr sip_addr_gsm := tr_SipAddr_from_val(cp.comp.sip_url_gsm);
	var template SipAddr sip_addr_ext := tr_SipAddr_from_val(cp.comp.sip_url_ext);

	f_create_sip_expect(cp.comp.sip_url_ext.addr.nameAddr.addrSpec);

	/* MSC -> OSC: MSC sends SETUP.ind after CC SETUP was received from MS */
	MNCC.send(ts_MNCC_SETUP_ind(cp.mncc_call_id, dst, src, "262420123456789"));

	/* MSC -> OSC: Simulate a CC DISCONNET from the MT user *before* responding to the RTP_CREATE */
	MNCC.send(ts_MNCC_DISC_ind(cp.mncc_call_id, ts_MNCC_cause(0)));

	/* MSC <- OSC: Create GSM side RTP socket (too late) */
	MNCC.receive(tr_MNCC_RTP_CREATE(cp.mncc_call_id)) {
		var MNCC_PDU mncc := valueof(ts_MNCC_RTP_CREATE(cp.mncc_call_id));
		mncc.u.rtp.payload_msg_type := oct2int('0300'O);
		MNCC.send(mncc);	/* FIXME: port/ip */
		}

	/* OSC -> SIP: We should never receive INVITE */
	timer T := 10.0;
	T.start;
	alt {
	[] SIP.receive(tr_SIP_INVITE(?, sip_addr_gsm, sip_addr_ext, ?, ?)) {
		setverdict(fail, "Received unexpected INVITE");
		}
	[] T.timeout {
		setverdict(pass);
		}
	}
}
testcase TC_mo_setup_disc_late_rtp() runs on test_CT {
	var ConnHdlrPars pars;
	var ConnHdlr vc_conn;
	f_init();
	pars := valueof(t_Pars);
	vc_conn := f_start_handler(refers(f_TC_mo_setup_disc_late_rtp), pars);
	vc_conn.done;
}

control {
	execute( TC_mt_success_rel_gsm() );
	execute( TC_mt_success_rel_sip() );
	execute( TC_mo_success_rel_gsm() );
	execute( TC_mo_success_rel_sip() );
	execute( TC_mo_setup_disc_late_rtp() );
}



}
