# Copyright © 2018-2020 Felix Lechner
# Copyright © 2019 Chris Lamb <lamby@debian.org>
#
# 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, you can find it on the World Wide
# Web at http://www.gnu.org/copyleft/gpl.html, or write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA

package Test::Lintian::Prepare;

=head1 NAME

Test::Lintian::Prepare -- routines to prepare the work directories

=head1 SYNOPSIS

  use Test::Lintian::Prepare qw(prepare);

=head1 DESCRIPTION

The routines in this module prepare the work directories in which the
tests are run. To do so, they use the specifications in the test set.

=cut

use v5.20;
use warnings;
use utf8;

use Exporter qw(import);

BEGIN {
    our @EXPORT_OK = qw(
      prepare
      filleval
    );
}

use Carp;
use Const::Fast;
use Cwd qw(getcwd);
use File::Copy;
use File::Find::Rule;
use File::Path qw(make_path remove_tree);
use File::stat;
use List::Util qw(max);
use Path::Tiny;
use Text::Template;
use Try::Tiny;
use Unicode::UTF8 qw(encode_utf8);

use Lintian::Deb822::Section;

use Test::Lintian::ConfigFile qw(read_config write_config);
use Test::Lintian::Helper qw(rfc822date copy_dir_contents);
use Test::Lintian::Templates
  qw(copy_skeleton_template_sets remove_surplus_templates fill_skeleton_templates);

const my $EMPTY => q{};
const my $SPACE => q{ };
const my $SLASH => q{/};
const my $COMMA => q{,};

=head1 FUNCTIONS

=over 4

=item prepare(SPEC_PATH, SOURCE_PATH, TEST_SET, REBUILD)

Populates a work directory SOURCE_PATH with data from the test located
in SPEC_PATH. The optional parameter REBUILD forces a rebuild if true.

=cut

