module BTS_Tests_SMSCB {

/* Integration Tests for OsmoBTS
 * (C) 2019 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 the SMSCB (Cell Broadcast) related functionality of
 * OsmoBTS by attaching to the A-bis RSL and Um interface and emulating both
 * BSC and MS.
 */

import from Misc_Helpers all;
import from General_Types all;
import from Osmocom_Types all;
import from GSM_Types all;
import from L1CTL_PortType all;
import from L1CTL_Types all;
import from LAPDm_Types all;
import from IPA_Emulation all;
import from GSM_RR_Types all;
import from L3_Templates all;

import from RSL_Types all;

import from PCUIF_Types all;
import from PCUIF_CodecPort all;

import from Osmocom_VTY_Functions all;

import from BTS_Tests all;

/***********************************************************************
 * Cell Broadcast related tests
 ***********************************************************************/

/* Test parameters for one channel (e.g. Basic, Extended) */
type record CbchTestParsChan {
	/* list of "normal" (sent-once) SMSCB messages */
	CbchTestMsgs	msgs,
	/* default message, if any */
	CbchTestMsg	default_msg optional
}

private template (value) CbchTestParsChan
t_CbchPC(template (value) CbchTestMsgs msgs, template (omit) CbchTestMsg def := omit) := {
	msgs := msgs,
	default_msg := def
}

/* CBCH test parameters for most of our tests */
type record CbchTestPars {
	/* Should we execute on SDCCH4 or SDCCH8? */
	RslChannelNr chan_nr,
	/* Frequency Hopping parameters */
	FreqHopPars fhp,
	/* Parameters for BASIC CBCH */
	CbchTestParsChan basic,
	/* Parameters for EXTENDED CBCH */
	CbchTestParsChan extended optional
};

type record CbchTestMsg {
	/* config / input data */
	RSL_CbCommand	rsl_cb_cmd,
	uint2_t		last_block, /* 0..3 */
	octetstring	payload,
	/* computed / result data */
	CbchBlocks	blocks optional
};
type record of CbchTestMsg CbchTestMsgs;

/* a single 22byte block within a CbchTestMsg */
type record CbchBlock {
	uint4_t		seq_nr, /* as per TS 04.12 */
	boolean		is_last,
	OCT22		payload,
	boolean 	seen_once
};
type record of CbchBlock CbchBlocks;

/* compute the expected blocks for given test parameters */
private function f_cbch_compute_exp_blocks(inout CbchTestPars pars) {
	f_cbch_compute_exp_blocks_chan(pars.basic);
	if (ispresent(pars.extended)) {
		f_cbch_compute_exp_blocks_chan(pars.extended);
	}
}
private function f_cbch_compute_exp_blocks_chan(inout CbchTestParsChan pars_chan) {
	var integer i;
	for (i := 0; i < lengthof(pars_chan.msgs); i := i+1) {
		pars_chan.msgs[i].blocks := f_comp_blocks(pars_chan.msgs[i]);
	}
	if (ispresent(pars_chan.default_msg)) {
		pars_chan.default_msg.blocks := f_comp_blocks(pars_chan.default_msg);
	}
}
private function f_comp_blocks(in CbchTestMsg msg) return CbchBlocks {
	var CbchBlocks blocks := {};
	var integer i;

	for (i := 0; i <= msg.last_block; i := i+1) {
		var CbchBlock block := {
			seq_nr := i,
			is_last := false,
			payload := substr(msg.payload, 22*i, 22),
			seen_once := false
			};
		if (msg.rsl_cb_cmd == RSL_CB_CMD_SCHEDULE and i == 0) {
			block.seq_nr := 8;
		}
		if (i == msg.last_block) {
			block.is_last := true;
		}
		blocks := blocks & {block};
	}

	return blocks;
};

/* TS 48.058 Section 9.3.41 */
private function f_cbch_block_nr2rsl(uint2_t nr) return uint2_t {
	select (nr) {
	case (0) { return 1; }
	case (1) { return 2; }
	case (2) { return 3; }
	case (3) { return 0; }
	}
	setverdict(fail, "Invalid block number");
	mtc.stop;
}

private function f_cbch_fn2tb(uint32_t fn) return integer
{
	return (fn/51) mod 8; /* TS 05.02 Section 6.5.4 */
}

/* Verify the CBCH TB scheduling rules of TS 05.02 Section 6.5.4 */
private function f_cbch_fn_verify(uint32_t fn, CBCH_Block cb)
{
	var integer tb := f_cbch_fn2tb(fn);
	if (cb.block_type.seq_nr == 15 /* null */) {
		/* always permitted */
		return;
	} else if (cb.block_type.seq_nr == 8 /* schedule */) {
		if (tb != 0) {
			setverdict(fail, "Schedule block at TB=", tb);
		}
	} else if (cb.block_type.seq_nr < 4) {
		if (cb.block_type.seq_nr != tb and cb.block_type.seq_nr+4 != tb) {
			setverdict(fail, "Normal block at wrong TB=", tb, ": ", cb);
		}
	}
}

private function f_rsl_smscb_default_null() runs on test_CT
{
	var RSL_IE_CbCommandType cmd_type :=
				valueof(ts_RSL_IE_CbCmdType(RSL_CB_CMD_DEFAULT, 1, true));
	RSL_CCHAN.send(ts_ASP_RSL_UD(ts_RSL_SMSCB_CMD(cmd_type, ''O)));
}

template RSL_IE t_RSL_IE_SMSCB_EXT := t_RSL_IE(RSL_IE_SMSCB_CHAN_INDICATOR, {smscb_chan_ind := 1});

private function f_smscb_setup_rsl_chan(inout CbchTestParsChan pars_chan, boolean extd := false)
runs on test_CT {
	var integer i;
	var CbchTestMsg msg;
	var uint2_t rsl_last_block;
	var RSL_IE_CbCommandType cmd_type;
	var RSL_Message rsl;

	/* send SMSCB[s] via RSL */
	for (i := 0; i < lengthof(pars_chan.msgs); i := i+1) {
		msg := pars_chan.msgs[i];
		rsl_last_block := f_cbch_block_nr2rsl(msg.last_block);
		cmd_type := valueof(ts_RSL_IE_CbCmdType(msg.rsl_cb_cmd, rsl_last_block));
		rsl := valueof(ts_RSL_SMSCB_CMD(cmd_type, msg.payload));
		if (extd) {
			rsl.ies := rsl.ies & { valueof(t_RSL_IE_SMSCB_EXT) };
		}
		RSL_CCHAN.send(ts_ASP_RSL_UD(rsl));
	}
	if (ispresent(pars_chan.default_msg)) {
		msg := pars_chan.default_msg;
		rsl_last_block := f_cbch_block_nr2rsl(msg.last_block);
		cmd_type := valueof(ts_RSL_IE_CbCmdType(msg.rsl_cb_cmd, rsl_last_block, false));
		rsl := valueof(ts_RSL_SMSCB_CMD(cmd_type, msg.payload));
		if (extd) {
			rsl.ies := rsl.ies & { valueof(t_RSL_IE_SMSCB_EXT) };
		}
		RSL_CCHAN.send(ts_ASP_RSL_UD(rsl));
	}
}

private function f_vty_cbch_setup(in RslChannelNr chan_nr) runs on test_CT {

	if (match(chan_nr, t_RslChanNr_CBCH4(0))) {
		f_vty_config2(BSCVTY, { "network", "bts 0", "trx 0", "timeslot 0"},
					"phys_chan_config CCCH+SDCCH4+CBCH");
		/* (Re)configure timeslots 1..3 as TCH/F */
		for (var integer tn := 1; tn <= 3; tn := tn + 1) {
			f_vty_config2(BSCVTY, { "network", "bts 0", "trx 0",
						"timeslot " & int2str(tn) },
						"phys_chan_config TCH/F");
		}
	} else if (match(chan_nr, t_RslChanNr_CBCH8(?))) {
		f_vty_config2(BSCVTY, { "network", "bts 0", "trx 0", "timeslot 0"},
					"phys_chan_config CCCH+SDCCH4");
		f_vty_config2(BSCVTY, { "network", "bts 0", "trx 0",
					"timeslot " & int2str(chan_nr.tn) },
					"phys_chan_config SDCCH8+CBCH");

		/* (Re)configure timeslots 1..3 (excluding the given one) as TCH/F */
		for (var integer tn := 1; tn <= 3; tn := tn + 1) {
			if (tn == chan_nr.tn)
				{ continue; }
			f_vty_config2(BSCVTY, { "network", "bts 0", "trx 0",
						"timeslot " & int2str(tn) },
						"phys_chan_config TCH/F");
		}
	}
	f_vty_transceive(BSCVTY, "drop bts connection 0 oml");
	f_sleep(2.0);
}
private function f_smscb_setup(inout CbchTestPars pars) runs on test_CT {
	/* Make sure we've got either SDCCH4+CBCH or SDCCH8+CBCH.
	 * SDCCH4+CBCH can only be allocated on TS0, SDCCH8+CBCH on TS0..3.
	 * On C0 the first timeslot shall always transmit BCCH, thus TS1..3.*/
	if (not match(pars.chan_nr, (t_RslChanNr_CBCH4(0), t_RslChanNr_CBCH8(1),
				     t_RslChanNr_CBCH8(2), t_RslChanNr_CBCH8(3)))) {
		setverdict(inconc, "Unhandled channel number: ", pars.chan_nr);
		mtc.stop;
	}

	/* Obtain frequency hopping parameters for a given timeslot */
	if (mp_freq_hop_enabled and mp_transceiver_num > 1)
		{ f_resolve_fh_params(pars.fhp, pars.chan_nr.tn); }
	else
		{ pars.fhp.enabled := false; }

	f_cbch_compute_exp_blocks(pars);

	f_init_vty_bsc();
	/* ensure that a CBCH is present in channel combination */
	f_vty_cbch_setup(pars.chan_nr);
	f_init();

	f_init_l1ctl();

	/* Tune L1 to the given CBCH timeslot (SDCCH4+CBCH or SDCCH8+CBCH)  */
	if (match(pars.chan_nr, t_RslChanNr_CBCH4(0))) {
		f_l1_tune(L1CTL, ccch_mode := CCCH_MODE_COMBINED_CBCH);
	} else {
		f_l1_tune(L1CTL, ccch_mode := CCCH_MODE_COMBINED);

		if (pars.fhp.enabled) {
			L1CTL.send(ts_L1CTL_DM_EST_REQ_H1(pars.chan_nr, mp_tsc_def,
							  pars.fhp.maio_hsn.hsn,
							  pars.fhp.maio_hsn.maio,
							  pars.fhp.ma));
		} else {
			L1CTL.send(ts_L1CTL_DM_EST_REQ_H0(pars.chan_nr, mp_tsc_def,
							  mp_trx_pars[0].arfcn));
		}
	}

	/* send SMSCB[s] via RSL */
	f_smscb_setup_rsl_chan(pars.basic, false);
	if (ispresent(pars.extended)) {
		f_smscb_setup_rsl_chan(pars.extended, true);
	}
}

/* construct a receive/match template for given block_nr in given msg */
private function f_get_block_template(CbchTestMsg msg, integer block_nr) return template CBCH_Block {
	var template CBCH_Block tr;
	if (block_nr < lengthof(msg.blocks)) {
		var CbchBlock b := msg.blocks[block_nr];
		tr := tr_CBCH_Block(b.seq_nr, b.is_last, b.payload);
	} else {
		tr := tr_CBCH_Block(15, ?, ?);
	}
	return tr;
}

/* the heart of the CBCH test case matching engine for one channel (basic, extended) */
private function f_cbch_match(inout CbchTestParsChan pars_chan, CBCH_Block cb, integer tb)
{
	var integer block_nr := tb mod 4;
	var integer i;

	if (not match(cb, tr_CBCH_Block)) {
		setverdict(fail, "Illegal CBCH Block received: ", cb);
	} else {
		var boolean matched := false;
		/* check if it's any of our expected blocks */
		for (i := 0; i < lengthof(pars_chan.msgs); i := i+1) {
			if (block_nr < lengthof(pars_chan.msgs[i].blocks)) {
				if (match(cb, f_get_block_template(pars_chan.msgs[i], block_nr))) {
					log("Found block_nr ", block_nr, " of msg ", i);
					if (not pars_chan.msgs[i].blocks[block_nr].seen_once) {
						pars_chan.msgs[i].blocks[block_nr].seen_once := true;
						setverdict(pass);
					} else {
						setverdict(fail, "Received SMSCB twice! ", cb);
					}
					matched := true;
					break;
				}
			}
		}
		if (not matched) {
			var template CBCH_Block tr;
			if (ispresent(pars_chan.default_msg)) {
				/* it must be a block of the default message */
				tr := f_get_block_template(pars_chan.default_msg, block_nr);
			} else {
				/* it must be a NULL block */
				tr := tr_CBCH_Block(15, ?, ?);
			}
			if (not match(cb, tr)) {
				setverdict(fail, "Received unexpected CBCH block: ", cb);
			} else {
				log("Found block_nr ", block_nr, " of DEFAULT/NULL");
				if (ispresent(pars_chan.default_msg) and
				    block_nr < lengthof(pars_chan.default_msg.blocks)) {
					pars_chan.default_msg.blocks[block_nr].seen_once := true;
				}
			}
		}
	}
}

/* Report/Evaluate the per-channel CBCH test results */
private function f_cbch_report(CbchTestParsChan pars_chan, charstring id)
{
	var integer i, j;

	/* verify that each block of each message has been seen once */
	for (i := 0; i < lengthof(pars_chan.msgs); i := i+1) {
		for (j := 0; j < lengthof(pars_chan.msgs[i].blocks); j := j+1) {
			var CbchBlock b := pars_chan.msgs[i].blocks[j];
			if (not b.seen_once) {
				setverdict(fail, "Timeout waiting for ", id, " CBCH block ",
					   j, " of msg ", i);
			}
		}
	}
	if (ispresent(pars_chan.default_msg)) {
		/* verify that each block of default message has been seen at least once */
		for (j := 0; j < lengthof(pars_chan.default_msg.blocks); j := j+1) {
			var CbchBlock b := pars_chan.default_msg.blocks[j];
			if (not b.seen_once) {
				setverdict(fail, "Timeout waiting for at leaset one instance of ",
					   "CBCH block ", j, " of DEFAULT msg");
			}
		}
	}
}

/* shared function doing the heavy lifting for most CBCH tests */
private function f_TC_smscb(CbchTestPars pars) runs on test_CT {
	var L1ctlDlMessage dl;
	var integer msg_count;
	timer T;

	msg_count := lengthof(pars.basic.msgs);
	if (ispresent(pars.basic.default_msg)) {
		msg_count := msg_count + 1;
	}
	if (ispresent(pars.extended)) {
		msg_count := msg_count + lengthof(pars.extended.msgs);
		if (ispresent(pars.extended.default_msg)) {
			msg_count := msg_count + 1;
		}
	}

	f_smscb_setup(pars);

	/* dynamically adjust timeout based on number of messages */
	T.start(5.0 + 3.0 * int2float(msg_count));
	/* Expect this to show up exactly once on the basic CBCH (four blocks) */
	alt {
	[] L1CTL.receive(tr_L1CTL_DATA_IND(pars.chan_nr)) -> value dl {
		var integer tb := f_cbch_fn2tb(dl.dl_info.frame_nr);
		var CBCH_Block cb := dec_CBCH_Block(dl.payload.data_ind.payload);
		log("Tb=", tb, ", CBCH: ", dl, ", block: ", cb);

		/* detect the proper CBCH messages; check frame number */
		f_cbch_fn_verify(dl.dl_info.frame_nr, cb);

		if (tb < 4) {
			f_cbch_match(pars.basic, cb, tb);
		} else {
			if (not ispresent(pars.extended)) {
				/* no parameters for ext. BCCH given: ignore */
				repeat;
			}
			f_cbch_match(pars.extended, cb, tb);
		}
		repeat;
		}
	[] L1CTL.receive { repeat; }
	[] T.timeout {
		f_cbch_report(pars.basic, "Basic");
		if (ispresent(pars.extended)) {
			f_cbch_report(pars.extended, "Extended");
		}
		}
	}

	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

private function f_TC_smscb_default_only(CbchTestPars pars) runs on test_CT {
	var L1ctlDlMessage dl;
	timer T := 5.0;

	f_smscb_setup(pars);

	/* ensure whatever initial NULL messages have all been drained */
	f_sleep(5.0);
	L1CTL.clear;

	T.start;
	alt {
	[] L1CTL.receive(tr_L1CTL_DATA_IND(pars.chan_nr)) -> value dl {
		var integer tb := f_cbch_fn2tb(dl.dl_info.frame_nr);
		log("CBCH: ", dl);
		var CBCH_Block cb := dec_CBCH_Block(dl.payload.data_ind.payload);
		/* detect the proper CBCH messages; check frame number */
		f_cbch_fn_verify(dl.dl_info.frame_nr, cb);
		if (tb >= 4) {
			/* skip extended CBCH for now */
			repeat;
		}
		if (not match(cb, tr_CBCH_Block)) {
			setverdict(fail, "Illegal CBCH Block received: ", cb);
		} else {
			var uint4_t rx_seq_nr := cb.block_type.seq_nr;
			var template CBCH_Block tr;
			if (rx_seq_nr < lengthof(pars.basic.msgs[0].blocks)) {
				var CbchBlock b := pars.basic.msgs[0].blocks[rx_seq_nr];
				tr := tr_CBCH_Block(b.seq_nr, b.is_last, b.payload);
			} else {
				tr := tr_CBCH_Block(15, ?, ?);
			}
			if (match(cb, tr)) {
				setverdict(pass); /* FIXME: check that all blocks are received? */
				repeat;
			} else {
				setverdict(fail, "Unexpected CBCH block ", cb, ", expected ", tr);
			}
		}
		}
	[] L1CTL.receive { repeat; }
	[] T.timeout {}
	}

	/* don't shut down; some tests still want to continue */
}

private const CbchTestMsgs msgs_1m_1b_norm := {
	{ RSL_CB_CMD_NORMAL, 0, '001000320f1141660c344dd3cba09a0c000000000000'O, omit }
}

private const CbchTestMsgs msgs_1m_2b_norm := {
	{ RSL_CB_CMD_NORMAL, 1, '001000320f1141660c344dd3cba09a0c000000000000'O &
				'000102030405060708090a0b0c0d0e0f101213141516'O,
	  omit }
}

private const CbchTestMsgs msgs_1m_3b_norm := {
	{ RSL_CB_CMD_NORMAL, 2, '001000320f1141660c344dd3cba09a0c000000000000'O &
				'000102030405060708090a0b0c0d0e0f101213141516'O &
				'101112131415161718191a1b1c1d1e1f202223242526'O,
	  omit }
}

private const CbchTestMsgs msgs_1m_4b_norm := {
	{ RSL_CB_CMD_NORMAL, 3, '001000320f1141660c344dd3cba09a0c000000000000'O &
				'000102030405060708090a0b0c0d0e0f101213141516'O &
				'101112131415161718191a1b1c1d1e1f202223242526'O &
				'202122232425262728292a2b2c2d2e2f303233343536'O,
	  omit }
}

private const CbchTestMsgs msgs_1m_4b_sched := {
	{ RSL_CB_CMD_SCHEDULE, 3, '001000320f1141660c344dd3cba09a0c000000000000'O &
				  '000102030405060708090a0b0c0d0e0f101213141516'O &
				  '101112131415161718191a1b1c1d1e1f202223242526'O &
				  '202122232425262728292a2b2c2d2e2f303233343536'O,
	  omit }
}

private const CbchTestMsgs msgs_3m_4b_norm := {
	{ RSL_CB_CMD_NORMAL, 3, '001000320f1141660c344dd3cba09a0c000000000000'O &
				'000102030405060708090a0b0c0d0e0f101213141516'O &
				'101112131415161718191a1b1c1d1e1f202223242526'O &
				'201122232425262728292a2b2c2d2e2f303233343536'O,
	  omit },
	{ RSL_CB_CMD_NORMAL, 3, '002000320f1141660c344dd3cba09a0c000000000000'O &
				'002102030405060708090a0b0c0d0e0f101213141516'O &
				'102112131415161718191a1b1c1d1e1f202223242526'O &
				'202122232425262728292a2b2c2d2e2f303233343536'O,
	  omit },
	{ RSL_CB_CMD_NORMAL, 3, '003000320f1141660c344dd3cba09a0c000000000000'O &
				'003102030405060708090a0b0c0d0e0f101213141516'O &
				'103112131415161718191a1b1c1d1e1f202223242526'O &
				'203122232425262728292a2b2c2d2e2f303233343536'O,
	  omit }
}

private const CbchTestMsgs msgs_1m_3b_default := {
	{ RSL_CB_CMD_DEFAULT, 2, '001000320f1141660c344dd3cba09a0c000000000000'O &
				'000102030405060708090a0b0c0d0e0f101213141516'O &
				'101112131415161718191a1b1c1d1e1f202223242526'O,
	  omit }
}
private const CbchTestMsg msg_default := {
	RSL_CB_CMD_DEFAULT, 0, '010203040506070708090a0b0c0d0e0f101112131415'O,
	omit
}

/* transmit single-block SMSCB COMMAND */
testcase TC_sms_cb_cmd_sdcch4_1block() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_1b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}
testcase TC_sms_cb_cmd_sdcch8_1block() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH8(2)),
		basic := valueof(t_CbchPC(msgs_1m_1b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}

/* transmit dual-block SMSCB COMMAND */
testcase TC_sms_cb_cmd_sdcch4_2block() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_2b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}
testcase TC_sms_cb_cmd_sdcch8_2block() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH8(2)),
		basic := valueof(t_CbchPC(msgs_1m_2b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}

/* transmit triple-block SMSCB COMMAND */
testcase TC_sms_cb_cmd_sdcch4_3block() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_3b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}
testcase TC_sms_cb_cmd_sdcch8_3block() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH8(2)),
		basic := valueof(t_CbchPC(msgs_1m_3b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}

/* transmit quad-block SMSCB COMMAND */
testcase TC_sms_cb_cmd_sdcch4_4block() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_4b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}
testcase TC_sms_cb_cmd_sdcch8_4block() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH8(2)),
		basic := valueof(t_CbchPC(msgs_1m_4b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}

/* transmit multiple commands of each 4 blocks */
testcase TC_sms_cb_cmd_sdcch4_multi() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_3m_4b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}
testcase TC_sms_cb_cmd_sdcch8_multi() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH8(2)),
		basic := valueof(t_CbchPC(msgs_3m_4b_norm)),
		extended := omit
	};
	f_TC_smscb(pars);
}

