# -*- 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.
#
# Portions taken from processbug.cgi and Bug.pm 
# which are copyrighted by
#                 Terry Weissman <terry@mozilla.org>
#                 Dan Mosedale <dmose@mozilla.org>
#                 Dave Miller <justdave@syndicomm.com>
#                 Christopher Aillon <christopher@aillon.com>
#                 Myk Melez <myk@mozilla.org>
#                 Jeff Hedlund <jeff.hedlund@matrixsi.com>
#                 Frederic Buclin <LpSolit@gmail.com>
#
# Contributor(s): Greg Hendricks <ghendricks@novell.com>

=head1 NAME

Bugzilla::Testopia::TestCase - Testopia Test Case object

=head1 DESCRIPTION

This module represents a test case in Testopia. Each test case must 
be linked to one or more test plans.

=head1 SYNOPSIS

use Bugzilla::Testopia::TestCase;

 $case = Bugzilla::Testopia::TestCase->new($case_id);
 $case = Bugzilla::Testopia::TestCase->new(\%case_hash);

=cut

package Bugzilla::Testopia::TestCase;

use strict;

use Bugzilla::Util;
use Bugzilla::Bug;
use Bugzilla::User;
use Bugzilla::Config;
use Bugzilla::Error;
use Bugzilla::Constants;
use Bugzilla::Testopia::Util;
use Bugzilla::Testopia::TestPlan;
use Bugzilla::Testopia::TestRun;
use Bugzilla::Testopia::TestCaseRun;
use Bugzilla::Testopia::Category;

use Text::Diff;

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

=head1 FIELDS

    case_id
    case_status_id
    category_id
    priority_id
    author_id
    default_tester_id  
    creation_date
    isautomated
    sortkey
    script
    arguments
    summary
    requirement
    alias

=cut

use constant DB_COLUMNS => qw(
    test_cases.case_id
    test_cases.case_status_id
    test_cases.category_id
    test_cases.priority_id
    test_cases.author_id
    test_cases.default_tester_id  
    test_cases.creation_date
    test_cases.isautomated
    test_cases.sortkey
    test_cases.script
    test_cases.arguments
    test_cases.summary
    test_cases.requirement
    test_cases.alias
);

use constant ALIAS_MAX_LENGTH => 255;
use constant REQUIREMENT_MAX_LENGTH => 255;
use constant SUMMARY_MAX_LENGTH => 255;
use constant TAG_MAX_LENGTH => 255;

our $columns = join(", ", DB_COLUMNS);

sub display_columns {
my $self = shift;
my @columns = 
   [{column => 'case_id',        desc => 'ID'          },
    {column => 'case_status_id', desc => 'Status'      },
    {column => 'category_id',    desc => 'Category'    },
    {column => 'priority_id',    desc => 'Priority'    },
    {column => 'summary',        desc => 'Summary'     },
    {column => 'requirement',    desc => 'Requirement' },
    {column => 'alias',          desc => 'Alias'       }];

$self->{'display_columns'} = \@columns;
return $self->{'display_columns'};
}

sub report_columns {
    my $self = shift;
    my %columns;
    # Changes here need to match Report.pm
    $columns{'Status'}          = "case_status";        
    $columns{'Priority'}        = "priority";
    $columns{'Product'}         = "product_id";
    $columns{'Component'}       = "component";
    $columns{'Category'}        = "category";
    $columns{'Automated'}       = "isautomated";
    $columns{'Tags'}            = "tags";
    $columns{'Requirement'}     = "requirement";
    $columns{'Author'}          = "author";
    $columns{'Default tester'}  = "default_tester";
    my @result;
    push @result, {'name' => $_, 'id' => $columns{$_}} foreach (sort(keys %columns));
    unshift @result, {'name' => '<none>', 'id'=> ''};
    return \@result;     
        
}

=head1 METHODS

=cut

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

=head2 new

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

=cut

sub new {
    my $invocant = shift;
    my $class = ref($invocant) || $invocant;
    my $self = {};
    bless($self, $class);
    return $self->_init(@_);
}

=head2 _init

Private Test Case constructor. This does the actual work of building 
a test case object which is passed to new to be blessed

=cut

sub _init {
    my $self = shift;
    my ($param) = (@_);
    my $dbh = Bugzilla->dbh;

    my $id = $param unless (ref $param eq 'HASH');
    my $obj;

    if (defined $id && detaint_natural($id)) {

        $obj = $dbh->selectrow_hashref(qq{
            SELECT $columns FROM test_cases
            WHERE case_id = ?}, undef, $id);
    } elsif (ref $param eq 'HASH'){
         $obj = $param;   
    } else {
        ThrowCodeError('bad_arg',
            {argument => 'param',
             function => 'Testopia::TestCase::_init'});
    }

    return undef unless (defined $obj);

    foreach my $field (keys %$obj) {
        $self->{$field} = $obj->{$field};
    }
    return $self;
}

=head2 get_selectable_components

Returns a reference to a list of selectable components not already
associated with this case. 

=cut

#TODO: This can be done with a single query
sub get_selectable_components {
    my $self = shift;
    my ($byid) = @_;
    my $dbh = Bugzilla->dbh;
    my @exclusions;
    unless ($byid) {
        foreach my $e (@{$self->components}){
            push @exclusions, $e->{'id'};
        }
    }
    my $query = "SELECT DISTINCT id FROM components
                 WHERE product_id IN (" . join(",", @{$self->get_product_ids}) . ") ";
    if (@exclusions){
        $query .= "AND id NOT IN(". join(",", @exclusions) .") ";
    }
    $query .= "ORDER BY name";
    
    my $ref = $dbh->selectcol_arrayref($query);
    my @comps;
    push @comps, {'id' => '0', 'name' => '--Please Select--'} unless $byid;
    foreach my $id (@$ref){
        push @comps, Bugzilla::Component->new($id);
    }
    return \@comps;
}