sub prepare {
    my ($specpath, $sourcepath, $testset, $force_rebuild)= @_;

    say encode_utf8('------- Preparation starts here -------');
    say encode_utf8("Work directory is $sourcepath.");

    # for template fill, earliest date without timewarp warning
    my $data_epoch = $ENV{'POLICY_EPOCH'}//time;

    # read defaults
    my $defaultspath = "$testset/defaults";

    # read default file names
    my $defaultfilespath = "$defaultspath/files";
    die encode_utf8("Cannot find $defaultfilespath")
      unless -e $defaultfilespath;

    # read file and adjust data age threshold
    my $files = read_config($defaultfilespath);
    #    $data_epoch= max($data_epoch, stat($defaultfilespath)->mtime);

    # read test data
    my $descpath = $specpath . $SLASH . $files->unfolded_value('Fill-Values');
    my $desc = read_config($descpath);
    #    $data_epoch= max($data_epoch, stat($descpath)->mtime);

    # read test defaults
    my $descdefaultspath
      = $defaultspath . $SLASH . $files->unfolded_value('Fill-Values');
    my $defaults = read_config($descdefaultspath);
    #    $data_epoch= max($data_epoch, stat($descdefaultspath)->mtime);

    # start with a shallow copy of defaults
    my $testcase = Lintian::Deb822::Section->new;
    $testcase->store($_, $defaults->value($_)) for $defaults->names;

    die encode_utf8("Name missing for $specpath")
      unless $desc->declares('Testname');

    die encode_utf8('Outdated test specification (./debian/debian exists).')
      if -e "$specpath/debian/debian";

    if (-d $sourcepath) {

        # check for old build artifacts
        my $buildstamp = "$sourcepath/build-stamp";
        say encode_utf8('Found old build artifact.') if -e $buildstamp;

        # check for old debian/debian directory
        my $olddebiandir = "$sourcepath/debian/debian";
        say encode_utf8('Found old debian/debian directory.')
          if -e $olddebiandir;

        # check for rebuild demand
        say encode_utf8('Forcing rebuild.') if $force_rebuild;

        # delete work directory
        if($force_rebuild || -e $buildstamp || -e $olddebiandir) {
            say encode_utf8("Removing work directory $sourcepath.");
            remove_tree($sourcepath);
        }
    }

    # create work directory
    unless (-d $sourcepath) {
        say encode_utf8("Creating directory $sourcepath.");
        make_path($sourcepath);
    }

    # delete old test scripts
    my @oldrunners = File::Find::Rule->file->name('*.t')->in($sourcepath);
    if (@oldrunners) {
        unlink(@oldrunners)
          or die encode_utf8("Cannot unlink @oldrunners");
    }

    my $skeletonname = $desc->unfolded_value('Skeleton');
    if (length $skeletonname) {

        # load skeleton
        my $skeletonpath = "$testset/skeletons/$skeletonname";
        my $skeleton = read_config($skeletonpath);

        $testcase->store($_, $skeleton->value($_)) for $skeleton->names;
    }

    # populate working directory with specified template sets
    copy_skeleton_template_sets($testcase->value('Template-Sets'),
        $sourcepath, $testset)
      if $testcase->declares('Template-Sets');

    # delete templates for which we have originals
    remove_surplus_templates($specpath, $sourcepath);

    # copy test specification to working directory
    my $offset = path($specpath)->relative($testset)->stringify;
    say encode_utf8(
        "Copy test specification $offset from $testset to $sourcepath.");
    copy_dir_contents($specpath, $sourcepath);

    my $valuefolder = $testcase->unfolded_value('Fill-Values-Folder');
    if (length $valuefolder) {

        # load all the values in the fill values folder
        my $valuepath = "$sourcepath/$valuefolder";
        my @filepaths
          = File::Find::Rule->file->name('*.values')->in($valuepath);

        for my $filepath (sort @filepaths) {
            my $fill_values = read_config($filepath);

            $testcase->store($_, $fill_values->value($_))
              for $fill_values->names;
        }
    }

    # add individual settings after skeleton
    $testcase->store($_, $desc->value($_)) for $desc->names;

    # record path to specification
    $testcase->store('Spec-Path', $specpath);

    # record path to specification
    $testcase->store('Source-Path', $sourcepath);

    # add other helpful info to testcase
    $testcase->store('Source', $testcase->unfolded_value('Testname'))
      unless $testcase->declares('Source');

    # record our effective data age as date, unless given
    $testcase->store('Date', rfc822date($data_epoch))
      unless $testcase->declares('Date');

    warn encode_utf8('Cannot override Architecture: in test '
          . $testcase->unfolded_value('Testname'))
      if $testcase->declares('Architecture');

    die encode_utf8('DEB_HOST_ARCH is not set.')
      unless defined $ENV{'DEB_HOST_ARCH'};
    $testcase->store('Host-Architecture', $ENV{'DEB_HOST_ARCH'});

    die encode_utf8('Could not get POLICY_VERSION.')
      unless defined $ENV{'POLICY_VERSION'};
    $testcase->store('Standards-Version', $ENV{'POLICY_VERSION'})
      unless $testcase->declares('Standards-Version');

    die encode_utf8('Could not get DEFAULT_DEBHELPER_COMPAT.')
      unless defined $ENV{'DEFAULT_DEBHELPER_COMPAT'};
    $testcase->store('Dh-Compat-Level', $ENV{'DEFAULT_DEBHELPER_COMPAT'})
      unless $testcase->declares('Dh-Compat-Level');

    # add additional version components
    if ($testcase->declares('Version')) {

        # add upstream version
        my $upstream_version = $testcase->unfolded_value('Version');
        $upstream_version =~ s/-[^-]+$//;
        $upstream_version =~ s/(-|^)(\d+):/$1/;
        $testcase->store('Upstream-Version', $upstream_version);

        # version without epoch
        my $no_epoch = $testcase->unfolded_value('Version');
        $no_epoch =~ s/^\d+://;
        $testcase->store('No-Epoch', $no_epoch);

        unless ($testcase->declares('Prev-Version')) {
            my $prev_version = '0.0.1';
            $prev_version .= '-1'
              unless $testcase->unfolded_value('Type') eq 'native';

            $testcase->store('Prev-Version', $prev_version);
        }
    }

    # calculate build dependencies
    warn encode_utf8('Cannot override Build-Depends:')
      if $testcase->declares('Build-Depends');
    combine_fields($testcase, 'Build-Depends', $COMMA . $SPACE,
        'Default-Build-Depends', 'Extra-Build-Depends');

    # calculate build conflicts
    warn encode_utf8('Cannot override Build-Conflicts:')
      if $testcase->declares('Build-Conflicts');
    combine_fields($testcase, 'Build-Conflicts', $COMMA . $SPACE,
        'Default-Build-Conflicts', 'Extra-Build-Conflicts');

    # fill testcase with itself; do it twice to make sure all is done
    my $hashref = deb822_section_to_hash($testcase);
    $hashref = fill_hash_from_hash($hashref);
    $hashref = fill_hash_from_hash($hashref);
    write_hash_to_deb822_section($hashref, $testcase);

    say encode_utf8($EMPTY);

    # fill remaining templates
    fill_skeleton_templates($testcase->value('Fill-Targets'),
        $hashref, $data_epoch, $sourcepath, $testset)
      if $testcase->declares('Fill-Targets');

    # write the dynamic file names
    my $runfiles = path($sourcepath)->child('files');
    write_config($files, $runfiles->stringify);

    # set mtime for dynamic file names
    $runfiles->touch($data_epoch);

    # write the dynamic test case file
    my $rundesc
      = path($sourcepath)->child($files->unfolded_value('Fill-Values'));
    write_config($testcase, $rundesc->stringify);

    # set mtime for dynamic test data
    $rundesc->touch($data_epoch);

    say encode_utf8($EMPTY);

    # announce data age
    say encode_utf8('Data epoch is : '. rfc822date($data_epoch));

    return;
}

