module BSC_Tests_LCLS {

/* Integration Tests for OsmoBSC
 * (C) 2018 by 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
 *
 * This test suite tests OsmoBSC while emulating both multiple BTS + MS as
 * well as the MSC. See README for more details.
 *
 * There are test cases that run in so-called 'handler mode' and test cases
 * that run directly on top of the BSSAP and RSL CodecPorts.  The "handler mode"
 * tests abstract the multiplexing/demultiplexing of multiple SCCP connections
 * and/or RSL channels and are hence suitable for higher-level test cases, while
 * the "raw" tests directly on top of the CodecPorts are more suitable for lower-
 * level testing.
 */

import from General_Types all;
import from Osmocom_Types all;
import from GSM_Types all;
import from IPL4asp_Types all;

import from BSSAP_Types all;
import from RAN_Adapter all;
import from BSSAP_CodecPort all;
import from BSSMAP_Templates all;
import from IPA_Emulation all;
import from IPA_CodecPort all;
import from IPA_Types all;
import from RSL_Types all;
import from RSL_Emulation all;
import from MGCP_Types all;
import from MGCP_Emulation all;
import from MGCP_Templates all;
import from SDP_Types all;
import from SDP_Templates all;
import from Native_Functions all;

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

import from Osmocom_VTY_Functions all;
import from TELNETasp_PortType all;

import from MobileL3_CommonIE_Types all;
import from MobileL3_Types all;
import from L3_Templates all;
import from GSM_RR_Types all;

import from BSSMAP_Templates all;
import from RAN_Emulation all;

import from MSC_ConnectionHandler all;
import from BSC_Tests all;

/* The philosophy of this testsuite is to re-use as much as possible the existing components
 * and functions that we have in BSC_Tests and its dependencies.  However, as opposed to those
 * normal BSC tests, we here have to run *two* ConnHdlr and synchronize activity between them.
 *
 * We do this by adding some special-purpose ports between the main test component running the
 * test case [lcls_]test_CT and the per-connection [LCLS_]MSC_ConnHdlr.
 */


/* take test_CT from BSC_Tests and extend it with LCLS specific bits */
type component lcls_test_CT extends test_CT {
	/* Component references */
	var LCLS_MSC_ConnHdlr vc_CONN_A;
	var LCLS_MSC_ConnHdlr vc_CONN_B;
	/* Ports to the two call legs */
	port LCLS_InterComp_PT CONN_A;
	port LCLS_InterComp_PT CONN_B;
}

/* take MSC_ConnHdlr and extend it with LCLS specific bits */
type component LCLS_MSC_ConnHdlr extends MSC_ConnHdlr {
	/* Port back to the controlling lcls_test_CT */
	port LCLS_InterComp_PT MASTER;
}

/* port type between lcls_test_CT and LCLS_MSC_ConnHdlr */
type port LCLS_InterComp_PT message {
		/* BSSAP from BSSA_ConnHdlr */
	inout	PDU_BSSAP, RAN_Conn_Prim, PDU_DTAP_MO, PDU_DTAP_MT,
		/* RSL from RSL_DchanHdlr */
		RSLDC_ChanRqd, RSL_Message,
		/* MGCP from MGCP_ConnHdlr */
		MgcpCommand, MgcpResponse,
		LclsCompSync;
} with { extension "internal" };

type enumerated LclsCompSync {
	/* ConnHdlr signals to master component that assignment has completed */
	LCLS_COMP_SYNC_ASS_COMPL
}


/* forward messages between the RSL/MGCP/BSSAP Emulation and the master component */
private altstep as_lcls_conn_hdlr_proxy() runs on LCLS_MSC_ConnHdlr {
	var PDU_BSSAP bssap;
	var RAN_Conn_Prim bssap_p;
	var PDU_DTAP_MO dtap_mo;
	var PDU_DTAP_MT dtap_mt;
	var MgcpCommand mgcp_cmd;
	var MgcpResponse mgcp_rsp;
	var RSL_Message rsl_msg;
	/* from ConnHdlr to master process */
	[] RAN_CONN.receive(PDU_BSSAP:?) -> value bssap { MASTER.send(bssap); }
	[] RAN_CONN.receive(RAN_Conn_Prim:?) -> value bssap_p { MASTER.send(bssap_p); }
	[] RAN_CONN.receive(PDU_DTAP_MO:?) -> value dtap_mo { MASTER.send(dtap_mo); }
	[] RAN_CONN.receive(PDU_DTAP_MT:?) -> value dtap_mt { MASTER.send(dtap_mt); }
	[] MGCP.receive(MgcpCommand:?) -> value mgcp_cmd { MASTER.send(mgcp_cmd); }
	[] MGCP.receive(MgcpResponse:?) -> value mgcp_rsp { MASTER.send(mgcp_rsp); }
	[] RSL.receive(RSL_Message:?) -> value rsl_msg { MASTER.send(rsl_msg); }
	/* from master process to ConnHdlr */
	[] MASTER.receive(PDU_BSSAP:?) -> value bssap { RAN_CONN.send(bssap); }
	[] MASTER.receive(RAN_Conn_Prim:?) -> value bssap_p { RAN_CONN.send(bssap_p); }
	[] MASTER.receive(PDU_DTAP_MO:?) -> value dtap_mo { RAN_CONN.send(dtap_mo); }
	[] MASTER.receive(PDU_DTAP_MT:?) -> value dtap_mt { RAN_CONN.send(dtap_mt); }
	[] MASTER.receive(MgcpCommand:?) -> value mgcp_cmd { MGCP.send(mgcp_cmd); }
	[] MASTER.receive(MgcpResponse:?) -> value mgcp_rsp { MGCP.send(mgcp_rsp); }
	[] MASTER.receive(RSL_Message:?) -> value rsl_msg { RSL.send(rsl_msg); }
}


private function f_lcls_connhdlr_main(charstring id) runs on LCLS_MSC_ConnHdlr {
	/* 1) establish the connection between RSL and BSSAP side */
	var PDU_BSSAP ass_req := f_gen_ass_req();

	var template PDU_BSSAP ass_compl := f_gen_exp_compl();
	ass_req.pdu.bssmap.assignmentRequest.codecList := g_pars.ass_codec_list;
	ass_req.pdu.bssmap.assignmentRequest.channelType :=
				f_BSSMAP_chtype_from_codec(g_pars.ass_codec_list.codecElements[0]);

	f_establish_fully(ass_req, ass_compl);

	/* 2) notify master that assignment has completed */
	MASTER.send(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);

	/* 3) proxy packets between master component and various ports */
	while (true) {
		as_lcls_conn_hdlr_proxy();
	}
}

type function lcls_void_fn(charstring id) runs on LCLS_MSC_ConnHdlr;

/* first function inside ConnHdlr component; sets g_pars + starts function */
private function f_handler_init(lcls_void_fn fn, charstring id, template (omit) TestHdlrParams pars := omit)
runs on LCLS_MSC_ConnHdlr {
	if (isvalue(pars)) {
		g_pars := valueof(pars);
	}
	fn.apply(id);
}

/* helper function to create and connect a MSC_ConnHdlr component */
/* FIXME: Why can't we use BSC_Tests.f_connect_andler() ?!?
 * TODO: allow connecting to TRX1..N, not only TRX0 */
private function f_connect_handler(inout LCLS_MSC_ConnHdlr vc_conn, integer bssap_idx := 0, integer mgwpool_idx := 0) runs on lcls_test_CT {
	connect(vc_conn:RAN, g_bssap[bssap_idx].vc_RAN:PROC);
	connect(vc_conn:RSL, bts[0][0].rsl.vc_RSL:CLIENT_PT);
	connect(vc_conn:RSL_PROC, bts[0][0].rsl.vc_RSL:RSL_PROC);
	if (isvalue(bts[1][0])) {
		connect(vc_conn:RSL1, bts[1][0].rsl.vc_RSL:CLIENT_PT);
		connect(vc_conn:RSL1_PROC, bts[1][0].rsl.vc_RSL:RSL_PROC);
	}
	connect(vc_conn:RAN_CONN, g_bssap[bssap_idx].vc_RAN:CLIENT);
	connect(vc_conn:MGCP_PROC, vc_MGCP[mgwpool_idx]:MGCP_PROC);
	connect(vc_conn:MGCP, vc_MGCP[mgwpool_idx]:MGCP_CLIENT);
}

/* function creating the two ConnHdlrs, connecting them + starting them */
private function f_lcls_test_init(TestHdlrParams pars_a, TestHdlrParams pars_b) runs on lcls_test_CT {
	var charstring id_a := testcasename() & "-A";
	var charstring id_b := testcasename() & "-B";

	pars_b.imsi := '002029876543210'H;
	pars_b.media_nr := 2;

	/* create and connect the two ConnHandlers */
	vc_CONN_A := LCLS_MSC_ConnHdlr.create(id_a);
	f_connect_handler(vc_CONN_A);
	connect(vc_CONN_A:MASTER, self:CONN_A);

	vc_CONN_B := LCLS_MSC_ConnHdlr.create(id_b);
	f_connect_handler(vc_CONN_B);
	connect(vc_CONN_B:MASTER, self:CONN_B);

	/* start the two components */
	vc_CONN_A.start(f_handler_init(refers(f_lcls_connhdlr_main), id_a, pars_a));
	f_sleep(3.0);
	vc_CONN_B.start(f_handler_init(refers(f_lcls_connhdlr_main), id_b, pars_b));
}

private function f_lcls_test_fini() runs on lcls_test_CT {
	vc_CONN_A.stop;
	vc_CONN_B.stop;
}

/* ignore some messages which we're not interested in evaluating (yet) */
private altstep as_ignore() runs on lcls_test_CT {
	[] CONN_A.receive(tr_DLCX) { repeat; }
	[] CONN_B.receive(tr_DLCX) { repeat; }
}

/* fail if any notify is being received */
private altstep as_fail_on_lcls_notify() runs on lcls_test_CT
{
	[] CONN_A.receive(tr_BSSMAP_LclsNotification(?, *)) {
		setverdict(fail, "Unexpected BSSMAP LCLS Notification");
	}
	[] CONN_B.receive(tr_BSSMAP_LclsNotification(?, *)) {
		setverdict(fail, "Unexpected BSSMAP LCLS Notification");
	}
}

private function f_wait_fail_notify() runs on lcls_test_CT
{
	timer T := 3.0;
	T.start;
	alt {
	[] as_fail_on_lcls_notify();
	[] T.timeout { }
	}
}

private function f_lcls_init(boolean bts_mode := false, integer nr_bts := 1) runs on lcls_test_CT
{
	var default d;

	d := activate(as_ignore());
	f_init(nr_bts, true);
	f_sleep(1.0);

	f_init_vty();
	if (bts_mode == true) {
		f_vty_config(BSCVTY, "msc", "lcls-mode bts-loop");
	} else {
		f_vty_config(BSCVTY, "msc", "lcls-mode mgw-loop");
	}
}


/* Send an ASSIGNMENT REQ with LCLS GCR only, without LCLS CFG or CSC */
testcase TC_lcls_gcr_only() runs on lcls_test_CT {
	var TestHdlrParams pars := f_gen_test_hdlr_pars();
	var MSC_ConnHdlr vc_conn;

	f_lcls_init();

	pars.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	/* Expect LCLS status to be not reported, as no LCLS config was signalled */
	pars.lcls.exp_sts := omit;

	f_lcls_test_init(pars, pars);
	CONN_A.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	CONN_B.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	f_wait_fail_notify();
	f_lcls_test_fini();
}

private function f_tc_lcls_recv_ls_exp_mgcp() runs on lcls_test_CT {
	var MgcpCommand mgcp_cmd;

	interleave {
	[] CONN_A.receive(tr_BSSMAP_LclsNotificationSts(LCLS_STS_not_yet_ls));
	[] CONN_A.receive(tr_BSSMAP_LclsNotificationSts(LCLS_STS_locally_switched));
	[] CONN_B.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	[] CONN_A.receive(tr_MDCX) -> value mgcp_cmd {
		CONN_A.send(f_build_mdcx_rsp(mgcp_cmd));
		}
	/* not needed, as this MDCX is still handled within MSC_ConnectionHandler
	[] CONN_B.receive(tr_MDCX) -> value mgcp_cmd {
		CONN_B.send(f_build_mdcx_rsp(mgcp_cmd));
		}
	*/
	}
}

private function f_tc_lcls_ack_rsl_mdcx(RSL_Message rsl_msg, boolean send_on_a) runs on lcls_test_CT {
	var boolean fixme_unused;
	var RSL_IE_Body ie;
	var RslChannelNr chan_nr;
	var uint16_t conn_id;
	var uint7_t rtp_pt := 0;
	var HostName host;
	var PortNumber port_num;

	if (f_rsl_find_ie(rsl_msg, RSL_IE_CHAN_NR, ie) == true) {
		chan_nr := ie.chan_nr;
	} else {
		log("Unable to find chan# in ", rsl_msg);
	}

        fixme_unused := f_rsl_find_ie(rsl_msg, RSL_IE_IPAC_CONN_ID, ie);
        conn_id := ie.ipa_conn_id;

        /* mandatory fields */
	fixme_unused := f_rsl_find_ie(rsl_msg, RSL_IE_IPAC_REMOTE_IP, ie);
	host := f_inet_ntoa(ie.ipa_remote_ip);

        fixme_unused := f_rsl_find_ie(rsl_msg, RSL_IE_IPAC_REMOTE_PORT, ie);
	port_num := ie.ipa_remote_port;
	log("LCLS IPA MDCX for lchan ", chan_nr, " connection ID ", conn_id, " host ", host, ":", port_num);

        /* optional */
	if (f_rsl_find_ie(rsl_msg, RSL_IE_IPAC_RTP_PAYLOAD, ie)) {
		rtp_pt := ie.ipa_rtp_pt;
	}

	if (send_on_a == true) {
		CONN_A.send(ts_RSL_IPA_MDCX_ACK(chan_nr, conn_id, f_inet_addr(host), port_num, rtp_pt));
	} else {
		CONN_B.send(ts_RSL_IPA_MDCX_ACK(chan_nr, conn_id, f_inet_addr(host), port_num, rtp_pt));
	}
}

private function f_tc_lcls_recv_ls_exp_rsl() runs on lcls_test_CT {
	var RSL_Message rsl_msg;
	interleave {
		[] CONN_A.receive(tr_BSSMAP_LclsNotificationSts(LCLS_STS_not_yet_ls)) {}
		[] CONN_A.receive(tr_BSSMAP_LclsNotificationSts(LCLS_STS_locally_switched)) {}
		[] CONN_B.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL) {}
		[] CONN_A.receive(tr_RSL_IPA_MDCX(?, ?)) -> value rsl_msg {
			f_tc_lcls_ack_rsl_mdcx(rsl_msg, true)
		}
	}
}

