# frozen_string_literal: true

# @TODO: replace hashie!
require 'hashie'

module RopenPi
  module Specs
    # rubocop:disable Metrics/ModuleLength
    module ExampleGroupHelpers
      def path(template, metadata = {}, &block)
        metadata[:path_item] = { template: template }
        describe(template, metadata, &block)
      end

      %i[get post patch put delete head].each do |verb|
        define_method(verb) do |summary, &block|
          api_metadata = { operation: { verb: verb, summary: summary } }
          describe(verb, api_metadata, &block)
        end
      end

      %i[operationId deprecated security].each do |attr_name|
        define_method(attr_name) do |value|
          metadata[:operation][attr_name] = value
        end
      end

      # NOTE: 'description' requires special treatment because ExampleGroup already
      # defines a method with that name. Provide an override that supports the existing
      # functionality while also setting the appropriate metadata if applicable
      def description(value = nil)
        return super() if value.nil?

        metadata[:operation][:description] = value
      end

      # These are array properties - note the splat operator
      %i[tags consumes produces schemes].each do |attr_name|
        define_method(attr_name) do |*value|
          metadata[:operation][attr_name] = value
        end
      end

      def request_body(attributes)
        # can make this generic, and accept any incoming hash (like parameter method)
        attributes.compact!

        if metadata[:operation][:requestBody].blank?
          metadata[:operation][:requestBody] = attributes
        elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content]
          # merge in
          content_hash = metadata[:operation][:requestBody][:content]
          incoming_content_hash = attributes[:content]
          content_hash.merge!(incoming_content_hash) if incoming_content_hash
        end
      end

      def request_body_json(schema:, required: true, description: nil, examples: nil)
        passed_examples = Array(examples)
        content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} }
        request_body(description: description, required: required, content: content_hash)

        handle_examples(schema: schema, examples: passed_examples, required: required) if passed_examples.any?
      end

      # rubocop:disable Metrics/MethodLength
      def handle_examples(schema:, examples:, required:)
        # the request_factory is going to have to resolve the different ways that the example can be given
        # it can contain a 'value' key which is a direct hash (easiest)
        # it can contain a 'external_value' key which makes an external call to load the json
        # it can contain a '$ref' key. Which points to #/components/examples/blog
        # rubocop:disable Metrics/BlockLength
        examples.each do |passed_example|
          param_attributes =  if passed_example.is_a?(Symbol)
                                example_key_name = passed_example
                                # TODO: write more tests around this adding to the parameter
                                # if symbol try and use save_request_example
                                {
                                  name: example_key_name,
                                  in: :body,
                                  required: required,
                                  param_value: example_key_name,
                                  schema: schema
                                }
                              elsif passed_example.is_a?(Hash) && passed_example[:externalValue]
                                {
                                  name: passed_example,
                                  in: :body,
                                  required: required,
                                  param_value: passed_example[:externalValue],
                                  schema: schema
                                }
                              elsif passed_example.is_a?(Hash) && passed_example['$ref']
                                {
                                  name: passed_example,
                                  in: :body,
                                  required: required,
                                  param_value: passed_example['$ref'],
                                  schema: schema
                                }
                              end
          parameter(param_attributes)
        end
        # rubocop:enable Metrics/BlockLength
      end
      # rubocop:enable Metrics/MethodLength

      def request_body_text_plain(required: false, description: nil, examples: nil)
        content_hash = { 'test/plain' => { schema: { type: :string }, examples: examples }.compact! || {} }
        request_body(description: description, required: required, content: content_hash)
      end

      # TODO: add examples to this like we can for json, might be large lift
      # as many assumptions are made on content-type
      def request_body_xml(schema:, required: false, description: nil, examples: nil)
        # passed_examples = Array(examples)
        content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} }
        request_body(description: description, required: required, content: content_hash)
      end

      # rubocop:disable Metrics/MethodLength
      def request_body_multipart(schema:, description: nil)
        content_hash = { 'multipart/form-data' => { schema: schema } }
        request_body(description: description, content: content_hash)

        schema.extend(Hashie::Extensions::DeepLocate)
        file_properties = schema.deep_locate ->(_k, v, _obj) { v == :binary }
        hash_locator = []

        file_properties.each do |match|
          hash_match = schema.deep_locate ->(_k, v, _obj) { v == match }
          hash_locator.concat(hash_match) unless hash_match.empty?
        end

        property_hashes = hash_locator.flat_map do |locator|
          locator.select { |_k, v| file_properties.include?(v) }
        end

        existing_keys = []
        property_hashes.each do |property_hash|
          property_hash.keys.each do |k|
            next if existing_keys.include?(k)

            file_name = k
            existing_keys << k
            parameter name: file_name, in: :formData, type: :file, required: true
          end
        end
      end
      # rubocop:enable Metrics/MethodLength

      def parameter(attributes)
        attributes[:required] = true if attributes[:in] && attributes[:in].to_sym == :path
        attributes[:schema] = { type: attributes[:type] } if attributes[:type] && attributes[:schema].nil?

        if metadata.key?(:operation)
          metadata[:operation][:parameters] ||= []
          metadata[:operation][:parameters] << attributes
        else
          metadata[:path_item][:parameters] ||= []
          metadata[:path_item][:parameters] << attributes
        end
      end

      def response(code, description, metadata = {}, &block)
        metadata[:response] = { code: code, description: description }
        context(description, metadata, &block)
      end

      def schema(value, content_type: 'application/json')
        content_hash = { content_type => { schema: value } }
        metadata[:response][:content] = content_hash
      end

      def header(name, attributes)
        metadata[:response][:headers] ||= {}

        if attributes[:type] && attributes[:schema].nil?
          attributes[:schema] = { type: attributes[:type] }
          attributes.delete(:type)
        end

        metadata[:response][:headers][name] = attributes
      end

      # NOTE: Similar to 'description', 'examples' need to handle the case when
      # being invoked with no params to avoid overriding 'examples' method of
      # rspec-core ExampleGroup
      def examples(example = nil)
        return super() if example.nil?

        metadata[:response][:examples] = example
      end

      # checks the examples in the parameters should be able to add $ref and externalValue examples.
      # This syntax would look something like this in the integration _spec.rb file
      #
      # request_body_json schema: { '$ref' => '#/components/schemas/blog' },
      #   examples: [:blog, {name: :external_blog,
      #                       externalValue: 'http://api.sample.org/myjson_example'},
      #                     {name: :another_example,
      #                       '$ref' => '#/components/examples/flexible_blog_example'}]
      # The first value :blog, points to a let param of the same name, and is used to make the request in the
      # integration test (it is used to build the request payload)
      #
      # The second item in the array shows how to add an externalValue for the examples in the requestBody section
      # The third item shows how to add a $ref item that points to the components/examples section of the swagger spec.
      #
      # NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui
      # will not show it yet
      def merge_other_examples!(example_metadata)
        # example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
        content_node = example_metadata[:operation][:requestBody][:content]['application/json']
        return unless content_node

        external_example = example_metadata[:operation]&.dig(:parameters)&.detect do |p|
          p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue]
        end || {}

        ref_example = example_metadata[:operation]&.dig(:parameters)&.detect do |p|
          p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref']
        end || {}

        examples_node = content_node[:examples] ||= {}

        nodes_to_add = []
        nodes_to_add << external_example unless external_example.empty?
        nodes_to_add << ref_example unless ref_example.empty?

        nodes_to_add.each do |node|
          json_request_examples = examples_node ||= {}
          other_name = node[:name][:name]
          other_key = node[:name][:externalValue] ? :externalValue : '$ref'

          json_request_examples.merge!(other_name => { other_key => node[:param_value] }) if other_name
        end
      end

      def run_test!(&block)
        app_json = 'application/json'

        before do |example|
          submit_request(example.metadata)
        end

        it "returns a #{metadata[:response][:code]} response" do |example|
          assert_response_matches_metadata(example.metadata, &block)
          example.instance_exec(response, &block) if block_given?
        end

        after do |example|
          body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect do |p|
            p[:in] == :body && p[:required]
          end

          if body_parameter && respond_to?(body_parameter[:name]) && \
             example.metadata[:operation][:requestBody][:content][app_json]

            # save response examples by default
            if example.metadata[:response][:examples].nil? || example.metadata[:response][:examples].empty?
              unless response.body.to_s.empty?
                example.metadata[:response][:examples] = {
                  app_json => JSON.parse(response.body, symbolize_names: true)
                }
              end
            end

            # save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test
            if response.code.to_s =~ /^2\d{2}$/

              example.metadata[:operation][:requestBody][:content][app_json] = { examples: {} } \
                unless example.metadata[:operation][:requestBody][:content][app_json][:examples]

              json_request_examples = example.metadata[:operation][:requestBody][:content][app_json][:examples]
              json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) }

              example.metadata[:operation][:requestBody][:content][app_json][:examples] = json_request_examples
            end
          end

          self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody]
        end
      end
    end
    # rubocop:enable Metrics/ModuleLength
  end
end
