# frozen_string_literal: true

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

# rubocop:disable Lint/ConstantDefinitionInBlock
# we define some modules and classes inside of blocks like RSpec.configure
# in this file, but we fully expect them to be globally accessible
# moving them outside of the block where they're defined would distance them
# from their use, making things harder to find

begin
  require "debug/prelude"
rescue LoadError
  nil
end

require "crystalball"
require "rspec/openapi"

ENV["RAILS_ENV"] = "test"

# SimpleCov needs to be added first thing in the spec_helper.rb
# No other configurations should be loaded/performed before this block
if ENV["COVERAGE"] == "1" && (ENV["SUPPRESS_OUTPUT"] != "1")
  require_relative("canvas_simplecov")
  require_relative("coverage_tool")
  puts "Code Coverage enabled" unless ENV["SUPPRESS_OUTPUT"] == "1"
  CoverageTool.start("RSpec")
end

if ENV["CRYSTALBALL_MAP"] == "1"
  require_relative("support/crystalball")

  Coverage.start unless Coverage.running?
  Crystalball::MapGenerator.start! do |config|
    config.register Crystalball::MapGenerator::CoverageStrategy.new
    config.map_storage_path = "log/results/crystalball_results/#{SecureRandom.uuid}_#{ENV.fetch("PARALLEL_INDEX", "0")}_map.yml"
    config.dump_threshold = 50_000
  end

  module Crystalball
    class MapGenerator
      class CoverageStrategy
        def after_register
          Coverage.start unless Coverage.running?
        end

        def call(example_map, example)
          puts "Calling Coverage Strategy for #{example.inspect}"
          before = Coverage.peek_result
          yield example_map, example
          after = Coverage.peek_result
          example_map.push(*execution_detector.detect(before, after).sort)

          if example.metadata[:location].include?("selenium")
            # rubocop:disable Specs/NoExecuteScript
            js_coverage = ::SeleniumDriverSetup.driver.execute_script("return window.__coverage__")&.keys&.uniq
            # rubocop:enable Specs/NoExecuteScript
            example_map.used_files.concat(js_coverage.sort) if js_coverage
          end
        end
      end
    end
  end
end

require_relative "../config/environment"

require "rspec/rails"

require "webmock"
require "webmock/rspec/matchers"
WebMock.allow_net_connect!
WebMock.enable!
# unlike webmock/rspec, only reset in groups that actually do stubbing
module WebMock::API
  include WebMock::Matchers

  def self.included(other)
    other.before { allow(CanvasHttp).to receive(:insecure_host?).and_return(false) }
    other.after { WebMock.reset! }
  end
end

require "delayed/testing"
Rails.root.glob("spec/support/**/*.rb") { |f| require f }
require "sharding_spec_helper"

# nuke the db (say, if `rake db:migrate RAILS_ENV=test` created records),
# and then ensure people aren't creating records outside the rspec
# lifecycle, e.g. inside a describe/context block rather than a
# let/before/example
TestDatabaseUtils.reset_database! unless ENV["DB_VALIDITY_ENSURED"] == "1"
TestDatabaseUtils.check_migrations! unless ENV["DB_VALIDITY_ENSURED"] == "1"
Setting.reset_cache!
BlankSlateProtection.install!

ActionView::TestCase::TestController.view_paths = ApplicationController.view_paths
ActionView::Base.streaming_completion_on_exception = "</html>"

# this makes sure that a broken transaction becomes functional again
# by the time we hit rescue_action_in_public, so that the error report
# can be recorded
module SpecTransactionWrapper
  def self.wrap_block_in_transaction(block)
    exception = nil
    ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
      block.call
    rescue ActiveRecord::StatementInvalid
      # these need to properly roll back the transaction
      raise
    rescue
      # anything else, the transaction needs to commit, but we need to re-raise outside the transaction
      exception = $!
    end
    raise exception if exception
  end
end
ActionController::Base.set_callback(:process_action,
                                    :around,
                                    ->(_r, block) { SpecTransactionWrapper.wrap_block_in_transaction(block) })

ActionController::Base.set_callback(:process_action,
                                    :before,
                                    ->(_r) { @streaming_template = false })

