# frozen_string_literal: true

require 'aws-eventstream'

module Aws
  module Stubbing
    module Protocols
      # @api private
      class Rest

        include Seahorse::Model::Shapes

        def stub_data(api, operation, data)
          resp = new_http_response
          apply_status_code(operation, resp, data)
          apply_headers(operation, resp, data)
          apply_body(api, operation, resp, data)
          resp
        end

        private

        def new_http_response
          resp = Seahorse::Client::Http::Response.new
          resp.status_code = 200
          resp.headers['x-amzn-RequestId'] = 'stubbed-request-id'
          resp
        end

        def apply_status_code(operation, resp, data)
          operation.output.shape.members.each do |member_name, member_ref|
            if member_ref.location == 'statusCode'
              resp.status_code = data[member_name] if data.key?(member_name)
            end
          end
        end

        def apply_headers(operation, resp, data)
          Aws::Rest::Request::Headers.new(operation.output).apply(resp, data)
        end

        def apply_body(api, operation, resp, data)
          resp.body = build_body(api, operation, data)
        end

        def build_body(api, operation, data)
          rules = operation.output
          if head_operation(operation)
            ''
          elsif streaming?(rules)
            data[rules[:payload]]
          elsif rules[:payload]
            body_for(api, operation, rules[:payload_member], data[rules[:payload]])
          else
            filtered = Seahorse::Model::Shapes::ShapeRef.new(
              shape: Seahorse::Model::Shapes::StructureShape.new.tap do |s|
                rules.shape.members.each do |member_name, member_ref|
                  s.add_member(member_name, member_ref) if member_ref.location.nil?
                end
              end
            )
            body_for(api, operation, filtered, data)
          end
        end

        def streaming?(ref)
          if ref[:payload]
            case ref[:payload_member].shape
            when StringShape then true
            when BlobShape then true
            else false
            end
          else
            false
          end
        end

        def head_operation(operation)
          operation.http_method == 'HEAD'
        end

        def eventstream?(rules)
          rules.eventstream
        end

        def encode_eventstream_response(rules, data, builder)
          data.inject('') do |stream, event_data|
            # construct message headers and payload
            opts = {headers: {}}
            case event_data.delete(:message_type)
            when 'event'
              encode_event(opts, rules, event_data, builder)
            when 'error'
              # errors are unmodeled
              encode_error(opts, event_data)
            when 'exception'
              # Pending
              raise 'Stubbing :exception event is not supported'
            end
            [stream, Aws::EventStream::Encoder.new.encode(
              Aws::EventStream::Message.new(opts)
            )].pack('a*a*')
          end
        end

        def encode_error(opts, event_data)
          opts[:headers][':error-message'] = Aws::EventStream::HeaderValue.new(
            value: event_data[:error_message],
            type: 'string'
          )
          opts[:headers][':error-code'] = Aws::EventStream::HeaderValue.new(
            value: event_data[:error_code],
            type: 'string'
          )
          opts[:headers][':message-type'] = Aws::EventStream::HeaderValue.new(
            value: 'error',
            type: 'string'
          )
          opts
        end

        def encode_unknown_event(opts, event_type, event_data)
          # right now h2 events are only rest_json
          opts[:payload] = StringIO.new(Aws::Json.dump(event_data))
          opts[:headers][':event-type'] = Aws::EventStream::HeaderValue.new(
            value: event_type.to_s,
            type: 'string'
          )
          opts[:headers][':message-type'] = Aws::EventStream::HeaderValue.new(
            value: 'event',
            type: 'string'
          )
          opts
        end

        def encode_modeled_event(opts, rules, event_type, event_data, builder)
          event_ref = rules.shape.member(event_type)
          explicit_payload = false
          implicit_payload_members = {}
          event_ref.shape.members.each do |name, ref|
            if ref.eventpayload
              explicit_payload = true
            else
              implicit_payload_members[name] = ref
            end
          end

          if !explicit_payload && !implicit_payload_members.empty?
            unless implicit_payload_members.size > 1
              m_name, _ = implicit_payload_members.first
              value = {}
              value[m_name] = event_data[m_name]
              opts[:payload] = StringIO.new(builder.new(event_ref).serialize(value))
            end
          end

          event_data.each do |k, v|
            member_ref = event_ref.shape.member(k)
            if member_ref.eventheader
              opts[:headers][member_ref.location_name] = Aws::EventStream::HeaderValue.new(
                value: v,
                type: member_ref.eventheader_type
              )
            elsif member_ref.eventpayload
              case member_ref.eventpayload_type
              when 'string'
                opts[:payload] = StringIO.new(v)
              when 'blob'
                opts[:payload] = v
              when 'structure'
                opts[:payload] = StringIO.new(builder.new(member_ref).serialize(v))
              end
            end
          end
          opts[:headers][':event-type'] = Aws::EventStream::HeaderValue.new(
            value: event_ref.location_name,
            type: 'string'
          )
          opts[:headers][':message-type'] = Aws::EventStream::HeaderValue.new(
            value: 'event',
            type: 'string'
          )
          opts
        end

        def encode_event(opts, rules, event_data, builder)
          event_type = event_data.delete(:event_type)

          if rules.shape.member?(event_type)
            encode_modeled_event(opts, rules, event_type, event_data, builder)
          else
            encode_unknown_event(opts, event_type, event_data)
          end
        end

      end
    end
  end
end
