# -*- 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 Testopia System.
#
# The Initial Developer of the Original Code is Greg Hendricks.
# Portions created by Greg Hendricks are Copyright (C) 2006
# Novell. All Rights Reserved.
#
# Contributor(s): Greg Hendricks <ghendricks@novell.com>

package Testopia::Build;

use strict;

use Bugzilla::Util;
use Bugzilla::Error;
use Testopia::Product;
use JSON;

use base qw(Exporter Bugzilla::Object);
@Testopia::Build::EXPORT = qw(check_build);

###############################
####    Initialization     ####
###############################
use constant DB_TABLE   => "test_builds";
use constant NAME_FIELD => "name";
use constant ID_FIELD   => "build_id";
use constant DB_COLUMNS => qw(
    build_id
    product_id
    name
    description
    milestone
    isactive
);

use constant REQUIRED_CREATE_FIELDS => qw(product_id name milestone isactive);
use constant UPDATE_COLUMNS         => qw(name description milestone isactive);

use constant VALIDATORS => {
    product_id  => \&_check_product,
    isactive    => \&_check_isactive,
};

###############################
####       Validators      ####
###############################
sub _check_product {
    my ($invocant, $product_id) = @_;
    $product_id = trim($product_id);
    
    ThrowUserError("testopia-create-denied", {'object' => 'build'}) unless Bugzilla->user->in_group('Testers');
    
    my $product;
    if (trim($product_id) !~ /^\d+$/ ){
        $product = Bugzilla::Product::check_product($product_id);
    }
    else {
        $product = Testopia::Product->new($product_id);
    }
    
    if (ref $invocant){
        $invocant->{'product'} = $product; 
        return $product->id;
    } 
    return $product;
}

sub _check_name {
    my ($invocant, $name, $product_id) = @_;
    $name = clean_text($name) if $name;

    if (!defined $name || $name eq '') {
        ThrowUserError('testopia-missing-required-field', {'field' => 'name'});
    }
    
    trick_taint($name);
    
    # Check that we don't already have a build with that name in this product.    
    my $orig_id = check_build($name, $product_id);
    my $notunique;

    if (ref $invocant){
        # If updating, we have matched ourself at least
        $notunique = 1 if (($orig_id && $orig_id != $invocant->id))
    }
    else {
        # In new build any match is one too many
        $notunique = 1 if $orig_id;
    }

    ThrowUserError('testopia-name-not-unique', 
                  {'object' => 'Build', 
                   'name' => $name}) if $notunique;
               
    return $name;
}

sub _check_milestone {
    my ($invocant, $milestone, $product) = @_;
    if (ref $invocant){
        $product = $invocant->product;
    }
    $milestone = trim($milestone);
    $milestone = Bugzilla::Milestone->check({product => $product, name => $milestone});
    return $milestone->name;
}

sub _check_isactive {
    my ($invocant, $isactive) = @_;
    ThrowCodeError('bad_arg', {argument => 'isactive', function => 'set_isactive'}) unless ($isactive =~ /(1|0)/);
    return $isactive;
}

###############################
####       Mutators        ####
###############################
sub set_description { $_[0]->set('description', $_[1]); }
sub set_isactive    { $_[0]->set('isactive', $_[1]); }
sub set_milestone { 
    my ($self, $value) = @_;
    $value = $self->_check_milestone($value);
    $self->set('milestone', $value); 
}
sub set_name { 
    my ($self, $value) = @_;
    
    $value = $self->_check_name($value, $self->product);
    $self->set('name', $value); 
}

sub new {
    my $invocant = shift;
    my $class = ref($invocant) || $invocant;
    my $param = shift;
    
    # We want to be able to supply an empty object to the templates for numerous
    # lists etc. This is much cleaner than exporting a bunch of subroutines and
    # adding them to $vars one by one. Probably just Laziness shining through.
    if (ref $param eq 'HASH'){
        if (!keys %$param || $param->{PREVALIDATED}){
            bless($param, $class);
            return $param;
        }
    }
    
    unshift @_, $param;
    my $self = $class->SUPER::new(@_);
    
    return $self; 
}

sub run_create_validators {
    my $class  = shift;
    my $params = $class->SUPER::run_create_validators(@_);
    my $product = $params->{product_id}; # Returns actual product object
    
    $params->{milestone} = $class->_check_milestone($params->{milestone}, $product);
    $params->{name}      = $class->_check_name($params->{name}, $product);
    
    return $params;
}

