# -*- Mode: perl; indent-tabs-mode: nil -*-

package QA::Util;

use strict;
use Data::Dumper;
use HTTP::Cookies;
use Test::More;
use Test::WWW::Selenium;
use WWW::Selenium::Util qw(server_is_running);

use base qw(Exporter);
@QA::Util::EXPORT = qw(
    trim
    url_quote
    random_string
    pause
    _ok

    log_in
    logout
    file_bug_in_product
    go_to_admin
    edit_product
    add_product
    open_advanced_search_page
    set_parameters

    delete_bugs
    delete_flag_types

    get_selenium
    get_rpc_clients

    WAIT_TIME
    CHROME_MODE
);

# How long we wait for pages to load.
use constant WAIT_TIME => 120000;
use constant CONF_FILE =>  "../config/selenium_test.conf";
use constant CHROME_MODE => 1;

#####################
# Utility Functions #
#####################

sub _ok($$)
{
    ok($_[0], $_[1]) or pause(1);
}

sub pause
{
    my $i = $_[0] || 0;
    my $c;
    do { $c = [caller($i++)] } while ($c->[0] eq 'Test::WWW::Selenium' || $c->[0] eq 'WWW::Selenium' || $c->[0] eq 'QA::Util');
    print STDERR "***PAUSED*** at ".$c->[1].":".$c->[2].". Press Enter to continue\n";
    getc;
}

sub random_string {
    my $size = shift || 30; # default to 30 chars if nothing specified
    return join("", map{ ('0'..'9','a'..'z','A'..'Z')[rand 62] } (1..$size));
}

# Remove consecutive as well as leading and trailing whitespaces.
sub trim {
    my ($str) = @_;
    if ($str) {
      $str =~ s/[\r\n\t\s]+/ /g;
      $str =~ s/^\s+//g;
      $str =~ s/\s+$//g;
    }
    return $str;
}

# This originally came from CGI.pm, by Lincoln D. Stein
sub url_quote {
    my ($toencode) = (@_);
    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
    return $toencode;
}

###################
# Setup Functions #
###################

sub get_config {
    # read the test configuration file
    my $conf_file = CONF_FILE;
    my $config = do($conf_file)
        or die "can't read configuration '$conf_file': $!$@";
}

sub get_selenium {
    my $chrome_mode = shift;
    my $config = get_config();

    if (!server_is_running) {
        die "Selenium Server isn't running!";
    }

    my $sel = Test::WWW::Selenium->new(
        host        => $config->{host},
        port        => $config->{port},
        browser     => $chrome_mode ? $config->{experimental_browser_launcher} : $config->{browser},
        browser_url => $config->{browser_url},
        error_callback => \&pause,
    );

    return ($sel, $config);
}

sub get_xmlrpc_client {
    my $config = get_config();
    my $xmlrpc_url = $config->{browser_url} . "/"
                    . $config->{bugzilla_installation} . "/xmlrpc.cgi";

    require QA::RPC::XMLRPC;
    # A temporary cookie jar that isn't saved after the script closes.
    my $cookie_jar = new HTTP::Cookies();
    my $rpc        = new QA::RPC::XMLRPC(proxy => $xmlrpc_url);
    $rpc->transport->cookie_jar($cookie_jar);
    return ($rpc, $config);
}

sub get_jsonrpc_client {
    require QA::RPC::JSONRPC;
    # A temporary cookie jar that isn't saved after the script closes.
    my $cookie_jar = new HTTP::Cookies();
    my $rpc = new QA::RPC::JSONRPC();
    $rpc->transport->cookie_jar($cookie_jar);
    # If we don't set a long timeout, then the Bug.add_comment test
    # where we add a too-large comment fails.
    $rpc->transport->timeout(180);
    $rpc->version('1.0');
    return $rpc;
}

sub get_rpc_clients {
    my ($xmlrpc, $config) = get_xmlrpc_client();
    my $jsonrpc = get_jsonrpc_client();
    return ($xmlrpc, $jsonrpc, $config);
}

################################
# Helpers for Selenium Scripts #
################################

# Go to the home/login page and log in.
sub log_in {
    my ($sel, $config, $user) = @_;

    $sel->open_ok("/$config->{bugzilla_installation}/", undef, "Go to the login page");
    $sel->type_ok("Bugzilla_login_top", $config->{"${user}_user_login"}, "Enter $user login name");
    $sel->type_ok("Bugzilla_password_top", $config->{"${user}_user_passwd"}, "Enter $user password");
    $sel->click_ok("log_in_top", undef, "Submit credentials");
    $sel->wait_for_page_to_load(WAIT_TIME);
    $sel->title_is("Bugzilla Main Page", "User is logged in");
}

# Log out. Will fail if you are not logged in.
sub logout {
    my $sel = shift;

    $sel->click_ok("link=Log out", undef, "Logout");
    $sel->wait_for_page_to_load_ok(WAIT_TIME);
    $sel->title_is("Logged Out");
}