/* transmit multiple commands of each 4 blocks on CBCH EXTD */
testcase TC_sms_cb_cmd_sdcch4_extd_multi() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC({})),
		extended := valueof(t_CbchPC(msgs_3m_4b_norm))
	};
	f_TC_smscb(pars);
}
testcase TC_sms_cb_cmd_sdcch8_extd_multi() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH8(2)),
		basic := valueof(t_CbchPC({})),
		extended := valueof(t_CbchPC(msgs_3m_4b_norm))
	};
	f_TC_smscb(pars);
}

/* transmit SMSCB COMMAND with SCHEDULE payload */
testcase TC_sms_cb_cmd_sdcch4_schedule() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_4b_sched)),
		extended := omit
	};
	f_TC_smscb(pars);
}
testcase TC_sms_cb_cmd_sdcch8_schedule() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH8(2)),
		basic := valueof(t_CbchPC(msgs_1m_4b_sched)),
		extended := omit
	};
	f_TC_smscb(pars);
}

/* set a DEFAULT message; verify it gets transmitted all the time */
testcase TC_sms_cb_cmd_sdcch4_default_only() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_3b_default)),
		extended := omit
	};
	f_TC_smscb_default_only(pars);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}
testcase TC_sms_cb_cmd_sdcch8_default_only() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_3b_default)),
		extended := omit
	};
	f_TC_smscb_default_only(pars);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

