#!/usr/bin/perl

# Copyright 2019 Simon McVittie
# SPDX-License-Identifier: GPL-2.0-or-later
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

use strict;
use warnings;

use Cwd qw(getcwd);
use Data::Dumper;
use Dpkg::IPC;
use File::Temp qw(tempdir);
use Test::More;

use Dpkg::Control;

my $srcdir = getcwd;
my $top_srcdir = getcwd . '/..';
my $mergechanges = "$top_srcdir/scripts/mergechanges.sh";

if (defined $ARGV[0] && $ARGV[0] eq '--installed') {
    $mergechanges = 'mergechanges';
}

my $tmp = tempdir(CLEANUP => 1);
my $stdout;
my $stderr;
my $fh;
my $merged;
my $all = 'xdg-desktop-portal_1.2.0-1_all.changes';
my $amd64 = 'xdg-desktop-portal_1.2.0-1_amd64.changes';
my $source = 'xdg-desktop-portal_1.2.0-1_source.changes';
my %controls;
my @words;
my @lines;
my $orig;

sub run {
    my ($argv, %opts) = @_;
    diag("Running: @{$argv}") if $opts{verbose};
    delete $opts{verbose};
    spawn(
        %opts,
        exec       => $argv,
        wait_child => 1,
        no_check   => 1,
        # XXX: Backwards compatibility, remove after dpkg 1.24.0.
        nocheck    => 1,
    );
    return $? == 0;
}

sub verbose_run {
    my ($argv, %opts) = @_;
    return run($argv, verbose => 1, %opts);
}

sub capture {
    my $output;
    my $argv = shift;
    ok(verbose_run($argv, to_string => \$output), "@{$argv}");
    chomp $output;
    return $output;
}

sub uniq {
    my %seen;
    my @ret;
    foreach my $member (@_) {
        push @ret, $member unless defined $seen{$member};
        $seen{$member} = 1;
    }
    return @ret;
}

sub verbose_is_deeply {
    diag Dumper($_[0], $_[1]);
    is_deeply(@_);
}

foreach my $name ($all, $amd64, $source) {
    $controls{$name} = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
    $controls{$name}->load("mergechanges/$name");
}

diag('Help');
$stdout = capture([
    $mergechanges,
    '--help',
]);
like($stdout, qr{Usage:});

diag('Version');
$stdout = capture([
    $mergechanges,
    '--version',
]);
like($stdout, qr{devscripts package});

diag('Simple merge');
$stdout = capture([
    $mergechanges,
    "mergechanges/$all",
    "mergechanges/$amd64",
    "mergechanges/$source",
]);
#diag $stdout;
unlike($stdout, qr/BEGIN PGP/);
unlike($stdout, qr/END PGP/);
$merged = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
open($fh, '<', \$stdout);
$merged->parse($fh, 'stdout of mergechanges');
close($fh);
is($merged->{Format}, $controls{$source}->{Format});
is($merged->{Date}, $controls{$source}->{Date});
is($merged->{Source}, $controls{$source}->{Source});
@words = sort split / /, $merged->{Binary};
is_deeply(\@words, [sort qw(
        xdg-desktop-portal
        xdg-desktop-portal-dbgsym
        xdg-desktop-portal-dev
        xdg-desktop-portal-tests
        xdg-desktop-portal-tests-dbgsym
)]);
@words = sort split / /, $merged->{Architecture};
is_deeply(\@words, [sort qw(amd64 all source)]);
is($merged->{Version}, $controls{$source}->{Version});
is($merged->{Distribution}, $controls{$source}->{Distribution});
is($merged->{Urgency}, $controls{$source}->{Urgency});
is($merged->{Maintainer}, $controls{$source}->{Maintainer});
is($merged->{'Changed-By'}, $controls{$source}->{'Changed-By'});
isnt($merged->{Description}, undef);
@lines = sort split /\n/, $merged->{Description};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Description}),
                (split /\n/, $controls{$amd64}->{Description}),
))]);
is($merged->{Changes}, $controls{$source}->{Changes});
@lines = sort split /\n/, $merged->{Files};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Files}),
                (split /\n/, $controls{$amd64}->{Files}),
                (split /\n/, $controls{$source}->{Files}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha1'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{'Checksums-Sha1'}),
                (split /\n/, $controls{$amd64}->{'Checksums-Sha1'}),
                (split /\n/, $controls{$source}->{'Checksums-Sha1'}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha256'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{'Checksums-Sha256'}),
                (split /\n/, $controls{$amd64}->{'Checksums-Sha256'}),
                (split /\n/, $controls{$source}->{'Checksums-Sha256'}),
))]);

