The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use Test::More tests => 9;

use utf8;
use strict;
use warnings;

use Encode;
use_ok('Net::AS2');
use_ok('Net::AS2::MDN');
use_ok('Net::AS2::Message');


my $original_message_id = 'some-random@example.com';
my $async_url = 'https://example.com:8081/hey';
my $mic = 'TESTBASE64++=';
my $data = "测试\x01\x02\r\nGood";

my $msg_success = Net::AS2::Message->new($original_message_id, $async_url, 1, $mic, $data);
my $msg_error = Net::AS2::Message->create_error_message($original_message_id, $async_url, 0, 'error-status', 'error-plain');
my $msg_failure = Net::AS2::Message->create_failure_message($original_message_id, undef, 1, 'failure-status', 'failure-plain');

sub check_pair {
    my ($msg, $mdn) = @_;
    is(defined $mdn->async_url ? 1 : 0, $msg->is_mdn_async ? 1 : 0);
    is($mdn->async_url, $msg->async_url);
    is($mdn->should_sign, $msg->should_mdn_sign);
    is($mdn->original_message_id, $msg->message_id);
    is($mdn->{mic_hash}, $msg->mic)
        if defined $msg->mic;
    if (defined $msg->error_status_text) {
        is($mdn->{status_text}, $msg->error_status_text);
        is($mdn->{plain_text}, $msg->error_plain_text);
    }
    mdn_self_check($mdn);
    message_self_check($msg);
}

sub mdn_self_check {
    my ($mdn) = @_;

    $mdn->recipient('TEST ""');
    my $new_mdn = Net::AS2::MDN->parse_mdn($mdn->as_mime->stringify);

    foreach (qw(
        success warning error failure unparsable
        recipient
        status_text plain_text original_message_id mic_hash mic_alg
    ))
    {
        is($new_mdn->{$_}, $mdn->{$_}, "Self duplicated MDN: $_")
            unless 
                # The text is hardcoded
                $mdn->is_success && $_ eq 'status_text';
    }
}