testcase TC_sms_cb_cmd_sdcch4_default_and_normal() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_3b_norm, msg_default)),
		extended := omit
	};
	f_TC_smscb(pars);
}
testcase TC_sms_cb_cmd_sdcch8_default_and_normal() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH8(2)),
		basic := valueof(t_CbchPC(msgs_1m_3b_norm, msg_default)),
		extended := omit
	};
	f_TC_smscb(pars);
}

/* first set a DEFAULT message, then disable it again */
testcase TC_sms_cb_cmd_sdcch4_default_then_null() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_3b_default)),
		extended := omit
	};
	var L1ctlDlMessage dl;
	timer T := 5.0;

	f_TC_smscb_default_only(pars);

	/* disable DEFAULT message; switch back to NULL */
	f_rsl_smscb_default_null();

	/* ensure whatever initial non-NULL messages have all been drained */
	f_sleep(5.0);
	L1CTL.clear;

	T.start;
	alt {
	[] L1CTL.receive(tr_L1CTL_DATA_IND(t_RslChanNr_CBCH4(0))) -> value dl {
		log("CBCH: ", dl);
		var CBCH_Block cb := dec_CBCH_Block(dl.payload.data_ind.payload);
		/* detect the proper CBCH messages; check frame number */
		f_cbch_fn_verify(dl.dl_info.frame_nr, cb);
		if (not match(cb, tr_CBCH_Block)) {
			setverdict(fail, "Illegal CBCH Block received: ", cb);
		} else {
			if (not match(cb, tr_CBCH_Block(15, ?, ?))) {
				setverdict(fail, "Unexpected non-NULL CBCH block received");
			}
			repeat;
		}
		}
	[] L1CTL.receive { repeat; }
	[] T.timeout {
		setverdict(pass);
		}
	}

	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* Verify there are no CBCH load indications if no CBCH is present */
