module STP_Tests_IPA {

/* Osmocom STP test suite in in TTCN-3
 * (C) 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
 */

friend module STP_Tests_IPA_M3UA;

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

import from TELNETasp_PortType all;
import from Osmocom_VTY_Functions all;

import from SCCP_Types all;
import from SCCP_Templates all;
import from SCCPasp_Types all;
import from SCCP_Emulation all;

import from IPA_Types all;
import from IPA_CodecPort all;
import from IPA_Emulation all;

import from M3UA_Emulation all;
import from M3UA_CodecPort all;
import from MTP3asp_Types all;
import from MTP3asp_PortType all;

import from STP_Tests_Common all;

private const integer MAX_NR_IPA := 8;

type record of charstring AspNameArray;

modulepar {
	charstring mp_stp_ipa_ip := "127.0.0.1";
	charstring mp_local_ipa_ip := "127.0.0.1";
	integer mp_stp_ipa_port := 5000;
	//integer mp_local_ipa_port := 20000;
	/* local ASPs. Set in .cfg file: */
	IpaConfigs mp_ipa_configs := {}
}

type record IpaConfig {
	/* Name of the ASP in the STP, (eg. to access it over VTY) */
	charstring asp_name,
	/* Name of the AS in the STP, (eg. to access it over VTY) */
	charstring as_name,
	/* Whether local side is a TCP/SCTP server */
	boolean is_server,
	/* STP-side SCTP (or TCP) port for M3UA */
	integer remote_port,
	/* local M3UA base port on TTCN3 side */
	integer local_port,
	/* point code routed via this M3U */
	integer point_code
};
type record of IpaConfig IpaConfigs;

type component IPA_CT extends Test_CT {
	/* for IPA we use the IPA_Emulation and not directly IPA_CodecPort to avoid
	 * having to re-invent IPA CCM handling here */
	port MTP3asp_PT IPA[MAX_NR_IPA];
	port IPA_SP_PT IPA_CTRL[MAX_NR_IPA];
	port IPA_CFG_PT IPA_CFG[MAX_NR_IPA];
	var IPA_Emulation_CT vc_IPA[MAX_NR_IPA];
	var IPA_CCM_Parameters g_ccm_pars[MAX_NR_IPA];
	var IpaConfigs g_ipa_configs;
}

private altstep as_IPA_wait_tcp_conn_closed(integer idx) runs on IPA_CT {
	var ASP_IPA_Event evt;
	[] IPA_CTRL[idx].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_DOWN)) -> value evt {
		log("Peer closed conn: ", evt)
	}
}

friend function ipa_build_configs(template (omit) Misc_Helpers.ro_charstring asps := omit)
return IpaConfigs
{
	if (not isvalue(asps)) {
		return mp_ipa_configs;
	}

	var IpaConfigs ipa_configs := {};
	for (var integer i := 0; i < lengthof(asps); i := i + 1) {
		var boolean found := false;
		for (var integer j := 0; j < lengthof(mp_ipa_configs); j := j + 1) {
			if (mp_ipa_configs[j].asp_name == valueof(asps[i])) {
				found := true;
				ipa_configs := ipa_configs & {mp_ipa_configs[j]};
				break;
			}
		}
		if (not found) {
			Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
						log2str("Could not find ASP ", asps[i], ", check your configuration"));
		}
	}
	return ipa_configs;
}

private function f_vty_cs7_listen_ipa_cmd(charstring cmd, integer ipa_port := 5000) runs on IPA_CT
{
	f_vty_config2(VTY, {"cs7 instance 0", "listen ipa " & int2str(ipa_port)}, cmd);
}

friend function f_vty_cs7_ipa_as_cmd(charstring as_name, charstring cmd) runs on IPA_CT
{
	f_vty_config2(VTY, {"cs7 instance 0", "as " & as_name & " ipa"}, cmd);
}

private function f_ipa_asp_cfg_str(in IpaConfig cfg)
return charstring {
	var charstring str;

	str := "asp " & cfg.asp_name;
	str := str & " " & int2str(cfg.local_port);
	str := str & " " & int2str(cfg.remote_port);
	str := str & " ipa";

	return str;
}

private function f_vty_cs7_ipa_asp_cmd(IpaConfig cfg, charstring cmd) runs on IPA_CT
{
	var charstring asp_cfg_str := f_ipa_asp_cfg_str(cfg);
	f_vty_config2(VTY, {"cs7 instance 0", asp_cfg_str}, cmd);
}

