require 'active_support/concern'

module LegacyApiDsl
  extend ActiveSupport::Concern

  def validate_response(model, json, expected_values: {}, ignored_attributes: [], expected_attributes: nil)
    ignored_attributes.push :guid, :options
    (expected_attributes || expected_attributes_for_model(model)).each do |expected_attribute|
      # refactor: pass exclusions, and figure out which are valid to not be there
      next if ignored_attributes.include? expected_attribute

      # if a relationship is not present, its url should not be present
      next if field_is_url_and_relationship_not_present?(json, expected_attribute)

      expect(json).to have_key expected_attribute.to_s
      expect(json[expected_attribute.to_s]).to eq(expected_values[expected_attribute.to_sym]) if expected_values.key? expected_attribute.to_sym
    end
  end

  def standard_list_response(response_json, model, expected_attributes: nil)
    standard_paginated_response_format? response_json
    resource = response_json['resources'].first
    standard_entity_response resource, model, expected_attributes:
  end

  def standard_entity_response(json, model, expected_values: {}, expected_attributes: nil)
    expect(json).to include('metadata')
    expect(json).to include('entity')
    standard_metadata_response_format? json['metadata'], model
    validate_response(model, json['entity'], expected_values:, expected_attributes:)
  end

  def standard_paginated_response_format?(json)
    validate_response VCAP::RestAPI::PaginatedResponse, json
  end

  def standard_metadata_response_format?(json, model)
    ignored_attributes = []
    ignored_attributes = [:updated_at] unless model_has_updated_at?(model)
    validate_response(VCAP::RestAPI::MetadataMessage, json, ignored_attributes:)
  end

  def expected_attributes_for_model(model)
    return model.fields.keys if model.respond_to? :fields

    model_class_from_name(model).export_attrs
  end

  def parsed_response
    parse(response_body)
  end

  def field_is_url_and_relationship_not_present?(json, field)
    return unless field =~ /(.*)_url$/

    !json[:"#{Regexp.last_match[1]}_guid"]
  end

  def audited_event(event)
    attributes = event.columns.map do |column|
      if column == :metadata
        { attribute_name: column.to_s, value: Oj.dump(Oj.load(event[column])), is_json: true }
      else
        { attribute_name: column.to_s, value: event[column], is_json: false }
      end
    end

    RSpec.current_example.metadata[:audit_records] ||= []
    RSpec.current_example.metadata[:audit_records] << { type: event[:type], attributes: attributes }
  end

  def field_data(name)
    self.class.metadata[:body_parameters].detect do |field|
      name == field[:name]
    end
  end

  def fields_json(overrides={})
    Oj.dump(required_fields.merge(overrides))
  end

  def required_fields
    self.class.metadata[:body_parameters].each_with_object({}) do |field, memo|
      memo[field[:name].to_sym] = (field[:valid_values] || field[:example_values]).first if field[:required]
    end
  end

  private

  def model_class_from_name(model)
    "VCAP::CloudController::#{model.to_s.classify}".constantize
  end

  def model_has_updated_at?(model)
    model_class_from_name(model).columns.include?(:updated_at)
  end

  def add_deprecation_warning
    example.metadata[:description] << ' (deprecated)' if response_headers['X-Cf-Warnings'] && response_headers['X-Cf-Warnings'][/deprecated/i]
  end

  module ClassMethods
    def api_version
      '/v2'
    end

    def root(model)
      "#{api_version}/#{model.to_s.pluralize}"
    end

    def standard_model_list(model, controller, options={}, &)
      outer_model_description = ''
      model_name = options[:path] || model
      title = options[:title] || model_name.to_s.pluralize.titleize

      outer_model = options[:outer_model]
      if outer_model
        model_name = options[:path] if options[:path]
        path = "#{outer_model.to_s.pluralize}/:guid/#{model_name}"
        outer_model_description = " for the #{outer_model.to_s.singularize.titleize}"
      else
        path = options[:path] || model
      end

      get root(path) do
        include_context 'response_fields' if options[:response_fields]

        standard_list_parameters(controller, outer_model: outer_model, exclude_parameters: options.fetch(:exclude_parameters, []), &)

        example_request "List all #{title}#{outer_model_description}" do
          expect(status).to eq 200
          standard_list_response(parsed_response, model, expected_attributes: options[:export_attributes])
        end
      end
    end

    def nested_model_associate(model, outer_model, opts={})
      outer_model_path = opts[:path] || outer_model
      experimental = if opts[:experimental]
                       ' (experimental)'
                     else
                       ''
                     end
      path = "#{api_version}/#{outer_model_path.to_s.pluralize}/:guid/#{model.to_s.pluralize}/:#{model}_guid"

      put path do
        example_request "Associate #{model.to_s.titleize} with the #{outer_model.to_s.titleize}#{experimental}" do
          expect(status).to eq 201
          standard_entity_response parsed_response, outer_model
        end
      end
    end

    def nested_model_remove(model, outer_model)
      path_name = "#{api_version}/#{outer_model.to_s.pluralize}/:guid/#{model.to_s.pluralize}/:#{model}_guid"

      delete path_name do
        example "Remove #{model.to_s.titleize} from the #{outer_model.to_s.titleize}" do
          path = "#{self.class.api_version}/#{outer_model.to_s.pluralize}/#{send(:guid)}/#{model.to_s.pluralize}/#{send("associated_#{model}_guid")}"
          client.delete path, '', headers
          expect(status).to eq 204
        end
      end
    end

    def standard_model_get(model, options={})
      path = options[:path] || model
      title = options[:title] || path.to_s.singularize.titleize
      get "#{root(path)}/:guid" do
        include_context 'response_fields' if options[:response_fields]

        example_request "Retrieve a Particular #{title}" do
          standard_entity_response(parsed_response, model, expected_attributes: options[:export_attributes])
          if options[:nested_associations]
            options[:nested_associations].each do |association_name|
              expect(parsed_response['entity'].keys).to include("#{association_name}_url")
            end
          end
        end
      end
    end

    def standard_model_delete(model, options={}, &)
      title = options[:title] || model.to_s.titleize
      query_string = "?#{options[:query_string]}"
      delete "#{root(model)}/:guid#{query_string}" do
        parameter :guid, "The guid of the #{title}"

        instance_eval(&) if block_given?

        request_parameter :async, "Will run the delete request in a background job. Recommended: 'true'.", valid_values: [true, false] unless options[:async] == false

        example_request "Delete a Particular #{title}" do
          expect(status).to eq 204
          after_standard_model_delete(guid) if respond_to?(:after_standard_model_delete)
        end
      end
    end

    def standard_model_delete_without_async(model)
      delete "#{root(model)}/:guid" do
        example_request "Delete a Particular #{model.to_s.titleize}" do
          expect(status).to eq 204
          after_standard_model_delete(guid) if respond_to?(:after_standard_model_delete)
        end
      end
    end

    def standard_list_parameters(controller, outer_model: nil, exclude_parameters: [], &)
      query_parameters = controller.query_parameters - exclude_parameters
      unless query_parameters.empty?
        query_parameter_description = 'Parameters used to filter the result set.<br/>'
        query_parameter_description += 'Format queries as &lt;filter&gt;&lt;op&gt;&lt;value&gt;<br/>'
        query_parameter_description += ' Valid ops: : &gt;= &lt;= &lt; &gt; IN<br/>'
        query_parameter_description += " Valid filters: #{query_parameters.to_a.join(', ')}"
        if outer_model && query_parameters.include?(outer_model.to_s + '_guid')
          query_parameter_description += "<br/> (Note that filtering on #{outer_model}_guid will return an empty list " \
                                         'if you specify anything other than the guid included in the path.)'
        end

        examples = ['q=filter:value', 'q=filter>value', 'q=filter IN a,b,c']
        request_parameter :q, query_parameter_description, { html: true, example_values: examples }
      end
      pagination_parameters
      instance_eval(&) if block_given?
      request_parameter :'inline-relations-depth', "0 - don't inline any relations and return URLs.  Otherwise, inline to depth N.", deprecated: true
      request_parameter :'orphan-relations', '0 - de-duplicate object entries in response', deprecated: true
      request_parameter :'exclude-relations', 'comma-delimited list of relations to drop from response', deprecated: true
      request_parameter :'include-relations', 'comma-delimited list of the only relations to include in response', deprecated: true
    end

    def pagination_parameters
      request_parameter :page, 'Page of results to fetch'
      request_parameter :'results-per-page', 'Number of results per page'
      request_parameter :'order-direction', 'Order of the results: asc (default) or desc'
    end

    def request_parameter(name, description, options={})
      options[:description_html] = description if options[:html]

      parameter name, description, options
      metadata[:request_parameters] ||= []
      metadata[:request_parameters].push(options.merge(name: name.to_s, description: description))
    end

    def modify_fields_for_update
      metadata[:body_parameters] = metadata[:body_parameters].collect do |field|
        field.delete(:required)
        field.delete(:default)
        field
      end
    end

    def authenticated_request
      header 'AUTHORIZATION', :admin_auth_header
    end

    def field(*)
      body_parameter(*)
    end
  end
end