diag('Source only');
$stdout = capture([
    $mergechanges,
    '-S',
    "mergechanges/$all",
    "mergechanges/$amd64",
    "mergechanges/$source",
]);
#diag $stdout;
unlike($stdout, qr/BEGIN PGP/);
unlike($stdout, qr/END PGP/);
$merged = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
open($fh, '<', \$stdout);
$merged->parse($fh, 'stdout of mergechanges');
close($fh);
is($merged->{Format}, $controls{$source}->{Format});
is($merged->{Date}, $controls{$source}->{Date});
is($merged->{Source}, $controls{$source}->{Source});
is($merged->{Binary}, undef);
@words = sort split / /, $merged->{Architecture};
is_deeply(\@words, [sort qw(source)]);
is($merged->{Version}, $controls{$source}->{Version});
is($merged->{Distribution}, $controls{$source}->{Distribution});
is($merged->{Urgency}, $controls{$source}->{Urgency});
is($merged->{Maintainer}, $controls{$source}->{Maintainer});
is($merged->{'Changed-By'}, $controls{$source}->{'Changed-By'});
is($merged->{Description}, undef);
is($merged->{Changes}, $controls{$source}->{Changes});
@lines = sort split /\n/, $merged->{Files};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{Files}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha1'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{'Checksums-Sha1'}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha256'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{'Checksums-Sha256'}),
))]);

diag('Indep only');
$stdout = capture([
    $mergechanges,
    '-i',
    "mergechanges/$all",
    "mergechanges/$amd64",
    "mergechanges/$source",
]);
#diag $stdout;
unlike($stdout, qr/BEGIN PGP/);
unlike($stdout, qr/END PGP/);
$merged = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
open($fh, '<', \$stdout);
$merged->parse($fh, 'stdout of mergechanges');
close($fh);
is($merged->{Format}, $controls{$source}->{Format});
is($merged->{Date}, $controls{$source}->{Date});
is($merged->{Source}, $controls{$source}->{Source});
is($merged->{Binary}, 'xdg-desktop-portal-dev');
@words = sort split / /, $merged->{Architecture};
is_deeply(\@words, [sort qw(all source)]);
is($merged->{Version}, $controls{$source}->{Version});
is($merged->{Distribution}, $controls{$source}->{Distribution});
is($merged->{Urgency}, $controls{$source}->{Urgency});
is($merged->{Maintainer}, $controls{$source}->{Maintainer});
is($merged->{'Changed-By'}, $controls{$source}->{'Changed-By'});
isnt($merged->{Description}, undef);
@lines = sort split /\n/, $merged->{Description};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Description}),
))]);
is($merged->{Changes}, $controls{$source}->{Changes});
@lines = sort split /\n/, $merged->{Files};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{Files}),
                (split /\n/, $controls{$all}->{Files}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha1'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{'Checksums-Sha1'}),
                (split /\n/, $controls{$all}->{'Checksums-Sha1'}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha256'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{'Checksums-Sha256'}),
                (split /\n/, $controls{$all}->{'Checksums-Sha256'}),
))]);