friend function f_IPA_send(integer idx, octetstring data) runs on IPA_CT {
	var MTP3_Field_sio sio := { ni := '00'B, prio := '00'B, si := '0011'B };
	IPA[idx].send(t_ASP_MTP3_TRANSFERreq(sio, 0, 0, 0, data));
}

friend function f_IPA_exp(integer idx, template (present) octetstring data, float timeout_val := 5.0) runs on IPA_CT {
	timer T := timeout_val;
	T.start;
	alt {
	[] IPA[idx].receive(t_ASP_MTP3_TRANSFERind(?, ?, ?, ?, data)) {
		setverdict(pass);
		}
	[] IPA[idx].receive {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Received unexpected data on IPA port while waiting for ", data));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Timeout waiting for IPA[", idx, "] ", data));
		}
	}
}

/* Test if traffic is routed from idx_tx to idx_rx */
private function f_test_traffic(integer idx_tx, integer idx_rx)
runs on IPA_CT {
	var octetstring data := f_SCCP_UDT();
	f_IPA_send(idx_tx, data);
	f_IPA_exp(idx_rx, data);
}

friend function f_init_ipa(template (omit) IpaConfigs ipa_configs := omit) runs on IPA_CT {
	var integer i;

	f_init_common();

	if (not istemplatekind(ipa_configs, "omit")) {
		g_ipa_configs := valueof(ipa_configs);
	} else {
		g_ipa_configs := mp_ipa_configs;
	}

	for (i := 0; i < lengthof(g_ipa_configs); i:=i+1) {
		vc_IPA[i] := IPA_Emulation_CT.create("IPA" & int2str(i));
		map(vc_IPA[i]:IPA_PORT, system:IPA_CODEC_PT);
		connect(self:IPA[i], vc_IPA[i]:MTP3_SP_PORT);
		connect(self:IPA_CTRL[i], vc_IPA[i]:IPA_SP_PORT);
		connect(self:IPA_CFG[i], vc_IPA[i]:CFG_PORT);
		g_ccm_pars[i] := c_IPA_default_ccm_pars;
		g_ccm_pars[i].name := g_ipa_configs[i].as_name;
	}
}

private function f_IPA_close(integer i) runs on IPA_CT {
	var Result res;
	if (vc_IPA[i] == null) {
		log("Not close()ing ipacfg := ", g_ipa_configs[i], " (not connected)");
		/* not connected */
		return;
	}
	vc_IPA[i].stop;
	vc_IPA[i] := null;
}

private function f_clear_ipa() runs on IPA_CT {
	var integer i;

	log("Clearing IPA...");

	for (i := 0; i < lengthof(g_ipa_configs); i:=i+1) {
		f_IPA_close(i);
	}
	setverdict(pass, "IPA cleared");
}

friend function f_connect_ipa(integer idx, boolean exp_act := true) runs on IPA_CT {
	vc_IPA[idx].start(IPA_Emulation.main_client(mp_stp_ipa_ip,
						    g_ipa_configs[idx].remote_port,
						    mp_local_ipa_ip,
						    g_ipa_configs[idx].local_port,
						    g_ccm_pars[idx]));
	IPA_CTRL[idx].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_UP));
	alt {
	[exp_act] IPA_CTRL[idx].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_ID_ACK));
	[not exp_act] IPA_CTRL[idx].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_DOWN));
	[] IPA_CTRL[idx].receive(tr_ASP_IPA_EV(?)) { repeat; }
	}
}

private function f_start_ipa_server(integer idx) runs on IPA_CT {
	vc_IPA[idx].start(IPA_Emulation.main_server(mp_local_ipa_ip, g_ipa_configs[idx].local_port));
}

friend function f_listen_ipa(integer idx, float to_val := 10.0) runs on IPA_CT {
	f_start_ipa_server(idx);

	/* wait for incoming connection to IPA port before proceeding */
	var integer st := 0;
	timer T := to_val;
	T.start;
	alt {
	[st == 0] IPA_CTRL[idx].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_UP)) {
		st := 1; repeat;
		}
	[st == 1] IPA_CTRL[idx].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_ID_RESP)) {
		st := 2; repeat;
		}
	[st == 2] IPA_CTRL[idx].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_ID_ACK)) {
		setverdict(pass);
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("No connection to IPA[", idx, "] Port"));
		}
	}
}