testcase TC_cbch_load_idle_no_cbch() runs on test_CT {
	var ASP_RSL_Unitdata rx_ud;
	timer T := 10.0;

	f_init();
	f_init_vty_bsc();

	T.start;
	alt {
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_BASIC)) -> value rx_ud {
		setverdict(fail, "Received unexpected CBCH LOAD IND: ", rx_ud);
		}
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_EXTD)) -> value rx_ud {
		setverdict(fail, "Received unexpected CBCH LOAD IND: ", rx_ud);
		}
	[] RSL_CCHAN.receive { repeat; }
	[] T.timeout {
		setverdict(pass);
		}
	}
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
}

/* Verify the CBCH load indications of an idle cell (without CBCH load) */
function f_TC_cbc_load_idle() runs on test_CT {
	template integer tr_slot_count := (2 .. 15);
	const integer min_load_ind := 4;
	var integer basic_count := 0;
	var integer extd_count := 0;
	var ASP_RSL_Unitdata rx_ud;
	timer T := 10.0;

	f_init();

	RSL_CCHAN.clear;
	T.start;
	alt {
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_BASIC(false, tr_slot_count))) {
		basic_count := basic_count + 1;
		repeat;
		}
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_EXTD(false, tr_slot_count))) -> value rx_ud {
		extd_count := extd_count + 1;
		repeat;
		}
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_BASIC)) -> value rx_ud {
		setverdict(fail, "Received unexpected CBCH LOAD IND: ", rx_ud);
		}
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_EXTD)) -> value rx_ud {
		setverdict(fail, "Received unexpected CBCH LOAD IND: ", rx_ud);
		}
	[] RSL_CCHAN.receive { repeat; }
	[] T.timeout {
		if ((basic_count >= min_load_ind) and (extd_count >= min_load_ind)) {
			setverdict(pass);
		} else {
			setverdict(fail, "Insufficient number of CBCH LOAD IND: ",
				   "BASIC=", basic_count, " EXTD=", extd_count);
		}
		}
	}

	Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
}
testcase TC_cbc_sdcch4_load_idle() runs on test_CT {
	f_init_vty_bsc();
	f_vty_cbch_setup(valueof(ts_RslChanNr_CBCH4(0)));
	f_TC_cbc_load_idle();
}
testcase TC_cbc_sdcch8_load_idle() runs on test_CT {
	f_init_vty_bsc();
	f_vty_cbch_setup(valueof(ts_RslChanNr_CBCH8(2)));
	f_TC_cbc_load_idle();
}

