# -*- 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 Test Runner System.
#
# The Initial Developer of the Original Code is Maciej Maczynski.
# Portions created by Maciej Maczynski are Copyright (C) 2001
# Maciej Maczynski. All Rights Reserved.
#
# Contributor(s): Greg Hendricks <ghendricks@novell.com>
#                 Ed Fuentetaja <efuentetaja@acm.org>
#                 Joel Smith <jsmith@novell.com>

package Bugzilla::Testopia::TestRun;

use strict;

use Bugzilla::Util;
use Bugzilla::User;
use Bugzilla::Error;
use Bugzilla::Constants;
use Bugzilla::Bug;
use Bugzilla::Config;

use Bugzilla::Testopia::Constants;
use Bugzilla::Testopia::Environment;
use Bugzilla::Testopia::Build;

use JSON;
use Date::Parse;
use base qw(Exporter Bugzilla::Object);

@Bugzilla::Testopia::TestRun::EXPORT = qw(calculate_percent);

###############################
####    Initialization     ####
###############################

use constant DB_TABLE => "test_runs";
use constant NAME_FIELD => "summary";
use constant ID_FIELD => "run_id";
use constant DB_COLUMNS => qw(
    run_id
    plan_id
    environment_id
    product_version
    build_id
    plan_text_version
    manager_id  
    start_date
    stop_date
    summary
    notes
    target_pass
    target_completion
);

sub report_columns {
    my $self = shift;
    my %columns;
    # Changes here need to match Report.pm
    $columns{'Status'}        = "run_status";        
    $columns{'Version'}       = "default_product_version";
    $columns{'Product'}       = "product";
    $columns{'Build'}         = "build";
    $columns{'Milestone'}     = "milestone";
    $columns{'Environment'}   = "environment";
    $columns{'Tags'}          = "tags";
    $columns{'Manager'}       = "manager";
    my @result;
    push @result, {'name' => $_, 'id' => $columns{$_}} foreach (sort(keys %columns));
    unshift @result, {'name' => '<none>', 'id'=> ''};
    return \@result;     
        
}

use constant REQUIRED_CREATE_FIELDS => qw(plan_id environment_id build_id 
                                          product_version summary manager_id 
                                          plan_text_version);

use constant UPDATE_COLUMNS         => qw(environment_id build_id product_version 
                                          summary manager_id plan_text_version notes
                                          stop_date target_pass target_completion);

use constant VALIDATORS => {
    plan_id           => \&_check_plan,
    environment_id    => \&_check_env,
    build_id          => \&_check_build,
    summary           => \&_check_summary,
    manager_id        => \&_check_manager,
    plan_text_version => \&_check_plan_text_version,
    notes             => \&_check_notes,
    target_pass       => \&_check_target,
    target_completion => \&_check_target,
};

###############################
####       Validators      ####
###############################
sub _check_plan {
    my ($invocant, $plan_id) = @_;
    trick_taint($plan_id);
    ThrowUserError('testopia-missing-required-field', {'field' => 'plan'}) unless $plan_id;
    Bugzilla::Testopia::Util::validate_test_id($plan_id, 'plan');
    return $plan_id;
}

sub _check_env {
    my ($invocant, $env_id) = @_;
    trick_taint($env_id);
    ThrowUserError('testopia-missing-required-field', {'field' => 'environment'}) unless $env_id;
    Bugzilla::Testopia::Util::validate_test_id($env_id, 'environment');
    return $env_id;
}

sub _check_build {
    my ($invocant, $build_id) = @_;
    trick_taint($build_id);
    ThrowUserError('testopia-missing-required-field', {'field' => 'build'}) unless $build_id;
    Bugzilla::Testopia::Util::validate_test_id($build_id, 'build');
    return $build_id;
}

sub _check_product_version {
    my ($invocant, $version, $product) = @_;
    if (ref $invocant){
        $product = $invocant->plan->product;
    }
    $version = trim($version);
    trick_taint($version);
    $version = Bugzilla::Version->check({product => $product, name => $version});
    return $version->name;
}

sub _check_summary{
    my ($invocant, $summary) = @_;
    $summary = clean_text($summary) if $summary;
    trick_taint($summary);
    if (!defined $summary || $summary eq '') {
        ThrowUserError('testopia-missing-required-field', {'field' => 'summary'});
    }
    return $summary;
}

sub _check_manager {
    my ($invocant, $login) = @_;
    $login = trim($login);
    ThrowUserError('testopia-missing-required-field', {'field' => 'manager'}) unless $login;
    if ($login =~ /^\d+$/){
        $login = Bugzilla::User->new($login);
        return $login->id;
    }
    else {
        my $id = login_to_id($login, THROW_ERROR);
        return $id;
    }
}

#TODO: Check that version is in plan versions
sub _check_plan_text_version {
    my ($invocant, $version) = @_;
    trick_taint($version);
    ThrowUserError('testopia-missing-required-field', {'field' => 'plan_version'}) unless $version;
    return $version;
}

