# frozen_string_literal: true

if ENV["COVERAGE"]
  require "simplecov"
  if ENV["TEST_ENV_NUMBER"]
    SimpleCov.command_name "#{SimpleCov.command_name} #{ENV["TEST_ENV_NUMBER"]}"
  end
  SimpleCov.start "rails" do
    add_group "Libraries", %r{^/lib/(?!tasks).*$}
    add_group "Scripts", "script"
    add_group "Serializers", "app/serializers"
    add_group "Services", "app/services"
    add_group "Tasks", "lib/tasks"
  end
end

require "rubygems"
require "rbtrace" if RUBY_ENGINE == "ruby"
require "pry"
require "pry-byebug"
require "pry-rails"
require "fabrication"
require "mocha/api"
require "certified"
require "webmock/rspec"
require "minio_runner"

class RspecErrorTracker
  def self.exceptions
    @exceptions ||= {}
  end

  def self.clear_exceptions
    @exceptions&.clear
  end

  def self.report_exception(path, exception)
    exceptions[path] = exception
  end

  def initialize(app, config = {})
    @app = app
  end

  def call(env)
    begin
      @app.call(env)

      # This is a little repetitive, but since WebMock::NetConnectNotAllowedError
      # and also Mocha::ExpectationError inherit from Exception instead of StandardError
      # they do not get captured by the rescue => e shorthand :(
    rescue WebMock::NetConnectNotAllowedError, Mocha::ExpectationError, StandardError => e
      RspecErrorTracker.report_exception(env["PATH_INFO"], e)
      raise e
    end
  end
end

ENV["RAILS_ENV"] ||= "test"
require File.expand_path("../../config/environment", __FILE__)
require "rspec/rails"
require "shoulda-matchers"
require "sidekiq/testing"
require "selenium-webdriver"
require "capybara/rails"

# The shoulda-matchers gem no longer detects the test framework
# you're using or mixes itself into that framework automatically.
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :active_record
    with.library :active_model
  end
end

# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/requests/examples/*.rb")].each { |f| require f }

Dir[Rails.root.join("spec/system/helpers/**/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/system/page_objects/**/base.rb")].each { |f| require f }
Dir[Rails.root.join("spec/system/page_objects/**/*.rb")].each { |f| require f }

Dir[Rails.root.join("spec/fabricators/*.rb")].each { |f| require f }
require_relative "./helpers/redis_snapshot_helper"

# Require plugin helpers at plugin/[plugin]/spec/plugin_helper.rb (includes symlinked plugins).
if ENV["LOAD_PLUGINS"] == "1"
  Dir[Rails.root.join("plugins/*/spec/plugin_helper.rb")].each { |f| require f }

  Dir[Rails.root.join("plugins/*/spec/fabricators/**/*.rb")].each { |f| require f }

  Dir[Rails.root.join("plugins/*/spec/system/page_objects/**/*.rb")].each { |f| require f }
end

# let's not run seed_fu every test
SeedFu.quiet = true if SeedFu.respond_to? :quiet

SiteSetting.automatically_download_gravatars = false

SeedFu.seed

# we need this env var to ensure that we can impersonate in test
# this enable integration_helpers sign_in helper
ENV["DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE"] = "1"

module TestSetup
  # This is run before each test and before each before_all block
  def self.test_setup(x = nil)
    RateLimiter.disable
    PostActionNotifier.disable
    SearchIndexer.disable
    UserActionManager.disable
    NotificationEmailer.disable
    SiteIconManager.disable
    WordWatcher.disable_cache

    SiteSetting.provider.all.each { |setting| SiteSetting.remove_override!(setting.name) }

    # very expensive IO operations
    SiteSetting.automatically_download_gravatars = false

    Discourse.clear_readonly!
    Sidekiq::Worker.clear_all

    I18n.locale = SiteSettings::DefaultsProvider::DEFAULT_LOCALE

    RspecErrorTracker.clear_exceptions

    if $test_cleanup_callbacks
      $test_cleanup_callbacks.reverse_each(&:call)
      $test_cleanup_callbacks = nil
    end

    # in test this is very expensive, we explicitly enable when needed
    Topic.update_featured_topics = false

    # Running jobs are expensive and most of our tests are not concern with
    # code that runs inside jobs. run_later! means they are put on the redis
    # queue and never processed.
    Jobs.run_later!

    # Don't track ApplicationRequests in test mode unless opted in
    ApplicationRequest.disable

    # Don't queue badge grant in test mode
    BadgeGranter.disable_queue

    OmniAuth.config.test_mode = false

    Middleware::AnonymousCache.disable_anon_cache
    BlockRequestsMiddleware.allow_requests!
  end
