#include "../src/ss7_as.h"
#include "../src/ss7_asp.h"
#include "../src/ss7_link.h"
#include "../src/ss7_linkset.h"
#include "../src/ss7_route.h"
#include "../src/ss7_route_table.h"
#include "../src/ss7_user.h"
#include "../src/xua_internal.h"
#include "../src/xua_asp_fsm.h"

#include <osmocom/sigtran/osmo_ss7.h>
#include <osmocom/sigtran/protocol/m3ua.h>
#include <osmocom/sigtran/protocol/sua.h>

#include <osmocom/core/utils.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/application.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>

static struct osmo_ss7_instance *s7i;

static void test_pc_transcode(uint32_t pc)
{
	const char *pc_str = osmo_ss7_pointcode_print(s7i, pc);
	uint32_t pc_reenc = osmo_ss7_pointcode_parse(s7i, pc_str);

	printf("%s(%u) -> %s -> %u\n", __func__, pc, pc_str, pc_reenc);
	OSMO_ASSERT(pc == pc_reenc);
}

static void test_pc_defaults(void)
{
	/* ensure the default point code format settings apply */
	OSMO_ASSERT(s7i->cfg.pc_fmt.component_len[0] == 3);
	OSMO_ASSERT(s7i->cfg.pc_fmt.component_len[1] == 8);
	OSMO_ASSERT(s7i->cfg.pc_fmt.component_len[2] == 3);
	OSMO_ASSERT(s7i->cfg.pc_fmt.delimiter == '.');
}

static void parse_print_mask(const char *in)
{
	uint32_t mask = osmo_ss7_pointcode_parse_mask_or_len(s7i, in);
	const char *pc_str = osmo_ss7_pointcode_print(s7i, mask);
	printf("mask %s => %u (0x%x) %s\n", in, mask, mask, pc_str);
}

static void test_pc_parser_itu(void)
{
	/* ITU Style */
	printf("Testing ITU-style point code format\n");
	osmo_ss7_instance_set_pc_fmt(s7i, 3, 8, 3);
	test_pc_transcode(0);
	test_pc_transcode(1);
	test_pc_transcode(1 << 3);
	test_pc_transcode(1 << (3+8));
	test_pc_transcode(7 << (3+8));
	test_pc_transcode(100);
	test_pc_transcode(2342);
	test_pc_transcode((1 << 14)-1);

	parse_print_mask("/1");
	parse_print_mask("7.0.0");
	parse_print_mask("/14");
}

static void test_pc_parser_ansi(void)
{
	/* ANSI Style */
	printf("Testing ANSI-style point code format\n");
	osmo_ss7_instance_set_pc_fmt(s7i, 8, 8, 8);
	s7i->cfg.pc_fmt.delimiter = '-';
	test_pc_transcode(0);
	test_pc_transcode(1);
	test_pc_transcode(1 << 8);
	test_pc_transcode(1 << 16);
	test_pc_transcode(1 << (3+8));
	test_pc_transcode((1 << 24)-1);
	test_pc_transcode(100);
	test_pc_transcode(2342);

	parse_print_mask("/1");
	parse_print_mask("/16");
	parse_print_mask("/24");

	/* re-set to default (ITU) */
	osmo_ss7_instance_set_pc_fmt(s7i, 3, 8, 3);
	s7i->cfg.pc_fmt.delimiter = '.';
}

static int test_user_prim_cb(struct osmo_prim_hdr *oph, void *priv)
{
	OSMO_ASSERT(priv == (void *) 0x1234);

	return 23;
}

