# Skip the system configuration steps when we load the environment/routes,
# because we'll set and reload routes later
SKIP_SYSTEM_CONFIGURATION = true

# Standard test initialization code
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../../config/environment")
require 'rails/test_help'

# Require the <tt>common_test_methods</tt> file
require File.expand_path(File.dirname(__FILE__) + "/../common_test_methods")

# Load the libraries from installed gems required to run the tests
load_testing_libs

# Request permission to alter the Zebra database (if this has already been run,
# the environment check will just skip this step)
verify_zebra_changes_allowed

# Stop, initialize, start, and bootstrap the zebra databases ready for the tests
bootstrap_zebra_with_initial_records

# Load the factories we have (for quick user / basket generation)
require File.expand_path(File.dirname(__FILE__) + "/../factories")

# Overwrite all constants there may be with defaults so we can continue testing
configure_environment do
  require File.expand_path(File.dirname(__FILE__) + "/../system_configuration_constants")
end

# Turn off error 500 page opening (it's output to the console aleady)
# Turn the mode to rails as well (or we get errors)
if defined?(SELENIUM_MODE) && SELENIUM_MODE
  Webrat.configure do |config|
    config.mode = :selenium
    config.application_environment = :test
    config.open_error_files = false
  end
else
  Webrat.configure do |config|
    config.mode = :rails
    config.open_error_files = false
  end
end

