# frozen_string_literal: true

require "rspec/expectations"
require "coach/middleware"

# Middleware stubbing ######################################

# rubocop:disable Metrics/AbcSize
def build_middleware(name)
  Class.new(Coach::Middleware) do
    # To access `name`, we need to use `define_method` instead of `def`
    define_method(:to_s) { "<Middleware#{name}>" }
    define_method(:name) { name }
    define_singleton_method(:name) { name }

    def call
      config[:callback].call if config.include?(:callback)
      log_metadata(**{ name.to_sym => true })

      # Emulate newer hash syntax for older Ruby versions
      hash_response = config.except(:callback).map { |k, v| "#{k}: #{v}" }.join(", ")
      response = [name + "{#{hash_response}}"]

      # Build up a list of middleware called, in the order they were called
      if next_middleware
        response.concat(next_middleware.call)
      else
        response
      end
    end
  end
end
# rubocop:enable Metrics/AbcSize

def null_middleware
  double(call: nil)
end

# Response matchers ########################################

RSpec::Matchers.define :respond_with_status do |expected_status|
  match do |middleware|
    @middleware = middleware
    @response = middleware.call
    @response[0] == expected_status
  end

  failure_message do |_actual|
    "expected #{@middleware.class.name} to respond with #{expected_status} but got " \
      "#{@response[0]}"
  end
end

RSpec::Matchers.define :respond_with_body_that_matches do |body_regex|
  match do |middleware|
    @response_body = middleware.call[2].join
    @response_body.match(body_regex)
  end

  failure_message do |_actual|
    "expected that \"#{@response_body}\" would match #{body_regex}"
  end
end

RSpec::Matchers.define :respond_with_envelope do |envelope, keys = []|
  match do |middleware|
    @response = JSON.parse(middleware.call[2].join)
    expect(@response).to include(envelope.to_s)

    @envelope = @response[envelope.to_s].with_indifferent_access
    expect(@envelope).to match(hash_including(*keys))
  end

  failure_message do |_actual|
    "expected that \"#{@response}\" would have envelope \"#{envelope}\" that matches " \
      "hash_including(#{keys})"
  end
end

RSpec::Matchers.define :respond_with_header do |header, value_regex|
  match do |middleware|
    response_headers = middleware.call.second
    @header_value = response_headers[header]
    @header_value.match(value_regex)
  end

  failure_message do |_actual|
    "expected #{header} header in response to match #{value_regex} but found " \
      "\"#{@header_value}\""
  end
end

# Chain matchers ###########################################

RSpec::Matchers.define :call_next_middleware do
  match do |middleware|
    @middleware = middleware
    allow(middleware.next_middleware).to receive(:call)
    middleware.call
    begin
      expect(middleware.next_middleware).to have_received(:call)
      true
    rescue RSpec::Expectations::ExpectationNotMetError
      false
    end
  end

  failure_message do
    "expected that \"#{@middleware.class.name}\" would call next middleware"
  end
end

# Provide/Require matchers #################################

RSpec::Matchers.define :provide do |key|
  match do |middleware|
    allow(middleware).to receive(:provide)
    expect(middleware).to receive(:provide).with(hash_including(key)).and_call_original
    middleware.call
    true
  end
end