static void test_user(void)
{
	struct osmo_ss7_user *user, *user2;
	struct osmo_mtp_prim omp = {
		.oph = {
			.sap = MTP_SAP_USER,
			.primitive = OSMO_MTP_PRIM_TRANSFER,
			.operation = PRIM_OP_INDICATION,
		},
		.u.transfer = {
			.sio = 1,
		},
	};

	printf("Testing SS7 user\n");
	user = osmo_ss7_user_create(s7i, "testuser");
	osmo_ss7_user_set_prim_cb(user, test_user_prim_cb);
	osmo_ss7_user_set_priv(user, (void *) 0x1234);

	user2 = osmo_ss7_user_create(s7i, "testuser_notregistered");
	osmo_ss7_user_set_prim_cb(user, test_user_prim_cb);
	osmo_ss7_user_set_priv(user, (void *) 0x1234);

	/* registration */
	OSMO_ASSERT(osmo_ss7_user_register(s7i, 1, user) == 0);
	OSMO_ASSERT(osmo_ss7_user_register(s7i, 1, NULL) == -EBUSY);
	OSMO_ASSERT(osmo_ss7_user_register(s7i, 255, NULL) == -EINVAL);

	/* primitive delivery */
	OSMO_ASSERT(ss7_mtp_to_user(s7i, &omp) == 23);

	/* cleanup */
	OSMO_ASSERT(osmo_ss7_user_unregister(s7i, 255, NULL) == -EINVAL);
	OSMO_ASSERT(osmo_ss7_user_unregister(s7i, 10, NULL) == -ENODEV);
	OSMO_ASSERT(osmo_ss7_user_unregister(s7i, 1, user2) == -EINVAL);
	OSMO_ASSERT(osmo_ss7_user_unregister(s7i, 1, user) == 0);

	/* primitive delivery should fail now */
	OSMO_ASSERT(ss7_mtp_to_user(s7i, &omp) == -ENODEV);

	/* wrong primitive delivery should also fail */
	omp.oph.primitive = OSMO_MTP_PRIM_PAUSE;
	OSMO_ASSERT(ss7_mtp_to_user(s7i, &omp) == -EINVAL);

	osmo_ss7_user_destroy(user);
	osmo_ss7_user_destroy(user2);
}

#define RT_LABEL(opc_val, dpc_val, sls_val) \
	(struct osmo_ss7_route_label){ .opc = (opc_val), .dpc = (dpc_val), .sls = (sls_val) }

