require 'rails_helper'
require 'ostruct'

describe Agents::PostAgent do
  before do
    @valid_options = {
      'post_url' => "http://www.example.com",
      'expected_receive_period_in_days' => 1,
      'payload' => {
        'default' => 'value'
      }
    }
    @valid_params = {
      name: "somename",
      options: @valid_options
    }

    @checker = Agents::PostAgent.new(@valid_params)
    @checker.user = users(:jane)
    @checker.save!

    @event = Event.new
    @event.agent = agents(:jane_weather_agent)
    @event.payload = {
      'somekey' => 'somevalue',
      'someotherkey' => {
        'somekey' => 'value'
      }
    }
    @requests = 0
    @sent_requests = Hash.new { |hash, method| hash[method] = [] }

    stub_request(:any, /:/).to_return { |request|
      method = request.method
      @requests += 1
      @sent_requests[method] << req = OpenStruct.new(uri: request.uri, headers: request.headers)
      case method
      when :get, :delete
        req.data = request.uri.query
      else
        content_type = request.headers['Content-Type'][/\A[^;\s]+/]
        case content_type
        when 'application/x-www-form-urlencoded'
          req.data = request.body
        when 'application/json'
          req.data = ActiveSupport::JSON.decode(request.body)
        when 'text/xml'
          req.data = Hash.from_xml(request.body)
        when Agents::PostAgent::MIME_RE
          req.data = request.body
        else
          raise "unexpected Content-Type: #{content_type}"
        end
      end
      { status: 200, body: "<html>a webpage!</html>", headers: { 'Content-type' => 'text/html' } }
    }
  end

  it_behaves_like WebRequestConcern
  it_behaves_like 'FileHandlingConsumer'

  it 'renders the description markdown without errors' do
    expect { @checker.description }.not_to raise_error
  end

  describe "making requests" do
    it "can make requests of each type" do
      %w[get put post patch delete].each.with_index(1) do |verb, index|
        @checker.options['method'] = verb
        expect(@checker).to be_valid
        @checker.check
        expect(@requests).to eq(index)
        expect(@sent_requests[verb.to_sym].length).to eq(1)
      end
    end
  end

  describe "#receive" do
    it "can handle multiple events and merge the payloads with options['payload']" do
      event1 = Event.new
      event1.agent = agents(:bob_weather_agent)
      event1.payload = {
        'xyz' => 'value1',
        'message' => 'value2',
        'default' => 'value2'
      }

      expect {
        expect {
          @checker.receive([@event, event1])
        }.to change { @sent_requests[:post].length }.by(2)
      }.not_to change { @sent_requests[:get].length }

      expect(@sent_requests[:post][0].data).to eq(@event.payload.merge('default' => 'value').to_query)
      expect(@sent_requests[:post][1].data).to eq(event1.payload.to_query)
    end

    it "can make GET requests" do
      @checker.options['method'] = 'get'

      expect {
        expect {
          @checker.receive([@event])
        }.to change { @sent_requests[:get].length }.by(1)
      }.not_to change { @sent_requests[:post].length }

      expect(@sent_requests[:get][0].data).to eq(@event.payload.merge('default' => 'value').to_query)
    end

    it "can make a GET request merging params in post_url, payload and event" do
      @checker.options['method'] = 'get'
      @checker.options['post_url'] = "http://example.com/a/path?existing_param=existing_value"
      @event.payload = {
        "some_param" => "some_value",
        "another_param" => "another_value"
      }
      @checker.receive([@event])
      uri = @sent_requests[:get].first.uri
      # parameters are alphabetically sorted by Faraday
      expect(uri.request_uri).to eq("/a/path?another_param=another_value&default=value&existing_param=existing_value&some_param=some_value")
    end

    it "can skip merging the incoming event when no_merge is set, but it still interpolates" do
      @checker.options['no_merge'] = 'true'
      @checker.options['payload'] = {
        'key' => 'it said: {{ someotherkey.somekey }}'
      }
      @checker.receive([@event])
      expect(@sent_requests[:post].first.data).to eq({ 'key' => 'it said: value' }.to_query)
    end

    it "interpolates when receiving a payload" do
      @checker.options['post_url'] = "https://{{ domain }}/{{ variable }}?existing_param=existing_value"
      @event.payload = {
        'domain' => 'google.com',
        'variable' => 'a_variable'
      }
      @checker.receive([@event])
      uri = @sent_requests[:post].first.uri
      expect(uri.scheme).to eq('https')
      expect(uri.host).to eq('google.com')
      expect(uri.path).to eq('/a_variable')
      expect(uri.query).to eq("existing_param=existing_value")
    end

    it "interpolates outgoing headers with the event payload" do
      @checker.options['headers'] = {
        "Foo" => "{{ variable }}"
      }
      @event.payload = {
        'variable' => 'a_variable'
      }
      @checker.receive([@event])
      headers = @sent_requests[:post].first.headers
      expect(headers["Foo"]).to eq("a_variable")
    end

    it 'makes a multipart request when receiving a file_pointer' do
      WebMock.reset!
      stub_request(:post, "http://www.example.com/").
        with(:body => "-------------RubyMultipartPost\r\nContent-Disposition: form-data; name=\"default\"\r\n\r\nvalue\r\n-------------RubyMultipartPost\r\nContent-Disposition: form-data; name=\"file\"; filename=\"local.path\"\r\nContent-Length: 8\r\nContent-Type: \r\nContent-Transfer-Encoding: binary\r\n\r\ntestdata\r\n-------------RubyMultipartPost--\r\n\r\n",
             :headers => {'Accept-Encoding'=>'gzip,deflate', 'Content-Length'=>'307', 'Content-Type'=>'multipart/form-data; boundary=-----------RubyMultipartPost', 'User-Agent'=>'Huginn - https://github.com/cantino/huginn'}).
        to_return(:status => 200, :body => "", :headers => {})
      event = Event.new(payload: {file_pointer: {agent_id: 111, file: 'test'}})
      io_mock = mock()
      mock(@checker).get_io(event) { StringIO.new("testdata") }
      @checker.options['no_merge'] = true
      @checker.receive([event])
    end
  end

  describe "#check" do
    it "sends options['payload'] as a POST request" do
      expect {
        @checker.check
      }.to change { @sent_requests[:post].length }.by(1)

      expect(@sent_requests[:post][0].data).to eq(@checker.options['payload'].to_query)
    end

    it "sends options['payload'] as JSON as a POST request" do
      @checker.options['content_type'] = 'json'
      expect {
        @checker.check
      }.to change { @sent_requests[:post].length }.by(1)

      expect(@sent_requests[:post][0].data).to eq(@checker.options['payload'])
    end

    it "sends options['payload'] as XML as a POST request" do
      @checker.options['content_type'] = 'xml'
      expect {
        @checker.check
      }.to change { @sent_requests[:post].length }.by(1)

      expect(@sent_requests[:post][0].data.keys).to eq([ 'post' ])
      expect(@sent_requests[:post][0].data['post']).to eq(@checker.options['payload'])
    end

    it "sends options['payload'] as XML with custom root element name, as a POST request" do
      @checker.options['content_type'] = 'xml'
      @checker.options['xml_root'] = 'foobar'
      expect {
        @checker.check
      }.to change { @sent_requests[:post].length }.by(1)

      expect(@sent_requests[:post][0].data.keys).to eq([ 'foobar' ])
      expect(@sent_requests[:post][0].data['foobar']).to eq(@checker.options['payload'])
    end

    it "sends options['payload'] as a GET request" do
      @checker.options['method'] = 'get'
      expect {
        expect {
          @checker.check
        }.to change { @sent_requests[:get].length }.by(1)
      }.not_to change { @sent_requests[:post].length }

      expect(@sent_requests[:get][0].data).to eq(@checker.options['payload'].to_query)
    end

    it "sends options['payload'] as a string POST request when content-type continas a MIME type" do
      @checker.options['payload'] = '<test>hello</test>'
      @checker.options['content_type'] = 'application/xml'
      expect {
        @checker.check
      }.to change { @sent_requests[:post].length }.by(1)

      expect(@sent_requests[:post][0].data).to eq('<test>hello</test>')
    end

    it "interpolates outgoing headers" do
      @checker.options['headers'] = {
        "Foo" => "{% credential aws_key %}"
      }
      @checker.check
      headers = @sent_requests[:post].first.headers
      expect(headers["Foo"]).to eq("2222222222-jane")
    end

    describe "emitting events" do
      context "when emit_events is not set to true" do
        it "does not emit events" do
          expect {
            @checker.check
          }.not_to change { @checker.events.count }
        end
      end

      context "when emit_events is set to true" do
        before do
          @checker.options['emit_events'] = 'true'
          @checker.save!
        end

        it "emits the response status" do
          expect {
            @checker.check
          }.to change { @checker.events.count }.by(1)
          expect(@checker.events.last.payload['status']).to eq 200
        end

        it "emits the body" do
          @checker.check
          expect(@checker.events.last.payload['body']).to eq '<html>a webpage!</html>'
        end

        it "emits the response headers capitalized by default" do
          @checker.check
          expect(@checker.events.last.payload['headers']).to eq({ 'Content-Type' => 'text/html' })
        end

        it "emits the response headers capitalized" do
          @checker.options['event_headers_style'] = 'capitalized'
          @checker.check
          expect(@checker.events.last.payload['headers']).to eq({ 'Content-Type' => 'text/html' })
        end

        it "emits the response headers downcased" do
          @checker.options['event_headers_style'] = 'downcased'
          @checker.check
          expect(@checker.events.last.payload['headers']).to eq({ 'content-type' => 'text/html' })
        end

        it "emits the response headers snakecased" do
          @checker.options['event_headers_style'] = 'snakecased'
          @checker.check
          expect(@checker.events.last.payload['headers']).to eq({ 'content_type' => 'text/html' })
        end
      end
    end
  end

  describe "#working?" do
    it "checks if events have been received within expected receive period" do
      expect(@checker).not_to be_working
      Agents::PostAgent.async_receive @checker.id, [@event.id]
      expect(@checker.reload).to be_working
      two_days_from_now = 2.days.from_now
      stub(Time).now { two_days_from_now }
      expect(@checker.reload).not_to be_working
    end
  end

  describe "validation" do
    before do
      expect(@checker).to be_valid
    end

    it "should validate presence of post_url" do
      @checker.options['post_url'] = ""
      expect(@checker).not_to be_valid
    end

    it "should validate presence of expected_receive_period_in_days" do
      @checker.options['expected_receive_period_in_days'] = ""
      expect(@checker).not_to be_valid
    end

    it "should validate method as post, get, put, patch, or delete, defaulting to post" do
      @checker.options['method'] = ""
      expect(@checker.method).to eq("post")
      expect(@checker).to be_valid

      @checker.options['method'] = "POST"
      expect(@checker.method).to eq("post")
      expect(@checker).to be_valid

      @checker.options['method'] = "get"
      expect(@checker.method).to eq("get")
      expect(@checker).to be_valid

      @checker.options['method'] = "patch"
      expect(@checker.method).to eq("patch")
      expect(@checker).to be_valid

      @checker.options['method'] = "wut"
      expect(@checker.method).to eq("wut")
      expect(@checker).not_to be_valid
    end

    it "should validate that no_merge is 'true' or 'false', if present" do
      @checker.options['no_merge'] = ""
      expect(@checker).to be_valid

      @checker.options['no_merge'] = "true"
      expect(@checker).to be_valid

      @checker.options['no_merge'] = "false"
      expect(@checker).to be_valid

      @checker.options['no_merge'] = false
      expect(@checker).to be_valid

      @checker.options['no_merge'] = true
      expect(@checker).to be_valid

      @checker.options['no_merge'] = 'blarg'
      expect(@checker).not_to be_valid
    end

    it "should validate payload as a hash, if present" do
      @checker.options['payload'] = ""
      expect(@checker).to be_valid

      @checker.options['payload'] = "hello"
      expect(@checker).not_to be_valid

      @checker.options['payload'] = ["foo", "bar"]
      expect(@checker).not_to be_valid

      @checker.options['payload'] = { 'this' => 'that' }
      expect(@checker).to be_valid
    end

    it "should not validate payload as a hash if content_type includes a MIME type and method is not get or delete" do
      @checker.options['no_merge'] = 'true'
      @checker.options['content_type'] = 'text/xml'
      @checker.options['payload'] = "test"
      expect(@checker).to be_valid

      @checker.options['method'] = 'get'
      expect(@checker).not_to be_valid

      @checker.options['method'] = 'delete'
      expect(@checker).not_to be_valid
    end

    it "requires `no_merge` to be set to true when content_type contains a MIME type" do
      @checker.options['content_type'] = 'text/xml'
      @checker.options['payload'] = "test"
      expect(@checker).not_to be_valid
    end

    it "requires headers to be a hash, if present" do
      @checker.options['headers'] = [1,2,3]
      expect(@checker).not_to be_valid

      @checker.options['headers'] = "hello world"
      expect(@checker).not_to be_valid

      @checker.options['headers'] = ""
      expect(@checker).to be_valid

      @checker.options['headers'] = {}
      expect(@checker).to be_valid

      @checker.options['headers'] = { "Authorization" => "foo bar" }
      expect(@checker).to be_valid
    end

    it "requires emit_events to be true or false" do
      @checker.options['emit_events'] = 'what?'
      expect(@checker).not_to be_valid

      @checker.options.delete('emit_events')
      expect(@checker).to be_valid

      @checker.options['emit_events'] = 'true'
      expect(@checker).to be_valid

      @checker.options['emit_events'] = 'false'
      expect(@checker).to be_valid

      @checker.options['emit_events'] = true
      expect(@checker).to be_valid
    end
  end
end
