# -*- 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>

package Bugzilla::Testopia::TestPlan;

use strict;

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

use Bugzilla::Testopia::Constants;
use Bugzilla::Testopia::Util;
use Bugzilla::Testopia::TestTag;
use Bugzilla::Testopia::Product;
use Bugzilla::Testopia::Attachment;

use Text::Diff;
use JSON;

use base qw(Exporter Bugzilla::Object);
@Bugzilla::Testopia::TestCase::EXPORT = qw(lookup_type_by_name);

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

=head1 FIELDS

    plan_id
    product_id
    author_id
    type_id
    default_product_version
    name
    creation_date
    isactive

=cut

use constant DB_TABLE   => "test_plans";
use constant NAME_FIELD => "name";
use constant ID_FIELD   => "plan_id";
use constant DB_COLUMNS => qw(
    plan_id
    product_id
    author_id
    type_id
    default_product_version
    name
    creation_date
    isactive
);

use constant REQUIRED_CREATE_FIELDS => qw(product_id author_id type_id default_product_version name);
use constant UPDATE_COLUMNS         => qw(product_id type_id default_product_version name isactive);

use constant VALIDATORS => {
    product_id => \&_check_product,
    author_id  => \&_check_author,
    type_id    => \&_check_type,
    isactive   => \&_check_isactive,
};

use constant NAME_MAX_LENGTH => 255;

sub report_columns {
    my $self = shift;
    my %columns;
    # Changes here need to match Report.pm
    $columns{'Type'}          = "plan_type";        
    $columns{'Version'}       = "default_product_version";
    $columns{'Product'}       = "product";
    $columns{'Archived'}      = "archived";
    $columns{'Tags'}          = "tags";
    $columns{'Author'}        = "author";
    my @result;
    push @result, {'name' => $_, 'id' => $columns{$_}} foreach (sort(keys %columns));
    unshift @result, {'name' => '<none>', 'id'=> ''};
    return \@result;     
        
}

###############################
####       Validators      ####
###############################

sub _check_product {
    my ($invocant, $product_id) = @_;

    if (ref $invocant){
        ThrowUserError("plan-has-children") 
          if (($invocant->test_case_count || $invocant->test_run_count) && $invocant->product_id != $product_id);
    }
    
    $product_id = trim($product_id);
    my $product;
    if ($product_id !~ /^\d+$/ ){
        $product = Bugzilla::Product::check_product($product_id);
        $product = Bugzilla::Testopia::Product->new($product->id);
    }
    else {
        $product = Bugzilla::Testopia::Product->new($product_id);
    }

    ThrowUserError("invalid-test-id-non-existent", {'id' => $product_id, 'type' => 'product'}) unless $product;
    ThrowUserError("testopia-create-denied", {'object' => 'plan'}) unless $product->canedit;
    if (ref $invocant){
        $invocant->{'product'} = $product; 
        return $product->id;
    } 
    return $product;
}

sub _check_author {
    my ($invocant, $author) = @_;
    $author = trim($author);
    if ($author =~ /^\d+$/){
        $author = Bugzilla::User->new($author);
    }
    else {
        my $id = login_to_id($author, THROW_ERROR);
        return $id;
    }
    return $author->id;
}

sub _check_type {
    my ($invocant, $type_id) = @_;
    $type_id = trim($type_id);
    trick_taint($type_id);
    if ($type_id !~ /^\d+$/){
        $type_id = lookup_type_by_name($type_id) || $type_id;
    }
    Bugzilla::Testopia::Util::validate_selection($type_id, 'type_id', 'test_plan_types');
    return $type_id;
}

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

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

