require 'rubygems'
require 'activesupport'
require 'taza/settings'
require 'spec/matchers'

module Taza
  # An abstraction of a website, but more really a container for a sites pages.
  #
  # You can generate a site by performing the following command:
  #   $ ./script/generate site google
  #
  # This will generate a site file for google, a flows folder, and a pages folder in lib
  #
  # Example:
  #
  #   require 'taza'
  #
  #   class Google < Taza::Site
  #
  #   end
  class Site
    
    # These methods are available for user contexts, including Site itself
    module Methods
      attr_accessor :browser, :site
      
      # Return an instance of the specified page. The name
      # Given should be the human-form of the page, without the
      # "page" suffix.
      # If a block is given, it yields to the page.
      def page(page_name, &block)
        method_name = page_name.computerize + '_page'
        send method_name, &block
      end

      # Send the browser to a url, relative to the site origin.
      def goto relative_url
        destination = File.join(@site.origin, relative_url) 
        @browser.goto destination
      end
      
      def pages
        @pages || @site.pages
      end

    end
    
    @@before_browser_closes = Proc.new() {}
    # Use this to do something with the browser before it closes, but note that it is a class method which
    # means that this will get called for any instance of a site.
    #
    # Here's an example of how you might use it to print the DOM output of a browser before it closes:
    #
    #   Taza::Site.before_browser_closes do |browser|
    #     puts browser.html
    #   end
    def self.before_browser_closes(&block)
      @@before_browser_closes = block
    end

    # Site
    attr_accessor :methods_module

    # A site can be called a few different ways
    #
    # The following example creates a new browser object and closes it:
    #  Google.new do
    #    google.search.set "taza"
    #    google.submit.click
    #  end
    #
    # This example will create a browser object but not close it:
    #  Google.new.search.set "taza"
    #
    # Sites can take a couple of parameters in the constructor:
    #   :browser => a browser object to act on instead of creating one automatically
    # (not sure if this is a useful feature or not)
    def initialize(params={}, &block)
      @site = self
      @parent_module = self.class.parent
      @module_name = @parent_module.to_s
      @class_name  = self.class.to_s.split("::").last

      define_flows
      load_methods_files
      
      page_loader = PageLoader.new(@module_name, pages_path)
      @pages = page_loader.page_names
      @methods_module = page_loader.page_methods
      @methods_module.send(:include, Methods)
      @methods_module.send(:include, Spec::Matchers)
      @methods_module.send(:include, @parent_module::Methods) if defined?(@parent_module::Methods)
      self.extend(@methods_module)

      @browser = params[:browser]
      initialize_browser

      execute_block_and_close_browser(&block) if block_given?
    end
    
    # Initializes browser based on configuration settings.
    def initialize_browser
      if @browser
        @leave_open = true
      else
        @browser = Browser.create(config)
      end
      @browser.goto origin
      bring_to_front_if_appropriate
      @leave_open ||= config[:leave_open]
    end

    def config
      Settings.config(@class_name)
    end
    
    # The base url of the site. This is configured in environments.yml.
    def origin
      config[:url]
    end

    def bring_to_front_if_appropriate
      return unless config[:bring_to_front]
      return unless config[:browser] == 'ie' 
      @browser.bring_to_front
    end

    def execute_block_and_close_browser
      begin
        yield self
      rescue => site_block_exception
      ensure
        begin
          close
        rescue => close_exception
          ""
        end
        exception = site_block_exception || close_exception
        raise exception if exception
      end
    end
    private :execute_block_and_close_browser

    def close
      begin
        @@before_browser_closes.call(@browser)
      rescue => before_browser_closes_block_exception
        "" # so basically rcov has a bug where it would insist this block is uncovered when empty
      end
      begin
        @browser.close unless @leave_open
      ensure
        raise before_browser_closes_block_exception if before_browser_closes_block_exception
      end
    end

    # Return a context that supports the "watircraft" commands. 
    # Currently used by the script\console
    def execution_context
      initialize_context!(Object.new)
    end
    
    def initialize_context!(context)
      context.extend @methods_module
      context.site = @site
      context.browser = @site.browser
      context
    end
    
    private
    def pages_path # :nodoc:
      File.join(path,'pages','**','*.rb') # does this need to include partials?
    end

    def define_flows # :nodoc:
      Dir.glob(flows_path) do |file|
        require file
      end
    end
    
    def load_methods_files # :nodoc:
      Dir.glob(methods_path) do |file|
        require file
      end      
    end

    def flows_path # :nodoc:
      File.join(path,'flows','*.rb')
    end
    
    def methods_path # :nodoc:
      File.join(path,'methods','*.rb')
    end

    def path # :nodoc:
      File.join(base_path,'lib')
    end

    def base_path # :nodoc:
      APP_ROOT
    end
    
    class PageLoader
      attr_reader :page_methods, :page_names
      def initialize site_module, pages_path
        @site_module = site_module
        @pages_path = pages_path
        @page_methods = Module.new
        @page_names = []
        define_site_pages
      end
      private
      def define_site_pages # :nodoc:
        Dir.glob(@pages_path) do |file|
          require file
          page_name = File.basename(file,'.rb')
          @page_names << page_name
          page_class = "#{@site_module}::#{page_name.camelize}"
          @page_methods.module_eval <<-EOS
          def #{page_name}
            page = #{page_class}.new
            page.browser = @browser
            page.site = @site
            yield page if block_given?
            page
          end
          EOS
        end
      end
    end
  end
end