end

if ENV["PREFABRICATION"] == "0"
  module Prefabrication
    def fab!(name, **opts, &blk)
      blk ||= proc { Fabricate(name) }
      let!(name, &blk)
    end
  end
else
  require "test_prof/recipes/rspec/let_it_be"
  require "test_prof/before_all/adapters/active_record"

  TestProf::BeforeAll.configure do |config|
    config.after(:begin) do
      DB.test_transaction = ActiveRecord::Base.connection.current_transaction
      TestSetup.test_setup
    end
  end

  module Prefabrication
    def fab!(name, **opts, &blk)
      blk ||= proc { Fabricate(name) }
      let_it_be(name, refind: true, **opts, &blk)
    end
  end
end

PER_SPEC_TIMEOUT_SECONDS = 45
BROWSER_READ_TIMEOUT = 30

RSpec.configure do |config|
  config.fail_fast = ENV["RSPEC_FAIL_FAST"] == "1"
  config.silence_filter_announcements = ENV["RSPEC_SILENCE_FILTER_ANNOUNCEMENTS"] == "1"
  config.extend RedisSnapshotHelper
  config.extend Prefabrication
  config.include Helpers
  config.include MessageBus
  config.include RSpecHtmlMatchers
  config.include IntegrationHelpers, type: :request
  config.include SystemHelpers, type: :system
  config.include DiscourseWebauthnIntegrationHelpers
  config.include SiteSettingsHelpers
  config.include SidekiqHelpers
  config.include UploadsHelpers
  config.include OneboxHelpers
  config.include FastImageHelpers
  config.mock_framework = :mocha
  config.order = "random"
  config.infer_spec_type_from_file_location!

  if ENV["GITHUB_ACTIONS"]
    # Enable color output in GitHub Actions
    # This eventually will be `config.color_mode = :on` in RSpec 4?
    config.tty = true
    config.color = true
  end

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = true

  # Sometimes you may have a large string or object that you are comparing
  # with some expectation, and you want to see the full diff between actual
  # and expected without rspec truncating 90% of the diff. Setting the
  # max_formatted_output_length to nil disables this truncation completely.
  #
  # c.f. https://www.rubydoc.info/gems/rspec-expectations/RSpec/Expectations/Configuration#max_formatted_output_length=-instance_method
  if ENV["RSPEC_DISABLE_DIFF_TRUNCATION"]
    config.expect_with :rspec do |expectation|
      expectation.max_formatted_output_length = nil
    end
  end

  # If true, the base class of anonymous controllers will be inferred
  # automatically. This will be the default behavior in future versions of
  # rspec-rails.
  config.infer_base_class_for_anonymous_controllers = true

  config.full_cause_backtrace = true

  config.before(:suite) do
    CachedCounting.disable

    begin
      ActiveRecord::Migration.check_pending!
    rescue ActiveRecord::PendingMigrationError
      raise "There are pending migrations, run RAILS_ENV=test bin/rake db:migrate"
    end

    Sidekiq.error_handlers.clear

    # Ugly, but needed until we have a user creator
    User.skip_callback(:create, :after, :ensure_in_trust_level_group)

    DiscoursePluginRegistry.reset! if ENV["LOAD_PLUGINS"] != "1"
    Discourse.current_user_provider = TestCurrentUserProvider

    SiteSetting.refresh!

    # Rebase defaults
    #
    # We nuke the DB storage provider from site settings, so need to yank out the existing settings
    #  and pretend they are default.
    # There are a bunch of settings that are seeded, they must be loaded as defaults
    SiteSetting.current.each do |k, v|
      # skip setting defaults for settings that are in unloaded plugins
      SiteSetting.defaults.set_regardless_of_locale(k, v) if SiteSetting.respond_to? k
    end

    SiteSetting.provider = TestLocalProcessProvider.new

    # Used for S3 system specs, see also setup_s3_system_test.
    MinioRunner.config do |minio_runner_config|
      minio_runner_config.minio_domain = ENV["MINIO_RUNNER_MINIO_DOMAIN"] || "minio.local"
      minio_runner_config.buckets =
        (
          if ENV["MINIO_RUNNER_BUCKETS"]
            ENV["MINIO_RUNNER_BUCKETS"].split(",")
          else
            ["discoursetest"]
          end
        )
      minio_runner_config.public_buckets =
        (
          if ENV["MINIO_RUNNER_PUBLIC_BUCKETS"]
            ENV["MINIO_RUNNER_PUBLIC_BUCKETS"].split(",")
          else
            ["discoursetest"]
          end
        )
    end

    WebMock.disable_net_connect!(
      allow_localhost: true,
      allow: [
        *MinioRunner.config.minio_urls,
        URI(MinioRunner::MinioBinary.platform_binary_url).host,
        ENV["CAPYBARA_REMOTE_DRIVER_URL"],
      ].compact,
    )

    if ENV["CAPYBARA_DEFAULT_MAX_WAIT_TIME"].present?
      Capybara.default_max_wait_time = ENV["CAPYBARA_DEFAULT_MAX_WAIT_TIME"].to_i
    else
      Capybara.default_max_wait_time = 4
    end

    Capybara.threadsafe = true
    Capybara.disable_animation = true

    # Click offsets is calculated from top left of element
    Capybara.w3c_click_offset = false

    Capybara.configure do |capybara_config|
      capybara_config.server_host = ENV["CAPYBARA_SERVER_HOST"].presence || "localhost"

      capybara_config.server_port =
        (ENV["CAPYBARA_SERVER_PORT"].presence || "31_337").to_i + ENV["TEST_ENV_NUMBER"].to_i
    end

    module IgnoreUnicornCapturedErrors
      def raise_server_error!
        super
      rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN => e
        # Ignore these exceptions - caused by client. Handled by unicorn in dev/prod
        # https://github.com/defunkt/unicorn/blob/d947cb91cf/lib/unicorn/http_server.rb#L570-L573
      end
    end

    Capybara::Session.class_eval { prepend IgnoreUnicornCapturedErrors }

    module CapybaraTimeoutExtension
      class CapybaraTimedOut < StandardError
        attr_reader :cause

        def initialize(wait_time, cause)
          @cause = cause
          super "This spec passed, but capybara waited for the full wait duration (#{wait_time}s) at least once. " +
                  "This will slow down the test suite. " +
                  "Beware of negating the result of selenium's RSpec matchers."
        end
      end

      def synchronize(seconds = nil, errors: nil)
        return super if session.synchronized # Nested synchronize. We only want our logic on the outermost call.
        begin
          super
        rescue StandardError => e
          seconds = session_options.default_max_wait_time if [nil, true].include? seconds
          if catch_error?(e, errors) && seconds != 0
            # This error will only have been raised if the timer expired
            timeout_error = CapybaraTimedOut.new(seconds, e)
            if RSpec.current_example
              # Store timeout for later, we'll only raise it if the test otherwise passes
              RSpec.current_example.metadata[:_capybara_timeout_exception] ||= timeout_error
              raise # re-raise original error
            else
              # Outside an example... maybe a `before(:all)` hook?
              raise timeout_error
            end
          else
            raise
          end
        end
      end
    end

    Capybara::Node::Base.prepend(CapybaraTimeoutExtension)

    config.after(:each, type: :system) do |example|
      # If test passed, but we had a capybara finder timeout, raise it now
      if example.exception.nil? &&
           (capybara_timeout_error = example.metadata[:_capybara_timeout_exception])
        raise capybara_timeout_error
      end
    end

    # possible values: OFF, SEVERE, WARNING, INFO, DEBUG, ALL
    browser_log_level = ENV["SELENIUM_BROWSER_LOG_LEVEL"] || "WARNING"

    chrome_browser_options =
      Selenium::WebDriver::Chrome::Options
        .new(logging_prefs: { "browser" => browser_log_level, "driver" => "ALL" })
        .tap do |options|
          apply_base_chrome_options(options)
          options.add_argument("--window-size=1400,1400")
          options.add_preference("download.default_directory", Downloads::FOLDER)
        end

    driver_options = { browser: :chrome, timeout: BROWSER_READ_TIMEOUT }

    if ENV["CAPYBARA_REMOTE_DRIVER_URL"].present?
      driver_options[:browser] = :remote
      driver_options[:url] = ENV["CAPYBARA_REMOTE_DRIVER_URL"]
    end

    desktop_driver_options = driver_options.merge(options: chrome_browser_options)

    Capybara.register_driver :selenium_chrome do |app|
      Capybara::Selenium::Driver.new(app, **desktop_driver_options)
    end

    Capybara.register_driver :selenium_chrome_headless do |app|
      chrome_browser_options.add_argument("--headless=new")
      Capybara::Selenium::Driver.new(app, **desktop_driver_options)
    end

    mobile_chrome_browser_options =
      Selenium::WebDriver::Chrome::Options
        .new(logging_prefs: { "browser" => browser_log_level, "driver" => "ALL" })
        .tap do |options|
          options.add_emulation(device_name: "iPhone 12 Pro")
          options.add_argument(
            '--user-agent="--user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/36.0  Mobile/15E148 Safari/605.1.15"',
          )
          apply_base_chrome_options(options)
        end

    mobile_driver_options = driver_options.merge(options: mobile_chrome_browser_options)

    Capybara.register_driver :selenium_mobile_chrome do |app|
      Capybara::Selenium::Driver.new(app, **mobile_driver_options)
    end

    Capybara.register_driver :selenium_mobile_chrome_headless do |app|
      mobile_chrome_browser_options.add_argument("--headless=new")
      Capybara::Selenium::Driver.new(app, **mobile_driver_options)
    end

    Capybara.register_driver :selenium_firefox_headless do |app|
      options =
        Selenium::WebDriver::Firefox::Options.new(
          args: %w[--window-size=1400,1400 --headless],
          prefs: {
            "browser.download.dir": Downloads::FOLDER,
          },
          log_level: ENV["SELENIUM_BROWSER_LOG_LEVEL"] || :warn,
        )
      Capybara::Selenium::Driver.new(
        app,
        browser: :firefox,
        timeout: BROWSER_READ_TIMEOUT,
        options: options,
      )
    end

    if ENV["ELEVATED_UPLOADS_ID"]
      DB.exec "SELECT setval('uploads_id_seq', 10000)"
    else
      DB.exec "SELECT setval('uploads_id_seq', 1)"
    end

    # Prevents 500 errors for site setting URLs pointing to test.localhost in system specs.
    SiteIconManager.clear_cache!
  end

  class TestLocalProcessProvider < SiteSettings::LocalProcessProvider
    attr_accessor :current_site

    def initialize
      super
      self.current_site = "test"
    end
  end

  config.after(:suite) do
    FileUtils.remove_dir(concurrency_safe_tmp_dir, true) if SpecSecureRandom.value
    Downloads.clear
    MinioRunner.stop
  end

  config.around :each do |example|
    before_event_count = DiscourseEvent.events.values.sum(&:count)
    example.run
    after_event_count = DiscourseEvent.events.values.sum(&:count)
    expect(before_event_count).to eq(after_event_count),
    "DiscourseEvent registrations were not cleaned up"
  end

  if ENV["CI"]
    class SpecTimeoutError < StandardError
    end

    mutex = Mutex.new
    condition_variable = ConditionVariable.new
    test_running = false
    is_waiting = false

    backtrace_logger =
      Thread.new do
        loop do
          mutex.synchronize do
            is_waiting = true
            condition_variable.wait(mutex)
            is_waiting = false
          end

          sleep PER_SPEC_TIMEOUT_SECONDS - 1

          if mutex.synchronize { test_running }
            puts "::group::[#{Process.pid}] Threads backtraces 1 second before timeout"

            Thread.list.each do |thread|
              puts "\n"
              thread.backtrace.each { |line| puts line }
              puts "\n"
            end

            puts "::endgroup::"
          end
        rescue StandardError => e
          puts "Error in backtrace logger: #{e}"
        end
      end

    config.around do |example|
      Timeout.timeout(
        PER_SPEC_TIMEOUT_SECONDS,
        SpecTimeoutError,
        "Spec timed out after #{PER_SPEC_TIMEOUT_SECONDS} seconds",
      ) do
        mutex.synchronize do
          test_running = true
          condition_variable.signal
        end

        example.run
      rescue SpecTimeoutError
      ensure
        mutex.synchronize { test_running = false }
        backtrace_logger.wakeup
        sleep 0.01 while !mutex.synchronize { is_waiting }
      end
    end
  end

  if ENV["DISCOURSE_RSPEC_PROFILE_EACH_EXAMPLE"]
    config.around :each do |example|
      measurement = Benchmark.measure { example.run }
      RSpec.current_example.metadata[:run_duration_ms] = (measurement.real * 1000).round(2)
    end
  end

  if ENV["GITHUB_ACTIONS"]
    config.around :each, capture_log: true do |example|
      original_logger = ActiveRecord::Base.logger
      io = StringIO.new
      io_logger = Logger.new(io)
      io_logger.level = Logger::DEBUG
      ActiveRecord::Base.logger = io_logger

      example.run

      RSpec.current_example.metadata[:active_record_debug_logs] = io.string
    ensure
      ActiveRecord::Base.logger = original_logger
    end
  end

  config.before :each do
    # This allows DB.transaction_open? to work in tests. See lib/mini_sql_multisite_connection.rb
    DB.test_transaction = ActiveRecord::Base.connection.current_transaction
    TestSetup.test_setup
  end

  # Match the request hostname to the value in `database.yml`
  config.before(:each, type: %i[request multisite system]) { host! "test.localhost" }

  system_tests_initialized = false

  config.before(:each, type: :system) do |example|
    if !system_tests_initialized
      # Use a file system lock to get `selenium-manager` to download the `chromedriver` binary that is required for
      # system tests to support running system tests in multiple processes. If we don't download the `chromedriver` binary
      # before running system tests in multiple processes, each process will end up calling the `selenium-manager` binary
      # to download the `chromedriver` binary at the same time but the problem is that the binary is being downloaded to
      # the same location and this can interfere with the running tests in another process.
      #
      # The long term fix here is to get `selenium-manager` to download the `chromedriver` binary to a unique path for each
      # process but the `--cache-path` option for `selenium-manager` is currently not supported in `selenium-webdriver`.
      File.open("#{Rails.root}/tmp/chrome_driver_flock", File::RDWR | File::CREAT, 0644) do |file|
        file.flock(File::LOCK_EX)

        if !File.directory?(File.expand_path("~/.cache/selenium"))
          `#{Selenium::WebDriver::SeleniumManager.send(:binary)} --browser chrome`
        end
      end

      # On Rails 7, we have seen instances of deadlocks between the lock in [ActiveRecord::ConnectionAdapaters::AbstractAdapter](https://github.com/rails/rails/blob/9d1673853f13cd6f756315ac333b20d512db4d58/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L86)
      # and the lock in [ActiveRecord::ModelSchema](https://github.com/rails/rails/blob/9d1673853f13cd6f756315ac333b20d512db4d58/activerecord/lib/active_record/model_schema.rb#L550).
      # To work around this problem, we are going to preload all the model schemas before running any system tests so that
      # the lock in ActiveRecord::ModelSchema is not acquired at runtime. This is a temporary workaround while we report
      # the issue to the Rails.
      ActiveRecord::Base.connection.data_sources.map do |table|
        ActiveRecord::Base.connection.schema_cache.add(table)
      end

      system_tests_initialized = true
    end

    driver = [:selenium]
    driver << :mobile if example.metadata[:mobile]
    driver << (aarch64? ? :firefox : :chrome)
    driver << :headless unless ENV["SELENIUM_HEADLESS"] == "0"

    if driver.include?(:firefox)
      STDERR.puts(
        "WARNING: Running system specs using the Firefox driver is not officially supported. Some tests will fail.",
      )
    end

    driven_by driver.join("_").to_sym

    setup_system_test
  end

  config.after(:each, type: :system) do |example|
    lines = RSpec.current_example.metadata[:extra_failure_lines]

    # This is disabled by default because it is super verbose,
    # if you really need to dig into how selenium is communicating
    # for system tests then enable it.
    if ENV["SELENIUM_VERBOSE_DRIVER_LOGS"]
      lines << "~~~~~~~ DRIVER LOGS ~~~~~~~"
      page.driver.browser.logs.get(:driver).each { |log| lines << log.message }
      lines << "~~~~~ END DRIVER LOGS ~~~~~"
    end

    # The logs API isn’t available (yet?) with the Firefox driver
    js_logs = aarch64? ? [] : page.driver.browser.logs.get(:browser)

    # Recommended that this is not disabled, since it makes debugging
    # failed system tests a lot trickier.
    if ENV["SELENIUM_DISABLE_VERBOSE_JS_LOGS"].blank?
      if example.exception
        lines << "~~~~~~~ JS LOGS ~~~~~~~"

        if js_logs.empty?
          lines << "(no logs)"
        else
          js_logs.each do |log|
            # System specs are full of image load errors that are just noise, no need
            # to log this.
            if (
                 log.message.include?("Failed to load resource: net::ERR_CONNECTION_REFUSED") &&
                   (log.message.include?("uploads") || log.message.include?("images"))
               ) || log.message.include?("favicon.ico")
              next
            end

            lines << log.message
          end
        end

        lines << "~~~~~ END JS LOGS ~~~~~"
      end
    end

    js_logs.each do |log|
      next if log.level != "WARNING"
      deprecation_id = log.message[/\[deprecation id: ([^\]]+)\]/, 1]
      next if deprecation_id.nil?

      deprecations = RSpec.current_example.metadata[:js_deprecations] ||= {}
      deprecations[deprecation_id] ||= 0
      deprecations[deprecation_id] += 1
    end

    page.execute_script("if (typeof MessageBus !== 'undefined') { MessageBus.stop(); }")

    # Block all incoming requests before resetting Capybara session which will wait for all requests to finish
    BlockRequestsMiddleware.block_requests!

    Capybara.reset_session!
    MessageBus.backend_instance.reset! # Clears all existing backlog from memory backend
    Discourse.redis.flushdb
  end

  config.after :each do |example|
    if example.exception && RspecErrorTracker.exceptions.present?
      lines = (RSpec.current_example.metadata[:extra_failure_lines] ||= +"")

      lines << "~~~~~~~ SERVER EXCEPTIONS ~~~~~~~"

      RspecErrorTracker.exceptions.each_with_index do |(path, ex), index|
        lines << "\n"
        lines << "Error encountered while proccessing #{path}"
        lines << "  #{ex.class}: #{ex.message}"
        ex.backtrace.each { |line| lines << "    #{line}\n" }
      end

      lines << "~~~~~~~ END SERVER EXCEPTIONS ~~~~~~~"
      lines << "\n"
    end

    unfreeze_time
    ActionMailer::Base.deliveries.clear
  end

  config.before(:each, type: :multisite) do
    Rails.configuration.multisite = true # rubocop:disable Discourse/NoDirectMultisiteManipulation

    RailsMultisite::ConnectionManagement.config_filename = "spec/fixtures/multisite/two_dbs.yml"

    RailsMultisite::ConnectionManagement.establish_connection(db: "default")
  end

  config.after(:each, type: :multisite) do
    ActiveRecord::Base.clear_all_connections!
    Rails.configuration.multisite = false # rubocop:disable Discourse/NoDirectMultisiteManipulation
    RailsMultisite::ConnectionManagement.clear_settings!
    ActiveRecord::Base.establish_connection
  end

  class TestCurrentUserProvider < Auth::DefaultCurrentUserProvider
    def log_on_user(user, session, cookies, opts = {})
      session[:current_user_id] = user.id
      super
    end

    def log_off_user(session, cookies)
      session[:current_user_id] = nil
      super
    end
  end

  # Normally we `use_transactional_fixtures` to clear out a database after a test
  # runs. However, this does not apply to tests done for multisite. The second time
  # a test runs you can end up with stale data that breaks things. This method will
  # force a rollback after using a multisite connection.
  def test_multisite_connection(name)
    RailsMultisite::ConnectionManagement.with_connection(name) do
      ActiveRecord::Base.transaction(joinable: false) do
        yield
        raise ActiveRecord::Rollback
      end
    end
  end
