module RemsimBankd_Tests {

/* Integration Tests for osmo-remsim-bankd
 * (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 osmo-remsim-bankd by attaching to the external interfaces
 * such as RSPRO for simulated clients + server.
 */

import from Osmocom_Types all;
import from IPA_Emulation all;
import from Misc_Helpers all;

import from VPCD_Types all;
import from VPCD_CodecPort all;
import from VPCD_Adapter all;

import from RSPRO all;
import from RSRES all;
import from RSPRO_Types all;
import from RSPRO_Server all;
import from REMSIM_Tests all;

modulepar {
	integer mp_bank_id := 1;
	integer mp_num_slots := 8;
}

/* We implement a RSPRO server to simulate the remsim-server and a
   RSPRO client to simulate a remsim-client connecting to bankd */
type component bankd_test_CT extends rspro_server_CT, rspro_client_CT, VPCD_Adapter_CT {
	timer g_T_guard := 60.0;
}

private altstep as_Tguard() runs on bankd_test_CT {
	[] g_T_guard.timeout {
		setverdict(fail, "Timeout of T_guard");
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__);
		}
}

private function f_init(boolean start_client := false) runs on bankd_test_CT {
	var ComponentIdentity srv_comp_id := valueof(ts_CompId(remsimServer, "ttcn-server"));

	/* Start the guard timer */
	g_T_guard.start;
	activate(as_Tguard());

	f_rspro_srv_init(0, mp_server_ip, mp_server_port, srv_comp_id);

	if (start_client) {
		f_init_client(0);
	}
}

private function f_init_client(integer i := 0) runs on rspro_client_CT {
	var ComponentIdentity clnt_comp_id := valueof(ts_CompId(remsimClient, "ttcn-client"));
	f_rspro_init(rspro[0], mp_bankd_ip, mp_bankd_port, clnt_comp_id, 0);
	rspro[0].rspro_client_slot := { clientId := 23+i, slotNr := 0 };
}



/* Test if the bankd disconnects the TCP/IPA session if we don't respond to connectBankReq */
testcase TC_connectBankReq_timeout() runs on bankd_test_CT {
	timer T := 20.0;
	f_init();

	f_rspro_srv_exp(tr_RSPRO_ConnectBankReq(?, ?, ?));
	T.start;
	alt {
	[] RSPRO_SRV[0].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_DOWN)) {
		setverdict(pass);
		}
	[] RSPRO_SRV[0].receive { repeat; }
	[] T.timeout {
		setverdict(fail, "Timeout waiting for disconnect");
		}
	}
}

