#!perl -w
# Copyright (C) all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
use v5.12;
use PublicInbox::TestCommon;
use autodie;
use Socket qw(AF_UNIX SOCK_STREAM SOCK_SEQPACKET);
pipe(my $r, my $w);
my ($send, $recv);
require_ok 'PublicInbox::Spawn';
require POSIX;

my $do_test = sub { SKIP: {
	my ($type, $flag, $desc) = @_;
	my ($s1, $s2);
	my $src = 'some payload' x 40;
	socketpair($s1, $s2, AF_UNIX, $type, 0);
	my $sfds = [ fileno($r), fileno($w), fileno($s1) ];
	$send->($s1, $sfds, $src, $flag);
	my (@fds) = $recv->($s2, my $buf, length($src) + 1);
	is($buf, $src, 'got buffer payload '.$desc);
	my ($r1, $w1, $s1a);
	my $opens = sub {
		ok(open($r1, '<&=', $fds[0]), 'opened received $r');
		ok(open($w1, '>&=', $fds[1]), 'opened received $w');
		ok(open($s1a, '+>&=', $fds[2]), 'opened received $s1');
	};
	$opens->();
	my @exp = stat $r;
	my @cur = stat $r1;
	is("$exp[0]\0$exp[1]", "$cur[0]\0$cur[1]", '$r dev/ino matches');
	@exp = stat $w;
	@cur = stat $w1;
	is("$exp[0]\0$exp[1]", "$cur[0]\0$cur[1]", '$w dev/ino matches');
	@exp = stat $s1;
	@cur = stat $s1a;
	is("$exp[0]\0$exp[1]", "$cur[0]\0$cur[1]", '$s1 dev/ino matches');
	if ($type == SOCK_SEQPACKET) {
		$r1 = $w1 = $s1a = undef;
		$src = (',' x 1023) . '-' .('.' x 1024);
		$send->($s1, $sfds, $src, $flag);
		(@fds) = $recv->($s2, $buf, 1024);
		is($buf, (',' x 1023) . '-', 'silently truncated buf');
		$opens->();
		$r1 = $w1 = $s1a = undef;

		$s2->blocking(0);
		@fds = $recv->($s2, $buf, length($src) + 1);
		ok($!{EAGAIN}, "EAGAIN set by ($desc)");
		is($buf, '', "recv buffer emptied on EAGAIN ($desc)");
		is_deeply(\@fds, [ undef ], "EAGAIN $desc");
		$s2->blocking(1);

		if ('test ALRM') {
			my $alrm = 0;
			local $SIG{ALRM} = sub { $alrm++ };
			my $tgt = $$;
			my $pid = fork;
			if ($pid == 0) {
				# need to loop since Perl signals are racy
				# (the interpreter doesn't self-pipe)
				my $n = 3;
				while (tick(0.01 * $n) && --$n) {
					kill('ALRM', $tgt)
				}
				close $s1;
				POSIX::_exit(1);
			}
			close $s1;
			@fds = $recv->($s2, $buf, length($src) + 1);
			waitpid($pid, 0);
			is_deeply(\@fds, [], "EINTR->EOF $desc");
			ok($alrm, 'SIGALRM hit');
		}

		@fds = $recv->($s2, $buf, length($src) + 1);
		is_deeply(\@fds, [], "no FDs on EOF $desc");
		is($buf, '', "buffer cleared on EOF ($desc)");

		socketpair($s1, $s2, AF_UNIX, $type, 0);
		$s1->blocking(0);
		my $nsent = 0;
		my $srclen = length($src);
		while (defined(my $n = $send->($s1, $sfds, $src, $flag))) {
			$nsent += $n;
			fail "sent $n bytes of $srclen" if $srclen != $n;
		}
		ok($!{EAGAIN} || $!{ETOOMANYREFS} || $!{EMSGSIZE},
			"hit EAGAIN || ETOOMANYREFS || EMSGSIZE on send $desc")
			or diag "send failed with: $! (nsent=$nsent)";
		ok($nsent > 0, 'sent some bytes');

		socketpair($s1, $s2, AF_UNIX, $type, 0);
		is($send->($s1, [], $src, $flag), length($src), 'sent w/o FDs');
		$buf = 'nope';
		@fds = $recv->($s2, $buf, length($src));
		is(scalar(@fds), 0, 'no FDs received');
		is($buf, $src, 'recv w/o FDs');

		my $nr = 2 * 1024 * 1024;
		while (1) {
			vec(my $vec = '', $nr - 1, 8) = 1;
			my $n = $send->($s1, [], $vec, $flag);
			if (defined($n)) {
				$n == length($vec) or
					fail "short send: $n != ".length($vec);
				diag "sent $nr, retrying with more";
				$nr += 2 * 1024 * 1024;
			} else {
				ok($!{EMSGSIZE} || $!{ENOBUFS},
					'got EMSGSIZE or ENOBUFS') or
					diag "$nr bytes fails with: $!";
				last;
			}
		}
	}
} };

my $send_ic = PublicInbox::Spawn->can('send_cmd4');
my $recv_ic = PublicInbox::Spawn->can('recv_cmd4');
SKIP: {
	($send_ic && $recv_ic) or skip 'Inline::C not installed/enabled', 12;
	$send = $send_ic;
	$recv = $recv_ic;
	$do_test->(SOCK_STREAM, 0, 'Inline::C stream');
	$do_test->(SOCK_SEQPACKET, 0, 'Inline::C seqpacket');
}

SKIP: {
	require_mods('Socket::MsgHdr', 13);
	require_ok 'PublicInbox::CmdIPC4';
	$send = PublicInbox::CmdIPC4->can('send_cmd4');
	$recv = PublicInbox::CmdIPC4->can('recv_cmd4');
	$do_test->(SOCK_STREAM, 0, 'MsgHdr stream');
	$do_test->(SOCK_SEQPACKET, 0, 'MsgHdr seqpacket');
	SKIP: {
		($send_ic && $recv_ic) or
			skip 'Inline::C not installed/enabled', 12;
		$recv = $recv_ic;
		$do_test->(SOCK_STREAM, 0, 'Inline::C -> MsgHdr stream');
		$do_test->(SOCK_SEQPACKET, 0, 'Inline::C -> MsgHdr seqpacket');
	}
}

SKIP: {
	require_ok 'PublicInbox::Syscall';
	$send = PublicInbox::Syscall->can('send_cmd4') or
		skip "send_cmd4 not defined for $^O arch", 1;
	$recv = PublicInbox::Syscall->can('recv_cmd4') or
		skip "recv_cmd4 not defined for $^O arch", 1;
	$do_test->(SOCK_STREAM, 0, 'pure Perl stream');
	$do_test->(SOCK_SEQPACKET, 0, 'pure Perl seqpacket');
}

done_testing;