=head2 get_product_components

Returns a list of components divided by product

=cut

sub get_product_components {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my @exclusions;
    my $products = $dbh->selectall_arrayref(
            "SELECT id,name FROM products ORDER BY name",{'Slice' => {}});
    my %prods;
    foreach my $p (@$products){
        my $comps = $dbh->selectall_arrayref(
            "SELECT id,name FROM components 
             WHERE product_id = ?
             ORDER BY name",
             {'Slice' => {}},$p->{'id'});
        
        $prods{$p->{'name'}} = $comps;
    }
    return \%prods;
}
 
=head2 get_available_components

Returns a list of all user visible components for use in searches

=cut

#TODO: This can be done with a single query
sub get_available_components{
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    #TODO: 2.22 use Product.pm
    my $products = $dbh->selectcol_arrayref(
            "SELECT id FROM products");
    my @selectable;
    foreach my $p (@{$products}){
        if (Bugzilla::Testopia::Util::can_view_product($p)){
            my $ref = $dbh->selectcol_arrayref(
                "SELECT name
                 FROM components WHERE product_id=?", 
                 undef, $p);
            push @selectable, @{$ref};
        }
    }
    # weed out duplicate names
    my %comps;
    foreach my $c (@selectable){
        $comps{$c} = 1;
    }
    @selectable = ();
    foreach my $k (sort(keys %comps)){
        push @selectable, {'id' => $k, 'name' => $k};
    }
    return \@selectable;             
}

=head2 get_category_list

Returns a list of categories associated with products in all
plans referenced by this case.

=cut