###############################
####       Mutators        ####
###############################
sub set_name        { $_[0]->set('name', $_[1]); }
sub set_type        { $_[0]->set('type_id', $_[1]); }
sub set_isactive    { $_[0]->set('isactive', $_[1]); }
sub set_product_id  { $_[0]->set('product_id', $_[1]); }
sub set_default_product_version { 
    my ($self, $value) = @_;
    $value = $self->_check_product_version($value);
    $self->set('default_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'){
        if (keys %$param){
            bless($param, $class);
            return $param;
        }
        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};
    
    $params->{default_product_version} = $class->_check_product_version($params->{default_product_version}, $product);
    $params->{product_id} = $product->id;
    
    return $params;
}

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

    $class->SUPER::check_required_create_fields($params);
    my $field_values = $class->run_create_validators($params);
    
    $field_values->{creation_date} = Bugzilla::Testopia::Util::get_time_stamp();
    $field_values->{isactive}  = 1;

    #We have to handle the plan document text a bit differently since it has its own table.
    my $plan_document = $field_values->{text};
    
    delete $field_values->{text};
    
    my $self = $class->SUPER::insert_create_data($field_values);

    $self->store_text($self->id, $field_values->{'author_id'}, $plan_document, $field_values->{creation_date});

    # Add permissions for the plan
    $self->add_tester($self->{'author_id'},15);
    if (Bugzilla->params->{'testopia-default-plan-testers-regexp'}) {
        $self->set_tester_regexp( Bugzilla->params->{"testopia-default-plan-testers-regexp"}, 3);
        $self->derive_regexp_testers(Bugzilla->params->{'testopia-default-plan-testers-regexp'});
    }
    
    # Create default category
    unless (scalar @{$self->product->categories}){
        require Bugzilla::Testopia::Category;
        my $category = Bugzilla::Testopia::Category->create(
            {'name' => '--default--',
             'description' => 'Default product category for test cases',
             'product_id' => $self->product->id });
    }
    delete $self->{'product'};
    return $self;
}

sub update {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $timestamp = Bugzilla::Testopia::Util::get_time_stamp();
    $dbh->bz_lock_tables('test_plans WRITE', 'test_plan_activity WRITE',
            'test_fielddefs READ');

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

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

=head2 store_text

Stores the test plan document in the test_plan_texts 
table. Used by both store and copy. Accepts the the test plan id,
author id, text, and a an optional timestamp. 

=cut

sub store_text {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my ($key, $author, $text, $timestamp) = @_;
    if (!defined $timestamp){
        ($timestamp) = Bugzilla::Testopia::Util::get_time_stamp();
    }
    $text ||= '';
    trick_taint($text);

    my $version = $self->version || 0;
    $dbh->do("INSERT INTO test_plan_texts 
              (plan_id, plan_text_version, who, creation_ts, plan_text)
              VALUES(?,?,?,?,?)",
              undef, $key, ++$version, $author, 
              $timestamp, $text);
    
    $self->{'version'} = $version;
}

=head2 clone

Creates a copy of this test plan. Accepts the name of the new plan
and a boolean representing whether to copy the plan document as well.

=cut

sub clone {
    my $self = shift;
    my ($name, $author, $product_id, $version, $store_doc) = @_;
    $store_doc = 1 unless defined($store_doc);
    my $dbh = Bugzilla->dbh;
    # Exclude the auto-incremented field from the column list.
    my $columns = join(", ", grep {$_ ne 'plan_id'} DB_COLUMNS);
    my ($timestamp) = Bugzilla::Testopia::Util::get_time_stamp();

    $dbh->do("INSERT INTO test_plans ($columns) VALUES (?,?,?,?,?,?,?)",
              undef, ($product_id, $author,
              $self->{'type_id'}, $version, $name,
              $timestamp, 1));
    my $key = $dbh->bz_last_key( 'test_plans', 'plan_id' );
    my $text = $store_doc ? $self->text->{'plan_text'} : ''; 
    $self->store_text($key, $self->{'author_id'}, $text, $timestamp);
    return $key;
    
}

=head2 toggle_archive

Toggles the archive bit on the plan.

=cut

sub toggle_archive {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    
    my $oldvalue = $self->isactive;
print STDERR "OLD $oldvalue";
    my $newvalue = $oldvalue == 1 ? 0 : 1;
print STDERR "NEW $newvalue";
    $self->set_isactive($newvalue);
print STDERR "UPDATED " . $self->isactive;    
    $self->update;
}

=head2 add_tag

Associates a tag with this test plan. Takes the tag_id of the tag
to link.

=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;
        }
        push @tags, split(',', $t);
    }

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