diag('To file');
ok(run(['cp', "mergechanges/$source", "$tmp/source.changes"]));
$stdout = capture([
    $mergechanges,
    '-f',
    "$tmp/source.changes",
    "mergechanges/$all",
]);
ok(-e "$tmp/source.changes");
is($stdout, '');
#system("cat", "$tmp/xdg-desktop-portal_1.2.0-1_multi.changes");
$merged = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
$merged->load("$tmp/xdg-desktop-portal_1.2.0-1_multi.changes");
is($merged->{Format}, $controls{$source}->{Format});
is($merged->{Date}, $controls{$source}->{Date});
is($merged->{Source}, $controls{$source}->{Source});
is($merged->{Binary}, 'xdg-desktop-portal-dev');
@words = sort split / /, $merged->{Architecture};
is_deeply(\@words, [sort qw(all source)]);
is($merged->{Version}, $controls{$source}->{Version});
is($merged->{Distribution}, $controls{$source}->{Distribution});
is($merged->{Urgency}, $controls{$source}->{Urgency});
is($merged->{Maintainer}, $controls{$source}->{Maintainer});
is($merged->{'Changed-By'}, $controls{$source}->{'Changed-By'});
isnt($merged->{Description}, undef);
@lines = sort split /\n/, $merged->{Description};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Description}),
))]);
is($merged->{Changes}, $controls{$source}->{Changes});
@lines = sort split /\n/, $merged->{Files};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{Files}),
                (split /\n/, $controls{$all}->{Files}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha1'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{'Checksums-Sha1'}),
                (split /\n/, $controls{$all}->{'Checksums-Sha1'}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha256'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{'Checksums-Sha256'}),
                (split /\n/, $controls{$all}->{'Checksums-Sha256'}),
))]);

diag('Deleting');
ok(run(['cp', "mergechanges/$source", "$tmp/source.changes"]));
ok(run(['cp', "mergechanges/$all", "$tmp/all.changes"]));
$stdout = capture([
    $mergechanges,
    '-d',
    '-f',
    "$tmp/source.changes",
    "$tmp/all.changes",
]);
ok(! -e "$tmp/source.changes");
ok(! -e "$tmp/all.changes");
is($stdout, '');
#system("cat", "$tmp/xdg-desktop-portal_1.2.0-1_multi.changes");
$merged = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
$merged->load("$tmp/xdg-desktop-portal_1.2.0-1_multi.changes");
is($merged->{Format}, $controls{$source}->{Format});
is($merged->{Date}, $controls{$source}->{Date});
is($merged->{Source}, $controls{$source}->{Source});
is($merged->{Binary}, 'xdg-desktop-portal-dev');
@words = sort split / /, $merged->{Architecture};
is_deeply(\@words, [sort qw(all source)]);
is($merged->{Version}, $controls{$source}->{Version});
is($merged->{Distribution}, $controls{$source}->{Distribution});
is($merged->{Urgency}, $controls{$source}->{Urgency});
is($merged->{Maintainer}, $controls{$source}->{Maintainer});
is($merged->{'Changed-By'}, $controls{$source}->{'Changed-By'});
isnt($merged->{Description}, undef);
@lines = sort split /\n/, $merged->{Description};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Description}),
))]);
is($merged->{Changes}, $controls{$source}->{Changes});
@lines = sort split /\n/, $merged->{Files};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{Files}),
                (split /\n/, $controls{$all}->{Files}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha1'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{'Checksums-Sha1'}),
                (split /\n/, $controls{$all}->{'Checksums-Sha1'}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha256'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$source}->{'Checksums-Sha256'}),
                (split /\n/, $controls{$all}->{'Checksums-Sha256'}),
))]);

