# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
#   Max Kanat-Alexander <mkanat@bugzilla.org>

# This module represents the tests that get run on a single
# operator/field combination for Bugzilla::Test::Search.
# This is where all the actual testing happens.
package Bugzilla::Test::Search::FieldTest;

use strict;
use warnings;
use Bugzilla::Test::Search::FakeCGI;
use Bugzilla::Search;
use Bugzilla::Test::Search::Constants;

use Data::Dumper;
use Scalar::Util qw(blessed);
use Test::More;
use Test::Exception;

###############
# Constructor #
###############

sub new {
    my ($class, $operator_test, $field, $test) = @_;
    return bless { operator_test => $operator_test,
                   field_object => $field,
                   raw_test     => $test }, $class;
}

#############
# Accessors #
#############

sub num_tests { return TESTS_PER_RUN }

# The Bugzilla::Test::Search::OperatorTest that this is a child of.
sub operator_test { return $_[0]->{operator_test} }
# The Bugzilla::Field being tested.
sub field_object { return $_[0]->{field_object} }
# The name of the field being tested, which we need much more often
# than we need the object.
sub field {
    my ($self) = @_;
    return $self->{field_name} ||= $self->field_object->name;
    return $self->{field_name};
}
# The Bugzilla::Test::Search object that this is a child of.
sub search_test { return $_[0]->operator_test->search_test }
# The operator being tested
sub operator { return $_[0]->operator_test->operator }
# The bugs currently being tested by Bugzilla::Test::Search.
sub bugs { return $_[0]->search_test->bugs }
sub bug {
    my $self = shift;
    return $self->search_test->bug(@_);
}

# The name displayed for this test by Test::More. Used in test descriptions.
sub name {
    my ($self) = @_;
    my $field = $self->field;
    my $operator = $self->operator;
    my $value = $self->main_value;
    
    my $name = "$field-$operator-$value";
    if (my $extra_name = $self->test->{extra_name}) {
        $name .= "-$extra_name";
    }
    return $name;
}

# The appropriate value from the TESTS constant for this test, taking
# into account overrides.
sub test {
    my $self = shift;
    return $self->{test} if $self->{test};
    
    my %test = %{ $self->{raw_test} };
    
    # We have field name overrides...
    my $override = $test{override}->{$self->field};
    # And also field type overrides.
    if (!$override) {
        $override = $test{override}->{$self->field_object->type} || {};
    }
    
    foreach my $key (%$override) {
        $test{$key} = $override->{$key};
    }
    
    $self->{test} = \%test;
    return $self->{test};
}

# All the values for all the bugs for this field.
sub _field_values {
    my ($self) = @_;
    return $self->{field_values} if $self->{field_values};
    
    my %field_values;
    foreach my $number (1..NUM_BUGS) {
        $field_values{$number} = $self->_field_values_for_bug($number);
    }
    $self->{field_values} = \%field_values;
    return $self->{field_values};
}
# The values for this field for the numbered bug.
sub bug_values {
    my ($self, $number) = @_;
    return @{ $self->_field_values->{$number} };
}

# The untranslated, non-overriden value--used in the name of the test
# and other places.
sub main_value { return $_[0]->{raw_test}->{value} }
# The untranslated test value, taking into account overrides.
sub test_value { return $_[0]->test->{value} };
# The value translated appropriately for passing to Bugzilla::Search.
sub translated_value {
    my $self = shift;
    if (!exists $self->{translated_value}) {
        my $value = $self->search_test->value_translation_cache($self);
        if (!defined $value) {
            $value = $self->_translate_value();
            $self->search_test->value_translation_cache($self, $value);
        }
        $self->{translated_value} = $value;
    }
    return $self->{translated_value};
}
# Used in failure diagnostic messages.
sub debug_value {
    my ($self) = @_;
    return "Value: '" . $self->translated_value . "'";
}

# True for a bug if we ran the "transform" function on it and the
# result was equal to its first value.
sub transformed_value_was_equal {
    my ($self, $number, $value) = @_;
    if (defined $value) {
        $self->{transformed_value_was_equal}->{$number} = $value;
    }
    return $self->{transformed_value_was_equal}->{$number};
}