static void test_route(void)
{
	struct osmo_ss7_route_table *rtbl;
	struct osmo_ss7_linkset *lset_a, *lset_b;
	struct osmo_ss7_link *l_a, *l_b;
	struct osmo_ss7_route *rt, *rt12, *rtdef;
	struct osmo_ss7_route_label route_label;

	printf("Testing SS7 routing\n");

	/* creation / destruction */
	OSMO_ASSERT(ss7_route_table_find(s7i, "foobar") == NULL);
	rtbl = ss7_route_table_find_or_create(s7i, "foobar");
	OSMO_ASSERT(rtbl);
	OSMO_ASSERT(ss7_route_table_find_or_create(s7i, "foobar") == rtbl);
	ss7_route_table_destroy(rtbl);
	OSMO_ASSERT(ss7_route_table_find(s7i, "foobar") == NULL);

	/* we now work with system route table */
	rtbl = ss7_route_table_find(s7i, "system");
	OSMO_ASSERT(rtbl && rtbl == s7i->rtable_system);

	lset_a = ss7_linkset_find_or_create(s7i, "a", 100);
	OSMO_ASSERT(lset_a);
	l_a = ss7_link_find_or_create(lset_a, 0);
	OSMO_ASSERT(l_a);
	l_a->cfg.adm_state = OSMO_SS7_LS_ENABLED;

	lset_b = ss7_linkset_find_or_create(s7i, "b", 200);
	OSMO_ASSERT(lset_b);
	l_b = ss7_link_find_or_create(lset_b, 0);
	OSMO_ASSERT(l_b);
	l_b->cfg.adm_state = OSMO_SS7_LS_ENABLED;

	/* route with full mask */
	route_label = RT_LABEL(0, 12, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == NULL);
	rt = ss7_route_create(rtbl, 12, 0xffff, false, "a");
	printf("route with full mask: %s\n", osmo_ss7_route_print(rt));
	OSMO_ASSERT(rt);
	route_label = RT_LABEL(0, 12, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt);
	ss7_route_destroy(rt);

	/* route with partial mask */
	rt = ss7_route_create(rtbl, 8, 0xfff8, false, "a");
	printf("route with partial mask: %s\n", osmo_ss7_route_print(rt));
	route_label = RT_LABEL(0, 8, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt);
	route_label = RT_LABEL(0, 9, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt);
	route_label = RT_LABEL(0, 12, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt);
	route_label = RT_LABEL(0, 15, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt);
	route_label = RT_LABEL(0, 16, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == NULL);
	/* insert more specific route for 12, must have higher priority
	 * than existing one */
	rt12 = ss7_route_create(rtbl, 12, 0xffff, false, "b");
	route_label = RT_LABEL(0, 12, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt12);
	route_label = RT_LABEL(0, 15, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt);
	route_label = RT_LABEL(0, 16, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == NULL);
	/* add a default route, which should have lowest precedence */
	rtdef = ss7_route_create(rtbl, 0, 0, false, "a");
	route_label = RT_LABEL(0, 12, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt12);
	route_label = RT_LABEL(0, 15, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rt);
	route_label = RT_LABEL(0, 16, 0);
	OSMO_ASSERT(ss7_route_table_lookup_route(rtbl, &route_label) == rtdef);

	ss7_route_destroy(rtdef);
	ss7_route_destroy(rt12);
	ss7_route_destroy(rt);

	rt = ss7_route_create(rtbl, 8, 0xfff9, false, "a");
	printf("route with non-consecutive mask: %s\n", osmo_ss7_route_print(rt));
	ss7_route_destroy(rt);

	ss7_linkset_destroy(lset_a);
	ss7_linkset_destroy(lset_b);
}

static void test_linkset(void)
{
	struct osmo_ss7_linkset *lset_a, *lset_b;
	struct osmo_ss7_link *l_a1, *l_a2;

	printf("Testing SS7 linkset/link\n");

	OSMO_ASSERT(ss7_linkset_find_by_name(s7i, "a") == NULL);
	OSMO_ASSERT(ss7_linkset_find_by_name(s7i, "b") == NULL);

	lset_a = ss7_linkset_find_or_create(s7i, "a", 100);
	OSMO_ASSERT(lset_a);
	OSMO_ASSERT(ss7_linkset_find_by_name(s7i, "a") == lset_a);

	lset_b = ss7_linkset_find_or_create(s7i, "b", 200);
	OSMO_ASSERT(lset_b);
	OSMO_ASSERT(ss7_linkset_find_by_name(s7i, "b") == lset_b);

	l_a1 = ss7_link_find_or_create(lset_a, 1);
	OSMO_ASSERT(l_a1);
	l_a2 = ss7_link_find_or_create(lset_a, 2);
	OSMO_ASSERT(l_a2);

	/* ID too high */
	OSMO_ASSERT(ss7_link_find_or_create(lset_a, 1000) == NULL);
	/* already exists */
	OSMO_ASSERT(ss7_link_find_or_create(lset_a, 1) == l_a1);

	ss7_link_destroy(l_a1);
	ss7_link_destroy(l_a2);

	ss7_linkset_destroy(lset_a);
	ss7_linkset_destroy(lset_b);
}