diag('Merge with itself');
$stdout = capture([
    $mergechanges,
    '--indep',
    "mergechanges/$all",
    "mergechanges/$all",
]);
#diag $stdout;
unlike($stdout, qr/BEGIN PGP/);
unlike($stdout, qr/END PGP/);
$merged = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
open($fh, '<', \$stdout);
$merged->parse($fh, 'stdout of mergechanges');
close($fh);
is($merged->{Format}, $controls{$source}->{Format});
is($merged->{Date}, $controls{$source}->{Date});
is($merged->{Source}, $controls{$source}->{Source});
is($merged->{Binary}, 'xdg-desktop-portal-dev');
@words = sort split / /, $merged->{Architecture};
is_deeply(\@words, [sort qw(all)]);
is($merged->{Version}, $controls{$source}->{Version});
is($merged->{Distribution}, $controls{$source}->{Distribution});
is($merged->{Urgency}, $controls{$source}->{Urgency});
is($merged->{Maintainer}, $controls{$source}->{Maintainer});
is($merged->{'Changed-By'}, $controls{$source}->{'Changed-By'});
isnt($merged->{Description}, undef);
@lines = sort split /\n/, $merged->{Description};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Description}),
))]);
is($merged->{Changes}, $controls{$source}->{Changes});
@lines = sort split /\n/, $merged->{Files};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Files}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha1'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{'Checksums-Sha1'}),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha256'};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{'Checksums-Sha256'}),
))]);

diag('Format 1.7 and 1.8 are compatible');
$stdout = capture([
    $mergechanges,
    '--indep',
    "mergechanges/$all",
    "mergechanges/format-1.7.changes",
]);
diag $stdout;
unlike($stdout, qr/BEGIN PGP/);
unlike($stdout, qr/END PGP/);
$merged = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
open($fh, '<', \$stdout);
$merged->parse($fh, 'stdout of mergechanges');
close($fh);
# Formats 1.8 and 1.7 merge to 1.7
is($merged->{Format}, '1.7');
is($merged->{Date}, $controls{$source}->{Date});
is($merged->{Source}, $controls{$source}->{Source});
is($merged->{Binary}, 'xdg-desktop-portal-dev');
@words = sort split / /, $merged->{Architecture};
is_deeply(\@words, [sort qw(all)]);
is($merged->{Version}, $controls{$source}->{Version});
is($merged->{Distribution}, $controls{$source}->{Distribution});
is($merged->{Urgency}, $controls{$source}->{Urgency});
is($merged->{Maintainer}, $controls{$source}->{Maintainer});
is($merged->{'Changed-By'}, $controls{$source}->{'Changed-By'});
isnt($merged->{Description}, undef);
@lines = sort split /\n/, $merged->{Description};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Description}),
))]);
is($merged->{Changes}, $controls{$source}->{Changes});
@lines = sort split /\n/, $merged->{Files};
is_deeply(\@lines, [sort(uniq(
                (split /\n/, $controls{$all}->{Files}),
))]);
# Format 1.7 didn't have Checksums-*
is($merged->{'Checksums-Sha1'}, undef);
is($merged->{'Checksums-Sha256'}, undef);

diag('Only one');
ok(! verbose_run([
    $mergechanges,
    "mergechanges/$source",
], to_string => \$stdout, error_to_string => \$stderr));
is($stdout, '');
like($stderr, qr{Not enough parameters});

diag('ENOENT');
ok(! verbose_run([
    $mergechanges,
    "mergechanges/$source",
    "mergechanges/does-not-exist.changes",
], to_string => \$stdout, error_to_string => \$stderr));
is($stdout, '');
like($stderr, qr{ERROR: Cannot read mergechanges/does-not-exist\.changes});

diag('Different description');
ok(! verbose_run([
    $mergechanges,
    "mergechanges/$all",
    "mergechanges/different-description.changes",
], to_string => \$stdout, error_to_string => \$stderr));
is($stdout, '');
like($stderr, qr{Error: Descriptions do not match});

diag('Different format');
ok(! verbose_run([
    $mergechanges,
    "mergechanges/$all",
    "mergechanges/unsupported-format.changes",
], to_string => \$stdout, error_to_string => \$stderr));
is($stdout, '');
like($stderr, qr{Error: Changes files have different Format fields});

