/* (C) 2018 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
 * Author: Stefan Sperling <ssperling@sysmocom.de>
 * 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 module provides functions which implement IPA protocol tests.
 * There are no test cases defined here. Instead, there are test functions which
 * can be called by test cases in our test suites. Each such function will create
 * an IPA_CT component and execute a test on this component, and expects destination
 * IP address, TCP port, and connection mode parameters. Depending on the connection
 * mode, a test function will either connect to an IPA server on the specified
 * address and port, or listen for an IPA client on the specified address and port.
 * This allows IPA tests to be run against any IPA speakers used by various test suites.
 */

module IPA_Testing {

import from IPL4asp_Types all;
import from IPL4asp_PortType all;
import from IPA_Types all;
import from Osmocom_Types all;

type enumerated IPA_ConnectionMode {
	CONNECT_TO_SERVER,
	LISTEN_FOR_CLIENT
};

/* Encoded IPA messages (network byte order) */
const octetstring ipa_msg_ping := '0001FE00'O;
const octetstring ipa_msg_pong := '0001FE01'O;
const octetstring ipa_msg_id_req_hdr := '0007FE'O;
const octetstring ipa_msg_id_req_payload := '04010801070102'O;

/* A component which represents the system on which the IPA speaker is running. */
type component system_CT {
	port IPL4asp_PT IPL4;
}

/* Main component provided by this module. */
type component IPA_CT {
	port IPL4asp_PT IPL4;
	timer g_Tguard;
}

/* This guard timer prevents us from waiting too long if the IPA TCP connection hangs. */
private altstep as_Tguard() runs on IPA_CT {
	[] g_Tguard.timeout {
		setverdict(fail, "Tguard timeout");
		mtc.stop;
	}
}

/* Send an encoded IPA message across an IPA TCP connection. */
private function f_send_ipa_data(charstring ipa_ip, integer ipa_tcp_port, ConnectionId connId,
				 octetstring data) runs on IPA_CT {
	var IPL4asp_Types.Result res;
	var ASP_SendTo asp := {
		connId := connId,
		remName := ipa_ip,
		remPort := ipa_tcp_port,
		proto := {tcp := {}},
		msg := data
	};
	IPL4.send(asp);
}

/* Match an incoming IPA message. */
private template ASP_RecvFrom t_recvfrom(template octetstring msg) := {
	connId := ?,
	remName := ?,
	remPort := ?,
	locName := ?,
	locPort := ?,
	proto := {tcp := {}},
	userData := ?,
	msg := msg
}

/* Perform set up steps for a test function. */
private function f_init(charstring ipa_ip, integer ipa_tcp_port,
			IPA_ConnectionMode conmode) runs on IPA_CT return ConnectionId {
	var IPL4asp_Types.Result res;
	var ConnectionId connId;

	map(self:IPL4, system:IPL4);
	if (conmode == CONNECT_TO_SERVER) {
		/* Create an IPA connection over TCP. */
		res := IPL4asp_PortType.f_IPL4_connect(IPL4, ipa_ip, ipa_tcp_port, "", -1, 0, {tcp := {}});
		if (not ispresent(res.connId)) {
			setverdict(fail, "Could not connect IPA socket to ", ipa_ip, " port ",
				   ipa_tcp_port, "; check your configuration");
			mtc.stop;
		}
	} else {
		/* Listen for an incoming IPA connection on TCP. */
		res := IPL4asp_PortType.f_IPL4_listen(IPL4, ipa_ip, ipa_tcp_port, {tcp := {}});
		if (not ispresent(res.connId)) {
			setverdict(fail, "Could not listen on address ", ipa_ip, " port ",
				   ipa_tcp_port, "; check your configuration");
			mtc.stop;
		}
	}

	/*
	 * Activate guard timer. When changing the timeout value, keep in mind
	 * that test functions below may wait for some amount of time, which
	 * this guard timer should always exceed to avoid spurious failures.
	 */
	g_Tguard.start(60.0);
	activate(as_Tguard());

	return res.connId;
}

/*
 * Individual test case implementations.
 */

private function f_send_chopped_ipa_msg(charstring ipa_ip, integer ipa_tcp_port, ConnectionId connId,
					octetstring msg) runs on IPA_CT {
	const float delay := 6.0;
	for (var integer i := 0; i < lengthof(msg); i := i + 1) {
		log("sending byte ", msg[i]);
		f_send_ipa_data(ipa_ip, ipa_tcp_port, connId, msg[i]);
		f_sleep(delay);
	}
}

/* Send a ping message one byte at a time, waiting for TCP buffer to flush between each byte. */
private function f_TC_chopped_ipa_ping(charstring ipa_ip, integer ipa_tcp_port,
				       IPA_ConnectionMode conmode) runs on IPA_CT system system_CT {
	var ConnectionId connId;
	var ASP_RecvFrom asp_rx;

	connId := f_init(ipa_ip, ipa_tcp_port, conmode);

	if (conmode == CONNECT_TO_SERVER) {
		f_send_chopped_ipa_msg(ipa_ip, ipa_tcp_port, connId, ipa_msg_ping);
	} else {
		var PortEvent port_evt;
		IPL4.receive(PortEvent:{connOpened := ?}) -> value port_evt {
			var ConnectionOpenedEvent conn := port_evt.connOpened;
			f_send_chopped_ipa_msg(conn.remName, conn.remPort, conn.connId, ipa_msg_ping);
		}
	}

	/* Expect a pong response. */
	alt {
		[] IPL4.receive(t_recvfrom(ipa_msg_pong)) -> value asp_rx {
			log("received pong from ", asp_rx.remName, " port ", asp_rx.remPort, ": ", asp_rx.msg);
			setverdict(pass);
		}
		[] IPL4.receive {
			repeat;
		}
	}
}

/* Send a complete IPA "ID REQ" message header in one piece, and then send the payload one byte at a time,
 * waiting for TCP buffer to flush between each byte. */
private function f_TC_chopped_ipa_payload(charstring ipa_ip, integer ipa_tcp_port,
					  IPA_ConnectionMode conmode) runs on IPA_CT system system_CT {
	var ConnectionId connId;
	var ASP_RecvFrom asp_rx;

	connId := f_init(ipa_ip, ipa_tcp_port, conmode);

	if (conmode == CONNECT_TO_SERVER) {
		var PortEvent port_evt;
		f_send_ipa_data(ipa_ip, ipa_tcp_port, connId, ipa_msg_id_req_hdr);
		f_send_chopped_ipa_msg(ipa_ip, ipa_tcp_port, connId, ipa_msg_id_req_payload);
		/* Server will close the connection upon receiving an ID REQ. */
		alt {
			[] IPL4.receive(PortEvent:{connClosed := ?}) -> value port_evt {
				if (port_evt.connClosed.connId == connId) {
					setverdict(pass);
				} else {
					repeat;
				}
			}
			[] IPL4.receive {
				repeat;
			}
		}
	} else {
		var PortEvent port_evt;
		IPL4.receive(PortEvent:{connOpened := ?}) -> value port_evt {
			var ConnectionOpenedEvent conn := port_evt.connOpened;
			f_send_ipa_data(conn.remName, conn.remPort, conn.connId, ipa_msg_id_req_hdr);
			f_send_chopped_ipa_msg(conn.remName, conn.remPort, conn.connId, ipa_msg_id_req_payload);
		}

		/* Expect an encoded IPA ID RESP message from the client. */
		alt {
			[] IPL4.receive(t_recvfrom(?)) -> value asp_rx {
				log("received IPA message from ", asp_rx.remName, " port ", asp_rx.remPort, ": ",
				    asp_rx.msg);
				if (lengthof(asp_rx.msg) > 4
				    and asp_rx.msg[2] == 'FE'O /* PROTO_IPACCESS */
				    and asp_rx.msg[3] == '05'O /* ID RESP */) {
					setverdict(pass);
				} else {
					repeat;
				}
			}
			[] IPL4.receive {
				repeat;
			}
		}
	}
}

/*
 * Public functions.
 * Test suites may call these functions to create an IPA_CT component and run a test to completion.
 */

function f_run_TC_chopped_ipa_ping(charstring ipa_ip, integer ipa_tcp_port, IPA_ConnectionMode conmode) {
	var IPA_Testing.IPA_CT vc_IPA_Testing := IPA_Testing.IPA_CT.create;
	vc_IPA_Testing.start(IPA_Testing.f_TC_chopped_ipa_ping(ipa_ip, ipa_tcp_port, conmode));
	vc_IPA_Testing.done;
}

function f_run_TC_chopped_ipa_payload(charstring ipa_ip, integer ipa_tcp_port, IPA_ConnectionMode conmode) {
	var IPA_Testing.IPA_CT vc_IPA_Testing := IPA_Testing.IPA_CT.create;
	vc_IPA_Testing.start(IPA_Testing.f_TC_chopped_ipa_payload(ipa_ip, ipa_tcp_port, conmode));
	vc_IPA_Testing.done;
}

}
