module Browsery
  module Utils

    # Page object-related helper methods.
    module PageObjectHelper

      # Helper method to instantiate a new page object. This method should only
      # be used when first loading; subsequent page objects are automatically
      # instantiated by calling #cast on the page object.
      #
      # Pass optional parameter Driver, which can be initialized in test and will override the global driver here.
      #
      # @param name [String, Driver]
      # @return [PageObject::Base]
      def page(name, override_driver=nil)
        # Get the fully-qualified class name
        klass_name = "browsery/page_objects/#{name}".camelize
        klass = begin
          klass_name.constantize
        rescue => exc
          msg = ""
          msg << "Cannot find page object '#{name}', "
          msg << "because could not load class '#{klass_name}' "
          msg << "with underlying error:\n  #{exc.class}: #{exc.message}\n"
          msg << exc.backtrace.map { |str| "    #{str}" }.join("\n")
          raise NameError, msg
        end

        # Get a default connector
        @driver = Browsery::Connector.get_default if override_driver.nil?
        @driver = override_driver if !override_driver.nil?
        instance = klass.new(@driver)

        # Set SauceLabs session(job) name to test's name if running on Saucelabs
        begin
          update_sauce_session_name if connector_is_saucelabs? && !@driver.nil?
        rescue
          self.logger.debug "Failed setting saucelabs session name for #{name()}"
        end

        # Before visiting the page, do any pre-processing necessary, if any,
        # but only visit the page if the pre-processing succeeds
        if block_given?
          retval = yield instance
          instance.go! if retval
        else
          instance.go! if override_driver.nil?
        end

        # similar like casting a page, necessary to validate some element on a page
        begin
          instance.validate!
        rescue Minitest::Assertion => exc
          raise Browsery::PageObjects::InvalidePageState, "#{klass}: #{exc.message}"
        end

        # Return the instance as-is
        instance
      end

      # Local teardown for page objects. Any page objects that are loaded will
      # be finalized upon teardown.
      #
      # @return [void]
      def teardown
        if !passed? && !skipped? && !@driver.nil?
          json_save_to_ever_failed if Browsery.settings.rerun_failure
          print_sauce_link if connector_is_saucelabs?
          take_screenshot
        end
        begin
          update_sauce_session_status if connector_is_saucelabs? && !@driver.nil? && !skipped?
        rescue
          self.logger.debug "Failed setting saucelabs session status for #{name()}"
        end

        Browsery::Connector.finalize!
        super
      end

      # Take screenshot and save as png with test name as file name
      def take_screenshot
        @driver.save_screenshot("logs/#{name}.png")
      end

      # Create new/override same file ever_failed_tests.json with fail count
      def json_save_to_ever_failed
        ever_failed_tests = 'logs/tap_results/ever_failed_tests.json'
        data_hash = {}
        if File.file?(ever_failed_tests) && !File.zero?(ever_failed_tests)
          data_hash = JSON.parse(File.read(ever_failed_tests))
        end

        if data_hash[name]
          data_hash[name]["fail_count"] += 1
        else
          data_hash[name] = { "fail_count" => 1 }
        end
        begin
          data_hash[name]["last_fail_on_sauce"] = "saucelabs.com/tests/#{@driver.session_id}"
        rescue
          self.logger.debug "Failed setting last_fail_on_sauce, driver may not be available"
        end

        File.open(ever_failed_tests, 'w+') do |file|
          file.write JSON.pretty_generate(data_hash)
        end
      end

      # Print out a link of a saucelabs's job when a test is not passed
      # Rescue to skip this step for tests like cube tracking
      def print_sauce_link
        begin
          puts "Find test on saucelabs: https://saucelabs.com/tests/#{@driver.session_id}"
        rescue
          puts 'can not retrieve driver session id, no link to saucelabs'
        end
      end

      # Update SauceLabs session(job) name and build number/name
      def update_sauce_session_name
        http_auth = Browsery.settings.sauce_session_http_auth(@driver)
        body = { 'name' => name() }
        unless (build_number = ENV['JENKINS_BUILD_NUMBER']).nil?
          body['build'] = build_number
        end
        RestClient.put(http_auth, body.to_json, {:content_type => "application/json"})
      end

      # Update session(job) status if test is not skipped
      def update_sauce_session_status
        http_auth = Browsery.settings.sauce_session_http_auth(@driver)
        body = { "passed" => passed? }
        RestClient.put(http_auth, body.to_json, {:content_type => "application/json"})
      end

      def connector_is_saucelabs?
        Browsery.settings.connector.include? 'saucelabs'
      end

      # Generic page object helper method to clear and send keys to a web element found by driver
      # @param [Element, String]
      def put_value(web_element, value)
        web_element.clear
        web_element.send_keys(value)
      end

      # Helper method for retrieving value from yml file
      # todo should be moved to FileHelper.rb once we created this file in utils
      # @param [String, String]
      # keys, eg. "timeouts:implicit_wait"
      def read_yml(file_name, keys)
        data = Hash.new
        begin
          data = YAML.load_file "#{file_name}"
        rescue
          raise Exception, "File #{file_name} doesn't exist" unless File.exist?(file_name)
        rescue
          raise YAMLErrors, "Failed to load #{file_name}"
        end
        keys_array = keys.split(/:/)
        value = data
        keys_array.each do |key|
          value = value[key]
        end
        value
      end

      # Retry a block of code for a number of times
      def retry_with_count(count, &block)
        try = 0
        count.times do
          try += 1
          begin
            block.call
            return true
          rescue Exception => e
            Browsery.logger.warn "Exception: #{e}\nfrom\n#{block.source_location.join(':')}"
            Browsery.logger.warn "Retrying" if try < count
          end
        end
      end

      def with_url_change_wait(&block)
        starting_url = @driver.current_url
        block.call
        wait(timeout: 15, message: 'Timeout waiting for URL to change')
          .until { @driver.current_url != starting_url }
      end

      # Check if a web element exists on page or not, without wait
      def is_element_present?(how, what, driver = nil)
        element_appeared?(how, what, driver)
      end

      # Check if a web element exists and displayed on page or not, without wait
      def is_element_present_and_displayed?(how, what, driver = nil)
        element_appeared?(how, what, driver, check_display = true)
      end

      def wait_for_element_to_display(how, what, friendly_name = "element")
          wait(timeout: 15, message: "Timeout waiting for #{friendly_name} to display")
            .until {is_element_present_and_displayed?(how, what)}
      end

      def wait_for_element_to_be_present(how, what, friendly_name = "element")
        wait(timeout: 15, message: "Timeout waiting for #{friendly_name} to be present")
          .until {is_element_present?(how, what)}
      end

      # Useful when you want to wait for the status of an element attribute to change
      # Example: the class attribute of <body> changes to include 'logged-in' when a user signs in to rent.com
      # Example usage: wait_for_attribute_status_change(:css, 'body', 'class', 'logged-in', 'sign in')
      def wait_for_attribute_to_have_value(how, what, attribute, value, friendly_name = "attribute")
        wait(timeout: 15, message: "Timeout waiting for #{friendly_name} status to update")
          .until { driver.find_element(how, what).attribute(attribute).include?(value) rescue retry }
      end

      def current_page(calling_page)
        calling_page.class.to_s.split('::').last.downcase
      end

      private

      # @param  eg. (:css, 'button.cancel') or (*BUTTON_SUBMIT_SEARCH)
      # @param  also has an optional parameter-driver, which can be @element when calling this method in a widget object
      # @return [boolean]
      def element_appeared?(how, what, driver = nil, check_display = false)
        original_timeout = read_yml("config/browsery/connectors/saucelabs.yml", "timeouts:implicit_wait")
        @driver.manage.timeouts.implicit_wait = 0
        result = false
        parent_element = @driver if driver == nil
        parent_element = driver if driver != nil
        elements = parent_element.find_elements(how, what)
        if check_display
          begin
            result = true if elements.size() > 0 && elements[0].displayed?
          rescue
            result = false
          end
        else
          result = true if elements.size() > 0
        end
        @driver.manage.timeouts.implicit_wait = original_timeout
        result
      end

      # Method that overrides click to send the enter key to the element if the current browser
      # is internet explorer. Used when sending the enter key to the element will work
      def browser_safe_click(element)
        driver.browser == :internet_explorer ? element.send_keys(:enter) : element.click
      end

      # Method that overrides click to send the space key to the checkbox if the current browser
      # is internet explorer. Used when sending the space key to the checkbox will work
      def browser_safe_checkbox_click(element)
        (driver.browser == :internet_explorer || driver.browser == :firefox) ? element.send_keys(:space) : element.click
      end

    end

  end
end