/* accept an inbound connection from bankd to simulated server */
testcase TC_connectBankReq() runs on bankd_test_CT {
	f_init();

	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* attempt to create a mapping */
testcase TC_createMapping() runs on bankd_test_CT {
	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);
	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	var ClientSlot cs := { clientId := 23, slotNr := 42 };
	f_rspro_srv_create_slotmap(cs, bs);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* attempt to create a mapping for a slot that already has a mapping */
testcase TC_createMapping_busySlot() runs on bankd_test_CT {
	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);
	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	var ClientSlot cs := { clientId := 23, slotNr := 42 };
	/* create the mapping the first time */
	f_rspro_srv_create_slotmap(cs, bs);
	/* re-create the mapping a second time */
	f_rspro_srv_create_slotmap(cs, bs);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* attempt to create a mapping for an out-of-range slot number */
testcase TC_createMapping_invalidSlot() runs on bankd_test_CT {
	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);
	var BankSlot bs := { bankId := mp_bank_id, slotNr := 200 };
	var ClientSlot cs := { clientId := 23, slotNr := 42 };
	f_rspro_srv_create_slotmap(cs, bs, exp_res := illegalSlotId);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* attempt to create a mapping for an invalid bankID */
testcase TC_createMapping_invalidBank() runs on bankd_test_CT {
	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);
	var BankSlot bs := { bankId := 200, slotNr := 0 };
	var ClientSlot cs := { clientId := 23, slotNr := 42 };
	f_rspro_srv_create_slotmap(cs, bs, exp_res := illegalBankId);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* attempt to remove a non-existant mapping */
testcase TC_removeMapping_unknownMap() runs on bankd_test_CT {
	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);
	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	var ClientSlot cs := { clientId := 23, slotNr := 42 };
	f_rspro_srv_remove_slotmap(cs, bs, exp_res := unknownSlotmap);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* add and then remove a mapping, expect both to be successful */
testcase TC_removeMapping() runs on bankd_test_CT {
	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);
	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	var ClientSlot cs := { clientId := 23, slotNr := 42 };
	f_rspro_srv_create_slotmap(cs, bs);
	f_rspro_srv_remove_slotmap(cs, bs);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* connect from client to bankd without specifying a clientId */
testcase TC_clientConnect_missingSlot() runs on bankd_test_CT {
	f_init_client(0);
	RSPRO[0].send(ts_RSPRO_ConnectClientReq(rspro[0].rspro_id, omit));
	f_rspro_exp(tr_RSPRO_ConnectClientRes(?, ResultCode:illegalClientId), 0);
	f_rspro_exp_disconnect(0);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* connect from client to bankd using a clientId for which bankd has no map */
testcase TC_clientConnect_unknown() runs on bankd_test_CT {
	f_init_client(0);
	f_rspro_connect_client(0, tr_Status_ok_or_nocard);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* connect from client to bankd using a clientSlot for which bankd has no map */


/* first connect client, then later add matching mapping from server */
testcase TC_clientConnect_createMapping() runs on bankd_test_CT {
	f_init_client(0);
	f_rspro_connect_client(0, tr_Status_ok_or_nocard);

	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);

	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	f_rspro_srv_create_slotmap(rspro[0].rspro_client_slot, bs);
	f_sleep(10.0);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}


/* first add mapping, then connect matching client */
testcase TC_createMapping_clientConnect() runs on bankd_test_CT {
	/* FIXME: this would only be done in f_init_client(), but we need it before */
	rspro[0].rspro_client_slot := { clientId := 23+0, slotNr := 0 };

	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);

	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	f_rspro_srv_create_slotmap(rspro[0].rspro_client_slot, bs);

	f_sleep(1.0);

	f_init_client(0);
	f_rspro_connect_client(0, tr_Status_ok_or_nocard);
	/* FIXME: how to determine that bank correctly mapped us */
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* first add mapping, then connect client, then change mapping (expect disconnect) */
testcase TC_createMapping_connectClient_changeMapping() runs on bankd_test_CT {
	/* FIXME: this would only be done in f_init_client(), but we need it before */
	rspro[0].rspro_client_slot := { clientId := 23+0, slotNr := 0 };

	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);

	/* create slotmap */
	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	f_rspro_srv_create_slotmap(rspro[0].rspro_client_slot, bs);

	f_sleep(1.0);

	/* connect client */
	f_init_client(0);
	f_rspro_connect_client(0, tr_Status_ok_or_nocard);
	/* FIXME: how to determine that bank correctly mapped us */

	/* create another mapping for the same bank-slot */
	var ClientSlot cs := { clientId := 987, slotNr := 654 };
	f_rspro_srv_create_slotmap(cs, bs);

	/* expect client to be disconnected */
	timer T := 5.0;
	T.start;
	alt {
	[] RSPRO[0].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_DOWN)) {
		setverdict(pass);
		}
	[] RSPRO[0].receive {
		setverdict(fail, "Unexpected RSPRO on client connection");
		}
	[] T.timeout {
		setverdict(fail, "Timeout waiting for client being disconnected");
		}
	}
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

/* first add mapping, then connect client, then re-crete mapping (expect no disconnect) */
testcase TC_createMapping_connectClient_recreateMapping() runs on bankd_test_CT {
	/* FIXME: this would only be done in f_init_client(), but we need it before */
	rspro[0].rspro_client_slot := { clientId := 23+0, slotNr := 0 };

	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);

	/* create slotmap */
	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	f_rspro_srv_create_slotmap(rspro[0].rspro_client_slot, bs);

	f_sleep(1.0);

	/* connect client */
	f_init_client(0);
	f_rspro_connect_client(0, tr_Status_ok_or_nocard);
	/* FIXME: how to determine that bank correctly mapped us */

	/* re-create same mapping for the same bank-slot */
	f_rspro_srv_create_slotmap(rspro[0].rspro_client_slot, bs);

	/* expect client not to be disconnected */
	timer T := 5.0;
	T.start;
	alt {
	[] RSPRO[0].receive(tr_ASP_IPA_EV(ASP_IPA_EVENT_DOWN)) {
		setverdict(fail, "Unexpected client disconnect");
		}
	[] RSPRO[0].receive {
		repeat;
		}
	[] T.timeout {
		setverdict(pass);
		}
	}
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}



/* add mapping, connect matching client, disconnect + reconnect */
testcase TC_createMapping_clientReconnect() runs on bankd_test_CT {
	/* FIXME: this would only be done in f_init_client(), but we need it before */
	rspro[0].rspro_client_slot := { clientId := 23+0, slotNr := 0 };

	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);

	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	f_rspro_srv_create_slotmap(rspro[0].rspro_client_slot, bs);

	f_sleep(1.0);

	f_init_client(0);
	f_rspro_connect_client(0, tr_Status_ok_or_nocard);
	/* TODO: works only with empty slot, as setAtrReq isn't handled */
	/* FIXME: how to determine that bank correctly mapped us */
	f_sleep(5.0);
	f_rspro_fini(rspro[0], 0);

	f_init_client(0);
	f_rspro_connect_client(0, tr_Status_ok_or_nocard);
	/* FIXME: how to determine that bank correctly mapped us */
	f_sleep(5.0);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}