# Display the bug form to enter a bug in the given product.
sub file_bug_in_product {
    my ($sel, $product, $classification, $component) = @_;

    $classification ||= "All";
    $sel->click_ok("link=New", undef, "Go create a new bug");
    $sel->wait_for_page_to_load(WAIT_TIME);
    my $title = $sel->get_title();
    if ($title eq "Select Classification") {
        ok(1, "More than one enterable classification available. Display them in a list");
        $sel->click_ok("link=$classification", undef, "Choose $classification");
        $sel->wait_for_page_to_load(WAIT_TIME);
        $title = $sel->get_title();
    }
    if ($title eq "Enter Bug") {
        ok(1, "Display the list of enterable products");
        $sel->click_ok("link=$product", undef, "Choose $product");
        $sel->wait_for_page_to_load(WAIT_TIME);
    }
    else {
        ok(1, "Only one product available in $classification. Skipping the 'Choose product' page.")
    }
    $sel->title_is("Enter Bug: $product", "Display form to enter bug data");
    $sel->select("component", $component ? "label=$component" : "index=0");
}

# Go to admin.cgi.
sub go_to_admin {
    my $sel = shift;

    $sel->click_ok("link=Administration", undef, "Go to the Admin page");
    $sel->wait_for_page_to_load(WAIT_TIME);
    $sel->title_like(qr/^Administer your installation/, "Display admin.cgi");
}

# Go to editproducts.cgi and display the given product.
sub edit_product {
    my ($sel, $product, $classification) = @_;

    $classification ||= "Unclassified";
    go_to_admin($sel);
    $sel->click_ok("link=Products", undef, "Go to the Products page");
    $sel->wait_for_page_to_load(WAIT_TIME);
    my $title = $sel->get_title();
    if ($title eq "Select Classification") {
        ok(1, "More than one enterable classification available. Display them in a list");
        $sel->click_ok("link=$classification", undef, "Choose $classification");
        $sel->wait_for_page_to_load(WAIT_TIME);
    }
    else {
        $sel->title_is("Select product", "Display the list of enterable products");
    }
    $sel->click_ok("link=$product", undef, "Choose $product");
    $sel->wait_for_page_to_load(WAIT_TIME);
    $sel->title_is("Edit Product '$product'", "Display properties of $product");
}

sub add_product {
    my ($sel, $classification) = @_;

    $classification ||= "Unclassified";
    go_to_admin($sel);
    $sel->click_ok("link=Products", undef, "Go to the Products page");
    $sel->wait_for_page_to_load(WAIT_TIME);
    my $title = $sel->get_title();
    if ($title eq "Select Classification") {
        ok(1, "More than one enterable classification available. Display them in a list");
        $sel->click_ok("//a[contains(\@href, 'editproducts.cgi?action=add&classification=$classification')]",
                       undef, "Add product to $classification");
    }
    else {
        $sel->title_is("Select product", "Display the list of enterable products");
        $sel->click_ok("link=Add", undef, "Add a new product");
    }
    $sel->wait_for_page_to_load(WAIT_TIME);
    $sel->title_is("Add Product", "Display the new product form");
}

sub open_advanced_search_page {
    my $sel = shift;

    $sel->click_ok("link=Search");
    $sel->wait_for_page_to_load(WAIT_TIME);
    my $title = $sel->get_title();
    if ($title eq "Simple Search") {
        ok(1, "Display the simple search form");
        $sel->click_ok("link=Advanced Search");
        $sel->wait_for_page_to_load(WAIT_TIME);
    }
    $sel->title_is("Search for bugs", "Display the Advanced search form");
}

# $params is a hashref of the form:
# {section1 => { param1 => {type => '(text|select)', value => 'foo'},
#                param2 => {type => '(text|select)', value => 'bar'},
#                param3 => undef },
#  section2 => { param4 => ...},
# }
# section1, section2, ... is the name of the section
# param1, param2, ... is the name of the parameter (which must belong to the given section)
# type => 'text' is for text fields
# type => 'select' is for drop-down select fields
# undef is for radio buttons (in which case the parameter must be the ID of the radio button)
# value => 'foo' is the value of the parameter (either text or label)
sub set_parameters {
    my ($sel, $params) = @_;

    go_to_admin($sel);
    $sel->click_ok("link=Parameters", undef, "Go to the Config Parameters page");
    $sel->wait_for_page_to_load(WAIT_TIME);
    $sel->title_is("Configuration: Required Settings");
    my $last_section = "Required Settings";

    foreach my $section (keys %$params) {
        if ($section ne $last_section) {
            $sel->click_ok("link=$section");
            $sel->wait_for_page_to_load_ok(WAIT_TIME);
            $sel->title_is("Configuration: $section");
            $last_section = $section;
        }
        my $param_list = $params->{$section};
        foreach my $param (keys %$param_list) {
            my $data = $param_list->{$param};
            if (defined $data) {
                my $type = $data->{type};
                my $value = $data->{value};

                if ($type eq 'text') {
                    $sel->type_ok($param, $value);
                }
                elsif ($type eq 'select') {
                    $sel->select_ok($param, "label=$value");
                }
                else {
                    ok(0, "Unknown parameter type: $type");
                }
            }
            else {
                # If the value is undefined, then the param name is
                # the ID of the radio button.
                $sel->click_ok($param);
            }
        }
        $sel->click_ok('//input[@type="submit" and @value="Save Changes"]', undef, "Save Changes");
        $sel->wait_for_page_to_load_ok(WAIT_TIME);
        $sel->title_is("Parameters Updated");
    }
}

