# encoding: utf-8
=begin
Based on simple_test.rb
https://github.com/appium/appium/blob/82995f47408530c80c3376f4e07a1f649d96ba22/sample-code/examples/ruby/simple_test.rb
https://github.com/appium/appium/blob/c58eeb66f2d6fa3b9a89d188a2e657cca7cb300f/LICENSE
=end

# Load appium.txt (toml format) into system ENV
# the basedir of this file + appium.txt is what's used
# @param opts [Hash] file: '/path/to/appium.txt', verbose: true
# @return [Array<String>] the require files. nil if require doesn't exist
def load_appium_txt opts
  raise 'opts must be a hash' unless opts.kind_of? Hash
  opts.each_pair { |k,v| opts[k.to_s.downcase.strip.intern] = v }
  opts = {} if opts.nil?
  file = opts.fetch :file, nil
  raise 'Must pass file' unless file
  verbose = opts.fetch :verbose, false
  # Check for env vars in .txt
  parent_dir = File.dirname file
  toml = File.expand_path File.join parent_dir, 'appium.txt'
  puts "appium.txt path: #{toml}" if verbose
  # @private
  def update data, *args
    args.each do |name|
      var = data[name]
      ENV[name] = var if var
    end
  end

  toml_exists = File.exists? toml
  puts "Exists? #{toml_exists}" if verbose
  data = nil

  if toml_exists
    require 'toml'
    require 'ap'
    puts "Loading #{toml}" if verbose

    # bash requires A="OK"
    # toml requires A = "OK"
    #
    # A="OK" => A = "OK"
    data = File.read toml

    data = data.split("\n").map do |line|
      line.sub /([^\s])\=/, "\\1 = "
    end.join "\n"

    data = TOML::Parser.new(data).parsed
    ap data unless data.empty? if verbose

    update data, 'APP_PATH', 'APP_APK', 'APP_PACKAGE',
           'APP_ACTIVITY', 'APP_WAIT_ACTIVITY',
           'DEVICE'

    # Ensure app path is absolute
    ENV['APP_PATH'] = File.expand_path ENV['APP_PATH'] if ENV['APP_PATH'] &&
        !ENV['APP_PATH'].empty?

    if ! %w(ios android selendroid).include? ENV['DEVICE']
      raise 'DEVICE must be ios, android, or selendroid'
    end
  end

  # return list of require files as an array
  # nil if require doesn't exist
  if data && data['require']
    r = data['require']
    r = r.kind_of?(Array) ? r : [ r ]
    # ensure files are absolute
    r.map! do |file|
      file = file.include?(File::Separator) ? file :
             File.join(parent_dir, file)
      file = File.expand_path file
      File.exists?(file) ? file : nil
    end
    r.compact # remove nils
  end
end

# Fix uninitialized constant Minitest (NameError)
module Minitest
  # Fix superclass mismatch for class Spec
  class Runnable; end
  class Test < Runnable; end
  class Spec < Test; end
end