/* Make sure no new conn is attemptent from the peer during the specified time frame: */
private function f_exp_no_ipa_conn(integer idx, float to_val := 10.0) runs on IPA_CT {
	timer T;

	T.start(to_val);
	alt {
	[] IPA_CTRL[idx].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_UP)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
			log2str("Got unexpected connection to IPA[", idx, "] Port"));
		}
	[] T.timeout {
			setverdict(pass);
		}
	}
}

/* "accept-asp-connections pre-configured" and client from unknown ASP source
 * (but known AS/ipa_unit_name) */
testcase TC_unknown_client_nodynamic() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-loadshare-sender0" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	/* Build an ASP unknown (port) from osmo-stp: */
	ipa_configs[0].asp_name := "ipa-asp-unknown";
	ipa_configs[0].local_port := 3000;

	f_init_common();
	f_vty_cs7_listen_ipa_cmd("accept-asp-connections pre-configured", ipa_configs[0].remote_port);
	f_init_ipa(ipa_configs := ipa_configs);
	f_connect_ipa(0, false);

	/* switch back to default */
	f_vty_cs7_listen_ipa_cmd("accept-asp-connections dynamic-permitted", ipa_configs[0].remote_port);
	setverdict(pass);
}

/* "accept-asp-connections pre-configured" and client from known ASP (TCP endpoint)
 * but unknown AS (ipa_unit_name) */
testcase TC_unknown_as_client_nodynamic() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-loadshare-sender0" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	/* Build an AS unknown (ipa_unit_id) from osmo-stp: */
	ipa_configs[0].as_name := "ipa-as-unknown";

	f_init_common();
	f_vty_cs7_listen_ipa_cmd("accept-asp-connections pre-configured", ipa_configs[0].remote_port);
	f_init_ipa(ipa_configs := ipa_configs);
	f_connect_ipa(0, false);

	/* switch back to default */
	f_vty_cs7_listen_ipa_cmd("accept-asp-connections dynamic-permitted", ipa_configs[0].remote_port);
	setverdict(pass);
}

/* "accept-asp-connections pre-configured" and client from known source (ASP & AS) */
testcase TC_known_client_nodynamic() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-loadshare-sender0" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);

	f_init_common();
	f_vty_cs7_listen_ipa_cmd("accept-asp-connections pre-configured", ipa_configs[0].remote_port);
	f_init_ipa(ipa_configs := ipa_configs);
	f_connect_ipa(0);

	/* switch back to default */
	f_vty_cs7_listen_ipa_cmd("accept-asp-connections dynamic-permitted", ipa_configs[0].remote_port);
	setverdict(pass);
}


/* "accept-asp-connections dynamic-permitted" and client from unknown ASP source
 * (but known AS/ipa_unit_name) */
testcase TC_unknown_client_dynamic() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-loadshare-sender0" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	/* Build an ASP unknown (port) from osmo-stp: */
	ipa_configs[0].asp_name := "ipa-asp-unknown";
	ipa_configs[0].local_port := 3001;

	f_init_common();
	f_init_ipa(ipa_configs := ipa_configs);
	f_connect_ipa(0);
	setverdict(pass);
}

/* "accept-asp-connections dynamic-permitted" and client from known ASP (TCP endpoint)
 * but unknown AS (ipa_unit_name) */
testcase TC_unknown_as_client_dynamic() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-loadshare-sender0" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	/* Build an AS unknown (ipa_unit_id) from osmo-stp: */
	ipa_configs[0].as_name := "ipa-as-unknown";

	f_init_common();
	f_init_ipa(ipa_configs := ipa_configs);
	f_connect_ipa(0, false);
	setverdict(pass);
}

private function f_tc_tmt_override(boolean unknown_dynamic_asp)
runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-override-sender",
						 "ipa-asp-override-receiver0",
						 "ipa-asp-override-receiver1" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	if (unknown_dynamic_asp) {
		ipa_configs[0].local_port := 3002;
		ipa_configs[1].local_port := 3003;
		ipa_configs[2].local_port := 3004;
	}
	f_init_ipa(ipa_configs := ipa_configs);

	/* bring up the 'sender' side (single ASP in AS) */
	f_connect_ipa(0);
	/* activate the first 'receiver' side ASP */
	f_connect_ipa(1);

	/* verify traffic is routed from sender to [sole] receiver */
	f_test_traffic(0, 1);

	/* activate the second 'receiver' side ASP */
	f_connect_ipa(2);

	/* verify traffic is routed from sender to new receiver */
	f_test_traffic(0, 2);
	setverdict(pass);
}