sub message_self_check
{
    my ($msg) = @_;
    my $new_msg =
        Net::AS2::Message->create_from_serialized_state(
            $msg->serialized_state()
        );

    foreach (qw(success error failure message_id mic status_text should_mdn_sign plain_text async_url))
    {
        is($new_msg->{$_} // '', $msg->{$_} // '', "Self duplicated message: $_")
    }
}

subtest 'MDN Success' => sub {
    my $mdn = Net::AS2::MDN->create_success($msg_success, 'success');
    ok($mdn->is_success); ok(!$mdn->with_warning);
    ok(!$mdn->is_failure); ok(!$mdn->is_error); ok(!$mdn->is_unparsable);
    check_pair($msg_success, $mdn);
};

subtest 'MDN Error From Message' => sub {
    my $mdn = Net::AS2::MDN->create_from_unsuccessful_message($msg_error);
    ok(!$mdn->is_success); ok(!$mdn->with_warning);
    ok(!$mdn->is_failure); ok($mdn->is_error); ok(!$mdn->is_unparsable);
    check_pair($msg_error, $mdn);
};

subtest 'MDN Failure From Message' => sub {
    my $mdn = Net::AS2::MDN->create_from_unsuccessful_message($msg_failure);
    ok(!$mdn->is_success); ok(!$mdn->with_warning);
    ok($mdn->is_failure); ok(!$mdn->is_error); ok(!$mdn->is_unparsable);
    check_pair($msg_failure, $mdn);
};

subtest 'MDN Warning' => sub {
    my $mdn = Net::AS2::MDN->create_warning($msg_success, 'warning');
    ok($mdn->is_success); ok($mdn->with_warning);
    ok(!$mdn->is_failure); ok(!$mdn->is_error); ok(!$mdn->is_unparsable);
    mdn_self_check($mdn);
};

subtest 'MDN Error' => sub {
    my $mdn = Net::AS2::MDN->create_error($msg_success, 'error human');
    ok(!$mdn->is_success); ok(!$mdn->with_warning);
    ok(!$mdn->is_failure); ok($mdn->is_error); ok(!$mdn->is_unparsable);
    mdn_self_check($mdn);
};

subtest 'MDN Failure' => sub {
    my $mdn = Net::AS2::MDN->create_failure($msg_success, 'failure robot', 'failure human');
    ok(!$mdn->is_success); ok(!$mdn->with_warning);
    ok($mdn->is_failure); ok(!$mdn->is_error); ok(!$mdn->is_unparsable);
    mdn_self_check($mdn);
};

__END__

my %config_1 = (
    MyId => 'Mr 1', MyKey => key(1), MyCertificate => cert(1), 
    PartnerId => 'Mr 2', PartnerCertificate => cert(2),
    PartnerUrl => 'http://example.com/dummy/a_2/msg');

my %config_2 = (
    MyId => 'Mr 2', MyKey => key(2), MyCertificate => cert(2), 
    PartnerId => 'Mr 1', PartnerCertificate => cert(1),
    PartnerUrl => 'http://example.com/dummy/a_1/msg');

my $test_async = sub {
    my ($mod) = @_;
    my $a1 = Mock::Net::AS2->new(%config_1,
        Mdn => 'async',
        MdnAsyncUrl => 'http://example.com/dummy/a_1/mdn',
        %{$mod}
    );

    my $a2 = Mock::Net::AS2->new(%config_2, %{$mod});

    my $data = "测试\nThis is a test\r\n\x01\x02\x00";
    my $message_id = rand . '@' . 'localhost';

    my ($mdn_temp, $mic1) = $a1->send($data, 'Type' => 'text/plain', 'MessageId' => $message_id);
    ok($mdn_temp->is_unparsable, 'ASYNC data unparsable');
    my $req = $Mock::LWP::UserAgent::last_request;

    my $msg = $a2->decode_message(extract_headers($req), $req->content);

    ok($msg->is_success, 'Message received sucessfully');
    ok($msg->is_mdn_async, 'MDN is async');
    is($msg->async_url, 'http://example.com/dummy/a_1/mdn');
    is(decode('utf8', $msg->content), $data, 'Content matches');
    is($mic1, $msg->mic, 'MIC matches');

    $a2->send_async_mdn(Net::AS2::MDN->create_success($msg));

    my $mdn_req = $Mock::LWP::UserAgent::last_request;
    my $mdn = $a1->decode_mdn(extract_headers($mdn_req), $mdn_req->content);
    ok($mdn->is_success, 'MDN is success');
    ok($mdn->match_mic($mic1, 'sha1'), 'MDN MIC matches');
    is($mdn->original_message_id, $message_id, 'MDN message id matches');
};

subtest 'Send and Async - Signature + Encryption' => sub { $test_async->({}); };
subtest 'Send and Async - Signature Only ' => sub { $test_async->({ Encryption => 0 }); };
subtest 'Send and Async - Encryption Only' => sub { $test_async->({ Signature => 0 }); };
subtest 'Send and Async - Plain' => sub { $test_async->({ Encryption => 0, Signature => 0 }); };

my $test_sync = sub {
    my ($mod) = @_;
    my $a1 = Mock::Net::AS2->new(%config_1,
        Mdn => 'sync',
        %{$mod}
    );

    my $a2 = Mock::Net::AS2->new(%config_2, %{$mod});

    my $data = "测试\nThis is a test\r\n\x01\x02\x00";
    my $message_id = rand . '@' . 'localhost';

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $msg = $a2->decode_message(extract_headers($req), $req->content);
        ok($msg->is_success, 'Message received sucessfully');
        ok(!$msg->is_mdn_async, 'MDN is sync');
        is(decode('utf8', $msg->content), $data, 'Content matches');

        my ($h, $c) = $a2->prepare_sync_mdn(Net::AS2::MDN->create_success($msg));
        my $r = HTTP::Response->new(200, 'OK', $h, $c);
        return $r;
    };

    my ($mdn, $mic1) = $a1->send($data, 'Type' => 'text/plain', 'MessageId' => $message_id);

    use Data::Dumper;

    ok($mdn->is_success, 'MDN is success');
    ok($mdn->match_mic($mic1, 'sha1'), 'MDN MIC matches');
    is($mdn->original_message_id, $message_id, 'MDN message id matches');
};

subtest 'Send and Sync - Signature + Encryption' => sub { $test_sync->({}); };
subtest 'Send and Sync - Signature Only ' => sub { $test_sync->({ Encryption => 0 }); };
subtest 'Send and Sync - Encryption Only' => sub { $test_sync->({ Signature => 0 }); };
subtest 'Send and Sync - Plain' => sub { $test_sync->({ Encryption => 0, Signature => 0 }); };

subtest 'Encryption required check' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1, Mdn => 'sync', Encryption => 0);
    my $a2 = Mock::Net::AS2->new(%config_2);

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $msg = $a2->decode_message(extract_headers($req), $req->content);
        ok($msg->is_error, 'Message received with error');
        is($msg->error_status_text, 'insufficient-message-security');
        ok($msg->error_plain_text =~ /encryption/i);

        my $r = HTTP::Response->new(200, 'OK', [], '');
        return $r;
    };
    $a1->send("Test", 'Type' => 'text/plain');
};

subtest 'Encryption optional pass' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1, Mdn => 'sync');
    my $a2 = Mock::Net::AS2->new(%config_2, Encryption => 0);

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $msg = $a2->decode_message(extract_headers($req), $req->content);
        ok($msg->is_success, 'Message received sucessfully');

        my $r = HTTP::Response->new(200, 'OK', [], '');
        return $r;
    };
    $a1->send("Test", 'Type' => 'text/plain');
};

