module Shoulda
  module Matchers
    module ActiveRecord
      # The `validate_uniqueness_of` matcher tests usage of the
      # `validates_uniqueness_of` validation. It first checks for an existing
      # instance of your model in the database, creating one if necessary. It
      # then takes a new instance of that model and asserts that it fails
      # validation if the attribute or attributes you've specified in the
      # validation are set to values which are the same as those of the
      # pre-existing record (thereby failing the uniqueness check).
      #
      #     class Post < ActiveRecord::Base
      #       validates :permalink, uniqueness: true
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it { should validate_uniqueness_of(:permalink) }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:permalink)
      #     end
      #
      # #### Caveat
      #
      # This matcher works a bit differently than other matchers. As noted
      # before, it will create an instance of your model if one doesn't already
      # exist. Sometimes this step fails, especially if you have database-level
      # restrictions on any attributes other than the one which is unique. In
      # this case, the solution is to populate these attributes with values
      # before you call `validate_uniqueness_of`.
      #
      # For example, say you have the following migration and model:
      #
      #     class CreatePosts < ActiveRecord::Migration
      #       def change
      #         create_table :posts do |t|
      #           t.string :title
      #           t.text :content, null: false
      #         end
      #       end
      #     end
      #
      #     class Post < ActiveRecord::Base
      #       validates :title, uniqueness: true
      #     end
      #
      # You may be tempted to test the model like this:
      #
      #     RSpec.describe Post, type: :model do
      #       it { should validate_uniqueness_of(:title) }
      #     end
      #
      # However, running this test will fail with an exception such as:
      #
      #     Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher::ExistingRecordInvalid:
      #       validate_uniqueness_of works by matching a new record against an
      #       existing record. If there is no existing record, it will create one
      #       using the record you provide.
      #
      #       While doing this, the following error was raised:
      #
      #         PG::NotNullViolation: ERROR:  null value in column "content" violates not-null constraint
      #         DETAIL:  Failing row contains (1, null, null).
      #         : INSERT INTO "posts" DEFAULT VALUES RETURNING "id"
      #
      #       The best way to fix this is to provide the matcher with a record where
      #       any required attributes are filled in with valid values beforehand.
      #
      # (The exact error message will differ depending on which database you're
      # using, but you get the idea.)
      #
      # This happens because `validate_uniqueness_of` tries to create a new post
      # but cannot do so because of the `content` attribute: though unrelated to
      # this test, it nevertheless needs to be filled in. As indicated at the
      # end of the error message, the solution is to build a custom Post object
      # ahead of time with `content` filled in:
      #
      #     RSpec.describe Post, type: :model do
      #       describe "validations" do
      #         subject { Post.new(content: "Here is the content") }
      #         it { should validate_uniqueness_of(:title) }
      #       end
      #     end
      #
      # Or, if you're using
      # [FactoryBot](https://github.com/thoughtbot/factory_bot) and you have a
      # `post` factory defined which automatically fills in `content`, you can
      # say:
      #
      #     RSpec.describe Post, type: :model do
      #       describe "validations" do
      #         subject { FactoryBot.build(:post) }
      #         it { should validate_uniqueness_of(:title) }
      #       end
      #     end
      #
      # #### Qualifiers
      #
      # Use `on` if your validation applies only under a certain context.
      #
      #     class Post < ActiveRecord::Base
      #       validates :title, uniqueness: true, on: :create
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it { should validate_uniqueness_of(:title).on(:create) }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:title).on(:create)
      #     end
      #
      # ##### with_message
      #
      # Use `with_message` if you are using a custom validation message.
      #
      #     class Post < ActiveRecord::Base
      #       validates :title, uniqueness: true, message: 'Please choose another title'
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it do
      #         should validate_uniqueness_of(:title).
      #           with_message('Please choose another title')
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:title).
      #         with_message('Please choose another title')
      #     end
      #
      # ##### scoped_to
      #
      # Use `scoped_to` to test usage of the `:scope` option. This asserts that
      # a new record fails validation if not only the primary attribute is not
      # unique, but the scoped attributes are not unique either.
      #
      #     class Post < ActiveRecord::Base
      #       validates :slug, uniqueness: { scope: :journal_id }
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it { should validate_uniqueness_of(:slug).scoped_to(:journal_id) }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:slug).scoped_to(:journal_id)
      #     end
      #
      # NOTE: Support for testing uniqueness validation scoped to an array of
      # associations is not available.
      #
      # For more information, please refer to
      # https://github.com/thoughtbot/shoulda-matchers/issues/814
      #
      # ##### case_insensitive
      #
      # Use `case_insensitive` to test usage of the `:case_sensitive` option
      # with a false value. This asserts that the uniquable attributes fail
      # validation even if their values are a different case than corresponding
      # attributes in the pre-existing record.
      #
      #     class Post < ActiveRecord::Base
      #       validates :key, uniqueness: { case_sensitive: false }
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it { should validate_uniqueness_of(:key).case_insensitive }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:key).case_insensitive
      #     end
      #
      # ##### ignoring_case_sensitivity
      #
      # By default, `validate_uniqueness_of` will check that the
      # validation is case sensitive: it asserts that uniquable attributes pass
      # validation when their values are in a different case than corresponding
      # attributes in the pre-existing record.
      #
      # Use `ignoring_case_sensitivity` to skip this check. This qualifier is
      # particularly handy if your model has somehow changed the behavior of
      # attribute you're testing so that it modifies the case of incoming values
      # as they are set. For instance, perhaps you've overridden the writer
      # method or added a `before_validation` callback to normalize the
      # attribute.
      #
      #     class User < ActiveRecord::Base
      #       validates :email, uniqueness: true
      #
      #       def email=(value)
      #         super(value.downcase)
      #       end
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it do
      #         should validate_uniqueness_of(:email).ignoring_case_sensitivity
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:email).ignoring_case_sensitivity
      #     end
      #
      # ##### allow_nil
      #
      # Use `allow_nil` to assert that the attribute allows nil.
      #
      #     class Post < ActiveRecord::Base
      #       validates :author_id, uniqueness: true, allow_nil: true
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it { should validate_uniqueness_of(:author_id).allow_nil }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:author_id).allow_nil
      #     end
      #
      # @return [ValidateUniquenessOfMatcher]
      #
      # ##### allow_blank
      #
      # Use `allow_blank` to assert that the attribute allows a blank value.
      #
      #     class Post < ActiveRecord::Base
      #       validates :author_id, uniqueness: true, allow_blank: true
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it { should validate_uniqueness_of(:author_id).allow_blank }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:author_id).allow_blank
      #     end
      #
      # @return [ValidateUniquenessOfMatcher]
      #
      # ##### alternatives
      #
      # Use `alternatives` to specify alternative valid values to use
      #   for testing uniqueness.
      #
      #     class Post < ActiveRecord::Base
      #       validates :title, uniqueness: true
      #     end
      #
      #     # RSpec
      #     RSpec.describe Post, type: :model do
      #       it do
      #         should validate_uniqueness_of(:title).alternatives('Alternative Title')
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostTest < ActiveSupport::TestCase
      #       should validate_uniqueness_of(:title).alternatives(['Alternative Title', 'Another Title'])
      #     end
      #
      # @param values [String, Array<String>]
      #   Alternative value(s) to use for testing uniqueness instead of using
      #   the `succ` operator on the existing value.
      #
      # @return [ValidateUniquenessOfMatcher]
      #
      # @example
      #   it { should validate_uniqueness_of(:title).alternatives('Alternative Title') }
      #   it { should validate_uniqueness_of(:title).alternatives(['Title 1', 'Title 2']) }
      def validate_uniqueness_of(attr)
        ValidateUniquenessOfMatcher.new(attr)
      end

      # @private
      class ValidateUniquenessOfMatcher < ActiveModel::ValidationMatcher
        include ActiveModel::Helpers

        def initialize(attribute)
          super(attribute)
          @expected_message = :taken
          @options = {
            case_sensitivity_strategy: :sensitive,
          }
          @existing_record_created = false
          @failure_reason = nil
          @attribute_setters = {
            existing_record: AttributeSetters.new,
            new_record: AttributeSetters.new,
          }
        end

        def scoped_to(*scopes)
          @options[:scopes] = [*scopes].flatten.map(&:to_sym)
          self
        end

        # @param values [String, Array<String>]
        #   Alternative value(s) to use for testing uniqueness instead of using
        #   the `succ` operator on the existing value.
        #
        # @return [ValidateUniquenessOfMatcher]
        #
        # @example
        #   it { should validate_uniqueness_of(:title).alternatives('Alternative Title') }
        #   it { should validate_uniqueness_of(:title).alternatives(['Title 1', 'Title 2']) }
        def alternatives(values)
          @options[:alternatives] = values
          self
        end

        def case_insensitive
          @options[:case_sensitivity_strategy] = :insensitive
          self
        end

        def ignoring_case_sensitivity
          @options[:case_sensitivity_strategy] = :ignore
          self
        end

        def allow_nil
          @options[:allow_nil] = true
          self
        end

        def expects_to_allow_nil?
          @options[:allow_nil] == true
        end

        def allow_blank
          @options[:allow_blank] = true
          self
        end

        def expects_to_allow_blank?
          @options[:allow_blank] == true
        end

        def simple_description
          description = "validate that :#{@attribute} is"
          description << description_for_case_sensitive_qualifier
          description << ' unique'

          if @options[:scopes].present?
            description << " within the scope of #{inspected_expected_scopes}"
          end

          description
        end

        def matches?(given_record)
          @given_record = given_record
          @all_records = model.all

          matches_presence_of_attribute? &&
            matches_presence_of_scopes? &&
            matches_scopes_configuration? &&
            matches_uniqueness_without_scopes? &&
            matches_uniqueness_with_case_sensitivity_strategy? &&
            matches_uniqueness_with_scopes? &&
            matches_allow_nil? &&
            matches_allow_blank?
        ensure
          Uniqueness::TestModels.remove_all
        end

        def does_not_match?(given_record)
          @given_record = given_record
          @all_records = model.all

          does_not_match_presence_of_scopes? ||
            does_not_match_scopes_configuration? ||
            does_not_match_uniqueness_without_scopes? ||
            does_not_match_uniqueness_with_case_sensitivity_strategy? ||
            does_not_match_uniqueness_with_scopes? ||
            does_not_match_allow_nil? ||
            does_not_match_allow_blank?
        ensure
          Uniqueness::TestModels.remove_all
        end

        protected

        def failure_reason
          @failure_reason || super
        end

        def build_allow_or_disallow_value_matcher(args)
          super.tap do |matcher|
            matcher.failure_message_preface = method(:failure_message_preface)
            matcher.attribute_changed_value_message =
              method(:attribute_changed_value_message)
          end
        end

        private

        def case_sensitivity_strategy
          @options[:case_sensitivity_strategy]
        end

        def new_record
          unless defined?(@new_record)
            build_new_record
          end

          @new_record
        end
        alias_method :subject, :new_record

        def description_for_case_sensitive_qualifier
          case case_sensitivity_strategy
          when :sensitive
            ' case-sensitively'
          when :insensitive
            ' case-insensitively'
          else
            ''
          end
        end

        def validations
          model.validators_on(@attribute).select do |validator|
            validator.is_a?(::ActiveRecord::Validations::UniquenessValidator)
          end
        end

        def matches_scopes_configuration?
          if scopes_match?
            true
          else
            @failure_reason = String.new('Expected the validation ').tap do |failure_reason_string|
              failure_reason_string <<
                if expected_scopes.empty?
                  'not to be scoped to anything, '
                else
                  "to be scoped to #{inspected_expected_scopes}, "
                end

              if actual_sets_of_scopes.any?
                failure_reason_string << 'but it was scoped to '
                failure_reason_string << "#{inspected_actual_scopes} instead."
              else
                failure_reason_string << 'but it was not scoped to anything.'
              end
            end

            false
          end
        end

        def build_failure_reason
          String.new('Expected the validation ').tap do |failure_reason_string|
            failure_reason_string <<
              if expected_scopes.empty?
                'not to be scoped to anything, '
              else
                "to be scoped to #{inspected_expected_scopes}, "
              end

            if actual_sets_of_scopes.any?
              failure_reason_string << 'but it was scoped to '
              failure_reason_string << "#{inspected_actual_scopes} instead."
            else
              failure_reason_string << 'but it was not scoped to anything.'
            end
          end
        end

        def does_not_match_scopes_configuration?
          if scopes_match?
            @failure_reason = String.new('Expected the validation ').tap do |failure_reason|
              if expected_scopes.empty?
                failure_reason << 'to be scoped to nothing, '
                failure_reason << 'but it was scoped to '
                failure_reason << "#{inspected_actual_scopes} instead."
              else
                failure_reason << 'not to be scoped to '
                failure_reason << inspected_expected_scopes
              end
            end

            false
          else
            true
          end
        end

        def scopes_match?
          actual_sets_of_scopes.empty? && expected_scopes.empty? ||
            actual_sets_of_scopes.any? { |scopes| scopes == expected_scopes }
        end

        def inspected_expected_scopes
          expected_scopes.map(&:inspect).to_sentence
        end

        def inspected_actual_scopes
          inspected_actual_sets_of_scopes.to_sentence(
            words_connector: ' and ',
            last_word_connector: ', and',
          )
        end

        def inspected_actual_sets_of_scopes
          inspected_sets_of_scopes = actual_sets_of_scopes.map do |scopes|
            scopes.map(&:inspect)
          end

          if inspected_sets_of_scopes.many?
            inspected_sets_of_scopes.map { |x| "(#{x.to_sentence})" }
          else
            inspected_sets_of_scopes.map(&:to_sentence)
          end
        end

        def expected_scopes
          Array.wrap(@options[:scopes])
        end

        def actual_sets_of_scopes
          validations.map do |validation|
            Array.wrap(validation.options[:scope]).map(&:to_sym)
          end.reject(&:empty?)
        end

        def matches_allow_nil?
          !expects_to_allow_nil? || (
            update_existing_record!(nil) &&
            allows_value_of(nil, @expected_message)
          )
        end

        def does_not_match_allow_nil?
          expects_to_allow_nil? && update_existing_record!(nil) &&
            (@failure_reason = disallows_value_of(nil, @expected_message)
            )
        end

        def matches_allow_blank?
          !expects_to_allow_blank? || (
            update_existing_record!('') &&
            allows_value_of('', @expected_message)
          )
        end

        def does_not_match_allow_blank?
          expects_to_allow_blank? && update_existing_record!('') &&
            (@failure_reason = disallows_value_of('', @expected_message))
        end

        def existing_record
          unless defined?(@existing_record)
            find_or_create_existing_record
          end

          @existing_record
        end

        def find_or_create_existing_record
          @existing_record = find_existing_record

          unless @existing_record
            @existing_record = create_existing_record
            @existing_record_created = true
          end
        end

        def find_existing_record
          model.first.presence
        end

        def create_existing_record
          @given_record.tap do |existing_record|
            existing_record.save(validate: false)
          end
        rescue ::ActiveRecord::StatementInvalid => e
          raise ExistingRecordInvalid.create(underlying_exception: e)
        end

        def update_existing_record!(value)
          if existing_value_read != value
            set_attribute_on_existing_record!(@attribute, value)
            # It would be nice if we could ensure that the record was valid,
            # but that would break users' existing tests
            existing_record.save(validate: false)
          end

          true
        end

        def arbitrary_non_blank_value
          non_blank_value = dummy_value_for(@attribute)
          limit = column_limit_for(@attribute)

          is_string_value = non_blank_value.is_a?(String)
          if is_string_value && limit && limit < non_blank_value.length
            'x' * limit
          else
            non_blank_value
          end
        end

        def has_secure_password?
          Shoulda::Matchers::RailsShim.has_secure_password?(subject, @attribute)
        end

        def build_new_record
          @new_record = existing_record.dup

          attribute_names_under_test.each do |attribute_name|
            set_attribute_on_new_record!(
              attribute_name,
              existing_record.public_send(attribute_name),
            )
          end

          @new_record
        end

        def matches_presence_of_attribute?
          if attribute_present_on_model?
            true
          else
            @failure_reason =
              ":#{attribute} does not seem to be an attribute on #{model.name}."
            false
          end
        end

        def does_not_match_presence_of_attribute?
          if attribute_present_on_model?
            @failure_reason =
              ":#{attribute} seems to be an attribute on #{model.name}."
            false
          else
            true
          end
        end

        def attribute_present_on_model?
          model.method_defined?("#{attribute}=") ||
            model.columns_hash.key?(attribute.to_s)
        end

        def matches_presence_of_scopes?
          if scopes_missing_on_model.none?
            true
          else
            inspected_scopes = scopes_missing_on_model.map(&:inspect)

            @failure_reason = String.new.tap do |reason|
              reason << inspected_scopes.to_sentence

              reason <<
                if inspected_scopes.many?
                  ' do not seem to be attributes'
                else
                  ' does not seem to be an attribute'
                end

              reason << " on #{model.name}."
            end

            false
          end
        end

        def does_not_match_presence_of_scopes?
          if scopes_missing_on_model.any?
            true
          else
            inspected_scopes = scopes_present_on_model.map(&:inspect)

            @failure_reason = String.new.tap do |reason|
              reason << inspected_scopes.to_sentence

              reason <<
                if inspected_scopes.many?
                  ' seem to be attributes'
                else
                  ' seems to be an attribute'
                end

              reason << " on #{model.name}."
            end

            false
          end
        end

        def scopes_present_on_model
          @_scopes_present_on_model ||= expected_scopes.select do |scope|
            model.method_defined?("#{scope}=")
          end
        end

        def scopes_missing_on_model
          @_scopes_missing_on_model ||= expected_scopes.reject do |scope|
            model.method_defined?("#{scope}=")
          end
        end

        def matches_uniqueness_without_scopes?
          if existing_value_read.blank?
            update_existing_record!(arbitrary_non_blank_value)
          end

          disallows_value_of(existing_value_read, @expected_message)
        end

        def does_not_match_uniqueness_without_scopes?
          @failure_reason = nil

          if existing_value_read.blank?
            update_existing_record!(arbitrary_non_blank_value)
          end

          allows_value_of(existing_value_read, @expected_message)
        end

        def matches_uniqueness_with_case_sensitivity_strategy?
          if should_test_case_sensitivity?
            value = existing_value_read
            swapcased_value = value.swapcase

            if case_sensitivity_strategy == :sensitive
              if value == swapcased_value
                raise NonCaseSwappableValueError.create(
                  model:,
                  attribute: @attribute,
                  value:,
                )
              end

              allows_value_of(swapcased_value, @expected_message)
            else
              disallows_value_of(swapcased_value, @expected_message)
            end
          else
            true
          end
        end

        def does_not_match_uniqueness_with_case_sensitivity_strategy?
          if should_test_case_sensitivity?
            @failure_reason = nil

            value = existing_value_read
            swapcased_value = value.swapcase

            if case_sensitivity_strategy == :sensitive
              disallows_value_of(swapcased_value, @expected_message)
            else
              if value == swapcased_value
                raise NonCaseSwappableValueError.create(
                  model:,
                  attribute: @attribute,
                  value:,
                )
              end

              allows_value_of(swapcased_value, @expected_message)
            end
          else
            true
          end
        end

        def should_test_case_sensitivity?
          case_sensitivity_strategy != :ignore &&
            existing_value_read.respond_to?(:swapcase) &&
            !existing_value_read.empty?
        end

        def model_class?(model_name)
          model_name.constantize.ancestors.include?(::ActiveRecord::Base)
        rescue NameError
          false
        end

        def matches_uniqueness_with_scopes?
          expected_scopes.none? ||
            all_scopes_are_booleans? ||
            expected_scopes.all? do |scope|
              setting_next_value_for(scope) do
                allows_value_of(existing_value_read, @expected_message)
              end
            end
        end

        def does_not_match_uniqueness_with_scopes?
          expected_scopes.any? &&
            !all_scopes_are_booleans? &&
            expected_scopes.any? do |scope|
              setting_next_value_for(scope) do
                @failure_reason = nil
                disallows_value_of(existing_value_read, @expected_message)
              end
            end
        end

        def setting_next_value_for(scope)
          previous_value = @all_records.map(&scope).compact.max

          next_value =
            if previous_value.blank?
              dummy_value_for(scope)
            else
              next_value_for(scope, previous_value)
            end

          set_attribute_on_new_record!(scope, next_value)

          yield
        ensure
          set_attribute_on_new_record!(scope, previous_value)
        end

        def dummy_value_for(scope)
          column = column_for(scope)

          if column.respond_to?(:array) && column.array
            [dummy_scalar_value_for(column)]
          else
            dummy_scalar_value_for(column)
          end
        end

        def dummy_scalar_value_for(column)
          Shoulda::Matchers::Util.dummy_value_for(column.type)
        end

        def next_value_for(scope, previous_value)
          if previous_value.is_a?(Array)
            [next_scalar_value_for(scope, previous_value[0])]
          else
            next_scalar_value_for(scope, previous_value)
          end
        end

        def next_scalar_value_for(scope, previous_value)
          column = column_for(scope)

          if column.type == :uuid
            SecureRandom.uuid
          elsif defined_as_enum?(scope)
            available_values = available_enum_values_for(scope, previous_value)
            available_values.keys.last
          elsif polymorphic_type_attribute?(scope, previous_value)
            Uniqueness::TestModels.create(previous_value).to_s
          elsif @options[:alternatives] && scope == @attribute
            alternatives = Array.wrap(@options[:alternatives])
            alternatives.first
          elsif previous_value.respond_to?(:next)
            previous_value.next
          elsif previous_value.respond_to?(:to_datetime)
            previous_value.to_datetime.in(60).next
          elsif boolean_value?(previous_value)
            !previous_value
          else
            previous_value.to_s.next
          end
        end

        def all_scopes_are_booleans?
          @options[:scopes].all? do |scope|
            @all_records.map(&scope).all? { |s| boolean_value?(s) }
          end
        end

        def boolean_value?(value)
          [true, false].include?(value)
        end

        def defined_as_enum?(scope)
          model.respond_to?(:defined_enums) &&
            new_record.defined_enums[scope.to_s]
        end

        def polymorphic_type_attribute?(scope, previous_value)
          scope.to_s =~ /_type$/ && model_class?(previous_value)
        end

        def available_enum_values_for(scope, previous_value)
          new_record.defined_enums[scope.to_s].reject do |key, _|
            key == previous_value
          end
        end

        def set_attribute_on!(record_type, record, attribute_name, value)
          attribute_setter = build_attribute_setter(
            record,
            attribute_name,
            value,
          )
          attribute_setter.set!

          @attribute_setters[record_type] << attribute_setter
        end

        def set_attribute_on_existing_record!(attribute_name, value)
          set_attribute_on!(
            :existing_record,
            existing_record,
            attribute_name,
            value,
          )
        end

        def set_attribute_on_new_record!(attribute_name, value)
          set_attribute_on!(
            :new_record,
            new_record,
            attribute_name,
            value,
          )
        end

        def attribute_setter_for_existing_record
          @attribute_setters[:existing_record].last
        end

        def attribute_setters_for_new_record
          @attribute_setters[:new_record] +
            [last_attribute_setter_used_on_new_record]
        end

        def attribute_names_under_test
          [@attribute] + expected_scopes
        end

        def build_attribute_setter(record, attribute_name, value)
          Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeSetter.
            new(
              matcher_name: :validate_uniqueness_of,
              object: record,
              attribute_name:,
              value:,
              ignore_interference_by_writer:,
            )
        end

        def existing_value_read
          existing_record.public_send(@attribute)
        end

        def existing_value_written
          if attribute_setter_for_existing_record
            attribute_setter_for_existing_record.value_written
          else
            existing_value_read
          end
        end

        def column_for(scope)
          model.columns_hash[scope.to_s]
        end

        def column_limit_for(attribute)
          column_for(attribute).try(:limit)
        end

        def model
          @given_record.class
        end

        def failure_message_preface # rubocop:disable Metrics/MethodLength
          String.new.tap do |prefix|
            if @existing_record_created
              prefix << "After taking the given #{model.name}"

              if attribute_setter_for_existing_record
                prefix << ', setting '
                prefix << description_for_attribute_setter(
                  attribute_setter_for_existing_record,
                )
              else
                prefix << ", whose :#{attribute} is "
                prefix << "‹#{existing_value_read.inspect}›"
              end

              prefix << ', and saving it as the existing record, then'
            elsif attribute_setter_for_existing_record
              prefix << "Given an existing #{model.name},"
              prefix << ' after setting '
              prefix << description_for_attribute_setter(
                attribute_setter_for_existing_record,
              )
              prefix << ', then'
            else
              prefix << "Given an existing #{model.name} whose :#{attribute}"
              prefix << ' is '
              prefix << Shoulda::Matchers::Util.inspect_value(
                existing_value_read,
              )
              prefix << ', after'
            end

            prefix << " making a new #{model.name} and setting "

            prefix << descriptions_for_attribute_setters_for_new_record

            prefix << ", the matcher expected the new #{model.name} to be"
          end
        end

        def attribute_changed_value_message
          <<-MESSAGE.strip