private function f_tc_lcls_gcr_bway_connect(boolean hr, boolean bts_mode := false) runs on lcls_test_CT {
	var TestHdlrParams pars_a := f_gen_test_hdlr_pars();
	var TestHdlrParams pars_b;
	var MSC_ConnHdlr vc_conn;

	f_lcls_init(bts_mode);

	if (hr == true) {
		pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecHR}));
	} else {
		pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	}
	pars_a.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars_a.lcls.cfg := LCLS_CFG_both_way;
	pars_a.lcls.csc := LCLS_CSC_connect;
        pars_a.lcls.adjust_cx_exp := not bts_mode;
	pars_b := pars_a;

	/* first call is not possible to be LS (no second leg yet) */
	pars_a.lcls.exp_sts := LCLS_STS_not_possible_ls;
	/* second call should then reuslt in LS */
	pars_b.lcls.exp_sts := LCLS_STS_locally_switched;

	f_lcls_test_init(pars_a, pars_b);

	CONN_A.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);

	if (bts_mode == true) {
		f_tc_lcls_recv_ls_exp_rsl();
	} else {
		f_tc_lcls_recv_ls_exp_mgcp();
	}

	f_lcls_test_fini();
}

/* Send an ASSIGNMENT REQ with LCLS GCR+CFG+CSC; expect connect both-way (full rate)*/
testcase TC_lcls_gcr_bway_connect() runs on lcls_test_CT {
	 f_tc_lcls_gcr_bway_connect(false)
}