static void test_as(void)
{
	struct osmo_ss7_as *as;
	struct osmo_ss7_asp *asp;
	struct xua_msg *xua;
	int rc;

	OSMO_ASSERT(osmo_ss7_as_find_by_name(s7i, "as1") == NULL);
	as = osmo_ss7_as_find_or_create(s7i, "as1", OSMO_SS7_ASP_PROT_M3UA);
	OSMO_ASSERT(as);
	OSMO_ASSERT(osmo_ss7_as_find_by_name(s7i, "as1") == as);
	OSMO_ASSERT(osmo_ss7_as_find_by_rctx(s7i, 2342) == NULL);
	as->cfg.routing_key.context = 2342;
	OSMO_ASSERT(osmo_ss7_as_find_by_rctx(s7i, 2342) == as);
	OSMO_ASSERT(osmo_ss7_as_add_asp(as, "asp1") == -ENODEV);

	asp = osmo_ss7_asp_find_or_create2(s7i, "asp1", 0, M3UA_PORT,
					   IPPROTO_SCTP, OSMO_SS7_ASP_PROT_M3UA);
	OSMO_ASSERT(asp);

	OSMO_ASSERT(osmo_ss7_as_has_asp(as, asp) == false);
	OSMO_ASSERT(osmo_ss7_as_add_asp(as, "asp1") == 0);

	osmo_ss7_asp_restart(asp);

	/* ask FSM to send ASP-UP.req */
	rc = osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_M_ASP_UP_REQ, NULL);
	OSMO_ASSERT(rc == 0);

	xua = xua_msg_alloc();
	xua->hdr = XUA_HDR(SUA_MSGC_ASPSM, SUA_ASPSM_UP_ACK);
	rc = osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_ASPSM_ASPUP_ACK, xua);
	xua_msg_free(xua);
	OSMO_ASSERT(rc == 0);

	rc = osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_ASPTM_ASPAC_ACK, NULL);
	OSMO_ASSERT(rc == 0);

	OSMO_ASSERT(osmo_ss7_as_del_asp(as, "asp1") == 0);
	OSMO_ASSERT(osmo_ss7_as_del_asp(as, "asp2") == -ENODEV);
	OSMO_ASSERT(osmo_ss7_as_del_asp(as, "asp1") == -EINVAL);

	osmo_ss7_asp_destroy(asp);
	osmo_ss7_as_destroy(as);
	OSMO_ASSERT(osmo_ss7_as_find_by_name(s7i, "as1") == NULL);
}

/***********************************************************************
 * Initialization
 ***********************************************************************/

static const struct log_info_cat log_info_cat[] = {
};

static const struct log_info log_info = {
	.cat = log_info_cat,
	.num_cat = ARRAY_SIZE(log_info_cat),
};

static void init_logging(void)
{
	const int log_cats[] = { DLSS7, DLSUA, DLM3UA, DLSCCP, DLINP };
	unsigned int i;
	void *tall_ctx = talloc_named_const(NULL, 1, "example");
	msgb_talloc_ctx_init(tall_ctx, 0);
	osmo_init_logging2(tall_ctx, &log_info);

	log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_NONE);

	for (i = 0; i < ARRAY_SIZE(log_cats); i++)
		log_set_category_filter(osmo_stderr_target, log_cats[i], 1, LOGL_DEBUG);
}

int main(int argc, char **argv)
{
	init_logging();
	osmo_fsm_log_addr(false);

	/* init */
	OSMO_ASSERT(osmo_ss7_init() == 0);
	s7i = osmo_ss7_instance_find_or_create(NULL, 0);
	OSMO_ASSERT(osmo_ss7_instance_find(0) == s7i);
	OSMO_ASSERT(osmo_ss7_instance_find(23) == NULL);

	/* test osmo_ss7_pc_is_local() */
	s7i->cfg.primary_pc = 55;
	OSMO_ASSERT(osmo_ss7_pc_is_local(s7i, 55) == true);
	OSMO_ASSERT(osmo_ss7_pc_is_local(s7i, 23) == false);

	/* further tests */
	test_pc_defaults();
	test_pc_parser_itu();
	test_pc_parser_ansi();
	test_user();
	test_route();
	test_linkset();
	test_as();

	/* destroy */
	osmo_ss7_instance_destroy(s7i);
	OSMO_ASSERT(osmo_ss7_instance_find(0) == NULL);

	exit(0);
}
