class Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher

@private

def actual_default_value

def actual_default_value
  attribute_schema = if model.respond_to?(:_default_attributes)
                       model._default_attributes[attribute_name.to_s]
                     else
                       model.attributes_to_define_after_schema_loads[attribute_name.to_s]
                     end
  if Kernel.const_defined?('ActiveModel::Attribute::UserProvidedDefault') &&
     attribute_schema.is_a?(::ActiveModel::Attribute::UserProvidedDefault)
    attribute_schema = attribute_schema.marshal_dump
  end
  value = case attribute_schema
          in [_, { default: default_value } ]
            default_value
          in [_, default_value, *]
            default_value
          in [_, default_value]
            default_value
          end
  value.respond_to?(:call) ? value.call : value
end

def actual_enum_values

def actual_enum_values
  model.send(attribute_name.to_s.pluralize)
end

def backed_by_column_of_type(expected_column_type)

def backed_by_column_of_type(expected_column_type)
  options[:expected_column_type] = expected_column_type
  self
end

def column

def column
  key = attribute_name.to_s
  column_name = model.attribute_alias(key) || key
  model.columns_hash[column_name]
end

def column_type_matches?

def column_type_matches?
  if column.type == expected_column_type.to_sym
    true
  else
    @failure_message_continuation =
      "However, #{attribute_name.inspect} is "\
      "#{Shoulda::Matchers::Util.a_or_an(column.type)}"\
      ' column'
    false
  end
end

def default_value_matches?

def default_value_matches?
  return true if options[:default].blank?
  if actual_default_value.nil?
    @failure_message_continuation = 'However, no default value was set'
    return false
  end
  if actual_default_value == expected_default_value
    true
  else
    String.new.tap do |message|
      message << 'However, the default value is '
      message << Shoulda::Matchers::Util.inspect_value(actual_default_value)
      @failure_message_continuation = message
    end
    false
  end
end

def description

def description
  description = "#{simple_description} backed by "
  description << Shoulda::Matchers::Util.a_or_an(expected_column_type)
  if expected_enum_values.any?
    description << ' with values '
    description << Shoulda::Matchers::Util.inspect_value(
      expected_enum_values,
    )
  end
  if options[:prefix]
    description << ", prefix: #{options[:prefix].inspect}"
  end
  if options[:suffix]
    description << ", suffix: #{options[:suffix].inspect}"
  end
  description
end

def enum_defined?

def enum_defined?
  if model.defined_enums.include?(attribute_name.to_s)
    true
  else
    @failure_message_continuation =
      "no such enum exists on #{model}"
    false
  end
end

def enum_value_methods_exist?

def enum_value_methods_exist?
  if options[:instance_methods]
    return true if instance_methods_exist?
    message = missing_methods_message
    message << " (we can't tell which)" if [expected_prefix, expected_suffix].any?
    @failure_message_continuation = message
    false
  elsif instance_methods_exist?
    message = "#{attribute_name.inspect} does map to these values"
    message << ' with instance methods, but expected no instance methods'
    @failure_message_continuation = message
    false
  else
    true
  end
end

def enum_values_match?

def enum_values_match?
  passed =
    expected_enum_values.empty? ||
    normalized_actual_enum_values == normalized_expected_enum_values
  if passed
    true
  else
    @failure_message_continuation =
      "However, #{attribute_name.inspect} actually maps " +
      presented_enum_mapping(normalized_actual_enum_values)
    false
  end
end

def exclude_scopes?

def exclude_scopes?
  !options[:scopes]
end

def expectation # rubocop:disable Metrics/MethodLength

rubocop:disable Metrics/MethodLength
def expectation # rubocop:disable Metrics/MethodLength
  if enum_defined?
    expectation = "#{simple_description} backed by "
    expectation << Shoulda::Matchers::Util.a_or_an(expected_column_type)
    if expected_enum_values.any?
      expectation << ', mapping '
      expectation << presented_enum_mapping(
        normalized_expected_enum_values,
      )
    end
    if options[:default].present?
      expectation << ', with a default value of '
      expectation << Shoulda::Matchers::Util.inspect_value(expected_default_value)
    end
    if expected_validating?
      expectation << ', and being validated '
      expectation <<
        if expected_allowing_nil?
          'allowing nil values'
        else
          'not allowing nil values'
        end
    end
    if expected_prefix
      expectation <<
        if expected_suffix
          ', '
        else
          ' and '
        end
      expectation << 'prefixing accessor methods with '
      expectation << "#{expected_prefix}_".inspect
    end
    if expected_suffix
      expectation <<
        if expected_prefix
          ', and '
        else
          ' and '
        end
      expectation << 'suffixing accessor methods with '
      expectation << "_#{expected_suffix}".inspect
    end
    if exclude_scopes?
      expectation << ' with no scopes'
    end
    expectation
  else
    simple_description
  end
end

def expected_allowing_nil?

def expected_allowing_nil?
  options[:allowing_nil].present?
end

def expected_column_type

def expected_column_type
  options[:expected_column_type] || :integer
end

def expected_default_value

def expected_default_value
  options[:default].respond_to?(:call) ? options[:default].call : options[:default]
end

def expected_enum_value_names

def expected_enum_value_names
  to_array(expected_enum_values)
end

def expected_enum_values

def expected_enum_values
  options[:expected_enum_values]
end

def expected_instance_methods

def expected_instance_methods
  methods = expected_enum_value_names.map do |name|
    [expected_prefix, name, expected_suffix].
      select(&:present?).
      join('_')
  end
  methods.flat_map do |m|
    ["#{m}?".to_sym, "#{m}!".to_sym]
  end
end

def expected_instance_methods?