/* test "traffic-mode override" behavior */
testcase TC_tmt_override() runs on IPA_CT {
	f_tc_tmt_override(false);
}

/* test "traffic-mode override" behavior, with "accept-asp-connections dynamic-permitted" and clients from unknown ASPs */
testcase TC_unknown_client_dynamic_tmt_override() runs on IPA_CT {
	f_tc_tmt_override(true);
}

private altstep as_count_rx(integer idx, template (present) octetstring exp, inout integer counter)
runs on IPA_CT {
	[] IPA[idx].receive(t_ASP_MTP3_TRANSFERind(?, ?, ?, ?, exp)) {
		counter := counter + 1;
		}
}

private function f_tc_tmt_loadshare_roundrobin(boolean unknown_dynamic_asp)
runs on IPA_CT {
	var integer i;
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-loadshare-sender0",
						 "ipa-asp-loadshare-receiver0",
						 "ipa-asp-loadshare-receiver1" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	if (unknown_dynamic_asp) {
		ipa_configs[0].local_port := 3005;
		ipa_configs[1].local_port := 3006;
		ipa_configs[2].local_port := 3007;
	}

	f_init_ipa(ipa_configs := ipa_configs);

	f_vty_cs7_ipa_as_cmd(ipa_configs[1].as_name, "traffic-mode roundrobin");

	/* bring up the 'sender' side (single ASP in AS) */
	f_connect_ipa(0);
	/* activate the first 'receiver' side ASP */
	f_connect_ipa(1);

	/* verify traffic is routed from sender to [sole] receiver */
	for (i := 0; i < 10; i := i+1) {
		f_test_traffic(0, 1);
	}

	/* activate the second 'receiver' side ASP */
	f_connect_ipa(2);

	/* verify traffic is routed from sender to new receiver */
	const integer iter_per_asp := 5;
	var integer num_rx[2] := { 0, 0 };
	for (i := 0; i < 2*iter_per_asp; i := i+1) {
		var octetstring data := f_SCCP_UDT();
		f_IPA_send(0, data);
		alt {
		[] as_count_rx(1, data, num_rx[0]);
		[] as_count_rx(2, data, num_rx[1]);
		}
	}
	/* FIXME: check for extraneous messages? */
	for (i := 0; i < 2; i := i+1) {
		if (num_rx[i] != iter_per_asp) {
			setverdict(fail, "Received ", num_rx[i], " out of expected ", iter_per_asp,
				   "DATA messages at IPA port ", i);
		}
	}
	setverdict(pass);

	f_vty_cs7_ipa_as_cmd(ipa_configs[1].as_name, "no traffic-mode");
}

/* test "traffic-mode load-share" behavior */
testcase TC_tmt_loadshare_roundrobin() runs on IPA_CT {
	f_tc_tmt_loadshare_roundrobin(false);
}

private altstep as_count_rx_sls(integer idx, template (present) octetstring exp, inout Integers sls_counter, inout integer rx_counter)
runs on IPA_CT {
	var ASP_MTP3_TRANSFERind rx;
	[] IPA[idx].receive(t_ASP_MTP3_TRANSFERind(?, ?, ?, ?, exp)) -> value rx {
		var integer sls := rx.sls; /* This should in general be 0 in IPA. */
		sls_counter[sls] := sls_counter[sls] + 1;
		rx_counter := rx_counter + 1;
		}
}

