require "rubygems"
require "hpricot"
require "English"

module Webrat
  class Page
    include Logging

    attr_reader :session
    attr_reader :url
    
    def initialize(session, url = nil, method = :get, data = {})
      @session  = session
      @url      = url
      @method   = method
      @data     = data

      reset_dom
      reloads if @url
      
      session.current_page = self
    end
    
    # Verifies an input field or textarea exists on the current page, and stores a value for
    # it which will be sent when the form is submitted.
    #
    # Examples:
    #   fills_in "Email", :with => "user@example.com"
    #   fills_in "user[email]", :with => "user@example.com"
    #
    # The field value is required, and must be specified in <tt>options[:with]</tt>.
    # <tt>field</tt> can be either the value of a name attribute (i.e. <tt>user[email]</tt>)
    # or the text inside a <tt><label></tt> element that points at the <tt><input></tt> field.
    def fills_in(id_or_name_or_label, options = {})
      field = find_field(id_or_name_or_label, TextField, TextareaField, PasswordField)
      field.set(options[:with])
    end

    # Verifies that an input checkbox exists on the current page and marks it
    # as checked, so that the value will be submitted with the form.
    #
    # Example:
    #   checks 'Remember Me'
    def checks(id_or_name_or_label)
      field = find_field(id_or_name_or_label, CheckboxField)
      field.check
    end

    # Verifies that an input checkbox exists on the current page and marks it
    # as unchecked, so that the value will not be submitted with the form.
    #
    # Example:
    #   unchecks 'Remember Me'
    def unchecks(id_or_name_or_label)
      field = find_field(id_or_name_or_label, CheckboxField)
      field.uncheck
    end

    # Verifies that an input radio button exists on the current page and marks it
    # as checked, so that the value will be submitted with the form.
    #
    # Example:
    #   chooses 'First Option'
    def chooses(label)
      field = find_field(label, RadioField)
      field.choose
    end

    # Verifies that a an option element exists on the current page with the specified
    # text. You can optionally restrict the search to a specific select list by
    # assigning <tt>options[:from]</tt> the value of the select list's name or
    # a label. Stores the option's value to be sent when the form is submitted.
    #
    # Examples:
    #   selects "January"
    #   selects "February", :from => "event_month"
    #   selects "February", :from => "Event Month"
    def selects(option_text, options = {})
      id_or_name_or_label = options[:from]
      
      if id_or_name_or_label
        field = find_field(id_or_name_or_label, SelectField)
        option = field.find_option(option_text)
      else
        option = find_select_option(option_text)
      end
        
      flunk("Could not find option #{option_text.inspect}") if option.nil?
      option.choose
    end

    # Verifies that an input file field exists on the current page and sets
    # its value to the given +file+, so that the file will be uploaded
    # along with the form.
    #
    # Example:
    #   attaches_file "Photo", "/path/to/the/photo.jpg"
    def attaches_file(id_or_name_or_label, path)
      field = find_field(id_or_name_or_label, FileField)
      field.set(path)
    end

    # Saves the page out to RAILS_ROOT/tmp/ and opens it in the default
    # web browser if on OS X. Useful for debugging.
    # 
    # Example:
    #   save_and_open
    def save_and_open
      return unless File.exist?(session.saved_page_dir)

      filename = "#{session.saved_page_dir}/webrat-#{Time.now.to_i}.html"
      
      File.open(filename, "w") do |f|
        f.write rewrite_css_and_image_references(session.response_body)
      end

      open_in_browser(filename)
    end
    
    def open_in_browser(path) # :nodoc
      `open #{path}`
    end

    # Issues a request for the URL pointed to by a link on the current page,
    # follows any redirects, and verifies the final page load was successful.
    #
    # clicks_link has very basic support for detecting Rails-generated 
    # JavaScript onclick handlers for PUT, POST and DELETE links, as well as
    # CSRF authenticity tokens if they are present.
    #
    # Example:
    #   clicks_link "Sign up"
    def clicks_link(link_text)
      link = find_link(link_text)
      link.click
    end

    # Works like clicks_link, but only looks for the link text within a given selector
    # 
    # Example:
    #   clicks_link_within "#user_12", "Vote"
    def clicks_link_within(selector, link_text)
      link = find_link(link_text, selector)
      link.click
    end

    # Works like clicks_link, but forces a GET request
    # 
    # Example:
    #   clicks_get_link "Log out"
    def clicks_get_link(link_text)
      link = find_link(link_text)
      link.click(:get)
    end

    # Works like clicks_link, but issues a DELETE request instead of a GET
    # 
    # Example:
    #   clicks_delete_link "Log out"
    def clicks_delete_link(link_text)
      link = find_link(link_text)
      link.click(:delete)
    end

    # Works like clicks_link, but issues a POST request instead of a GET
    # 
    # Example:
    #   clicks_post_link "Vote"
    def clicks_post_link(link_text)
      link = find_link(link_text)
      link.click(:post)
    end

    # Works like clicks_link, but issues a PUT request instead of a GET
    # 
    # Example:
    #   clicks_put_link "Update profile"
    def clicks_put_link(link_text)
      link = find_link(link_text)
      link.click(:put)
    end

    # Verifies that a submit button exists for the form, then submits the form, follows
    # any redirects, and verifies the final page was successful.
    #
    # Example:
    #   clicks_button "Login"
    #   clicks_button
    #
    # The URL and HTTP method for the form submission are automatically read from the
    # <tt>action</tt> and <tt>method</tt> attributes of the <tt><form></tt> element.
    def clicks_button(value = nil)
      button = nil
      
      forms.each do |form|
        button = form.find_button(value)
        break if button
      end

      flunk("Could not find button #{value.inspect}") if button.nil?
      button.click
    end

    # Reloads the last page requested. Note that this will resubmit forms
    # and their data.
    #
    # Example:
    #   reloads
    def reloads
      request_page(@url, @method, @data)
    end

    def submits_form(form_id = nil) # :nodoc:
    end

  protected
    
    def find_select_option(option_text)
      forms.each do |form|
        result = form.find_select_option(option_text)
        return result if result
      end
      
      nil
    end
  
    
    def find_link(text, selector = nil)
      matching_links = []
      
      links_within(selector).each do |possible_link|
        matching_links << possible_link if possible_link.matches_text?(text)
      end
      
      if matching_links.any?
        matching_links.sort_by { |l| l.text.length }.first
      else
        flunk("Could not find link with text #{text.inspect}")
      end
    end
    
    def find_field(id_or_name_or_label, *field_types)
      forms.each do |form|
        result = form.find_field(id_or_name_or_label, *field_types)
        return result if result
      end
      
      flunk("Could not find #{field_types.inspect}: #{id_or_name_or_label.inspect}")
    end
    
    def request_page(url, method, data)
      debug_log "REQUESTING PAGE: #{method.to_s.upcase} #{url} with #{data.inspect}"
      
      session.send "#{method}", url, data || {}

      if session.response_body =~ /Exception caught/ || session.response_body.blank? 
        save_and_open
      end

      flunk("Page load was not successful (Code: #{session.response_code.inspect})") unless (200..299).include?(session.response_code)
      reset_dom
    end
    
    def reset_dom
      @dom    = nil
      @forms  = nil
    end
    
    def links_within(selector)
      (dom / selector / "a[@href]").map do |link_element|
        Link.new(self, link_element)
      end
    end
    
    def forms
      return @forms if @forms
      
      @forms = (dom / "form").map do |form_element|
        Form.new(self, form_element)
      end
    end
      
    def dom # :nodoc:
      return @dom if defined?(@dom) && @dom
      flunk("You must visit a path before working with the page.") unless @session.response_code
      @dom = Hpricot(@session.response_body)
    end
    
    def flunk(message)
      raise message
    end
    
    def rewrite_css_and_image_references(response_html) # :nodoc
      return response_html unless session.doc_root
      response_html.gsub(/"\/(stylesheets|images)/, session.doc_root + '/\1')
    end
    
  end
end