def expected_instance_methods?
  options[:instance_methods]
end

def expected_prefix

def expected_prefix
  if options.include?(:prefix)
    if options[:prefix] == true
      attribute_name
    else
      options[:prefix]
    end
  end
end

def expected_singleton_methods

def expected_singleton_methods
  expected_enum_value_names.map do |name|
    [expected_prefix, name, expected_suffix].
      select(&:present?).
      join('_').
      to_sym
  end
end

def expected_suffix

def expected_suffix
  if options.include?(:suffix)
    if options[:suffix] == true
      attribute_name
    else
      options[:suffix]
    end
  end
end

def expected_validating?

def expected_validating?
  options[:validating].present?
end

def failure_message

def failure_message
  message =
    if enum_defined?
      "Expected #{model} to #{expectation}. "
    else
      "Expected #{model} to #{expectation}, but "
    end
  message << "#{failure_message_continuation}."
  Shoulda::Matchers.word_wrap(message)
end

def failure_message_when_negated

def failure_message_when_negated
  message = "Expected #{model} not to #{expectation}, but it did."
  Shoulda::Matchers.word_wrap(message)
end

def find_enum_validator

def find_enum_validator
  record.class.validators.detect do |validator|
    validator.kind == :inclusion &&
      validator.attributes.include?(attribute_name.to_s) &&
      validator.options[:in] == expected_enum_value_names
  end
end

def initialize(attribute_name)

def initialize(attribute_name)
  @attribute_name = attribute_name
  @options = { expected_enum_values: [], scopes: true, instance_methods: true }
end

def instance_methods_exist?

def instance_methods_exist?
  expected_instance_methods.all? do |method|
    record.methods.include?(method)
  end
end

def matches?(subject)

def matches?(subject)
  @record = subject
  enum_defined? &&
    enum_values_match? &&
    column_type_matches? &&
    enum_value_methods_exist? &&
    scope_presence_matches? &&
    default_value_matches? &&
    validating_matches?
end

def missing_methods_message

def missing_methods_message
  message = "#{attribute_name.inspect} does map to these "
  message << 'values, but the enum is '
  if expected_prefix
    if expected_suffix
      message << 'configured with either a different prefix or '
      message << 'suffix, or no prefix or suffix at all'
    else
      message << 'configured with either a different prefix or no '
      message << 'prefix at all'
    end
  elsif expected_suffix
    message << 'configured with either a different suffix or no '
    message << 'suffix at all'
  elsif expected_instance_methods?
    message << 'configured with no instance methods'
  else
    ''
  end
end

def model

def model
  record.class
end

def normalized_actual_enum_values

def normalized_actual_enum_values
  to_hash(actual_enum_values)
end

def normalized_expected_enum_values

def normalized_expected_enum_values
  to_hash(expected_enum_values)
end

def presented_enum_mapping(enum_values)

def presented_enum_mapping(enum_values)
  enum_values.
    map { |output_to_input|
      output_to_input.
        map(&Shoulda::Matchers::Util.method(:inspect_value)).
        join(' to ')
    }.
    to_sentence
end

def scope_presence_matches?

def scope_presence_matches?
  if exclude_scopes?
    if singleton_methods_exist?
      message = "#{attribute_name.inspect} does map to these values "
      message << 'but class scope methods were present'
      @failure_message_continuation = message
      false
    else
      true
    end
  elsif singleton_methods_exist?
    true
  else
    if enum_defined?
      message = 'But the class scope methods are not present'
    else
      message = missing_methods_message
      message << 'or the class scope methods are not present'
      message << " (we can't tell which)"
    end
    @failure_message_continuation = message
    false
  end
end

def simple_description

def simple_description
  "define :#{attribute_name} as an enum"
end

def singleton_methods_exist?

def singleton_methods_exist?
  expected_singleton_methods.all? do |method|
    model.singleton_methods.include?(method)
  end
end

def to_array(value)

def to_array(value)
  if value.is_a?(Array)
    value.map(&:to_s)
  else
    value.keys.map(&:to_s)
  end
end

def to_hash(value)

def to_hash(value)
  if value.is_a?(Array)
    value.each_with_index.inject({}) do |hash, (item, index)|
      hash.merge!(item.to_s => index)
    end
  else
    value.stringify_keys
  end
end

def validating(value = true, allowing_nil: false)

def validating(value = true, allowing_nil: false)
  options[:validating] = value
  options[:allowing_nil] = allowing_nil
  self
end

def validating_matches?

def validating_matches?
  return true if options[:validating].nil?
  validator = find_enum_validator
  if expected_validating? == !!validator
    if validator&.options&.dig(:allow_nil).present? == expected_allowing_nil?
      true
    else
      @failure_message_continuation =
        "However, #{attribute_name.inspect} is allowing nil values"
      false
    end
  else
    @failure_message_continuation =
      if expected_validating?
        "However, #{attribute_name.inspect} is not being validated"
      else
        "However, #{attribute_name.inspect} is being validated"
      end
    false
  end
end

def with_default(default_value)

def with_default(default_value)
  options[:default] = default_value
  self
end

def with_prefix(expected_prefix = true)

def with_prefix(expected_prefix = true)
  options[:prefix] = expected_prefix
  self
end

def with_suffix(expected_suffix = true)

def with_suffix(expected_suffix = true)
  options[:suffix] = expected_suffix
  self
end

def with_values(expected_enum_values)

def with_values(expected_enum_values)
  options[:expected_enum_values] = expected_enum_values
  self
end

def without_instance_methods

def without_instance_methods
  options[:instance_methods] = false
  self
end

def without_scopes

def without_scopes
  options[:scopes] = false
  self
end