# Overload the IntegrationTest class to ensure tear down occurs OK.
class ActionController::IntegrationTest
  include ZoomControllerHelpers

  # setup basket variables for use later
  @@site_basket ||= Basket.site_basket
  @@help_basket ||= Basket.help_basket
  @@about_basket ||= Basket.about_basket
  @@documentation_basket ||= Basket.documentation_basket

  # how many users/baskets do we have when we start these tests
  @@user_count = User.count
  @@basket_count = Basket.count

  # setup object creation variables for use later
  @@users_created = []
  @@baskets_created = []

  # Attempt to logout. Can be called anywhere (even without being logged in).
  def logout
    visit "/site/account/logout"
  end

  # Attempt to login. If we arn't on the login page aleady, we'll navigate to the login by first logging out
  # (see <tt>logout</tt>) visiting the root path (site basket homepage) and clicking the link "Login" in the
  # header. Then fill in fields and click "Log in" Takes required username, and optional password, whether to
  # navigate to login (not needed if we're there already) and whether we are expecting this login to fail
  def login_as(username, password = 'test', options = {})
    options = {
      :navigate_to_login => false,
      :by_form => false,
      :logout_first => false,
      :test_success => false,
      :should_fail_login => false
    }.merge(options)

    options[:logout_first] = true if options[:navigate_to_login]
    options[:test_success] = true if options[:navigate_to_login]

    if options[:logout_first]
      logout # make sure we arn't logged in first
    end

    if options[:navigate_to_login]
      visit "/site/account/login"
    end

    if options[:by_form]
      body_should_contain "Login to Kete"
      fill_in "login", :with => username.to_s
      fill_in "password", :with => password
      submit_form "login"
    else
      # we just want the user to have an authenticated state
      # no need to go through all the requests, etc.
      visit "/site/account/login", :post, { :login => username.to_s, :password => password }
    end

    body_should_contain("Logged in successfully") if options[:test_success] && !options[:should_fail_login]
  end

  # Asserts whether the supplied text is within the response body returned from a visit request.
  # Takes required text, optional options hash, which can set :number_of_times (how many occurances of text
  # should be on this page), and :dump_response option (which will output the entire response body (html
  # source) to the console)
  def body_should_contain(text, options = {})
    raise "body_should_contain method should be called after a page visit" if response.nil? || response.body.nil?
    response_body = response.body.squish
    unless text.kind_of?(Regexp)
      text = options[:escape_chars] ? escape(text.squish) : text.squish
    end
    save_and_open_page if options[:dump_response]
    if !options[:number_of_times].nil?
      occurances = response_body.scan(text).size
      assert (occurances == options[:number_of_times]),
             (options[:message] || "Body should contain '#{text}' #{options[:number_of_times]} times, but has #{occurances}.")
    else
      if text.kind_of?(Regexp)
        assert (response_body =~ text), (options[:message] || "Body should contain '#{text}', but does not.")
      else
        assert response_body.include?(text), (options[:message] || "Body should contain '#{text}', but does not.")
      end
    end
  end

  # Asserts whether the supplied text is not within the response body returned from a visit request.
  # Takes required text, optional options hash, which can set :number_of_times (how many occurances of text
  # should be on this page), and :dump_response option (which will output the entire response body (html
  # source) to the console)
  def body_should_not_contain(text, options = {})
    raise "body_should_not_contain method should be called after a page visit" if response.nil? || response.body.nil?
    response_body = response.body.squish
    unless text.kind_of?(Regexp)
      text = options[:escape_chars] ? escape(text.squish) : text.squish
    end
    save_and_open_page if options[:dump_response]
    if !options[:number_of_times].nil?
      occurances = response_body.scan(text).size
      assert !(occurances == options[:number_of_times]),
             (options[:message] || "Body should not contain '#{text}' #{options[:number_of_times]} times, but does.")
    else
      if text.kind_of?(Regexp)
        assert !(response_body =~ text), (options[:message] || "Body should not contain '#{text}', but does.")
      else
        assert !response_body.include?(text), (options[:message] || "Body should not contain '#{text}', but does.")
      end
    end
  end

  # UGLY METHOD - FIND BETTER WAY
  # Checks elements exist on a page in the order they are rendered. Pass in an array in the order they
  # should appear, and a divider which seperates each text in the array (a div, hr, new line etc)
  def body_should_contain_in_order(text_array, divider, options = {})
    raise "body_should_contain_in_order method should be called after a page visit" if response.nil? || response.body.nil?
    save_and_open_page if options[:dump_response]
    response_body = response.body.squish
    parts = response_body.split(divider).compact.flatten
    offset = options[:offset] ? options[:offset] : 0
    parts.each_with_index do |part, index|
      next if (index - offset) < 0 || text_array[(index - offset)].nil?
      assert part.include?(text_array[(index - offset)]), "#{text_array[(index - offset)]} is not in the right order it should be."
    end
  end

  # Asserts whether the supplied text is within the request url of the currently viewed page
  # Takes required text, optional options hash which can set :dump_response option (which will output the
  # entire request url to the console)
  def url_should_contain(text, options = {})
    puts request.url if options[:dump_response]
    if text.is_a?(Regexp)
      assert (request.url =~ text), "URL should contain '#{text}', but does not."
    else
      assert request.url.include?(text), "URL should contain '#{text}', but does not."
    end
  end

  # Asserts whether the supplied text is not within the request url of the currently viewed page
  # Takes required text, optional options hash which can set :dump_response option (which will output the
  # entire request url to the console)
  def url_should_not_contain(text, options = {})
    puts request.url if options[:dump_response]
    if text.is_a?(Regexp)
      assert !(request.url =~ text), "URL should not contain '#{text}', but does."
    else
      assert !request.url.include?(text), "URL should not contain '#{text}', but does."
    end
  end

  # Create a new item by navigating to the item new page, filling in fields and clicking "Create". While
  # this method works properly, it is advised you use the functionality provided by <tt>method_missing</tt>,
  # such as new_topic or new_audio_recording (which will save you having to provide the zoom_class on the
  # end as it automatically determines that from the method name).
  # Takes all optional parameters (which will be populated with defaults if they remain nil)
  # options takes a hash of field values to be filled in. basket takes Basket object where the item will be
  # added to. is_homepage_topic specifies if the topic should be created through the Baskets "Add new basket
  # homepage topic" option on the homepage options page.
  # If you plan to make a homepage topic, use <tt>new_homepage_topic</tt> instead as it provides a cleaner
  # syntax zoom_class specifies what type of item is being added. Should be the class of the item such as
  # Topic or AudioRecording
  def new_item(options = nil, basket = nil, is_homepage_topic = nil, zoom_class = nil)
    # because we use method missing, something like  new_topic()  (without any options) will return nil when
    # it calls this method and because of some funkyness in ruby, setting defaults in the options above is
    # replaced by nil, rather than the value so instead of setting it there, we set them here instead,
    # which should provide better support
    options = {} if options.nil?
    basket = @@site_basket if basket.nil?
    is_homepage_topic = false if is_homepage_topic.nil?
    zoom_class = 'Topic' if zoom_class.nil?

    # Now we have the zoom_class, lets get the controller and field_prefix from it
    controller = zoom_class_controller(zoom_class)
    field_prefix = zoom_class.underscore

    # Set a bunch of default values to enter. Only title and description fields exist on every item so
    # only those can be set at this point. :new_path is also provided here, but later removed using
    # .delete(:new_path)
    fields = {
      :new_path => "/#{basket.urlified_name}/#{controller}/new",
      :title => "#{zoom_class_humanize(zoom_class)} Title",
      :description => "#{zoom_class_humanize(zoom_class)} Description",
      :success_message => "#{zoom_class_humanize(zoom_class)} was successfully created.",
      :relate_to => nil,
      :topic_type => "Topic"
    }
    fields.merge!(options)

    # If we're dealing with portraits, lets tack on params to the end of new_path
    if zoom_class == "StillImage"
      if fields.delete(:portrait)
        fields[:new_path] = "#{fields[:new_path]}?portrait=true"
        fields[:success_message] = "#{zoom_class_humanize(zoom_class)} was successfully created as a portrait."
      elsif fields.delete(:selected_portrait)
        fields[:new_path] = "#{fields[:new_path]}?selected_portrait=true"
        fields[:success_message] = "#{zoom_class_humanize(zoom_class)} was successfully created as your selected portrait."
      end
    end

    # Delete these here because they arn't fields and will <tt>get_webrat_actions_from</tt> to raise
    # an exception
    new_path = fields.delete(:new_path)
    success_message = fields.delete(:success_message)
    relate_to = fields.delete(:relate_to)
    go_to_related = fields.delete(:go_to_related)
    topic_type = fields.delete(:topic_type)
    should_fail_create = fields.delete(:should_fail)

    unless relate_to.nil? || relate_to.is_a?(Topic)
      raise "ERROR: You must relate an item to a Topic, not a #{relate_to.class.name}"
    end

    # If we are making a topic, and it is intended as a homepage, then make sure we append index_for_basket,
    # otherwise, if we are making a different item or a topic that isn't a homepage, browse directly to the
    # add item form for that type (created above as :new_path)
    if controller == 'topics' && is_homepage_topic
      visit "/#{basket.urlified_name}/topics/new?index_for_basket=#{basket.id}"
    elsif !relate_to.nil?
      visit "/#{relate_to.basket.urlified_name}/#{controller}/new?relate_to_item=#{relate_to.to_param}&relate_to_type=Topic"
    else
      visit new_path
    end

    # If we are making a Topic, it has one more step before we actually reach the new topic page, and that
    # is to provide a Topic Type
    if controller == 'topics'
      select(/#{topic_type}/, :from => "topic_topic_type_id")
      click_button("Choose Type")
    end

    # Convert the field values into webrat actions (strings to fields, booleans to radio buttons etc)
    get_webrat_actions_from(fields, field_prefix)

    # If we have been passed in a block of additional actions (because for example
    # <tt>get_webrat_actions_from</tt> doesn't support what we need), then yield that block here, passing
    # to it the field_prefix
    yield(field_prefix) if block_given?

    # With all fields filled in, create the item
    click_button "Create"

    # Get the last created item (the one created above)
    item = zoom_class.constantize.last

    # If we made a homepage, then we should gets text saying that we did so successfully,
    # otherwise we get test saying the Item was created successfully.
    if controller == 'topics' && is_homepage_topic
      if should_fail_create
        body_should_not_contain "Basket homepage was successfully created."
      else
        body_should_contain "Basket homepage was successfully created."
      end
    elsif !relate_to.nil?
      body_should_contain "Related #{zoom_class_humanize(zoom_class)} was successfully created."
      body_should_contain relate_to.title
      body_should_not_contain 'No Public Version Available'
      if item.latest_version_is_private?
        item.private_version!
        body_should_contain item.title.to_s
        body_should_contain "/#{basket.urlified_name}/#{controller}/show/#{item.id}?private=true"
      else
        body_should_contain item.title.to_s
        body_should_contain "/#{basket.urlified_name}/#{controller}/show/#{item.id}"
      end
      click_link item.title.to_s if go_to_related.nil? || go_to_related
    else
      if should_fail_create
        body_should_not_contain success_message
      else
        body_should_contain success_message
      end
    end

    # Finally, lets return the last item of this type made (we assigned item earlier)
    item
  end

  # A quick method for adding a new homepage topic.
  # Takes both optional arguments. To see what should be supplied for options and basket, see the definition
  # of add_item above
  def new_homepage_topic(options = {}, basket = @@site_basket)
    # Homepage topics are always made through homepage topic form, and are always of class Topic,
    # so we can remove two arguments by supplyin them both here manually
    new_item(options, basket, true, 'Topic')
  end

  # Update the item with a set of values from either options or passed in as a block
  # Takes a required item argument (the Object of whatever item you're wanting to update),
  # and an optional options value, a hash of field values to be filled in
  def update_item(item, options = {})
    # Lets get the controller from the item passed in, the zoom_class from the controller, and the
    # field_prefix from the zoom_class
    controller = zoom_class_controller(item.class.name)
    zoom_class = zoom_class_from_controller(controller)
    field_prefix = zoom_class.underscore

    # Set a bunch of default values to enter. Only title and description fields exist on every item so
    # only those can be set at this point. :edit_path is also provided here, but later removed using
    # .delete(:edit_path)
    fields = {
      :edit_path => "/#{item.basket.urlified_name}/#{controller}/edit/#{item.to_param}",
      :title => "#{zoom_class_humanize(zoom_class)} Updated Title",
      :description => "#{zoom_class_humanize(zoom_class)} Updated Description",
      :success_message => "#{zoom_class_humanize(zoom_class)} was successfully updated."
    }
    fields[:edit_path] += "?private=true" if item.is_private?
    fields.merge!(options)
    # Delete these here because they arn't fields and will <tt>get_webrat_actions_from</tt> to raise an
    # exception
    edit_path = fields.delete(:edit_path)
    success_message = fields.delete(:success_message)

    # Visit the items edit url (formed from either the items values, or pass in the path manually using
    # :edit_path in the options param), and confirm we are on the right page
    visit edit_path

    body_should_contain "Editing #{zoom_class_humanize(zoom_class)}"

    # Convert the field values into webrat actions (strings to fields, booleans to radio buttons etc). See
    # the declartion of <tt>get_webrat_actions_from</tt> to see how this is done.
    get_webrat_actions_from(fields, field_prefix)

    # If we have been passed in a block of additional actions (because for example
    # <tt>get_webrat_actions_from</tt> doesn't support what we need), then yield that block here, passing to
    # it the field_prefix
    yield(field_prefix) if block_given?

    # With all fields filled in, update the item
    click_button "Update"

    # Confirm the item was successfully edited before continuing
    body_should_contain success_message

    # Finally, lets reload the item so the values are repopulated for use in later assertions
    item.reload
  end

  # Deletes an item by going to it's show page and clicking the "Delete" button
  # Takes a required item argument (the Object of whatever item you're wanting to delete)
  def delete_item(item)
    # Lets get the controller from the item passed in
    controller = zoom_class_controller(item.class.name)
    # Go to the items delete URL
    visit "/#{item.basket.urlified_name}/#{controller}/destroy/#{item.to_param}", :post
    # Confirm the item was deleted and we are redirected to the items browse page before continuing
    body_should_contain "Refine your results"
    # we actually want this to fail and return nil, it means the item was deleted properly
    begin
      return item.reload # if this works, its bad
    rescue
      return nil # if this gets called, thats good
    end
  end

  # Add a new basket via the forms (as apposed to create_new_method basket that creates the objects)
  # Optionally receives a block which could be webrat control methods run on the basket creation form
  # prior to clicking "Create". Returns the newly created basket instance.
  def new_basket(options = {})
    fields = { :name => "New basket" }
    fields.merge!(options)

    visit '/site/baskets/new'
    body_should_contain 'New basket'

    # Convert the field values into webrat actions (strings to fields, booleans to radio buttons etc). See
    # the declartion of <tt>get_webrat_actions_from</tt> to see how this is done.
    get_webrat_actions_from(fields, 'basket')

    yield('basket') if block_given?

    click_button 'Create'

    body_should_contain 'Basket was successfully created.'
    body_should_contain "#{fields[:name]} Edit"

    # Return the last basket (the basket we just created)
    basket = Basket.last
    @@baskets_created << basket
    basket
  end

  # Delete a basket via the Delete button from the Basket edit page
  # Takes require basket object. Returns true if basket was deleted or false if the basket still remains
  def delete_basket(basket)
    visit "/#{basket.urlified_name}/baskets/destroy/#{basket.to_param}", :post

    body_should_contain 'Basket was successfully deleted.'
    # should return to site basket, not sub basket
    body_should_contain 'Browse'
    body_should_not_contain 'Browse:'

    @@baskets_created.delete(basket)

    begin
      basket.reload
      false
    rescue
      true
    end
  end

  # Quick and easy flagging for any item
  # Takes item object and flag string/symbol
  def flag_item_with(item, flag, version = nil)
    version ||= item.version
    visit "/#{item.basket.urlified_name}/#{zoom_class_controller(item.class.name)}/flag_form/#{item.id}?flag=#{flag}&version=#{version}"
    fill_in 'message_', :with => 'Testing'
    click_button 'Flag'
    body_should_contain 'Thank you for your input. A moderator has been notified and will review the item in question. The item has been reverted to a non-contested version for the time being'
    item.reload # get the new version
  end

  # Restore a moderated item (make live).
  # Takes required item object, and optional options hash, that can set :version to the version of the item
  # you wish to make live (default version is the latest version of the item)
  def moderate_restore(item, options = {})
    item_class = item.class.name
    controller = zoom_class_controller(item_class)
    version = options[:version] || item.version - 1
    visit "/#{item.basket.urlified_name}/#{controller}/preview/#{item.id}?version=#{version}"
    save_and_open_page unless response.body.include?("Preview revision")
    body_should_contain 'Preview revision'
    click_link I18n.t('topics.preview_actions.make_live')
    body_should_contain "The content of this #{zoom_class_humanize(item_class)} has been approved
                         from the selected revision."
    item.reload # get the new version
  end

  # Reject a moderated item.
  # Takes a require item object, and an option options hash, that can set :message (the reason for
  # rejection), and :version of the version of the item you wish to reject (default version is the
  # latest version of the item)
  def moderate_reject(item, options = {})
    item_class = item.class.name
    controller = zoom_class_controller(item_class)
    message = options[:message] || ""
    version = options[:version] || item.version - 1
    visit "/#{item.basket.urlified_name}/#{controller}/preview/#{item.id}?version=#{version}"
    body_should_contain 'Preview revision'
    click_link 'reject'
    body_should_contain "Reject this revision"
    fill_in 'message_', :with => message
    click_button 'Reject'
    body_should_contain "This version of this #{zoom_class_humanize(item_class)} has been rejected.
                         The user who submitted the revision will be notified by email."
    item.reload # get the new version
  end

  # Turn on full moderation on a basket
  def turn_on_full_moderation(basket)
    visit "/#{basket.urlified_name}/baskets/edit/#{basket.id}"
    select "moderator views before item approved", :from => "settings_fully_moderated"
    click_button "Update"
    body_should_contain "Basket was successfully updated."
    assert_equal "true", basket.settings[:fully_moderated].to_s,
                 "Basket fully_moderated setting should be true, but is not."
  end

  # Turn off full moderation on a basket
  def turn_off_full_moderation(basket)
    visit "/#{basket.urlified_name}/baskets/edit/#{basket.id}"
    select "moderation upon being flagged", :from => "settings_fully_moderated"
    click_button "Update"
    body_should_contain "Basket was successfully updated."
    assert_equal "false", basket.settings[:fully_moderated].to_s,
                 "Basket fully_moderated setting should be false, but is not."
  end

  # Check that an item occurs in search results only once
  # Note that an important limitation of this method is that it only checks the first page of results,
  # and hence is not useful for big result sets.
  def should_appear_once_in_search_results(item, options = {})
    # Reload to ensure that item is progressed past moderation version
    item.reload

    options = {
      :title => item.title
    }.merge!(options)

    if item.title == BLANK_TITLE
      error = "You asked to check that item is in search results, but item is pending moderation."
      error += "\n\n#{item.inspect}\n\n#{item.versions.inspect}\n\n"
      raise error
    end

    visit "/#{item.basket.urlified_name}/all/#{zoom_class_controller(item.class.name)}/"

    basket_mention = item.basket == @@site_basket ? "" : item.basket.name + " "
    body_should_contain "Results in #{basket_mention}#{zoom_class_plural_humanize(item.class.name).downcase}"

    # We can't use the item title because it can appear several times legitimately.
    body_should_contain "item_#{item.id}_wrapper", :number_of_times => 1
    body_should_contain options[:title]
  end

  # Check that an item DOES NOT occur in search results
  # Note that an important limitation of this method is that it only checks the first page of results,
  # and hence is not useful for big result sets.
  def should_not_appear_in_search_results(item)
    visit "/#{item.basket.urlified_name}/all/#{zoom_class_controller(item.class.name)}/"

    basket_mention = item.basket == @@site_basket ? "" : item.basket.name + " "
    body_should_contain "Results in #{basket_mention}#{zoom_class_plural_humanize(item.class.name).downcase}"

    # We can't use the item title because it can appear several times legitimately.
    body_should_not_contain "item_#{item.id}_wrapper"
  end

  # Redefine the Webrat attach_file method because we repeat actions each time we use it
  # So lets put them in a method that reduces the code needed to get it to work, and then call
  # super passing in the values we generate. Still provide the option to overwrite the mime type
  # incase the one mimetype-fu tries to use is not compatible
  def attach_file(locator, filename, mime_type = nil)
    file_path = File.join(RAILS_ROOT, "test/fixtures/files/#{filename}")
    file = File.open(file_path)
    mime_type = File.mime_type?(file).split(';').first if mime_type.blank?
    file.close
    super(locator, file_path, mime_type)
  end

  # A quick way to attach the appropriate file when adding an item
  def fill_in_needed_information_for(zoom_class)
    case zoom_class
    when 'StillImage'
      attach_file "image_file_uploaded_data", "white.jpg"
    when 'Video'
      attach_file "video[uploaded_data]", "teststrip.mpg", "video/mpeg"
    when 'AudioRecording'
      attach_file "audio_recording[uploaded_data]", "Sin1000Hz.mp3"
    when 'Document'
      attach_file "document[uploaded_data]", "test.pdf"
    when 'WebLink'
      # Because web link needs to be unique, we add a random query param on the end
      fill_in "web_link[url]", :with => "http://google.co.nz/?q=#{rand}"
    end
  end

  # When a test is finished, reset the constants, and remove all users/baskets created, ready for the next test
  # Make sure we also call the super (parent) teardown method so things continue to run properly
  def teardown
    configure_environment do
      require File.expand_path(File.dirname(__FILE__) + "/../system_configuration_constants")
    end
    # at the end of tests, we get rid of all baskets and users created to prevent naming collisions
    @@users_created.each { |user| user.destroy }
    @@users_created = []
    @@baskets_created.each { |basket| basket.destroy }
    @@baskets_created = []
    # we need to ensure at the end of tests that we are left with only the users and baskets we started
    # the tests with. If there are more, they were added outside of the helpers, and this cannot be
    # permitted, or you'll run into unaccounted issues later with basket/login names already existing
    if User.count > @@user_count
      logins = User.all.collect { |user| user.login }
      raise "A user(s) was created outside of the standard helpers. Remaining ones are: #{logins.join(',')}"
    end
    if Basket.count > @@basket_count
      baskets = Basket.all.collect { |basket| basket.urlified_name }
      raise "A basket(s) was created outside of the standard helpers. Remaining ones are: #{baskets.join(',')}"
    end
    super
  end

  # Enables production mode simulation (page caching, template caching, all request are remote)
  # This means we get error 404's when normally we'd get error 500's. We can also test caches are
  # being cleared properly. Also clears the cache
  def enable_production_mode
    Rake::Task['tmp:cache:clear'].execute(ENV)
    ActionController::Base.consider_all_requests_local = false
    ActionController::Base.perform_caching = true
    ActionView::Base.cache_template_loading = true
  end

  # Disables production mode simulation (no page caching, no template caching, all request are local)
  # This means we get error 500's instead of 404's on some pages. We can also test things are working
  # properly before caching. Also clears the cache
  def disable_production_mode
    ActionView::Base.cache_template_loading = false
    ActionController::Base.perform_caching = false
    ActionController::Base.consider_all_requests_local = true
    Rake::Task['tmp:cache:clear'].execute(ENV)
  end

  private

  # We define a variety methods that are created on the fly, to make writing tests easier. These methods include
  #   new_[item_type]    (e.g.  new_audio_recording, a quick method that passes the arguments to <tt>add_item</tt>)
  #   add_[name]_as_[role]_to (e.g. add_bob_as_moderator_to(@@site_basket), a quick way to add users needed for testing)
  #   add_[name]_as_super_user (e.g. add_joe_as_super_user, adds jow as site/tech admin in site basket and admin in all others)
  #   add_[name]_as_regular_user (e.g. add_jane_as_regular_user, adds jane as a member to default baskets (site, help, about, docs))
  #   add_[name] (e.g. add_jill, add jill but without any roles on any baskets)
  # Each user action automatically assigns an instance variable by the same name as the one provided in the method declaration
  # If a method doesn't match any of these, it is passed up to the super (parent) method_missing declaration
  def method_missing(method_sym, *args, &block)
    method_name = method_sym.to_s
    if method_name =~ /^new_(\w+)$/
      # new_topic / new_audio_recording
      # takes basket and a hash of values, plus an optional block
      # provides a more readable option for the <tt>add_item</tt> declaration
      valid_zoom_types = ['topic', 'still_image', 'audio_recording', 'video', 'web_link', 'document']
      unless valid_zoom_types.include?($1)
        raise "ERROR: Invalid item type '#{$1}'. Must be one of #{valid_zoom_types.join(', ')}."
      end
      if block_given?
        new_item(args[0], args[1], args[2], $1.classify, &block)
      else
        new_item(args[0], args[1], args[2], $1.classify)
      end
    elsif method_name =~ /^add_(\w+)_as_(\w+)_to$/
      # add_bob_as_moderator_to(@@site_basket)
      # can take single basket, or an array of them
      baskets = args[0] || []
      args = args[1] || {}
      @user = create_new_user({ :login => $1 }.merge(args))
      baskets = [baskets] unless baskets.kind_of?(Array)
      baskets.each { |basket| @user.has_role($2, basket) }
      @@users_created << @user
      eval("@#{$1} = @user")
    elsif method_name =~ /^add_(\w+)_as_super_user$/
      # add_bob_as_super_user
      args = args[0] || {}
      @user = create_new_user({ :login => $1 }.merge(args))
      @user.has_role('site_admin', @@site_basket)
      @user.has_role('tech_admin', @@site_basket)
      Basket.all(:conditions => ["id != 1"]).each { |basket| @user.has_role('admin', basket) }
      @@users_created << @user
      eval("@#{$1} = @user")
    elsif method_name =~ /^add_(\w+)$/
      # add_bob_as_regular_user
      # add_john
      login = $1
      add_to_baskets = false
      if $1 =~ /^(\w+)_as_regular_user$/
        login = $1
        add_to_baskets = true
      end
      args = args[0] || {}
      @user = create_new_user({ :login => login }.merge(args))
      @user.add_as_member_to_default_baskets if add_to_baskets
      @@users_created << @user
      eval("@#{login} = @user")
    else
      super
    end
  end

  # This method exists in the test/factories.rb file, however, that file does not have access to the
  # @@baskets_created class variable, and so any baskets created using that method don't get deleted at
  # the end. We fix this by redefining the method, calling it's super, and setting what it returns here
  def create_new_basket(options)
    basket = super
    @@baskets_created << basket
    basket
  end

  # This method exists in the test/factories.rb file, however, that file does not have access to the
  # @@users_created class variable, and so any users created using that method don't get deleted at the
  # end. We fix this by redefining the method, calling it's super, and setting what it returns here
  def create_new_user(options)
    user = super
    @@users_created << user
    user
  end

  # Convert a hash of options to webrat actions
  # Current supports:
  #   String    ->  fill_in   (text fields)
  #   TrueClass ->  choose    (radio buttons)
  # If a field isn't supported here, it will raise and exception.
  # The field can still be added via the block syntax that add_item takes
  def get_webrat_actions_from(hash, field_prefix)
    hash.each do |key, value|
      if value.kind_of?(String)
        fill_in "#{field_prefix}_#{key}", :with => value.to_s
      elsif value.is_a?(TrueClass)
        choose "#{field_prefix}_#{key}"
      else
        raise "Don't know what to do with #{key} and value #{value}"
      end
    end
  end

  # Escapes the &, <, >, and " chars
  # Used when you enter content into fields which is saved into a database and then displayed on a page
  # They'll convert to htmlentities on display, so we need to do the same thing since body_should_contain
  # works on page source, not on the generated display
  def escape(text)
    text.gsub(/&/, '&amp;').gsub(/</, '&lt;').gsub(/>/, '&gt;').gsub(/"/, '&quot;')
  end

  # tranform an array of ids
  # to a hash suitable to post as params
  # as if checkboxes of ids with value true
  def item_checkbox_hash_from(*ids)
    item_checkbox_hash = {}
    ids.each { |id| item_checkbox_hash[id.to_s] = "true" }
    item_checkbox_hash
  end

  # directly hit the link_related action
  # assumes relation_candidates are of the same class
  # by doing this through the web interface we trigger all the zebra interaction and cache clearing
  def add_relation_between(topic, zoom_class, *relation_candidate_ids)
    topic = topic.id.to_s if topic.is_a?(Topic)
    item_checkbox_hash = item_checkbox_hash_from(relation_candidate_ids)
    post '/site/search/link_related', :relate_to_item => topic, :relate_to_type => 'Topic', :related_class => zoom_class, :item => item_checkbox_hash
    assert_response :redirect
    # body_should_contain "Successfully added item relationships"
  end

  # shortcut to unlink related items
  def unlink_relation_between(topic, zoom_class, *relation_candidate_ids)
    topic = topic.id.to_s if topic.is_a?(Topic)
    item_checkbox_hash = item_checkbox_hash_from(relation_candidate_ids)
    post '/site/search/unlink_related', :relate_to_item => topic, :relate_to_type => 'Topic', :related_class => zoom_class, :item => item_checkbox_hash
    assert_response :redirect
    # body_should_contain "Successfully removed item relationships."
  end
end
