require "spec_helper"

# Note: these tests access instance variables and private methods as a means of
# not muddying the public API. This object should expose a simple buffer like
# API, tests should not alter that.
describe Logtail::LogDevices::HTTP do
  describe "#initialize" do
    it "should initialize properly" do
      http = described_class.new("MYKEY", flush_interval: 0.1)

      # Ensure that threads have not started
      thread = http.instance_variable_get(:@flush_thread)
      expect(thread).to be_nil
      thread = http.instance_variable_get(:@request_outlet_thread)
      expect(thread).to be_nil
    end
  end

  describe "#write" do
    let(:http) { described_class.new("MYKEY") }
    let(:msg_queue) { http.instance_variable_get(:@msg_queue) }

    it "should buffer the messages" do
      http.write("test log message")
      expect(msg_queue.flush).to eq(["test log message"])
      http.close
    end

    it "should start the flush threads" do
      http.write("test log message")

      thread = http.instance_variable_get(:@flush_thread)
      expect(thread).to be_alive
      thread = http.instance_variable_get(:@request_outlet_thread)
      expect(thread).to be_alive
      expect(http).to receive(:flush).exactly(1).times
      http.close
    end

    context "with a low batch size" do
      let(:http) { described_class.new("MYKEY", :batch_size => 2) }

      it "should attempt a delivery when the limit is exceeded" do
        http.write("test")
        expect(http).to receive(:flush_async).exactly(1).times
        http.write("my log message")
        expect(http).to receive(:flush).exactly(1).times
        http.close
      end
    end
  end

  describe "#close" do
    let(:http) { described_class.new("MYKEY") }

    it "should kill the threads" do
      http.send(:ensure_flush_threads_are_started)
      http.close
      thread = http.instance_variable_get(:@flush_thread)
      sleep 0.1 # too fast!
      expect(thread).to_not be_alive
      thread = http.instance_variable_get(:@request_outlet_thread)
      sleep 0.1 # too fast!
      expect(thread).to_not be_alive
    end

    it "should attempt a delivery" do
      message = "a" * 19
      http.write(message)
      expect(http).to receive(:flush).exactly(1).times
      http.close
    end
  end

  # Testing a private method because it helps break down our tests
  describe "#flush" do
    let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }

    it "should deliver the request" do
      http = described_class.new("MYKEY", flush_continuously: false)
      log_entry = Logtail::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
      http.write(log_entry)
      log_entry = Logtail::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
      http.write(log_entry)
      expect(http).to receive(:flush_async).exactly(2).times
      http.send(:flush)
      http.close
    end
  end

  # Testing a private method because it helps break down our tests
  describe "#flush_async" do
    let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }

    it "should add a request to the queue" do
      http = described_class.new("MYKEY", flush_continuously: false)
      log_entry = Logtail::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
      http.write(log_entry)
      log_entry = Logtail::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
      http.write(log_entry)
      http.send(:flush_async)
      request_queue = http.instance_variable_get(:@request_queue)
      request_attempt = request_queue.deq
      expect(request_attempt.request).to be_kind_of(Net::HTTP::Post)
      expect(request_attempt.request.body).to start_with("\x92\x84\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z\xA7message\xB2test log message 1".force_encoding("ASCII-8BIT"))

      message_queue = http.instance_variable_get(:@msg_queue)
      expect(message_queue.size).to eq(0)
    end
  end

  # Testing a private method because it helps break down our tests
  describe "#intervaled_flush" do
    it "should start a intervaled flush thread and flush on an interval" do
      http = described_class.new("MYKEY", flush_interval: 0.1)
      http.send(:ensure_flush_threads_are_started)
      expect(http).to receive(:flush_async).at_least(3).times
      sleep 1.1 # iterations check every 0.5 seconds
      http.close
    end
  end

  # Outlet
  describe "#request_outlet" do
    let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }

    it "should deliver requests on an interval" do
      stub = stub_request(:post, "https://in.logtail.com/").
        with(
          :body => start_with("\x92\x84\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z\xA7message\xB2test log message 1".force_encoding("ASCII-8BIT")),
          :headers => {
            'Authorization' => 'Bearer MYKEY',
            'Content-Type' => 'application/msgpack',
            'User-Agent' => "Logtail Ruby/#{Logtail::VERSION} (HTTP)"
          }
        ).
        to_return(:status => 200, :body => "", :headers => {})

      http = described_class.new("MYKEY", flush_interval: 0.1)
      log_entry1 = Logtail::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
      http.write(log_entry1)
      log_entry2 = Logtail::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
      http.write(log_entry2)
      sleep 2

      expect(stub).to have_been_requested.times(1)

      http.close
    end
  end

  describe "#deliver_requests" do
    it "should handle exceptions properly and return" do
      allow_any_instance_of(Net::HTTP).to receive(:request).and_raise("boom")

      http_device = described_class.new("MYKEY", flush_continuously: false)
      req_queue = http_device.instance_variable_get(:@request_queue)

      # Place a request on the queue
      request = Net::HTTP::Post.new("/")
      request_attempt = Logtail::LogDevices::HTTP::RequestAttempt.new(request)
      request_attempt.attempted!
      req_queue.enq(request_attempt)

      # Start a HTTP connection to test the method directly
      http = http_device.send(:build_http)
      http.start do |conn|
        result = http_device.send(:deliver_requests, conn)
        expect(result).to eq(false)
      end

      expect(req_queue.size).to eq(1)

      # Start a HTTP connection to test the method directly
      http = http_device.send(:build_http)
      http.start do |conn|
        result = http_device.send(:deliver_requests, conn)
        expect(result).to eq(false)
      end

      # Ensure the request gets discards after 3 attempts
      expect(req_queue.size).to eq(0)
    end
  end
end