require './core/utils/bmp_utils'
require './core/base/page_helper'
require './core/data/spec_data'
require './core/utils/logger_utils'
require './core/configs/driver_config'

module GridConfig
  # Selenium Grid Configuration
  include BmpUtils
  include LoggerUtils

  # Points the Selenium Driver to a remote grid
  def point_to_grid
    if ENV['GRID_HOST'] == 'dev'
      # Dev Environment for Grid
      host = 'cbg-dev.internal.justin.tv:443/wd/hub'
      url = "https://#{SpecData.grid_username}:#{SpecData.grid_access_key}@#{host}"
    elsif saucelabs?
      url = "http://#{SpecData.saucelabs_user}:#{SpecData.saucelabs_key}@ondemand.saucelabs.com:80/wd/hub"
    else
      host = 'cbg.internal.justin.tv:443/wd/hub'
      url = "https://#{SpecData.grid_username}:#{SpecData.grid_access_key}@#{host}"
    end

    # Register Driver To The Grid
    Capybara.register_driver :selenium do |app|
      # Configure some of the timeout settings
      client = Selenium::WebDriver::Remote::Http::Default.new

      client.read_timeout = 120 # Seconds. Sets for the Selenium Remote Driver.
      # Timeout configuration is also done in spec_helper.rb

      Capybara::Selenium::Driver.new(app,
        :browser => :remote,
        :url => url,
        :desired_capabilities => set_capabilities(
          SpecData.browser,
          SpecData.browser_version,
          SpecData.os_platform,
          SpecData.os_version,
          SpecData.screen_resolution
        ),
        :http_client => client)
    end

    connect_to_grid

    puts "Running on: #{SpecData.os_platform} - #{SpecData.browser}"
    # Grab and Output Proxy Information if avaliable.
    output_proxy_information
  end

  # Establishes connection to Grid
  # @param [Int] timeout The duration to timeout when connecting to Grid, in seconds
  # @param [Int] wait_amount The duration to wait in between retries, in seconds
  def connect_to_grid(timeout=500, wait_amount=15) # TODO: Rename this to simply connect when it's apart of a class. Afraid of namespace collision with module.
    start_time = Time.now

    begin
      Capybara.current_session.driver.browser # Establish a connection
    rescue *grid_connection_exceptions => e
      puts "[WARN] When connecting to Grid, encountered #{e.class}: #{e.message}"
      sleep(wait_amount)
      if retry_connection?(start_time, timeout)
        retry
      else
        raise e
      end
    end

    DriverConfig.set_timeouts
    # Print to console grid host information.
    display_connection
  end

  # @param [Time] start_time The time at which tests began
  # @param [Int] timeout The duration at which tests should time out, in seconds
  # @return [Boolean] Whether or not the connection should be retried, or if the max timeout has been reached
  def retry_connection?(start_time, timeout)
    time_since_initiated = (Time.now - start_time)
    if time_since_initiated < timeout
      puts "[INFO] Trying to connect to Grid again (will timeout in roughly #{(timeout - time_since_initiated).round} seconds)"
      return true
    else
      puts "[ERROR] Unable to connect to Grid after #{time_since_initiated.round} seconds."
      return false
    end
  end

  # Disconnects and Reconnects to Selenium Grid
  def reconnect_to_grid
    Capybara.current_session.driver.quit
    connect_to_grid

    PageHelper.set_browser_window_to_default_size
    return true
  end

  # Sets the capabilities for a test to run within the grid
  # @param browser [String] Supports Chrome or Firefox. Defaults to Firefox.
  # @return [Selenium::WebDriver::Remote::Capabilities] Object for capabilities to pass to the grid
  def set_capabilities(browser, browser_version, os_platform, os_version, screen_resolution)
    # Default Params if passed as nil
    browser ||= 'firefox'
    browser = browser_formatter(browser)

    os_platform ||= 'LINUX' # Default to Linux
    caps = nil

    if browser == 'chrome'
      caps = create_chrome_specific_capabilities
    else
      caps = Selenium::WebDriver::Remote::Capabilities.new
    end

    caps[:proxy] = create_webdriver_proxy if bmp_enabled? || ENV['ANALYTICS'] == 'true'
    caps[:logging_prefs] = {:browser => "ALL"}

    # Specific capabilities for SauceLabs
    if saucelabs?
      caps['platform'] = os_platform + ' ' + os_version
      caps['browserName'] = browser

      if browser_version_set?(browser_version)
        caps['version'] = browser_version
      end

      caps[:proxy] = nil # Override Proxy, it's not necessary on Sauce. Temporary, should better handle this
      caps[:screenResolution] = screen_resolution if screen_resolution
      caps[:tunnelIdentifier] = SpecData.sauce_tunnel_id unless SpecData.sauce_tunnel_id.nil?
      caps[:seleniumVersion] = '3.3.1' # Temporarily hardcoded for Sauce Labs Support
      caps[:maxDuration] = '3600' # 60 Minutes Max Duration of a Job
      caps[:idleTimeout] = '300' # 2.5 Minutes between commands - ideal for chat scenarios
      caps[:javascript_enabled] = true
    else
      caps[:browser_name] = browser
      caps[:version] = browser_version if browser_version_set?(browser_version)
      caps = set_firefox_caps(caps, browser, browser_version)

      if legacy_platform?(os_platform)
        caps[:platform] = os_platform
      else
        caps['osPlatform'] = os_platform unless TwitchUtils.nil_or_empty?(os_platform)
        caps['osVersion'] = os_version unless TwitchUtils.nil_or_empty?(os_version)
      end
    end

    return caps
  end

  # Sets capabilities specific for Firefox
  # @param caps [Selenium::WebDriver::Remote::Capabilities] The capabilities
  # @param browser [String] The browser being used
  # @param browser_version [Int] The browser version being used
  # @return [Selenium::WebDriver::Remote::Capabilities] Capabilities with Firefox specific caps
  def set_firefox_caps(caps, browser, browser_version)
    return caps if browser.downcase != 'firefox' # Return if not firefox, no need for marionette
    caps = configure_marionette(caps, browser, browser_version)
    caps = set_cert_cap(caps, browser)
    return caps
  end

  # Firefox by default does not trust insecure certs. BMP can cause this to occur.
  # This method will instruct Firefox to accept insecure certs if necessary
  # @param caps [Selenium::WebDriver::Remote::Capabilities] The capabilities
  # @param browser [String] The browser being used
  # @return [Selenium::WebDriver::Remote::Capabilities] Capabilities with the insecure cert capability if necessary
  def set_cert_cap(caps, browser)
    caps[:acceptInsecureCerts] = true if browser.downcase == 'firefox'
    return caps
  end

  # Temporary to determine if using Legacy Platform Names. This includes WIN10
  # Going to attempt to get platform name moved to "Windows"
  # @param [String] platformName The platform name to check
  # @return [Boolean] If it's a legacy platform name
  def legacy_platform?(platformName)
    case platformName.downcase
      when 'win10'
        return true
      else
        return false
    end
  end

  # Determines if the browser version was set
  # If the properties config read an empty version, it will be nil.
  # @param version [Int] The browser version
  # @return [Boolean] Whether the version was set or not
  def browser_version_set?(version)
    return !(version.nil?)
  end

  # If the browser is Firefox, and BrowserVersion is specified, add the marionette capability.
  # Otherwise, don't provide it and Grid will do the best to match.
  # @param caps [Selenium::WebDriver::Remote::Capabilities] The capabilities
  # @param browser [String] The browser being used
  # @param browser_version [Int] The browser version being used
  # @return [Selenium::WebDriver::Remote::Capabilities] Capabilities with the marionette capability if necessary
  def configure_marionette(caps, browser, browser_version)
    return caps if browser.downcase != 'firefox' # Return if not firefox, no need for marionette
    caps[:marionette] = use_marionette?(browser_version)
    return caps
  end

  # Determines if Marionette should be used or not, based on the browser version.
  # Marionette is used only if the version is greater than 47
  # @param browser_version [String] The browser version being used. If not set, defaults to true.
  # @return [Boolean] Whether Marionette should be used or not
  def use_marionette?(browser_version)
    return true unless browser_version_set?(browser_version) # Return true if browser version was not set - DEFAULTLY USE MARIONETTE
    return browser_version.to_i > 47
  end

  def create_chrome_specific_capabilities
    return Selenium::WebDriver::Remote::Capabilities.chrome(
      # Enables flash playback in chrome
      # https://support.google.com/chrome/answer/3123708
      'chromeOptions' => {
        'prefs' => {
          'profile' => {
            'content_settings' => {
              'exceptions' => {
                'plugins' => {
                  'https://*,*' => {
                    'last_modified' => "0",
                    'setting' => 1
                  }
                }
              }
            }
          }
        }
      }
    )
  end

  # Converts known browsers to a Selenium Capability recognized format
  # @param browser [String] The requested browser name
  # @return [String] Selenium capability matched browser, if known
  def browser_formatter(browser)
    # Normalize the data by downcasing it.
    if browser.is_a?(String)
      inputted_browser = browser.downcase
    else
      inputted_browser = browser
    end

    case inputted_browser
      when 'edge', 'microsoft edge'
        return 'MicrosoftEdge'
      else
        return browser
    end
  end

  def os_formatter(os)
    if os.is_a?(String)
      inputted_os = os.downcase
    else
      inputted_os = os
    end

    case inputted_os
      when 'mac', 'macos'
        return 'MacOS'
      else
        return os
    end
  end

  def grid_enabled?
    ENV['GRID'] == 'true'
  end

  def saucelabs?
    ENV['GRID_HOST'] == 'saucelabs'
  end

  # Analyzes an exception to determine if the Grid connection was terminated
  # @param exception Exception The exception to be analyzed
  # @return Boolean If the Grid connection was terminated based on the exception
  def grid_connection_terminated?(exception)
    if exception.class == RSpec::Core::MultipleExceptionError

      # Multiple Exceptions... need to go through each
      exception.all_exceptions.each do |e|
        next unless grid_connection_exceptions.include?(e.class)

        if e.class == Selenium::WebDriver::Error::UnknownError
          # Since it's unknown, we need to analyze its message
          return terminated_exception_message?(e)
        else
          return true
        end
      end

    else
      # Only one exception to analyze
      return false unless grid_connection_exceptions.include?(exception.class)

      if exception.class == Selenium::WebDriver::Error::UnknownError
        # Since it's unknown, we need to analyze its message
        return terminated_exception_message?(exception)
      else
        return true
      end
    end
  end

  # @return [Array] Exceptions pertaining to Grid Connection failures
  def grid_connection_exceptions
    return [
      Errno::ETIMEDOUT, # Grid Host is completely unavailable
      Errno::ECONNREFUSED, # Host is online, but Grid is not running
      Selenium::WebDriver::Error::UnknownError # Unknown, various conditions
    ]
  end

  # Validates the proxy was succesfully passed to Browser desired_capabilities.
  def output_proxy_information
    if !Capybara.current_session.driver.options[:desired_capabilities].proxy.nil?
      proxy_addr = Capybara.current_session.driver.options[:desired_capabilities].proxy.http
    else
      proxy_addr = nil
    end

    if bmp_enabled? && proxy_addr.nil?
      puts "ERROR CONNECTING TO PROXY!"
    elsif bmp_enabled? && !proxy_addr.nil?
      # Inverse Boolean - Proxy is not nil
      puts "Connected to Proxy: #{proxy_addr}"
    end
  end

  # @return [Boolean] Whether connected to an internal or external grid
  def internal_grid?
    return grid_enabled? && (SpecData.grid_host == 'prod' || SpecData.grid_host == 'dev')
  end
end

# Parses an exception's message to see if there are signs of a disconnected session
# @param exception [Exception] The exception to analyze
# @return [Boolean] Whether the exception indicates Grid was terminated
private def terminated_exception_message?(exception)
  error_messages = [/Error communicating with the remote browser/,
                    /was terminated due to SO_TIMEOUT/,
                    /was terminated due to FORWARDING_TO_NODE_FAILED/,
                    /was terminated due to TIMEOUT/,
                    /was terminated due to CLIENT_GONE/]

  error_messages.each do |error_message|
    return true if exception.message =~ error_message
  end

  return false # If it got here, it didn't find a match
end

private def display_connection
  message = "[INFO] Connected to Grid Hub. SessionID: #{Capybara.page.driver.browser.session_id}"
  if SpecData.parallel == 'true'
    # Append the port number if running in Parallel
    message += " (Port #{Capybara.server_port})"
  end
  puts message
end
