# frozen_string_literal: true

module Capybara
  module Node
    module Matchers
      ##
      #
      # Checks if a given selector is on the page or a descendant of the current node.
      #
      #     page.has_selector?('p#foo')
      #     page.has_selector?(:xpath, './/p[@id="foo"]')
      #     page.has_selector?(:foo)
      #
      # By default it will check if the expression occurs at least once,
      # but a different number can be specified.
      #
      #     page.has_selector?('p.foo', count: 4)
      #
      # This will check if the expression occurs exactly 4 times.
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     page.has_selector?('li', text: 'Horse', visible: true)
      #
      # {#has_selector?} can also accept XPath expressions generated by the
      # XPath gem:
      #
      #     page.has_selector?(:xpath, XPath.descendant(:p))
      #
      # @param (see Capybara::Node::Finders#all)
      # @option options [Integer] :count (nil)     Number of matching elements that should exist
      # @option options [Integer] :minimum (nil)   Minimum number of matching elements that should exist
      # @option options [Integer] :maximum (nil)   Maximum number of matching elements that should exist
      # @option options [Range]   :between (nil)   Range of number of matching elements that should exist
      # @return [Boolean]                       If the expression exists
      #
      def has_selector?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_selector(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Checks if a given selector is not on the page or a descendant of the current node.
      # Usage is identical to {#has_selector?}.
      #
      # @param (see #has_selector?)
      # @return [Boolean]
      #
      def has_no_selector?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_no_selector(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Checks if a an element has the specified CSS styles.
      #
      #     element.matches_style?( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
      #
      # @param styles [Hash]
      # @return [Boolean]                       If the styles match
      #
      def matches_style?(styles = nil, **options)
        styles, options = options, {} if styles.nil?
        make_predicate(options) { assert_matches_style(styles, **options) }
      end

      ##
      # @deprecated Use {#matches_style?} instead.
      #
      def has_style?(styles = nil, **options)
        Capybara::Helpers.warn "DEPRECATED: has_style? is deprecated, please use matches_style? : #{Capybara::Helpers.filter_backtrace(caller)}"
        matches_style?(styles, **options)
      end

      ##
      #
      # Asserts that a given selector is on the page or a descendant of the current node.
      #
      #     page.assert_selector('p#foo')
      #     page.assert_selector(:xpath, './/p[@id="foo"]')
      #     page.assert_selector(:foo)
      #
      # By default it will check if the expression occurs at least once,
      # but a different number can be specified.
      #
      #     page.assert_selector('p#foo', count: 4)
      #
      # This will check if the expression occurs exactly 4 times. See
      # {Capybara::Node::Finders#all} for other available result size options.
      #
      # If a `:count` of 0 is specified, it will behave like {#assert_no_selector};
      # however, use of that method is preferred over this one.
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     page.assert_selector('li', text: 'Horse', visible: true)
      #
      # {#assert_selector} can also accept XPath expressions generated by the
      # XPath gem:
      #
      #     page.assert_selector(:xpath, XPath.descendant(:p))
      #
      # @param (see Capybara::Node::Finders#all)
      # @option options [Integer] :count (nil)    Number of times the expression should occur
      # @raise [Capybara::ExpectationNotMet]      If the selector does not exist
      #
      def assert_selector(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block) do |result, query|
          unless result.matches_count? && (result.any? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.failure_message
          end
        end
      end

      ##
      #
      # Asserts that an element has the specified CSS styles.
      #
      #     element.assert_matches_style( 'color' => 'rgb(0,0,255)', 'font-size' => /px/ )
      #
      # @param styles [Hash]
      # @raise [Capybara::ExpectationNotMet]    If the element doesn't have the specified styles
      #
      def assert_matches_style(styles = nil, **options)
        styles, options = options, {} if styles.nil?
        query_args, query_opts = _set_query_session_options(styles, options)
        query = Capybara::Queries::StyleQuery.new(*query_args, **query_opts)
        synchronize(query.wait) do
          raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self)
        end
        true
      end

      ##
      # @deprecated Use {#assert_matches_style} instead.
      #
      def assert_style(styles = nil, **options)
        warn 'assert_style is deprecated, please use assert_matches_style instead'
        assert_matches_style(styles, **options)
      end

      # Asserts that all of the provided selectors are present on the given page
      # or descendants of the current node.  If options are provided, the assertion
      # will check that each locator is present with those options as well (other than `:wait`).
      #
      #     page.assert_all_of_selectors(:custom, 'Tom', 'Joe', visible: all)
      #     page.assert_all_of_selectors(:css, '#my_div', 'a.not_clicked')
      #
      # It accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      # The `:wait` option applies to all of the selectors as a group, so all of the locators must be present
      # within `:wait` (defaults to {Capybara.configure default_max_wait_time}) seconds.
      #
      # @overload assert_all_of_selectors([kind = Capybara.default_selector], *locators, **options)
      #
      def assert_all_of_selectors(*args, **options, &optional_filter_block)
        _verify_multiple(*args, **options) do |selector, locator, opts|
          assert_selector(selector, locator, opts, &optional_filter_block)
        end
      end

      # Asserts that none of the provided selectors are present on the given page
      # or descendants of the current node. If options are provided, the assertion
      # will check that each locator is not present with those options as well (other than `:wait`).
      #
      #     page.assert_none_of_selectors(:custom, 'Tom', 'Joe', visible: all)
      #     page.assert_none_of_selectors(:css, '#my_div', 'a.not_clicked')
      #
      # It accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      # The `:wait` option applies to all of the selectors as a group, so none of the locators must be present
      # within `:wait` (defaults to {Capybara.configure default_max_wait_time}) seconds.
      #
      # @overload assert_none_of_selectors([kind = Capybara.default_selector], *locators, **options)
      #
      def assert_none_of_selectors(*args, **options, &optional_filter_block)
        _verify_multiple(*args, **options) do |selector, locator, opts|
          assert_no_selector(selector, locator, opts, &optional_filter_block)
        end
      end

      # Asserts that any of the provided selectors are present on the given page
      # or descendants of the current node. If options are provided, the assertion
      # will check that each locator is present with those options as well (other than `:wait`).
      #
      #     page.assert_any_of_selectors(:custom, 'Tom', 'Joe', visible: all)
      #     page.assert_any_of_selectors(:css, '#my_div', 'a.not_clicked')
      #
      # It accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      # The `:wait` option applies to all of the selectors as a group, so any of the locators must be present
      # within `:wait` (defaults to {Capybara.configure default_max_wait_time}) seconds.
      #
      # @overload assert_any_of_selectors([kind = Capybara.default_selector], *locators, **options)
      #
      def assert_any_of_selectors(*args, wait: nil, **options, &optional_filter_block)
        wait = session_options.default_max_wait_time if wait.nil?
        selector = extract_selector(args)
        synchronize(wait) do
          res = args.map do |locator|
            assert_selector(selector, locator, options, &optional_filter_block)
            break nil
          rescue Capybara::ExpectationNotMet => e
            e.message
          end
          raise Capybara::ExpectationNotMet, res.join(' or ') if res

          true
        end
      end

      ##
      #
      # Asserts that a given selector is not on the page or a descendant of the current node.
      # Usage is identical to {#assert_selector}.
      #
      # Query options such as `:count`, `:minimum`, `:maximum`, and `:between` are
      # considered to be an integral part of the selector. This will return
      # `true`, for example, if a page contains 4 anchors but the query expects 5:
      #
      #     page.assert_no_selector('a', minimum: 1) # Found, raises Capybara::ExpectationNotMet
      #     page.assert_no_selector('a', count: 4)   # Found, raises Capybara::ExpectationNotMet
      #     page.assert_no_selector('a', count: 5)   # Not Found, returns true
      #
      # @param (see #assert_selector)
      # @raise [Capybara::ExpectationNotMet]      If the selector exists
      #
      def assert_no_selector(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block) do |result, query|
          if result.matches_count? && (!result.empty? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.negative_failure_message
          end
        end
      end

      ##
      #
      # Checks if a given XPath expression is on the page or a descendant of the current node.
      #
      #     page.has_xpath?('.//p[@id="foo"]')
      #
      # By default it will check if the expression occurs at least once,
      # but a different number can be specified.
      #
      #     page.has_xpath?('.//p[@id="foo"]', count: 4)
      #
      # This will check if the expression occurs exactly 4 times.
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     page.has_xpath?('.//li', text: 'Horse', visible: true)
      #
      # {#has_xpath?} can also accept XPath expressions generated by the
      # XPath gem:
      #
      #     xpath = XPath.generate { |x| x.descendant(:p) }
      #     page.has_xpath?(xpath)
      #
      # @param [String] path                      An XPath expression
      # @param options                            (see Capybara::Node::Finders#all)
      # @option options [Integer] :count (nil)    Number of times the expression should occur
      # @return [Boolean]                         If the expression exists
      #
      def has_xpath?(path, **options, &optional_filter_block)
        has_selector?(:xpath, path, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if a given XPath expression is not on the page or a descendant of the current node.
      # Usage is identical to {#has_xpath?}.
      #
      # @param (see #has_xpath?)
      # @return [Boolean]
      #
      def has_no_xpath?(path, **options, &optional_filter_block)
        has_no_selector?(:xpath, path, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if a given CSS selector is on the page or a descendant of the current node.
      #
      #     page.has_css?('p#foo')
      #
      # By default it will check if the selector occurs at least once,
      # but a different number can be specified.
      #
      #     page.has_css?('p#foo', count: 4)
      #
      # This will check if the selector occurs exactly 4 times.
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     page.has_css?('li', text: 'Horse', visible: true)
      #
      # @param [String] path                      A CSS selector
      # @param options                            (see Capybara::Node::Finders#all)
      # @option options [Integer] :count (nil)    Number of times the selector should occur
      # @return [Boolean]                         If the selector exists
      #
      def has_css?(path, **options, &optional_filter_block)
        has_selector?(:css, path, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if a given CSS selector is not on the page or a descendant of the current node.
      # Usage is identical to {#has_css?}.
      #
      # @param (see #has_css?)
      # @return [Boolean]
      #
      def has_no_css?(path, **options, &optional_filter_block)
        has_no_selector?(:css, path, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a link with the given
      # text or id.
      #
      # @param [String] locator           The text or id of a link to check for
      # @option options [String, Regexp] :href    The value the href attribute must be
      # @return [Boolean]                 Whether it exists
      #
      def has_link?(locator = nil, **options, &optional_filter_block)
        has_selector?(:link, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no link with the given
      # text or id.
      #
      # @param (see #has_link?)
      # @return [Boolean]            Whether it doesn't exist
      #
      def has_no_link?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:link, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a button with the given
      # text, value or id.
      #
      # @param [String] locator      The text, value or id of a button to check for
      # @return [Boolean]            Whether it exists
      #
      def has_button?(locator = nil, **options, &optional_filter_block)
        has_selector?(:button, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no button with the given
      # text, value or id.
      #
      # @param [String] locator      The text, value or id of a button to check for
      # @return [Boolean]            Whether it doesn't exist
      #
      def has_no_button?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:button, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a form field with the given
      # label, name or id.
      #
      # For text fields and other textual fields, such as textareas and
      # HTML5 email/url/etc. fields, it's possible to specify a `:with`
      # option to specify the text the field should contain:
      #
      #     page.has_field?('Name', with: 'Jonas')
      #
      # It is also possible to filter by the field type attribute:
      #
      #     page.has_field?('Email', type: 'email')
      #
      # NOTE: 'textarea' and 'select' are valid type values, matching the associated tag names.
      #
      # @param [String] locator                  The label, name or id of a field to check for
      # @option options [String, Regexp] :with   The text content of the field or a Regexp to match
      # @option options [String] :type           The type attribute of the field
      # @return [Boolean]                        Whether it exists
      #
      def has_field?(locator = nil, **options, &optional_filter_block)
        has_selector?(:field, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no form field with the given
      # label, name or id. See {#has_field?}.
      #
      # @param [String] locator                  The label, name or id of a field to check for
      # @option options [String, Regexp] :with   The text content of the field or a Regexp to match
      # @option options [String] :type           The type attribute of the field
      # @return [Boolean]                        Whether it doesn't exist
      #
      def has_no_field?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:field, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a radio button or
      # checkbox with the given label, value, id, or {Capybara.configure test_id} attribute that is currently
      # checked.
      #
      # @param [String] locator           The label, name or id of a checked field
      # @return [Boolean]                 Whether it exists
      #
      def has_checked_field?(locator = nil, **options, &optional_filter_block)
        has_selector?(:field, locator, **options.merge(checked: true), &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no radio button or
      # checkbox with the given label, value or id, or {Capybara.configure test_id} attribute that is currently
      # checked.
      #
      # @param [String] locator           The label, name or id of a checked field
      # @return [Boolean]                 Whether it doesn't exist
      #
      def has_no_checked_field?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:field, locator, **options.merge(checked: true), &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a radio button or
      # checkbox with the given label, value or id, or {Capybara.configure test_id} attribute that is currently
      # unchecked.
      #
      # @param [String] locator           The label, name or id of an unchecked field
      # @return [Boolean]                 Whether it exists
      #
      def has_unchecked_field?(locator = nil, **options, &optional_filter_block)
        has_selector?(:field, locator, **options.merge(unchecked: true), &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no radio button or
      # checkbox with the given label, value or id, or {Capybara.configure test_id} attribute that is currently
      # unchecked.
      #
      # @param [String] locator           The label, name or id of an unchecked field
      # @return [Boolean]                 Whether it doesn't exist
      #
      def has_no_unchecked_field?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:field, locator, **options.merge(unchecked: true), &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a select field with the
      # given label, name or id.
      #
      # It can be specified which option should currently be selected:
      #
      #     page.has_select?('Language', selected: 'German')
      #
      # For multiple select boxes, several options may be specified:
      #
      #     page.has_select?('Language', selected: ['English', 'German'])
      #
      # It's also possible to check if the exact set of options exists for
      # this select box:
      #
      #     page.has_select?('Language', options: ['English', 'German', 'Spanish'])
      #
      # You can also check for a partial set of options:
      #
      #     page.has_select?('Language', with_options: ['English', 'German'])
      #
      # @param [String] locator                         The label, name or id of a select box
      # @option options [Array] :options                Options which should be contained in this select box
      # @option options [Array] :with_options           Partial set of options which should be contained in this select box
      # @option options [String, Array] :selected       Options which should be selected
      # @option options [String, Array] :with_selected  Partial set of options which should minimally be selected
      # @return [Boolean]                               Whether it exists
      #
      def has_select?(locator = nil, **options, &optional_filter_block)
        has_selector?(:select, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no select field with the
      # given label, name or id. See {#has_select?}.
      #
      # @param (see #has_select?)
      # @return [Boolean]     Whether it doesn't exist
      #
      def has_no_select?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:select, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has a table with the given id
      # or caption:
      #
      #    page.has_table?('People')
      #
      # @param [String] locator  The id or caption of a table
      # @option options [Array<Array<String>>] :rows
      #   Text which should be contained in the tables `<td>` elements organized by row (`<td>` visibility is not considered)
      # @option options [Array<Array<String>>, Array<Hash<String,String>>] :with_rows
      #   Partial set of text which should be contained in the tables `<td>` elements organized by row (`<td>` visibility is not considered)
      # @option options [Array<Array<String>>] :cols
      #   Text which should be contained in the tables `<td>` elements organized by column (`<td>` visibility is not considered)
      # @option options [Array<Array<String>>, Array<Hash<String,String>>] :with_cols
      #   Partial set of text which should be contained in the tables `<td>` elements organized by column (`<td>` visibility is not considered)
      # @return [Boolean]        Whether it exists
      #
      def has_table?(locator = nil, **options, &optional_filter_block)
        has_selector?(:table, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the page or current node has no table with the given id
      # or caption. See {#has_table?}.
      #
      # @param (see #has_table?)
      # @return [Boolean]       Whether it doesn't exist
      #
      def has_no_table?(locator = nil, **options, &optional_filter_block)
        has_no_selector?(:table, locator, **options, &optional_filter_block)
      end

      ##
      #
      # Asserts that the current node matches a given selector.
      #
      #     node.assert_matches_selector('p#foo')
      #     node.assert_matches_selector(:xpath, '//p[@id="foo"]')
      #     node.assert_matches_selector(:foo)
      #
      # It also accepts all options that {Capybara::Node::Finders#all} accepts,
      # such as `:text` and `:visible`.
      #
      #     node.assert_matches_selector('li', text: 'Horse', visible: true)
      #
      # @param (see Capybara::Node::Finders#all)
      # @raise [Capybara::ExpectationNotMet]      If the selector does not match
      #
      def assert_matches_selector(*args, &optional_filter_block)
        _verify_match_result(args, optional_filter_block) do |result|
          raise Capybara::ExpectationNotMet, 'Item does not match the provided selector' unless result.include? self
        end
      end

      ##
      #
      # Asserts that the current node does not match a given selector.
      # Usage is identical to {#assert_matches_selector}.
      #
      # @param (see #assert_matches_selector)
      # @raise [Capybara::ExpectationNotMet]      If the selector matches
      #
      def assert_not_matches_selector(*args, &optional_filter_block)
        _verify_match_result(args, optional_filter_block) do |result|
          raise Capybara::ExpectationNotMet, 'Item matched the provided selector' if result.include? self
        end
      end

      ##
      #
      # Checks if the current node matches given selector.
      #
      # @param (see #has_selector?)
      # @return [Boolean]
      #
      def matches_selector?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_matches_selector(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Checks if the current node matches given XPath expression.
      #
      # @param [String, XPath::Expression] xpath The XPath expression to match against the current code
      # @return [Boolean]
      #
      def matches_xpath?(xpath, **options, &optional_filter_block)
        matches_selector?(:xpath, xpath, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the current node matches given CSS selector.
      #
      # @param [String] css The CSS selector to match against the current code
      # @return [Boolean]
      #
      def matches_css?(css, **options, &optional_filter_block)
        matches_selector?(:css, css, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the current node does not match given selector.
      # Usage is identical to {#has_selector?}.
      #
      # @param (see #has_selector?)
      # @return [Boolean]
      #
      def not_matches_selector?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_not_matches_selector(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Checks if the current node does not match given XPath expression.
      #
      # @param [String, XPath::Expression] xpath The XPath expression to match against the current code
      # @return [Boolean]
      #
      def not_matches_xpath?(xpath, **options, &optional_filter_block)
        not_matches_selector?(:xpath, xpath, **options, &optional_filter_block)
      end

      ##
      #
      # Checks if the current node does not match given CSS selector.
      #
      # @param [String] css The CSS selector to match against the current code
      # @return [Boolean]
      #
      def not_matches_css?(css, **options, &optional_filter_block)
        not_matches_selector?(:css, css, **options, &optional_filter_block)
      end

      ##
      # Asserts that the page or current node has the given text content,
      # ignoring any HTML tags.
      #
      # @!macro text_query_params
      #   @overload $0(type, text, **options)
      #     @param [:all, :visible] type               Whether to check for only visible or all text. If this parameter is missing or nil then we use the value of {Capybara.configure ignore_hidden_elements}, which defaults to `true`, corresponding to `:visible`.
      #     @param [String, Regexp] text               The string/regexp to check for. If it's a string, text is expected to include it. If it's a regexp, text is expected to match it.
      #     @option options [Integer] :count (nil)     Number of times the text is expected to occur
      #     @option options [Integer] :minimum (nil)   Minimum number of times the text is expected to occur
      #     @option options [Integer] :maximum (nil)   Maximum number of times the text is expected to occur
      #     @option options [Range]   :between (nil)   Range of times that is expected to contain number of times text occurs
      #     @option options [Numeric] :wait            Maximum time that Capybara will wait for text to eq/match given string/regexp argument. Defaults to {Capybara.configure default_max_wait_time}.
      #     @option options [Boolean] :exact           Whether text must be an exact match or just substring. Defaults to {Capybara.configure exact_text}.
      #     @option options [Boolean] :normalize_ws (false) When `true` replace all whitespace with standard spaces and collapse consecutive whitespace to a single space
      #   @overload $0(text, **options)
      #     @param [String, Regexp] text               The string/regexp to check for. If it's a string, text is expected to include it. If it's a regexp, text is expected to match it.
      #     @option options [Integer] :count (nil)     Number of times the text is expected to occur
      #     @option options [Integer] :minimum (nil)   Minimum number of times the text is expected to occur
      #     @option options [Integer] :maximum (nil)   Maximum number of times the text is expected to occur
      #     @option options [Range]   :between (nil)   Range of times that is expected to contain number of times text occurs
      #     @option options [Numeric] :wait            Maximum time that Capybara will wait for text to eq/match given string/regexp argument. Defaults to {Capybara.configure default_max_wait_time}.
      #     @option options [Boolean] :exact           Whether text must be an exact match or just substring. Defaults to {Capybara.configure exact_text}.
      #     @option options [Boolean] :normalize_ws (false) When `true` replace all whitespace with standard spaces and collapse consecutive whitespace to a single space
      # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
      # @return [true]
      #
      def assert_text(type_or_text, *args, **opts)
        _verify_text(type_or_text, *args, **opts) do |count, query|
          unless query.matches_count?(count) && (count.positive? || query.expects_none?)
            raise Capybara::ExpectationNotMet, query.failure_message
          end
        end
      end

      ##
      # Asserts that the page or current node doesn't have the given text content,
      # ignoring any HTML tags.
      #
      # @macro text_query_params
      # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
      # @return [true]
      #
      def assert_no_text(type_or_text, *args, **opts)
        _verify_text(type_or_text, *args, **opts) do |count, query|
          if query.matches_count?(count) && (count.positive? || query.expects_none?)
            raise Capybara::ExpectationNotMet, query.negative_failure_message
          end
        end
      end

      ##
      # Checks if the page or current node has the given text content,
      # ignoring any HTML tags.
      #
      # By default it will check if the text occurs at least once,
      # but a different number can be specified.
      #
      #     page.has_text?('lorem ipsum', between: 2..4)
      #
      # This will check if the text occurs from 2 to 4 times.
      #
      # @macro text_query_params
      # @return [Boolean]                            Whether it exists
      #
      def has_text?(*args, **options)
        make_predicate(options) { assert_text(*args, **options) }
      end
      alias_method :has_content?, :has_text?

      ##
      # Checks if the page or current node does not have the given text
      # content, ignoring any HTML tags and normalizing whitespace.
      #
      # @macro text_query_params
      # @return [Boolean]  Whether it doesn't exist
      #
      def has_no_text?(*args, **options)
        make_predicate(options) { assert_no_text(*args, **options) }
      end
      alias_method :has_no_content?, :has_no_text?

      ##
      #
      # Asserts that a given selector matches an ancestor of the current node.
      #
      #     element.assert_ancestor('p#foo')
      #
      # Accepts the same options as {#assert_selector}
      #
      # @param (see Capybara::Node::Finders#find)
      # @raise [Capybara::ExpectationNotMet]      If the selector does not exist
      #
      def assert_ancestor(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block, Capybara::Queries::AncestorQuery) do |result, query|
          unless result.matches_count? && (result.any? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.failure_message
          end
        end
      end

      def assert_no_ancestor(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block, Capybara::Queries::AncestorQuery) do |result, query|
          if result.matches_count? && (!result.empty? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.negative_failure_message
          end
        end
      end

      ##
      #
      # Predicate version of {#assert_ancestor}
      #
      def has_ancestor?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_ancestor(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Predicate version of {#assert_no_ancestor}
      #
      def has_no_ancestor?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_no_ancestor(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Asserts that a given selector matches a sibling of the current node.
      #
      #     element.assert_sibling('p#foo')
      #
      # Accepts the same options as {#assert_selector}
      #
      # @param (see Capybara::Node::Finders#find)
      # @raise [Capybara::ExpectationNotMet]      If the selector does not exist
      #
      def assert_sibling(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block, Capybara::Queries::SiblingQuery) do |result, query|
          unless result.matches_count? && (result.any? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.failure_message
          end
        end
      end

      def assert_no_sibling(*args, &optional_filter_block)
        _verify_selector_result(args, optional_filter_block, Capybara::Queries::SiblingQuery) do |result, query|
          if result.matches_count? && (!result.empty? || query.expects_none?)
            raise Capybara::ExpectationNotMet, result.negative_failure_message
          end
        end
      end

      ##
      #
      # Predicate version of {#assert_sibling}
      #
      def has_sibling?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_sibling(*args, options, &optional_filter_block) }
      end

      ##
      #
      # Predicate version of {#assert_no_sibling}
      #
      def has_no_sibling?(*args, **options, &optional_filter_block)
        make_predicate(options) { assert_no_sibling(*args, options, &optional_filter_block) }
      end

      def ==(other)
        eql?(other) || (other.respond_to?(:base) && base == other.base)
      end

    private

      def extract_selector(args)
        args.first.is_a?(Symbol) ? args.shift : session_options.default_selector
      end

      def _verify_multiple(*args, wait: nil, **options)
        wait = session_options.default_max_wait_time if wait.nil?
        selector = extract_selector(args)
        synchronize(wait) do
          args.each { |locator| yield(selector, locator, options) }
        end
      end

      def _verify_selector_result(query_args, optional_filter_block, query_type = Capybara::Queries::SelectorQuery)
        # query_args, query_opts = if query_args[0].is_a? Symbol
        #   a,o = _set_query_session_options(*query_args.slice(2..))
        #   [query_args.slice(0..1).concat(a), o]
        # else
        #   _set_query_session_options(*query_args)
        # end
        query_args, query_opts = _set_query_session_options(*query_args)
        query = query_type.new(*query_args, **query_opts, &optional_filter_block)
        synchronize(query.wait) do
          yield query.resolve_for(self), query
        end
        true
      end

      def _verify_match_result(query_args, optional_filter_block)
        query_args, query_opts = _set_query_session_options(*query_args)
        query = Capybara::Queries::MatchQuery.new(*query_args, **query_opts, &optional_filter_block)
        synchronize(query.wait) do
          yield query.resolve_for(parent || session&.document || query_scope)
        end
        true
      end

      def _verify_text(type = nil, expected_text, **query_options) # rubocop:disable Style/OptionalArguments
        query_options[:session_options] = session_options
        query = Capybara::Queries::TextQuery.new(type, expected_text, **query_options)
        synchronize(query.wait) do
          yield query.resolve_for(self), query
        end
        true
      end

      def _set_query_session_options(*query_args)
        query_args, query_options = query_args.dup, {}
        # query_options = query_args.pop if query_options.empty? && query_args.last.is_a?(Hash)
        query_options = query_args.pop if query_args.last.is_a?(Hash)
        query_options[:session_options] = session_options
        [query_args, query_options]
      end

      def make_predicate(options)
        options[:wait] = 0 unless options.key?(:wait) || session_options.predicates_wait
        yield
      rescue Capybara::ExpectationNotMet
        false
      end
    end
  end
end