#TODO: Move to Testopia::Product.pm in 2.22
sub get_category_list{
    my $self = shift;
    my ($by_name) = @_;
    my $dbh = Bugzilla->dbh;
    
    my @product_ids;
    foreach my $p (@{$self->plans}){
        push @product_ids, $p->product_id;
    }
    my $id_part;
    my $name_part;
    if ($by_name){
        $id_part = "cat.name AS id";
        $name_part = ", cat.name as name";
    }
    else {
        $id_part = "category_id";
    }
    my $query =  "SELECT DISTINCT $id_part $name_part ";
       $query .= "FROM test_case_categories AS cat " .
             "JOIN products ON cat.product_id = products.id " .
             "LEFT JOIN group_control_map " .
              "ON group_control_map.product_id = products.id ";
    if (Param('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 ";
    $query .= "AND cat.product_id IN (". join(",", @product_ids) . ") " unless ($by_name || !@product_ids);  
    $query .= "ORDER BY cat.name";

    if ($by_name){
        my $ref = $dbh->selectall_arrayref($query, {'Slice'=>{}});
        return $ref;
    }
    my $ids = $dbh->selectcol_arrayref($query);
    
    my @categories;
    foreach my $c (@$ids){
        push @categories, Bugzilla::Testopia::Category->new($c);
    }
    return \@categories;    
}

=head2 get_product_ids

Returns the list of product ids that this case is associated with

=cut

sub get_product_ids {
    my $self = shift;
    
    if ($self->id == 0){
        my @ids;
        foreach my $plan (@{$self->plans}){
            push @ids, $plan->product_id if Bugzilla->user->can_see_product($plan->product->name);
        }
        return \@ids;
    }
    
    my $dbh = Bugzilla->dbh;
    
    my $ref = $dbh->selectcol_arrayref(
            "SELECT DISTINCT products.id FROM products
               JOIN test_plans AS plans ON plans.product_id = products.id
               JOIN test_case_plans ON plans.plan_id = test_case_plans.plan_id
               JOIN test_cases AS cases ON cases.case_id = test_case_plans.case_id  
              WHERE cases.case_id = ?
              ORDER BY products.id", undef, $self->{'case_id'});
    return $ref;
}

=head2 get_status_list

Returns the list of legal statuses for a test case

=cut

sub get_status_list {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $ref = $dbh->selectall_arrayref("
        SELECT case_status_id AS id, name 
        FROM test_case_status", {"Slice"=>{}});
    return $ref
}

=head2 get_priority_list

Returns a list of legal priorities

=cut

sub get_priority_list {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $ref = $dbh->selectall_arrayref("
        SELECT id, value AS name FROM priority
        ORDER BY sortkey", {"Slice"=>{}});
    return $ref
}

=head2 get_caserun_count

Takes a status and returns the count of that status

=cut

sub get_caserun_count {
    my $self = shift;
    my ($status) = @_;
    my $dbh = Bugzilla->dbh;
    
    my $query = "SELECT COUNT(*) 
                   FROM test_case_runs 
                  WHERE case_id = ? ";
       $query .= "AND case_run_status_id = ?" if $status;
       
    my $count;
    if ($status){
        ($count) = $dbh->selectrow_array($query,undef,($self->id,$status));
    }
    else {
        ($count) = $dbh->selectrow_array($query,undef,$self->id);
    }
    return $count;
}

=head2 add_tag

Associates a tag with this test case

=cut

sub add_tag {
    my $self = shift;
    my ($tag_id) = @_;
    my $dbh = Bugzilla->dbh;

    $dbh->bz_lock_tables('test_case_tags WRITE');
    my $tagged = $dbh->selectrow_array(
             "SELECT 1 FROM test_case_tags 
              WHERE tag_id = ? AND case_id = ?",
              undef, $tag_id, $self->{'case_id'});
    if ($tagged) {
        $dbh->bz_unlock_tables();
        return 1;
    }
    $dbh->do("INSERT INTO test_case_tags(tag_id, case_id, userid) VALUES(?,?,?)",
              undef, $tag_id, $self->{'case_id'}, Bugzilla->user->id);
    $dbh->bz_unlock_tables();

    return 0;
}

=head2 remove_tag

Disassociates a tag from this test case

=cut

sub remove_tag {    
    my $self = shift;
    my ($tag_id) = @_;
    my $dbh = Bugzilla->dbh;
    $dbh->do("DELETE FROM test_case_tags 
              WHERE tag_id=? AND case_id=?",
              undef, $tag_id, $self->{'case_id'});
    return;
}

=head2 attach_bug

Attaches the specified bug to this test case

=cut

sub attach_bug {
    my $self = shift;
    my ($bug, $case_id) = @_;
    $case_id ||= $self->{'case_id'};
    my $dbh = Bugzilla->dbh;

    $dbh->bz_lock_tables('test_case_bugs WRITE');
    my ($exists) = $dbh->selectrow_array(
            "SELECT bug_id 
               FROM test_case_bugs 
              WHERE case_id=?
                AND bug_id=?", 
             undef, ($case_id, $bug));
    if ($exists) {
        $dbh->bz_unlock_tables();
        return;
    }
    $dbh->do("INSERT INTO test_case_bugs (bug_id, case_run_id, case_id)
              VALUES(?,?,?)", undef, ($bug, undef, $self->{'case_id'}));
    $dbh->bz_unlock_tables();
}

=head2 detach_bug

Removes the association of the specified bug from this test case-run

=cut

sub detach_bug {
    my $self = shift;
    my ($bug) = @_;
    my $dbh = Bugzilla->dbh;

    $dbh->do("DELETE FROM test_case_bugs 
               WHERE bug_id = ? 
                 AND case_id = ?", 
             undef, ($bug, $self->{'case_id'}));
}

=head2 add_component

Associates a component with this test case

=cut

sub add_component {
    my $self = shift;
    my ($comp_id) = @_;
    my $dbh = Bugzilla->dbh;
    #TODO: Check for existing component
    $dbh->do("INSERT INTO test_case_components (case_id, component_id)
              VALUES (?,?)",undef,  $self->{'case_id'}, $comp_id);
    delete $self->{'components'};          
}

=head2 remove_component

Disassociates a component with this test case

=cut

sub remove_component {
    my $self = shift;
    my ($comp_id) = @_;
    my $dbh = Bugzilla->dbh;
    $dbh->do("DELETE FROM test_case_components
              WHERE case_id = ? AND component_id = ?",
              undef,  $self->{'case_id'}, $comp_id);    
}

=head2 compare_doc_versions

Returns a unified diff of two versions of a case document (action 
and effect). It takes two arguments both integers representing
the first and second versions to compare.

=cut

sub compare_doc_versions {
    my $self = shift;
    my ($newversion, $oldversion) = @_;
    detaint_natural($newversion);
    detaint_natural($oldversion);
    my $dbh = Bugzilla->dbh;
    my %diff;
    my ($newaction, $neweffect, $newsetup, $newbreakdown) = $dbh->selectrow_array(
            "SELECT action, effect, setup, breakdown FROM test_case_texts
             WHERE case_id = ? AND case_text_version = ?",
             undef, $self->{'case_id'}, $newversion);
    
    my ($oldaction, $oldeffect, $oldsetup, $oldbreakdown) = $dbh->selectrow_array(
            "SELECT action, effect, setup, breakdown FROM test_case_texts
             WHERE case_id = ? AND case_text_version = ?",
             undef, $self->{'case_id'}, $oldversion);
    $diff{'action'} = diff(\$newaction, \$oldaction);
    $diff{'effect'} = diff(\$neweffect, \$oldeffect);
    $diff{'setup'} = diff(\$newsetup, \$oldsetup);
    $diff{'breakdown'} = diff(\$newbreakdown, \$oldbreakdown);
    return \%diff;
}

=head2 diff_case_doc

Returns the diff of the latest case document (action and effect) 
with some text passed as an argument. Used to determine if the 
text has changed and thus requiring a new version be created.

=cut

sub diff_case_doc {
    my $self = shift;
    my ($newaction, $neweffect, $newsetup, $newbreakdown) = @_;
    my $dbh = Bugzilla->dbh;
    my ($oldaction, $oldeffect, $oldsetup, $oldbreakdown) = $dbh->selectrow_array(
            "SELECT action, effect, setup, breakdown
               FROM test_case_texts
              WHERE case_id = ? AND case_text_version = ?",
             undef, ($self->{'case_id'}, $self->version));
    my $diff = diff(\$newaction, \$oldaction);
    $diff .= diff(\$neweffect, \$oldeffect);
    $diff .= diff(\$newsetup, \$oldsetup);
    $diff .= diff(\$newbreakdown, \$oldbreakdown);
    return $diff
}

=head2 get_fields

Returns a reference to a list of test case 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_cases");
    return $types;
}

=head2 store

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

=cut

sub store {
    my $self = shift;
    my $dbh = Bugzilla->dbh;    
    my ($timestamp) = Bugzilla::Testopia::Util::get_time_stamp();
    $dbh->do("INSERT INTO test_cases ($columns)
              VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
              undef, (undef, $self->{'case_status_id'}, $self->{'category_id'}, 
              $self->{'priority_id'}, $self->{'author_id'}, $self->{'default_tester_id'},
              $timestamp, $self->{'isautomated'}, $self->sortkey, $self->{'script'},
              $self->{'arguments'}, $self->{'summary'}, $self->{'requirement'},
              $self->{'alias'}));

    my $key = $dbh->bz_last_key( 'test_cases', 'case_id' );

    $self->store_text($key, $self->{'author_id'}, $self->{'action'}, $self->{'effect'},  
                      $self->{'setup'}, $self->{'breakdown'},0 ,$timestamp);
    $self->update_deps($self->{'dependson'}, $self->{'blocks'}, $key);    
    foreach my $p (@{$self->{'plans'}}){
        $self->link_plan($p->id, $key);
    }
    return $key;
}

=head2 store_text

Stores the test case document (action and effect) in the test_case_texts 
table. Used by both store and copy. Accepts the the test case id,
author id, action text, effect text, and an optional timestamp. 

=cut

sub store_text {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my ($key, $author, $action, $effect, $setup, $breakdown, $reset_version, $timestamp) = @_;
    if (!defined $timestamp){
        ($timestamp) = Bugzilla::Testopia::Util::get_time_stamp();
    }
    my $version = $reset_version ? 0 : $self->version || 0;
    $dbh->do("INSERT INTO test_case_texts 
              (case_id, case_text_version, who, creation_ts, action, effect, setup, breakdown) 
              VALUES(?,?,?,?,?,?,?,?)",
              undef, $key, ++$version, $author, 
              $timestamp, $action, $effect, $setup, $breakdown);
    $self->{'version'} = $version;
    return $self->{'version'};

}

=head2 link_plan

Creates a link to the specified plan id. Optionally can create a link
for an arbitrary test case, not just this one.

=cut

sub link_plan {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my ($plan_id, $case_id) = @_;
    $case_id = $self->{'case_id'} unless defined $case_id;
    
    #Check that it isn't linked already

    $dbh->bz_lock_tables('test_case_plans WRITE');
    my ($is) = $dbh->selectrow_array(
            "SELECT 1 
               FROM test_case_plans
              WHERE case_id = ?
                AND plan_id = ?",
               undef, ($case_id, $plan_id));
    if ($is) {
        $dbh->bz_unlock_tables();
        return;
    }
    $dbh->do("INSERT INTO test_case_plans (plan_id, case_id) 
              VALUES (?,?)", undef, $plan_id, $case_id);
    $dbh->bz_unlock_tables();
    
    # Update the plans array to include new plan added.
        
   	push @{$self->{'plans'}}, Bugzilla::Testopia::TestPlan->new($plan_id);

}


=head2 unlink_plan

Removes the link to the specified plan id. 

=cut

sub unlink_plan {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my ($plan_id) = @_;
    
    $dbh->bz_lock_tables('test_case_plans WRITE', 'test_case_runs WRITE', 
                         'test_runs READ', 'test_plans READ');
    
    if (!$self->can_unlink_plan($plan_id)){
        $dbh->bz_unlock_tables();
        return 0;
    }
    
    $dbh->do("DELETE FROM test_case_plans 
               WHERE plan_id = ? 
                 AND case_id = ?", undef, $plan_id, $self->{'case_id'});
    
    $dbh->bz_unlock_tables();   

    # Update the plans array.
    delete $self->{'plans'}; 

  	return 1;    
}

=head2 copy

Creates a copy of this test case. Accepts the plan id to link to and 
a boolean representing whether to copy the case document as well.

=cut

sub copy {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my ($planid, $author, $copydoc) = @_;
    my ($timestamp) = Bugzilla::Testopia::Util::get_time_stamp();
    $dbh->do("INSERT INTO test_cases ($columns)
              VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
              undef, (undef, $self->{'case_status_id'}, $self->{'category_id'}, 
              $self->{'priority_id'}, $author, $self->{'default_tester_id'}, 
              $timestamp, $self->{'isautomated'}, $self->sortkey, $self->{'script'},
              $self->{'arguments'}, $self->{'summary'}, $self->{'requirement'},
              undef));

    my $key = $dbh->bz_last_key( 'test_cases', 'case_id' );
    
    if ($copydoc){
        $self->store_text($key, Bugzilla->user->id, $self->text->{'action'}, 
                          $self->text->{'effect'}, $self->text->{'setup'}, 
                          $self->text->{'breakdown'},'VRESET' , $timestamp);
    }
    return $key;
    
}

=head2 check_alias

Checks if the given alias exists already. Returns the case_id of
the matching case if it does.

=cut

sub check_alias {
    my $self = shift;
    my ($alias) = @_;
    
    return unless $alias;
    my $id = $self->{'case_id'} || '';
    my $dbh = Bugzilla->dbh;
    ($id) = $dbh->selectrow_array(
            "SELECT case_id 
               FROM test_cases 
              WHERE alias = ?
                AND case_id != ?",
              undef, ($alias, $id));

    return $id;
}

=head2 class_check_alias

Checks if the given alias exists already. Returns the case_id of
the matching test case if it does.

=cut

sub class_check_alias {
    my ($alias) = @_;
    
    return unless $alias;
    
    my $dbh = Bugzilla->dbh;
    my ($id) = $dbh->selectrow_array(
            "SELECT case_id 
               FROM test_cases 
              WHERE alias = ?",
              undef, ($alias));

    return $id;
}

=head2 update

Updates this test case with new values supplied by the user.
Accepts a reference to a hash with keys identical to a test cases
fields and values representing the new values entered.
Validation tests should be performed on the values 
before calling this method. If a field is changed, a history 
of that change is logged in the test_case_activity table.

=cut

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

    $dbh->bz_lock_tables('test_cases WRITE', 'test_case_activity WRITE',
        'test_fielddefs READ');
    foreach my $field (keys %{$newvalues}){
        if ($self->{$field} ne $newvalues->{$field}){
            trick_taint($newvalues->{$field});
            $dbh->do("UPDATE test_cases 
                      SET $field = ? WHERE case_id = ?",
                      undef, $newvalues->{$field}, $self->{'case_id'});
            # Update the history
            my $field_id = Bugzilla::Testopia::Util::get_field_id($field, "test_cases");
            $dbh->do("INSERT INTO test_case_activity 
                      (case_id, fieldid, who, changed, oldvalue, newvalue)
                      VALUES(?,?,?,?,?,?)",
                      undef, $self->{'case_id'}, $field_id, Bugzilla->user->id,
                      $timestamp, $self->{$field}, $newvalues->{$field});
            $self->{$field} = $newvalues->{$field};
        }
    }
    $dbh->bz_unlock_tables();
}

=head2 history

Returns a reference to a list of history entries from the 
test_case_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_case_activity AS a
               JOIN test_fielddefs AS defs ON a.fieldid = defs.fieldid
               JOIN profiles AS p ON a.who = p.userid
              WHERE a.case_id = ?",
              {'Slice'=>{}}, $self->{'case_id'});

    foreach my $row (@$ref){
        if ($row->{'what'} eq 'Case Status'){
            $row->{'oldvalue'} = $self->lookup_status($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_status($row->{'newvalue'});
        }
        elsif ($row->{'what'} eq 'Category'){
            $row->{'oldvalue'} = $self->lookup_category($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_category($row->{'newvalue'});
        }
        elsif ($row->{'what'} eq 'Priority'){
            $row->{'oldvalue'} = $self->lookup_priority($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_priority($row->{'newvalue'});
        }
        elsif ($row->{'what'} eq 'Default Tester'){
            $row->{'oldvalue'} = $self->lookup_default_tester($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_default_tester($row->{'newvalue'});
        }
    }        
    return $ref;
}

sub last_changed {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    
    my ($date) = $dbh->selectrow_array(
            "SELECT MAX(changed)
               FROM test_case_activity 
              WHERE case_id = ?",
              undef, $self->id);
    
    return $self->{'creation_date'} unless $date;
    return $date;
    
}

=head2 lookup_status

Takes an ID of the status field and returns the value

=cut

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

=head2 lookup_status_by_name

Returns the id of the status name passed.

=cut

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

=head2 lookup_category

Takes an ID of the category field and returns the value

=cut

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

=head2 lookup_category_by_name

Returns the id of the category name passed.

=cut

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

=head2 lookup_priority

Takes an ID of the priority field and returns the value

=cut

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

=head2 lookup_priority_by_name

Returns the id of the priority name passed.

=cut

sub lookup_priority_by_value {
    my ($value) = @_;
    my $dbh = Bugzilla->dbh;
    my ($id) = $dbh->selectrow_array(
            "SELECT id 
               FROM priority
              WHERE value = ?",
              undef, $value);
    return $id;
}


=head2 lookup_default_tester

Takes an ID of the default_tester field and returns the value

=cut

sub lookup_default_tester {
    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;
}

# From process bug
sub _snap_shot_deps {
    my ($i, $target, $me) = (@_);
    my $dbh = Bugzilla->dbh;
    my $ref = $dbh->selectcol_arrayref(
        "SELECT $target 
         FROM test_case_dependencies 
         WHERE $me = ? ORDER BY $target", undef, $i);
    
    return join(',', @{$ref});
}

# Taken from Bugzilla::Bug
sub _get_dep_lists {
    my ($myfield, $targetfield, $case_id) = (@_);
    my $dbh = Bugzilla->dbh;
    my $list_ref =
        $dbh->selectcol_arrayref(
          "SELECT test_case_dependencies.$targetfield
             FROM test_case_dependencies
             JOIN test_cases ON test_cases.case_id = test_case_dependencies.$targetfield
            WHERE test_case_dependencies.$myfield = ?
         ORDER BY test_case_dependencies.$targetfield",
         undef, ($case_id));
    return $list_ref;
}

sub update_deps {
    my $self = shift;
    my ($dependson, $blocks, $case_id) = @_;
    $case_id = $self->{'case_id'} unless $case_id;
    my $dbh = Bugzilla->dbh;
    my $fields = {};
    $fields->{'dependson'} = $dependson;
    $fields->{'blocked'} = $blocks;
# From process bug
    foreach my $field ("dependson", "blocked") {
        if ($fields->{$field}) {
            my @validvalues;
            foreach my $id (split(/[\s,]+/, $fields->{$field})) {
                next unless $id;
                Bugzilla::Testopia::Util::validate_test_id($id, 'case');
                push(@validvalues, $id);
            }
            $fields->{$field} = join(",", @validvalues);
        }
    }
#From Bug.pm sub ValidateDependencies($$$)
#    my $fields = {};
#    $fields->{'dependson'} = shift;
#    $fields->{'blocked'} = shift;
    my $id = $case_id || 0;

    unless (defined($fields->{'dependson'})
            || defined($fields->{'blocked'}))
    {
        return;
    }

    my %deps;
    my %deptree;
    foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) {
        my ($me, $target) = @{$pair};
        $deptree{$target} = [];
        $deps{$target} = [];
        next unless $fields->{$target};

        my %seen;
        foreach my $i (split('[\s,]+', $fields->{$target})) {
            if ($id == $i) {
                ThrowUserError("dependency_loop_single");
            }
            if (!exists $seen{$i}) {
                push(@{$deptree{$target}}, $i);
                $seen{$i} = 1;
            }
        }
        # populate $deps{$target} as first-level deps only.
        # and find remainder of dependency tree in $deptree{$target}
        @{$deps{$target}} = @{$deptree{$target}};
        my @stack = @{$deps{$target}};
        while (@stack) {
            my $i = shift @stack;
            my $dep_list =
                $dbh->selectcol_arrayref("SELECT $target
                                          FROM test_case_dependencies
                                          WHERE $me = ?", undef, $i);
            foreach my $t (@$dep_list) {
                # ignore any _current_ dependencies involving this test_case,
                # as they will be overwritten with data from the form.
                if ($t != $id && !exists $seen{$t}) {
                    push(@{$deptree{$target}}, $t);
                    push @stack, $t;
                    $seen{$t} = 1;
                }
            }
        }
    }

    my @deps   = @{$deptree{'dependson'}};
    my @blocks = @{$deptree{'blocked'}};
    my @union = ();
    my @isect = ();
    my %union = ();
    my %isect = ();
    foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ }
    @union = keys %union;
    @isect = keys %isect;
    if (scalar(@isect) > 0) {
        my $both = "";
        foreach my $i (@isect) {
           $both .= "<a href=\"tr_show_case.cgi?case_id=$i\">$i</a> " ;
        }
        ThrowUserError("dependency_loop_multi", { both => $both });
    }

#from process_bug
    foreach my $pair ("blocked/dependson", "dependson/blocked") {
        my ($me, $target) = split("/", $pair);

        my @oldlist = @{$dbh->selectcol_arrayref("SELECT $target FROM test_case_dependencies
                                                  WHERE $me = ? ORDER BY $target",
                                                  undef, $id)};
#        @dependencychanged{@oldlist} = 1;

        if (defined $fields->{$target}) {
            my %snapshot;
            my @newlist = sort {$a <=> $b} @{$deps{$target}};
#            @dependencychanged{@newlist} = 1;

            while (0 < @oldlist || 0 < @newlist) {
                if (@oldlist == 0 || (@newlist > 0 &&
                                      $oldlist[0] > $newlist[0])) {
                    $snapshot{$newlist[0]} = _snap_shot_deps($newlist[0], $me,
                                                          $target);
                    shift @newlist;
                } elsif (@newlist == 0 || (@oldlist > 0 &&
                                           $newlist[0] > $oldlist[0])) {
                    $snapshot{$oldlist[0]} = _snap_shot_deps($oldlist[0], $me,
                                                          $target);
                    shift @oldlist;
                } else {
                    if ($oldlist[0] != $newlist[0]) {
                        die "Error in list comparing code";
                    }
                    shift @oldlist;
                    shift @newlist;
                }
            }
            my @keys = keys(%snapshot);
            if (@keys) {
                my $oldsnap = _snap_shot_deps($id, $target, $me);
                $dbh->do("DELETE FROM test_case_dependencies WHERE $me = ?", undef, $id);
                foreach my $i (@{$deps{$target}}) {
                    $dbh->do("INSERT INTO test_case_dependencies ($me, $target) VALUES (?,?)", undef, $id, $i);
                }
#                foreach my $k (@keys) {
#                    LogDependencyActivity($k, $snapshot{$k}, $me, $target, $timestamp);
#                }
#                LogDependencyActivity($id, $oldsnap, $target, $me, $timestamp);
#                $check_dep_bugs = 1;
            }
        }
    }  
}

=head2 get_dep_tree

Returns a list of test case dependencies 

=cut

sub get_dep_tree {
    my $self = shift;
    $self->{'dep_list'} = ();
    $self->_generate_dep_tree($self->{'case_id'});
    return $self->{'dep_list'};
}

=head2 _generate_dep_tree

Private method that recursivly gets a list of the test cases this blocks

=cut

sub _generate_dep_tree {
    my $self = shift;
    my ($case_id) = @_;
    my $deps = _get_dep_lists("dependson", "blocked", $case_id);
    return unless scalar @$deps;
    foreach my $id (@$deps){
        $self->_generate_dep_tree($id);
        push @{$self->{'dep_list'}}, $id
    }
}

=head2 can_unlink_plan

Returns true if this test case can be unlinked from the given plan

=cut

sub can_unlink_plan {
    my $self = shift;
    my ($plan_id) = @_;
    
    return 0 if (!UserInGroup('managetestplans'));
    
    return 0 if scalar @{$self->plans} < 2;
    
    my $dbh = Bugzilla->dbh;
    my ($res) = $dbh->selectrow_array(
            "SELECT 1 
               FROM test_case_runs
         INNER JOIN test_runs ON test_case_runs.run_id = test_runs.run_id
              WHERE test_runs.plan_id = ? 
                AND test_case_runs.case_id = ?",
                undef, ($plan_id, $self->{'case_id'}));
    
    return !$res;
}

=head2 obliterate

Removes this case and all things that reference it.

=cut

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

    foreach my $obj (@{$self->attachments}){
        $obj->obliterate;
    }
    foreach my $obj (@{$self->caseruns}){
        $obj->obliterate;
    }

    $dbh->do("DELETE FROM test_case_texts WHERE case_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_case_plans WHERE case_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_case_components WHERE case_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_case_tags WHERE case_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_case_bugs WHERE case_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_case_activity WHERE case_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_case_dependencies
               WHERE dependson = ? OR blocked = ?", undef, ($self->id, $self->id));
    $dbh->do("DELETE FROM test_cases WHERE case_id = ?", undef, $self->id);
    return 1;
}

=head2 canedit

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

=cut

sub canedit {
    my $self = shift;
    return $self->canview 
      && UserInGroup('managetestplans') 
        || UserInGroup('edittestcases');
}

=head2 canview

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

=cut

sub canview {
    my $self = shift;
    return $self->{'canview'} if exists $self->{'canview'};
    my $ret = 1;
    foreach my $p (@{$self->plans}){
        $ret = 0 unless Bugzilla::Testopia::Util::can_view_product($p->product_id);
    }
    $self->{'canview'} = $ret;
    return $self->{'canview'};
}

=head2 candelete

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

=cut

sub candelete {
    my $self = shift;
    return 0 unless $self->canedit && Param("allow-test-deletion");
    return 1 if Bugzilla->user->in_group("admin");

    # Allow plan author to delete if this case is linked only to plans she owns.
    my $own_all = 1;
    foreach my $plan (@{$self->plans}){
        if (Bugzilla->user->id != $plan->author->id) {
            $own_all = 0;
            last;
        }
    }
    return 1 if $own_all;
    
    # Allow case author to delete if this case is not in any runs.
    return 1 if Bugzilla->user->id == $self->author->id &&
        $self->get_caserun_count == 0;

    return 0;
}

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

=head1 ACCESSOR METHODS

=head2 id

Returns the ID of this object

=head2 author

Returns a Bugzilla::User object representing the Author of this case

=head2 default_tester

Returns a Bugzilla::User object representing the run's default tester

=head2 creation_date

Returns the creation time stamp of this object

=head2 isautomated

Returns true if this is an automatic test case

=head2 script

Returns the script of this object

=head2 status_id

Returns the status_id of this object

=head2 arguments

Returns the arguments for the script of this object

=head2 summary

Returns the summary of this object

=head2 requirements

Returns the requirements of this object

=head2 alias

Returns the alias of this object

=cut

sub id              { return $_[0]->{'case_id'};       }
sub author          { return Bugzilla::User->new($_[0]->{'author_id'});  }
sub default_tester  { return Bugzilla::User->new($_[0]->{'default_tester_id'});  }
sub creation_date   { return $_[0]->{'creation_date'}; }
sub isautomated     { return $_[0]->{'isautomated'};   }
sub script          { return $_[0]->{'script'};        }
sub status_id       { return $_[0]->{'case_status_id'};}
sub arguments       { return $_[0]->{'arguments'};     }
sub summary         { return $_[0]->{'summary'};       }
sub requirement     { return $_[0]->{'requirement'};   }
sub alias           { return $_[0]->{'alias'};         }

=head2 type

Returns 'case'

=cut

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

=head2 attachments

Returns a reference to a list of attachments associated with this
case.

=cut

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

    my $attachments = $dbh->selectcol_arrayref(
        "SELECT attachment_id
         FROM test_attachments
         WHERE case_id = ?", 
         undef, $self->{'case_id'});
    
    my @attachments;
    foreach my $a (@{$attachments}){
        push @attachments, Bugzilla::Testopia::Attachment->new($a);
    }
    $self->{'attachments'} = \@attachments;
    return $self->{'attachments'};
    
}

=head2 version

Returns the case text version. This number is incremented any time
changes are made to the case docs (action and effect).

=cut

sub version { 
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'version'} if exists $self->{'version'};
    my ($ver) = $dbh->selectrow_array("SELECT MAX(case_text_version)
                                       FROM test_case_texts
                                       WHERE case_id = ?", 
                                       undef, $self->{'case_id'});
    $self->{'version'} = $ver;
    return $self->{'version'};
}

=head2 status

Looks up the case status based on the case_status_id of this case.

=cut

sub status { 
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'status'} if exists $self->{'status'};
    my ($res) = $dbh->selectrow_array("SELECT name
                                       FROM test_case_status
                                       WHERE case_status_id = ?", 
                                       undef, $self->{'case_status_id'});
    $self->{'status'} = $res;
    return $self->{'status'};
}

=head2 priority

Looks up the Bugzilla priority value based on the priority_id 
of this case.

=cut

sub priority { 
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'priority'} if exists $self->{'priority'};
    my ($res) = $dbh->selectrow_array("SELECT value
                                       FROM priority
                                       WHERE id = ?", 
                                       undef, $self->{'priority_id'});
    $self->{'priority'} = $res;
    return $self->{'priority'};
}

=head2 category

Returns the category name based on the category_id of this case

=cut

sub category {
    my $self = shift;
    return $self->{'category'} if exists $self->{'category'};
    $self->{'category'} = Bugzilla::Testopia::Category->new($self->{'category_id'});
    return $self->{'category'};
}

=head2 components

Returns a reference to a list of bugzilla components assoicated with 
this test case.

=cut

sub components {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'components'} if exists $self->{'components'};
    #TODO: 2.22 use Bugzilla::Component
    my $comps = $dbh->selectcol_arrayref(
        "SELECT comp.id 
           FROM components AS comp
           JOIN test_case_components AS tcc ON tcc.component_id = comp.id
           JOIN test_cases ON tcc.case_id = test_cases.case_id
          WHERE test_cases.case_id = ?", 
         {'Slice' => {}},  $self->{'case_id'}); 
    
    my @comps;
    foreach my $id (@$comps){
		my $comp = Bugzilla::Component->new($id);
		my $prod = Bugzilla::Product->new($comp->product_id);
		$comp->{'product_name'} = $prod->name;
        push @comps, $comp;
    }
    $self->{'components'} = \@comps;
    return $self->{'components'};
}

=head2 tags

Returns a reference to a list of Bugzilla::Testopia::TestTag objects 
associated with this case.

=cut

sub tags {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'tags'} if exists $self->{'tags'};
    my $tagids = $dbh->selectcol_arrayref("SELECT tag_id 
                                          FROM test_case_tags
                                          WHERE case_id = ?", 
                                          undef, $self->{'case_id'});
    my @tags;
    foreach my $id (@{$tagids}){
        push @tags, Bugzilla::Testopia::TestTag->new($id);
    }
    $self->{'tags'} = \@tags;
    return $self->{'tags'};
}

=head2 plans

Returns a reference to a list of Bugzilla::Testopia::TestPlan objects 
associated with this case.

=cut

sub plans {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'plans'} if exists $self->{'plans'};
    my $ref = $dbh->selectcol_arrayref("SELECT plan_id
                                       FROM test_case_plans
                                       WHERE case_id = ?", 
                                       undef, $self->{'case_id'});
    my @plans;
    foreach my $id (@{$ref}){
        push @plans, Bugzilla::Testopia::TestPlan->new($id);
    }
    $self->{'plans'} = \@plans;
    return $self->{'plans'};
}

=head2 bugs

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

=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 
          WHERE case_id = ?", 
         undef, $self->{'case_id'});
    my @bugs;
    foreach my $id (@{$ref}){
        push @bugs, Bugzilla::Bug->new($id, Bugzilla->user->id) if Bugzilla->user->can_see_bug($id);
    }
    $self->{'bugs'} = \@bugs;
    return $self->{'bugs'};
}