/* Verify CBCH overload indications are sent when sending too many SMS CB CMD */
function f_TC_cbc_load_overload(CbchTestPars pars) runs on test_CT {
	template integer tr_slot_count_basic := (11 .. 13);
	template integer tr_slot_count_extd := (2 .. 15);
	const integer min_load_ind := 4;
	var integer basic_count := 0;
	var integer extd_count := 0;
	var ASP_RSL_Unitdata rx_ud;
	timer T_total := 10.0;
	timer T_retransmit := 0.2;
	timer T_initial_guard := 2.0;
	var integer i;

	f_init();

	/* send tons of SMSCB Command */
	for (i := 0; i < 30; i := i+1) {
		f_smscb_setup_rsl_chan(pars.basic);
	}

	/* keep sending SMSCB Commands for another two seconds */
	T_initial_guard.start;
	T_retransmit.start;
	alt {
	[] T_retransmit.timeout {
		f_smscb_setup_rsl_chan(pars.basic);
		T_retransmit.start;
		repeat;
		}
	[] T_initial_guard.timeout { }
	}
	/* clear any pending messages (where load may not have peaked yet) */
	RSL_CCHAN.clear;

	/* keep sending SMSCB Commands while verifying LOAD INDICATIONS */
	T_total.start;
	T_retransmit.start;
	alt {
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_BASIC(true, tr_slot_count_basic))) {
		basic_count := basic_count + 1;
		repeat;
		}
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_EXTD(false, tr_slot_count_extd))) {
		extd_count := extd_count + 1;
		repeat;
		}
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_BASIC)) -> value rx_ud {
		setverdict(fail, "Received unexpected CBCH LOAD IND: ", rx_ud);
		}
	[] RSL_CCHAN.receive(tr_ASP_RSL_UD(tr_RSL_CBCH_LOAD_IND_EXTD)) -> value rx_ud {
		setverdict(fail, "Received unexpected CBCH LOAD IND: ", rx_ud);
		}
	[] RSL_CCHAN.receive { repeat; }
	[] T_retransmit.timeout {
		f_smscb_setup_rsl_chan(pars.basic);
		T_retransmit.start;
		repeat;
		}
	[] T_total.timeout {
		if ((basic_count >= min_load_ind) and (extd_count >= min_load_ind)) {
			setverdict(pass);
		} else {
			setverdict(fail, "Insufficient number of CBCH LOAD IND: ",
				   "BASIC=", basic_count, " EXTD=", extd_count);
		}
		}
	}

	Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
}
testcase TC_cbc_sdcch4_load_overload() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(0)),
		basic := valueof(t_CbchPC(msgs_1m_3b_norm)),
		extended := omit
	};

	f_init_vty_bsc();
	f_vty_cbch_setup(pars.chan_nr);
	f_TC_cbc_load_overload(pars);
}
testcase TC_cbc_sdcch8_load_overload() runs on test_CT {
	var CbchTestPars pars := {
		chan_nr := valueof(ts_RslChanNr_CBCH4(2)),
		basic := valueof(t_CbchPC(msgs_1m_3b_norm)),
		extended := omit
	};

	f_init_vty_bsc();
	f_vty_cbch_setup(pars.chan_nr);
	f_TC_cbc_load_overload(pars);
}


