/**
 * @file test/peerconn.c  Tests for peer connection
 *
 * Copyright (C) 2025 Alfred E. Heggestad
 */
#include <re.h>
#include <baresip.h>
#include "test.h"


struct fixture {
	struct agent *a;
	struct agent *b;
	struct mqueue *mq;
	bool terminated;
};


struct agent {
	struct fixture *fix;         /* pointer */
	struct media_track *media;   /* pointer */
	struct peer_connection *pc;
	const char *name;
	uint32_t magic;
	bool use_audio;
	bool use_video;
	bool got_sdp;
	bool got_estab_audio;
	bool got_estab_video;
	bool got_audio;
	bool got_video;
	int err;
};


static const uint32_t MAGIC_AGENT = 0x0ee0c001;


static struct agent *agent_peer(const struct agent *ag)
{
	const struct fixture *fix = ag->fix;

	if (!fix)
		return NULL;

	if (ag == fix->a)
		return fix->b;
	else if (ag == fix->b)
		return fix->a;
	else
		return NULL;
}


static void agent_close(struct agent *ag, int err)
{
	ag->media = NULL;
	ag->err = err;

	if (ag->fix)
		ag->fix->terminated = true;

	re_cancel();
}


static bool agents_are_complete(const struct agent *ag)
{
	const struct agent *peer = agent_peer(ag);

	if (ag->use_audio) {

		if (!ag->got_estab_audio)
			return false;

		bool got_audio = (ag->got_audio || peer->got_audio);
		if (!got_audio)
			return false;
	}

	if (ag->use_video) {

		if (!ag->got_estab_video)
			return false;

		bool got_video = (ag->got_video || peer->got_video);
		if (!got_video)
			return false;
	}

	return true;
}


static int agent_handle_sdp(struct agent *ag, enum sdp_type type,
			    struct mbuf *sdp)
{
	struct session_description sd = {
		.type = type,
		.sdp = sdp
	};

	int err = peerconnection_set_remote_descr(ag->pc, &sd);
	TEST_ERR(err);

	ag->got_sdp = true;

	struct agent *peer = agent_peer(ag);

	if (peer && peer->got_sdp) {

		err = peerconnection_start_ice(ag->pc);
		TEST_ERR(err);

		err = peerconnection_start_ice(peer->pc);
		TEST_ERR(err);
	}

 out:
	return err;
}


static void peerconnection_gather_handler(void *arg)
{
	struct agent *ag = arg;
	struct mbuf *mb = NULL;
	enum sdp_type type = SDP_NONE;
	int err;

	ASSERT_EQ(MAGIC_AGENT, ag->magic);

	if (ag->err)
		return;

	switch (peerconnection_signaling(ag->pc)) {

	case SS_STABLE:
		type = SDP_OFFER;
		break;

	case SS_HAVE_LOCAL_OFFER:
		warning("gather: illegal state HAVE_LOCAL_OFFER\n");
		type = SDP_OFFER;
		break;

	case SS_HAVE_REMOTE_OFFER:
		type = SDP_ANSWER;
		break;
	}

	if (type == SDP_OFFER) {
		err = peerconnection_create_offer(ag->pc, &mb);
		TEST_ERR(err);
	}
	else {
		err = peerconnection_create_answer(ag->pc, &mb);
		TEST_ERR(err);
	}

	err = agent_handle_sdp(agent_peer(ag), type, mb);
	TEST_ERR(err);

 out:
	mem_deref(mb);

	if (err) {
		agent_close(ag, err);
	}
}


static void peerconnection_estab_handler(struct media_track *media, void *arg)
{
	struct agent *ag = arg;
	struct audio *au;
	int err = 0;

	ASSERT_EQ(MAGIC_AGENT, ag->magic);

	switch (mediatrack_kind(media)) {

	case MEDIA_KIND_AUDIO:
		ag->got_estab_audio = true;
		ag->media = media;

		au = media_get_audio(media);

		err = audio_set_devicename(au, "440", "default");
		TEST_ERR(err);

		err = audio_set_source(au, "ausine", "440");
		TEST_ERR(err);

		err = audio_set_player(au, "mock-auplay", "default");
		TEST_ERR(err);

		err = audio_set_bitrate(au, 64000);
		TEST_ERR(err);

		err = mediatrack_start_audio(media, baresip_ausrcl(),
					     baresip_aufiltl());
		TEST_ERR(err);
		break;

	case MEDIA_KIND_VIDEO:
		ag->got_estab_video = true;

		err = mediatrack_start_video(media);
		TEST_ERR(err);
		break;

	default:
		break;
	}

 out:
	if (err || agents_are_complete(ag)) {
		agent_close(ag, err);
	}
}