sub create {
    my ($class, $params) = @_;

    $class->SUPER::check_required_create_fields($params);
    my $field_values = $class->run_create_validators($params);
    
    $field_values->{isactive}  = 1;
    $field_values->{product_id} = $field_values->{product_id}->id;
    my $self = $class->SUPER::insert_create_data($field_values);
    
    return $self;
}
###############################
####      Functions        ####
###############################
sub check_build {
    my ($name, $product, $throw) = @_;
    my $pid = ref $product ? $product->id : $product;
    my $dbh = Bugzilla->dbh;
    trick_taint($name);
    trick_taint($pid);
    my $is = $dbh->selectrow_array(
        "SELECT build_id FROM test_builds 
         WHERE name = ? AND product_id = ?",
         undef, $name, $pid);
    if ($throw){
        ThrowUserError('invalid-test-id-non-existent', {type => 'Build', id => $name}) unless $is;
        return Testopia::Build->new($is);
    }
    return $is;
}

###############################
####       Methods         ####
###############################
sub TO_JSON {
    my $self = shift;
    my $obj;
    my $json = new JSON;
    
    foreach my $field ($self->DB_COLUMNS){
        $obj->{$field} = $self->{$field};
    }
    $obj->{isactive} = $self->{isactive} ? JSON::true : JSON::false;
        
    return $json->encode($obj); 
}

###############################
####      Accessors        ####
###############################
sub id              { return $_[0]->{'build_id'};   }
sub product_id      { return $_[0]->{'product_id'}; }
sub name            { return $_[0]->{'name'};       }
sub description     { return $_[0]->{'description'};}
sub milestone       { return $_[0]->{'milestone'};}
sub isactive        { return $_[0]->{'isactive'};}

sub bugs {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'bugs'} if exists $self->{'bugs'};
    my $ref = $dbh->selectcol_arrayref(
          "SELECT DISTINCT bug_id
             FROM test_case_bugs b
             JOIN test_case_runs r ON r.case_run_id = b.case_run_id
            WHERE r.build_id = ?", 
           undef, $self->id);
    my @bugs;
    foreach my $id (@{$ref}){
        push @bugs, Bugzilla::Bug->new($id, Bugzilla->user->id);
    }
    $self->{'bugs'} = \@bugs if @bugs;
    $self->{'bug_list'} = join(',', @$ref);
    return $self->{'bugs'};
}

sub product {
    my ($self) = @_;
    
    return $self->{'product'} if exists $self->{'product'};

    $self->{'product'} = Testopia::Product->new($self->product_id);
    return $self->{'product'};
}

sub run_count {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'run_count'} if exists $self->{'run_count'};

    $self->{'run_count'} = $dbh->selectrow_array(
          "SELECT COUNT(run_id) FROM test_runs 
           WHERE build_id = ?", undef, $self->{'build_id'});
          
    return $self->{'run_count'};
}

sub case_run_count {
    my $self = shift;
    my ($status_id, $builds) = @_;
    my $dbh = Bugzilla->dbh;
    
    my @build_ids;
    if ($builds){
        push @build_ids, $_->id foreach (@$builds);
    }
    push @build_ids, $self->id if $self->id;
    
    my $ids = join (',', @build_ids);
    
    my $query = "SELECT COUNT(case_run_id) FROM test_case_runs 
           WHERE build_id IN (". $ids . ")";
    $query .= " AND case_run_status_id = ?" if $status_id;
    
    my $count;
    if ($status_id){
        $count = $dbh->selectrow_array($query, undef, ($status_id));
    }
    else {
        $count = $dbh->selectrow_array($query);
    }
          
    return $count;
}

sub runs {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'runs'} if exists $self->{'runs'};
    
    require Testopia::TestRun;
    
    my $runids = $dbh->selectcol_arrayref("SELECT run_id FROM test_runs
                                          WHERE build_id = ?", 
                                          undef, $self->id);
    my @runs;
    foreach my $id (@{$runids}){
        push @runs, Testopia::TestRun->new($id);
    }
    
    $self->{'runs'} = \@runs;
    return $self->{'runs'};
}

=head2 caseruns

Returns a reference to a list of test caseruns useing this build

=cut

sub caseruns {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'caseruns'} if exists $self->{'caseruns'};
    
    require Testopia::TestCaseRun;

    my $ids = $dbh->selectcol_arrayref("SELECT case_run_id FROM test_case_runs
                                          WHERE build_id = ?", 
                                          undef, $self->id);
    my @caseruns;
    foreach my $id (@{$ids}){
        push @caseruns, Testopia::TestCaseRun->new($id);
    }
    
    $self->{'caseruns'} = \@caseruns;
    return $self->{'caseruns'};
}

1;

__END__

=head1 NAME

Testopia::Build

=head1 EXTENDS

Bugzilla::Object

=head1 DESCRIPTION

