require "active_attr/attribute_definition"

module ActiveAttr
  module Matchers
    # Specify that a model should have an attribute matching the criteria. See
    # {HaveAttributeMatcher}
    #
    # @example Person should have a name attribute
    #   describe Person do
    #     it { should have_attribute(:first_name) }
    #   end
    #
    # @param [Symbol, String, #to_sym] attribute_name
    #
    # @return [ActiveAttr::HaveAttributeMatcher]
    #
    # @since 0.2.0
    def have_attribute(attribute_name)
      HaveAttributeMatcher.new(attribute_name)
    end

    # Verifies that an ActiveAttr-based model has an attribute matching the
    # given criteria. See {Matchers#have_attribute}
    #
    # @since 0.2.0
    class HaveAttributeMatcher
      attr_reader :attribute_name
      private :attribute_name

      # @param [Symbol, String, #to_sym] attribute_name
      # @private
      def initialize(attribute_name)
        raise TypeError, "can't convert #{attribute_name.class} into Symbol" unless attribute_name.respond_to? :to_sym
        @attribute_name = attribute_name.to_sym
        @attribute_options = {}
        @description = "attribute named #{attribute_name}"
        @expected_ancestors = ["ActiveAttr::Attributes"]
        @attribute_expectations = [lambda { actual_attribute_definition }]
      end

      # Specify that the attribute should have the given type
      #
      # @example Person's first name should be a String
      #   describe Person do
      #     it { should have_attribute(:first_name).of_type(String) }
      #   end
      #
      # @param [Class] type The expected type
      #
      # @return [HaveAttributeMatcher] The matcher
      #
      # @since 0.5.0
      def of_type(type)
        @attribute_options[:type] = type
        @description << " of type #{type}"
        @expected_ancestors << "ActiveAttr::TypecastedAttributes"
        @attribute_expectations << lambda { @model_class._attribute_type(attribute_name) == type }
        self
      end

      # Specify that the attribute should have the given default value
      #
      # @example Person's first name should default to John
      #   describe Person do
      #     it do
      #       should have_attribute(:first_name).with_default_value_of("John")
      #     end
      #   end
      #
      # @param [Object] default_value The expected default value
      #
      # @return [HaveAttributeMatcher] The matcher
      #
      # @since 0.5.0
      def with_default_value_of(default_value)
        @attribute_options[:default] = default_value
        @description << " with a default value of #{default_value.inspect}"
        @expected_ancestors << "ActiveAttr::AttributeDefaults"
        @attribute_expectations << lambda { @model_class.allocate.send(:_attribute_default, @attribute_name) == default_value }
        self
      end

      # @return [String] Description
      # @private
      def description
        "have #{@description}"
      end

      # @private
      def matches?(model_or_model_class)
        @model_class = Class === model_or_model_class ? model_or_model_class : model_or_model_class.class
        missing_ancestors.none? && @attribute_expectations.all? { | expectation| expectation.call }
      end

      # @return [String] Failure message
      # @private
      def failure_message
        if missing_ancestors.any?
          "expected #{@model_class.name} to include #{missing_ancestors.first}"
        elsif actual_attribute_definition
          "expected: #{expected_attribute_definition.inspect}\n" +
          "     got: #{actual_attribute_definition.inspect}"
        else
          "expected #{@model_class.name} to have #{@description}"
        end
      end

      # @return [String] Negative failure message
      # @private
      def failure_message_when_negated
        "expected not: #{expected_attribute_definition.inspect}\n" +
        "         got: #{actual_attribute_definition.inspect}"
      end
      alias_method :negative_failure_message, :failure_message_when_negated

      private

      def actual_attribute_definition
        @model_class.attributes[attribute_name]
      end

      def expected_attribute_definition
        AttributeDefinition.new @attribute_name, @attribute_options
      end

      def missing_ancestors
        model_ancestor_names = @model_class.ancestors.map(&:name)

        @expected_ancestors.reject do |ancestor_name|
          model_ancestor_names.include? ancestor_name
        end
      end
    end
  end
end