/* Send an ASSIGNMENT REQ with LCLS GCR+CFG+CSC; expect connect both-way (half rate) */
testcase TC_lcls_gcr_bway_connect_hr() runs on lcls_test_CT {
	f_tc_lcls_gcr_bway_connect(true)
}

/* BTS-loop: send an ASSIGNMENT REQ with LCLS GCR+CFG+CSC; expect connect both-way (full rate)*/
testcase TC_lcls_bts_gcr_bway_connect() runs on lcls_test_CT {
	f_tc_lcls_gcr_bway_connect(false, true)
}

/* BTS-loop: send an ASSIGNMENT REQ with LCLS GCR+CFG+CSC; expect connect both-way (half rate) */
testcase TC_lcls_bts_gcr_bway_connect_hr() runs on lcls_test_CT {
	f_tc_lcls_gcr_bway_connect(true, true)
}

/* Unless explicitly enabled, osmo-bsc will avoid LCLSs when the codecs or rates
 * of both legs are different */
testcase TC_lcls_gcr_bway_codec_mismatch() runs on lcls_test_CT {
	var TestHdlrParams pars_a := f_gen_test_hdlr_pars();
	var TestHdlrParams pars_b;
	var MSC_ConnHdlr vc_conn;
	var MgcpCommand mgcp_cmd;

	f_lcls_init();

	/* First call leg uses full rate */
	pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars_a.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars_a.lcls.cfg := LCLS_CFG_both_way;
	pars_a.lcls.csc := LCLS_CSC_connect;

	/* The second call leg uses half-rate */
	pars_b := pars_a;
	pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecHR}));

	/* first call is not possible to be LS (no second leg yet) */
	pars_a.lcls.exp_sts := LCLS_STS_not_possible_ls;

	/* second call is also not possible to be LS (codec/rate does not match) */
	pars_b.lcls.exp_sts := LCLS_STS_not_yet_ls;
	f_lcls_test_init(pars_a, pars_b);

	interleave {
	[] CONN_A.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	[] CONN_B.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	[] CONN_A.receive(tr_BSSMAP_LclsNotificationSts(LCLS_STS_not_yet_ls));
	}

	f_lcls_test_fini();
}