/* remove mapping while client is connected */
testcase TC_removeMapping_connected() runs on bankd_test_CT {
	f_init_client(0);
	f_rspro_connect_client(0, tr_Status_ok_or_nocard);
	/* TODO: works only with empty slot, as setAtrReq isn't handled */

	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);

	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	f_rspro_srv_create_slotmap(rspro[0].rspro_client_slot, bs);
	/* FIXME: how to determine that bank correctly mapped us */
	f_sleep(5.0);
	f_rspro_srv_remove_slotmap(rspro[0].rspro_client_slot, bs);
	f_rspro_exp_disconnect(0);
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}

private altstep as_vpcd() runs on VPCD_Adapter_CT {
	[] VPCD.receive(tr_VPCD_Recv(g_vpcd_conn_id, tr_VPCD_CTRL_ATR)) {
		f_vpcd_send(ts_VPCD_DATA('3B9F96801FC78031A073BE21136743200718000001A5'O));
		repeat;
		}
	[] VPCD.receive(tr_VPCD_Recv(g_vpcd_conn_id, tr_VPCD_CTRL_OFF)) {
		repeat;
		}
	[] VPCD.receive(tr_VPCD_Recv(g_vpcd_conn_id, tr_VPCD_CTRL_ON)) {
		repeat;
		}
}

/* transceive a TPDU from modem to card (and back) */
function f_rspro_xceive_mdm2card_vpcd(integer idx, BankSlot bs, template (value) octetstring data,
				 template (value) TpduFlags flags, template (value) octetstring res) runs on bankd_test_CT return octetstring {
	var RsproPDU rx;
	RSPRO[idx].send(ts_RSPRO_TpduModemToCard(rspro[idx].rspro_client_slot, bs, flags, data));
	f_vpcd_exp(tr_VPCD_DATA(data));
	f_vpcd_send(ts_VPCD_DATA(res));
	rx := f_rspro_exp(tr_RSPRO_TpduCardToModem(bs, rspro[idx].rspro_client_slot, ?, ?));
	if (rx.msg.tpduCardToModem.data != valueof(res)) {
		setverdict(fail, "Expected ", res, " from card, but got ", rx.msg.tpduCardToModem.data);
	}
	return rx.msg.tpduCardToModem.data;
}


/* first add mapping, then connect matching client and exchange some TPDUs */
testcase TC_createMapping_exchangeTPDU() runs on bankd_test_CT {
	/* FIXME: this would only be done in f_init_client(), but we need it before */
	rspro[0].rspro_client_slot := { clientId := 23+0, slotNr := 0 };

	VPCD_Adapter.f_connect();
	activate(as_vpcd());
	f_sleep(2.0);

	f_init();
	as_connectBankReq(bid := mp_bank_id, nslots := mp_num_slots);
	f_rspro_srv_reset_state(ok);

	var BankSlot bs := { bankId := mp_bank_id, slotNr := 0 };
	f_rspro_srv_create_slotmap(rspro[0].rspro_client_slot, bs);

	f_sleep(1.0);

	f_init_client(0);
	f_rspro_connect_client(0, ok);
	/* FIXME: how to determine that bank correctly mapped us */
	f_rspro_exp(tr_RSPRO_SetAtrReq(rspro[0].rspro_client_slot, ?));

	var TpduFlags f := {tpduHeaderPresent:=true, finalPart:=true, procByteContinueTx:=false,
			    procByteContinueRx:=false};
	for (var integer i := 0; i < 10; i:=i+1) {
		f_rspro_xceive_mdm2card_vpcd(0, bs, 'A0A40000023F00'O, f, '9000'O);
	}
	Misc_Helpers.f_shutdown(__BFILE__, __LINE__, pass);
}



control {
	execute( TC_connectBankReq_timeout() );
	execute( TC_connectBankReq() );

	execute( TC_createMapping() );
	execute( TC_createMapping_busySlot() );
	execute( TC_createMapping_invalidSlot() );
	execute( TC_createMapping_invalidBank() );

	execute( TC_removeMapping_unknownMap() );
	execute( TC_removeMapping() );

	execute( TC_clientConnect_missingSlot() );
	execute( TC_clientConnect_unknown() );
	execute( TC_clientConnect_createMapping() );
	execute( TC_createMapping_clientConnect() );
	execute( TC_createMapping_clientReconnect() );
	execute( TC_removeMapping_connected() );

	execute( TC_createMapping_connectClient_changeMapping() );
	execute( TC_createMapping_connectClient_recreateMapping() );

	execute( TC_createMapping_exchangeTPDU() );
}





}