private function f_tc_tmt_loadshare_sls(boolean unknown_dynamic_asp)
runs on IPA_CT {
	var integer i;

	var Misc_Helpers.ro_charstring asps := { "ipa-asp-loadshare-sender0",
						 "ipa-asp-loadshare-sender1",
						 "ipa-asp-loadshare-receiver0",
						 "ipa-asp-loadshare-receiver1" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	if (unknown_dynamic_asp) {
		ipa_configs[0].local_port := 9998;
		ipa_configs[1].local_port := 9999;
	}

	f_init_ipa(ipa_configs := ipa_configs);

	f_vty_config3(VTY, {"cs7 instance 0", "as " & ipa_configs[2].as_name & " ipa"},
			   { "traffic-mode loadshare", "binding-table reset" });

	/* bring up the 'sender' side (single ASP in AS) */
	f_connect_ipa(0);
	/* activate the first 'receiver' side ASP */
	f_connect_ipa(2);

	/* verify traffic is routed from sender to [sole] receiver */
	for (i := 0; i < 10; i := i+1) {
		f_test_traffic(0, 2);
	}

	/* activate the second 'receiver' side ASP */
	f_connect_ipa(3);

	/* Since we are using dynamic ASPs, they were unknown by STP until they got connected.
	 * Hence, reset the binding-table so that load is now properly spread among them: */
	f_vty_cs7_ipa_as_cmd(ipa_configs[2].as_name, "binding-table reset");

	/* verify traffic is routed from sender to new receiver */
	const integer iter_per_asp := 20;
	const integer NUM_SLS := 16; /* SLS in ITU is 4 bits. */
	var integer num_rx_1 := 0;
	var integer num_rx_2 := 0;
	var Integers sls_num_rx_1 := {};
	var Integers sls_num_rx_2 := {};
	for (i := 0; i < NUM_SLS; i := i + 1) {
		sls_num_rx_1 := sls_num_rx_1 & {0};
		sls_num_rx_2 := sls_num_rx_2 & {0};
	}
	for (i := 0; i < iter_per_asp; i := i+1) {
		var octetstring data := f_SCCP_UDT();
		f_IPA_send(0, data);
		alt {
		[] as_count_rx_sls(2, data, sls_num_rx_1, num_rx_1);
		[] as_count_rx_sls(3, data, sls_num_rx_2, num_rx_2);
		}
	}

	/* All traffic should still be sent to the same receiver, since
	 * OPC+DPC+SLS cannot change in IPA ASPs.
	 * However, depending on how the SLS seed tables were allocated, it can
	 * be that STP was routing through Alternative Route and now that the
	 * 2nd receiver is up it is now routing to it (Normal Route). Account
	 * for both scenarios. */
	if (not ((num_rx_1 == iter_per_asp and num_rx_2 == 0) or (num_rx_2 == iter_per_asp and num_rx_1 == 0))) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Expected all traffic through same receiver!");
	}


	/* activate the second 'sender' side ASP */
	f_connect_ipa(1);


	num_rx_1 := 0;
	num_rx_2 := 0;
	sls_num_rx_1 := {};
	sls_num_rx_2 := {};
	for (i := 0; i < NUM_SLS; i := i + 1) {
		sls_num_rx_1 := sls_num_rx_1 & {0};
		sls_num_rx_2 := sls_num_rx_2 & {0};
	}
	for (i := 0; i < 2*iter_per_asp; i := i+1) {
		var octetstring data := f_SCCP_UDT();
		f_IPA_send(i mod 2, data);
		alt {
		[] as_count_rx_sls(2, data, sls_num_rx_1, num_rx_1);
		[] as_count_rx_sls(3, data, sls_num_rx_2, num_rx_2);
		}
	}
	/* Make sure traffic was sent over both ASPs and that it was distributed: */
	if (num_rx_1 == 0) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Rx 0 packets in 1st ASP!");
	}
	if (num_rx_2 == 0) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Rx 0 packets in 2nd ASP!");
	}
	if (num_rx_1 + num_rx_2 != 2*iter_per_asp) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Rx ", num_rx_1 + num_rx_2 ," packets in total vs exp ", 2*iter_per_asp));
	}
	setverdict(pass);

	f_vty_cs7_ipa_as_cmd(ipa_configs[2].as_name, "no traffic-mode");
}

/* test "traffic-mode load-share" behavior */
testcase TC_tmt_loadshare_sls() runs on IPA_CT {
	f_tc_tmt_loadshare_sls(false);
}

/* test "traffic-mode loadshare" behavior, with "accept-asp-connections dynamic-permitted" and clients from unknown ASPs */
testcase TC_unknown_client_dynamic_tmt_loadshare() runs on IPA_CT {
	f_tc_tmt_loadshare_sls(true);
}