static void peerconnection_close_handler(int err, void *arg)
{
	struct agent *ag = arg;

	ASSERT_EQ(MAGIC_AGENT, ag->magic);

	if (err) {
		warning("[ %s ] peer connection closed (%m)\n",
			ag->name, err);
	}
	else {
		info("[ %s ] peer connection closed\n", ag->name);
	}

 out:
	agent_close(ag, err);
}


/* called in the context of the main thread */
static void mqueue_handler(int id, void *data, void *arg)
{
	struct fixture *fix = arg;

	(void)id;
	(void)data;
	(void)fix;

	re_cancel();
}


static void destructor(void *arg)
{
	struct agent *ag = arg;

	mem_deref(ag->pc);
}


static int agent_alloc(struct agent **agp, struct fixture *fix,
		       const char *name,
		       const struct mnat *mnat, const struct menc *menc,
		       bool use_audio, bool use_video, bool offerer)
{
	struct rtc_configuration config = {
		.offerer = offerer
	};

	struct agent *ag = mem_zalloc(sizeof(*ag), destructor);
	if (!ag)
		return ENOMEM;

	ag->magic = MAGIC_AGENT;
	ag->fix = fix;
	ag->name = name;
	ag->use_audio = use_audio;
	ag->use_video = use_video;

	int err = peerconnection_new(&ag->pc, &config, mnat, menc,
				     peerconnection_gather_handler,
				     peerconnection_estab_handler,
				     peerconnection_close_handler, ag);
	TEST_ERR(err);

	if (use_audio) {
		err = peerconnection_add_audio_track(ag->pc, conf_config(),
						     baresip_aucodecl(),
						     SDP_SENDRECV);
		TEST_ERR(err);
	}

	if (use_video) {
		err = peerconnection_add_video_track(ag->pc, conf_config(),
						     baresip_vidcodecl(),
						     SDP_SENDRECV);
		TEST_ERR(err);
	}

 out:
	if (err)
		mem_deref(ag);
	else
		*agp = ag;

	return err;
}


/* NOTE: called from main-thread or worker-threads */
static void auframe_handler(struct auframe *af, const char *dev, void *arg)
{
	struct fixture *fix = arg;
	(void)af;
	(void)dev;

	if (fix->terminated)
		return;

	struct agent *ag = fix->b;

	if (!ag)
		return;

	struct audio *au = media_get_audio(ag->media);

	/* Does auframe come from the decoder ? */
	if (!audio_rxaubuf_started(au)) {
		debug("test: [ %s ] no audio received from decoder yet\n",
		      ag->name);
		return;
	}

	ag->got_audio = true;

	if (agents_are_complete(ag)) {
		mqueue_push(fix->mq, 0, NULL);
	}
}


static void mock_vidisp_handler(const struct vidframe *frame,
				uint64_t timestamp, const char *title,
				void *arg)
{
	struct fixture *fix = arg;
	struct agent *ag = fix->b;
	(void)frame;
	(void)timestamp;
	(void)title;

	ag->got_video = true;

	if (agents_are_complete(ag)) {
		mqueue_push(fix->mq, 0, NULL);
	}
}