# True if this test is supposed to contain the numbered bug.
sub bug_is_contained {
    my ($self, $number) = @_;
    my $contains = $self->test->{contains};
    if ($self->transformed_value_was_equal($number)) {
        $contains = $self->test->{if_equal}->{contains};
    }
    return grep($_ == $number, @$contains) ? 1 : 0;
}

###################################################
# Accessors: Ways of doing SKIP and TODO on tests #
###################################################

# The tests we know are broken for this operator/field combination.
sub _known_broken {
    my $self = shift;
    my $field = $self->field;
    my $type = $self->field_object->type;
    my $operator = $self->operator;
    my $value = $self->main_value;
    
    my $value_name = "$operator-$value";
    my $value_broken = KNOWN_BROKEN->{$value_name}->{$field};
    $value_broken ||= KNOWN_BROKEN->{$value_name}->{$type};
    return $value_broken if $value_broken;
    my $operator_broken = KNOWN_BROKEN->{$operator}->{$field};
    $operator_broken ||= KNOWN_BROKEN->{$operator}->{$type};
    return $operator_broken if $operator_broken;
    return {};
}

# True if the "contains" search for the numbered bug is broken.
# That is, either the result is supposed to contain it and doesn't,
# or the result is not supposed to contain it and does.
sub contains_known_broken {
    my ($self, $number) = @_;
    my $field = $self->field;
    my $operator = $self->operator;

    my $contains_broken = $self->_known_broken->{contains} || [];
    if (grep($_ == $number, @$contains_broken)) {
        return "$field $operator contains $number is known to be broken";
    }
    return undef;
}

# Returns a string if creating a Bugzilla::Search object throws an error,
# with this field/operator/value combination.
sub search_known_broken {
    my ($self) = @_;
    my $field = $self->field;
    my $operator = $self->operator;
    if ($self->_known_broken->{search}) {
        return "Bugzilla::Search for $field $operator is known to be broken";
    }
    return undef;
}
    
# Returns a string if we haven't yet implemented the tests for this field,
# but we plan to in the future.
sub field_not_yet_implemented {
    my ($self) = @_;
    my $skip_this_field = grep { $_ eq $self->field } SKIP_FIELDS;
    if ($skip_this_field) {
        my $field = $self->field;
        return "$field testing not yet implemented";
    }
    return undef;
}

# Returns a message if this field/operator combination can't ever be run.
# At no time in the future will this field/operator combination ever work.
sub invalid_field_operator_combination {
    my ($self) = @_;
    my $field = $self->field;
    my $operator = $self->operator;
    
    if ($field eq 'content' && $operator !~ /matches/) {
        return "content field does not support $operator";
    }
    elsif ($operator =~ /matches/ && $field ne 'content') {
        return "matches operator does not support fields other than content";
    }
    return undef;
}

# True if this field is broken in an OR combination.
sub join_broken {
    my ($self, $or_broken_map) = @_;
    my $or_broken = $or_broken_map->{$self->field . '-' . $self->operator};
    if (!$or_broken) {
        # See if this is a comment field, and in that case, if there's
        # a generic entry for all comment fields.
        my $is_comment_field = COMMENT_FIELDS->{$self->field};
        if ($is_comment_field) {
            $or_broken = $or_broken_map->{'longdescs.-' . $self->operator};
        }
    }
    return $or_broken;
}

#########################################
# Accessors: Bugzilla::Search Arguments #
#########################################

# The CGI object that will get passed to Bugzilla::Search as its arguments.
sub search_params {
    my $self = shift;
    return $self->{search_params} if $self->{search_params};

    my $field = $self->field;
    my $operator = $self->operator;
    my $value = $self->translated_value;
    
    my $cgi = new Bugzilla::Test::Search::FakeCGI;
    $cgi->param("field0-0-0", $field);
    $cgi->param('type0-0-0', $operator);
    $cgi->param('value0-0-0', $value);
    
    $self->{search_params} = $cgi;
    return $self->{search_params};
}

sub search_columns {
    my ($self) = @_;
    my $field = $self->field;
    my @search_fields = qw(bug_id);
    if ($self->field_object->buglist) {
        my $col_name = COLUMN_TRANSLATION->{$field} || $field;
        push(@search_fields, $col_name);
    }
    return \@search_fields;
}


################
# Field Values #
################