/* Test AS Loadsharing, (distributing traffic within linksets/ASs of a combined linkset).
 * STP routing table is configured with 1 combined linkset towards PC 32 mask /14 with 2
 * AS routes ("ipa-as-LS0-0", "ipa-as-LS0-1") with same priority, each using 1
 * ASP ("ipa-asp-LS0-0-0", "ipa-asp-LS0-1-0").
 * We take advantage of the fact that AS "ipa-as-loadshare-receiver" is never used
 * to transmit in other tests and use it here for the routing of its configured
 * "point-code override dpc 32" towards the "ipa-as-LS0-{0,1}" in the STP config.
 */
private function f_tc_combinedlset_loadshare(boolean unknown_dynamic_asp)
runs on IPA_CT {
	var integer i;

	var Misc_Helpers.ro_charstring asps := { "ipa-asp-loadshare-receiver0",
						 "ipa-asp-loadshare-receiver1",
						 "ipa-asp-LS0-0-0",
						 "ipa-asp-LS0-1-0" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	if (unknown_dynamic_asp) {
		ipa_configs[0].local_port := 9998;
		ipa_configs[1].local_port := 9999;
	}

	f_init_ipa(ipa_configs := ipa_configs);

	/* bring up the 'sender' side (single ASP in AS) */
	f_connect_ipa(0);
	/* activate the first 'receiver' side ASP */
	f_connect_ipa(2);

	/* verify traffic is routed from sender to [sole] receiver */
	for (i := 0; i < 10; i := i+1) {
		f_test_traffic(0, 2);
	}

	/* activate the second 'receiver' side ASP */
	f_connect_ipa(3);

	/* verify traffic is routed from sender to new receiver */
	const integer iter_per_asp := 20;
	const integer NUM_SLS := 16; /* SLS in ITU is 4 bits. */
	var integer num_rx_1 := 0;
	var integer num_rx_2 := 0;
	var Integers sls_num_rx_1 := {};
	var Integers sls_num_rx_2 := {};
	for (i := 0; i < NUM_SLS; i := i + 1) {
		sls_num_rx_1 := sls_num_rx_1 & {0};
		sls_num_rx_2 := sls_num_rx_2 & {0};
	}
	for (i := 0; i < iter_per_asp; i := i+1) {
		var octetstring data := f_SCCP_UDT();
		f_IPA_send(0, data);
		alt {
		[] as_count_rx_sls(2, data, sls_num_rx_1, num_rx_1);
		[] as_count_rx_sls(3, data, sls_num_rx_2, num_rx_2);
		}
	}

	/* All traffic should still be sent to the same receiver, since
	 * OPC+DPC+SLS cannot change in IPA ASPs.
	 * However, depending on how the SLS seed tables were allocated, it can
	 * be that STP was routing through Alternative Route and now that the
	 * 2nd receiver is up it is now routing to it (Normal Route). Account
	 * for both scenarios. */
	if (not ((num_rx_1 == iter_per_asp and num_rx_2 == 0) or (num_rx_2 == iter_per_asp and num_rx_1 == 0))) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Expected all traffic through same receiver!");
	}


	/* activate the second 'sender' side ASP */
	f_connect_ipa(1);

	num_rx_1 := 0;
	num_rx_2 := 0;
	sls_num_rx_1 := {};
	sls_num_rx_2 := {};
	for (i := 0; i < NUM_SLS; i := i + 1) {
		sls_num_rx_1 := sls_num_rx_1 & {0};
		sls_num_rx_2 := sls_num_rx_2 & {0};
	}
	for (i := 0; i < 2*iter_per_asp; i := i+1) {
		var octetstring data := f_SCCP_UDT();
		f_IPA_send(i mod 2, data);
		alt {
		[] as_count_rx_sls(2, data, sls_num_rx_1, num_rx_1);
		[] as_count_rx_sls(3, data, sls_num_rx_2, num_rx_2);
		}
	}
	/* Make sure traffic was sent over both ASPs and that it was distributed: */
	if (num_rx_1 == 0) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Rx 0 packets in 1st ASP!");
	}
	if (num_rx_2 == 0) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, "Rx 0 packets in 2nd ASP!");
	}
	if (num_rx_1 + num_rx_2 != 2*iter_per_asp) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Rx ", num_rx_1 + num_rx_2 ," packets in total vs exp ", 2*iter_per_asp));
	}
	setverdict(pass);
}
testcase TC_combinedlset_loadshare() runs on IPA_CT {
	f_tc_combinedlset_loadshare(false);
}
/* same as above, with "accept-asp-connections dynamic-permitted" and clients from unknown ASPs */
testcase TC_unknown_client_dynamic_combinedlset_loadshare() runs on IPA_CT {
	f_tc_combinedlset_loadshare(true);
}