private template GsmRrMessage tr_PagingType1 := {
	header := t_RrHeader(PAGING_REQUEST_TYPE_1, ?),
	payload :=?
};

/* we expect four blocks of 14 bytes, let's fill them with content easily distinguishable */
const octetstring c_etws_seg0 := '000102030405060708090a0b0c0d'O;
const octetstring c_etws_seg1 := '101112131415161718191a1b1c1d'O;
const octetstring c_etws_seg2 := '202122232425262728292a2b2c2d'O;
const octetstring c_etws_seg3 := '303132333435363738393a3b3c3d'O;
const octetstring c_etws := c_etws_seg0 & c_etws_seg1 & c_etws_seg2 & c_etws_seg3;

/* Ensure only Paging Type 1 with segmented ETWS Primary Notification are sent after RSL_OSMO_ETWS_CMD */
testcase TC_etws_p1ro() runs on test_CT {
	/* decoding the actual entire P1 rest octets by manually generated code is
	 * too much effort; instead simply do a binary compare to this constant */
	const bitstring c_P1RO_hdr := '00101011101'B;
	var integer seg_received[4] := { 0, 0, 0, 0 };
	var L1ctlDlMessage dl;
	timer T := 10.0;

	f_init();
	f_init_l1ctl();
	f_l1_tune(L1CTL, ccch_mode := CCCH_MODE_COMBINED_CBCH);

	RSL_CCHAN.send(ts_ASP_RSL_UD(ts_RSL_OSMO_ETWS_CMD(c_etws)));
	/* wait for a bit until old non-ETWS Paging messages are gone */
	f_sleep(1.0);
	L1CTL.clear;
	T.start;
	alt {
	[] L1CTL.receive(tr_L1CTL_DATA_IND(t_RslChanNr_PCH_AGCH(0))) -> value dl {
		var GsmRrMessage l3 := dec_GsmRrMessage(dl.payload.data_ind.payload);
		select (l3) {
		case (tr_PAG_REQ1(tr_MI_LV(t_MI_NoIdentity()))) {
			var octetstring p1ro := l3.payload.pag_req_1.rest_octets;
			var bitstring midamble := oct2bit(substr(p1ro, 0, 3));
			var octetstring segment := substr(p1ro, 3, lengthof(p1ro)-3);
			var BIT1 not_first := substr(midamble, 11, 1);
			var integer seg_nr := bit2int(substr(midamble, 12, 4));
			var boolean err := false;
			if (substr(midamble, 0, 11) != c_P1RO_hdr) {
				setverdict(fail, "Received unexpected P1 RO header ", midamble);
			}
			if (not_first == '1'B) {
				select (seg_nr) {
				case (2) {
					if (segment != c_etws_seg1) {
						err := true
					} else {
						seg_received[1] := seg_received[1] + 1;
					}}
				case (3) {
					if (segment != c_etws_seg2) {
						err := true
					} else {
						seg_received[2] := seg_received[2] + 1;
					}}
				case (4) {
					if (segment != c_etws_seg3) {
						err := true
					} else {
						seg_received[3] := seg_received[3] + 1;
					}}
				case else { setverdict(fail, "Unknown segment Nr ", seg_nr); }
				}
				if (err) {
					setverdict(fail, "Unexpected segment ", seg_nr, ": ", segment);
				}
			} else {
				if (seg_nr != 4) {
					setverdict(fail, "Invalid number of segments ", seg_nr);
					err := true;
				}
				if (segment != c_etws_seg0) {
					setverdict(fail, "Invalid first segment ", segment);
					err := true;
				}
				if (not err) {
					seg_received[0] := seg_received[0] + 1;
				}
			}
			}
		case (tr_PagingType1) {
			setverdict(fail, "Received unexpected PAGING TYPE 1: ", l3);
			}
		}
		repeat;
		}
	[] L1CTL.receive { repeat; }
	[] T.timeout {
		setverdict(pass);
		}
	}
	log("Quantity of received ETWS PN segments: ", seg_received);
	var integer i;
	for (i := 0; i < 4; i := i+1) {
		if (seg_received[i] < 15) {
			setverdict(fail, "Segment ", i, " not received often enough");
		}
	}

	Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
}