=head2 bug_list

Returns a comma separated list of bug ids associated with this case

=cut

sub bug_list {
    my $self = shift;
    return $self->{'bug_list'} if exists $self->{'bug_list'};
    my $dbh = Bugzilla->dbh;
    my @bugs;
    my $bugids = $dbh->selectcol_arrayref("SELECT bug_id 
                                     FROM test_case_bugs 
                                     WHERE case_id=?", 
                                     undef, $self->id);
    my @visible;
    foreach my $bugid (@{$bugids}){
        push @visible, $bugid if Bugzilla->user->can_see_bug($bugid);
    }
    $self->{'bug_list'} = join(",", @$bugids);
    
    return $self->{'bug_list'};
}

=head2 text

Returns a hash reference representing the action and effect of this
case.

=cut


sub text {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'text'} if exists $self->{'text'};
    my $text = $dbh->selectrow_hashref(
        "SELECT action, effect, setup, breakdown, who author_id, case_text_version AS version
           FROM test_case_texts
          WHERE case_id=? AND case_text_version=?",
        undef, $self->{'case_id'}, 
        $self->version);
    $self->{'text'} = $text;
    return $self->{'text'};
}

=head2 runs

Returns a reference to a list of Bugzilla::Testopia::TestRun objects 
associated with this case.