module RSpec::Core::Hooks
  class AfterContextHook < Hook
    def run(example)
      exception_class = if defined?(RSpec::Support::AllExceptionsExceptOnesWeMustNotRescue)
                          RSpec::Support::AllExceptionsExceptOnesWeMustNotRescue
                        else
                          Exception
                        end
      example.instance_exec(example, &block)
    rescue exception_class => e
      # TODO: Come up with a better solution for this.
      RSpec.configuration.reporter.message <<~TEXT
        An error occurred in an `after(:context)` hook.
          #{e.class}: #{e.message}
          occurred at #{e.backtrace.join("\n")}
      TEXT
    end
  end
end

module TimeComparisonWithRounding
  # ignore nanoseconds, since the database doesn't persist them
  def <=>(other)
    other = other.change(usec: other.usec) if other.respond_to?(:usec)
    if nsec % 1_000 == 0
      super
    else
      change(usec:) <=> other
    end
  end
end
Time.prepend(TimeComparisonWithRounding)

# we use ivars too extensively for factories; prevent them from
# being propagated to views in view specs
# yes, I'm overwriting the method in-place, rather than prepend,
# because the ancestor chain for RSpec::Rails::ViewExampleGroup
# has already been built, and I can't put myself between the two
module ActionView::TestCase::Behavior
  def view_assigns
    if is_a?(RSpec::Rails::HelperExampleGroup)
      # the original implementation. we can't call super because
      # we replaced the whole original method
      return _user_defined_ivars.to_h do |ivar|
        [ivar[1..].to_sym, instance_variable_get(ivar)]
      end
    end
    {}
  end
end

if ENV["ENABLE_AXE_SELENIUM"] == "1"
  require "stormbreaker"
  Stormbreaker.install!
  Stormbreaker.configure do |config|
    config.driver = -> { SeleniumDriverSetup.driver }
    config.skip = [:"color-contrast", :"duplicate-id"]
    config.rules = %i[wcag2a wcag2aa section508]
    if ENV["RSPEC_PROCESSES"]
      config.serialize_output = true
      config.serialize_prefix = "log/results/stormbreaker_results"
    end
  end
end

module RSpec::Rails
  module ViewExampleGroup
    module ExampleMethods
      delegate :content_for, to: :view
    end
  end

  RSpec::Matchers.define :have_tag do |expected|
    match do |actual|
      !!Nokogiri::HTML5(actual).at_css(expected)
    end
  end

  RSpec::Matchers.define :be_checked do
    match do |node|
      if node.is_a?(Nokogiri::XML::Element)
        node.attr("checked") == "checked"
      elsif node.respond_to?(:checked?)
        node.checked?
      else
        node.attribute("checked")
      end
    end
  end
end

module ReadOnlySecondaryStub
  def self.reset
    ActiveRecord::Base.connection.execute("RESET ROLE")
    Thread.current[:stubbed_guard_rail_env] = nil
  end

  def datbase_username
    Rails.configuration.database_configuration.dig("test", "username") ||
      `whoami`.strip
  end

  def readonly_user_exists?
    return @read_only_user if instance_variable_defined?(:@read_only_user)

    @read_only_user = !!ActiveRecord::Base.connection.select_value("SELECT 1 AS one FROM pg_roles WHERE pg_roles.rolname='canvas_readonly_user'")
  end

  def readonly_user_can_read?
    return @literate if instance_variable_defined?(:@literate)

    sql = "SELECT privilege_type FROM information_schema.table_privileges WHERE grantee ='canvas_readonly_user' AND table_name = 'courses'"
    @literate = ActiveRecord::Base.connection.select_values(sql).include?("SELECT")
  end

  def test_db_name
    ActiveRecord::Base.connection.current_database
  end

  def switch_role!(env)
    if readonly_user_exists? && readonly_user_can_read?
      ActiveRecord::Base.connection.execute((env == :secondary) ? "SET ROLE canvas_readonly_user" : "RESET ROLE")
    else
      puts "The database #{test_db_name} is not setup with a secondary/readonly_user to fix run the following."
      puts "psql -c 'ALTER USER #{datbase_username} CREATEDB CREATEROLE' -d #{test_db_name}"
      puts "psql -c 'GRANT canvas_readonly_user TO #{datbase_username}' -d #{test_db_name}"
      puts "RAILS_ENV=#{Rails.env} bundle exec rake db:migrate:redo VERSION=20211101220306"
    end
  end

  def environment
    Thread.current[:stubbed_guard_rail_env] || super
  end

  def activate(env)
    return super if environment == :deploy
    return super unless [:primary, :secondary].include?(env)

    previous_stub = Thread.current[:stubbed_guard_rail_env]
    previous_env = previous_stub || :primary
    return yield if previous_env == env

    begin
      switch_role!(env)
      Thread.current[:stubbed_guard_rail_env] = env
      yield
    ensure
      switch_role!(previous_env)
      Thread.current[:stubbed_guard_rail_env] = previous_stub
    end
  end