end

class TrackTimeStub
  def self.stubbed
    false
  end
end

def before_next_spec(&callback)
  ($test_cleanup_callbacks ||= []) << callback
end

def global_setting(name, value)
  GlobalSetting.reset_s3_cache!

  GlobalSetting.stubs(name).returns(value)

  before_next_spec { GlobalSetting.reset_s3_cache! }
end

def set_cdn_url(cdn_url)
  global_setting :cdn_url, cdn_url
  Rails.configuration.action_controller.asset_host = cdn_url
  ActionController::Base.asset_host = cdn_url

  before_next_spec do
    Rails.configuration.action_controller.asset_host = nil
    ActionController::Base.asset_host = nil
  end
end

def freeze_time(now = Time.now)
  time = now
  datetime = now

  if Time === now
    datetime = now.to_datetime
  elsif DateTime === now
    time = now.to_time
  else
    datetime = DateTime.parse(now.to_s)
    time = Time.parse(now.to_s)
  end

  if block_given?
    raise "nested freeze time not supported" if TrackTimeStub.stubbed
  end

  DateTime.stubs(:now).returns(datetime)
  Time.stubs(:now).returns(time)
  Date.stubs(:today).returns(datetime.to_date)
  TrackTimeStub.stubs(:stubbed).returns(true)

  if block_given?
    begin
      yield
    ensure
      unfreeze_time
    end
  else
    time
  end