Builds are used to classify test runs. They correspond to the results of 
a period of work in software development. Builds are product level attributes
and are associated with a milestone if targetmilestones are used in Bugzilla.

=head1 SYNOPSIS

=head2 Creating

 $build = Testopia::Build->new($build_id);
 $build = Testopia::Build->new({name => $name});

 $new_build = Testopia::Build->create({name => $name, 
                                                 description => $desc
                                                 ... });

=head2 Updating

 $build->set_name($name);
 $build->set_description($name);
 $build->set_milestone($milestone);
 $build->set_isactive($isactive);

 $build->update();

=head2 Accessors

 my $id            = $build->id;
 my $name          = $build->name;
 my $desc          = $build->description;
 my $pid           = $build->product_id;
 my $milestone     = $build->milestone;
 my $crc           = $build->case_run_count;
 my $active        = $build->isactive;

=head1 FIELDS

    +-------------+------------------+------+-----+---------+----------------+
    | Field       | Type             | Null | Key | Default | Extra          |
    +-------------+------------------+------+-----+---------+----------------+
    | build_id    | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
    | product_id  | smallint(6)      | NO   | MUL | 0       |                |
    | milestone   | varchar(20)      | YES  | MUL | NULL    |                |
    | name        | varchar(255)     | YES  | MUL | NULL    |                |
    | description | text             | YES  |     | NULL    |                |
    | isactive    | tinyint(4)       | NO   |     | 1       |                |
    +-------------+------------------+------+-----+---------+----------------+

=over

=item C<build_id> 

The unique id of this build in the database. 

=item C<name> B<REQUIRED>

A unique name for this build.

=item C<product_id> B<REQUIRED> B<CREATE ONLY>

The id of the Bugzilla product this build is attached to.

=item C<milestone> I<OPTIONAL>

The value from the Bugzilla product milestone table this build is associated with.
Defautlts to the product default milestone.

=item C<description> I<OPTIONAL>

A description of this build.

=item C<isactive> I<OPTIONAL>

Boolean - Determines whether to show this build in lists for selection. 
          Defaults to true.  

=back

=head1 FUNCTIONS

=over

=item C<check_build($name, $product_id)>

 Description: Checks if a build of a given name exists for a given product.

 Params:      name - string representing the name to check for.
              product_id - the product to lookup the build in.

 Returns:     The id of the build if one matches.
              undef if it does not match any build.

=back

=head1 METHODS

=over

=item C<new($param)>

 Description: Used to load an existing build from the database.

 Params:      $param - An integer representing the ID in the database
                       or a hash with the "name" key representing the named
                       build in the database.

 Returns:     A blessed Testopia::Build object

=item C<create()>

 Description: Creates a new build object and stores it in the database

 Params:      A hash with keys and values matching the fields of the build to 
              be created.

 Returns:     The newly created object.

=item C<set_description()>

 Description: Replaces the current build's description. Must call update to 
              store the change in the database.

 Params:      text - the new description.

 Returns:     nothing.

=item C<set_isactive()>

 Description: Sets the isactive field. 

 Params:      boolean - 1 for active 0 for inactive.

 Returns:     nothing.

=item C<set_milestone()>

 Description: Assigns this build to a different milestone

 Params:      string - the new milestone value

 Returns:     nothing.

=item C<set_name()>

 Description: Renames the current build. If the new name is already in use
              by another build in this product, an error will be thrown.
              The update method must be called to make the change in the database.

 Params:      string - the new name

 Returns:     nothing.

=item C<to_json()>

 Description: Outputs a JSON representation of the object.

 Params:      none

 Returns:     A JSON string.

=back

=head1 ACCESSORS

=over

=item C<case_run_count()>

 Params:      case_run_status_id - optional; 

 Returns:     The number of case-runs in this build. Optionally for a given status.

=item C<caseruns()>

 Params:      none 

 Returns:     A list of blessed caserun objects that use this build

=item C<description()>

 Returns the description of this build.

=item C<id()>

 Returns the id of the build

=item C<isactive()>

 Returns 1 if this build is visible in pick lists for runs and caserund and 0 if not.

=item C<milestone()>

 Returns the milestone value that this build is associated with.

=item C<name()>

 Returns the name of this build

=item C<product()>

 Returns a Testopia::Product object of the product this build is of.

=item C<product_id()>

 Returns the product id of the build.

=item C<run_count()>

 Returns an integer representing the number of runs this build is associated to.

=item C<runs()>

 Params:      none 

 Returns:     A list of blessed run objects that use this build
 
=back

=head1 SEE ALSO

=over

L<Testopia::Product>
L<Testopia::TestRun> 
L<Bugzilla::Object> 

=back

=head1 AUTHOR

Greg Hendricks <ghendricks@novell.com>