# frozen_string_literal: true

require "action_policy/testing"

module ActionPolicy
  module RSpec
    # Authorization matcher `be_authorized_to`.
    #
    # Verifies that a block of code has been authorized using specific policy.
    #
    # Example:
    #
    #   # in controller/request specs
    #   subject { patch :update, id: product.id }
    #
    #   it "is authorized" do
    #     expect { subject }
    #       .to be_authorized_to(:manage?, product)
    #       .with(ProductPolicy)
    #   end
    #
    class BeAuthorizedTo < ::RSpec::Matchers::BuiltIn::BaseMatcher
      attr_reader :rule, :target, :policy, :actual_calls, :context

      def initialize(rule, target)
        @rule = rule
        @target = target
      end

      def with(policy)
        @policy = policy
        self
      end

      def with_context(context)
        @context = context
        self
      end

      def match(_expected, actual)
        raise "This matcher only supports block expectations" unless actual.is_a?(Proc)

        @policy ||= ::ActionPolicy.lookup(target)
        @context ||= nil

        begin
          ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }
        rescue ActionPolicy::Unauthorized
          # we don't want to care about authorization result
        end

        @actual_calls = ActionPolicy::Testing::AuthorizeTracker.calls

        actual_calls.any? { it.matches?(policy, rule, target, context) }
      end

      def does_not_match?(*)
        raise "This matcher doesn't support negation"
      end

      def supports_block_expectations?() = true

      def failure_message
        "expected #{formatted_record} " \
        "to be authorized with #{policy}##{rule}, " \
        "#{context ? "and context #{context.inspect}, " : ""}" \
        "but #{actual_calls_message}"
      end

      def actual_calls_message
        if actual_calls.empty?
          "no authorization calls have been made"
        else
          "the following calls were encountered:\n" \
          "#{formatted_calls}"
        end
      end

      def formatted_calls
        actual_calls.map do
          " - #{it.inspect}"
        end.join("\n")
      end

      def formatted_record(record = target) = ::RSpec::Support::ObjectFormatter.format(record)
    end
  end
end

RSpec.configure do |config|
  config.include(Module.new do
    def be_authorized_to(rule, target)
      ActionPolicy::RSpec::BeAuthorizedTo.new(rule, target)
    end
  end)
end