=item filleval(SPEC_PATH, EVAL_PATH, TEST_SET, REBUILD)

Populates a evaluation directory EVAL_PATH with data from the test located
in SPEC_PATH. The optional parameter REBUILD forces a rebuild if true.

=cut

sub filleval {
    my ($specpath, $evalpath, $testset, $force_rebuild)= @_;

    say encode_utf8('------- Filling evaluation starts here -------');
    say encode_utf8("Evaluation directory is $evalpath.");

    # read defaults
    my $defaultspath = "$testset/defaults";

    # read default file names
    my $defaultfilespath = "$defaultspath/files";
    die encode_utf8("Cannot find $defaultfilespath")
      unless -e $defaultfilespath;

    # read file with default file names
    my $files = read_config($defaultfilespath);

    # read test data
    my $descpath
      = $specpath . $SLASH . $files->unfolded_value('Test-Specification');
    my $desc = read_config($descpath);

    # read test defaults
    my $descdefaultspath
      = $defaultspath . $SLASH . $files->unfolded_value('Test-Specification');
    my $defaults = read_config($descdefaultspath);

    # start with a shallow copy of defaults
    my $testcase = Lintian::Deb822::Section->new;
    $testcase->store($_, $defaults->value($_)) for $defaults->names;

    die encode_utf8("Name missing for $specpath")
      unless $desc->declares('Testname');

    # delete old test scripts
    my @oldrunners = File::Find::Rule->file->name('*.t')->in($evalpath);
    if (@oldrunners) {
        unlink(@oldrunners)
          or die encode_utf8("Cannot unlink @oldrunners");
    }

    $testcase->store('Skeleton', $desc->value('Skeleton'))
      unless $testcase->declares('Skeleton');

    my $skeletonname = $testcase->unfolded_value('Skeleton');
    if (length $skeletonname) {

        # load skeleton
        my $skeletonpath = "$testset/skeletons/$skeletonname";
        my $skeleton = read_config($skeletonpath);

        $testcase->store($_, $skeleton->value($_)) for $skeleton->names;
    }

    # add individual settings after skeleton
    $testcase->store($_, $desc->value($_)) for $desc->names;

    # populate working directory with specified template sets
    copy_skeleton_template_sets($testcase->value('Template-Sets'),
        $evalpath, $testset)
      if $testcase->declares('Template-Sets');

    # delete templates for which we have originals
    remove_surplus_templates($specpath, $evalpath);

    # copy test specification to working directory
    my $offset = path($specpath)->relative($testset)->stringify;
    say encode_utf8(
        "Copy test specification $offset from $testset to $evalpath.");
    copy_dir_contents($specpath, $evalpath);

    my $valuefolder = $testcase->unfolded_value('Fill-Values-Folder');
    if (length $valuefolder) {

        # load all the values in the fill values folder
        my $valuepath = "$evalpath/$valuefolder";
        my @filepaths
          = File::Find::Rule->file->name('*.values')->in($valuepath);

        for my $filepath (sort @filepaths) {
            my $fill_values = read_config($filepath);

            $testcase->store($_, $fill_values->value($_))
              for $fill_values->names;
        }
    }

    # add individual settings after skeleton
    $testcase->store($_, $desc->value($_)) for $desc->names;

    # fill testcase with itself; do it twice to make sure all is done
    my $hashref = deb822_section_to_hash($testcase);
    $hashref = fill_hash_from_hash($hashref);
    $hashref = fill_hash_from_hash($hashref);
    write_hash_to_deb822_section($hashref, $testcase);

    say encode_utf8($EMPTY);

    # fill remaining templates
    fill_skeleton_templates($testcase->value('Fill-Targets'),
        $hashref, time, $evalpath, $testset)
      if $testcase->declares('Fill-Targets');

    # write the dynamic file names
    my $runfiles = path($evalpath)->child('files');
    write_config($files, $runfiles->stringify);

    # write the dynamic test case file
    my $rundesc
      = path($evalpath)->child($files->unfolded_value('Test-Specification'));
    write_config($testcase, $rundesc->stringify);

    say encode_utf8($EMPTY);

    return;
}