=head2 remove_tag

Removes a tag from this plan. Takes the tag_id of the tag to remove.

=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_plan_tags 
               WHERE tag_id=? AND plan_id=?",
              undef, ($tag->id, $self->{'plan_id'}));
}

sub get_used_categories {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my $ref = $dbh->selectall_arrayref(
            "SELECT DISTINCT test_case_categories.category_id AS id, name 
               FROM test_case_categories
               JOIN test_cases ON test_cases.category_id = test_case_categories.category_id
               JOIN test_case_plans ON test_cases.case_id = test_case_plans.case_id 
              WHERE plan_id = ?
           ORDER BY name",
           {'Slice'=>{}}, $self->id);
           
    return $ref;
}

=head2 get_plan_types

Returns a list of types from the test_plan_types table

=cut

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

    my $types = $dbh->selectall_arrayref(
            "SELECT type_id AS id, name 
               FROM test_plan_types
           ORDER BY name", 
            {"Slice"=>{}});
    return $types;

}

=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_plan_activity 
              WHERE plan_id = ?",
              undef, $self->id);

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

=head2 plan_type_ref

Returns a type name matching the given type id

=cut

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

    my $type = $dbh->selectrow_hashref(
            "SELECT type_id AS id, name, description 
               FROM test_plan_types
              WHERE type_id = ?", 
            undef, $type_id);
    
    return $type;
}

=head2 check_plan_type

Returns true if a type with the given name exists

=cut

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

    my $type = $dbh->selectrow_hashref(
            "SELECT 1 
               FROM test_plan_types
              WHERE name = ?", 
            undef, $name);
    
    return $type;
}

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

    my ($exists) = $dbh->selectrow_array(
            "SELECT 1 
               FROM test_plan_permissions
              WHERE userid = ? AND plan_id = ? AND grant_type = ?", 
            undef, ($userid, $self->id, GRANT_DIRECT));
    
    return $exists;
    
}

=head2 update_plan_type

Update the given type

=cut

sub update_plan_type {
    my $self = shift;
    my ($type_id, $name, $desc) = @_;
    my $dbh = Bugzilla->dbh;    

    my $type = $dbh->do(
            "UPDATE test_plan_types
                SET name = ?, description = ? 
              WHERE type_id = ?", 
            undef, ($name, $desc, $type_id));
    
}

=head2 add_plan_type

Add the given type

=cut

sub add_plan_type {
    my $self = shift;
    my ($name, $desc) = @_;
    my $dbh = Bugzilla->dbh;    

    my $type = $dbh->do(
            "INSERT INTO test_plan_types (name, description) VALUES(?, ?)",
             undef, ($name, $desc));
}

=head2 get_fields

Returns a list of fields from the fielddefs table associated with
a plan

=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_plans");
    return $types;
}

=head2 get_text_versions

Returns the list of versions of the plan document.

=cut

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

    my $versions = $dbh->selectall_arrayref(
            "SELECT plan_text_version AS id, plan_text_version AS name 
               FROM test_plan_texts
              WHERE plan_id = ?
              ORDER BY plan_text_version", 
            {'Slice' =>{}}, $self->id);
    return $versions;
}

=head2 diff_plan_doc

Returns either the diff of the latest version with a new text
or two numerical versions.

=cut

sub diff_plan_doc {
    my $self = shift;
    my ($new, $old) = @_;
    $old ||= $self->version;
    my $dbh = Bugzilla->dbh;
    my $newdoc;
    my $text = $new;
    if (detaint_natural($new)){
        # we are looking for a version 
        $newdoc = $dbh->selectrow_array(
            "SELECT plan_text FROM test_plan_texts
              WHERE plan_id = ? AND plan_text_version = ?",
            undef, ($self->{'plan_id'}, $new));
    }
    else {
        $newdoc = $text;
    }
    detaint_natural($old);
    my $olddoc = $dbh->selectrow_array(
            "SELECT plan_text FROM test_plan_texts
              WHERE plan_id = ? AND plan_text_version = ?",
            undef, ($self->{'plan_id'}, $old));
    my $diff = diff(\$newdoc, \$olddoc);
    return $diff
}