=cut

sub runs {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'runs'} if exists $self->{'runs'};
    my $ref = $dbh->selectcol_arrayref(
      "SELECT DISTINCT t.run_id
       FROM test_runs t
       INNER JOIN  test_case_runs r ON r.run_id = t.run_id
       WHERE case_id = ?", 
       undef, $self->{'case_id'});
    my @runs;
    foreach my $id (@{$ref}){
        push @runs, Bugzilla::Testopia::TestRun->new($id);
    }
    $self->{'runs'} = \@runs;
    return $self->{'runs'};
}

=head2 caseruns

Returns a reference to a list of Bugzilla::Testopia::TestCaseRun objects 
associated with this case.

=cut

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

sub sortkey {
    my $self = shift;
    return $self->{'sortkey'} if exists $self->{'sortkey'};
    my $dbh = Bugzilla->dbh;
    my ($sortkey) = $dbh->selectrow_array("SELECT MAX(sortkey) FROM test_cases");
    $self->{'sortkey'} = ++$sortkey;
    return $self->{'sortkey'};
}

=head2 blocked

Returns a reference to a list of Bugzilla::Testopia::TestCase objects 
which are blocked by this test case.

=cut

sub blocked {
    my ($self) = @_;
    return $self->{'blocked'} if exists $self->{'blocked'};
    my @deps;
    my $ref = _get_dep_lists("dependson", "blocked", $self->{'case_id'});
    foreach my $id (@{$ref}){
        push @deps, Bugzilla::Testopia::TestCase->new($id);
    }
    $self->{'blocked'} = \@deps;
    return $self->{'blocked'};
}