/* Send an ASSIGNMENT REQ with LCLS CFG+CSC enabling LCLS but GCR doesn't match! */
testcase TC_lcls_gcr_nomatch_bway_connect() runs on lcls_test_CT {
	var TestHdlrParams pars_a := f_gen_test_hdlr_pars();
	var TestHdlrParams pars_b;
	var MSC_ConnHdlr vc_conn;
	var MgcpCommand mgcp_cmd;

	f_lcls_init();

	pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars_a.lcls.cfg := LCLS_CFG_both_way;
	pars_a.lcls.csc := LCLS_CSC_connect;
	pars_a.lcls.exp_sts := LCLS_STS_not_possible_ls;

	pars_b := pars_a;

	/* first call is not possible to be LS (no second leg yet) */
	pars_a.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars_b.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090b'O));

	f_lcls_test_init(pars_a, pars_b);
	CONN_A.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	CONN_B.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	f_wait_fail_notify();
	f_lcls_test_fini();
}

/* check for the cases where LCLS is not possible due to some reason */
private function f_lcls_not_yet_ls() runs on lcls_test_CT {
	CONN_A.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	CONN_B.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	CONN_A.receive(tr_BSSMAP_LclsNotificationSts(LCLS_STS_not_yet_ls));
}

/* Send an ASSIGNMENT REQ with LCLS GCR+CFG+CSC; expect no connect */
testcase TC_lcls_gcr_bway_dont_connect() runs on lcls_test_CT {
	var TestHdlrParams pars_a := f_gen_test_hdlr_pars();
	var TestHdlrParams pars_b;
	var MSC_ConnHdlr vc_conn;

	f_lcls_init();

	pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars_a.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars_a.lcls.cfg := LCLS_CFG_both_way;
	pars_a.lcls.csc := LCLS_CSC_do_not_connect;
	pars_b := pars_a;

	/* first call is not possible to be LS (no second leg yet) */
	pars_a.lcls.exp_sts := LCLS_STS_not_possible_ls;
	/* Expect LCLS is *NOT* established */
	pars_b.lcls.exp_sts := LCLS_STS_not_yet_ls;

	f_lcls_test_init(pars_a, pars_b);
	f_lcls_not_yet_ls();
	f_wait_fail_notify();
	f_lcls_test_fini();
}