/* test "traffic-mode override" behavior */
testcase TC_inaccessible_sp() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-override-sender" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	f_init_ipa(ipa_configs := ipa_configs);

	/* bring up the 'sender' side (single ASP in AS) */
	f_connect_ipa(0);
	/* Here none of ipa-asp-override-receiver* is brought up on purpose, so test route failure inside IUT. */

	var octetstring data := f_SCCP_UDT();
	f_IPA_send(0, data);

	/* STP discards the message to be forwarded, since its destination is not accessible. */
	/* FIXME: DUNA doesn't exist in IPA. Sould we probably receive SCCP UDTS here? */
	f_sleep(1.0);

	setverdict(pass);
}

private function f_IPA_ping(integer idx) runs on IPA_CT
{
	var IpaCcmMsgtype ipa_ping_msg := IPAC_MSGT_PING;
	var IpaCcmMsgtype ipa_pong_msg := IPAC_MSGT_PONG;
	var octetstring ipa_ping_data := int2oct(enum2int(ipa_ping_msg), 1);
	var octetstring ipa_pong_data := int2oct(enum2int(ipa_pong_msg), 1);
	timer T := 3.0;
	T.start;

	IPA_CTRL[idx].send(valueof(t_ASP_IPA_UD(IPAC_PROTO_CCM, ipa_ping_data)));

	alt {
	[] IPA_CTRL[idx].receive(t_ASP_IPA_UD(IPAC_PROTO_CCM, ipa_pong_data)) {
		setverdict(pass);
		}
	[] IPA_CTRL[idx].receive(t_ASP_IPA_UD(IPAC_PROTO_CCM, ipa_ping_data)) {
		/* Answer any PING from peer meanwhile: */
		IPA_CTRL[0].send(valueof(t_ASP_IPA_UD(IPAC_PROTO_CCM, ipa_pong_data)));
		repeat;
		}
	}
}

/* Test administrative state, VTY "[no] shutdown" */
testcase TC_ipa_tcp_srv_adm_shutdown() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-override-sender" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	f_init_ipa(ipa_configs := ipa_configs);
	f_connect_ipa(0);

	f_sleep(1.0);
	f_vty_cs7_ipa_asp_cmd(ipa_configs[0], "shutdown");
	log("ASP should now be DOWN")
	as_IPA_wait_tcp_conn_closed(0);
	f_clear_ipa();
	f_init_ipa(ipa_configs := ipa_configs);

	f_sleep(1.0);
	/* Make sure SCTP conn will be closed when we connect to it. */
	log("Connecting to ASP (expect closed):");
	f_connect_ipa(0, exp_act := false);
	f_clear_ipa();

	/* Now let the ASP be active again, it should allow us to reconnect: */
	f_vty_cs7_ipa_asp_cmd(ipa_configs[0], "no shutdown");
	log("Connecting to ASP (expect success):");
	f_init_ipa(ipa_configs := ipa_configs);
	f_connect_ipa(0);
	setverdict(pass);
}

/* Test administrative state, VTY "[no] shutdown" */
testcase TC_ipa_tcp_cli_adm_shutdown() runs on IPA_CT {
	var PortEvent pev;
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-client0" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);

	f_init_ipa(ipa_configs := ipa_configs);
	f_listen_ipa(0);
	f_sleep(1.0);
	f_vty_cs7_ipa_asp_cmd(ipa_configs[0], "shutdown");
	log("ASP should now be DOWN")
	as_IPA_wait_tcp_conn_closed(0);
	f_clear_ipa();

	/* Wait for a while to make sure ASP doesn't reconnect to us: */
	f_init_ipa(ipa_configs := ipa_configs);
	f_start_ipa_server(0);
	f_exp_no_ipa_conn(0, to_val := 10.0);
	f_clear_ipa();

	/* Now let the ASP be active again, it should reconnect to us: */
	f_vty_cs7_ipa_asp_cmd(ipa_configs[0], "no shutdown");
	log("Waiting for client ASP to reconnect to us");
	f_init_ipa(ipa_configs := ipa_configs);
	f_listen_ipa(0);
	setverdict(pass);
}

