# SPDX-License-Identifier: GPL-2.0-or-later

package Amavis::IO::RWTest;

use strict;
use warnings;
use Test::Most;
use base 'Test::Class';

sub class { 'Amavis::IO::RW' }

sub startup : Tests(startup => 1) {
  my $test = shift;
  use_ok $test->class;
}

sub line_boundary_chunking : Tests(15) {
    my $class = shift->class;

    Amavis::IO::RWTest::Sink->run_with(sub {
        my $client = $class->new($_[0], Eol => "\015\012", Timeout => 10);
        isa_ok $client, $class;
        ok $client->print("data\r"), '1st chunk written';
        ok length($client->{out}), '1st chunk still buffered';
        ok !$client->at_line_boundary, '1st chunk does not end at Eol';
        ok $client->flush, '1st chunk flushed';
        ok !length($client->{out}), '1st chunk transmitted';
        ok !$client->at_line_boundary, 'client state is still not at Eol';
        ok $client->print("\n"), '2nd chunk written';
        ok $client->at_line_boundary, '2nd chunk ends at Eol';
        ok $client->flush, '2nd chunk flushed';
        ok !length($client->{out}), '2nd chunk transmitted';
        ok $client->at_line_boundary, 'client state is still at Eol';
        $client->close;
    })
}

# This test cases tests scenarios at the start of the stream with a
# eol string that might overlap with itself. The start of a stream is
# the start of the first line and thus a line boundary. One could
# imagine that this means that there is an imaginary eol before the
# start of the stream. If we have an eol string of 'aba' that can
# overlap with itself, this could mean that writing 'ba' to such a
# stream would end up at a line boundary, because the imaginary
# pre-stream eol would together be 'ababa' which ends with a eol. We
# do not want this and this test ensures that no such overlap happens.
sub line_boundary_at_start : Tests(10) {
    my $class = shift->class;

    Amavis::IO::RWTest::Sink->run_with(sub {
        my $client = $class->new($_[0], Eol => "aba", Timeout => 10);
        isa_ok $client, $class;
        ok $client->at_line_boundary, 'at line boundary without data written';
        ok $client->print('ba'), 'end of line overlapping with imagined initial eol written';
        ok !$client->at_line_boundary, 'not at line end after overlap with imagined initial eol';
        ok $client->print('ba'), 'end of real eol written';
        ok $client->at_line_boundary, 'at eol after writing eol overlapping with last write';
        $client->close;
    })
}

package Amavis::IO::RWTest::Sink;
use Net::Server;
use base 'Net::Server';

use Test::Most;

sub new {
    my $class = shift;
    bless { @_ }, $class;
}

sub pre_loop_hook {
    my $self = shift;
    print { $self->{source} } 'sink initialized';
    close $self->{source};
}

sub process_request {
  my $self = shift;
  my $sink;
  1 while sysread STDIN, $sink, 1024;
  $self->done(1);
}

sub run_with {
    my ($cls, $code) = @_;
    my $port = 12345;

    my $pid = open(my $from_child, '-|');
    if ($pid) {
        ok($pid, 'test forked');
    }
    return unless defined($pid);
    if ($pid) {
        is(<$from_child>, 'sink initialized', 'sink initialized');
        $code->("[127.0.0.1]:$port");
        kill 'TERM', $pid;
        ok(waitpid($pid, 0), 'sink terminated');
    } else {
        my $sink = Amavis::IO::RWTest::Sink->new(source => \*STDOUT);
        $sink->run(port => $port);
    }
}

1;