diag('Different source package');
ok(! verbose_run([
    $mergechanges,
    "mergechanges/$all",
    "mergechanges/different-source.changes",
], to_string => \$stdout, error_to_string => \$stderr));
is($stdout, '');
like($stderr, qr{Error: Source packages do not match});

diag('Different version');
ok(! verbose_run([
    $mergechanges,
    "mergechanges/$all",
    "mergechanges/different-version.changes",
], to_string => \$stdout, error_to_string => \$stderr));
is($stdout, '');
like($stderr, qr{ERROR: Version numbers do not match});

diag('Unsupported checksums');
ok(! verbose_run([
    $mergechanges,
    "mergechanges/$all",
    "mergechanges/unsupported-checksum.changes",
], to_string => \$stdout, error_to_string => \$stderr));
is($stdout, '');
like($stderr, qr{Error: Unsupported checksum fields});

diag('Unsupported format');
ok(! verbose_run([
    $mergechanges,
    "mergechanges/unsupported-format.changes",
    "mergechanges/unsupported-format.changes",
], to_string => \$stdout, error_to_string => \$stderr));
is($stdout, '');
like($stderr, qr{Error: Changes files use unknown Format});

diag('Multi-line Binary');
$stdout = capture([
    $mergechanges,
    '--indep',
    'mergechanges/linux_4.9.161-1_amd64.changes',
    'mergechanges/linux_4.9.161-1_amd64.changes',
]);
unlike($stdout, qr/BEGIN PGP/);
unlike($stdout, qr/END PGP/);
$merged = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
open($fh, '<', \$stdout);
$merged->parse($fh, 'stdout of mergechanges');
close($fh);
$orig = Dpkg::Control->new(type => CTRL_FILE_CHANGES);
$orig->load('mergechanges/linux_4.9.161-1_amd64.changes');
is($merged->{Format}, $orig->{Format});
is($merged->{Date}, $orig->{Date});
is($merged->{Source}, $orig->{Source});
@words = sort split / /, $merged->{Binary};
is_deeply(\@words, [sort qw(
        linux-doc-4.9
        linux-headers-4.9.0-9-common
        linux-headers-4.9.0-9-common-rt
        linux-manual-4.9
        linux-source-4.9
        linux-support-4.9.0-9
)]);
@words = sort split / /, $merged->{Architecture};
is_deeply(\@words, [sort qw(all source)]);
is($merged->{Version}, $orig->{Version});
is($merged->{Distribution}, $orig->{Distribution});
is($merged->{Urgency}, $orig->{Urgency});
is($merged->{Maintainer}, $orig->{Maintainer});
is($merged->{'Changed-By'}, $orig->{'Changed-By'});
isnt($merged->{Description}, undef);
@lines = sort split /\n/, $merged->{Description};
is_deeply(\@lines, [sort(uniq(
            grep({m/^$/ || m/^(linux-doc-4.9|linux-headers-4.9.0-9-common|linux-headers-4.9.0-9-common-rt|linux-manual-4.9|linux-source-4.9|linux-support-4.9.0-9) - /} (split /\n/, $orig->{Description})),
))]);
is($merged->{Changes}, $orig->{Changes});
@lines = sort split /\n/, $merged->{Files};
is_deeply(\@lines, [sort(uniq(
            grep({! /_amd64\./} (split /\n/, $orig->{Files})),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha1'};
is_deeply(\@lines, [sort(uniq(
            grep({! /_amd64\./} (split /\n/, $orig->{'Checksums-Sha1'})),
))]);
@lines = sort split /\n/, $merged->{'Checksums-Sha256'};
is_deeply(\@lines, [sort(uniq(
            grep({! /_amd64\./} (split /\n/, $orig->{'Checksums-Sha256'})),
))]);

done_testing;

# vim:set sts=4 sw=4 et:
