module Howitzer
  module Web
    # This module combines page dsl methods
    module PageDsl
      # This class is for private usage only
      class PageScope
        include RSpec::Matchers
        include RSpec::Wait

        def initialize(page_klass, &block)
          self.page_klass = page_klass
          self.outer_context = eval('self', block.binding) if block.present?
          instance_eval(&block)
        end

        # Makes current page as a subject for Rspec expectations
        # @example
        #  HomePage.on { expect(HomePage.given).to have_menu_section } # Bad
        #  HomePage.on { is_expected.to have_menu_section } # Good

        def is_expected # rubocop:disable Naming/PredicateName
          expect(page_klass.given)
        end

        # Proxies all methods to a page instance.
        #
        # @note There are some exceptions:
        #   * Methods with `be_` and `have_` prefixes are excluded
        #   * `out` method extracts an instance variable from an original context if starts from @.
        #    Otherwise it executes a method from an original context

        def method_missing(name, *args, **options, &block)
          return super if name =~ /\A(?:be|have)_/
          return eval_in_out_context(*args, **options, &block) if name == :out

          if options.present?
            page_klass.given.send(name, *args, **options, &block)
          else
            page_klass.given.send(name, *args, &block)
          end
        end

        # Makes proxied methods to be evaludated and returned as a proc
        # @see #method_missing

        def respond_to_missing?(name, include_private = false)
          name !~ /\A(?:be|have)_/ || super
        end

        private

        def eval_in_out_context(*args, **options, &block)
          return nil if args.size.zero?

          name = args.shift
          return get_outer_instance_variable(name) if name.to_s.start_with?('@')

          if options.present?
            outer_context.send(name, *args, **options, &block)
          else
            outer_context.send(name, *args, &block)
          end
        end

        def get_outer_instance_variable(name)
          outer_context.instance_variable_get(name)
        end

        attr_accessor :page_klass, :outer_context
      end

      def self.included(base) # :nodoc:
        base.extend(ClassMethods)
      end

      # This module holds page dsl class methods
      module ClassMethods
        # Allows to execute page methods in context of the page.
        # @note It additionally checks the page is really displayed
        #   on each method call, otherwise it raises error
        # @example Standard case
        #   LoginPage.open
        #   LoginPage.on do
        #     fill_form(name: 'John', email: 'jkarpensky@gmail.com')
        #     submit_form
        #   end
        # @example More complex case with outer context
        #   @name = 'John'
        #   def email(domain = 'gmail.com')
        #     "jkarpensky@#{domain}"
        #   end
        #   LoginPage.open
        #   LoginPage.on do
        #     fill_form(name: out(:@name), email: out(:email, 'yahoo.com'))
        #     submit_form
        #   end

        def on(&block)
          PageScope.new(self, &block)
          nil
        end
      end
    end
  end
end