/* Send an ASSIGNMENT REQ with LCLS GCR+CFG+CSC; expect no connect */
testcase TC_lcls_gcr_unsuppported_cfg() runs on lcls_test_CT {
	var TestHdlrParams pars := f_gen_test_hdlr_pars();
	var MSC_ConnHdlr vc_conn;

	f_lcls_init();

	pars.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars.lcls.cfg := LCLS_CFG_both_way_and_send_DL;
	pars.lcls.csc := LCLS_CSC_connect;
	/* Expect LCLS is *NOT* established with "LCLS_STS_req_lcls_not_supp" */
	pars.lcls.exp_sts := LCLS_STS_req_lcls_not_supp;

	f_lcls_test_init(pars, pars);
	CONN_A.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	CONN_B.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);
	f_wait_fail_notify();
	f_lcls_test_fini();
}

/* Send an ASSIGNMENT REQ with LCLS GCR+CFG+CSC; expect no connect */
testcase TC_lcls_gcr_unsuppported_csc() runs on lcls_test_CT {
	var TestHdlrParams pars_a := f_gen_test_hdlr_pars();
	var TestHdlrParams pars_b;
	var MSC_ConnHdlr vc_conn;

	f_lcls_init();

	pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars_a.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars_a.lcls.cfg := LCLS_CFG_both_way;
	pars_a.lcls.csc := LCLS_CSC_bicast_UL_and_recv_DL_at_handover;
	pars_b := pars_a;

	/* first call is not possible to be LS (no second leg yet) */
	pars_a.lcls.exp_sts := LCLS_STS_not_possible_ls;
	/* Expect LCLS is *NOT* established */
	pars_b.lcls.exp_sts := LCLS_STS_not_yet_ls;

	f_lcls_test_init(pars_a, pars_b);
	f_lcls_not_yet_ls();
	f_lcls_test_fini();
}