end

def unfreeze_time
  DateTime.unstub(:now)
  Time.unstub(:now)
  Date.unstub(:today)
  TrackTimeStub.unstub(:stubbed)
end

def file_from_fixtures(filename, directory = "images")
  tmp_file_path = File.join(concurrency_safe_tmp_dir, SecureRandom.hex << filename)
  FileUtils.cp("#{Rails.root}/spec/fixtures/#{directory}/#{filename}", tmp_file_path)
  File.new(tmp_file_path)
end

def file_from_contents(contents, filename, directory = "images")
  tmp_file_path = File.join(concurrency_safe_tmp_dir, SecureRandom.hex << filename)
  File.write(tmp_file_path, contents)
  File.new(tmp_file_path)
end

def plugin_from_fixtures(plugin_name)
  tmp_plugins_dir = File.join(concurrency_safe_tmp_dir, "plugins")

  FileUtils.mkdir(tmp_plugins_dir) if !Dir.exist?(tmp_plugins_dir)
  FileUtils.cp_r("#{Rails.root}/spec/fixtures/plugins/#{plugin_name}", tmp_plugins_dir)

  plugin = Plugin::Instance.new
  plugin.path = File.join(tmp_plugins_dir, plugin_name, "plugin.rb")
  plugin
end

def concurrency_safe_tmp_dir
  SpecSecureRandom.value ||= SecureRandom.hex
  dir_path = File.join(Dir.tmpdir, "rspec_#{Process.pid}_#{SpecSecureRandom.value}")
  FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path)
  dir_path
