require File.expand_path '../helper', __FILE__

class NotifierTest < Test::Unit::TestCase

  class OriginalException < Exception
  end

  class ContinuedException < Exception
  end

  include DefinesConstants

  def setup
    super
    reset_config
  end

  def assert_sent(notice, notice_args)
    assert_received(Airbrake::Notice, :new) {|expect| expect.with(has_entries(notice_args)) }
    assert_received(Airbrake.sender, :send_to_airbrake) {|expect| expect.with(notice) }
  end

  def set_public_env
    Airbrake.configure { |config| config.environment_name = 'production' }
  end

  def set_development_env
    Airbrake.configure { |config| config.environment_name = 'development' }
  end

  should "yield and save a configuration when configuring" do
    yielded_configuration = nil
    Airbrake.configure do |config|
      yielded_configuration = config
    end

    assert_kind_of Airbrake::Configuration, yielded_configuration
    assert_equal yielded_configuration, Airbrake.configuration
  end

  should "not remove existing config options when configuring twice" do
    first_config = nil
    Airbrake.configure do |config|
      first_config = config
    end
    Airbrake.configure do |config|
      assert_equal first_config, config
    end
  end

  should "configure the sender" do
    sender = stub_sender
    Airbrake::Sender.stubs(:new => sender)
    configuration = nil

    Airbrake.configure { |yielded_config| configuration = yielded_config }

    assert_received(Airbrake::Sender, :new) { |expect| expect.with(configuration) }
    assert_equal sender, Airbrake.sender
  end

  should "create and send a notice for an exception" do
    set_public_env
    exception = build_exception
    stub_sender!
    notice = stub_notice!

    Airbrake.notify(exception)

    assert_sent notice, :exception => exception
  end

  should "create and send a notice for a hash" do
    set_public_env
    notice = stub_notice!
    notice_args = { :error_message => 'uh oh' }
    stub_sender!

    Airbrake.notify(notice_args)

    assert_sent(notice, notice_args)
  end

  should "not pass the hash as an exception when sending a notice for it" do
    set_public_env
    stub_notice!
    notice_args = { :error_message => 'uh oh' }
    stub_sender!

    Airbrake.notify(notice_args)

    assert_received(Airbrake::Notice, :new) {|expect| expect.with(Not(has_key(:exception))) }
  end

  should "create and send a notice for an exception that responds to to_hash" do
    set_public_env
    exception = build_exception
    notice = stub_notice!
    notice_args = { :error_message => 'uh oh' }
    exception.stubs(:to_hash).returns(notice_args)
    stub_sender!

    Airbrake.notify(exception)

    assert_sent(notice, notice_args.merge(:exception => exception))
  end

  should "create and sent a notice for an exception and hash" do
    set_public_env
    exception = build_exception
    notice = stub_notice!
    notice_args = { :error_message => 'uh oh' }
    stub_sender!

    Airbrake.notify(exception, notice_args)

    assert_sent(notice, notice_args.merge(:exception => exception))
  end

  should "not create a notice in a development environment" do
    set_development_env
    sender = stub_sender!

    Airbrake.notify(build_exception)
    Airbrake.notify_or_ignore(build_exception)

    assert_received(sender, :send_to_airbrake) {|expect| expect.never }
  end

  should "not deliver an ignored exception when notifying implicitly" do
    set_public_env
    exception = build_exception
    sender = stub_sender!
    notice = stub_notice!
    notice.stubs(:ignore? => true)

    Airbrake.notify_or_ignore(exception)

    assert_received(sender, :send_to_airbrake) {|expect| expect.never }
  end

  should "deliver exception in async-mode" do
    Airbrake.configure do |config|
      config.environment_name = 'production'
      config.async do |notice|
        Airbrake.sender.send_to_airbrake(notice)
      end
    end
    exception = build_exception
    stub_sender!
    notice = stub_notice!

    Airbrake.notify(exception)

    assert_sent(notice, :exception => exception)
  end

  should "pass notice in async-mode" do
    received_notice = nil
    Airbrake.configure do |config|
      config.environment_name = 'production'
      config.async {|notice| received_notice = notice}
    end
    exception = build_exception
    stub_sender!
    notice = stub_notice!

    Airbrake.notify(exception)

    assert_equal received_notice, notice
  end

  should "deliver an ignored exception when notifying manually" do
    set_public_env
    exception = build_exception
    stub_sender!
    notice = stub_notice!
    notice.stubs(:ignore? => true)

    Airbrake.notify(exception)

    assert_sent(notice, :exception => exception)
  end

  should "pass config to created notices" do
    exception = build_exception
    config_opts = { 'one' => 'two', 'three' => 'four' }
    stub_notice!
    stub_sender!
    Airbrake.configuration = stub('config', :merge => config_opts, :configured? => true, :public? => true,:async? => nil)

    Airbrake.notify(exception)

    assert_received(Airbrake::Notice, :new) do |expect|
      expect.with(has_entries(config_opts))
    end
  end

  context "building notice JSON for an exception" do
    setup do
      @params    = { :controller => "users", :action => "create" }
      @exception = build_exception
      @hash      = Airbrake.build_lookup_hash_for(@exception, @params)
    end

    should "set action" do
      assert_equal @params[:action], @hash[:action]
    end

    should "set controller" do
      assert_equal @params[:controller], @hash[:component]
    end

    should "set line number" do
      assert @hash[:line_number] =~ /\d+/
    end

    should "set file" do
      assert_match(/test\/helper\.rb$/, @hash[:file])
    end

    should "set rails_env to production" do
      assert_equal 'production', @hash[:environment_name]
    end

    should "set error class" do
      assert_equal @exception.class.to_s, @hash[:error_class]
    end

    should "not set file or line number with no backtrace" do
      @exception.stubs(:backtrace).returns([])

      @hash = Airbrake.build_lookup_hash_for(@exception)

      assert_nil @hash[:line_number]
      assert_nil @hash[:file]
    end

    should "not set action or controller when not provided" do
      @hash = Airbrake.build_lookup_hash_for(@exception)

      assert_nil @hash[:action]
      assert_nil @hash[:controller]
    end

    context "when an exception that provides #original_exception is raised" do
      setup do
        @exception.stubs(:original_exception).returns(begin
          raise NotifierTest::OriginalException.new
        rescue Exception => e
          e
        end)
      end

      should "unwrap exceptions that provide #original_exception" do
        @hash = Airbrake.build_lookup_hash_for(@exception)
        assert_equal "NotifierTest::OriginalException", @hash[:error_class]
      end

      should "keep exception if #original_exception is nil" do
        @exception.stubs(:original_exception).returns(nil)
        @hash = Airbrake.build_lookup_hash_for(@exception)
        assert_equal "BacktracedException", @hash[:error_class]
      end
    end

    context "when an exception that provides #continued_exception is raised" do
      setup do
        @exception.stubs(:continued_exception).returns(begin
          raise NotifierTest::ContinuedException.new
        rescue Exception => e
          e
        end)
      end

      should "unwrap exceptions that provide #continued_exception" do
        @hash = Airbrake.build_lookup_hash_for(@exception)
        assert_equal "NotifierTest::ContinuedException", @hash[:error_class]
      end

      should "keep exception if #continued_exception is nil" do
        @exception.stubs(:continued_exception).returns(nil)
        @hash = Airbrake.build_lookup_hash_for(@exception)
        assert_equal "BacktracedException", @hash[:error_class]
      end
    end
  end
end