end
GuardRail.singleton_class.prepend ReadOnlySecondaryStub

module ForceTransactionCommitCallbacksToPrimary
  def commit_records
    GuardRail.activate(:primary) do
      super
    end
  end
end
ActiveRecord::ConnectionAdapters::Transaction.prepend ForceTransactionCommitCallbacksToPrimary

module RenderWithHelpers
  def assign(key, value)
    @assigned_variables ||= {}
    @assigned_variables[key] = value
    super
  end

  def render(*args)
    controller_class = begin
      "#{@controller.controller_path.camelize}Controller".constantize
    rescue NameError
      ApplicationController
    end

    controller_class.instance_variable_set(:@js_env, nil)
    # this extends the controller's helper methods to the view
    # however, these methods are delegated to the test controller
    view.singleton_class.class_eval do
      include controller_class._helpers unless included_modules.include?(controller_class._helpers)
    end

    # so create a "real_controller"
    # and delegate the helper methods to it
    @controller.singleton_class.class_eval do
      attr_accessor :real_controller

      controller_class._helper_methods.each do |helper|
        class_eval <<~RUBY, __FILE__, __LINE__ + 1
          def #{helper}(*args, **kwargs, &block)
            real_controller.send(:#{helper}, *args, **kwargs, &block)
          end
        RUBY
      end
    end

    real_controller = controller_class.new
    real_controller.instance_variable_set(:@_request, @controller.request)
    real_controller.instance_variable_set(:@context, @controller.instance_variable_get(:@context))
    @assigned_variables&.each do |key, value|
      real_controller.instance_variable_set(:"@#{key}", value)
    end
    if real_controller.instance_variable_get(:@domain_root_account).nil?
      real_controller.instance_variable_set(:@domain_root_account, Account.default)
    end
    @controller.real_controller = real_controller

    # just calling "render 'path/to/view'" by default looks for a partial
    if args.first.is_a?(String)
      file = args.shift
      args = [{ template: file }] + args
    end
    super
  end
end
RSpec::Rails::ViewExampleGroup::ExampleMethods.prepend(RenderWithHelpers)

require "rspec_mock_extensions"
require "ams_spec_helper"

require "i18n_tasks"
require "factories"

Dir[File.dirname(__FILE__) + "/shared_examples/**/*.rb"].each { |f| require f }

# rspec aliases :describe to :context in a way that it's pretty much defined
# globally on every object. :context is already heavily used in our application,
# so we remove rspec's definition. This does not prevent 'context' from being
# used within a 'describe' block.

if defined?(Spec::DSL::Main)
  module Spec::DSL::Main
    remove_method :context if respond_to? :context
  end
end

RSpec::Mocks.configuration.allow_message_expectations_on_nil = false

module RSpec::Matchers::Helpers
  # allows for matchers to use symbols and literals even though URIs are always strings.
  # i.e. `and_query({assignment_id: @assignment.id})`
  def self.cast_to_strings(expected:)
    expected.to_h { |k, v| [k.to_s, v.to_s] }
  end
end

module Helpers
  def assert_status(status = 500)
    expect(response.status.to_i).to eq status
  end

  def assert_unauthorized
    # we allow either a raw unauthorized or a redirect to login
    if response.status.to_i == 401
      assert_status(401)
    else
      # Certain responses require more privileges than the current user has (ie site admin)
      expect(response).to redirect_to(login_url)
        .or redirect_to(root_url)
    end
  end

  def assert_forbidden
    assert_status(403)
  end

  def assert_page_not_found
    yield
    assert_status(404)
  end

  def assert_require_login
    expect(response).to be_redirect
    expect(flash[:warning]).to eq "You must be logged in to access this page"
  end
end

RSpec::Expectations.configuration.on_potential_false_positives = :raise

require "rspec_junit_formatter"

RSpec.configure do |config|
  config.example_status_persistence_file_path = Rails.root.join("tmp/rspec#{ENV.fetch("PARALLEL_INDEX", "0").to_i}")
  config.fail_if_no_examples = true
  config.use_transactional_fixtures = true
  config.use_instantiated_fixtures = false
  config.fixture_paths = [Rails.root.join("spec/fixtures")]
  config.infer_spec_type_from_file_location!
  config.raise_errors_for_deprecations!
  config.color = true
  config.order = :random
  config.filter_run_when_matching :focus

  # The Pact specs have prerequisite setup steps so we exclude them by default
  config.filter_run_excluding :pact_live_events if ENV.fetch("RUN_LIVE_EVENTS_CONTRACT_TESTS", "0") == "0"

  if ENV["CRYSTALBALL_MAP"] == "1"
    config.filter_run_excluding :pact_live_events
    config.filter_run_excluding :pact
  end

  # The Pact build needs RspecJunitFormatter and does not run RSpecQ
  file = "log/results/results-#{ENV.fetch("PARALLEL_INDEX", "0").to_i}.xml"
  config.add_formatter "RspecJunitFormatter", file if (ENV["PACT_BROKER"] && ENV["JENKINS_HOME"]) || ENV["CRYSTALBALL_MAP"] == "1"

  config.include Helpers
  config.include Factories
  config.include RequestHelper, type: :request
  config.include Onceler::BasicHelpers
  config.include ActionDispatch::TestProcess::FixtureFile
  config.project_source_dirs << "gems" # so that failures here are reported properly

  if ENV["RSPEC_LOG"]
    config.add_formatter "ParallelTests::RSpec::RuntimeLogger", "log/parallel_runtime/parallel_runtime_rspec_tests-#{ENV.fetch("PARALLEL_INDEX", "0").to_i}.log"
  end

  if ENV["RAILS_LOAD_ALL_LOCALES"] && RSpec.configuration.filter.rules[:i18n]
    config.around do |example|
      SpecMultipleLocales.run(example)
    end
  end

  if ENV["OPENAPI"]
    config.define_derived_metadata(file_path: %r{spec/controllers}) do |metadata|
      metadata[:attempt_openapi_generation] = true
    end

    config.after(:example, :attempt_openapi_generation) do |example|
      OpenApiGenerator.generate(self, example)
    end

    config.after(:suite) do
      result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
      result_recorder.record_results!
      if result_recorder.errors?
        error_message = result_recorder.error_message
        colorizer = RSpec::Core::Formatters::ConsoleCodes
        RSpec.configuration.reporter.message colorizer.wrap(error_message, :failure)
      end
    end
  end

  config.around do |example|
    Rails.logger.info "STARTING SPEC #{example.full_description}"
    SpecTimeLimit.enforce(example, &example)
  end

  def reset_all_the_things!
    LocalCache.reset
    ReadOnlySecondaryStub.reset
    Time.zone = "UTC"
    LoadAccount.force_special_account_reload = true
    Account.clear_special_account_cache!(true)
    PluginSetting.current_account = nil
    AdheresToPolicy::Cache.clear
    Setting.reset_cache!
    HostUrl.reset_cache!
    Notification.reset_cache!
    ActiveRecord::Base.reset_any_instantiation!
    Folder.reset_path_lookups!
    Rails.logger.try(:info, "Running #{self.class.description} #{@method_name}")
    Attachment.current_root_account = nil
    DynamicSettings.reset_cache!
    ActiveRecord::Migration.verbose = false
    RequestStore.clear!
    MultiCache.reset
    TermsOfService.skip_automatic_terms_creation = true
    LiveEvents.clear_context!
    PageView.reset_cache! if PageView.respond_to?(:reset_cache!)
    $spec_api_tokens = {}

    remove_user_session
  end

  Notification.after_create do
    Notification.reset_cache!
    BroadcastPolicy.notification_finder.refresh_cache
  end

  # UTC for tests, cuz it's easier :P
  Account.time_zone_attribute_defaults[:default_time_zone] = "UTC"

  config.before :all do
    raise "all specs need to use transactions" unless using_transactions_properly?
  end

  Onceler.configure do |c|
    c.before :record do
      reset_all_the_things!
      Canvas::DynamoDB::DatabaseBuilder.reset
    end
  end

  config.before do
    raise "all specs need to use transactions" unless using_transactions_properly?

    reset_all_the_things!
  end

  # normally all specs should always use transactions; you can override
  # this in a specific example group if you need to do something fancy/
  # crazy/slow. but you probably don't. seriously. just use once-ler
  def using_transactions_properly?
    use_transactional_tests
  end

  config.before :suite do
    if ENV["COVERAGE"] == "1"
      simple_cov_cmd = "rspec:#{Process.pid}"
      puts "Starting SimpleCov command: #{simple_cov_cmd}"
      SimpleCov.command_name(simple_cov_cmd)
      SimpleCov.pid = Process.pid # because https://github.com/colszowka/simplecov/pull/377
    end

    Timecop.safe_mode = true

    # cache brand variables because if we try to look them up inside a Timecop
    # block, we will conflict our active record patch to prevent future
    # migrations.
    BrandableCSS.default_variables_md5
  end

  config.before do
    allow(AttachmentFu::Backends::S3Backend).to receive(:load_s3_config) { StubS3::AWS_CONFIG.dup }
    allow(Canvas::Vault).to receive(:read) { StubVault::AWS_CONFIG.dup }
  end

  # flush redis before the first spec, and before each spec that comes after
  # one that used redis
  module TrackRedisUsage
    def self.prepended(klass)
      klass.send(:attr_accessor, :redis_used)
    end

    def redis(*)
      self.redis_used = true
      super
    end
  end
  CanvasCache::Redis.singleton_class.prepend(TrackRedisUsage)
  CanvasCache::Redis.redis_used = true

  config.before do
    if CanvasCache::Redis.enabled? && CanvasCache::Redis.redis_used
      # yes, we really mean to run this dangerous redis command
      GuardRail.activate(:deploy) { CanvasCache::Redis.redis.flushdb(failsafe: nil) }
    end
    CanvasCache::Redis.redis_used = false
    @last_error_report_id = ErrorReport.maximum(:id)
  end

  class ErrorReportExceptionWrapper
    class ExceptionClassWrapper < SimpleDelegator
      attr_reader :name

      def initialize(klass, name)
        super(klass)
        @name = name
      end
    end

    def initialize(error_report)
      @error_report = error_report
      @klass = ExceptionClassWrapper.new(self.class, error_report.category)
    end

    delegate :message, to: :@error_report

    def class
      @klass
    end

    def cause; end

    def backtrace
      @error_report.backtrace.split("\n")
    end
  end

  config.after do |example|
    if %i[controller request].include?(example.metadata[:type]) &&
       example.exception &&
       !(errors = @last_error_report_id.nil? ? ErrorReport.all.to_a : ErrorReport.where("id>?", @last_error_report_id).to_a).empty?
      errors.each do |er|
        example.set_exception(ErrorReportExceptionWrapper.new(er))
      end
    end
  end

  # ****************************************************************
  # There used to be a lot of factory methods here!
  # In an effort to move us toward a nicer test factory solution,
  # all factories should now live in a separate file named to
  # correspond with the model that should be built by the factory.
  # Please see spec/factories for examples!
  # ****************************************************************

  def login_as(username = "nobody@example.com", password = "asdfasdf")
    post "/login/canvas",
         params: { "pseudonym_session[unique_id]" => username,
                   "pseudonym_session[password]" => password }
    follow_redirect! while response.redirect?
    assert_response :success
    expect(request.fullpath).to eq "/?login_success=1"
  end

  # Instead of directly comparing urls
  # this will make sure urls match
  # by parsing them, and comparing the results
  # meaning these would match
  #   http://test.dev/?foo=bar&other=1
  #   http://test.dev/?other=1&foo=bar
  def assert_url_parse_match(test_url, expected_url)
    parsed_test = URI.parse(test_url)
    parsed_expected = URI.parse(expected_url)

    parsed_test_query = Rack::Utils.parse_nested_query(parsed_test.query)
    parsed_expected_query = Rack::Utils.parse_nested_query(parsed_expected.query)

    expect(parsed_test.scheme).to eq parsed_expected.scheme
    expect(parsed_test.host).to eq parsed_expected.host
    expect(parsed_test_query).to eq parsed_expected_query
  end

  def assert_hash_contains(test_hash, expected_hash)
    expected_hash.each do |key, expected_value|
      expect(test_hash[key]).to eq expected_value
    end
  end

  def fixture_file_upload(path, mime_type = nil, binary = false)
    Rack::Test::UploadedFile.new(file_fixture(path), mime_type, binary)
  end

  def default_uploaded_data
    fixture_file_upload("docs/doc.doc", "application/msword", true)
  end

  def create_temp_dir!
    dir = Dir.mktmpdir
    @temp_dirs ||= []
    @temp_dirs << dir
    dir
  end

  def generate_csv_file(lines)
    tmp = Tempfile.new("sis_rspec")
    path = "#{tmp.path}.csv"
    tmp.close!
    File.open(path, "w+") { |f| f.puts lines.flatten.join "\n" }
    path
  end

  def process_csv_data(*lines)
    opts = lines.extract_options!
    opts.reverse_merge!(allow_printing: false)
    account = opts[:account] || @account || account_model
    user = opts[:user] || @user || user_model
    opts[:batch] ||= account.sis_batches.create!(user_id: user.id)

    path = generate_csv_file(lines)
    opts[:files] = [path]

    importer = SIS::CSV::ImportRefactored.process(account, opts)
    run_jobs

    File.unlink path

    importer
  end

  def process_csv_data_cleanly(*lines_or_opts)
    importer = process_csv_data(*lines_or_opts)
    raise "csv errors: #{importer.errors.inspect}" if importer.errors.present?

    importer
  end

  def set_cache(new_cache)
    cache_opts = {}
    if new_cache == :redis_cache_store
      if CanvasCache::Redis.enabled?
        cache_opts[:redis] = CanvasCache::Redis.redis
      else
        skip "redis required"
      end
    elsif new_cache == :memory_store
      cache_opts[:coder] = Marshal
    end
    new_cache ||= :null_store
    new_cache = ActiveSupport::Cache.lookup_store(new_cache, cache_opts)
    allow(Rails).to receive(:cache).and_return(new_cache)
    allow(ActionController::Base).to receive_messages(cache_store: new_cache, perform_caching: true)
    allow_any_instance_of(ActionController::Base).to receive(:cache_store).and_return(new_cache)
    allow_any_instance_of(ActionController::Base).to receive(:perform_caching).and_return(true)
    allow(MultiCache).to receive(:cache).and_return(new_cache)
  end

  def specs_require_cache(new_cache = :memory_store)
    before do
      set_cache(new_cache)
    end
  end

  def enable_cache(new_cache = :memory_store)
    previous_cache = Rails.cache
    previous_perform_caching = ActionController::Base.perform_caching
    previous_multicache = MultiCache.cache
    set_cache(new_cache)
    if block_given?
      begin
        yield
      ensure
        allow(Rails).to receive(:cache).and_return(previous_cache)
        allow(ActionController::Base).to receive_messages(cache_store: previous_cache, perform_caching: previous_perform_caching)
        allow_any_instance_of(ActionController::Base).to receive(:cache_store).and_return(previous_cache)
        allow_any_instance_of(ActionController::Base).to receive(:perform_caching).and_return(previous_perform_caching)
        allow(MultiCache).to receive(:cache).and_return(previous_multicache)
      end
    end
  end

  # enforce forgery protection, so we can verify usage of the authenticity token
  def enable_forgery_protection(enable = true)
    old_value = ActionController::Base.allow_forgery_protection
    allow(ActionController::Base).to receive(:allow_forgery_protection).and_return(enable)
    allow_any_instance_of(ActionController::Base).to receive(:allow_forgery_protection).and_return(enable)

    yield if block_given?
  ensure
    if block_given?
      allow(ActionController::Base).to receive(:allow_forgery_protection).and_return(old_value)
      allow_any_instance_of(ActionController::Base).to receive(:allow_forgery_protection).and_return(old_value)
    end
  end

  def stub_kaltura
    # trick kaltura into being activated
    allow(CanvasKaltura.plugin_settings).to receive(:settings).and_return({
                                                                            "domain" => "kaltura.example.com",
                                                                            "resource_domain" => "cdn.kaltura.example.com",
                                                                            "rtmp_domain" => "rtmp.kaltura.example.com",
                                                                            "partner_id" => "100",
                                                                            "subpartner_id" => "10000",
                                                                            "secret_key" => "fenwl1n23k4123lk4hl321jh4kl321j4kl32j14kl321",
                                                                            "user_secret_key" => "1234821hrj3k21hjk4j3kl21j4kl321j4kl3j21kl4j3k2l1",
                                                                            "player_ui_conf" => "1",
                                                                            "kcw_ui_conf" => "1",
                                                                            "upload_ui_conf" => "1",
                                                                            "hide_rte_button" => false
                                                                          })
  end

  def override_dynamic_settings(data)
    original_fallback = DynamicSettings.fallback_data
    DynamicSettings.fallback_data = data
    yield
  ensure
    DynamicSettings.fallback_data = original_fallback
  end

  def json_parse(json_string = response.body)
    JSON.parse(json_string)
  end

  # inspired by http://blog.jayfields.com/2007/08/ruby-calling-methods-of-specific.html
  module AttachmentStorageSwitcher
    BACKENDS = %w[FileSystem S3].map { |backend| AttachmentFu::Backends.const_get(:"#{backend}Backend") }.freeze

    class As # :nodoc:
      private(*instance_methods.grep_v(/(^__|^\W|^binding$|^untaint$|^object_id$)/))

      def initialize(subject, ancestor)
        @subject = subject
        @ancestor = ancestor
      end

      def method_missing(sym, ...)
        @ancestor.instance_method(sym).bind_call(@subject, ...)
      end
    end

    def self.included(base)
      base.cattr_accessor :current_backend
      base.current_backend = (base.ancestors & BACKENDS).first

      # make sure we have all the backends
      BACKENDS.each do |backend|
        base.include(backend) unless base.ancestors.include?(backend)
      end
      # remove the duplicate callbacks added by multiple backends
      base.before_update.uniq!

      BACKENDS.map(&:instance_methods).flatten.uniq.each do |method|
        # overridden by Attachment anyway; don't re-overwrite it
        next if base.instance_method(method).owner == base

        if method.to_s[-1..] == "="
          base.class_eval <<~RUBY, __FILE__, __LINE__ + 1
            def #{method}(arg)
              self.as(self.class.current_backend).#{method} arg
            end
          RUBY
        else
          base.class_eval <<~RUBY, __FILE__, __LINE__ + 1
            def #{method}(*args, &block)
              self.as(self.class.current_backend).#{method}(*args, &block)
            end
          RUBY
        end
      end
    end

    def as(ancestor)
      @__as ||= {}
      unless (r = @__as[ancestor])
        r = (@__as[ancestor] = As.new(self, ancestor))
      end
      r
    end
  end

  module StubS3
    AWS_CONFIG = {
      access_key_id: "stub_id",
      secret_access_key: "stub_key",
      credentials: Aws::Credentials.new("stub_id", "stub_key"),
      region: "us-east-1",
      stub_responses: true,
      bucket_name: "no-bucket"
    }.freeze

    def self.stubbed?
      false
    end

    def load(file, *args)
      return AWS_CONFIG.dup if StubS3.stubbed? && file == "amazon_s3"

      super
    end
  end

  module StubVault
    AWS_CONFIG = {
      access_key: "stub_access_key",
      secret_key: "stub_secret_key",
      security_token: "stub_security_token"
    }.freeze
  end

  def s3_storage!(opts = { stubs: true })
    [Attachment, Thumbnail].each do |model|
      model.include(AttachmentStorageSwitcher) unless model.ancestors.include?(AttachmentStorageSwitcher)
      allow(model).to receive_messages(current_backend: AttachmentFu::Backends::S3Backend,
                                       s3_storage?: true,
                                       local_storage?: false)
    end

    if opts[:stubs]
      ConfigFile.singleton_class.prepend(StubS3)
      allow(StubS3).to receive(:stubbed?).and_return(true)
    elsif Attachment.s3_config.blank? || Attachment.s3_config[:access_key_id] == "access_key"
      skip "Please put valid S3 credentials in config/amazon_s3.yml"
    end
  end

  def local_storage!
    [Attachment, Thumbnail].each do |model|
      model.include(AttachmentStorageSwitcher) unless model.ancestors.include?(AttachmentStorageSwitcher)
      allow(model).to receive_messages(current_backend: AttachmentFu::Backends::FileSystemBackend,
                                       s3_storage?: false,
                                       local_storage?: true)
    end
  end

  def run_job(job) # rubocop:disable Rails/Delegate
    Delayed::Testing.run_job(job)
  end

  def run_jobs
    Delayed::Testing.drain
  end

  def track_jobs(&)
    @jobs_tracking = Delayed::JobTracking.track(&)
  end

  def created_jobs
    @jobs_tracking.created
  end

  def expects_job_with_tag(tag, count = 1, &)
    track_jobs(&)
    expect(created_jobs.count { |j| j.tag == tag }).to eq count
  end

  def content_type_key
    "Content-Type"
  end

  class FakeHttpResponse
    def initialize(code, body = nil, headers = {})
      @code = code
      @body = body
      @headers = headers
    end

    def read_body(io = nil)
      return yield(@body) if block_given?
      return if io.nil?

      io << @body
    end

    def code
      @code.to_s
    end

    delegate :[], to: :@headers

    def content_type
      self["content-type"]
    end
  end

  # frd class, not a mock, so we can once-ler WebConferences (need to Marshal.dump)
  class WebConferencePluginMock
    attr_reader :id, :settings

    def initialize(id, settings)
      @id = id
      @settings = settings
    end

    def valid_settings?
      true
    end

    def enabled?
      true
    end

    def base; end

    def name
      id.to_s.humanize
    end
  end

  def web_conference_plugin_mock(id, settings)
    WebConferencePluginMock.new(id, settings)
  end

  def dummy_io
    fixture_file_upload("docs/doc.doc", "application/msword", true)
  end

  def consider_all_requests_local(value)
    old_value = Rails.application.config.consider_all_requests_local
    Rails.application.config.consider_all_requests_local = value
    yield
  ensure
    Rails.application.config.consider_all_requests_local = old_value
  end

  def skip_if_prepended_class_method_stubs_broken
    skip("stubbing prepended class methods is broken in new versions of ruby") # rubocop:disable Specs/NoSkipWithoutDate,Specs/NoSkipWithoutTicket
  end
end

module I18nStubs
  def stub(translations)
    new_locales = translations.keys - I18n.config.available_locales
    @stubs = translations.with_indifferent_access
    unless new_locales.empty?
      I18n.config.available_locales = I18n.config.available_locales + new_locales
    end
    yield
  ensure
    @stubs = nil
    unless new_locales.empty?
      I18n.config.available_locales = I18n.config.available_locales - new_locales
    end
  end

  def lookup(locale, key, scope = [], options = {})
    return super unless @stubs

    init_translations unless initialized?
    keys = I18n.normalize_keys(locale, key, scope, options[:separator])
    keys.inject(@stubs) { |h, k| h[k] if h.respond_to?(:key) } || super
  end

  def available_locales
    return super unless @stubs

    super | @stubs.keys.map(&:to_sym)
  end
end
I18n.backend.class.prepend(I18nStubs)

Rails.root.glob("{gems,vendor}/plugins/*/spec_canvas/spec_helper.rb") { |file| require file }

module DeveloperKeyStubs
  @@original_get_special_key = DeveloperKey.method(:get_special_key)

  def original_get_special_key(*, **)
    @@original_get_special_key.call(*, **)
  end

  def get_special_key(default_key_name, create_if_missing: true)
    Shard.birth.activate do
      @special_keys ||= {}

      # TODO: we have to do this because tests run in transactions
      testkey = DeveloperKey.where(name: default_key_name).first_or_initialize
      return nil if testkey.new_record? && !create_if_missing

      testkey.auto_expire_tokens = false if testkey.new_record?
      testkey.sns_arn = "arn:aws:s3:us-east-1:12345678910:foo/bar"
      testkey.save! if testkey.changed?
      return @special_keys[default_key_name] = testkey
    end
  end
end
DeveloperKey.singleton_class.prepend DeveloperKeyStubs

def enable_developer_key_account_binding!(developer_key)
  developer_key.developer_key_account_bindings.first.update!(
    workflow_state: "on"
  )
end

def disable_developer_key_account_binding!(developer_key)
  developer_key.developer_key_account_bindings.first.update!(
    workflow_state: "off"
  )
end

def enable_default_developer_key!
  enable_developer_key_account_binding!(DeveloperKey.default)
end

# register mime types for their responses being decoded as JSON
Mime::SET.select { |t| t.to_s.end_with?("+json") }.map(&:ref).each do |type|
  ActionDispatch::RequestEncoder.register_encoder(type,
                                                  response_parser: ->(body) { JSON.parse(body) })
end

# rubocop:enable Lint/ConstantDefinitionInBlock

module FileUpdateCheckerIgnoreTimecop
  private

  # Prevent Timecop from interfering with file update checking or we
  # can see weird Zeitwerk issues due to inappropriate constant reloading.
  def max_mtime(...)
    if Timecop.travelled? || Timecop.frozen?
      Timecop.return do
        super
      end
    else
      super
    end
  end
end

ActiveSupport::FileUpdateChecker.prepend(FileUpdateCheckerIgnoreTimecop)
