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;

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_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 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;
	AspNameArray mp_ipa_as_names := {"ipa-as-loadshare-sender",
					 "ipa-as-loadshare-sender",
					 "ipa-as-loadshare-receiver",
					 "ipa-as-loadshare-receiver",
					 "ipa-as-dynamic-asp",
					 "ipa-as-override-sender",
					 "ipa-as-override-receiver",
					 "ipa-as-override-receiver"
					 };
}

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[NR_IPA];
	port IPA_SP_PT IPA_CTRL[NR_IPA];
	var IPA_Emulation_CT vc_IPA[NR_IPA];
	var IPA_CCM_Parameters g_ccm_pars[NR_IPA];
}

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) runs on IPA_CT {
	alt {
	[] IPA[idx].receive(t_ASP_MTP3_TRANSFERind(?, ?, ?, ?, data)) {
		setverdict(pass);
		}
	[] IPA[idx].receive {
		setverdict(fail, "Received unexpected data on IPA port while waiting for ", data);
		mtc.stop;
		}
	}
}

private function f_rnd_ipa_len() runs on IPA_CT return integer {
	var integer rnd_len := f_rnd_int(100);
	/* We need at least 1 byte of data, othewise osmocom IPA stack will discard and close the socket */
	if (rnd_len == 0) {
		rnd_len := 1;
	}
	return rnd_len;
}

/* 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_rnd_octstring(f_rnd_ipa_len());
	f_IPA_send(idx_tx, data);
	f_IPA_exp(idx_rx, data);
}

friend function f_init_ipa() runs on IPA_CT {
	var integer i;

	f_init_common();

	for (i := 0; i < NR_IPA; 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);
		g_ccm_pars[i] := c_IPA_default_ccm_pars;
		g_ccm_pars[i].name := mp_ipa_as_names[i];
	}
}

friend function f_connect_ipa(integer idx, boolean use_unknown_asp_port := false, boolean exp_act := true) runs on IPA_CT {
	var integer port_offset := 0;
	if (use_unknown_asp_port) {
		/* Add 100 to the port since we know that port is not configured in any
		  ASP only up to NR_IPA are configured. */
		port_offset := 100;
	}
	vc_IPA[idx].start(IPA_Emulation.main_client(mp_stp_ipa_ip, mp_stp_ipa_port, mp_local_ipa_ip,
			mp_local_ipa_port + idx + port_offset, 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; }
	}
}


/* "accept-asp-connections pre-configured" and client from unknown source */
testcase TC_unknown_client_nodynamic() runs on IPA_CT {
	f_init_common();
	f_vty_config2(VTY, {"cs7 instance 0", "listen ipa 5000"},
		      "accept-asp-connections pre-configured");
	f_init_ipa();
	f_connect_ipa(0, true, false);

	/* switch back to default */
	f_vty_config2(VTY, {"cs7 instance 0", "listen ipa 5000"},
		      "accept-asp-connections dynamic-permitted");
	setverdict(pass);
}

/* "accept-asp-connections pre-configured" and client from known source */
testcase TC_known_client_nodynamic() runs on IPA_CT {
	f_init_common();
	f_vty_config2(VTY, {"cs7 instance 0", "listen ipa 5000"},
		      "accept-asp-connections pre-configured");
	f_init_ipa();
	f_connect_ipa(0, false);

	/* switch back to default */
	f_vty_config2(VTY, {"cs7 instance 0", "listen ipa 5000"},
		      "accept-asp-connections dynamic-permitted");
	setverdict(pass);
}


/* "accept-asp-connections dynamic-permitted" and client from unknown source */
testcase TC_unknown_client_dynamic() runs on IPA_CT {
	f_init_common();
	f_init_ipa();
	f_connect_ipa(0, true);
	setverdict(pass);
}

private function f_tc_tmt_override(boolean unknwon_dynamic_asp)
runs on IPA_CT {
	f_init_ipa();

	/* bring up the 'sender' side (single ASP in AS) */
	f_connect_ipa(5, unknwon_dynamic_asp);
	/* activate the first 'receiver' side ASP */
	f_connect_ipa(6, unknwon_dynamic_asp);

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

	/* activate the second 'receiver' side ASP */
	f_connect_ipa(7, unknwon_dynamic_asp);

	/* verify traffic is routed from sender to new receiver */
	f_test_traffic(5, 7);
	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 unknwon_dynamic_asp)
runs on IPA_CT {
	var integer i;

	f_init_ipa();

	f_vty_config2(VTY, {"cs7 instance 0", "as " & mp_ipa_as_names[2] & " ipa"}, "traffic-mode roundrobin");

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

	/* 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, unknwon_dynamic_asp);

	/* 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_rnd_octstring(f_rnd_ipa_len());
		f_IPA_send(0, data);
		alt {
		[] as_count_rx(2, data, num_rx[0]);
		[] as_count_rx(3, 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_config2(VTY, {"cs7 instance 0", "as " & mp_ipa_as_names[2] & " ipa"}, "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 unknwon_dynamic_asp)
runs on IPA_CT {
	var integer i;

	f_init_ipa();

	f_vty_config2(VTY, {"cs7 instance 0", "as " & mp_ipa_as_names[2] & " ipa"}, "traffic-mode loadshare");

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

	/* 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, unknwon_dynamic_asp);

	/* 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_rnd_octstring(f_rnd_ipa_len());
		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 not still be sent to the same receiver, since
	 * OPC+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, unknwon_dynamic_asp);


	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_rnd_octstring(f_rnd_ipa_len());
		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_config2(VTY, {"cs7 instance 0", "as " & mp_ipa_as_names[2] & " ipa"}, "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);
}

control {
	execute( TC_unknown_client_nodynamic() );
	execute( TC_known_client_nodynamic() );
	execute( TC_unknown_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() );
}


}