/* Ensure only Paging Type 1 without ETWS Primary Notification are sent after disabling them */
testcase TC_etws_p1ro_end() runs on test_CT {
	/* we expect four blocks of 14 bytes, let's fill them with content easily
	 * distinguishable */
	/* decoding the actual entire P1 rest octets by manually generated code is
	 * too much effort; instead simply do a binary compare to this constant */
	const bitstring c_P1RO_hdr := '00101011101'B;
	var L1ctlDlMessage dl;
	timer T := 10.0;

	f_init();
	f_init_l1ctl();
	f_l1_tune(L1CTL, ccch_mode := CCCH_MODE_COMBINED_CBCH);

	RSL_CCHAN.send(ts_ASP_RSL_UD(ts_RSL_OSMO_ETWS_CMD(c_etws)));
	/* wait for a bit until old non-ETWS Paging messages are gone */
	f_sleep(3.0);
	/* disable the ETWS PN again */
	RSL_CCHAN.send(ts_ASP_RSL_UD(ts_RSL_OSMO_ETWS_CMD(''O)));
	f_sleep(2.0);
	T.start;
	L1CTL.clear;
	alt {
	[] L1CTL.receive(tr_L1CTL_DATA_IND(t_RslChanNr_PCH_AGCH(0))) -> value dl {
		var GsmRrMessage l3 := dec_GsmRrMessage(dl.payload.data_ind.payload);
		select (l3) {
		case (tr_PAG_REQ1(tr_MI_LV(t_MI_NoIdentity()))) { repeat; }
		case (tr_PagingType1) {
			setverdict(fail, "Received non-empty PT1 after disabling ETWS PN: ", l3);
			}
		}
		repeat;
		}
	[] L1CTL.receive { repeat; }
	[] T.timeout {
		setverdict(pass);
		}
	}

	Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
}