sub delete_bugs
{
    my ($sel, $config, $bugs) = @_;
    # Enable allowbugdeletion
    set_parameters($sel, { "Administrative Policies" => {"allowbugdeletion-on" => undef } });
    edit_product($sel, 'TestProduct', 'Unclassified');
    $sel->click_ok("link=Edit components:");
    $sel->wait_for_page_to_load_ok(WAIT_TIME);
    $sel->title_is("Select component of product 'TestProduct'");
    my $text = trim($sel->get_text("bugzilla-body"));
    if ($text !~ /KillerComponent/)
    {
        # Create KillerComponent
        $sel->click_ok("link=Add");
        $sel->wait_for_page_to_load_ok(WAIT_TIME);
        $sel->type_ok("component", "KillerComponent");
        $sel->type_ok("description", "Component killing bugs");
        $sel->type_ok("initialowner", $config->{unprivileged_user_login});
        $sel->type_ok("initialqacontact", $config->{unprivileged_user_login});
        $sel->click_ok("create");
        $sel->wait_for_page_to_load_ok(WAIT_TIME);
    }
    # Move bugs into KillerComponent
    for (@$bugs)
    {
        $sel->type_ok("quicksearch_top", $_);
        $sel->click_ok("find_top");
        $sel->wait_for_page_to_load_ok(WAIT_TIME);
        $sel->title_like(qr/^Bug $_/);
        $sel->select_ok("product", "label=TestProduct");
        $sel->select_ok("component", "label=KillerComponent");
        $sel->click_ok("commit");
        $sel->wait_for_page_to_load_ok(WAIT_TIME);
        if ($sel->get_title() =~ /Verify/)
        {
            $sel->click_ok("change_product");
            $sel->wait_for_page_to_load_ok(WAIT_TIME);
        }
        $sel->title_is("Bug $_ processed");
    }
    # Delete KillerComponent and all bugs in it
    edit_product($sel, 'TestProduct', 'Unclassified');
    $sel->click_ok("link=Edit components:");
    $sel->wait_for_page_to_load_ok(WAIT_TIME);
    $sel->title_is("Select component of product 'TestProduct'");
    $sel->click_ok("//a[contains(\@href, 'editcomponents.cgi?action=del&product=TestProduct&component=KillerComponent')]");
    $sel->wait_for_page_to_load_ok(WAIT_TIME);
    $sel->title_is("Delete component 'KillerComponent' from 'TestProduct' product");
    $sel->click_ok("delete");
    $sel->wait_for_page_to_load_ok(WAIT_TIME);
    $sel->title_is("Component Deleted");
}

sub delete_flag_types
{
    my ($sel, $config, $regexp) = @_;
    go_to_admin($sel);
    $sel->click_ok("link=Flags");
    $sel->wait_for_page_to_load_ok(WAIT_TIME);
    $sel->title_is("Administer Flag Types");
    for ('flag_types_bugs', 'flag_types_attachments')
    {
        my $flagtype_count = $sel->get_xpath_count("//table[\@id='$_']/tbody/tr");
        for (my $i = 2; $i <= $flagtype_count; $i++)
        {
            my $flag_name = trim($sel->get_text("//table[\@id='$_']/tbody/tr[$i]/td[1]"));
            if ($flag_name =~ /$regexp/)
            {
                $sel->click_ok("//table[\@id='$_']/tbody/tr[$i]//a[contains(\@href,'action=confirmdelete')]");
                $sel->wait_for_page_to_load_ok(WAIT_TIME);
                $sel->title_is("Confirm Deletion of Flag Type '$flag_name'");
                $sel->click_ok("link=Yes, delete");
                $sel->wait_for_page_to_load_ok(WAIT_TIME);
                $sel->title_is("Flag Type '$flag_name' Deleted");
                my $msg = trim($sel->get_text("message"));
                _ok($msg eq "The flag type $flag_name has been deleted.", "Flag type $flag_name deleted");
                $flagtype_count = $sel->get_xpath_count("//table[\@id='$_']/tbody/tr");
                $i--;
            }
        }
    }
}

1;
__END__