/* Expect given LCLS status alongside with corresponding MDCX commands */
private function f_lcls_sts_mgcp(BIT4 expected_status) runs on lcls_test_CT {
	var MgcpCommand mgcp_cmd;

	interleave {
	[] CONN_B.receive(tr_BSSMAP_LclsConnCtrlAck(tr_BSSMAP_IE_LclsSts(expected_status)));
	[] CONN_A.receive(tr_BSSMAP_LclsNotificationSts(expected_status));
	[] CONN_A.receive(tr_MDCX) -> value mgcp_cmd {
		CONN_A.send(f_build_mdcx_rsp(mgcp_cmd));
		}
	[] CONN_B.receive(tr_MDCX) -> value mgcp_cmd {
		CONN_B.send(f_build_mdcx_rsp(mgcp_cmd));
		}
	}
}

private function f_lcls_sts_rsl(BIT4 expected_status) runs on lcls_test_CT {
	var RSL_Message rsl_msg;

	interleave {
	[] CONN_B.receive(tr_BSSMAP_LclsConnCtrlAck(tr_BSSMAP_IE_LclsSts(expected_status)));
	[] CONN_A.receive(tr_BSSMAP_LclsNotificationSts(expected_status));
        /*
	[] CONN_A.receive(RSL_Message:?) -> value rsl_msg {
		log("f_lcls_sts_rsl CONN_A top RSL is ", rsl_msg)
	}
	Ex. placeholder to catch any RSL message */
	[] CONN_A.receive(tr_RSL_IPA_MDCX(?, ?)) -> value rsl_msg {
		f_tc_lcls_ack_rsl_mdcx(rsl_msg, true)
	}
	[] CONN_B.receive(tr_RSL_IPA_MDCX(?, ?)) -> value rsl_msg {
		f_tc_lcls_ack_rsl_mdcx(rsl_msg, false)
	}
	}
}

