require 'spec_helper'

describe Syslogger do

  let(:fake_syslog) { double('syslog', :mask= => true) }

  describe '.new' do
    it 'should log to the default syslog facility, with the default options' do
      expect(Syslog).to receive(:open).with($0, Syslog::LOG_PID | Syslog::LOG_CONS, nil).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_WARNING, 'Some message')
      logger = Syslogger.new
      logger.warn 'Some message'
    end

    it 'should log to the user facility, with specific options' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_WARNING, 'Some message')
      logger = Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER)
      logger.warn 'Some message'
    end
  end

  describe '#add' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }

    it 'should respond to add' do
      expect(logger).to respond_to(:add)
    end

    it 'should correctly log' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger.add(Logger::INFO, 'message')
    end

    it 'should take the message from the block if :message is nil' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger.add(Logger::INFO) { 'message' }
    end

    it 'should use the given progname' do
      expect(Syslog).to receive(:open).with('progname', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger.add(Logger::INFO, 'message', 'progname')
    end

    it 'should use the default progname when message is passed in progname' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger.add(Logger::INFO, nil, 'message')
    end

    it 'should use the given progname if message is passed in block' do
      expect(Syslog).to receive(:open).with('progname', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger.add(Logger::INFO, nil, 'progname') { 'message' }
    end

    it "should substitute '%' for '%%' before adding the :message" do
      allow(Syslog).to receive(:open).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, "%%me%%ssage%%")
      logger.add(Logger::INFO, "%me%ssage%")
    end

    it 'should clean formatted message' do
      formatter = Class.new(Syslogger::SimpleFormatter) do
        def call(severity, timestamp, progname, msg)
          msg.split(//).join('%')
        end
      end

      allow(Syslog).to receive(:open).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, "m%%e%%s%%s%%a%%g%%e")

      original_formatter = logger.formatter

      begin
        logger.formatter = formatter.new
        logger.add(Logger::INFO, 'message')
      ensure
        logger.formatter = original_formatter
      end
    end

    it 'should clean tagged message' do
      allow(Syslog).to receive(:open).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, "[t%%a%%g%%g%%e%%d] [it] message")

      logger.tagged("t%a%g%g%e%d") do
        logger.tagged('it') do
          logger.add(Logger::INFO, 'message')
        end
      end
    end

    it 'should strip the :message' do
      allow(Syslog).to receive(:open).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger.add(Logger::INFO, "\n\nmessage  ")
    end

    it 'should not raise exception if asked to log with a nil message and body' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, '')
      expect {
        logger.add(Logger::INFO, nil)
      }.to_not raise_error
    end

    it 'should send an empty string if the message and block are nil' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, '')
      logger.add(Logger::INFO, nil)
    end

    it 'should split string over the max octet size' do
      logger.max_octets = 480
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'a' * 480).twice
      logger.add(Logger::INFO, 'a' * 960)
    end

    it 'should apply the log formatter to the message' do
      formatter = Class.new(Syslogger::SimpleFormatter) do
        def call(severity, timestamp, progname, msg)
          "test #{msg}!"
        end
      end
      allow(Syslog).to receive(:open).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'test message!')
      logger.formatter = formatter.new
      logger.add(Logger::INFO, 'message')
    end
  end

  describe '#<<' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }

    it 'should respond to <<' do
      expect(logger).to respond_to(:<<)
    end

    it 'should correctly log' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger << 'message'
    end
  end

  describe '#puts' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }

    it 'should respond to puts' do
      expect(logger).to respond_to(:puts)
    end

    it 'should correctly log' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger.puts 'message'
    end
  end

  describe '#write' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }

    it 'should respond to write' do
      expect(logger).to respond_to(:write)
    end

    it 'should correctly log' do
      expect(Syslog).to receive(:open).with('my_app', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log).with(Syslog::LOG_INFO, 'message')
      logger.write 'message'
    end
  end

  describe '#max_octets=' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }

    it 'should set the max_octets for the logger' do
      expect { logger.max_octets = 1 }.to change(logger, :max_octets)
      expect(logger.max_octets).to eq 1
    end
  end

  describe '#ident=' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID | Syslog::LOG_CONS, nil) }

    it 'should permanently change the ident string' do
      logger.ident = 'new_ident'
      expect(Syslog).to receive(:open).with('new_ident', Syslog::LOG_PID | Syslog::LOG_CONS, nil).and_yield(fake_syslog)
      expect(fake_syslog).to receive(:log)
      logger.warn('should get the new ident string')
    end
  end

  describe '#level=' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }

    { :debug => Logger::DEBUG,
      :info  => Logger::INFO,
      :warn  => Logger::WARN,
      :error => Logger::ERROR,
      :fatal => Logger::FATAL
    }.each_pair do |level_symbol, level_value|
      it "should allow using :#{level_symbol}" do
        logger.level = level_symbol
        expect(logger.level).to eq level_value
      end

      it "should allow using Integer #{level_value}" do
        logger.level = level_value
        expect(logger.level).to eq level_value
      end
    end

    it 'should not allow using random symbols' do
      expect {
        logger.level = :foo
      }.to raise_error(ArgumentError)
    end

    it 'should not allow using symbols mapping back to non-level constants' do
      expect {
        logger.level = :version
      }.to raise_error(ArgumentError)
    end

    it 'should not allow using strings' do
      expect {
        logger.level = 'warn'
      }.to raise_error(ArgumentError)
    end
  end

  describe '#:level? methods' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }

    %w{debug info warn error fatal}.each do |logger_method|
      it "should respond to the #{logger_method}? method" do
        expect(logger).to respond_to "#{logger_method}?".to_sym
      end
    end

    it 'should not have unknown? method' do
      expect(logger).to_not respond_to :unknown?
    end

    context 'when loglevel is Logger::DEBUG' do
      it 'should return true for all methods' do
        logger.level = Logger::DEBUG
        %w{debug info warn error fatal}.each do |logger_method|
          expect(logger.send("#{logger_method}?")).to be true
        end
      end
    end

    context 'when loglevel is Logger::INFO' do
      it 'should return true for all except debug?' do
        logger.level = Logger::INFO
        %w{info warn error fatal}.each do |logger_method|
          expect(logger.send("#{logger_method}?")).to be true
        end
        expect(logger.debug?).to be false
      end
    end

    context 'when loglevel is Logger::WARN' do
      it 'should return true for warn?, error? and fatal? when WARN' do
        logger.level = Logger::WARN
        %w{warn error fatal}.each do |logger_method|
          expect(logger.send("#{logger_method}?")).to be true
        end
        %w{debug info}.each do |logger_method|
          expect(logger.send("#{logger_method}?")).to be false
        end
      end
    end

    context 'when loglevel is Logger::ERROR' do
      it 'should return true for error? and fatal? when ERROR' do
        logger.level = Logger::ERROR
        %w{error fatal}.each do |logger_method|
          expect(logger.send("#{logger_method}?")).to be true
        end
        %w{warn debug info}.each do |logger_method|
          expect(logger.send("#{logger_method}?")).to be false
        end
      end
    end

    context 'when loglevel is Logger::FATAL' do
      it 'should return true only for fatal? when FATAL' do
        logger.level = Logger::FATAL
        expect(logger.fatal?).to be true
        %w{error warn debug info}.each do |logger_method|
          expect(logger.send("#{logger_method}?")).to be false
        end
      end
    end
  end

  # Fix https://github.com/crohr/syslogger/issues/29
  describe '#formatter' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }

    it 'should not raise error' do
      ActiveJob::Base.logger = logger
      expect(logger).to receive(:info).at_least(1).times.with(nil)
      expect {
        HelloJob.perform_later "Cristian"
      }.to_not raise_error
    end
  end

  describe '#push_tags' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }
    after(:each) { logger.clear_tags! }

    it 'saves tags' do
      logger.push_tags('tag1')
      logger.push_tags('tag2')
      expect(logger.current_tags).to eq ['tag1', 'tag2']
    end

    it 'saves uniq tags' do
      logger.push_tags('tag1')
      logger.push_tags('tag2')
      logger.push_tags('foo')
      logger.push_tags('foo')
      expect(logger.current_tags).to eq ['tag1', 'tag2', 'foo']
    end
  end

  describe '#clear_tags!' do
    let(:logger) { Syslogger.new('my_app', Syslog::LOG_PID, Syslog::LOG_USER) }
    after(:each) { logger.clear_tags! }

    it 'clears tags' do
      expect(logger.current_tags).to eq []
      logger.push_tags('tag1')
      logger.push_tags('tag2')
      expect(logger.current_tags).to eq ['tag1', 'tag2']
      logger.clear_tags!
      expect(logger.current_tags).to eq []
    end
  end

  describe 'logger methods (debug info warn error fatal unknown)' do
    %w{debug info warn error fatal unknown}.each do |logger_method|

      it "should respond to the #{logger_method.inspect} method" do
        expect(Syslogger.new).to respond_to logger_method.to_sym
      end

      it "should log #{logger_method} without raising an exception if called with a block" do
        severity = Syslogger::MAPPING[Logger.const_get(logger_method.upcase)]

        logger = Syslogger.new
        logger.level = Logger.const_get(logger_method.upcase)

        expect(Syslog).to receive(:open).and_yield(fake_syslog)
        expect(fake_syslog).to receive(:log).with(severity, 'Some message that dont need to be in a block')
        expect {
          logger.send(logger_method.to_sym) { 'Some message that dont need to be in a block' }
        }.to_not raise_error
      end

      it "should log #{logger_method} using message as progname with the block's result" do
        severity = Syslogger::MAPPING[Logger.const_get(logger_method.upcase)]

        logger = Syslogger.new
        logger.level = Logger.const_get(logger_method.upcase)

        expect(Syslog).to receive(:open).with('Woah', anything, nil).and_yield(fake_syslog)
        expect(fake_syslog).to receive(:log).with(severity, 'Some message that really needs a block')
        expect {
          logger.send(logger_method.to_sym, 'Woah') { 'Some message that really needs a block' }
        }.to_not raise_error
      end

      it "should log #{logger_method} without raising an exception if called with a nil message" do
        logger = Syslogger.new
        expect {
          logger.send(logger_method.to_sym, nil)
        }.to_not raise_error
      end

      it "should log #{logger_method} without raising an exception if called with a no message" do
        logger = Syslogger.new
        expect {
          logger.send(logger_method.to_sym)
        }.to_not raise_error
      end

      it "should log #{logger_method} without raising an exception if message splits on an escape" do
        logger = Syslogger.new
        logger.max_octets = 100
        msg  = 'A' * 99
        msg += "%BBB"
        expect {
          logger.send(logger_method.to_sym, msg)
        }.to_not raise_error
      end
    end
  end

  describe 'it should be thread safe' do
    it 'should not fail under chaos' do
      threads = []
      (1..10).each do
        threads << Thread.new do
          (1..100).each do |index|
            logger = Syslogger.new(Thread.current.inspect, Syslog::LOG_PID, Syslog::LOG_USER)
            logger.write index
          end
        end
      end

      threads.map(&:join)
    end

    it 'should allow multiple instances to log at the same time' do
      logger1 = Syslogger.new('my_app1', Syslog::LOG_PID, Syslog::LOG_USER)
      logger2 = Syslogger.new('my_app2', Syslog::LOG_PID, Syslog::LOG_USER)

      syslog1 = double('syslog1', :mask= => true)
      syslog2 = double('syslog2', :mask= => true)

      expect(Syslog).to receive(:open).exactly(5000).times.with('my_app1', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(syslog1)
      expect(Syslog).to receive(:open).exactly(5000).times.with('my_app2', Syslog::LOG_PID, Syslog::LOG_USER).and_yield(syslog2)

      expect(syslog1).to receive(:log).exactly(5000).times.with(Syslog::LOG_INFO, 'logger1')
      expect(syslog2).to receive(:log).exactly(5000).times.with(Syslog::LOG_INFO, 'logger2')

      threads = []

      threads << Thread.new do
        5000.times do |i|
          logger1.info 'logger1'
        end
      end

      threads << Thread.new do
        5000.times do |i|
          logger2.info 'logger2'
        end
      end

      threads.map(&:join)
    end
  end

  describe 'it should respect loglevel precedence when logging' do
    %w{debug info warn error}.each do |logger_method|
      it "should not log #{logger_method} when level is higher" do
        logger = Syslogger.new
        logger.level = Logger::FATAL
        expect(Syslog).to_not receive(:open).with($0, Syslog::LOG_PID | Syslog::LOG_CONS, nil).and_yield(fake_syslog)
        expect(fake_syslog).to_not receive(:log).with(Syslog::LOG_NOTICE, 'Some message')
        logger.send(logger_method.to_sym, 'Some message')
      end

      it "should not evaluate a block or log #{logger_method} when level is higher" do
        logger = Syslogger.new
        logger.level = Logger::FATAL
        expect(Syslog).to_not receive(:open).with($0, Syslog::LOG_PID | Syslog::LOG_CONS, nil).and_yield(fake_syslog)
        expect(fake_syslog).to_not receive(:log).with(Syslog::LOG_NOTICE, 'Some message')
        logger.send(logger_method.to_sym) { violated 'This block should not have been called' }
      end
    end
  end
end