=item combine_fields

=cut

sub combine_fields {
    my ($testcase, $destination, $delimiter, @sources) = @_;

    return
      unless length $destination;

    # we are combining these contents
    my @contents;
    for my $source (@sources) {
        push(@contents, $testcase->value($source))
          if length $source;
        $testcase->drop($source);
    }

    # combine
    for my $content (@contents) {
        $testcase->store(
            $destination,
            join($delimiter,
                grep { length }($testcase->value($destination),$content)));
    }

    # delete the combined entry if it is empty
    $testcase->drop($destination)
      unless length $testcase->value($destination);

    return;
}

=item deb822_section_to_hash

=cut

sub deb822_section_to_hash {
    my ($section) = @_;

    my %hash;
    for my $name ($section->names) {

        my $transformed = lc $name;
        $transformed =~ s/-/_/g;

        $hash{$transformed} = $section->value($name);
    }

    return \%hash;
}

=item write_hash_to_deb822_section

=cut

sub write_hash_to_deb822_section {
    my ($hashref, $section) = @_;

    for my $name ($section->names) {

        my $transformed = lc $name;
        $transformed =~ s/-/_/g;

        $section->store($name, $hashref->{$transformed});
    }

    return;
}

=item fill_hash_from_hash

=cut

sub fill_hash_from_hash {
    my ($hashref, $delimiters) = @_;

    $delimiters //= ['[%', '%]'];

    my %origin = %{$hashref};
    my %destination;

    # fill hash with itself
    for my $key (keys %origin) {

        my $template = $origin{$key} // $EMPTY;
        my $filler= Text::Template->new(TYPE => 'STRING', SOURCE => $template);
        croak encode_utf8(
            "Cannot read template $template: $Text::Template::ERROR")
          unless $filler;

        my $generated
          = $filler->fill_in(HASH => \%origin, DELIMITERS => $delimiters);
        croak encode_utf8("Could not create string from template $template")
          unless defined $generated;
        $destination{$key} = $generated;
    }

    return \%destination;
}

=back

=head1 AUTHOR

Originally written by Felix Lechner <felix.lechner@lease-up.com> for Lintian.

=head1 SEE ALSO

lintian(1)

=cut

1;

# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 sts=4 sr et