/* Send an ASSIGNMENT REQ with "do not connect" and enable later using LCLS CTRL */
testcase TC_lcls_gcr_bway_dont_connect_csc() runs on lcls_test_CT {
	var TestHdlrParams pars_a := f_gen_test_hdlr_pars();
	var TestHdlrParams pars_b;
	var MSC_ConnHdlr vc_conn;
	var MgcpCommand mgcp_cmd;

	f_lcls_init();

	pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars_a.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars_a.lcls.cfg := LCLS_CFG_both_way;
	pars_a.lcls.csc := LCLS_CSC_do_not_connect;
	pars_b := pars_a;

	/* first call is not possible to be LS (no second leg yet) */
	pars_a.lcls.exp_sts := LCLS_STS_not_possible_ls;
	/* Expect LCLS is *NOT* established */
	pars_b.lcls.exp_sts := LCLS_STS_not_yet_ls;

	/* start call and expect it to be "not yet" LS */
	f_lcls_test_init(pars_a, pars_b);
	f_lcls_not_yet_ls();
	f_sleep(2.0);

	/* send "connect" on A side, expect call to remain in "not yet" */
	CONN_A.send(ts_BSSMAP_LclsConnCtrl(omit, ts_BSSMAP_IE_LclsCsc(LCLS_CSC_connect)));
	CONN_A.receive(tr_BSSMAP_LclsConnCtrlAck(tr_BSSMAP_IE_LclsSts(LCLS_STS_not_yet_ls)));
	f_sleep(2.0);

	/* send "connect" on B side, expect call to go LS, with notify to A side */
	CONN_B.send(ts_BSSMAP_LclsConnCtrl(omit, ts_BSSMAP_IE_LclsCsc(LCLS_CSC_connect)));
	f_lcls_sts_mgcp(LCLS_STS_locally_switched);
	f_wait_fail_notify();
	f_lcls_test_fini();
}

private function f_build_mdcx_rsp(MgcpCommand mdcx) return MgcpResponse
{
	var MgcpConnectionId conn_id := f_MgcpCmd_extract_conn_id(mdcx);
	var SDP_Message sdp_in := mdcx.sdp;
	var MgcpResponse resp;
	var SDP_Message sdp_out;
	var integer rtp_pt := str2int(sdp_in.media_list[0].media_field.fmts[0]);

	sdp_out := valueof(ts_SDP(sdp_in.connection.conn_addr.addr, sdp_in.connection.conn_addr.addr,
				  "foo", "21", sdp_in.media_list[0].media_field.ports.port_number,
				  { int2str(rtp_pt) },
				  { valueof(ts_SDP_rtpmap(rtp_pt, "AMR/8000")),
				    valueof(ts_SDP_ptime(20)) } ));
	return valueof(ts_MDCX_ACK(mdcx.line.trans_id, conn_id, sdp_out));
}

private function f_lcls_connect_break(boolean bts_mode := false) runs on lcls_test_CT {
	var TestHdlrParams pars_a := f_gen_test_hdlr_pars();
	var TestHdlrParams pars_b;
	var MSC_ConnHdlr vc_conn;

	f_lcls_init(bts_mode);

	pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars_a.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars_a.lcls.cfg := LCLS_CFG_both_way;
	pars_a.lcls.csc := LCLS_CSC_connect;
        pars_a.lcls.adjust_cx_exp := not bts_mode;
	pars_b := pars_a;

	/* first call is not possible to be LS (no second leg yet) */
	pars_a.lcls.exp_sts := LCLS_STS_not_possible_ls;
	/* second call should then result in LS */
	pars_b.lcls.exp_sts := LCLS_STS_locally_switched;

	/* Expect LS to be established successfully */
	f_lcls_test_init(pars_a, pars_b);
	CONN_A.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);

	if (bts_mode == true) {
		f_tc_lcls_recv_ls_exp_rsl();
	} else {
		f_tc_lcls_recv_ls_exp_mgcp();
	}

	/* request LS release on "A" side; call continues to be locally switched */
	CONN_A.send(ts_BSSMAP_LclsConnCtrl(omit, ts_BSSMAP_IE_LclsCsc(LCLS_CSC_release_lcls)));
	CONN_A.receive(tr_BSSMAP_LclsConnCtrlAck(tr_BSSMAP_IE_LclsSts(LCLS_STS_locally_switched)));
	f_sleep(2.0);

	/* request LS release on "B" side; call LS is released */
	CONN_B.send(ts_BSSMAP_LclsConnCtrl(omit, ts_BSSMAP_IE_LclsCsc(LCLS_CSC_release_lcls)));

	if (bts_mode == true) {
		f_lcls_sts_rsl(LCLS_STS_no_longer_ls);
	} else {
		f_lcls_sts_mgcp(LCLS_STS_no_longer_ls);
	}

	f_lcls_test_fini();
}