static int test_peerconn_param(bool use_audio, bool use_aufilt, bool use_video)
{
	const char *aufilters[] = {
		"auconv",
		"auresamp",
		"mixausrc",
	};
	struct fixture fix = {0};
	struct auplay *auplay = NULL;
	struct vidisp *vidisp = NULL;
	int err = 0;

	if (use_audio) {
		err = mock_auplay_register(&auplay, baresip_auplayl(),
					   auframe_handler, &fix);
		TEST_ERR(err);

		err = module_load(".", "g711");
		TEST_ERR(err);
		err = module_load(".", "ausine");
		TEST_ERR(err);

		if (use_aufilt) {
			for (size_t i=0; i<RE_ARRAY_SIZE(aufilters); i++) {
				const char *name = aufilters[i];

				err = module_load(".", name);
				TEST_ERR(err);

				aufilt_enable(baresip_aufiltl(), name, true);
			}
		}
	}

	if (use_video) {
		/* NOTE: must be loaded before 'fakevideo' */
		err = mock_vidisp_register(&vidisp, mock_vidisp_handler, &fix);
		TEST_ERR(err);

		mock_vidcodec_register();

		err = module_load(".", "fakevideo");
		TEST_ERR(err);
	}

	const struct mnat *mnat = mnat_find(baresip_mnatl(), "ice");
	ASSERT_TRUE(mnat != NULL);

	const struct menc *menc = menc_find(baresip_mencl(), "dtls_srtp");
	ASSERT_TRUE(menc != NULL);

	err = mqueue_alloc(&fix.mq, mqueue_handler, &fix);
	TEST_ERR(err);

	err = agent_alloc(&fix.a, &fix, "A", mnat, menc,
			  use_audio, use_video, true);
	TEST_ERR(err);

	err = agent_alloc(&fix.b, &fix, "B", mnat, menc,
			  use_audio, use_video, false);
	TEST_ERR(err);

	err = re_main_timeout(10000);
	TEST_ERR(err);

	fix.terminated = true;

	err = fix.a->err;
	TEST_ERR(err);
	err = fix.b->err;
	TEST_ERR(err);

	ASSERT_TRUE(fix.a->got_sdp);
	ASSERT_TRUE(fix.b->got_sdp);

	if (use_audio) {
		ASSERT_TRUE(fix.a->got_estab_audio);
		ASSERT_TRUE(fix.b->got_estab_audio);
		ASSERT_TRUE(fix.a->got_audio || fix.b->got_audio);
	}
	if (use_video) {
		ASSERT_TRUE(fix.a->got_estab_video);
		ASSERT_TRUE(fix.b->got_estab_video);
		ASSERT_TRUE(fix.a->got_video || fix.b->got_video);
	}

 out:
	fix.b = mem_deref(fix.b);
	fix.a = mem_deref(fix.a);
	mem_deref(fix.mq);

	if (use_audio) {
		if (use_aufilt) {
			for (size_t i=0; i<RE_ARRAY_SIZE(aufilters); i++) {
				const char *name = aufilters[i];

				module_unload(name);
			}
		}

		module_unload("ausine");
		module_unload("g711");
		mem_deref(auplay);
	}
	if (use_video) {
		module_unload("fakevideo");
		mock_vidcodec_unregister();
		mem_deref(vidisp);
	}

	return err;
}


static bool if_handler(const char *ifname, const struct sa *sa, void *arg)
{
	bool *have_real_ip = arg;
	(void)ifname;

	if (sa_is_loopback(sa) || sa_is_linklocal(sa))
		return false;

	*have_real_ip = true;

	return true;
}


int test_peerconn(void)
{
	bool have_real_ip = false;
	int err;

	/* skip the test if no other interface than loopback is available */
	net_laddr_apply(baresip_network(), if_handler, &have_real_ip);
	if (!have_real_ip)
		return 0;

	if (conf_config()->avt.rxmode == RECEIVE_MODE_THREAD)
		return 0;

	err = module_load(".", "dtls_srtp");
	TEST_ERR(err);
	err = module_load(".", "ice");
	TEST_ERR(err);

	err = test_peerconn_param(1, 0, 0);
	TEST_ERR(err);
	err = test_peerconn_param(0, 0, 1);
	TEST_ERR(err);
	err = test_peerconn_param(1, 0, 1);
	TEST_ERR(err);

	err = test_peerconn_param(1, 1, 0);
	TEST_ERR(err);

 out:
	module_unload("ice");
	module_unload("dtls_srtp");

	return err;
}