subtest 'Encryption failed' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1);
    my $a2 = Mock::Net::AS2->new(%config_1, 
        MyId => $config_2{MyId}, PartnerId => $config_2{PartnerId},
        Signature => 0
        );

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $msg = $a2->decode_message(extract_headers($req), $req->content);
        ok($msg->is_error, 'Message received with error');
        is($msg->error_status_text, 'decryption-failed');
        ok($msg->error_plain_text =~ /decrypt/i);

        my $r = HTTP::Response->new(200, 'OK', [], '');
        return $r;
    };
    $a1->send("Test", 'Type' => 'text/plain');
};

subtest 'Signature required check' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1, Mdn => 'sync', Signature => 0);
    my $a2 = Mock::Net::AS2->new(%config_2);

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $msg = $a2->decode_message(extract_headers($req), $req->content);
        ok($msg->is_error, 'Message received with error');
        is($msg->error_status_text, 'insufficient-message-security');
        ok($msg->error_plain_text =~ /signature/i);

        my $r = HTTP::Response->new(200, 'OK', [], '');
        return $r;
    };
    $a1->send("Test", 'Type' => 'text/plain');
};

subtest 'Signature optional pass' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1, Mdn => 'sync');
    my $a2 = Mock::Net::AS2->new(%config_2, Signature => 0);

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $msg = $a2->decode_message(extract_headers($req), $req->content);
        ok($msg->is_success, 'Message received sucessfully');

        my $r = HTTP::Response->new(200, 'OK', [], '');
        return $r;
    };
    $a1->send("Test", 'Type' => 'text/plain');
};

subtest 'Signature failed' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1, Encryption => 0);
    my $a2 = Mock::Net::AS2->new(%config_1, 
        MyId => $config_2{MyId}, PartnerId => $config_2{PartnerId},
        Encryption => 0);

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $msg = $a2->decode_message(extract_headers($req), $req->content);
        ok($msg->is_error, 'Message received with error');
        is($msg->error_status_text, 'insufficient-message-security');
        ok($msg->error_plain_text =~ /unable to verify/i);

        my $r = HTTP::Response->new(200, 'OK', [], '');
        return $r;
    };
    $a1->send("Test", 'Type' => 'text/plain');
};

subtest 'Missing headers' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1);

    my $msg = $a1->decode_message({}, '');
    ok($msg->is_error, 'Message received with error');
    is($msg->error_status_text, 'unexpected-processing-error');
    ok($msg->error_plain_text =~ /headers/i);
};

subtest 'Mismatch AS2 Id' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1,);
    my $a2 = Mock::Net::AS2->new(%config_2, MyId => '_x', PartnerId => '_y');

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $msg = $a2->decode_message(extract_headers($req), $req->content);
        ok($msg->is_error, 'Message received with error');
        is($msg->error_status_text, 'authentication-failed');
        ok($msg->error_plain_text =~ /AS2-/i);

        my $r = HTTP::Response->new(200, 'OK', [], '');
        return $r;
    };
    $a1->send("Test", 'Type' => 'text/plain');
};

subtest 'Async MDN' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1);
    my $a2 = Mock::Net::AS2->new(%config_2);

    my $msg = Net::AS2::Message->new("orig-id", "http://example.com/async_url", 1, "mic", "data");

    local $Mock::LWP::UserAgent::response_handler = sub {
        my $req = shift;
        my $mdn = $a1->decode_mdn(extract_headers($req), $req->content);
        ok($mdn->match_mic('mic', 'sha1'));
        ok($mdn->is_success, 'Message received with error');
        is($mdn->original_message_id, 'orig-id');

        my $r = HTTP::Response->new(200, 'OK', [], '');
        return $r;
    };
    $a2->send_async_mdn(Net::AS2::MDN->create_success($msg), "MDN ID");
};

subtest 'Async MDN Unparsable' => sub {
    my $a1 = Mock::Net::AS2->new(%config_1);

    my $mdn = $a1->decode_mdn({}, '');
    ok($mdn->is_unparsable, 'Message received with error');
};


sub key {
    my $i = shift;

    local $/;
    open my $fh, '<', "t/test.$i.key";
    return <$fh>;
}

sub cert {
    my $i = shift;

    local $/;
    open my $fh, '<', "t/test.$i.cert";
    return <$fh>;
}

sub extract_headers
{
    my $req = shift;
    return
    { 
        map { 
            my $key = uc($_);
            $key =~ s/-/_/g;
            $key = 'HTTP_' . $key
                unless $key ~~ [qw(CONTENT_TYPE)];

            ( $key => $req->header($_) )
        } ($req->header_field_names) 
    };
}

package Mock::Net::AS2;
use base 'Net::AS2';

sub create_useragent
{
    return new Mock::LWP::UserAgent;
}

package Mock::LWP::UserAgent;
use base 'LWP::UserAgent';

our $response_handler;
our $last_request;

sub request
{
    my $class = shift;
    $last_request = shift;
    return $response_handler->($last_request) 
        if $response_handler;
    return HTTP::Response->new(200, 'OK', ['Context-Text' => 'text/html'], '');
}

1;