As indicated in the message above,
:#{last_attribute_setter_used_on_new_record.attribute_name} seems to be
changing certain values as they are set, and this could have something
to do with why this test is failing. If you or something else has
overridden the writer method for this attribute to normalize values by
changing their case in any way (for instance, ensuring that the
attribute is always downcased), then try adding
`ignoring_case_sensitivity` onto the end of the uniqueness matcher.
Otherwise, you may need to write the test yourself, or do something
different altogether.
          MESSAGE
        end

        def description_for_attribute_setter(
          attribute_setter,
          same_as_existing: nil
        )
          description = "its :#{attribute_setter.attribute_name} to "

          if same_as_existing == false
            description << 'a different value, '
          end

          description << Shoulda::Matchers::Util.inspect_value(
            attribute_setter.value_written,
          )

          if attribute_setter.attribute_changed_value?
            description << ' (read back as '
            description << Shoulda::Matchers::Util.inspect_value(
              attribute_setter.value_read,
            )
            description << ')'
          end

          if same_as_existing == true
            description << ' as well'
          end

          description
        end

        def descriptions_for_attribute_setters_for_new_record
          attribute_setter_descriptions_for_new_record.to_sentence
        end

        def attribute_setter_descriptions_for_new_record
          attribute_setters_for_new_record.map do |attribute_setter|
            same_as_existing = (
              attribute_setter.value_written ==
              existing_value_written
            )
            description_for_attribute_setter(
              attribute_setter,
              same_as_existing:,
            )
          end
        end

        def existing_and_new_values_are_same?
          last_value_set_on_new_record == existing_value_written
        end

        def last_attribute_setter_used_on_new_record
          last_submatcher_run.last_attribute_setter_used
        end

        def last_value_set_on_new_record
          last_submatcher_run.last_value_set
        end

        # @private
        class AttributeSetters
          include Enumerable

          def initialize
            @attribute_setters = []
          end

          def <<(given_attribute_setter)
            index = find_index_of(given_attribute_setter)

            if index
              @attribute_setters[index] = given_attribute_setter
            else
              @attribute_setters << given_attribute_setter
            end
          end

          def +(other_attribute_setters)
            dup.tap do |attribute_setters|
              other_attribute_setters.each do |attribute_setter|
                attribute_setters << attribute_setter
              end
            end
          end

          def each(&block)
            @attribute_setters.each(&block)
          end

          def last
            @attribute_setters.last
          end

          private

          def find_index_of(given_attribute_setter)
            @attribute_setters.find_index do |attribute_setter|
              attribute_setter.attribute_name ==
                given_attribute_setter.attribute_name
            end
          end
        end

        # @private
        class NonCaseSwappableValueError < Shoulda::Matchers::Error
          attr_accessor :model, :attribute, :value

          def message
            Shoulda::Matchers.word_wrap <<-MESSAGE
