require 'delegate'
require 'kookaburra/exceptions'
require 'kookaburra/assertion'
require 'kookaburra/ui_driver/has_ui_components'
require 'kookaburra/ui_driver/scoped_browser'

class Kookaburra
  class UIDriver
    # UIComponent is intended to be subclassed to represent each component of
    # your application-under-test's user interface. The purpose of the
    # UIComponent object is to abstract away the implementation details of your
    # interface when testing and allow you to concentrate on testing your
    # business requirements. For instance, a UIComponent subclass for your
    # sign-up form might have accessors for the individual fields as well as
    # methods that allow you to perform distinct operations:
    #
    # @example SignUpForm component
    #   class SignUpForm < Kookaburra::UIDriver::UIComponent
    #     def component_path
    #       '/signup'
    #     end
    #
    #     # If it can't be inferred from the class name
    #     def component_locator
    #       '#user-sign-up'
    #     end
    #
    #     def email
    #       find('#user_email').value
    #     end
    #
    #     def email=(new_email)
    #       fill_in 'user_email', :with => new_email
    #     end
    #
    #     def password
    #       find('#user_password').value
    #     end
    #
    #     def password=(new_password)
    #       fill_in 'user_password', :with => new_password
    #     end
    #
    #     def password_confirmation
    #       find('#user_password_confirmation').value
    #     end
    #
    #     def password_confirmation=(new_password_confirmation)
    #       fill_in 'user_password_confirmation', :with => new_password_confirmation
    #     end
    #
    #     def submit
    #       click_button 'Sign Up'
    #     end
    #
    #     def sign_up(data = {})
    #       self.email = data[:email]
    #       self.password = data[:password]
    #       self.password_confirmation = data[:password_confirmation]
    #       submit
    #     end
    #   end
    #
    # Note that the "browser operation" methods such as {#fill_in} and
    # {#click_button} are delegated to a {ScopedBrowser} and are
    # automatically scoped to the component's DOM element.
    #
    # @note Even though a {UIComponent} should respond to all of the
    #   methods on the browser (i.e. all of the Capybara DSL methods),
    #   for some reason call to {#select} get routed to {Kernel#select}.
    #   You can get around this by calling it as `self.select`. See
    #   https://gist.github.com/3192103 for an example of this behavior.
    #
    # @abstract Unless you override the default implementation of {#url}, you
    #   must override the {#component_path} method if you want the component to
    #   be navigable by the {Kookaburra::UIDriver::UIComponent::AddressBar}
    #   component.
    class UIComponent < SimpleDelegator
      include Assertion
      extend HasUIComponents

      # The {Kookaburra::Configuration} with which the component
      # instance was instantiated.
      attr_reader :configuration

      # The options Hash with which the component instance was
      # instantiated.
      attr_reader :options

      # New UIComponent instances are typically created for you by your
      # {Kookaburra::UIDriver} instance.
      #
      # @see Kookaburra::UIDriver.ui_component
      #
      # @param [Kookaburra::Configuration] configuration
      # @param [Hash] options An options hash that can be used to
      #   further configure a {UIComponent}'s behavior.
      def initialize(configuration, options = {})
        @configuration = configuration
        @options = options
        @browser = configuration.browser
        @app_host = configuration.app_host
        @server_error_detection = configuration.server_error_detection
        scoped_browser = ScopedBrowser.new(@browser, lambda { component_locator })
        super(scoped_browser)
      end

      # Is the component's element found on the page and is it considered
      # "visible" by the browser driver?
      def visible?
        visible = browser.has_css?(component_locator, visible: true)
        unless visible
          detect_server_error!
        end
        visible
      end

      # The opposite of {#visible?}
      #
      # @note This does not check for a server error.
      def not_visible?
        browser.has_no_css?(component_locator, visible: true)
      end

      # Returns the full URL by appending {#component_path} to the value of the
      # {Kookaburra::Configuration#app_host} from the initialized configuration.
      def url(*args)
        "#{@app_host}#{component_path(*args)}"
      end

      protected

      # The browser object from the initialized configuration
      attr_reader :browser

      # @abstract
      # @return [String] the URL path that should be loaded in order to reach this component
      # @raise [Kookaburra::ConfigurationError] raised if you haven't provided
      #   an implementation
      def component_path
        raise ConfigurationError, "You must define #{self.class.name}#component_path."
      end

      # The CSS3 selector that will find the element in the DOM
      #
      # Defaults to a "#" followed by the snake-cased (underscored) version of
      # the class name with '/' replaced by '-'. Override this method in your
      # subclasses if you need a different CSS3 selector to find your component.
      #
      # @example
      #   class My::Awesome::ComponentThingy < Kookaburra::UIDriver::UIComponent
      #   end
      #
      #   x = My::Awesome::ComponentThingy.allocate
      #   x.send(:component_locator)
      #   #=> '#my-awesome-component_thingy'
      #
      # @return [String]
      def component_locator
        "#" + self.class.name.gsub(/::/, '/').
          gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
          gsub(/([a-z\d])([A-Z])/,'\1_\2').
          tr("-", "_").
          gsub('/', '-').
          downcase
      end

      # Runs the server error detection function specified in
      # {Kookaburra::Configuration#server_error_detection}.
      #
      # It's a noop if no server error detection was specified.
      #
      # @raise [UnexpectedResponse] raised if the server error detection
      #   function returns true
      def detect_server_error!
        return if @server_error_detection.nil?
        if @server_error_detection.call(browser)
          raise UnexpectedResponse, "Server Error Detected:\n#{browser.text}"
        end
      end

      protected

      # Provides a reference to the HTML element represented by this UIComponent
      #
      # This is useful for getting at attributes of the current element, because
      # the normal find methods are scoped to run *inside* this element.
      #
      # @return Capybara::Element
      def this_element
        browser.find(component_locator)
      end

      private

      # As of Ruby 2.1.0, 'SimpleDelegator' delegates the '#raise' method to the
      # underlying object. Since our underlying object is probably a
      # 'BasicObject' that doesn't define '#raise', things get confusing as we
      # end up in a maze of '#method_missing' BS. This fixes it.
      def raise(*args)
        Kernel.raise(*args)
      end
    end
  end
end