package Leek::IntegrationTest;

use strict;
use warnings;

use Carp qw(croak);
use Exporter qw(import);
use File::Temp qw();
use IO::Socket::INET;
use Linux::Prctl qw(set_pdeathsig);
use POSIX qw(SIGTERM);

=head1 NAME

Leek::IntegrationTest - Leek integration test utilities

=head1 SYNOPSYS

    use Leek::IntegrationTest;

    with_server {
        my $response = send_command 'VERSION';
        assert_response_size $response, 2;
        assert_response_part $response, 0, 0, '0.0.0';
        assert_response_part $response, 1, 2, '';
    };

=head1 DESCRIPTION

All functions are exported by default.

=cut

our @EXPORT = qw(
    with_server
    send_command
    assert_response_size
    assert_response_part
);

our $socket;

=head2 with_server { ... }

Execute the block with leak-server running in the background, and a socket open
to it. leek-server will be terminated when the test exits for any reason (even
SIGKILL).

=cut

sub with_server (&) {
    defined $socket and croak 'with_server called inside of with_server';

    my $block = shift;

    my $data_directory = File::Temp->newdir();
    my $server_host = '127.0.0.1';
    my $server_port = '7916';

    system 'cargo', 'run', '--bin', 'leek-initialize', $data_directory
        and die $!;

    my $pid = fork // die $!;
    if ($pid == 0) {
        set_pdeathsig SIGTERM;
        if (getppid() == 1) {
            exit;
        }
        exec 'cargo', 'run', '--bin', 'leek-server',
            $data_directory, "$server_host:$server_port"
            or die $!;
    }
    sleep 1;

    local $socket = IO::Socket::INET->new(PeerHost => $server_host,
                                          PeerPort => $server_port)
        or die $!;

    $block->();
}

=head2 $response = send_command($command)

Send the C<$command> to the running leek-server instance that was started using
C<with_server>. The return value C<$response> is a scalar that can be passed to
any of the assertion functions below.

=cut

sub send_command {
    $socket // croak 'send_command called outside of with_server';

    my $command = shift;

    my $length = pack('V', length($command));
    $socket->print("$length$command") or die $!;

    my @response;
    for (;;) {
        my $response_part = receive_response_part();
        push @response, $response_part;
        if ($response_part->{status} == 2) {
            last;
        }
    }
    [@response];
}

sub receive_response_part {
    $socket // croak 'receive_response_part called outside of with_server';

    my ($status, $length, $entry);

    $socket->read($status, 1) // die $!;
    $socket->read($length, 4) // die $!;
    $socket->read($entry, unpack('V', $length)) // die $!;

    { status => ord($status), entry => $entry };
}

=head2 assert_response_size($response, $size)

Assert that C<$response> returned by C<send_command> has C<$size> number of
response parts.

=cut

sub assert_response_size {
    my ($response, $size) = @_;
    my $actual_size = @$response;
    if ($actual_size != $size) {
        croak "Incorrect response size: expected $size but got $actual_size";
    }
}

=head2 assert_response_part($response, $part_index, $status, $entry)

Assert that C<$response> returned by C<send_command> has at index C<$part_index>
a response part with C<$status> and C<$entry>.

=cut

sub assert_response_part {
    my ($response, $index, $status, $entry) = @_;
    my $response_part = $response->[$index];
    my $actual_status = $response_part->{status};
    my $actual_entry = $response_part->{entry};
    if ($actual_status != $status) {
        croak "Incorrect response part status: expected $status but got $actual_status";
    }
    if ($actual_entry ne $entry) {
        croak "Incorrect response part entry: expected '$entry' but got '$actual_entry'";
    }
}

1;