sub _check_notes {
    my ($invocant, $notes) = @_;
    trick_taint($notes);
    $notes = trim($notes);
    $notes =~ s/\n$//;
    return $notes;
}

sub _check_target {
    my ($invocant, $target) = @_;
    detaint_natural($target);
    return unless $target;
    ThrowUserError('invalid_target') unless $target >= 0 && $target <= 100;
    return $target;
}

###############################
####       Mutators        ####
###############################
sub set_environment        { $_[0]->set('environment_id', $_[1]); }
sub set_build              { $_[0]->set('build_id', $_[1]); }
sub set_summary            { $_[0]->set('summary', $_[1]); }
sub set_manager            { $_[0]->set('manager_id', $_[1]); }
sub set_plan_text_version  { $_[0]->set('plan_text_version', $_[1]); }
sub set_notes              { $_[0]->set('notes', $_[1]); }
sub set_stop_date          { $_[0]->set('stop_date', $_[1]); }
sub set_target_pass        { $_[0]->set('target_pass', $_[1]); }
sub set_target_completion  { $_[0]->set('target_completion', $_[1]); }

sub set_product_version    { 
    my ($self, $value) = @_;
    $value = $self->_check_product_version($value);
    $self->set('product_version', $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'){
        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 $plan = Bugzilla::Testopia::TestPlan->new($params->{plan_id});
    
    $params->{product_version} = $class->_check_product_version($params->{product_version}, $plan->product);
    
    return $params;
}

sub create {
    my ($class, $params) = @_;
    require Bugzilla::Testopia::TestPlan;
    
    $class->SUPER::check_required_create_fields($params);
    my $field_values = $class->run_create_validators($params);
    my $timestamp = Bugzilla::Testopia::Util::get_time_stamp();
    $field_values->{start_date} = $timestamp; 
    $field_values->{stop_date} = Bugzilla::Testopia::Util::get_time_stamp() if $field_values->{status} == 0;  
    delete $field_values->{status};
    
    my $self = $class->SUPER::insert_create_data($field_values);

    return $self;
}

sub update {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $timestamp = Bugzilla::Testopia::Util::get_time_stamp();

    $dbh->bz_start_transaction();
    
    my $changed = $self->SUPER::update();

    foreach my $field (keys %$changed){
        Bugzilla::Testopia::Util::log_activity('run', $self->id, $field, $timestamp, $changed->{$field}->[0], $changed->{$field}->[1]);
    }

    $dbh->bz_commit_transaction();
}

###############################
####       Methods         ####
###############################

=head2 calculate_percent_completed

Calculates a percentage from two numbers. Takes the total number
of IDLE case runs and the number of those that have another status
and adds them to get a total then takes the percentage.

=cut

sub calculate_percent {
  my ($total, $count) = (@_);
  my $percent;
  if ($total == 0) {
    $percent = 0;
  } else {
    $percent = $count*100/$total;
    $percent = int($percent + 0.5);
    if (($percent == 100) && ($count != $total)) {
      #I don't want to see 100% unless every test is run
      $percent = 99;
    }
  }
  return $percent; 
}

=head2 add_cc

Adds a user to the CC list for this run

=cut

sub add_cc{
    my $self = shift;
    my ($ccid) = (@_);
    my $dbh = Bugzilla->dbh;
    $dbh->do("INSERT INTO test_run_cc(run_id, who) 
              VALUES (?,?)", undef, $self->{'run_id'}, $ccid);
    #TODO: send mail
    return 1;
}

=head2 remove_cc

Removes a user from the CC list of this run

=cut

sub remove_cc{
    my $self = shift;
    my ($ccid) = (@_);
    my $dbh = Bugzilla->dbh;
    $dbh->do("DELETE FROM test_run_cc 
              WHERE run_id=? AND who=?", 
              undef, $self->{'run_id'}, $ccid);
    #TODO: send mail
    return 1;
}

=head2 add_tag

Associates a tag with this test run

=cut

sub add_tag {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my @tags;
    foreach my $t (@_){
        if (ref $t eq 'ARRAY'){
            push @tags, $_ foreach @$t;
        }
        else{
            push @tags, split(',', $t);
        }
    }

    foreach my $name (@tags){
        my $tag = Bugzilla::Testopia::TestTag->create({'tag_name' => $name});
        $tag->attach($self);
    }
}

=head2 remove_tag

Disassociates a tag from this test run

=cut

sub remove_tag {    
    my $self = shift;
    my ($tag_name) = @_;
    my $tag = Bugzilla::Testopia::TestTag->check_tag($tag_name);
    ThrowUserError('testopia-unknown-tag', {'name' => $tag}) unless $tag;
    my $dbh = Bugzilla->dbh;
    $dbh->do("DELETE FROM test_run_tags 
              WHERE tag_id=? AND run_id=?",
              undef, $tag->id, $self->{'run_id'});
    return;
}

=head2 add_case_run

Associates a test case with this run by adding a new row to 
the test_case_runs table

=cut

sub add_case_run {
    my $self = shift;
    my ($case_id, $sortkey, $status) = @_;
    $status ||=IDLE;
    trick_taint($case_id);
    return 0 if $self->check_case($case_id);
    my $case = Bugzilla::Testopia::TestCase->new($case_id);
    $sortkey = $case->sortkey unless $sortkey;

    return 0 if $case->status ne 'CONFIRMED';
    my $assignee = $case->default_tester ? $case->default_tester->id : undef;
    my $caserun = Bugzilla::Testopia::TestCaseRun->create({
        'run_id'     => $self->{'run_id'},
        'case_id'    => $case_id,
        'assignee'   => $assignee,
        'case_text_version'  => $case->version,
        'build_id'           => $self->build->id,
        'environment_id'     => $self->environment_id,
        'case_run_status_id' => $status,
        'sortkey'            => $sortkey,
    });
    return 1;
}

=head2 store

Stores a test run object in the database. This method is used to store a 
newly created test run. It returns the new ID.

=cut

sub store {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    # Exclude the auto-incremented field from the column list.
    my $columns = join(", ", grep {$_ ne 'run_id'} DB_COLUMNS);
    my $timestamp = Bugzilla::Testopia::Util::get_time_stamp();

    $dbh->do("INSERT INTO test_runs ($columns) VALUES (?,?,?,?,?,?,?,?,?,?)",
              undef, ($self->{'plan_id'}, $self->{'environment_id'},
              $self->{'product_version'}, $self->{'build_id'}, 
              $self->{'plan_text_version'}, $self->{'manager_id'}, 
              $timestamp, undef, $self->{'summary'}, $self->{'notes'}));
    my $key = $dbh->bz_last_key( 'test_runs', 'run_id' );
    return $key;
}

=head2 update_notes

Updates just the notes for this run

=cut

sub update_notes {
    my $self = shift;
    my ($notes) = @_;
    my $dbh = Bugzilla->dbh;
    $dbh->do("UPDATE test_runs 
              SET notes = ? WHERE run_id = ?",
              undef, $notes, $self->{'run_id'});
}

=head2 clone

Creates a copy of this test run. Accepts the summary of the new run
and the build id to use.

=cut

sub clone {
    my $self = shift;
    my ($summary, $manager, $plan_id, $build_id, $env_id) = @_;
    my $dbh = Bugzilla->dbh;
    # Exclude the auto-incremented field from the column list.
    my $columns = join(", ", grep {$_ ne 'run_id'} DB_COLUMNS);
    my $timestamp = Bugzilla::Testopia::Util::get_time_stamp();

    $dbh->do("INSERT INTO test_runs ($columns) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
              undef, ($plan_id, $env_id,
              $self->{'product_version'}, $build_id, 
              $self->{'plan_text_version'}, $manager, 
              $timestamp, undef, $summary, undef, 
              $self->{'target_pass'}, $self->{'target_completion'}));
    my $key = $dbh->bz_last_key( 'test_runs', 'run_id' );
    return $key;   
}

=head2 history

Returns a reference to a list of history entries from the 
test_run_activity table.

=cut

sub history {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $ref = $dbh->selectall_arrayref(
            "SELECT defs.description AS what, 
                    p.login_name AS who, a.changed, a.oldvalue, a.newvalue
               FROM test_run_activity AS a
               JOIN test_fielddefs AS defs ON a.fieldid = defs.fieldid
               JOIN profiles AS p ON a.who = p.userid
              WHERE a.run_id = ?",
              {'Slice'=>{}}, $self->{'run_id'});

    foreach my $row (@$ref){
        if ($row->{'what'} eq 'Environment'){
            $row->{'oldvalue'} = $self->lookup_environment($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_environment($row->{'newvalue'});
        }
        elsif ($row->{'what'} eq 'Default Build'){
            $row->{'oldvalue'} = $self->lookup_build($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_build($row->{'newvalue'});
        }
        elsif ($row->{'what'} eq 'Manager'){
            $row->{'oldvalue'} = $self->lookup_manager($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_manager($row->{'newvalue'});
        }
    }        
    return $ref;
}

=head2 obliterate

Removes this run and all things that reference it.

=cut

sub obliterate {
    my $self = shift;
    my ($cgi, $template) = @_;
    my $dbh = Bugzilla->dbh;
    my $vars;
    
    my $progress_interval = 500;
    my $i = 0;
    my $total = scalar @{$self->caseruns};
    
    foreach my $obj (@{$self->caseruns}){
        $i++;
        if ($cgi && $i % $progress_interval == 0){
            print $cgi->multipart_end;
            print $cgi->multipart_start;
            $vars->{'complete'} = $i;
            $vars->{'total'} = $total;
            $vars->{'process'} = "Deleting Run " . $self->id;
        
            $template->process("testopia/progress.html.tmpl", $vars)
              || ThrowTemplateError($template->error());
        }
    
        $obj->obliterate;
    }

    $dbh->do("DELETE FROM test_run_cc WHERE run_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_run_tags WHERE run_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_run_activity WHERE run_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_runs WHERE run_id = ?", undef, $self->id);
    return 1;
}

=head2 Check_case

Checks if the given test case is already associated with this run

=cut

sub check_case {
    my $self = shift;
    my ($case_id) = @_;
    my $dbh = Bugzilla->dbh;
    my ($value) = $dbh->selectrow_array(
            "SELECT case_run_id 
               FROM test_case_runs
              WHERE case_id = ? AND run_id = ?",
              undef, ($case_id, $self->{'run_id'}));
    return $value;
}

sub TO_JSON {
    my $self = shift;
    my $obj;
    my $json = new JSON;
    my $rc = $self->case_run_count;
    
    $self->bugs();
    
    foreach my $field ($self->DB_COLUMNS){
        $obj->{$field} = $self->{$field};
    }
    $obj->{'plan'}          = { id => $self->plan->id, product_id => $self->plan->product_id} if $self->plan;
    $obj->{'build'}         = { id => $self->build->id, name => $self->build->name} if $self->build;
    $obj->{'environment'}   = { id => $self->environment->id, name => $self->environment->name} if $self->environment;
    $obj->{'case_count'}    = $self->case_run_count;
    $obj->{'manager'}       = { login_name => $self->manager->login, name => $self->manager->name} if $self->manager;
    $obj->{'manager_name'}  = $self->manager->name if $self->manager;
    $obj->{'canedit'}       = $self->canedit;
    $obj->{'canview'}       = $self->canview;
    $obj->{'candelete'}     = $self->candelete;
    $obj->{'status'}        = $self->stop_date ? 'STOPPED' : 'RUNNING';
    $obj->{'type'}          = $self->type;
    $obj->{'id'}            = $self->id;
    $obj->{'product_id'}    = $self->plan->product_id if $self->plan;
    $obj->{'passed_pct'}    = $self->case_run_count(PASSED) / $rc if $rc; 
    $obj->{'failed_pct'}    = $self->case_run_count(FAILED) / $rc if $rc;
    $obj->{'blocked_pct'}   = $self->case_run_count(BLOCKED) / $rc if $rc;
    $obj->{'complete_pct'}  = $self->percent_complete() . '%';
    $obj->{'bug_list'}      = $self->{'bug_list'};
    
    return $json->encode($obj); 
}

=head2 lookup_environment

Takes an ID of the envionment field and returns the value

=cut

sub lookup_environment {
    my $self = shift;
    my ($id) = @_;
    my $dbh = Bugzilla->dbh;
    my ($value) = $dbh->selectrow_array(
            "SELECT name 
               FROM test_environments
              WHERE environment_id = ?",
              undef, $id);
    return $value;
}

=head2 lookup_environment_by_name

Takes the name of an envionment and returns its id

=cut

sub lookup_environment_by_name {
    my ($name) = @_;
    my $dbh = Bugzilla->dbh;
    my ($value) = $dbh->selectrow_array(
            "SELECT environment_id 
               FROM test_environments
              WHERE name = ?",
              undef, $name);
    return $value;
}

=head2 lookup_build

Takes an ID of the build field and returns the value

=cut

sub lookup_build {
    my $self = shift;
    my ($id) = @_;
    my $dbh = Bugzilla->dbh;
    my ($value) = $dbh->selectrow_array(
            "SELECT name 
               FROM test_builds
              WHERE build_id = ?",
              undef, $id);
    return $value;
}

=head2 lookup_manager

Takes an ID of the manager field and returns the value

=cut

sub lookup_manager {
    my $self = shift;
    my ($id) = @_;
    my $dbh = Bugzilla->dbh;
    my ($value) = $dbh->selectrow_array(
            "SELECT login_name 
               FROM profiles
              WHERE userid = ?",
              undef, $id);
    return $value;
}

=head2 last_changed

Returns the date of the last change in the history table

=cut

sub last_changed {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    
    my ($date) = $dbh->selectrow_array(
            "SELECT MAX(changed)
               FROM test_run_activity 
              WHERE run_id = ?",
              undef, $self->id);

    return $self->{'creation_date'} unless $date;
    return $date;
}

sub filter_case_categories {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    
    my $ids = $dbh->selectcol_arrayref(
            "SELECT DISTINCT tcc.category_id, tcc.name
               FROM test_case_categories AS tcc
               JOIN test_cases ON test_cases.category_id = tcc.category_id
               JOIN test_case_runs AS tcr ON test_cases.case_id = tcr.case_id  
              WHERE run_id = ?
              ORDER BY tcc.name",
              undef, $self->id);
    
    my @categories;
    foreach my $id (@$ids){
        push @categories, Bugzilla::Testopia::Category->new($id);
    }
    
    return \@categories;
}

sub filter_builds {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    
    my $ids = $dbh->selectcol_arrayref(
            "SELECT DISTINCT test_case_runs.build_id, test_builds.name
               FROM test_case_runs
               INNER JOIN test_builds on test_builds.build_id = test_case_runs.build_id
              WHERE run_id = ?
              ORDER BY test_builds.name",
              undef, $self->id);
    
    my @builds;
    foreach my $id (@$ids){
        push @builds, Bugzilla::Testopia::Build->new($id);
    }
    return \@builds;
}

sub filter_components {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    
    my $ids = $dbh->selectcol_arrayref(
            "SELECT DISTINCT components.id, components.name
               FROM components
               JOIN test_case_components AS tcc ON tcc.component_id = components.id
               JOIN test_cases ON test_cases.case_id = tcc.case_id
               JOIN test_case_runs AS tcr ON test_cases.case_id = tcr.case_id  
              WHERE run_id = ?
              ORDER BY components.name",
              undef, $self->id);
    
    my @components;
    foreach my $id (@$ids){
        push @components, Bugzilla::Component->new($id);
    }
    
    return \@components;
}

=head2 environments

Returns a reference to a list of Testopia::Environment objects.

=cut

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

    my $environments = 
      $dbh->selectcol_arrayref("SELECT environment_id
                                FROM test_environments");
    
    my @environments;
    foreach my $id (@{$environments}){
        push @environments, Bugzilla::Testopia::Environment->new($id);
    }
    $self->{'environments'} = \@environments;
    return $self->{'environments'};
}

=head2 get_status_list

Returns a list of statuses for a run

=cut

sub get_status_list {
    my @status = (
        { 'id' => 1, 'name' => 'RUNNING' },
        { 'id' => 0, 'name' => 'STOPPED' },
    );
    return \@status;
}


=head2 get_fields

Returns a reference to a list of test run field descriptions from 
the test_fielddefs table. 

=cut

sub get_fields {
    my $self = shift;
    my $dbh = Bugzilla->dbh;    

    my $types = $dbh->selectall_arrayref(
            "SELECT fieldid AS id, description AS name 
             FROM test_fielddefs 
             WHERE table_name=?", 
             {"Slice"=>{}}, "test_runs");
    unshift @$types, {id => '[Creation]', name => '[Started]'};
    return $types;
}

=head2 get_distinct_builds

Returns a list of build names for use in searches

=cut

sub get_distinct_builds {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $query = "SELECT build.name AS id, build.name " .
                  "FROM test_builds AS build " .
                  "JOIN products ON build.product_id = products.id " .
             "LEFT JOIN group_control_map " .
              "ON group_control_map.product_id = products.id ";
    if (Bugzilla->params->{'useentrygroupdefault'}) {
        $query .= "AND group_control_map.entry != 0 ";
    } else {
        $query .= "AND group_control_map.membercontrol = " .
              CONTROLMAPMANDATORY . " ";
    }
    if (%{Bugzilla->user->groups}) {
        $query .= "AND group_id NOT IN(" . 
              join(',', values(%{Bugzilla->user->groups})) . ") ";
    }
    $query .= "WHERE group_id IS NULL AND build.isactive = 1 ORDER BY build.name";
    
    my $ref = $dbh->selectall_arrayref($query, {'Slice'=>{}});

    return $ref;             
}

=head2 get_distinct_milestones

Returns a list of milestones for use in searches

=cut

sub get_distinct_milestones {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $ref = $dbh->selectall_arrayref(
            "SELECT DISTINCT value AS id, value as name
             FROM milestones
             ORDER BY sortkey", {'Slice'=>{}});

    return $ref;             
}

=head2 get_environments

Returns a list of environments for use in searches

=cut

sub get_environments {
    my $dbh = Bugzilla->dbh;
    my $ref = $dbh->selectall_arrayref(
                "SELECT DISTINCT name AS id, name
                 FROM test_environments
                 ORDER BY name",
                 {'Slice'=>{}});
                 
    return $ref;
}

=head2 canview

Returns true if the logged in user has rights to view this test run.

=cut

sub canview {
    my $self = shift;
    return 1 if Bugzilla->user->in_group('Testers');
    return 1 if $self->plan->get_user_rights(Bugzilla->user->id) & TR_READ;
    return 0;

}

=head2 canedit

Returns true if the logged in user has rights to edit this test run.

=cut

sub canedit {
    my $self = shift;
    return 1 if Bugzilla->user->in_group('Testers');
    return 1 if $self->plan->get_user_rights(Bugzilla->user->id) & TR_WRITE;
    return 0;
}

# Only certain people are able to change the status of a run.
sub canstatus {
    my $self = shift;
    return 1 if Bugzilla->user->in_group('admin');
    return 1 if $self->plan->get_user_rights(Bugzilla->user->id) & TR_ADMIN;
    return 1 if $self->manager->id == Bugzilla->user->id;
    return 0;
}

=head2 candelete

Returns true if the logged in user has rights to delete this test run.

=cut

sub candelete {
    my $self = shift;
    return 1 if Bugzilla->user->in_group('admin');
    return 0 unless Bugzilla->params->{"allow-test-deletion"};
    return 1 if Bugzilla->user->in_group('Testers') && Bugzilla->params->{"testopia-allow-group-member-deletes"};
    return 1 if $self->plan->get_user_rights(Bugzilla->user->id) & TR_DELETE;
    return 0;
}

sub completion_percent {
    my $self = shift;
    my ($products, $plans, $runs) = @_;
    my $dbh = Bugzilla->dbh;
    my @runs;
    foreach my $p (@$products){
        foreach my $plan (@{$p->plans}){
            push @runs, $_->id foreach (@{$plan->test_runs});
        }
    }
    push @runs, $_->id foreach (@{$plans->test_runs});
    push @runs, $_->id foreach (@$runs);
    return 0 unless scalar @runs;
    
    my $run_ids = join (',',@runs);
    
    
}

sub total_time {
    my $self = shift;

    return 0 unless $self->stop_date;
    
    my $seconds = str2time($self->stop_date) - str2time($self->start_date);    
    
    my @time = gmtime($seconds);
    my %time;
    
    $time{day} = $time[7];
    $time{hr}  = $time[2];
    $time{min} = $time[1];
    $time{sec} = $time[0];

    return $time{day}.":".$time{hr}.":".$time{min}.":".$time{sec};
}

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

=head1 ACCESSOR METHODS

=head2 id

Returns the ID for this object

=head2 plan_text_version

Returns the plan's text version of this run

=head2 plan_id

Returns the  plan idof this run

=head2 environment_id

Returns the environment id of this run

=head2 manager

Returns a Bugzilla::User object representing the run's manager

=head2 start_date

Returns the time stamp of when this run was started

=head2 stop_date

Returns the  time stamp of when this run was completed

=head2 summary

Returns the summary of this run

=head2 notes

Returns the notes for this run

=head2 product_version

Returns the product version of this run

=cut

sub id                { return $_[0]->{'run_id'};          }
sub plan_text_version { return $_[0]->{'plan_text_version'};  }
sub plan_id           { return $_[0]->{'plan_id'};  }
sub environment_id    { return $_[0]->{'environment_id'};  }
sub manager           { return Bugzilla::User->new($_[0]->{'manager_id'});  }
sub start_date        { return $_[0]->{'start_date'};        }
sub stop_date         { return $_[0]->{'stop_date'}; }
sub summary           { return $_[0]->{'summary'};  }
sub notes             { return $_[0]->{'notes'};  }
sub product_version   { return $_[0]->{'product_version'};  }
sub target_pass       { return $_[0]->{'target_pass'};  }
sub target_completion { return $_[0]->{'target_completion'};  }

=head2 type

Returns 'case'

=cut

sub type {
    my $self = shift;
    $self->{'type'} = 'run';
    return $self->{'type'};
}

=head2 plan

Returns the Testopia::TestPlan object of the plan this run 
is assoceated with

=cut

sub plan {
    my $self = shift;
    return $self->{'plan'} if exists $self->{'plan'};
    require Bugzilla::Testopia::TestPlan;
    $self->{'plan'} = Bugzilla::Testopia::TestPlan->new($self->{'plan_id'});
    return $self->{'plan'};
}

=head2 tags

Returns a reference to a list of Testopia::TestTag objects 
associated with this run

=cut

sub tags {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'tags'} if exists $self->{'tags'};
    my $tagids = $dbh->selectcol_arrayref("SELECT test_run_tags.tag_id 
                                          FROM test_run_tags
                                          INNER JOIN test_tags ON test_run_tags.tag_id = test_tags.tag_id
                                          WHERE run_id = ?
                                          ORDER BY test_tags.tag_name", 
                                          undef, $self->{'run_id'});
    my @tags;
    foreach my $t (@{$tagids}){
        push @tags, Bugzilla::Testopia::TestTag->new($t);
    }
    $self->{'tags'} = \@tags;
    return $self->{'tags'};
}

=head2 environment

Returns the Testopia::Environment object of the environment 
this run is assoceated with

=cut

sub environment {
    my $self = shift;
    return $self->{'environment'} if exists $self->{'environment'};
    $self->{'environment'} = Bugzilla::Testopia::Environment->new($self->{'environment_id'});
    return $self->{'environment'};
    
}

=head2 build

Returns the Testopia::Build object of the plan this run 
is assoceated with

=cut

sub build {
    my $self = shift;
    return $self->{'build'} if exists $self->{'build'};
    $self->{'build'} = Bugzilla::Testopia::Build->new($self->{'build_id'});
    return $self->{'build'};
    
}

=head2 runtime

Returns the total time the run took to complete

=cut

sub runtime {
    
}

=head2 bugs

Returns a reference to a list of Bugzilla::Bug objects associated
with this run

=cut

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.run_id = ? AND r.iscurrent = 1 ORDER BY bug_id", 
           undef, $self->{'run_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'};
}

=head2 cc

Returns a reference to a list of Bugzilla::User objects
on the CC list of this run

=cut

sub cc {
    my $self = shift;
    return $self->{'cc'} if exists $self->{'cc'};
    my $dbh = Bugzilla->dbh;
    my $ref = $dbh->selectcol_arrayref(
        "SELECT who FROM test_run_cc 
         WHERE run_id=?", undef, $self->{'run_id'});
    my @cc;     
    foreach my $id (@{$ref}){
        push @cc, Bugzilla::User->new($id);
    }
    $self->{'cc'} = \@cc;
    return $self->{'cc'};
}

=head2 cases

Returns a reference to a list of Testopia::TestCase objects 
associated with this run

=cut

sub cases {
    my $self = shift;
    return $self->{'cases'} if exists $self->{'cases'};
    my @cases;
    foreach my $cr (@{$self->current_caseruns}){
        push @cases, Bugzilla::Testopia::TestCase->new($cr->case_id);
    }
    $self->{'cases'} = \@cases;
    return $self->{'cases'};
    
}

sub case_ids {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'case_ids'} if exists $self->{'case_ids'};
    
    my $ref = $dbh->selectcol_arrayref(
        "SELECT DISTINCT case_id FROM test_case_runs
         WHERE run_id=? AND iscurrent=1", undef,
         $self->{'run_id'});
    
    $self->{'case_ids'} = $ref;
    return $self->{'case_ids'};
    
}

=head2 case_count

Returns a count of the test cases associated with this run

=cut

sub case_count {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    
    my ($count) = $dbh->selectrow_array(
                    "SELECT COUNT(case_run_id) FROM test_case_runs
                      WHERE run_id=? AND iscurrent=1", undef,
                      $self->{'run_id'});

    return scalar $count;
}

sub case_run_count {
    my $self = shift;
    my ($status_id, $runs, $plans, $products) = @_;    
    my $dbh = Bugzilla->dbh;
    
    my @runs;
    if ($products){
        foreach my $p (@$products){
            foreach my $plan (@{$p->plans}){
                push @runs, $_->id foreach (@{$plan->test_runs});
            }
        }
    }
    if ($plans){
        push @runs, $_->id foreach (@{$plans->test_runs});
    }
    if ($runs){
        push @runs, $_->id foreach (@$runs);
    }
    push @runs, $self->id if $self->id;
    
    return 0 unless scalar @runs > 0;
    
    my $run_ids = join (',', @runs);

    my $query = 
           "SELECT COUNT(*) 
              FROM test_case_runs 
             WHERE run_id IN (" . $run_ids .") AND iscurrent = 1";
    $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 case_run_count_by_date {
    my $self = shift;
    my ($start, $stop, $status_id, $tester, $runs, $plans, $products) = @_;    
    my $dbh = Bugzilla->dbh;
    
    my @runs;
    if ($products){
        foreach my $p (@$products){
            foreach my $plan (@{$p->plans}){
                push @runs, $_->id foreach (@{$plan->test_runs});
            }
        }
    }
    if ($plans){
        push @runs, $_->id foreach (@{$plans->test_runs});
    }
    if ($runs){
        push @runs, $_->id foreach (@$runs);
    }
    push @runs, $self->id if $self->id;
    
    return 0 unless scalar @runs > 0;
    
    my $run_ids = join (',', @runs);

    my $query = 
           "SELECT COUNT(*) 
              FROM test_case_runs 
             WHERE run_id IN (" . $run_ids .") 
               AND close_date >= ?
               AND close_date <= ?
               AND iscurrent = 1";
    $query .= " AND case_run_status_id = ?" if $status_id;
    $query .= " AND testedby = $tester" if $tester;
    
    my $count;
    if ($status_id){
        ($count) = $dbh->selectrow_array($query,undef,($start,$stop,$status_id));
    }
    else {
        ($count) = $dbh->selectrow_array($query,undef,($start,$stop));
    }

    return $count;
}

sub case_run_count_by_priority {
    my $self = shift;
    my ($priority, $status_id, $runs, $plans, $products) = @_;    
    trick_taint($priority);
    my $dbh = Bugzilla->dbh;
    
    my @runs;
    if ($products){
        foreach my $p (@$products){
            foreach my $plan (@{$p->plans}){
                push @runs, $_->id foreach (@{$plan->test_runs});
            }
        }
    }
    if ($plans){
        push @runs, $_->id foreach (@{$plans->test_runs});
    }
    if ($runs){
        push @runs, $_->id foreach (@$runs);
    }
    push @runs, $self->id if $self->id;
    
    return 0 unless scalar @runs > 0;
    
    my $run_ids = join (',', @runs);

    my $query = 
           "SELECT COUNT(*) 
              FROM test_case_runs
        INNER JOIN test_cases on test_case_runs.case_id = test_cases.case_id 
             WHERE run_id IN (" . $run_ids .") 
               AND test_cases.priority_id = ?
               AND iscurrent = 1";
    $query .= " AND case_run_status_id = ?" if $status_id;

    my $count;
    if ($status_id){
        ($count) = $dbh->selectrow_array($query,undef,($priority, $status_id));
    }
    else {
        ($count) = $dbh->selectrow_array($query,undef,($priority));
    }

    return $count;
}

sub finished_count {
    my $self = shift;
    my ($status_id) = @_;
    my $dbh = Bugzilla->dbh;
    my ($count) = $dbh->selectrow_array( 
           "SELECT COUNT(*) 
              FROM test_case_runs 
             WHERE run_id = ? AND iscurrent = 1
               AND case_run_status_id IN (?,?,?)",undef, ($self->id, FAILED, PASSED, BLOCKED)); 
        
    return $count;       
}



=head2 percent_complete

Returns a number representing the percentage of case-runs
that have a status vs. those with a status of IDLE

=cut

sub percent_complete {    
    my $self = shift;
    return $self->{'percent_complete'} if defined $self->{'percent_complete'};
    $self->{'percent_complete'} = calculate_percent($self->case_run_count,$self->finished_count);
    return $self->{'percent_complete'};
}

sub percent_of_total {
    my $self = shift;
    my ($status_id) = @_;
    return calculate_percent($self->case_run_count,$self->case_run_count($status_id));    
}

sub percent_of_finished {
    my $self = shift;
    my ($status_id) = @_;
    return calculate_percent($self->finished_count,$self->case_run_count($status_id));        
}

=head2 current_caseruns

Returns a reference to a list of TestCaseRun objects that are the
current case-runs on this run

=cut

sub current_caseruns {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'current_caseruns'} if exists $self->{'current_caseruns'};
    
    require Bugzilla::Testopia::TestCaseRun;
    
    my $ref = $dbh->selectcol_arrayref(
        "SELECT case_run_id FROM test_case_runs
         WHERE run_id=? AND iscurrent=1", undef,
         $self->{'run_id'});
    my @caseruns;
    
    foreach my $id (@{$ref}){
        push @caseruns, Bugzilla::Testopia::TestCaseRun->new($id);
    }
    $self->{'current_caseruns'} = \@caseruns;
    return $self->{'current_caseruns'};
}

=head2 caseruns

Returns a reference to a list of TestCaseRun objects that belong
to this run

=cut

sub caseruns {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'caseruns'} if exists $self->{'caseruns'};
    
    require Bugzilla::Testopia::TestCaseRun;
    
    my $ref = $dbh->selectcol_arrayref(
        "SELECT case_run_id FROM test_case_runs
         WHERE run_id=?", undef, $self->{'run_id'});
    my @caseruns;
    
    foreach my $id (@{$ref}){
        push @caseruns, Bugzilla::Testopia::TestCaseRun->new($id);
    }
    $self->{'caseruns'} = \@caseruns;
    return $self->{'caseruns'};
}

=head2 case_id_list

Returns a list of case_id's from the current case runs.

=cut

sub case_id_list {
    my $self = shift;
    my @ids;
    foreach my $c (@{$self->current_caseruns}){
        push @ids, $c->case_id;
    }
    
    return join(",", @ids);
}

=head1 SEE ALSO

Testopia::(TestPlan, TestCase, Category, Build, Environment)

=head1 AUTHOR

Greg Hendricks <ghendricks@novell.com>

=cut

1;

__END__

=head1 NAME

Bugzilla::Testopia::TestRun - Testopia Test Run object

=head1 DESCRIPTION

This module represents a test run in Testopia. A test run is the 
place where most of the work of testing is done. A run is associated 
with a single test plan and multiple test cases through the test 
case-runs.

=head1 SYNOPSIS

use Bugzilla::Testopia::TestRun;

 $run = Bugzilla::Testopia::TestRun->new($run_id);
 $run = Bugzilla::Testopia::TestRun->new(\%run_hash);

=cut

=head1 FIELDS

    run_id
    plan_id
    environment_id
    product_version
    build_id
    plan_text_version
    manager_id
    start_date
    stop_date
    summary
    notes

=cut

=head2 new

Instantiate a new Test Run. This takes a single argument 
either a test run ID or a reference to a hash containing keys 
identical to a test run's fields and desired values.

=cut