/* Establish LCLS "connect" followed by a MSC-initiated break */
testcase TC_lcls_connect_break() runs on lcls_test_CT {
	f_lcls_connect_break()
}

testcase TC_lcls_bts_connect_break() runs on lcls_test_CT {
	f_lcls_connect_break(true)
}

/* Establish LCLS "connect" followed by a SCCP-level release of one leg */
testcase TC_lcls_connect_clear() runs on lcls_test_CT {
	var TestHdlrParams pars_a := f_gen_test_hdlr_pars();
	var TestHdlrParams pars_b;
	var MSC_ConnHdlr vc_conn;
	var MgcpCommand mgcp_cmd;
	var RSL_Message rsl;

	f_lcls_init();

	pars_a.ass_codec_list := valueof(ts_BSSMAP_IE_CodecList({ts_CodecFR}));
	pars_a.lcls.gcr := valueof(ts_GCR('010203'O, '0405'O, '060708090a'O));
	pars_a.lcls.cfg := LCLS_CFG_both_way;
	pars_a.lcls.csc := LCLS_CSC_connect;
	pars_b := pars_a;

	/* first call is not possible to be LS (no second leg yet) */
	pars_a.lcls.exp_sts := LCLS_STS_not_possible_ls;
	/* second call should then reuslt in LS */
	pars_b.lcls.exp_sts := LCLS_STS_locally_switched;

	/* Expect LS to be established successfully */
	f_lcls_test_init(pars_a, pars_b);
	CONN_A.receive(LclsCompSync:LCLS_COMP_SYNC_ASS_COMPL);

	f_tc_lcls_recv_ls_exp_mgcp();

	/* Perform hard BSSMAP Clear on "A" side, expect no LS on "B" side */
	var myBSSMAP_Cause cause_val := GSM0808_CAUSE_CALL_CONTROL;
	CONN_A.send(ts_BSSMAP_ClearCommand(enum2int(cause_val)));
	interleave {
	[] CONN_A.receive(tr_RSL_DATA_REQ(?, tr_RslLinkID_DCCH(0), decmatch tr_RRM_RR_RELEASE));
	[] CONN_A.receive(tr_RSL_DEACT_SACCH(?));
	[] CONN_A.receive(tr_RSL_RF_CHAN_REL(?)) -> value rsl {
		var RSL_IE_Body ieb;
		if (f_rsl_find_ie(rsl, RSL_IE_CHAN_NR, ieb) == true) {
			CONN_A.send(ts_RSL_RF_CHAN_REL_ACK(ieb.chan_nr));
		} else {
			log("Unable to find chan# in RSL_RF_CHAN_REL")
		}
		}
	[] CONN_A.receive(tr_BSSMAP_ClearComplete) {
		CONN_A.send(ts_MSC_CONN_PRIM_DISC_REQ);
		}
	[] CONN_B.receive(tr_BSSMAP_LclsNotificationSts(LCLS_STS_not_possible_ls));
	}
	f_sleep(2.0);

	f_lcls_test_fini();
}




/* TODO:
	* verify IP/Port information in LCLS-triggered MDCX
	* establish with one side connect, then enable using LCLS CTRL
	* LCLS CTRL for call that doesn't have LCLS enabled
	* LCLS IEs without GCR in ASS CMD
	* GCR updates?
	* Handover related LCLS bits (after we have inter-BSC HO in OsmoBSC)
*/


control {

	execute( TC_lcls_gcr_only() );
	execute( TC_lcls_gcr_bway_connect() );
	execute( TC_lcls_gcr_bway_connect_hr() );
	execute( TC_lcls_gcr_bway_codec_mismatch() );
	execute( TC_lcls_gcr_nomatch_bway_connect() );
	execute( TC_lcls_gcr_bway_dont_connect() );
	execute( TC_lcls_gcr_unsuppported_cfg() );
	execute( TC_lcls_gcr_unsuppported_csc() );
	execute( TC_lcls_gcr_bway_dont_connect_csc() );
	execute( TC_lcls_connect_break() );
	execute( TC_lcls_connect_clear() );

	execute( TC_lcls_bts_gcr_bway_connect() );
	execute( TC_lcls_bts_gcr_bway_connect_hr() );
	execute( TC_lcls_bts_connect_break() );
}


}
