# Copyright (C) The Arvados Authors. All rights reserved.
#
# SPDX-License-Identifier: AGPL-3.0

require 'test_helper'
require 'capybara/rails'
require 'capybara/poltergeist'
require 'uri'
require 'yaml'

def available_port for_what
  begin
    Addrinfo.tcp("0.0.0.0", 0).listen do |srv|
      port = srv.connect_address.ip_port
      # Selenium needs an additional locking port, check if it's available
      # and retry if necessary.
      if for_what == 'selenium'
        locking_port = port - 1
        Addrinfo.tcp("0.0.0.0", locking_port).listen.close
      end
      STDERR.puts "Using port #{port} for #{for_what}"
      return port
    end
  rescue Errno::EADDRINUSE, Errno::EACCES
    retry
  end
end

def selenium_opts
  {
    port: available_port('selenium'),
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.firefox(
      acceptInsecureCerts: true,
    ),
  }
end

def poltergeist_opts
  {
    phantomjs_options: ['--ignore-ssl-errors=true'],
    port: available_port('poltergeist'),
    window_size: [1200, 800],
  }
end

Capybara.register_driver :poltergeist do |app|
  Capybara::Poltergeist::Driver.new app, poltergeist_opts
end

Capybara.register_driver :poltergeist_debug do |app|
  Capybara::Poltergeist::Driver.new app, poltergeist_opts.merge(inspector: true)
end

Capybara.register_driver :poltergeist_with_fake_websocket do |app|
  js = File.expand_path '../support/fake_websocket.js', __FILE__
  Capybara::Poltergeist::Driver.new app, poltergeist_opts.merge(extensions: [js])
end

Capybara.register_driver :poltergeist_without_file_api do |app|
  js = File.expand_path '../support/remove_file_api.js', __FILE__
  Capybara::Poltergeist::Driver.new app, poltergeist_opts.merge(extensions: [js])
end

Capybara.register_driver :selenium do |app|
  Capybara::Selenium::Driver.new app, selenium_opts
end

Capybara.register_driver :selenium_with_download do |app|
  profile = Selenium::WebDriver::Firefox::Profile.new
  profile['browser.download.dir'] = DownloadHelper.path.to_s
  profile['browser.download.downloadDir'] = DownloadHelper.path.to_s
  profile['browser.download.defaultFolder'] = DownloadHelper.path.to_s
  profile['browser.download.folderList'] = 2 # "save to user-defined location"
  profile['browser.download.manager.showWhenStarting'] = false
  profile['browser.helperApps.alwaysAsk.force'] = false
  profile['browser.helperApps.neverAsk.saveToDisk'] = 'text/plain,application/octet-stream'
  Capybara::Selenium::Driver.new app, selenium_opts.merge(profile: profile)
end

module WaitForAjax
  # FIXME: Huge side effect here
  # The following line changes the global default Capybara wait time, affecting
  # every test which follows this one. This should be removed and the failing tests
  # should have their individual wait times increased, if appropriate, using
  # the using_wait_time(N) construct to temporarily change the wait time.
  # Note: the below is especially bad because there are places that increase wait
  # times using a multiplier e.g. using_wait_time(3 * Capybara.default_max_wait_time)
  Capybara.default_max_wait_time = 10
  def wait_for_ajax
    timeout = 10
    count = 0
    while page.evaluate_script("jQuery.active").to_i > 0
      count += 1
      raise "AJAX request took more than #{timeout} seconds" if count > timeout * 10
      sleep(0.1)
    end
  end

end

module AssertDomEvent
  # Yield the supplied block, then wait for an event to arrive at a
  # DOM element.
  def assert_triggers_dom_event events, target='body'
    magic = 'received-dom-event-' + rand(2**30).to_s(36)
    page.execute_script <<eos
      $('#{target}').one('#{events}', function() {
        $('body').addClass('#{magic}');
      });
eos
    yield
    assert_selector "body.#{magic}"
    page.execute_script "$('body').removeClass('#{magic}');";
  end
end

