require 'rails_helper'
require 'ostruct'

describe Agents::PostAgent do
  let(:response_body) { '<html>an über webpage!</html>' }
  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!

    @message = Message.new
    @message.agent = agents(:jane_status_agent)
    @message.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: response_body, 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 messages and merge the payloads with options['payload']" do
      message1 = Message.new
      message1.agent = agents(:bob_status_agent)
      message1.payload = {
        'xyz' => 'value1',
        'message' => 'value2',
        'default' => 'value2'
      }

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

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

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

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

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

    it 'can make a GET request merging params in post_url, payload and message' do
      @checker.options['method'] = 'get'
      @checker.options['post_url'] = 'http://example.com/a/path?existing_param=existing_value'
      @message.payload = {
        'some_param' => 'some_value',
        'another_param' => 'another_value'
      }
      @checker.receive(@message)
      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 message 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(@message)
      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'
      @message.payload = {
        'domain' => 'google.com',
        'variable' => 'a_variable'
      }
      @checker.receive(@message)
      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 message payload' do
      @checker.options['headers'] = {
        'Foo' => '{{ variable }}'
      }
      @message.payload = {
        'variable' => 'a_variable'
      }
      @checker.receive(@message)
      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(headers: {
                'Accept-Encoding' => 'gzip,deflate',
                'Content-Type' => /\Amultipart\/form-data; boundary=/,
                'User-Agent' => 'ActiveWorkflow - https://github.com/automaticmode/active_workflow'
              }) { |request|
        qboundary = Regexp.quote(request.headers['Content-Type'][/ boundary=(.+)/, 1])
        /\A--#{qboundary}\r\nContent-Disposition: form-data; name="default"\r\n\r\nvalue\r\n--#{qboundary}\r\nContent-Disposition: form-data; name="file"; filename="local.path"\r\nContent-Length: 8\r\nContent-Type: text\/plain\r\nContent-Transfer-Encoding: binary\r\n\r\ntestdata\r\n--#{qboundary}--\r\n\z/ === request.body
      }.to_return(status: 200, body: '', headers: {})
      message = Message.new(payload: { file_pointer: { agent_id: 111, file: 'test' } })
      io_mock = mock()
      mock(@checker).get_io(message) { StringIO.new('testdata') }
      @checker.options['no_merge'] = true
      @checker.receive(message)
    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 messages' do
      context 'when emit_messages is not set to true' do
        it 'does not emit messages' do
          expect {
            @checker.check
          }.not_to change { @checker.messages.count }
        end
      end

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

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

        it 'emits the body' do
          @checker.check
          expect(@checker.messages.last.payload['body']).to eq '<html>an über webpage!</html>'
        end

        context 'when response is has no encoding (binary)' do
          let!(:response_body) { "an \xC3\xBCber webpage!".force_encoding('binary') }

          it 'emits the body decoded as UTF-8' do
            @checker.check
            expect(@checker.messages.last.payload['body']).to eq 'an über webpage!'
          end
        end

        context 'when response encoding is given explicitly' do
          let!(:response_body) { "a\xE8i\xFB!".force_encoding('binary') }

          it 'emits the body decoded according to force_response_encoding' do
            @checker.options['force_response_encoding'] = 'windows-1257'
            @checker.check
            expect(@checker.messages.last.payload['body']).to eq 'ačiū!'
          end
        end

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

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

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

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

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

          it 'emits the received message' do
            @checker.receive(@message)
            @checker.check
            expect(@checker.messages.last.payload['somekey']).to eq('somevalue')
            expect(@checker.messages.last.payload['someotherkey']).to eq({ 'somekey' => 'value' })
          end
        end
      end
    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 absence of expected_receive_period_in_days is allowed' do
      @checker.options['expected_receive_period_in_days'] = ''
      expect(@checker).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'] = %w[foo bar]
      expect(@checker).to be_valid

      @checker.options['payload'] = 'hello'
      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 or an array 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_messages to be true or false' do
      @checker.options['emit_messages'] = 'what?'
      expect(@checker).not_to be_valid

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

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

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

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

    it 'requires force_response_encoding to be supported encoding' do
      @checker.options['force_response_encoding'] = 'utf-19'
      expect(@checker).not_to be_valid

      @checker.options['force_response_encoding'] = 'windows-1257'
      expect(@checker).to be_valid
    end

    it "requires output_mode to be 'clean' or 'merge', if present" do
      @checker.options['output_mode'] = 'what?'
      expect(@checker).not_to be_valid

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

      @checker.options['output_mode'] = 'clean'
      expect(@checker).to be_valid

      @checker.options['output_mode'] = 'merge'
      expect(@checker).to be_valid

      @checker.options['output_mode'] = :clean
      expect(@checker).to be_valid

      @checker.options['output_mode'] = :merge
      expect(@checker).to be_valid

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

      @checker.options['output_mode'] = "{% if key == 'foo' %}merge{% else %}clean{% endif %}"
      expect(@checker).to be_valid
    end
  end
end