module Appium
  add_to_path __FILE__

  require 'selenium-webdriver'

  # common
  require 'common/helper'
  require 'common/patch'
  require 'common/version'
  require 'common/element/button'
  require 'common/element/text'
  require 'common/element/window'

  # ios
  require 'ios/helper'
  require 'ios/patch'
  require 'ios/element/alert'
  require 'ios/element/generic'
  require 'ios/element/textfield'

  # android
  require 'android/dynamic'
  require 'android/helper'
  require 'android/patch'
  require 'android/element/alert'
  require 'android/element/generic'
  require 'android/element/textfield'

  class Driver
    @@loaded = false

    attr_reader :default_wait, :app_path, :app_name, :device,
                :app_package, :app_activity, :app_wait_activity,
                :sauce_username, :sauce_access_key, :port, :debug
    # Creates a new driver.
    # :device is :android, :ios, or :selendroid
    #
    # ```ruby
    # # Options include:
    # :app_path, :app_name, :app_package, :app_activity,
    # :app_wait_activity, :sauce_username, :sauce_access_key,
    # :port, :os, :debug
    #
    # require 'rubygems'
    # require 'appium_lib'
    #
    # # Start iOS driver
    # app = { device: :ios, app_path: '/path/to/MyiOS.app'}
    # Appium::Driver.new(app).start_driver
    #
    # # Start Android driver
    # apk = { device: :android
    #         app_path: '/path/to/the.apk',
    #         app_package: 'com.example.pkg',
    #         app_activity: 'act.Start',
    #         app_wait_activity: 'act.Start'
    # }
    #
    # Appium::Driver.new(apk).start_driver
    # ```
    #
    # @param opts [Object] A hash containing various options.
    # @return [Driver]
    def initialize opts={}
      # quit last driver
      $driver.driver_quit if $driver
      opts = {} if opts.nil?
      # convert to downcased symbols
      opts.each_pair { |k,v| opts[k.to_s.downcase.strip.intern] = v }

      @default_wait = opts.fetch :wait, 30

      # Path to the .apk, .app or .app.zip.
      # The path can be local or remote for Sauce.
      @app_path = opts.fetch :app_path, ENV['APP_PATH']
      raise 'APP_PATH must be set.' if @app_path.nil?

      # The name to use for the test run on Sauce.
      @app_name = opts.fetch :app_name, ENV['APP_NAME']

      # Android app package
      @app_package = opts.fetch :app_package, ENV['APP_PACKAGE']

      # Android app starting activity.
      @app_activity = opts.fetch :app_activity, ENV['APP_ACTIVITY']

      # Android app waiting activity
      @app_wait_activity = opts.fetch :app_wait_activity, ENV['APP_WAIT_ACTIVITY']

      # Sauce Username
      @sauce_username = opts.fetch :sauce_username, ENV['SAUCE_USERNAME']

      # Sauce Key
      @sauce_access_key = opts.fetch :sauce_access_key, ENV['SAUCE_ACCESS_KEY']

      @port = opts.fetch :port, ENV['PORT'] || 4723

      # :ios, :android, :selendroid
      @device = opts.fetch :device, ENV['DEVICE'] || :ios
      @device = @device.intern # device must be a symbol

      # load common methods
      extend Appium::Common
      if @device == :android
        raise 'APP_ACTIVITY must be set.' if @app_activity.nil?

        # load Android specific methods
        extend Appium::Android
      else
        # load iOS specific methods
        extend Appium::Ios
      end

      # apply os specific patches
      patch_webdriver_element

      # enable debug patch
      # !!'constant' == true
      @debug = opts.fetch :debug, !!defined?(Pry)
      puts "Debug is: #{@debug}"
      if @debug
        ap opts unless opts.empty?
        puts "Device is: #{@device}"
        patch_webdriver_bridge
      end

      # Save global reference to last created Appium driver for top level methods.
      $driver = self

      # Promote exactly once the first time the driver is created.
      # Subsequent drivers do not trigger promotion.
      unless @@loaded
        @@loaded = true
        # Promote Appium driver methods to Object instance methods.
        $driver.public_methods(false).each do | m |
          # not MiniTest::Spec
          ::Minitest::Spec.class_eval do
            define_method m do | *args, &block |
                begin
                  # puts "[Object.class_eval] Calling super for '#{m}'"
                  # prefer existing method.
                  # super will invoke method missing on driver
                  super(*args, &block)
                  # minitest also defines a name method,
                  # so rescue argument error
                  # and call the name method on $driver
                rescue NoMethodError, ArgumentError
                  # puts "[Object.class_eval] '#{m}' not on super"
                  $driver.send m, *args, &block if $driver.respond_to?(m)
                end
            end
          end
        end
      end

      self # return newly created driver
    end # def initialize

    # Returns the status payload
    #
    # ```ruby
    # {"status"=>0,
    #  "value"=>
    #   {"build"=>
    #     {"version"=>"0.8.2",
    #      "revision"=>"f2a2bc3782e4b0370d97a097d7e04913cf008995"}},
    #  "sessionId"=>"8f4b34a7-a9a9-4ac5-b125-36258143446a"}
    # ```
    #
    #  Discover the Appium rev running on the server.
    #
    # `status["value"]["build"]["revision"]`
    # `f2a2bc3782e4b0370d97a097d7e04913cf008995`
    #
    # @return [JSON]
    def status
      driver.status.payload
    end

    # Returns the server's version string
    # @return [String]
    def server_version
      status['value']['build']['version']
    end

    # @private
    # WebDriver capabilities. Must be valid for Sauce to work.
    # https://github.com/jlipps/appium/blob/master/app/android.js
    def android_capabilities
      {
        browserName: 'Android',
        platform: 'LINUX',
        version: '4.2',
        device: @device == :android ? 'Android' : 'selendroid',
        name: @app_name || 'Ruby Console Android Appium',
        :'app-package' => @app_package,
        :'app-activity' => @app_activity,
        :'app-wait-activity' => @app_wait_activity || @app_activity
      }
    end

    # @private
    # WebDriver capabilities. Must be valid for Sauce to work.
    def ios_capabilities
      {
        browserName: 'iOS 6.0',
        platform: 'Mac 10.8',
        version: '6.0',
        device: 'iPhone Simulator',
        name: @app_name || 'Ruby Console iOS Appium'
      }
    end

    # @private
    def capabilities
      caps = @device == :ios ? ios_capabilities : android_capabilities
      caps[:app] = absolute_app_path unless @app_path.nil? || @app_path.empty?
      caps
    end

    # Converts environment variable APP_PATH to an absolute path.
    # @return [String] APP_PATH as an absolute path
    def absolute_app_path
      raise 'APP_PATH not set!' if @app_path.nil? || @app_path.empty?
      # Sauce storage API. http://saucelabs.com/docs/rest#storage
      return @app_path if @app_path.start_with? 'sauce-storage:'
      return @app_path if @app_path.match(/^http/) # public URL for Sauce
      if @app_path.match(/^\//) # absolute file path
        raise "App doesn't exist. #{@app_path}" unless File.exist? @app_path
        return @app_path
      end
      file = File.join(File.dirname(__FILE__), @app_path)
      raise "App doesn't exist #{file}" unless File.exist? file
      file
    end

    # Get the server url for sauce or local based on env vars.
    # @return [String] the server url
    def server_url
      if !@sauce_username.nil? && !@sauce_access_key.nil?
        "http://#{@sauce_username}:#{@sauce_access_key}@ondemand.saucelabs.com:80/wd/hub"
      else
        "http://127.0.0.1:#{@port}/wd/hub"
      end
    end

    # Restarts the driver
    # @return [Driver] the driver
    def restart
      driver_quit
      start_driver
    end

    # Returns the driver
    # @return [Driver] the driver
    def driver
      @driver
    end

    # Takes a png screenshot and saves to the target path.
    #
    # Example: screenshot '/tmp/hi.png'
    #
    # @param png_save_path [String] the full path to save the png
    # @return [void]
    def screenshot png_save_path
      @driver.save_screenshot png_save_path
    end

    # Quits the driver
    # @return [void]
    def driver_quit
      # rescue NoSuchDriverError
      begin; @driver.quit unless @driver.nil?; rescue; end
    end

    # Creates a new global driver and quits the old one if it exists.
    #
    # @return [Selenium::WebDriver] the new global driver
    def start_driver
      @client = @client || Selenium::WebDriver::Remote::Http::Default.new
      @client.timeout = 999999

      begin
        @driver = Selenium::WebDriver.for :remote, http_client: @client, desired_capabilities: capabilities, url: server_url
        # Load touch methods. Required for Selendroid.
        @driver.extend Selenium::WebDriver::DriverExtensions::HasTouchScreen
      rescue Errno::ECONNREFUSED
        raise 'ERROR: Unable to connect to Appium. Is the server running?'
      end

      # Set timeout to a large number so that Appium doesn't quit
      # when no commands are entered after 60 seconds.
      # broken on selendroid: https://github.com/appium/appium/issues/513
      mobile :setCommandTimeout, timeout: 9999 unless @device == :selendroid

      # Set implicit wait by default unless we're using Pry.
      @driver.manage.timeouts.implicit_wait = @default_wait unless defined? Pry

      @driver
    end

    # Set implicit wait to zero.
    def no_wait
      @driver.manage.timeouts.implicit_wait = 0
    end

    # Set implicit wait to timeout, defaults to 30.
    # @param timeout [Integer] the timeout in seconds
    # @return [void]
    def set_wait timeout=@default_wait
      @driver.manage.timeouts.implicit_wait = timeout
    end

    # Returns the default client side wait.
    # This value is independent of what the server is using
    # @return [Integer]
    def default_wait
      @default_wait
    end

    # Returns existence of element.
    #
    # Example:
    #
    # exists { button('sign in') } ? puts('true') : puts('false')
    #
    # @param pre_check [Integer]  the amount in seconds to set the
    #                             wait to before checking existance
    # @param post_check [Integer] the amount in seconds to set the
    #                             wait to after checking existance
    # @param search_block [Block] the block to call
    # @return [Boolean]
    def exists pre_check=0, post_check=@default_wait, &search_block
      set_wait pre_check # set wait to zero

      # the element exists unless an error is raised.
      exists = true

      begin
        search_block.call # search for element
      rescue
        exists = false # error means it's not there
      end

      # restore wait
      set_wait post_check if post_check != pre_check

      exists
    end

    # The same as @driver.execute_script
    # @param script [String] the script to execute
    # @param args [*args] the args to pass to the script
    # @return [Object]
    def execute_script script, *args
      @driver.execute_script script, *args
    end

    # Helper method for mobile gestures
    #
    # https://github.com/appium/appium/wiki/Automating-mobile-gestures
    #
    # driver.execute_script 'mobile: swipe', endX: 100, endY: 100, duration: 0.01
    #
    # becomes
    #
    # mobile :swipe, endX: 100, endY: 100, duration: 0.01
    # @param method [String, Symbol] the method to execute
    # @param args [*args] the args to pass to the method
    # @return [Object]
    def mobile method, *args
      raise 'Method must not be nil' if method.nil?
      raise 'Method must have .to_s' unless method.respond_to? :to_s

      @driver.execute_script "mobile: #{method.to_s}", *args
    end

    # Calls @driver.find_elements
    #
    # @param args [*args] the args to use
    # @return [Array<Element>] Array is empty when no elements are found.
    def find_elements *args
      @driver.find_elements *args
    end

    # Calls @driver.find_elements
    #
    # @param args [*args] the args to use
    # @return [Element]
    def find_element *args
      @driver.find_element *args
    end

    # Quit the driver and Pry.
    # quit and exit are reserved by Pry.
    # @return [void]
    def x
      driver_quit
      exit # exit pry
    end
  end # end class Driver
end # end module Appium

# Paging in Pry is annoying :q required to exit.
# With pager disabled, the output is similar to IRB
# Only set if Pry is defined.
Pry.config.pager = false if defined?(Pry)