sub blocked_list {
    my ($self) = @_;
    return $self->{'blocked_list'} if exists $self->{'blocked_list'};
    my @deps;
    my $ref = _get_dep_lists("dependson", "blocked", $self->{'case_id'});
    $self->{'blocked_list'} = join(",", @$ref);
    return $self->{'blocked_list'};
}

=head2 blocked_list_uncached

Returns a space separated list of test cases that are blocked by this test case.
This method does not cache the blocked test cases so each call will result
in a database read.

=cut

sub blocked_list_uncached {
    my ($self) = @_;
    my $ref = _get_dep_lists("dependson", "blocked", $self->{'case_id'});
    return join(" ", @$ref)
}

=head2 dependson

Returns a reference to a list of Bugzilla::Testopia::TestCase objects 
which depend on this test case.

=cut

sub dependson {
    my ($self) = @_;
    return $self->{'dependson'} if exists $self->{'dependson'};
    my @deps;
    my $ref = _get_dep_lists("blocked", "dependson", $self->{'case_id'});
    foreach my $id (@{$ref}){
        push @deps, Bugzilla::Testopia::TestCase->new($id);
    }
    $self->{'dependson'} = \@deps;
    return $self->{'dependson'};
}    

sub dependson_list {
    my ($self) = @_;
    return $self->{'dependson_list'} if exists $self->{'dependson_list'};
    my @deps;
    my $ref = _get_dep_lists("blocked", "dependson", $self->{'case_id'});
    $self->{'dependson_list'} = join(",", @$ref);
    return $self->{'dependson_list'};
}    

=head2 dependson_list_uncached

Returns a space separated list of test cases that depend on this test case.
This method does not cache the dependent test cases so each call will result
in a database read.

=cut

sub dependson_list_uncached {
    my ($self) = @_;
    my $ref = _get_dep_lists("blocked", "dependson", $self->{'case_id'});
    return join(" ", @$ref)
}


=head1 TODO

Move components to use Bugzilla::Components with 2.22

=head1 SEE ALSO

TestPlan, TestRun, TestCaseRun

=head1 AUTHOR

Greg Hendricks <ghendricks@novell.com>

=cut

1;