module HeadlessHelper
  class HeadlessSingleton
    @display = ENV['ARVADOS_TEST_HEADLESS_DISPLAY'] || rand(400)+100
    STDERR.puts "Using display :#{@display} for headless tests"
    def self.get
      @headless ||= Headless.new reuse: false, display: @display
    end
  end

  Capybara.default_driver = :rack_test

  def self.included base
    base.class_eval do
      setup do
        Capybara.use_default_driver
        @headless = false
      end

      teardown do
        if @headless
          @headless.stop
          @headless = false
        end
      end
    end
  end

  def need_selenium reason=nil, driver=:selenium
    Capybara.current_driver = driver
    unless ENV['ARVADOS_TEST_HEADFUL'] or @headless
      @headless = HeadlessSingleton.get
      @headless.start
    end
  end

  def need_javascript reason=nil
    unless Capybara.current_driver == :selenium
      Capybara.current_driver = :poltergeist
    end
  end
end

class ActionDispatch::IntegrationTest
  # Make the Capybara DSL available in all integration tests
  include Capybara::DSL
  include ApiFixtureLoader
  include WaitForAjax
  include AssertDomEvent
  include HeadlessHelper

  @@API_AUTHS = self.api_fixture('api_client_authorizations')

  def page_with_token(token, path='/')
    # Generate a page path with an embedded API token.
    # Typical usage: visit page_with_token('token_name', page)
    # The token can be specified by the name of an api_client_authorizations
    # fixture, or passed as a raw string.
    api_token = ((@@API_AUTHS.include? token) ?
                 @@API_AUTHS[token]['api_token'] : token)
    path_parts = path.partition("#")
    sep = (path_parts.first.include? '?') ? '&' : '?'
    q_string = URI.encode_www_form('api_token' => api_token)
    path_parts.insert(1, "#{sep}#{q_string}")
    path_parts.join("")
  end

  # Find a page element, but return false instead of raising an
  # exception if not found. Use this with assertions to explain that
  # the error signifies a failed test rather than an unexpected error
  # during a testing procedure.
  def find?(*args)
    begin
      find(*args)
    rescue Capybara::ElementNotFound
      false
    end
  end

  @@screenshot_count = 1
  def screenshot
    image_file = "./tmp/workbench-fail-#{@@screenshot_count}.png"
    begin
      page.save_screenshot image_file
    rescue Capybara::NotSupportedByDriverError
      # C'est la vie.
    else
      puts "Saved #{image_file}"
      @@screenshot_count += 1
    end
  end

  teardown do
    if !passed? && !skipped?
      screenshot
    end
    if Capybara.current_driver == :selenium
      # Clearing localStorage crashes on a page where JS isn't
      # executed. We also need to make sure we're clearing
      # localStorage for the test server's origin, even if we finished
      # the test on a different origin.
      host = Capybara.current_session.server.host
      port = Capybara.current_session.server.port
      base = "http://#{host}:#{port}"
      if page.evaluate_script("window.document.contentType") != "text/html" ||
         !page.evaluate_script("window.location.toString()").start_with?(base)
        visit "#{base}/404"
      end
      page.execute_script("window.localStorage.clear()")
    else
      page.driver.restart if defined?(page.driver.restart)
    end
    Capybara.reset_sessions!
  end

  def accept_alert
    if Capybara.current_driver == :selenium
      (0..9).each do
        begin
          page.driver.browser.switch_to.alert.accept
          break
        rescue Selenium::WebDriver::Error::NoSuchAlertError
         sleep 0.1
        end
      end
    else
      # poltergeist returns true for confirm, so no need to accept
    end
  end
end

def upload_data_and_get_collection(data, user, filename, owner_uuid=nil)
  token = api_token(user)
  datablock = `echo -n #{data.shellescape} | ARVADOS_API_TOKEN=#{token.shellescape} arv-put --no-progress --raw -`.strip
  assert $?.success?, $?
  col = nil
  use_token user do
    mtxt = ". #{datablock} 0:#{data.length}:#{filename}\n"
    if owner_uuid
      col = Collection.create(manifest_text: mtxt, owner_uuid: owner_uuid)
    else
      col = Collection.create(manifest_text: mtxt)
    end
  end
  return col
end