testcase TC_beat() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-override-sender" };
	var IpaConfigs ipa_configs := ipa_build_configs(asps);

	f_init_common();
	f_init_ipa(ipa_configs := ipa_configs);
	f_connect_ipa(0);

	f_ipa_cfg_chg_ccm_enabled(IPA_CFG[0], false);
	f_IPA_ping(0);
}

private function f_TC_beat_timeout(Misc_Helpers.ro_charstring asps) runs on IPA_CT
{
	var IpaConfigs ipa_configs := ipa_build_configs(asps);
	var ASP_MTP3_TRANSFERind rx;
	var float timeout_val := 3.0 + 1.0;
	var IpaCcmMsgtype ipa_ping_msg := IPAC_MSGT_PING;
	var IpaCcmMsgtype ipa_pong_msg := IPAC_MSGT_PONG;
	var octetstring ipa_ping_data := int2oct(enum2int(ipa_ping_msg), 1);
	var octetstring ipa_pong_data := int2oct(enum2int(ipa_pong_msg), 1);

	f_init_common();
	f_init_ipa(ipa_configs := ipa_configs);

	f_vty_cs7_ipa_asp_cmd(ipa_configs[0], "timer xua beat 3");

	if (ipa_configs[0].is_server) {
		f_listen_ipa(0);
	} else {
		f_connect_ipa(0);
	}
	/* receive CCM IPA PING: */
	f_ipa_cfg_chg_ccm_enabled(IPA_CFG[0], false);

	IPA_CTRL[0].receive(t_ASP_IPA_UD(IPAC_PROTO_CCM, ipa_ping_data));
	log("1st PING received");
	IPA_CTRL[0].send(valueof(t_ASP_IPA_UD(IPAC_PROTO_CCM, ipa_pong_data)));

	/* Make sure next HEARBEAT arrives around the time we configured.
	 * Do not answer this nor next heardbeat: */
	IPA_CTRL[0].receive(t_ASP_IPA_UD(IPAC_PROTO_CCM, ipa_ping_data));
	log("2nd PING received");
	IPA_CTRL[0].receive(t_ASP_IPA_UD(IPAC_PROTO_CCM, ipa_ping_data));
	log("3rd PING received");

	/* After 2*T(beat), IUT should figure out peer is not responding and terminate the conn: */
	timer T := timeout_val;
	T.start;
	alt {
	[] IPA_CTRL[0].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_DOWN)) {
		setverdict(pass);
	}
	[] IPA[0].receive(t_ASP_MTP3_TRANSFERind(?, ?, ?, ?, ?)) -> value rx {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Received unexpected IPA ", rx, " while waiting for connClosed"));
		}
	[] T.timeout {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					log2str("Timeout waiting for IPA connClosed"));
		}
	}

	/* return to default value: */
	f_vty_cs7_ipa_asp_cmd(g_ipa_configs[0], "timer xua beat 30");
}

/* Test the IUT sends heartbeat procedure when needed. */
testcase TC_beat_timeout() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-override-sender" };
	f_TC_beat_timeout(asps);
}

/* Test the IUT sends heartbeat procedure when needed. */
testcase TC_clnt_beat_timeout() runs on IPA_CT {
	var Misc_Helpers.ro_charstring asps := { "ipa-asp-client0" };
	f_TC_beat_timeout(asps);
}

control {
	execute( TC_unknown_client_nodynamic() );
	execute( TC_unknown_as_client_nodynamic() );
	execute( TC_known_client_nodynamic() );
	execute( TC_unknown_client_dynamic() );
	execute( TC_unknown_as_client_dynamic() );
	execute( TC_tmt_override() );
	execute( TC_unknown_client_dynamic_tmt_override() );
	execute( TC_tmt_loadshare_roundrobin() );
	execute( TC_tmt_loadshare_sls() );
	execute( TC_unknown_client_dynamic_tmt_loadshare() );
	execute( TC_combinedlset_loadshare() );
	execute( TC_unknown_client_dynamic_combinedlset_loadshare() );
	execute( TC_inaccessible_sp() );

	execute( TC_ipa_tcp_srv_adm_shutdown() );
	execute( TC_ipa_tcp_cli_adm_shutdown() );

	execute( TC_beat() );
	execute( TC_beat_timeout() );
	execute( TC_clnt_beat_timeout() );
}


}