/* Ensure ETWS Primary Notification is passed from RSL to PCU interface */
testcase TC_etws_pcu() runs on test_CT {
	timer T := 10.0;

	f_init_with_pcuif();
	f_init_l1ctl();
	f_l1_tune(L1CTL, ccch_mode := CCCH_MODE_COMBINED_CBCH);

	RSL_CCHAN.send(ts_ASP_RSL_UD(ts_RSL_OSMO_ETWS_CMD(c_etws)));

	T.start;
	alt {
	[] PCU.receive(t_SD_PCUIF(g_pcu_conn_id, tr_PCUIF_APP_INFO_REQ(0, 0, c_etws))) {
		setverdict(pass);
		}
	[] PCU.receive(t_SD_PCUIF(g_pcu_conn_id, tr_PCUIF_APP_INFO_REQ(?, ?, ?))) {
		setverdict(fail, "PCU socket received invalid APP INFO");
		}
	[] PCU.receive { repeat; }
	[] T.timeout {
		setverdict(fail, "PCU socket timeout receiving APP INFO (ETWS)");
		}
	}

	Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
}



/* SMSCB TODO:
   * multiple SMS BC CMD at the same time: Ensure all of them are sent exactly once
   * extended CBCH vs. normal CBCH
   * 
 */

control {
	execute( TC_cbch_load_idle_no_cbch() );

	execute( TC_sms_cb_cmd_sdcch4_1block() );
	execute( TC_sms_cb_cmd_sdcch4_2block() );
	execute( TC_sms_cb_cmd_sdcch4_3block() );
	execute( TC_sms_cb_cmd_sdcch4_4block() );
	execute( TC_sms_cb_cmd_sdcch4_multi() );
	execute( TC_sms_cb_cmd_sdcch4_schedule() );
	execute( TC_sms_cb_cmd_sdcch4_default_only() );
	execute( TC_sms_cb_cmd_sdcch4_default_and_normal() );
	execute( TC_sms_cb_cmd_sdcch4_default_then_null() );
	execute( TC_cbc_sdcch4_load_idle() );
	execute( TC_cbc_sdcch4_load_overload() );

	execute( TC_sms_cb_cmd_sdcch8_1block() );
	execute( TC_sms_cb_cmd_sdcch8_2block() );
	execute( TC_sms_cb_cmd_sdcch8_3block() );
	execute( TC_sms_cb_cmd_sdcch8_4block() );
	execute( TC_sms_cb_cmd_sdcch8_multi() );
	execute( TC_sms_cb_cmd_sdcch8_schedule() );
	execute( TC_sms_cb_cmd_sdcch8_default_only() );
	execute( TC_sms_cb_cmd_sdcch8_default_and_normal() );
	execute( TC_cbc_sdcch8_load_idle() );
	execute( TC_cbc_sdcch8_load_overload() );

	execute( TC_etws_p1ro() );
	execute( TC_etws_p1ro_end() );
	execute( TC_etws_pcu() );
}


}