sub _field_values_for_bug {
    my ($self, $number) = @_;
    my $field = $self->field;

    my @values;

    if ($field =~ /^attach.+\.(.+)$/ ) {
        my $attach_field = $1;
        $attach_field = ATTACHMENT_FIELDS->{$attach_field} || $attach_field;
        @values = $self->_values_for($number, 'attachments', $attach_field);
    }
    elsif (my $flag_field = FLAG_FIELDS->{$field}) {
        @values = $self->_values_for($number, 'flags', $flag_field);
    }
    elsif (my $translation = COMMENT_FIELDS->{$field}) {
        @values = $self->_values_for($number, 'comments', $translation);
        # We want the last value to come first, so that single-value
        # searches use the last comment.
        @values = reverse @values;
    }
    elsif ($field eq 'bug_group') {
        @values = $self->_values_for($number, 'groups_in', 'name');
    }
    elsif ($field eq 'keywords') {
        @values = $self->_values_for($number, 'keyword_objects', 'name'); 
    }
    elsif ($field eq 'content') {
        @values = $self->_values_for($number, 'short_desc');
    }
    # Bugzilla::Bug truncates creation_ts, but we need the full value
    # from the database. This has no special value for changedfrom,
    # because it never changes.
    elsif ($field eq 'creation_ts') {
        my $bug = $self->bug($number);
        my $creation_ts = Bugzilla->dbh->selectrow_array(
            'SELECT creation_ts FROM bugs WHERE bug_id = ?',
            undef, $bug->id);
        @values = ($creation_ts);
    }
    else {
        @values = $self->_values_for($number, $field);
    }

    # We convert user objects to their login name, here, all in one
    # block for simplicity.
    if (grep { $_ eq $field } USER_FIELDS) {
        # requestees.login_name is empty for most bugs (but checking
        # blessed(undef) handles that.
        # Values that come from %original_values aren't User objects.
        @values = map { blessed($_) ? $_->login : $_ } @values;
        @values = grep { defined $_ } @values;
    }
    
    return \@values;
}

sub _values_for {
    my ($self, $number, $bug_field, $item_field) = @_;

    my $item;
    if ($self->operator eq 'changedfrom') {
        $item = $self->search_test->bug_create_value($number, $bug_field);
    }
    else {
        my $bug = $self->bug($number);
        $item = $bug->$bug_field;
    }
    
    if ($item_field) {
        if ($bug_field eq 'flags' and $item_field eq 'name') {
            return (map { $_->name . $_->status } @$item);
        }
        return (map { $self->_get_item($_, $item_field) } @$item);
    }

    return @$item if ref($item) eq 'ARRAY';
    return $item if defined $item;
    return ();
}

sub _get_item {
    my ($self, $from, $field) = @_;
    if (blessed($from)) {
        return $from->$field;
    }
    return $from->{$field};
}

#####################
# Value Translation #
#####################

# This function translates the "value" specified in TESTS into an actual
# search value to pass to Search.pm. This means that we get the value
# from the current bug (or, in the case of changedfrom, from %original_values)
# and then we insert it as required into the "value" from TESTS. (For example,
# <1> becomes the value for the field from bug 1.)
sub _translate_value {
    my $self = shift;
    my $value = $self->test_value;
    foreach my $number (1..NUM_BUGS) {
        $value = $self->_translate_value_for_bug($number, $value);
    }
    # Sanity check to make sure that none of the <> stuff was left in.
    if ($value =~ /<\d/) {
        die $self->name . ": value untranslated: $value\n";
    }
    return $value;
}

sub _translate_value_for_bug {
    my ($self, $number, $value) = @_;
    
    my $bug = $self->bug($number);
    
    my $bug_id = $bug->id;
    $value =~ s/<$number-id>/$bug_id/g;
    my $bug_delta = $bug->delta_ts;
    $value =~ s/<$number-delta>/$bug_delta/g;
    my $reporter = $bug->reporter->login;
    $value =~ s/<$number-reporter>/$reporter/g;

    my @bug_values = $self->bug_values($number);    
    return $value if !@bug_values;
    
    if ($self->operator =~ /substr/) {
        @bug_values = map { $self->_substr_value($_) } @bug_values;
    }

    my $string_value = $bug_values[0];
    if ($self->operator =~ /word/) {
        $string_value = join(' ', @bug_values);
    }
    if (my $func = $self->test->{transform}) {
        my $transformed = $func->(@bug_values);
        my $is_equal = $transformed eq $bug_values[0] ? 1 : 0;
        $self->transformed_value_was_equal($number, $is_equal);
        $string_value = $transformed;
    }

    if ($self->test->{escape}) {
        $string_value = quotemeta($string_value);
    }
    $value =~ s/<$number>/$string_value/g;
    
    return $value;
}