Your #{model.name} model has a uniqueness validation on :#{attribute} which is
declared to be case-sensitive, but the value the uniqueness matcher used,
#{value.inspect}, doesn't contain any alpha characters, so using it to
test the case-sensitivity part of the validation is ineffective. There are
two possible solutions for this depending on what you're trying to do here:

a) If you meant for the validation to be case-sensitive, then you need to give
   the uniqueness matcher a saved instance of #{model.name} with a value for
   :#{attribute} that contains alpha characters.

b) If you meant for the validation to be case-insensitive, then you need to
   add `case_sensitive: false` to the validation and add `case_insensitive` to
   the matcher.

For more information, please see:

https://matchers.shoulda.io/docs/v#{Shoulda::Matchers::VERSION}/file.NonCaseSwappableValueError.html
            MESSAGE
          end
        end

        # @private
        class ExistingRecordInvalid < Shoulda::Matchers::Error
          include Shoulda::Matchers::ActiveModel::Helpers

          attr_accessor :underlying_exception

          def message
            <<-MESSAGE.strip
validate_uniqueness_of works by matching a new record against an
existing record. If there is no existing record, it will create one
using the record you provide.

While doing this, the following error was raised:

#{Shoulda::Matchers::Util.indent(underlying_exception.message, 2)}

The best way to fix this is to provide the matcher with a record where
any required attributes are filled in with valid values beforehand.
            MESSAGE
          end
        end
      end
    end
  end
end