end

def has_trigger?(trigger_name)
  DB.exec(<<~SQL) != 0
    SELECT 1
    FROM INFORMATION_SCHEMA.TRIGGERS
    WHERE trigger_name = '#{trigger_name}'
  SQL
end

def silence_stdout
  STDOUT.stubs(:write)
  yield
ensure
  STDOUT.unstub(:write)
end

def track_log_messages
  old_logger = Rails.logger
  logger = Rails.logger = FakeLogger.new
  yield logger
  logger
ensure
  Rails.logger = old_logger
end

# this takes a string and returns a copy where 2 different
# characters are swapped.
# e.g.
#   swap_2_different_characters("abc") => "bac"
#   swap_2_different_characters("aac") => "caa"
def swap_2_different_characters(str)
  swap1 = 0
  swap2 = str.split("").find_index { |c| c != str[swap1] }
  # if the string is made up of 1 character
  return str if !swap2
  str = str.dup
  str[swap1], str[swap2] = str[swap2], str[swap1]
  str
end

def create_request_env(path: nil)
  env = Rails.application.env_config.dup
  env.merge!(Rack::MockRequest.env_for(path)) if path
  env
end

def create_auth_cookie(token:, user_id: nil, trust_level: nil, issued_at: Time.current)
  data = { token: token, user_id: user_id, trust_level: trust_level, issued_at: issued_at.to_i }
  jar = ActionDispatch::Cookies::CookieJar.build(ActionDispatch::TestRequest.create, {})
  jar.encrypted[:_t] = { value: data }
  CGI.escape(jar[:_t])
