require "spec_helper"

describe "on_error callbacks" do
  it "runs callbacks on notify" do
    callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
    callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }

    Bugsnag.add_on_error(callback1)
    Bugsnag.add_on_error(callback2)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
      expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
    end)
  end

  it "accepts a block" do
    Bugsnag.on_error {|report| report.add_tab(:important, { hello: "world" }) }
    Bugsnag.on_error {|report| report.add_tab(:significant, { hey: "earth" }) }

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
      expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
    end)
  end

  it "can add callbacks in a configure block" do
    callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
    callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }

    Bugsnag.configure do |config|
      config.add_on_error(callback1)
      config.add_on_error(callback2)
      config.on_error do |report|
        report.add_tab(:critical, { hi: "planet" })
      end
    end

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
      expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
      expect(event["metaData"]["critical"]).to eq({ "hi" => "planet" })
    end)
  end

  it "can remove an already registered callback" do
    callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
    callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }

    Bugsnag.add_on_error(callback1)
    Bugsnag.add_on_error(callback2)

    Bugsnag.remove_on_error(callback1)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to be_nil
      expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
    end)
  end

  it "can remove an already registered block" do
    callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
    callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }

    Bugsnag.add_on_error(callback1)

    # pass callback2 as a block so that it can be removed
    Bugsnag.on_error(&callback2)

    Bugsnag.remove_on_error(callback2)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
      expect(event["metaData"]["significant"]).to be_nil
    end)
  end

  it "can remove all registered callbacks" do
    callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
    callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }

    Bugsnag.add_on_error(callback1)
    Bugsnag.add_on_error(callback2)

    Bugsnag.remove_on_error(callback2)
    Bugsnag.remove_on_error(callback1)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to be_nil
      expect(event["metaData"]["significant"]).to be_nil
    end)
  end

  it "does not remove an identical callback if it is not the same Proc" do
    callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
    callback1_duplicate = proc {|report| report.add_tab(:important, { hello: "world" }) }

    Bugsnag.add_on_error(callback1)
    Bugsnag.remove_on_error(callback1_duplicate)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
    end)
  end

  it "can re-add callbacks that have previously been removed" do
    callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
    callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }

    Bugsnag.add_on_error(callback1)
    Bugsnag.add_on_error(callback2)

    Bugsnag.remove_on_error(callback1)

    Bugsnag.add_on_error(callback1)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
      expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
    end)
  end

  it "will only add a callback once" do
    called_count = 0

    callback = proc do |report|
      called_count += 1

      report.add_tab(:important, { called: called_count })
    end

    1.upto(10) do |i|
      Bugsnag.add_on_error(callback)
    end

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(called_count).to be(1)
      expect(event["metaData"]["important"]).to eq({ "called" => 1 })
    end)
  end

  it "will ignore the report and stop calling callbacks if one returns false" do
    logger = spy('logger')
    Bugsnag.configuration.logger = logger

    called_count = 0

    callback1 = proc { called_count += 1 }
    callback2 = proc { called_count += 1 }
    callback3 = proc { false }
    callback4 = proc { called_count += 1 }
    callback5 = proc { called_count += 1 }

    Bugsnag.add_on_error(callback1)
    Bugsnag.add_on_error(callback2)
    Bugsnag.add_on_error(callback3)
    Bugsnag.add_on_error(callback4)
    Bugsnag.add_on_error(callback5)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).not_to have_sent_notification
    expect(called_count).to be(2)

    expect(logger).to have_received(:debug).with("[Bugsnag]") do |&block|
      expect(block.call).to eq("Not notifying RuntimeError due to ignore being signified in user provided middleware")
    end
  end

  it "callbacks are called in the same order they are added (FIFO)" do
    callback1 = proc do |report|
      expect(report.meta_data[:important]).to be_nil

      report.add_tab(:important, { magic_number: 9 })
    end

    callback2 = proc do |report|
      expect(report.meta_data[:important]).to eq({ magic_number: 9 })

      report.add_tab(:important, { magic_number: 99 })
    end

    callback3 = proc do |report|
      expect(report.meta_data[:important]).to eq({ magic_number: 99 })

      report.add_tab(:important, { magic_number: 999 })
    end

    Bugsnag.add_on_error(callback1)
    Bugsnag.add_on_error(callback2)
    Bugsnag.add_on_error(callback3)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "magic_number" => 999 })
    end)
  end

  it "callbacks continue being called after a callback raises" do
    logger = spy('logger')
    Bugsnag.configuration.logger = logger

    callback1 = proc {|report| report.add_tab(:important, { a: "b" }) }
    callback2 = proc {|_report| raise "bad things" }
    callback3 = proc {|report| report.add_tab(:important, { c: "d" }) }

    Bugsnag.add_on_error(callback1)
    Bugsnag.add_on_error(callback2)
    Bugsnag.add_on_error(callback3)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "a" => "b", "c" => "d" })
    end)

    message_index = 0
    expected_messages = [
      /^Error occurred in on_error callback: 'bad things'$/,
      /^on_error callback stacktrace:/
    ]

    expect(logger).to have_received(:warn).with("[Bugsnag]").twice do |&block|
      expect(block.call).to match(expected_messages[message_index])
      message_index += 1
    end
  end

  it "runs callbacks even if no other middleware is registered" do
    # Reset the middleware stack so any default middleware are removed
    Bugsnag.configuration.middleware = Bugsnag::MiddlewareStack.new

    callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
    callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }

    Bugsnag.add_on_error(callback1)
    Bugsnag.add_on_error(callback2)

    Bugsnag.notify(RuntimeError.new("Oh no!"))

    expect(Bugsnag).to(have_sent_notification do |payload, _headers|
      event = get_event_from_payload(payload)

      expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
      expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
    end)
  end

  describe "using callbacks across threads" do
    it "runs callbacks that are added in different threads" do
      callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
      callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }
      callback3 = proc {|report| report.add_tab(:crucial, { magic_number: 999 }) }

      Bugsnag.add_on_error(callback1)

      threads = [
        Thread.new { Bugsnag.add_on_error(callback2) },
        Thread.new { Bugsnag.add_on_error(callback3) }
      ]

      threads.each(&:join)

      Bugsnag.notify(RuntimeError.new("Oh no!"))

      expect(Bugsnag).to(have_sent_notification do |payload, _headers|
        event = get_event_from_payload(payload)

        expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
        expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
        expect(event["metaData"]["crucial"]).to eq({ "magic_number" => 999 })
      end)
    end

    it "can remove callbacks that are added in different threads" do
      callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) }
      callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) }

      # We need to create & join these one at a time so that callback1 has
      # definitely been added before it is removed, otherwise this test will fail
      # at random
      Thread.new { Bugsnag.add_on_error(callback1) }.join
      Thread.new { Bugsnag.remove_on_error(callback1) }.join
      Thread.new { Bugsnag.add_on_error(callback2) }.join

      Bugsnag.notify(RuntimeError.new("Oh no!"))

      expect(Bugsnag).to(have_sent_notification do |payload, _headers|
        event = get_event_from_payload(payload)

        expect(event["metaData"]["important"]).to be_nil
        expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" })
      end)
    end

    it "callbacks are called in FIFO order when added in separate threads" do
      callback1 = proc do |report|
        expect(report.meta_data[:important]).to be_nil

        report.add_tab(:important, { magic_number: 9 })
      end

      callback2 = proc do |report|
        expect(report.meta_data[:important]).to eq({ magic_number: 9 })

        report.add_tab(:important, { magic_number: 99 })
      end

      callback3 = proc do |report|
        expect(report.meta_data[:important]).to eq({ magic_number: 99 })

        report.add_tab(:important, { magic_number: 999 })
      end

      # As above, we need to create & join these one at a time so that each
      # callback is added in sequence
      Thread.new { Bugsnag.add_on_error(callback1) }.join
      Thread.new { Bugsnag.add_on_error(callback2) }.join
      Thread.new { Bugsnag.add_on_error(callback3) }.join

      Bugsnag.notify(RuntimeError.new("Oh no!"))

      expect(Bugsnag).to(have_sent_notification do |payload, _headers|
        event = get_event_from_payload(payload)

        expect(event["metaData"]["important"]).to eq({ "magic_number" => 999 })
      end)
    end
  end

  describe "using methods as callbacks" do
    it "runs callbacks on notify" do
      class OnErrorCallbackHaver
        def callback(report)
          report.add_tab(:important, { hello: "world" })
        end
      end

      Bugsnag.add_on_error(OnErrorCallbackHaver.new.method(:callback))

      Bugsnag.notify(RuntimeError.new("Oh no!"))

      expect(Bugsnag).to(have_sent_notification do |payload, _headers|
        event = get_event_from_payload(payload)

        expect(event["metaData"]["important"]).to eq({ "hello" => "world" })
      end)
    end

    it "can remove an already registered callback" do
      class OnErrorCallbackHaver
        def callback(report)
          report.add_tab(:important, { hello: "world" })
        end
      end

      instance = OnErrorCallbackHaver.new

      Bugsnag.add_on_error(instance.method(:callback))
      Bugsnag.remove_on_error(instance.method(:callback))

      Bugsnag.notify(RuntimeError.new("Oh no!"))

      expect(Bugsnag).to(have_sent_notification do |payload, _headers|
        event = get_event_from_payload(payload)

        expect(event["metaData"]["important"]).to be_nil
      end)
    end
  end

  describe "using an object that responds to #call" do
    it "runs callbacks on notify" do
      class RespondsToCall
        def call(report)
          report.add_tab(:important, { hi: "earth" })
        end
      end

      Bugsnag.add_on_error(RespondsToCall.new)

      Bugsnag.notify(RuntimeError.new("Oh no!"))

      expect(Bugsnag).to(have_sent_notification do |payload, _headers|
        event = get_event_from_payload(payload)

        expect(event["metaData"]["important"]).to eq({ "hi" => "earth" })
      end)
    end

    it "can remove an already registered callback" do
      class RespondsToCall
        def callback(report)
          report.add_tab(:important, { hi: "earth" })
        end
      end

      instance = RespondsToCall.new

      Bugsnag.add_on_error(instance)
      Bugsnag.remove_on_error(instance)

      Bugsnag.notify(RuntimeError.new("Oh no!"))

      expect(Bugsnag).to(have_sent_notification do |payload, _headers|
        event = get_event_from_payload(payload)

        expect(event["metaData"]["important"]).to be_nil
      end)
    end
  end
end