sub _substr_value {
    my ($self, $value) = @_;
    my $field = $self->field;
    my $type  = $self->field_object->type;
    my $substr_size = SUBSTR_SIZE;
    if (exists FIELD_SUBSTR_SIZE->{$field}) {
        $substr_size = FIELD_SUBSTR_SIZE->{$field};
    }
    elsif (exists FIELD_SUBSTR_SIZE->{$type}) {
        $substr_size = FIELD_SUBSTR_SIZE->{$type};
    }
    if ($substr_size > 0) {
        # The field name is included in every field value, and if it's
        # long, it might take up the whole substring, and we don't want that.
        if (!grep { $_ eq $field or $_ eq $type } SUBSTR_NO_FIELD_ADD) {
            $substr_size += length($field);
        }
        my $string = substr($value, 0, $substr_size);
        # Make percentage_complete substrings strings match integers uniquely,
        # by searching for the full decimal number.
        if ($field eq 'percentage_complete' and length($string) < $substr_size) {
            $string .= ".000";
        }
        return $string;
    }
    return substr($value, $substr_size);
}

#####################
# Main Test Methods #
#####################

sub run {
    my ($self) = @_;
    
    my $invalid_combination = $self->invalid_field_operator_combination;
    my $field_not_implemented = $self->field_not_yet_implemented;

    SKIP: {    
        skip($invalid_combination, $self->num_tests) if $invalid_combination;
        TODO: {
            todo_skip ($field_not_implemented, $self->num_tests) if $field_not_implemented;
            $self->do_tests();
        }
    }
}

sub do_tests {
    my ($self) = @_;
    my $name = $self->name;

    my $search_broken = $self->search_known_broken;
    
    my $search;
    TODO: {
        local $TODO = $search_broken if $search_broken;
        $search = $self->_test_search_object_creation();
    }
    
    my ($results, $sql);
    SKIP: {
        skip "Can't run SQL without Search object", 2 if !$search;
        lives_ok { $sql = $search->getSQL() } "$name: get SQL";
    
        # This prevents warnings from DBD::mysql if we pass undef $sql,
        # which happens if "new Bugzilla::Search" fails.
        $sql ||= '';
        $results = $self->_test_sql($sql);
    }

    $self->_test_content($results, $sql);
}

sub _test_search_object_creation {
    my ($self) = @_;
    my $name = $self->name;
    my @args = (fields => $self->search_columns, params => $self->search_params);
    my $search;
    lives_ok { $search = new Bugzilla::Search(@args) }
             "$name: create search object";
    return $search;
}

sub _test_sql {
    my ($self, $sql) = @_;
    my $dbh = Bugzilla->dbh;
    my $name = $self->name;
    my $results;
    lives_ok { $results = $dbh->selectall_arrayref($sql) } "$name: Run SQL Query"
        or diag($sql);
    return $results;
}

sub _test_content {
    my ($self, $results, $sql) = @_;

    SKIP: {
        skip "Without results we can't test them", NUM_BUGS if !$results;
        foreach my $number (1..NUM_BUGS) {
            $self->_test_content_for_bug($number, $results, $sql);
        }
    }
}

sub _test_content_for_bug {
    my ($self, $number, $results, $sql) = @_;
    my $name = $self->name;
    
    my $contains_known_broken = $self->contains_known_broken($number);
    
    my %result_ids = map { $_->[0] => 1 } @$results;
    my $bug_id = $self->bug($number)->id;
    
    TODO: {
        local $TODO = $contains_known_broken if $contains_known_broken;
        if ($self->bug_is_contained($number)) {
            ok($result_ids{$bug_id},
               "$name: contains bug $number ($bug_id)")
                or diag Dumper($results) . $self->debug_value . "\n\nSQL: $sql";
        }
        else {
            ok(!$result_ids{$bug_id},
               "$name: does not contain bug $number ($bug_id)")
                or diag Dumper($results) . $self->debug_value . "\n\nSQL: $sql";
        }
    }
}

1;