=head2 history

Returns a reference to a list of history entries from the 
test_plan_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_plan_activity AS a
               JOIN test_fielddefs AS defs ON a.fieldid = defs.fieldid
               JOIN profiles AS p ON a.who = p.userid
              WHERE a.plan_id = ?",
              {'Slice'=>{}}, $self->{'plan_id'});
    foreach my $row (@$ref){
        if ($row->{'what'} eq 'Product'){
            $row->{'oldvalue'} = $self->lookup_product($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_product($row->{'newvalue'});
        }
        elsif ($row->{'what'} eq 'Plan Type'){
            $row->{'oldvalue'} = $self->lookup_type($row->{'oldvalue'});
            $row->{'newvalue'} = $self->lookup_type($row->{'newvalue'});
        }
    }        
    return $ref;
}

sub copy_permissions {
    my $self = shift;
    my ($planid) = @_;
    my $dbh = Bugzilla->dbh;
    
    my ($regexp, $perms) = $dbh->selectrow_array(
        "SELECT user_regexp, permissions
           FROM test_plan_permissions_regexp
          WHERE plan_id = ?",undef, $self->id);

    $dbh->do("INSERT INTO test_plan_permissions_regexp (plan_id, user_regexp, permissions)
              VALUES(?,?,?)", undef,($planid, $regexp, $perms)) if $regexp;
              
    my $ref = $dbh->selectall_arrayref(
        "SELECT userid, permissions 
           FROM test_plan_permissions
          WHERE plan_id = ? AND grant_type = ?", 
          {'Slice' =>{}}, ($self->id, GRANT_DIRECT));
    foreach my $row (@$ref){
        $dbh->do("INSERT INTO test_plan_permissions (userid, plan_id, permissions, grant_type)
        VALUES(?,?,?,?)", undef, ($row->{'userid'}, $planid, $row->{'permissions'}, GRANT_DIRECT));
    }
}

=head2 lookup_type

Takes an ID of the type field and returns the value

=cut

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

=head2 lookup_type_by_name

Returns the id of the type name passed.

=cut

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

=head2 lookup_product

Takes an ID of the status field and returns the value

=cut

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

=head2 lookup_product_by_name

Returns the id of the product name passed.

=cut

sub lookup_product_by_name {
    my ($name) = @_;
    my $dbh = Bugzilla->dbh;
    
    # TODO 2.22 use Product.pm
    my ($value) = $dbh->selectrow_array(
            "SELECT id
             FROM products
             WHERE name = ?",
             undef, $name);
    return $value;
}

sub set_tester_regexp {
    my $self = shift;
    my ($regexp, $permissions) = @_;
    my $dbh = Bugzilla->dbh;
    
    ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/});
    
    return unless $regexp;
    my ($count) = $dbh->selectrow_array(
        "SELECT COUNT(*) 
           FROM profiles 
          WHERE login_name REGEXP(?)", 
          undef, $regexp);
    ThrowUserError("testopia-regexp-too-inclusive") if $count > Bugzilla->params->{'testopia-max-allowed-plan-testers'};
     
    my ($is, $oldreg, $oldperms) = $dbh->selectrow_array(
        "SELECT 1, user_regexp, permissions 
           FROM test_plan_permissions_regexp 
          WHERE plan_id = ?",undef, $self->id);
    
    return unless ($oldreg ne $regexp || $oldperms != $permissions);
    if ($is){
        $dbh->do("UPDATE test_plan_permissions_regexp
                     SET user_regexp = ?, permissions = ? 
                   WHERE plan_id = ?", undef, ($regexp, $permissions, $self->id));
    }
    else { 
        $dbh->do("INSERT INTO test_plan_permissions_regexp
                  (plan_id, user_regexp, permissions) 
                  VALUES(?,?,?)", 
                  undef, ($self->id, $regexp, $permissions));
    } 
    
    $self->derive_regexp_testers($regexp);
    
}

sub derive_regexp_testers {
    my $self = shift;
    my $regexp = shift;
    ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/});
    my $dbh = Bugzilla->dbh;
    # Get the permissions of the regexp testers so we can set it later.
    my ($permissions) = $dbh->selectrow_array(
        "SELECT permissions 
           FROM test_plan_permissions_regexp 
         WHERE plan_id = ?", undef, $self->id);
               
    my $sth = $dbh->prepare("SELECT profiles.userid, profiles.login_name, plan_id
                               FROM profiles
                          LEFT JOIN test_plan_permissions
                                 ON test_plan_permissions.userid = profiles.userid
                                AND test_plan_permissions.plan_id = ?
                                AND grant_type = ?");
    my $plan_add = $dbh->prepare("INSERT INTO test_plan_permissions
                                 (userid, plan_id, permissions, grant_type)
                                 VALUES (?,?,?,?)");
    my $plan_update = $dbh->prepare("UPDATE test_plan_permissions
                                 SET permissions = ? 
                                 WHERE userid = ? AND plan_id = ? AND grant_type = ?");
    my $plan_del = $dbh->prepare("DELETE FROM test_plan_permissions
                                 WHERE userid = ? AND plan_id = ?
                                 AND grant_type = ?");
    $sth->execute($self->id, GRANT_REGEXP);
    while (my ($userid, $login, $present) = $sth->fetchrow_array()) {
        if (($regexp =~ /\S+/) && ($login =~ m/$regexp/i)){
            if ($present){
                $plan_update->execute($permissions, $userid, $self->id, GRANT_REGEXP)
            }
            else {
                $plan_add->execute($userid, $self->id, $permissions, GRANT_REGEXP)
            } 
        }
        else {
            $plan_del->execute($userid, $self->id, GRANT_REGEXP) if $present;
        }
    }

}

sub remove_tester {
    my $self = shift;
    my ($userid) = @_;
    my $dbh = Bugzilla->dbh;
    
    $dbh->do("DELETE FROM test_plan_permissions 
              WHERE userid = ? AND plan_id = ? AND grant_type = ?",
              undef, ($userid, $self->id, GRANT_DIRECT));
}

sub add_tester {
    my $self = shift;
    my ($userid, $perms) = @_;
    my $dbh = Bugzilla->dbh;
    my ($is) = $dbh->selectrow_array(
        "SELECT userid 
           FROM test_plan_permissions
           WHERE userid = ? AND plan_id = ? AND grant_type = ?", 
           undef, ($userid, $self->id, GRANT_DIRECT));
    return if $is;
    
    $dbh->do("INSERT INTO test_plan_permissions
              (userid, plan_id, permissions, grant_type) 
              VALUES(?,?,?,?)", 
              undef, ($userid, $self->id, $perms, GRANT_DIRECT));
}

sub update_tester {
    my $self = shift;
    my ($userid, $perms) = @_;
    my $dbh = Bugzilla->dbh;

    $dbh->do("UPDATE test_plan_permissions SET permissions = ? 
          WHERE userid = ? AND plan_id = ? AND grant_type = ?", 
          undef, ($perms, $userid, $self->id, GRANT_DIRECT)); 
}

=head2 obliterate

Removes this plan and all things that reference it.

=cut

sub obliterate {
    my $self = shift;
    my ($cgi, $template) = @_;
    my $vars;
    my $dbh = Bugzilla->dbh;

    my $progress_interval = 250;
    my $i = 0;
    my $total = scalar @{$self->test_cases} + scalar @{$self->test_runs};

    foreach my $obj (@{$self->attachments}){
        $obj->obliterate;
    }
    foreach my $obj (@{$self->test_runs}){
        $obj->obliterate($cgi, $template);
    }
    foreach my $obj (@{$self->test_cases}){
        $i++;
        if ($cgi && $i % $progress_interval == 0){
            print $cgi->multipart_end;
            print $cgi->multipart_start;
            $vars->{'complete'} = $i;
            $vars->{'total'} = $total;
            $vars->{'process'} = "Deleting test cases";
            
            $template->process("testopia/progress.html.tmpl", $vars)
              || ThrowTemplateError($template->error());
        }
        
        $obj->obliterate if (scalar @{$obj->plans} == 1);
    }

    $dbh->do("DELETE FROM test_plan_texts WHERE plan_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_plan_tags WHERE plan_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_plan_activity WHERE plan_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_case_plans WHERE plan_id = ?", undef, $self->id);
    $dbh->do("DELETE FROM test_plans WHERE plan_id = ?", undef, $self->id);
    return 1;
}

=head2 canview

Returns true if the logged in user has rights to view this plan

=cut

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

=head2 canedit

Returns true if the logged in user has rights to edit this plan

=cut

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

}

=head2 candelete

Returns true if the logged in user has rights to delete this plan

=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->get_user_rights(Bugzilla->user->id) & TR_DELETE;
    return 0;
}

sub canadmin {
    my $self = shift;
    return 1 if Bugzilla->user->in_group("admin");
    return 1 if ($self->get_user_rights(Bugzilla->user->id) & TR_ADMIN);
    return 0;
}
  
sub get_user_rights {
    my $self = shift;
    my ($userid) = @_;
    
    my $dbh = Bugzilla->dbh;
    my ($perms) = $dbh->selectrow_array(
        "SELECT permissions FROM test_plan_permissions 
          WHERE userid = ? AND plan_id = ?", 
          undef, ($userid, $self->id));
    
    return $perms;
}

sub to_json {
    my $self = shift;
    my $obj;
    my $json = new JSON;
    
    foreach my $field ($self->DB_COLUMNS){
        $obj->{$field} = $self->{$field};
    }
    
    $obj->{'product_name'} = $self->product->name if $self->product;
    $obj->{'run_count'}    = $self->test_run_count;
    $obj->{'case_count'}   = $self->test_case_count;
    $obj->{'author_name'}  = $self->author->login;
    $obj->{'plan_type'}    = $self->plan_type;
    $obj->{'canedit'}      = $self->canedit;
    $obj->{'canview'}      = $self->canview;
    $obj->{'candelete'}    = $self->candelete;
    $obj->{'link_url'}     = 'tr_show_plan.cgi?plan_id=' . $self->id;
    $obj->{'type'}         = $self->type;
    $obj->{'id'}           = $self->id;
    
    return $json->objToJson($obj); 
}

###############################
####      Accessors        ####
###############################
=head1 ACCESSOR METHODS

=head2 id

Returns the ID for this object

=head2 creation_date

Returns the creation timestamp for this object

=head2 product_version

Returns the product version for this object

=head2 product_id

Returns the product id for this object

=head2 author

Returns a Bugzilla::User object representing the plan author

=head2 name

Returns the name of this plan

=head2 type_id

Returns the type id of this plan

=head2 isactive

Returns true if this plan is not archived

=cut

sub id              { return $_[0]->{'plan_id'};          }
sub creation_date   { return $_[0]->{'creation_date'};    }
sub product_version { return $_[0]->{'default_product_version'};  }
sub product_id      { return $_[0]->{'product_id'};       }
sub author          { return Bugzilla::User->new($_[0]->{'author_id'});  }
sub name            { return $_[0]->{'name'};    }
sub type_id         { return $_[0]->{'type_id'};    }
sub isactive        { return $_[0]->{'isactive'};  }

=head2 type

Returns 'case'

=cut

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

sub tester_regexp { 
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    
    my ($regexp) = $dbh->selectrow_array(
        "SELECT user_regexp 
           FROM test_plan_permissions_regexp
          WHERE plan_id = ?", undef, $self->id);

    return $regexp;
}

sub tester_regexp_permissions { 
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    
    my ($perms) = $dbh->selectrow_array(
        "SELECT permissions 
           FROM test_plan_permissions_regexp
          WHERE plan_id = ?", undef, $self->id);
    my $p;
    
    $p->{'read'}   = $perms >= TR_READ;
    $p->{'write'}  = $perms >= TR_WRITE;
    $p->{'delete'} = $perms >= TR_DELETE;
    $p->{'admin'}  = $perms >= TR_ADMIN;
  
    return $p;
}

sub access_list {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;

    my $ref = $dbh->selectall_arrayref(
        "SELECT tpp.userid, permissions 
           FROM test_plan_permissions AS tpp
           JOIN profiles ON profiles.userid = tpp.userid 
          WHERE plan_id = ? AND grant_type = ?
          ORDER BY profiles.realname", {'Slice' =>{}}, ($self->id, GRANT_DIRECT));
    my @rows;
    foreach my $row (@$ref){
        push @rows, {'user'   => Bugzilla::User->new($row->{'userid'}),
                     'read'   => $row->{'permissions'} >= TR_READ,
                     'write'  => $row->{'permissions'} >= TR_WRITE,
                     'delete' => $row->{'permissions'} >= TR_DELETE,
                     'admin'  => $row->{'permissions'} >= TR_ADMIN,
                    };
    }
    $self->{'access_list'} = \@rows;
    return $self->{'access_list'};
}

=head2 attachments

Returns a reference to a list of attachments on this plan

=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_plan_attachments
              WHERE plan_id = ?", 
             undef, $self->{'plan_id'});
    
    my @attachments;
    foreach my $attach (@{$attachments}){
        push @attachments, Bugzilla::Testopia::Attachment->new($attach);
    }
    $self->{'attachments'} = \@attachments;
    return $self->{'attachments'};
    
}

=head2 bugs

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

=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 
             JOIN test_cases ON test_case_bugs.case_id = test_cases.case_id
             JOIN test_case_plans ON test_case_plans.case_id = test_cases.case_id 
            WHERE test_case_plans.plan_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'};
}

=head2 product

Returns the product this plan is associated with

=cut

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

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

=head2 test_cases

Returns a reference to a list of Testopia::TestCase objects linked
to this plan

=cut

sub test_cases {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'test_cases'} if exists $self->{'test_cases'};
    
    require Bugzilla::Testopia::TestCase;
    
    my $caseids = $dbh->selectcol_arrayref(
            "SELECT case_id FROM test_case_plans
              WHERE plan_id = ?", 
             undef, $self->{'plan_id'});
    my @cases;
    foreach my $id (@{$caseids}){
        push @cases, Bugzilla::Testopia::TestCase->new($id);
    }

    $self->{'test_cases'} = \@cases;
    return $self->{'test_cases'};
}

=head2 test_case_count

Returns a count of the test cases linked to this plan

=cut

sub test_case_count {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'test_case_count'} if exists $self->{'test_case_count'};
    $self->{'test_case_count'} = $dbh->selectrow_array(
                                      "SELECT COUNT(case_id) FROM test_case_plans
                                       WHERE plan_id = ?", 
                                       undef, $self->{'plan_id'}) || 0;
    return $self->{'test_case_count'};
}

=head2 test_runs

Returns a reference to a list of test runs in this plan

=cut

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

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

=head2 test_run_count

Returns a count of the test cases linked to this plan

=cut

sub test_run_count {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'test_run_count'} if exists $self->{'test_run_count'};
    $self->{'test_run_count'} = $dbh->selectrow_array(
                                      "SELECT COUNT(run_id) FROM test_runs
                                       WHERE plan_id = ?", 
                                       undef, $self->{'plan_id'}) || 0;
    return $self->{'test_run_count'};
}

sub test_case_run_count {
    my $self = shift;
    my ($status_id) = @_;
    my $dbh = Bugzilla->dbh;
    my $query = 
        "SELECT count(case_run_id) FROM test_case_runs 
         INNER JOIN test_runs ON test_case_runs.run_id = test_runs.run_id
         INNER JOIN test_plans ON test_runs.plan_id = test_plans.plan_id
         WHERE test_case_runs.iscurrent = 1 AND test_plans.plan_id = ?";
       $query .= " AND test_case_runs.case_run_status_id = ?" if $status_id;
    my $count;
    if ($status_id){
        ($count) = $dbh->selectrow_array($query,undef,($self->id,$status_id));
    }
    else {
        ($count) = $dbh->selectrow_array($query,undef,$self->id);
    }
    
    return $count;
}

sub builds_seen {
    my $self = shift;
    my ($status_id) = @_;
    my $dbh = Bugzilla->dbh;
    
    require Bugzilla::Testopia::Build;
    
    my $ref = $dbh->selectcol_arrayref(
        "SELECT DISTINCT test_case_runs.build_id 
           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.iscurrent = 1",
          undef,$self->id);
    
    my @o;      
    foreach my $id (@$ref){
        push @o, Bugzilla::Testopia::Build->new($id);
    }
    return \@o;
}

sub environments_seen {
    my $self = shift;
    my ($status_id) = @_;
    my $dbh = Bugzilla->dbh;
    
    require Bugzilla::Testopia::Environment;
    
    my $ref = $dbh->selectcol_arrayref(
        "SELECT DISTINCT test_case_runs.environment_id 
           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.iscurrent = 1",
          undef,$self->id);
          
    my @o; 
    foreach my $id (@$ref){
        push @o, Bugzilla::Testopia::Environment->new($id);
    }
    return \@o;   
}

=head2 tags

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

=cut

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

=head2 text

Returns the text of the plan document from the latest version 
in the test_plan_texts table

=cut

sub text {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    my ($version) = @_;
    trick_taint($version) if $version;
    return $self->{'text'} if exists $self->{'text'} && !$version;
    
    $version = $version || $self->version;
    
    my $text = $dbh->selectrow_hashref(
        "SELECT plan_text, profiles.realname AS author, plan_text_version AS version
           FROM test_plan_texts AS tpt
          INNER JOIN profiles ON tpt.who = profiles.userid
          WHERE plan_id = ? AND plan_text_version = ?", 
          undef, ($self->{'plan_id'}, $version));
    
    return $text if scalar @_;
    
    $self->{'text'} = $text;
    
    return $self->{'text'};
}


=head2 version

Returns the plan text version. This number is incremented any time
changes are made to the plan document.

=cut

sub version { 
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'version'} if exists $self->{'version'};
    my ($ver) = $dbh->selectrow_array("SELECT MAX(plan_text_version)
                                       FROM test_plan_texts
                                       WHERE plan_id = ?", 
                                       undef, $self->{'plan_id'});

    $self->{'version'} = $ver;
    return $self->{'version'};


}

=head2 type

Returns the type of this plan

=cut

sub plan_type {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return $self->{'plan_type'} if exists $self->{'plan_type'};
    my ($type) = $dbh->selectrow_array("SELECT name
                                       FROM test_plan_types
                                       WHERE type_id = ?", 
                                       undef, $self->{'type_id'});

    $self->{'plan_type'} = $type;
    return $self->{'plan_type'};
}

=head1 TODO

Use Bugzilla::Product and Version in 2.22

=head1 SEE ALSO

Testopia::(TestRun, TestCase, Category, Build, Util)

=head1 AUTHOR

Greg Hendricks <ghendricks@novell.com>

=cut

1;

__END__

=head1 NAME

Bugzilla::Testopia::TestPlan - Testopia Test Plan object

=head1 DESCRIPTION

This module represents a test plan in Testopia. The test plan
is the glue of testopia. Virtually all other objects associate 
to a plan.

=head1 SYNOPSIS

use Bugzilla::Testopia::TestPlan;

 $plan = Bugzilla::Testopia::TestPlan->new($plan_id);
 $plan = Bugzilla::Testopia::TestPlan->new({});

=cut

=head2 new

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

=cut