end

def decrypt_auth_cookie(cookie)
  ActionDispatch::Cookies::CookieJar.build(
    ActionDispatch::TestRequest.create,
    { _t: cookie },
  ).encrypted[
    :_t
  ].with_indifferent_access
end

def apply_base_chrome_options(options)
  # possible values: undocked, bottom, right, left
  chrome_dev_tools = ENV["CHROME_DEV_TOOLS"]

  if chrome_dev_tools
    options.add_argument("--auto-open-devtools-for-tabs")
    options.add_preference(
      "devtools",
      "preferences" => {
        "currentDockState" => "\"#{chrome_dev_tools}\"",
        "panel-selectedTab" => '"console"',
      },
    )
  end

  options.add_argument("--no-sandbox")
  options.add_argument("--disable-dev-shm-usage")
  options.add_argument("--mute-audio")

  # A file that contains just a list of paths like so:
  #
  # /home/me/.config/google-chrome/Default/Extensions/bmdblncegkenkacieihfhpjfppoconhi/4.9.1_0
  #
  # These paths can be found for each individual extension via the
  # chrome://extensions/ page.
  if ENV["CHROME_LOAD_EXTENSIONS_MANIFEST"].present?
    File
      .readlines(ENV["CHROME_LOAD_EXTENSIONS_MANIFEST"])
      .each { |path| options.add_argument("--load-extension=#{path}") }
  end

  if ENV["CHROME_DISABLE_FORCE_DEVICE_SCALE_FACTOR"].blank?
    options.add_argument("--force-device-scale-factor=1")
  end
end

def aarch64?
  RUBY_PLATFORM == "aarch64-linux"
end

class SpecSecureRandom
  class << self
    attr_accessor :value
  end
end