# frozen_string_literal: true

require File.expand_path('../spec_helper',  __dir__)
require_test_helper 'controller_helpers'
require_test_helper 'exception_helpers'

describe ExceptionHandling do
  include ControllerHelpers
  include ExceptionHelpers

  before do
    @fail_count = 0
  end

  def dont_stub_log_error
    true
  end

  def append_organization_info_config(data)
    data[:user_details]                = {}
    data[:user_details][:username]     = "CaryP"
    data[:user_details][:organization] = "Invoca Engineering Dept."
  rescue StandardError
    # don't let these out!
  end

  def custom_data_callback_returns_nil_message_exception(_data)
    raise_exception_with_nil_message
  end

  def log_error_callback(_data, _ex, _treat_like_warning, _honeybadger_status)
    @fail_count += 1
  end

  def log_error_callback_config(data, _ex, treat_like_warning, honeybadger_status)
    @callback_data = data
    @treat_like_warning = treat_like_warning
    @fail_count += 1
    @honeybadger_status = honeybadger_status
  end

  def log_error_callback_with_failure(_data, _ex)
    raise "this should be rescued"
  end

  def log_error_callback_returns_nil_message_exception(_data, _ex)
    raise_exception_with_nil_message
  end

  module EventMachineStub
    class << self
      attr_accessor :block

      def schedule(&block)
        @block = block
      end
    end
  end

  class DNSResolvStub
    class << self
      attr_accessor :callback_block
      attr_accessor :errback_block

      def resolve(_hostname)
        self
      end

      def callback(&block)
        @callback_block = block
      end

      def errback(&block)
        @errback_block = block
      end
    end
  end

  class SmtpClientStub
    class << self
      attr_reader :block
      attr_reader :last_method

      def errback(&block)
        @block = block
      end

      def send_hash
        @send_hash ||= {}
      end

      def send(hash)
        @last_method = :send
        send_hash.clear
        send_hash.merge!(hash)
        self
      end

      def asend(hash)
        send(hash)
        @last_method = :asend
        self
      end
    end
  end

  class SmtpClientErrbackStub < SmtpClientStub
  end

  context "with warn and honeybadger notify stubbed" do
    before do
      allow(ExceptionHandling).to receive(:warn).with(any_args)
      allow(Honeybadger).to receive(:notify).with(any_args)
    end

    context "with logger stashed" do
      before { @original_logger = ExceptionHandling.logger }
      after { ExceptionHandling.logger = @original_logger }

      it "stores logger as-is if it has ContextualLogger::Mixin" do
        logger = Logger.new('/dev/null')
        logger.extend(ContextualLogger::LoggerMixin)
        ancestors = logger.singleton_class.ancestors.*.name

        ExceptionHandling.logger = logger
        expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to eq(ancestors)
      end

      it "allows logger = nil (no deprecation warning)" do
        expect(STDERR).to receive(:puts).with(/DEPRECATION WARNING/).never
        ExceptionHandling.logger = nil
      end

      it "[deprecated] mixes in ContextualLogger::Mixin if not there" do
        expect(STDERR).to receive(:puts).with(/DEPRECATION WARNING: implicit extend with ContextualLogger::LoggerMixin is deprecated and will be removed from exception_handling 3\.0/)
        logger = Logger.new('/dev/null')
        ancestors = logger.singleton_class.ancestors.*.name

        ExceptionHandling.logger = logger
        expect(ExceptionHandling.logger).to be_kind_of(ContextualLogger::LoggerMixin)
        expect(ExceptionHandling.logger.singleton_class.ancestors.*.name).to_not eq(ancestors)
      end

      context "#log_error" do
        it "takes in additional logging context hash and pass it to the logger" do
          ExceptionHandling.log_error('This is an Error', 'This is the prefix context', service_name: 'exception_handling')
          expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Error/)
          expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
          expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
        end

        it "logs with Severity::FATAL" do
          ExceptionHandling.log_error('This is a Warning', service_name: 'exception_handling')
          expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
        end
      end
    end

    context "#log_warning" do
      it "have empty array as a backtrace" do
        expected_args = if RUBY_VERSION < '2.7'
                          [ExceptionHandling::Warning, anything]
                        else
                          [ExceptionHandling::Warning]
                        end
        expect(ExceptionHandling).to receive(:log_error).with(*expected_args) do |error|
          expect(error.backtrace).to eq([])
        end
        ExceptionHandling.log_warning('Now with empty array as a backtrace!')
      end

      it "take in additional key word args as logging context and pass them to the logger" do
        ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
        expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Warning/)
        expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
        expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
      end

      it "log with Severity::WARN" do
        ExceptionHandling.log_warning('This is a Warning', service_name: 'exception_handling')
        expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
      end
    end

    context "#log_info" do
      it "take in additional key word args as logging context and pass them to the logger" do
        ExceptionHandling.log_info('This is an Info', service_name: 'exception_handling')
        expect(logged_excluding_reload_filter.last[:message]).to match(/This is an Info/)
        expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
        expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
      end

      it "log with Severity::INFO" do
        ExceptionHandling.log_info('This is a Warning', service_name: 'exception_handling')
        expect('INFO').to eq(logged_excluding_reload_filter.last[:severity])
      end
    end

    context "#log_debug" do
      it "take in additional key word args as logging context and pass them to the logger" do
        ExceptionHandling.log_debug('This is a Debug', service_name: 'exception_handling')
        expect(logged_excluding_reload_filter.last[:message]).to match(/This is a Debug/)
        expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
        expect(service_name: 'exception_handling').to eq(logged_excluding_reload_filter.last[:context])
      end

      it "log with Severity::DEBUG" do
        ExceptionHandling.log_debug('This is a Warning', service_name: 'exception_handling')
        expect('DEBUG').to eq(logged_excluding_reload_filter.last[:severity])
      end
    end

    context "#write_exception_to_log" do
      it "log warnings with Severity::WARN" do
        warning = ExceptionHandling::Warning.new('This is a Warning')
        ExceptionHandling.write_exception_to_log(warning, '', Time.now.to_i, service_name: 'exception_handling')
        expect('WARN').to eq(logged_excluding_reload_filter.last[:severity])
      end

      it "log everything else with Severity::FATAL" do
        error = RuntimeError.new('This is a runtime error')
        ExceptionHandling.write_exception_to_log(error, '', Time.now.to_i, service_name: 'exception_handling')
        expect('FATAL').to eq(logged_excluding_reload_filter.last[:severity])
      end
    end

    context "configuration with custom_data_hook or post_log_error_hook" do
      after do
        ExceptionHandling.custom_data_hook = nil
        ExceptionHandling.post_log_error_hook = nil
      end

      it "support a custom_data_hook" do
        capture_notifications

        ExceptionHandling.custom_data_hook = method(:append_organization_info_config)
        ExceptionHandling.ensure_safe("context") { raise "Some Exception" }

        expect(sent_notifications.last.enhanced_data['user_details'].to_s).to match(/Invoca Engineering Dept./)
      end

      it "support a log_error hook, and pass exception_data, treat_like_warning, and logged_to_honeybadger to it" do
        @honeybadger_status = nil
        ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)

        notify_args = []
        expect(Honeybadger).to receive(:notify).with(any_args) { |info| notify_args << info; '06220c5a-b471-41e5-baeb-de247da45a56' }
        ExceptionHandling.ensure_safe("context") { raise "Some Exception" }
        expect(@fail_count).to eq(1)
        expect(@treat_like_warning).to eq(false)
        expect(@honeybadger_status).to eq(:success)

        expect(@callback_data["notes"]).to eq("this is used by a test")
        expect(notify_args.size).to eq(1), notify_args.inspect
        expect(notify_args.last[:context].to_s).to match(/this is used by a test/)
      end

      it "plumb treat_like_warning and logged_to_honeybadger to log error hook" do
        @honeybadger_status = nil
        ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
        ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true)
        expect(@fail_count).to eq(1)
        expect(@treat_like_warning).to eq(true)
        expect(@honeybadger_status).to eq(:skipped)
      end

      it "include logging context in the exception data" do
        ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
        ExceptionHandling.log_error(StandardError.new("Some Exception"), "mooo", treat_like_warning: true, log_context_test: "contextual_logging")

        expected_log_context = {
          "log_context_test" => "contextual_logging"
        }
        expect(@callback_data[:log_context]).to eq(expected_log_context)
      end

      it "support rescue exceptions from a log_error hook" do
        ExceptionHandling.post_log_error_hook = method(:log_error_callback_with_failure)
        log_info_messages = []
        allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
          log_info_messages << message
        end
        expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
        expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
      end

      it "handle nil message exceptions resulting from the log_error hook" do
        ExceptionHandling.post_log_error_hook = method(:log_error_callback_returns_nil_message_exception)
        log_info_messages = []
        allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
          log_info_messages << message
        end
        expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.to_not raise_error
        expect(log_info_messages.find { |message| message =~ /Unable to execute custom log_error callback/ }).to be_truthy
      end

      it "handle nil message exceptions resulting from the custom data hook" do
        ExceptionHandling.custom_data_hook = method(:custom_data_callback_returns_nil_message_exception)
        log_info_messages = []
        allow(ExceptionHandling.logger).to receive(:info).with(any_args) do |message, _|
          log_info_messages << message
        end
        expect { ExceptionHandling.ensure_safe("mooo") { raise "Some Exception" } }.not_to raise_error
        expect(log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }).to be_truthy
      end
    end

    context "Exception Handling" do
      context "default_metric_name" do
        context "when metric_name is present in exception_data" do
          it "include metric_name in resulting metric name" do
            exception = StandardError.new('this is an exception')
            metric    = ExceptionHandling.default_metric_name({ 'metric_name' => 'special_metric' }, exception, true)
            expect(metric).to eq('exception_handling.special_metric')
          end
        end

        context "when metric_name is not present in exception_data" do
          it "return exception_handling.warning when using log warning" do
            warning = ExceptionHandling::Warning.new('this is a warning')
            metric  = ExceptionHandling.default_metric_name({}, warning, false)
            expect(metric).to eq('exception_handling.warning')
          end

          it "return exception_handling.exception when using log error" do
            exception = StandardError.new('this is an exception')
            metric    = ExceptionHandling.default_metric_name({}, exception, false)
            expect(metric).to eq('exception_handling.exception')
          end

          context "when using log error with treat_like_warning" do
            it "return exception_handling.unforwarded_exception when exception not present" do
              metric = ExceptionHandling.default_metric_name({}, nil, true)
              expect(metric).to eq('exception_handling.unforwarded_exception')
            end

            it "return exception_handling.unforwarded_exception with exception classname when exception is present" do
              module SomeModule
                class SomeException < StandardError
                end
              end

              exception = SomeModule::SomeException.new('this is an exception')
              metric    = ExceptionHandling.default_metric_name({}, exception, true)
              expect(metric).to eq('exception_handling.unforwarded_exception_SomeException')
            end
          end
        end
      end

      context "default_honeybadger_metric_name" do
        it "return exception_handling.honeybadger.success when status is :success" do
          metric = ExceptionHandling.default_honeybadger_metric_name(:success)
          expect(metric).to eq('exception_handling.honeybadger.success')
        end

        it "return exception_handling.honeybadger.failure when status is :failure" do
          metric = ExceptionHandling.default_honeybadger_metric_name(:failure)
          expect(metric).to eq('exception_handling.honeybadger.failure')
        end

        it "return exception_handling.honeybadger.skipped when status is :skipped" do
          metric = ExceptionHandling.default_honeybadger_metric_name(:skipped)
          expect(metric).to eq('exception_handling.honeybadger.skipped')
        end

        it "return exception_handling.honeybadger.unknown_status when status is not recognized" do
          metric = ExceptionHandling.default_honeybadger_metric_name(nil)
          expect(metric).to eq('exception_handling.honeybadger.unknown_status')
        end
      end

      context "ExceptionHandling.ensure_safe" do
        it "log an exception with call stack if an exception is raised." do
          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
          ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
        end

        if ActionView::VERSION::MAJOR >= 5
          it "log an exception with call stack if an ActionView template exception is raised." do
            expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:\d+\) \nActionView::Template::Error: \(blah\):\n /, any_args)
            ExceptionHandling.ensure_safe do
              begin
                # Rails 5 made the switch from ActionView::TemplateError taking in the original exception
                # as an argument to using the $! global to extract the original exception
                raise ArgumentError, "blah"
              rescue
                raise ActionView::TemplateError.new({})
              end
            end
          end
        else
          it "log an exception with call stack if an ActionView template exception is raised." do
            expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:\d+\) \nActionView::Template::Error: \(blah\):\n /, any_args)
            ExceptionHandling.ensure_safe { raise ActionView::TemplateError.new({}, ArgumentError.new("blah")) }
          end
        end

        it "should not log an exception if an exception is not raised." do
          expect(ExceptionHandling.logger).to_not receive(:fatal)
          ExceptionHandling.ensure_safe { ; }
        end

        it "return its value if used during an assignment" do
          expect(ExceptionHandling.logger).to_not receive(:fatal)
          b = ExceptionHandling.ensure_safe { 5 }
          expect(b).to eq(5)
        end

        it "return nil if an exception is raised during an assignment" do
          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
          b = ExceptionHandling.ensure_safe { raise ArgumentError, "blah" }
          expect(b).to be_nil
        end

        it "allow a message to be appended to the error when logged." do
          expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
          b = ExceptionHandling.ensure_safe("mooo") { raise ArgumentError, "blah" }
          expect(b).to be_nil
        end

        it "only rescue StandardError and descendents" do
          expect { ExceptionHandling.ensure_safe("mooo") { raise Exception } }.to raise_exception(Exception)

          expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nStandardError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)

          b = ExceptionHandling.ensure_safe("mooo") { raise StandardError, "blah" }
          expect(b).to be_nil
        end
      end

      context "ExceptionHandling.ensure_completely_safe" do
        it "log an exception if an exception is raised." do
          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
          ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
        end

        it "should not log an exception if an exception is not raised." do
          expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
          ExceptionHandling.ensure_completely_safe { ; }
        end

        it "return its value if used during an assignment" do
          expect(ExceptionHandling.logger).to receive(:fatal).exactly(0)
          b = ExceptionHandling.ensure_completely_safe { 5 }
          expect(b).to eq(5)
        end

        it "return nil if an exception is raised during an assignment" do
          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args) { nil }
          b = ExceptionHandling.ensure_completely_safe { raise ArgumentError, "blah" }
          expect(b).to be_nil
        end

        it "allow a message to be appended to the error when logged." do
          expect(ExceptionHandling.logger).to receive(:fatal).with(/mooo\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
          b = ExceptionHandling.ensure_completely_safe("mooo") { raise ArgumentError, "blah" }
          expect(b).to be_nil
        end

        it "rescue any instance or child of Exception" do
          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
          ExceptionHandling.ensure_completely_safe { raise Exception, "blah" }
        end

        it "not rescue the special exceptions that Ruby uses" do
          [SystemExit, SystemStackError, NoMemoryError, SecurityError].each do |exception|
            expect do
              ExceptionHandling.ensure_completely_safe do
                raise exception
              end
            end.to raise_exception(exception)
          end
        end
      end

      context "ExceptionHandling.ensure_escalation" do
        before do
          capture_notifications
          ActionMailer::Base.deliveries.clear
        end

        it "log the exception as usual and send the proper email" do
          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
          ExceptionHandling.ensure_escalation("Favorite Feature") { raise ArgumentError, "blah" }
          expect(ActionMailer::Base.deliveries.count).to eq(1)
          expect(sent_notifications.size).to eq(1), sent_notifications.inspect

          email = ActionMailer::Base.deliveries.last
          expect(email.subject).to eq("#{ExceptionHandling.email_environment} Escalation: Favorite Feature")
          expect(email.body.to_s).to match('ArgumentError: blah')
          expect(email.body.to_s).to match(ExceptionHandling.last_exception_timestamp.to_s)
        end

        it "should not escalate if an exception is not raised." do
          expect(ExceptionHandling.logger).to_not receive(:fatal)
          ExceptionHandling.ensure_escalation('Ignored') { ; }
          expect(ActionMailer::Base.deliveries.count).to eq(0)
        end

        it "log if the escalation email cannot be sent" do
          expect_any_instance_of(Mail::Message).to receive(:deliver).and_raise(RuntimeError.new, "Delivery Error")
          log_fatals = []
          expect(ExceptionHandling.logger).to receive(:fatal).with(any_args).at_least(:once) do |*args|
            log_fatals << args
          end

          ExceptionHandling.ensure_escalation("ensure context") { raise ArgumentError, "first_test_exception" }
          expect(log_fatals[0].first).to match(/ArgumentError.*first_test_exception/)
          expect(log_fatals[1].first).to match(/safe_email_deliver.*Delivery Error/m)

          expect(log_fatals.size).to eq(2), log_fatals.inspect

          expect(sent_notifications.size).to eq(1), sent_notifications.inspect # still sent to honeybadger
        end

        it "allow the caller to specify custom recipients" do
          custom_recipients = ['something@invoca.com']
          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
          ExceptionHandling.ensure_escalation("Favorite Feature", custom_recipients) { raise ArgumentError, "blah" }
          expect(ActionMailer::Base.deliveries.count).to eq(1)
          expect(sent_notifications.size).to eq(1), sent_notifications.inspect

          email = ActionMailer::Base.deliveries.last
          expect(email.subject).to eq("#{ExceptionHandling.email_environment} Escalation: Favorite Feature")
          expect(email.body.to_s).to match('ArgumentError: blah')
          expect(email.body.to_s).to match(ExceptionHandling.last_exception_timestamp.to_s)
          expect(email.to).to eq(custom_recipients)
        end
      end

      context "ExceptionHandling.ensure_alert" do
        it "log the exception as usual and fire a sensu event" do
          expect(ExceptionHandling::Sensu).to receive(:generate_event).with("Favorite Feature", "test context\nblah")
          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(blah\):\n.*exception_handling_spec\.rb/, any_args)
          ExceptionHandling.ensure_alert('Favorite Feature', 'test context') { raise ArgumentError, "blah" }
        end

        it "should not send sensu event if an exception is not raised." do
          expect(ExceptionHandling.logger).to_not receive(:fatal)
          expect(ExceptionHandling::Sensu).to_not receive(:generate_event)
          ExceptionHandling.ensure_alert('Ignored', 'test context') { ; }
        end

        it "log if the sensu event could not be sent" do
          expect(ExceptionHandling::Sensu).to receive(:send_event).with(anything) { raise "Failed to send" }
          expect(ExceptionHandling.logger).to receive(:fatal).with(/first_test_exception/, any_args)
          expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to send/, any_args)
          ExceptionHandling.ensure_alert("Not Used", 'test context') { raise ArgumentError, "first_test_exception" }
        end

        it "log if the exception message is nil" do
          expect(ExceptionHandling::Sensu).to receive(:generate_event).with("some alert", "test context\n")
          ExceptionHandling.ensure_alert('some alert', 'test context') { raise_exception_with_nil_message }
        end
      end

      context "ExceptionHandling.escalate_to_production_support" do
        it "notify production support" do
          subject = "Runtime Error found!"
          exception = RuntimeError.new("Test")
          recipients = ["prodsupport@example.com"]

          expect(ExceptionHandling).to receive(:production_support_recipients).and_return(recipients).exactly(2)
          expect(ExceptionHandling).to receive(:escalate).with(subject, exception, ExceptionHandling.last_exception_timestamp, recipients)
          ExceptionHandling.escalate_to_production_support(exception, subject)
        end
      end

      context "exception timestamp" do
        before do
          Time.now_override = Time.parse('1986-5-21 4:17 am UTC')
        end

        it "include the timestamp when the exception is logged" do
          capture_notifications

          expect(ExceptionHandling.logger).to receive(:fatal).with(/\(Error:517033020\) context\nArgumentError: \(blah\):\n.*exception_handling_spec\.rb/, any_args)
          b = ExceptionHandling.ensure_safe("context") { raise ArgumentError, "blah" }
          expect(b).to be_nil

          expect(ExceptionHandling.last_exception_timestamp).to eq(517_033_020)

          expect(sent_notifications.size).to eq(1), sent_notifications.inspect

          expect(sent_notifications.last.enhanced_data['timestamp']).to eq(517_033_020)
        end
      end

      it "log the error if the exception message is nil" do
        capture_notifications

        ExceptionHandling.log_error(exception_with_nil_message)

        expect(sent_notifications.size).to eq(1), sent_notifications.inspect
        expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
      end

      it "log the error if the exception message is nil and the exception context is a hash" do
        capture_notifications

        ExceptionHandling.log_error(exception_with_nil_message, "SERVER_NAME" => "exceptional.com")

        expect(sent_notifications.size).to eq(1), sent_notifications.inspect
        expect(sent_notifications.last.enhanced_data['error_string']).to eq('RuntimeError: ')
      end

      context "Honeybadger integration" do
        context "with Honeybadger not defined" do
          before do
            allow(ExceptionHandling).to receive(:honeybadger_defined?) { false }
          end

          it "not invoke send_exception_to_honeybadger when log_error is executed" do
            expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
            ExceptionHandling.log_error(exception_1)
          end

          it "not invoke send_exception_to_honeybadger when ensure_safe is executed" do
            expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
            ExceptionHandling.ensure_safe { raise exception_1 }
          end
        end

        context "with Honeybadger defined" do
          it "not send_exception_to_honeybadger when log_warning is executed" do
            expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
            ExceptionHandling.log_warning("This should not go to honeybadger")
          end

          it "not send_exception_to_honeybadger when log_error is called with a Warning" do
            expect(ExceptionHandling).to_not receive(:send_exception_to_honeybadger)
            ExceptionHandling.log_error(ExceptionHandling::Warning.new("This should not go to honeybadger"))
          end

          it "invoke send_exception_to_honeybadger when log_error is executed" do
            expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
            ExceptionHandling.log_error(exception_1)
          end

          it "invoke send_exception_to_honeybadger when log_error_rack is executed" do
            expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
            ExceptionHandling.log_error_rack(exception_1, {}, nil)
          end

          it "invoke send_exception_to_honeybadger when ensure_safe is executed" do
            expect(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args).and_call_original
            ExceptionHandling.ensure_safe { raise exception_1 }
          end

          it "specify error message as an empty string when notifying honeybadger if exception message is nil" do
            expect(Honeybadger).to receive(:notify).with(any_args) do |args|
              expect(args[:error_message]).to eq("")
            end
            ExceptionHandling.log_error(exception_with_nil_message)
          end

          context "with stubbed values" do
            before do
              Time.now_override = Time.now
              @env = { server: "fe98" }
              @parameters = { advertiser_id: 435, controller: "some_controller" }
              @session = { username: "jsmith" }
              @request_uri = "host/path"
              @controller = create_dummy_controller(@env, @parameters, @session, @request_uri)
              allow(ExceptionHandling).to receive(:server_name) { "invoca_fe98" }

              @exception = StandardError.new("Some Exception")
              @exception.set_backtrace([
                                         "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
                                         "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
                                       ])
              @exception_context = { "SERVER_NAME" => "exceptional.com" }
            end

            it "send error details and relevant context data to Honeybadger with log_context" do
              honeybadger_data = nil
              expect(Honeybadger).to receive(:notify).with(any_args) do |data|
                honeybadger_data = data
              end
              ExceptionHandling.logger.global_context = { service_name: "rails", region: "AWS-us-east-1" }
              log_context = { log_source: "gem/listen", service_name: "bin/console" }
              ExceptionHandling.log_error(@exception, @exception_context, @controller, **log_context) do |data|
                data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
                data[:user_details] = { username: "jsmith" }
                data[:event_response] = "Event successfully received"
                data[:other_section] = "This should not be included in the response"
              end

              expected_data = {
                error_class: :"Test Exception",
                error_message: "Some Exception",
                controller: "some_controller",
                exception: @exception,
                context: {
                  timestamp: Time.now.to_i,
                  error_class: "StandardError",
                  server: "invoca_fe98",
                  exception_context: { "SERVER_NAME" => "exceptional.com" },
                  scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
                  notes: "this is used by a test",
                  user_details: { "username" => "jsmith" },
                  request: {
                    "params" => { "advertiser_id" => 435, "controller" => "some_controller" },
                    "rails_root" => "Rails.root not defined. Is this a test environment?",
                    "url" => "host/path"
                  },
                  session: {
                    "key" => nil,
                    "data" => { "username" => "jsmith" }
                  },
                  environment: {
                    "SERVER_NAME" => "exceptional.com"
                  },
                  backtrace: [
                    "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
                    "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
                  ],
                  event_response: "Event successfully received",
                  log_context: { "service_name" => "bin/console", "region" => "AWS-us-east-1", "log_source" => "gem/listen" }
                }
              }
              expect(honeybadger_data).to eq(expected_data)
            end

            it "send error details and relevant context data to Honeybadger with empty log_context" do
              honeybadger_data = nil
              expect(Honeybadger).to receive(:notify).with(any_args) do |data|
                honeybadger_data = data
              end
              ExceptionHandling.logger.global_context = {}
              log_context = {}
              ExceptionHandling.log_error(@exception, @exception_context, @controller, **log_context) do |data|
                data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
                data[:user_details] = { username: "jsmith" }
                data[:event_response] = "Event successfully received"
                data[:other_section] = "This should not be included in the response"
              end

              expected_data = {
                error_class: :"Test Exception",
                error_message: "Some Exception",
                controller: "some_controller",
                exception: @exception,
                context: {
                  timestamp: Time.now.to_i,
                  error_class: "StandardError",
                  server: "invoca_fe98",
                  exception_context: { "SERVER_NAME" => "exceptional.com" },
                  scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
                  notes: "this is used by a test",
                  user_details: { "username" => "jsmith" },
                  request: {
                    "params" => { "advertiser_id" => 435, "controller" => "some_controller" },
                    "rails_root" => "Rails.root not defined. Is this a test environment?",
                    "url" => "host/path"
                  },
                  session: {
                    "key" => nil,
                    "data" => { "username" => "jsmith" }
                  },
                  environment: {
                    "SERVER_NAME" => "exceptional.com"
                  },
                  backtrace: [
                               "spec/unit/exception_handling_spec.rb:847:in `exception_1'",
                               "spec/unit/exception_handling_spec.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"
                             ],
                  event_response: "Event successfully received"
                }
              }
              expect(honeybadger_data).to eq(expected_data)
            end
          end

          context "with post_log_error_hook set" do
            after do
              ExceptionHandling.post_log_error_hook = nil
            end

            it "not send notification to honeybadger when exception description has the flag turned off and call log error callback with logged_to_honeybadger set to nil" do
              @honeybadger_status = nil
              ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
              filter_list = {
                NoHoneybadger: {
                  error: "suppress Honeybadger notification",
                  send_to_honeybadger: false
                }
              }
              allow(File).to receive(:mtime) { incrementing_mtime }
              expect(YAML).to receive(:load_file).with(any_args) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)

              expect(ExceptionHandling).to receive(:send_exception_to_honeybadger_unless_filtered).with(any_args).exactly(1).and_call_original
              expect(Honeybadger).to_not receive(:notify)
              ExceptionHandling.log_error(StandardError.new("suppress Honeybadger notification"))
              expect(@honeybadger_status).to eq(:skipped)
            end

            it "call log error callback with logged_to_honeybadger set to false if an error occurs while attempting to notify honeybadger" do
              @honeybadger_status = nil
              ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
              expect(Honeybadger).to receive(:notify).with(any_args) { raise "Honeybadger Notification Failure" }
              ExceptionHandling.log_error(exception_1)
              expect(@honeybadger_status).to eq(:failure)
            end

            it "call log error callback with logged_to_honeybadger set to false on unsuccessful honeybadger notification" do
              @honeybadger_status = nil
              ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
              expect(Honeybadger).to receive(:notify).with(any_args) { false }
              ExceptionHandling.log_error(exception_1)
              expect(@honeybadger_status).to eq(:failure)
            end

            it "call log error callback with logged_to_honeybadger set to true on successful honeybadger notification" do
              @honeybadger_status = nil
              ExceptionHandling.post_log_error_hook = method(:log_error_callback_config)
              expect(Honeybadger).to receive(:notify).with(any_args) { '06220c5a-b471-41e5-baeb-de247da45a56' }
              ExceptionHandling.log_error(exception_1)
              expect(@honeybadger_status).to eq(:success)
            end
          end
        end
      end

      class EventResponse
        def to_s
          "message from to_s!"
        end
      end

      it "allow sections to have data with just a to_s method" do
        capture_notifications

        ExceptionHandling.log_error("This is my RingSwitch example.") do |data|
          data.merge!(event_response: EventResponse.new)
        end

        expect(sent_notifications.size).to eq(1), sent_notifications.inspect
        expect(sent_notifications.last.enhanced_data['event_response'].to_s).to match(/message from to_s!/)
      end
    end

    it "return the error ID (timestamp)" do
      result = ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
      expect(result).to eq(ExceptionHandling.last_exception_timestamp)
    end

    it "rescue exceptions that happen in log_error" do
      allow(ExceptionHandling).to receive(:make_exception) { raise ArgumentError, "Bad argument" }
      expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
                                                     satisfy { |context| context['ExceptionHandlingError: log_error rescued exception while logging Runtime message'] },
                                                     any_args)
      ExceptionHandling.log_error(RuntimeError.new("A runtime error"), "Runtime message")
    end

    it "rescue exceptions that happen when log_error yields" do
      expect(ExceptionHandling).to receive(:write_exception_to_log).with(satisfy { |ex| ex.to_s['Bad argument'] },
                                                     satisfy { |context| context['Context message'] },
                                                     anything,
                                                     any_args)
      ExceptionHandling.log_error(ArgumentError.new("Bad argument"), "Context message") { |_data| raise 'Error!!!' }
    end

    context "Exception Filtering" do
      before do
        filter_list = { exception1: { 'error' => "my error message" },
                        exception2: { 'error' => "some other message", :session => "misc data" } }
        allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }

        # bump modified time up to get the above filter loaded
        allow(File).to receive(:mtime) { incrementing_mtime }
      end

      it "handle case where filter list is not found" do
        allow(YAML).to receive(:load_file) { raise Errno::ENOENT, "File not found" }

        capture_notifications

        ExceptionHandling.log_error("My error message is in list")
        expect(sent_notifications.size).to eq(1), sent_notifications.inspect
      end

      it "log exception and suppress email when exception is on filter list" do
        capture_notifications

        ExceptionHandling.log_error("Error message is not in list")
        expect(sent_notifications.size).to eq(1), sent_notifications.inspect

        sent_notifications.clear
        ExceptionHandling.log_error("My error message is in list")
        expect(sent_notifications.size).to eq(0), sent_notifications.inspect
      end

      it "allow filtering exception on any text in exception data" do
        filters = { exception1: { session: "data: my extra session data" } }
        allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filters) }

        capture_notifications

        ExceptionHandling.log_error("No match here") do |data|
          data[:session] = {
            key: "@session_id",
            data: "my extra session data"
          }
        end
        expect(sent_notifications.size).to eq(0), sent_notifications.inspect

        ExceptionHandling.log_error("No match here") do |data|
          data[:session] = {
            key: "@session_id",
            data: "my extra session <no match!> data"
          }
        end
        expect(sent_notifications.size).to eq(1), sent_notifications.inspect
      end

      it "reload filter list on the next exception if file was modified" do
        capture_notifications

        ExceptionHandling.log_error("Error message is not in list")
        expect(sent_notifications.size).to eq(1), sent_notifications.inspect

        filter_list = { exception1: { 'error' => "Error message is not in list" } }
        allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
        allow(File).to receive(:mtime) { incrementing_mtime }

        sent_notifications.clear
        ExceptionHandling.log_error("Error message is not in list")
        expect(sent_notifications.size).to eq(0), sent_notifications.inspect
      end

      it "not consider filter if both error message and body do not match" do
        capture_notifications

        # error message matches, but not full text
        ExceptionHandling.log_error("some other message")
        expect(sent_notifications.size).to eq(1), sent_notifications.inspect

        # now both match
        sent_notifications.clear
        ExceptionHandling.log_error("some other message") do |data|
          data[:session] = { some_random_key: "misc data" }
        end
        expect(sent_notifications.size).to eq(0), sent_notifications.inspect
      end

      it "skip environment keys not on whitelist" do
        capture_notifications

        ExceptionHandling.log_error("some message") do |data|
          data[:environment] = { SERVER_PROTOCOL: "HTTP/1.0", RAILS_SECRETS_YML_CONTENTS: 'password: VERY_SECRET_PASSWORD' }
        end
        expect(sent_notifications.size).to eq(1), sent_notifications.inspect

        mail = sent_notifications.last
        environment = mail.enhanced_data['environment']

        expect(environment["RAILS_SECRETS_YML_CONTENTS"]).to be_nil, environment.inspect # this is not on whitelist).to be_nil
        expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this is
      end

      it "omit environment defaults" do
        capture_notifications

        allow(ExceptionHandling).to receive(:send_exception_to_honeybadger).with(any_args) { |exception_info| sent_notifications << exception_info }

        ExceptionHandling.log_error("some message") do |data|
          data[:environment] = { SERVER_PORT: '80', SERVER_PROTOCOL: "HTTP/1.0" }
        end
        expect(sent_notifications.size).to eq(1), sent_notifications.inspect
        mail = sent_notifications.last
        environment = mail.enhanced_data['environment']

        expect(environment["SERVER_PORT"]).to be_nil, environment.inspect # this was default).to be_nil
        expect(environment["SERVER_PROTOCOL"]).to be_truthy, environment.inspect # this was not
      end

      it "reject the filter file if any contain all empty regexes" do
        filter_list = { exception1: { 'error' => "", :session => "" },
                        exception2: { 'error' => "is not in list", :session => "" } }
        allow(YAML).to receive(:load_file) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }
        allow(File).to receive(:mtime) { incrementing_mtime }

        capture_notifications

        ExceptionHandling.log_error("Error message is not in list")
        expect(sent_notifications.size).to eq(1), sent_notifications.inspect
      end

      it "reload filter file if filename changes" do
        catalog = ExceptionHandling.exception_catalog
        ExceptionHandling.filter_list_filename = "./config/other_exception_filters.yml"
        expect(ExceptionHandling.exception_catalog).to_not eq(catalog)
      end

      context "Exception Handling Mailer" do
        EXPECTED_SMTP_HASH =
          {
            host: '127.0.0.1',
            domain: 'localhost.localdomain',
            from: 'server@example.com',
            to: 'escalation@example.com'
          }.freeze

        [[true, false], [true, true]].each do |em_flag, synchrony_flag|
          context "eventmachine_safe = #{em_flag} && eventmachine_synchrony = #{synchrony_flag}" do
            before do
              ExceptionHandling.eventmachine_safe       = em_flag
              ExceptionHandling.eventmachine_synchrony  = synchrony_flag
              EventMachineStub.block = nil
              set_test_const('EventMachine', EventMachineStub)
              set_test_const('EventMachine::Protocols', Module.new)
              set_test_const('EventMachine::DNS', Module.new)
              set_test_const('EventMachine::DNS::Resolver', DNSResolvStub)
            end

            after do
              ExceptionHandling.eventmachine_safe       = false
              ExceptionHandling.eventmachine_synchrony  = false
            end

            it "schedule EventMachine STMP when EventMachine defined" do
              ActionMailer::Base.deliveries.clear

              set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)

              ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
              expect(EventMachineStub.block).to be_truthy
              EventMachineStub.block.call
              expect(DNSResolvStub.callback_block).to be_truthy
              DNSResolvStub.callback_block.call ['127.0.0.1']
              expect((SmtpClientStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }) .to eq(EXPECTED_SMTP_HASH), SmtpClientStub.send_hash.inspect
              expect(SmtpClientStub.last_method).to eq((synchrony_flag ? :asend : :send))
              expect(SmtpClientStub.send_hash[:content]).to match(/Exception to escalate/)
              assert_emails 0, ActionMailer::Base.deliveries.*.to_s
            end

            it "pass the content as a proper rfc 2822 message" do
              set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientStub)
              ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
              expect(EventMachineStub.block).to be_truthy
              EventMachineStub.block.call
              expect(DNSResolvStub.callback_block).to be_truthy
              DNSResolvStub.callback_block.call ['127.0.0.1']
              expect(content = SmtpClientStub.send_hash[:content]).to be_truthy
              expect(content).to match(/Content-Transfer-Encoding: 7bit/)
              expect(content).to match(/\r\n\.\r\n\z/)
            end

            it "log fatal on EventMachine STMP errback" do
              ActionMailer::Base.deliveries.clear

              set_test_const('EventMachine::Protocols::SmtpClient', SmtpClientErrbackStub)
              expect(ExceptionHandling.logger).to receive(:fatal).with(/Exception to escalate/, any_args)
              expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to email by SMTP: "credential mismatch"/)

              ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
              expect(EventMachineStub.block).to be_truthy
              EventMachineStub.block.call
              expect(DNSResolvStub.callback_block).to be_truthy
              DNSResolvStub.callback_block.call(['127.0.0.1'])
              SmtpClientErrbackStub.block.call("credential mismatch")
              expect((SmtpClientErrbackStub.send_hash & EXPECTED_SMTP_HASH.keys).map_hash { |_k, v| v.to_s }).to eq(EXPECTED_SMTP_HASH), SmtpClientErrbackStub.send_hash.inspect            end

            it "log fatal on EventMachine dns resolver errback" do
              expect(ExceptionHandling.logger).to receive(:fatal).with(/Exception to escalate/, any_args)
              expect(ExceptionHandling.logger).to receive(:fatal).with(/Failed to resolv DNS for localhost: "softlayer sucks"/)

              ExceptionHandling.ensure_escalation("ensure message") { raise 'Exception to escalate!' }
              expect(EventMachineStub.block).to be_truthy
              EventMachineStub.block.call
              DNSResolvStub.errback_block.call("softlayer sucks")
            end
          end
        end
      end
    end

    context "Exception mapping" do
      before do
        @data = {
          environment: {
            'HTTP_HOST' => "localhost",
            'HTTP_REFERER' => "http://localhost/action/controller/instance",
          },
          session: {
            data: {
              affiliate_id: defined?(Affiliate) ? Affiliate.first.id : '1',
              edit_mode: true,
              advertiser_id: defined?(Advertiser) ? Advertiser.first.id : '1',
              username_id: defined?(Username) ? Username.first.id : '1',
              user_id: defined?(User) ? User.first.id : '1',
              flash: {},
              impersonated_organization_pk: 'Advertiser_1'
            }
          },
          request: {},
          backtrace: ["[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:580:in `call_filters'", "[GEM_ROOT]/gems/actionpack-2.1.0/lib/action_controller/filters.rb:601:in `run_before_filters'"],
          api_key: "none",
          error_class: "StandardError",
          error: 'Some error message'
        }
      end

      it "clean backtraces" do
        begin
          raise "test exception"
        rescue => ex
          backtrace = ex.backtrace
        end
        result = ExceptionHandling.send(:clean_backtrace, ex).to_s
        expect(backtrace).to_not eq(result)
      end

      it "return entire backtrace if cleaned is emtpy" do
        begin
          backtrace = ["/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:312:in `find_with_ids'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/relation/finder_methods.rb:107:in `find'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `__send__'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activerecord/lib/active_record/querying.rb:5:in `find'",
                       "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `call'",
                       "/Library/Ruby/Gems/1.8/gems/shoulda-context-1.0.2/lib/shoulda/context/context.rb:398:in `test: Exception mapping should return entire backtrace if cleaned is emtpy. '",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `__send__'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:72:in `run'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:447:in `_run__1913317170__setup__4__callbacks'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `send'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:405:in `__run_callback'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:385:in `_run_setup_callbacks'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `send'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/callbacks.rb:81:in `run_callbacks'",
                       "/Users/peter/ringrevenue/web/vendor/rails-3.2.12/activesupport/lib/active_support/testing/setup_and_teardown.rb:70:in `run'",
                       "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'",
                       "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'",
                       "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'",
                       "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `old_run_suite'",
                       "(eval):12:in `run_suite'",
                       "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `send'",
                       "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:93:in `start_mediator'",
                       "/Applications/RubyMine.app/rb/testing/patch/testunit/test/unit/ui/teamcity/testrunner.rb:81:in `start'",
                       "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'",
                       "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'",
                       "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/test/unit.rb:279",
                       "-e:1"]

          module ::Rails
            class BacktraceCleaner
              def clean(_backtrace)
                []
              end
            end
          end

          rails = double(Rails)
          expect(rails).to receive(:backtrace_cleaner) { Rails::BacktraceCleaner.new }
          rails.backtrace_cleaner

          ex = Exception.new
          ex.set_backtrace(backtrace)
          result = ExceptionHandling.send(:clean_backtrace, ex)
          expect(result).to eq(backtrace)
        ensure
          Object.send(:remove_const, :Rails)
        end
      end
    end

    context "log_perodically" do
      before do
        Time.now_override = Time.now # Freeze time
        ExceptionHandling.logger.clear
      end

      after do
        Time.now_override = nil
      end

      it "take in additional logging context and pass them to the logger" do
        ExceptionHandling.log_periodically(:test_context_with_periodic, 30.minutes, "this will be written", service_name: 'exception_handling')
        expect(logged_excluding_reload_filter.last[:context]).to_not be_empty
        expect(logged_excluding_reload_filter.last[:context]).to eq({ service_name: 'exception_handling' })
      end

      it "log immediately when we are expected to log" do
        ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
        expect(logged_excluding_reload_filter.size).to eq(1)

        Time.now_override = Time.now + 5.minutes
        ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will not be written")
        expect(logged_excluding_reload_filter.size).to eq(1)

        ExceptionHandling.log_periodically(:test_another_periodic_exception, 30.minutes, "this will be written")
        expect(logged_excluding_reload_filter.size).to eq(2)

        Time.now_override = Time.now + 26.minutes

        ExceptionHandling.log_periodically(:test_periodic_exception, 30.minutes, "this will be written")
        expect(logged_excluding_reload_filter.size).to eq(3)
      end
    end
  end

  context "ExceptionHandling < 3.0 " do
    it "should return a deprecation warning" do
      ExceptionHandling.production_support_recipients = "prodsupport@example.com"
      expect { ExceptionHandling.escalate_to_production_support("blah", "invoca@example.com") }
        .to output(/DEPRECATION WARNING: escalate_to_production_support is deprecated and will be removed from ExceptionHandling 3.0/).to_stderr
    end

    it "should return a deprecation warning" do
      expect { ExceptionHandling.escalate_error("blah", "invoca@example.com") }
        .to output(/DEPRECATION WARNING: escalate_error is deprecated and will be removed from ExceptionHandling 3.0/).to_stderr
    end

    it "should return a deprecation warning" do
      expect { ExceptionHandling.escalate_warning("blah", "invoca@example.com") }
        .to output(/DEPRECATION WARNING: escalate_warning is deprecated and will be removed from ExceptionHandling 3.0/).to_stderr
    end

    it "should return a deprecation warning" do
      expect { ExceptionHandling.ensure_escalation("blah", "invoca@example.com") }
        .to output(/DEPRECATION WARNING: ensure_escalation is deprecated and will be removed from ExceptionHandling 3.0/).to_stderr
    end
  end

  private

  def logged_excluding_reload_filter
    ExceptionHandling.logger.logged.select { |l| l[:message] !~ /Reloading filter list/ }
  end

  def incrementing_mtime
    @mtime ||= Time.now
    @mtime += 1.day
  end

  def exception_1
    @exception_1 ||=
      begin
        raise StandardError, "Exception 1"
      rescue => ex
        ex
      end
  end

  def exception_2
    @exception_2 ||=
      begin
        raise StandardError, "Exception 2"
      rescue => ex
        ex
      end
  end
end