# frozen_string_literal: true

require 'matrix'

module Capybara
  module Queries
    class SelectorQuery < Queries::BaseQuery
      attr_reader :expression, :selector, :locator, :options

      SPATIAL_KEYS = %i[above below left_of right_of near].freeze
      VALID_KEYS = SPATIAL_KEYS + COUNT_KEYS +
                   %i[text id class style visible obscured exact exact_text normalize_ws match wait filter_set focused]
      VALID_MATCH = %i[first smart prefer_exact one].freeze

      def initialize(*args,
                     session_options:,
                     enable_aria_label: session_options.enable_aria_label,
                     enable_aria_role: session_options.enable_aria_role,
                     test_id: session_options.test_id,
                     selector_format: nil,
                     order: nil,
                     **options,
                     &filter_block)
        @resolved_node = nil
        @resolved_count = 0
        @options = options.dup
        @order = order
        @filter_cache = Hash.new { |hsh, key| hsh[key] = {} }

        if @options[:text].is_a?(Regexp) && [true, false].include?(@options[:exact_text])
          Capybara::Helpers.warn(
            "Boolean 'exact_text' option is not supported when 'text' option is a Regexp - ignoring"
          )
        end

        super(@options)
        self.session_options = session_options

        @selector = Selector.new(
          find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
          config: {
            enable_aria_label: enable_aria_label,
            enable_aria_role: enable_aria_role,
            test_id: test_id
          },
          format: selector_format
        )

        @locator = args.shift
        @filter_block = filter_block

        raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?

        @expression = selector.call(@locator, **@options)

        warn_exact_usage

        assert_valid_keys
      end

      def name; selector.name; end
      def label; selector.label || selector.name; end

      def description(only_applied = false) # rubocop:disable Style/OptionalBooleanParameter
        desc = +''
        show_for = show_for_stage(only_applied)

        if show_for[:any]
          desc << 'visible ' if visible == :visible
          desc << 'non-visible ' if visible == :hidden
        end

        desc << label.to_s
        desc << " #{locator.inspect}" unless locator.nil?

        if show_for[:any]
          desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
          desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
        end

        desc << " with id #{options[:id]}" if options[:id]
        desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
        desc << ' that is focused' if options[:focused]
        desc << ' that is not focused' if options[:focused] == false

        desc << case options[:style]
        when String
          " with style attribute #{options[:style].inspect}"
        when Regexp
          " with style attribute matching #{options[:style].inspect}"
        when Hash
          " with styles #{options[:style].inspect}"
        else ''
        end

        %i[above below left_of right_of near].each do |spatial_filter|
          if options[spatial_filter] && show_for[:spatial]
            desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" # rubocop:disable Style/RescueModifier
          end
        end

        desc << selector.description(node_filters: show_for[:node], **options)

        desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]

        desc << " within #{@resolved_node.inspect}" if describe_within?
        if locator.is_a?(String) && locator.start_with?('#', './/', '//') && !selector.raw_locator?
          desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
                  "Please see the documentation for acceptable locator values.\n\n"
        end
        desc
      end

      def applied_description
        description(true)
      end

      def matches_filters?(node, node_filter_errors = [])
        return true if (@resolved_node&.== node) && options[:allow_self]

        matches_locator_filter?(node) &&
          matches_system_filters?(node) &&
          matches_spatial_filters?(node) &&
          matches_node_filters?(node, node_filter_errors) &&
          matches_filter_block?(node)
      rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
        false
      end

      def visible
        case (vis = options.fetch(:visible) { default_visibility })
        when true then :visible
        when false then :all
        else vis
        end
      end

      def exact?
        supports_exact? ? options.fetch(:exact, session_options.exact) : false
      end

      def match
        options.fetch(:match, session_options.match)
      end

      def xpath(exact = nil)
        exact = exact? if exact.nil?
        expr = apply_expression_filters(@expression)
        expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
        expr = filtered_expression(expr)
        expr = "(#{expr})[#{xpath_text_conditions}]" if try_text_match_in_expression?
        expr
      end

      def css
        filtered_expression(apply_expression_filters(@expression))
      end

      # @api private
      def resolve_for(node, exact = nil)
        applied_filters.clear
        @filter_cache.clear
        @resolved_node = node
        @resolved_count += 1

        node.synchronize do
          children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
          Capybara::Result.new(ordered_results(children), self)
        end
      end

      # @api private
      def supports_exact?
        return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?

        @selector.supports_exact?
      end

      def failure_message
        "expected to find #{applied_description}" << count_message
      end

      def negative_failure_message
        "expected not to find #{applied_description}" << count_message
      end

    private

      def selector_format
        @selector.format
      end

      def matching_text
        options[:text] || options[:exact_text]
      end

      def text_fragments
        (text = matching_text).is_a?(String) ? text.split : []
      end

      def xpath_text_conditions
        case (text = matching_text)
        when String
          text.split.map { |txt| XPath.contains(txt) }.reduce(&:&)
        when Regexp
          condition = XPath.current
          condition = condition.uppercase if text.casefold?
          Selector::RegexpDisassembler.new(text).alternated_substrings.map do |strs|
            strs.flat_map(&:split).map { |str| condition.contains(str) }.reduce(:&)
          end.reduce(:|)
        end
      end

      def try_text_match_in_expression?
        first_try? &&
          matching_text &&
          @resolved_node.is_a?(Capybara::Node::Base) &&
          @resolved_node.session&.driver&.wait?
      end

      def first_try?
        @resolved_count == 1
      end

      def show_for_stage(only_applied)
        lambda do |stage = :any|
          !only_applied || (stage == :any ? applied_filters.any? : applied_filters.include?(stage))
        end
      end

      def applied_filters
        @applied_filters ||= []
      end

      def find_selector(locator)
        case locator
        when Symbol then Selector[locator]
        else Selector.for(locator)
        end || Selector[session_options.default_selector]
      end

      def find_nodes_by_selector_format(node, exact)
        hints = {}
        hints[:uses_visibility] = true unless visible == :all
        hints[:texts] = text_fragments unless selector_format == :xpath
        hints[:styles] = options[:style] if use_default_style_filter?
        hints[:position] = true if use_spatial_filter?

        case selector_format
        when :css
          if node.method(:find_css).arity == 1
            node.find_css(css)
          else
            node.find_css(css, **hints)
          end
        when :xpath
          if node.method(:find_xpath).arity == 1
            node.find_xpath(xpath(exact))
          else
            node.find_xpath(xpath(exact), **hints)
          end
        else
          raise ArgumentError, "Unknown format: #{selector_format}"
        end
      end

      def to_element(node)
        if @resolved_node.is_a?(Capybara::Node::Base)
          Capybara::Node::Element.new(@resolved_node.session, node, @resolved_node, self)
        else
          Capybara::Node::Simple.new(node)
        end
      end

      def valid_keys
        (VALID_KEYS + custom_keys).uniq
      end

      def matches_node_filters?(node, errors)
        applied_filters << :node

        unapplied_options = options.keys - valid_keys
        @selector.with_filter_errors(errors) do
          node_filters.all? do |filter_name, filter|
            next true unless apply_filter?(filter)

            if filter.matcher?
              unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
                unapplied_options.delete(option_name)
                filter.matches?(node, option_name, options[option_name], @selector)
              end
            elsif options.key?(filter_name)
              unapplied_options.delete(filter_name)
              filter.matches?(node, filter_name, options[filter_name], @selector)
            elsif filter.default?
              filter.matches?(node, filter_name, filter.default, @selector)
            else
              true
            end
          end
        end
      end

      def matches_filter_block?(node)
        return true unless @filter_block

        if node.respond_to?(:session)
          node.session.using_wait_time(0) { @filter_block.call(node) }
        else
          @filter_block.call(node)
        end
      end

      def filter_set(name)
        ::Capybara::Selector::FilterSet[name]
      end

      def node_filters
        if options.key?(:filter_set)
          filter_set(options[:filter_set])
        else
          @selector
        end.node_filters
      end

      def expression_filters
        filters = @selector.expression_filters
        filters.merge filter_set(options[:filter_set]).expression_filters if options.key?(:filter_set)
        filters
      end

      def ordered_results(results)
        case @order
        when :reverse
          results.reverse
        else
          results
        end
      end

      def custom_keys
        @custom_keys ||= node_filters.keys + expression_filters.keys
      end

      def assert_valid_keys
        unless VALID_MATCH.include?(match)
          raise ArgumentError, "Invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
        end

        unhandled_options = @options.keys.reject do |option_name|
          valid_keys.include?(option_name) ||
            expression_filters.any? { |_name, ef| ef.handles_option? option_name } ||
            node_filters.any? { |_name, nf| nf.handles_option? option_name }
        end

        return if unhandled_options.empty?

        invalid_names = unhandled_options.map(&:inspect).join(', ')
        valid_names = (valid_keys - [:allow_self]).map(&:inspect).join(', ')
        raise ArgumentError, "Invalid option(s) #{invalid_names}, should be one of #{valid_names}"
      end

      def filtered_expression(expr)
        conditions = {}
        conditions[:id] = options[:id] if use_default_id_filter?
        conditions[:class] = options[:class] if use_default_class_filter?
        conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
        builder(expr).add_attribute_conditions(**conditions)
      end

      def use_default_id_filter?
        options.key?(:id) && !custom_keys.include?(:id)
      end

      def use_default_class_filter?
        options.key?(:class) && !custom_keys.include?(:class)
      end

      def use_default_style_filter?
        options.key?(:style) && !custom_keys.include?(:style)
      end

      def use_default_focused_filter?
        options.key?(:focused) && !custom_keys.include?(:focused)
      end

      def use_spatial_filter?
        options.values_at(*SPATIAL_KEYS).compact.any?
      end

      def apply_expression_filters(expression)
        unapplied_options = options.keys - valid_keys
        expression_filters.inject(expression) do |expr, (name, ef)|
          next expr unless apply_filter?(ef)

          if ef.matcher?
            unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
              unapplied_options.delete(option_name)
              ef.apply_filter(memo, option_name, options[option_name], @selector)
            end
          elsif options.key?(name)
            unapplied_options.delete(name)
            ef.apply_filter(expr, name, options[name], @selector)
          elsif ef.default?
            ef.apply_filter(expr, name, ef.default, @selector)
          else
            expr
          end
        end
      end

      def warn_exact_usage
        return unless options.key?(:exact) && !supports_exact?

        warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression}\" has no effect."
      end

      def exact_text
        options.fetch(:exact_text, session_options.exact_text)
      end

      def describe_within?
        @resolved_node && !document?(@resolved_node) && !simple_root?(@resolved_node)
      end

      def document?(node)
        node.is_a?(::Capybara::Node::Document)
      end

      def simple_root?(node)
        node.is_a?(::Capybara::Node::Simple) && node.path == '/'
      end

      def apply_filter?(filter)
        filter.format.nil? || (filter.format == selector_format)
      end

      def matches_locator_filter?(node)
        return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)

        @selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
      end

      def matches_system_filters?(node)
        applied_filters << :system

        matches_visibility_filters?(node) &&
          matches_id_filter?(node) &&
          matches_class_filter?(node) &&
          matches_style_filter?(node) &&
          matches_focused_filter?(node) &&
          matches_text_filter?(node) &&
          matches_exact_text_filter?(node)
      end

      def matches_spatial_filters?(node)
        applied_filters << :spatial
        return true unless use_spatial_filter?

        node_rect = Rectangle.new(node.initial_cache[:position] || node.rect)

        if options[:above]
          el_rect = rect_cache(options[:above])
          return false unless node_rect.above? el_rect
        end

        if options[:below]
          el_rect = rect_cache(options[:below])
          return false unless node_rect.below? el_rect
        end

        if options[:left_of]
          el_rect = rect_cache(options[:left_of])
          return false unless node_rect.left_of? el_rect
        end

        if options[:right_of]
          el_rect = rect_cache(options[:right_of])
          return false unless node_rect.right_of? el_rect
        end

        if options[:near]
          return false if node == options[:near]

          el_rect = rect_cache(options[:near])
          return false unless node_rect.near? el_rect
        end

        true
      end

      def matches_id_filter?(node)
        return true unless use_default_id_filter? && options[:id].is_a?(Regexp)

        options[:id].match? node[:id]
      end

      def matches_class_filter?(node)
        return true unless use_default_class_filter? && need_to_process_classes?

        if options[:class].is_a? Regexp
          options[:class].match? node[:class]
        else
          classes = (node[:class] || '').split
          options[:class].select { |c| c.is_a? Regexp }.all? do |r|
            classes.any? { |cls| r.match? cls }
          end
        end
      end

      def matches_focused_filter?(node)
        return true unless use_default_focused_filter?

        (node == node.session.active_element) == options[:focused]
      end

      def need_to_process_classes?
        case options[:class]
        when Regexp then true
        when Array then options[:class].any?(Regexp)
        else
          false
        end
      end

      def matches_style_filter?(node)
        case options[:style]
        when String, nil
          true
        when Regexp
          options[:style].match? node[:style]
        when Hash
          matches_style?(node, options[:style])
        end
      end

      def matches_style?(node, styles)
        @actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
        styles.all? do |style, value|
          if value.is_a? Regexp
            value.match? @actual_styles[style.to_s]
          else
            @actual_styles[style.to_s] == value
          end
        end
      end

      def matches_text_filter?(node)
        value = options[:text]
        return true unless value
        return matches_text_exactly?(node, value) if exact_text == true && !value.is_a?(Regexp)

        regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
        matches_text_regexp?(node, regexp)
      end

      def matches_exact_text_filter?(node)
        case exact_text
        when String, Regexp
          matches_text_exactly?(node, exact_text)
        else
          true
        end
      end

      def matches_visibility_filters?(node)
        obscured = options[:obscured]
        return (visible != :hidden) && (node.initial_cache[:visible] != false) && !node.obscured? if obscured == false

        vis = case visible
        when :visible
          node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
        when :hidden
          # TODO: check why the 'visbile' cache spelling mistake wasn't caught in a test
          # (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
          (node.initial_cache[:visible] == false) || (node.initial_cache[:visible].nil? && !node.visible?)
        else
          true
        end

        vis && case obscured
               when true
                 node.obscured?
               when false
                 !node.obscured?
               else
                 true
               end
      end

      def matches_text_exactly?(node, value)
        regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
        matches_text_regexp(node, regexp).then { |m| m&.pre_match == '' && m&.post_match == '' }
      end

      def normalize_ws
        options.fetch(:normalize_ws, session_options.default_normalize_ws)
      end

      def matches_text_regexp(node, regexp)
        text_visible = visible
        text_visible = :all if text_visible == :hidden
        node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
      end

      def matches_text_regexp?(node, regexp)
        !matches_text_regexp(node, regexp).nil?
      end

      def default_visibility
        @selector.default_visibility(session_options.ignore_hidden_elements, options)
      end

      def builder(expr)
        selector.builder(expr)
      end

      def position_cache(key)
        @filter_cache[key][:position] ||= key.rect
      end

      def rect_cache(key)
        @filter_cache[key][:rect] ||= Rectangle.new(position_cache(key))
      end

      class Rectangle
        attr_reader :top, :bottom, :left, :right

        def initialize(position)
          # rubocop:disable Style/RescueModifier
          @top = position['top'] rescue position['y']
          @bottom = position['bottom'] rescue (@top + position['height'])
          @left = position['left'] rescue position['x']
          @right = position['right'] rescue (@left + position['width'])
          # rubocop:enable Style/RescueModifier
        end

        def distance(other)
          distance = Float::INFINITY

          line_segments.each do |ls1|
            other.line_segments.each do |ls2|
              distance = [
                distance,
                distance_segment_segment(*ls1, *ls2)
              ].min
            end
          end

          distance
        end

        def above?(other)
          bottom <= other.top
        end

        def below?(other)
          top >= other.bottom
        end

        def left_of?(other)
          right <= other.left
        end

        def right_of?(other)
          left >= other.right
        end

        def near?(other)
          distance(other) <= 50
        end

      protected

        def line_segments
          [
            [Vector[top, left], Vector[top, right]],
            [Vector[top, right], Vector[bottom, left]],
            [Vector[bottom, left], Vector[bottom, right]],
            [Vector[bottom, right], Vector[top, left]]
          ]
        end

      private

        def distance_segment_segment(l1p1, l1p2, l2p1, l2p2)
          # See http://geomalgorithms.com/a07-_distance.html
          # rubocop:disable Naming/VariableName
          u = l1p2 - l1p1
          v = l2p2 - l2p1
          w = l1p1 - l2p1

          a = u.dot u
          b = u.dot v
          c = v.dot v

          d = u.dot w
          e = v.dot w
          cap_d = (a * c) - (b**2)
          sD = tD = cap_d

          # compute the line parameters of the two closest points
          if cap_d < Float::EPSILON # the lines are almost parallel
            sN = 0.0 # force using point P0 on segment S1
            sD = 1.0 # to prevent possible division by 0.0 later
            tN = e
            tD = c
          else # get the closest points on the infinite lines
            sN = (b * e) - (c * d)
            tN = (a * e) - (b * d)
            if sN.negative? # sc < 0 => the s=0 edge is visible
              sN = 0
              tN = e
              tD = c
            elsif sN > sD # sc > 1 => the s=1 edge is visible
              sN = sD
              tN = e + b
              tD = c
            end
          end

          if tN.negative? # tc < 0 => the t=0 edge is visible
            tN = 0
            # recompute sc for this edge
            if (-d).negative?
              sN = 0.0
            elsif -d > a
              sN = sD
            else
              sN = -d
              sD = a
            end
          elsif tN > tD # tc > 1 => the t=1 edge is visible
            tN = tD
            # recompute sc for this edge
            if (-d + b).negative?
              sN = 0.0
            elsif (-d + b) > a
              sN = sD
            else
              sN = (-d + b)
              sD = a
            end
          end

          # finally do the division to get sc and tc
          sc = sN.abs < Float::EPSILON ? 0.0 : sN / sD
          tc = tN.abs < Float::EPSILON ? 0.0 : tN / tD

          # difference of the two closest points
          dP = w + (u * sc) - (v * tc)

          Math.sqrt(dP.dot(dP))
          # rubocop:enable Naming/VariableName
        end
      end

      private_constant :Rectangle
    end
  end
end

class SelectorQuery < < Queries::BaseQuery


def description(only_applied = false) # rubocop:disable Style/OptionalBooleanParameter
        desc = +''
        show_for = show_for_stage(only_applied)

        if show_for[:any]
          desc << 'visible ' if visible == :visible
          desc << 'non-visible ' if visible == :hidden
        end

        desc << label.to_s
        desc << " #{locator.inspect}" unless locator.nil?

        if show_for[:any]
          desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
          desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
        end

        desc << " with id #{options[:id]}" if options[:id]
        desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
        desc << ' that is focused' if options[:focused]
        desc << ' that is not focused' if options[:focused] == false

        desc << case options[:style]
        when String
          " with style attribute #{options[:style].inspect}"
        when Regexp
          " with style attribute matching #{options[:style].inspect}"
        when Hash
          " with styles #{options[:style].inspect}"
        else ''
        end

        %i[above below left_of right_of near].each do |spatial_filter|
          if options[spatial_filter] && show_for[:spatial]
            desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" # rubocop:disable Style/RescueModifier
          end
        end

        desc << selector.description(node_filters: show_for[:node], **options)

        desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]

        desc << " within #{@resolved_node.inspect}" if describe_within?
        if locator.is_a?(String) && locator.start_with?('#', './/', '//') && !selector.raw_locator?
          desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
                  "Please see the documentation for acceptable locator values.\n\n"
        end
        desc
      end


def matches_filters?(node, node_filter_errors = [])
        return true if (@resolved_node&.== node) && options[:allow_self]

        matches_locator_filter?(node) &&
          matches_system_filters?(node) &&
          matches_spatial_filters?(node) &&
          matches_node_filters?(node, node_filter_errors) &&
          matches_filter_block?(node)
      rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
        false
      end


def resolve_for(node, exact = nil)
        applied_filters.clear
        @filter_cache.clear
        @resolved_node = node
        @resolved_count += 1

        node.synchronize do
          children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
          Capybara::Result.new(ordered_results(children), self)
        end
      end


def xpath_text_conditions
        case (text = matching_text)
        when String
          text.split.map { |txt| XPath.contains(txt) }.reduce(&:&)
        when Regexp
          condition = XPath.current
          condition = condition.uppercase if text.casefold?
          Selector::RegexpDisassembler.new(text).alternated_substrings.map do |strs|
            strs.flat_map(&:split).map { |str| condition.contains(str) }.reduce(:&)
          end.reduce(:|)
        end
      end


def find_nodes_by_selector_format(node, exact)
        hints = {}
        hints[:uses_visibility] = true unless visible == :all
        hints[:texts] = text_fragments unless selector_format == :xpath
        hints[:styles] = options[:style] if use_default_style_filter?
        hints[:position] = true if use_spatial_filter?

        case selector_format
        when :css
          if node.method(:find_css).arity == 1
            node.find_css(css)
          else
            node.find_css(css, **hints)
          end
        when :xpath
          if node.method(:find_xpath).arity == 1
            node.find_xpath(xpath(exact))
          else
            node.find_xpath(xpath(exact), **hints)
          end
        else
          raise ArgumentError, "Unknown format: #{selector_format}"
        end
      end


def matches_node_filters?(node, errors)
        applied_filters << :node

        unapplied_options = options.keys - valid_keys
        @selector.with_filter_errors(errors) do
          node_filters.all? do |filter_name, filter|
            next true unless apply_filter?(filter)

            if filter.matcher?
              unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
                unapplied_options.delete(option_name)
                filter.matches?(node, option_name, options[option_name], @selector)
              end
            elsif options.key?(filter_name)
              unapplied_options.delete(filter_name)
              filter.matches?(node, filter_name, options[filter_name], @selector)
            elsif filter.default?
              filter.matches?(node, filter_name, filter.default, @selector)
            else
              true
            end
          end
        end
      end


def assert_valid_keys
        unless VALID_MATCH.include?(match)
          raise ArgumentError, "Invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
        end

        unhandled_options = @options.keys.reject do |option_name|
          valid_keys.include?(option_name) ||
            expression_filters.any? { |_name, ef| ef.handles_option? option_name } ||
            node_filters.any? { |_name, nf| nf.handles_option? option_name }
        end

        return if unhandled_options.empty?

        invalid_names = unhandled_options.map(&:inspect).join(', ')
        valid_names = (valid_keys - [:allow_self]).map(&:inspect).join(', ')
        raise ArgumentError, "Invalid option(s) #{invalid_names}, should be one of #{valid_names}"
      end


def apply_expression_filters(expression)
        unapplied_options = options.keys - valid_keys
        expression_filters.inject(expression) do |expr, (name, ef)|
          next expr unless apply_filter?(ef)

          if ef.matcher?
            unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
              unapplied_options.delete(option_name)
              ef.apply_filter(memo, option_name, options[option_name], @selector)
            end
          elsif options.key?(name)
            unapplied_options.delete(name)
            ef.apply_filter(expr, name, options[name], @selector)
          elsif ef.default?
            ef.apply_filter(expr, name, ef.default, @selector)
          else
            expr
          end
        end
      end


def matches_system_filters?(node)
        applied_filters << :system

        matches_visibility_filters?(node) &&
          matches_id_filter?(node) &&
          matches_class_filter?(node) &&
          matches_style_filter?(node) &&
          matches_focused_filter?(node) &&
          matches_text_filter?(node) &&
          matches_exact_text_filter?(node)
      end


def matches_spatial_filters?(node)
        applied_filters << :spatial
        return true unless use_spatial_filter?

        node_rect = Rectangle.new(node.initial_cache[:position] || node.rect)

        if options[:above]
          el_rect = rect_cache(options[:above])
          return false unless node_rect.above? el_rect
        end

        if options[:below]
          el_rect = rect_cache(options[:below])
          return false unless node_rect.below? el_rect
        end

        if options[:left_of]
          el_rect = rect_cache(options[:left_of])
          return false unless node_rect.left_of? el_rect
        end

        if options[:right_of]
          el_rect = rect_cache(options[:right_of])
          return false unless node_rect.right_of? el_rect
        end

        if options[:near]
          return false if node == options[:near]

          el_rect = rect_cache(options[:near])
          return false unless node_rect.near? el_rect
        end

        true
      end


def matches_class_filter?(node)
        return true unless use_default_class_filter? && need_to_process_classes?

        if options[:class].is_a? Regexp
          options[:class].match? node[:class]
        else
          classes = (node[:class] || '').split
          options[:class].select { |c| c.is_a? Regexp }.all? do |r|
            classes.any? { |cls| r.match? cls }
          end
        end
      end


def matches_style_filter?(node)
        case options[:style]
        when String, nil
          true
        when Regexp
          options[:style].match? node[:style]
        when Hash
          matches_style?(node, options[:style])
        end
      end


def matches_style?(node, styles)
        @actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
        styles.all? do |style, value|
          if value.is_a? Regexp
            value.match? @actual_styles[style.to_s]
          else
            @actual_styles[style.to_s] == value
          end
        end
      end


def matches_visibility_filters?(node)
        obscured = options[:obscured]
        return (visible != :hidden) && (node.initial_cache[:visible] != false) && !node.obscured? if obscured == false

        vis = case visible
        when :visible
          node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
        when :hidden
          # TODO: check why the 'visbile' cache spelling mistake wasn't caught in a test
          # (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
          (node.initial_cache[:visible] == false) || (node.initial_cache[:visible].nil? && !node.visible?)
        else
          true
        end

        vis && case obscured
               when true
                 node.obscured?
               when false
                 !node.obscured?
               else
                 true
               end
      end


def distance(other)
          distance = Float::INFINITY

          line_segments.each do |ls1|
            other.line_segments.each do |ls2|
              distance = [
                distance,
                distance_segment_segment(*ls1, *ls2)
              ].min
            end
          end

          distance
        end


def distance_segment_segment(l1p1, l1p2, l2p1, l2p2)
          # See http://geomalgorithms.com/a07-_distance.html
          # rubocop:disable Naming/VariableName
          u = l1p2 - l1p1
          v = l2p2 - l2p1
          w = l1p1 - l2p1

          a = u.dot u
          b = u.dot v
          c = v.dot v

          d = u.dot w
          e = v.dot w
          cap_d = (a * c) - (b**2)
          sD = tD = cap_d

          # compute the line parameters of the two closest points
          if cap_d < Float::EPSILON # the lines are almost parallel
            sN = 0.0 # force using point P0 on segment S1
            sD = 1.0 # to prevent possible division by 0.0 later
            tN = e
            tD = c
          else # get the closest points on the infinite lines
            sN = (b * e) - (c * d)
            tN = (a * e) - (b * d)
            if sN.negative? # sc < 0 => the s=0 edge is visible
              sN = 0
              tN = e
              tD = c
            elsif sN > sD # sc > 1 => the s=1 edge is visible
              sN = sD
              tN = e + b
              tD = c
            end
          end

          if tN.negative? # tc < 0 => the t=0 edge is visible
            tN = 0
            # recompute sc for this edge
            if (-d).negative?
              sN = 0.0
            elsif -d > a
              sN = sD
            else
              sN = -d
              sD = a
            end
          elsif tN > tD # tc > 1 => the t=1 edge is visible
            tN = tD
            # recompute sc for this edge
            if (-d + b).negative?
              sN = 0.0
            elsif (-d + b) > a
              sN = sD
            else
              sN = (-d + b)
              sD = a
            end
          end

          # finally do the division to get sc and tc
          sc = sN.abs < Float::EPSILON ? 0.0 : sN / sD
          tc = tN.abs < Float::EPSILON ? 0.0 : tN / tD

          # difference of the two closest points
          dP = w + (u * sc) - (v * tc)

          Math.sqrt(dP.dot(dP))
          # rubocop:enable Naming/VariableName
        end


# frozen_string_literal: true

module Capybara
  module Queries
    class SiblingQuery < SelectorQuery
      # @api private
      def resolve_for(node, exact = nil)
        @sibling_node = node
        node.synchronize do
          scope = node.respond_to?(:session) ? node.session.current_scope : node.find(:xpath, '/*')
          match_results = super(scope, exact)
          siblings = node.find_xpath((XPath.preceding_sibling + XPath.following_sibling).to_s)
                         .map(&method(:to_element))
                         .select { |el| match_results.include?(el) }
          Capybara::Result.new(ordered_results(siblings), self)
        end
      end

      def description(applied = false) # rubocop:disable Style/OptionalBooleanParameter
        desc = super
        sibling_query = @sibling_node&.instance_variable_get(:@query)
        desc += " that is a sibling of #{sibling_query.description}" if sibling_query
        desc
      end
    end
  end
end


# frozen_string_literal: true

module Capybara
  # @api private
  module Queries
    class StyleQuery < BaseQuery
      def initialize(expected_styles, session_options:, **options)
        @expected_styles = stringify_keys(expected_styles)
        @options = options
        @actual_styles = {}
        super(@options)
        self.session_options = session_options

        assert_valid_keys
      end

      def resolves_for?(node)
        @node = node
        @actual_styles = node.style(*@expected_styles.keys)
        @expected_styles.all? do |style, value|
          if value.is_a? Regexp
            value.match? @actual_styles[style]
          else
            @actual_styles[style] == value
          end
        end
      end

      def failure_message
        "Expected node to have styles #{@expected_styles.inspect}. " \
          "Actual styles were #{@actual_styles.inspect}"
      end

    private

      def stringify_keys(hsh)
        hsh.transform_keys(&:to_s)
      end

      def valid_keys
        %i[wait]
      end
    end
  end
end


# frozen_string_literal: true

module Capybara
  # @api private
  module Queries
    class TextQuery < BaseQuery
      def initialize(type = nil, expected_text, session_options:, **options) # rubocop:disable Style/OptionalArguments
        @type = type.nil? ? default_type : type
        raise ArgumentError, "#{@type} is not a valid type for a text query" unless valid_types.include?(@type)

        @options = options
        super(@options)
        self.session_options = session_options

        if expected_text.nil? && !exact?
          warn 'Checking for expected text of nil is confusing and/or pointless since it will always match. ' \
               "Please specify a string or regexp instead. #{Capybara::Helpers.filter_backtrace(caller)}"
        end

        @expected_text = expected_text.is_a?(Regexp) ? expected_text : expected_text.to_s

        @search_regexp = Capybara::Helpers.to_regexp(@expected_text, exact: exact?)

        assert_valid_keys
      end

      def resolve_for(node)
        @node = node
        @actual_text = text
        @count = @actual_text.scan(@search_regexp).size
      end

      def failure_message
        super << build_message(true)
      end

      def negative_failure_message
        super << build_message(false)
      end

      def description
        if @expected_text.is_a?(Regexp)
          "text matching #{@expected_text.inspect}"
        else
          "#{'exact ' if exact?}text #{@expected_text.inspect}"
        end
      end

    private

      def exact?
        options.fetch(:exact, session_options.exact_text)
      end

      def build_message(report_on_invisible)
        message = +''
        unless (COUNT_KEYS & @options.keys).empty?
          message << " but found #{@count} #{Capybara::Helpers.declension('time', 'times', @count)}"
        end
        message << " in #{@actual_text.inspect}"

        details_message = []
        details_message << case_insensitive_message if @node && check_case_insensitive?
        details_message << invisible_message if @node && check_visible_text? && report_on_invisible
        details_message.compact!

        message << ". (However, #{details_message.join(' and ')}.)" unless details_message.empty?
        message
      end

      def case_insensitive_message
        insensitive_regexp = Capybara::Helpers.to_regexp(@expected_text, options: Regexp::IGNORECASE)
        insensitive_count = @actual_text.scan(insensitive_regexp).size
        return if insensitive_count == @count

        "it was found #{occurrences insensitive_count} using a case insensitive search"
      end

      def invisible_message
        invisible_text = text(query_type: :all)
        invisible_count = invisible_text.scan(@search_regexp).size
        return if invisible_count == @count

        "it was found #{occurrences invisible_count} including non-visible text"
      rescue StandardError
        # An error getting the non-visible text (if element goes out of scope) should not affect the response
        nil
      end

      def valid_keys
        COUNT_KEYS + %i[wait exact normalize_ws]
      end

      def valid_types
        %i[all visible]
      end

      def check_visible_text?
        @type == :visible
      end

      def check_case_insensitive?
        !@expected_text.is_a?(Regexp)
      end

      def text(node: @node, query_type: @type)
        normalize_ws = options.fetch(:normalize_ws, session_options.default_normalize_ws)
        node.text(query_type, normalize_ws: normalize_ws)
      end

      def default_type
        Capybara.ignore_hidden_elements || Capybara.visible_text_only ? :visible : :all
      end
    end
  end
end


def build_message(report_on_invisible)
        message = +''
        unless (COUNT_KEYS & @options.keys).empty?
          message << " but found #{@count} #{Capybara::Helpers.declension('time', 'times', @count)}"
        end
        message << " in #{@actual_text.inspect}"

        details_message = []
        details_message << case_insensitive_message if @node && check_case_insensitive?
        details_message << invisible_message if @node && check_visible_text? && report_on_invisible
        details_message.compact!

        message << ". (However, #{details_message.join(' and ')}.)" unless details_message.empty?
        message
      end


# frozen_string_literal: true

class Capybara::RackTest::Browser
  include ::Rack::Test::Methods

  attr_reader :driver
  attr_accessor :current_host

  def initialize(driver)
    @driver = driver
    @current_fragment = nil
  end

  def app
    driver.app
  end

  def options
    driver.options
  end

  def visit(path, **attributes)
    @new_visit_request = true
    reset_cache!
    reset_host!
    process_and_follow_redirects(:get, path, attributes)
  end

  def refresh
    reset_cache!
    request(last_request.fullpath, last_request.env)
  end

  def submit(method, path, attributes, content_type: nil)
    path = request_path if path.nil? || path.empty?
    uri = build_uri(path)
    uri.query = '' if method.to_s.casecmp('get').zero?
    env = { 'HTTP_REFERER' => referer_url }
    env['CONTENT_TYPE'] = content_type if content_type
    process_and_follow_redirects(
      method,
      uri.to_s,
      attributes,
      env
    )
  end

  def follow(method, path, **attributes)
    return if fragment_or_script?(path)

    process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => referer_url)
  end

  def process_and_follow_redirects(method, path, attributes = {}, env = {})
    @current_fragment = build_uri(path).fragment
    process(method, path, attributes, env)
    return unless driver.follow_redirects?

    driver.redirect_limit.times do
      if last_response.redirect?
        if [307, 308].include? last_response.status
          process(last_request.request_method, last_response['Location'], last_request.params, env)
        else
          process(:get, last_response['Location'], {}, env)
        end
      end
    end

    if last_response.redirect? # rubocop:disable Style/GuardClause
      raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects."
    end
  end

  def process(method, path, attributes = {}, env = {})
    method = method.downcase
    new_uri = build_uri(path)
    @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
    @current_fragment = new_uri.fragment || @current_fragment
    reset_cache!
    @new_visit_request = false
    send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
  end

  def build_uri(path)
    uri = URI.parse(path)
    base_uri = base_relative_uri_for(uri)

    uri.path = base_uri.path + uri.path unless uri.absolute? || uri.path.start_with?('/')

    if base_uri.absolute?
      base_uri.merge(uri)
    else
      uri.scheme ||= @current_scheme
      uri.host ||= @current_host
      uri.port ||= @current_port unless uri.default_port == @current_port
      uri
    end
  end

  def current_url
    uri = build_uri(last_request.url)
    uri.fragment = @current_fragment if @current_fragment
    uri.to_s
  rescue Rack::Test::Error
    ''
  end

  def reset_host!
    uri = URI.parse(driver.session_options.app_host || driver.session_options.default_host)
    @current_scheme, @current_host, @current_port = uri.select(:scheme, :host, :port)
  end

  def reset_cache!
    @dom = nil
  end

  def dom
    @dom ||= Capybara::HTML(html)
  end

  def find(format, selector)
    if format == :css
      dom.css(selector, Capybara::RackTest::CSSHandlers.new)
    else
      dom.xpath(selector)
    end.map { |node| Capybara::RackTest::Node.new(self, node) }
  end

  def html
    last_response.body
  rescue Rack::Test::Error
    ''
  end

  def title
    dom.title
  end

  def last_request
    raise Rack::Test::Error if @new_visit_request

    super
  end

  def last_response
    raise Rack::Test::Error if @new_visit_request

    super
  end

protected

  def base_href
    find(:css, 'head > base').first&.[](:href).to_s
  end

  def base_relative_uri_for(uri)
    base_uri = URI.parse(base_href)
    current_uri = URI.parse(safe_last_request&.url.to_s).tap do |c|
      c.path.sub!(%r{/[^/]*$}, '/') unless uri.path.empty?
      c.path = '/' if c.path.empty?
    end

    if [current_uri, base_uri].any?(&:absolute?)
      current_uri.merge(base_uri)
    else
      base_uri.path = current_uri.path if base_uri.path.empty?
      base_uri
    end
  end

  def build_rack_mock_session
    reset_host! unless current_host
    Rack::MockSession.new(app, current_host)
  end

  def request_path
    last_request.path
  rescue Rack::Test::Error
    '/'
  end

  def safe_last_request
    last_request
  rescue Rack::Test::Error
    nil
  end

private

  def fragment_or_script?(path)
    path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
  end

  def referer_url
    build_uri(last_request.url).to_s
  rescue Rack::Test::Error
    ''
  end
end


def process_and_follow_redirects(method, path, attributes = {}, env = {})
    @current_fragment = build_uri(path).fragment
    process(method, path, attributes, env)
    return unless driver.follow_redirects?

    driver.redirect_limit.times do
      if last_response.redirect?
        if [307, 308].include? last_response.status
          process(last_request.request_method, last_response['Location'], last_request.params, env)
        else
          process(:get, last_response['Location'], {}, env)
        end
      end
    end

    if last_response.redirect? # rubocop:disable Style/GuardClause
      raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects."
    end
  end


def build_uri(path)
    uri = URI.parse(path)
    base_uri = base_relative_uri_for(uri)

    uri.path = base_uri.path + uri.path unless uri.absolute? || uri.path.start_with?('/')

    if base_uri.absolute?
      base_uri.merge(uri)
    else
      uri.scheme ||= @current_scheme
      uri.host ||= @current_host
      uri.port ||= @current_port unless uri.default_port == @current_port
      uri
    end
  end


def base_relative_uri_for(uri)
    base_uri = URI.parse(base_href)
    current_uri = URI.parse(safe_last_request&.url.to_s).tap do |c|
      c.path.sub!(%r{/[^/]*$}, '/') unless uri.path.empty?
      c.path = '/' if c.path.empty?
    end

    if [current_uri, base_uri].any?(&:absolute?)
      current_uri.merge(base_uri)
    else
      base_uri.path = current_uri.path if base_uri.path.empty?
      base_uri
    end
  end


# frozen_string_literal: true

class Capybara::RackTest::Form < Capybara::RackTest::Node
  # This only needs to inherit from Rack::Test::UploadedFile because Rack::Test checks for
  # the class specifically when determining whether to construct the request as multipart.
  # That check should be based solely on the form element's 'enctype' attribute value,
  # which should probably be provided to Rack::Test in its non-GET request methods.
  class NilUploadedFile < Rack::Test::UploadedFile
    def initialize # rubocop:disable Lint/MissingSuper
      @empty_file = Tempfile.new('nil_uploaded_file')
      @empty_file.close
    end

    def original_filename; ''; end
    def content_type; 'application/octet-stream'; end
    def path; @empty_file.path; end
    def size; 0; end
    def read; ''; end
    def append_to(_); end
    def set_encoding(_); end # rubocop:disable Naming/AccessorMethodName
  end

  def params(button)
    form_element_types = %i[input select textarea button]
    form_elements_xpath = XPath.generate do |xp|
      xpath = xp.descendant(*form_element_types).where(!xp.attr(:form))
      xpath += xp.anywhere(*form_element_types).where(xp.attr(:form) == native[:id]) if native[:id]
      xpath.where(!xp.attr(:disabled))
    end.to_s

    form_elements = native.xpath(form_elements_xpath).reject { |el| submitter?(el) && (el != button.native) }

    form_params = form_elements.each_with_object({}.compare_by_identity) do |field, params|
      case field.name
      when 'input', 'button' then add_input_param(field, params)
      when 'select' then add_select_param(field, params)
      when 'textarea' then add_textarea_param(field, params)
      end
    end

    form_params.each_with_object(make_params) do |(name, value), params|
      merge_param!(params, name, value)
    end.to_params_hash

    # form_elements.each_with_object(make_params) do |field, params|
    #   case field.name
    #   when 'input', 'button' then add_input_param(field, params)
    #   when 'select' then add_select_param(field, params)
    #   when 'textarea' then add_textarea_param(field, params)
    #   end
    # end.to_params_hash
  end

  def submit(button)
    action = button&.[]('formaction') || native['action']
    method = button&.[]('formmethod') || request_method
    driver.submit(method, action.to_s, params(button), content_type: native['enctype'])
  end

  def multipart?
    self[:enctype] == 'multipart/form-data'
  end

private

  class ParamsHash < Hash
    def to_params_hash
      self
    end
  end

  def request_method
    /post/i.match?(self[:method] || '') ? :post : :get
  end

  def merge_param!(params, key, value)
    key = key.to_s
    if Rack::Utils.respond_to?(:default_query_parser)
      Rack::Utils.default_query_parser.normalize_params(params, key, value, Rack::Utils.param_depth_limit)
    else
      Rack::Utils.normalize_params(params, key, value)
    end
  end

  def make_params
    if Rack::Utils.respond_to?(:default_query_parser)
      Rack::Utils.default_query_parser.make_params
    else
      ParamsHash.new
    end
  end

  def add_input_param(field, params)
    name, value = field['name'].to_s, field['value'].to_s
    return if name.empty?

    value = case field['type']
    when 'radio', 'checkbox'
      return unless field['checked']

      Capybara::RackTest::Node.new(driver, field).value.to_s
    when 'file'
      return if value.empty? && params.keys.include?(name) && Rack::Test::VERSION.to_f >= 2.0 # rubocop:disable Performance/InefficientHashSearch

      if multipart?
        file_to_upload(value)
      else
        File.basename(value)
      end
    else
      value
    end
    # merge_param!(params, name, value)
    params[name] = value
  end

  def file_to_upload(filename)
    if filename.empty?
      NilUploadedFile.new
    else
      mime_info = MiniMime.lookup_by_filename(filename)
      Rack::Test::UploadedFile.new(filename, mime_info&.content_type&.to_s)
    end
  end

  def add_select_param(field, params)
    name = field['name']
    if field.has_attribute?('multiple')
      value = field.xpath('.//option[@selected]').map do |option|
        # merge_param!(params, field['name'], (option['value'] || option.text).to_s)
        (option['value'] || option.text).to_s
      end
      params[name] = value unless value.empty?
    else
      option = field.xpath('.//option[@selected]').first || field.xpath('.//option').first
      # merge_param!(params, field['name'], (option['value'] || option.text).to_s) if option
      params[name] = (option['value'] || option.text).to_s if option
    end
  end

  def add_textarea_param(field, params)
    # merge_param!(params, field['name'], field['_capybara_raw_value'].to_s.gsub(/\r?\n/, "\r\n"))
    params[field['name']] = field['_capybara_raw_value'].to_s.gsub(/\r?\n/, "\r\n")
  end

  def submitter?(el)
    (%w[submit image].include? el['type']) || (el.name == 'button')
  end
end

class Capybara::RackTest::Form < < Capybara::RackTest::Node


def add_input_param(field, params)
    name, value = field['name'].to_s, field['value'].to_s
    return if name.empty?

    value = case field['type']
    when 'radio', 'checkbox'
      return unless field['checked']

      Capybara::RackTest::Node.new(driver, field).value.to_s
    when 'file'
      return if value.empty? && params.keys.include?(name) && Rack::Test::VERSION.to_f >= 2.0 # rubocop:disable Performance/InefficientHashSearch

      if multipart?
        file_to_upload(value)
      else
        File.basename(value)
      end
    else
      value
    end
    # merge_param!(params, name, value)
    params[name] = value
  end


def add_select_param(field, params)
    name = field['name']
    if field.has_attribute?('multiple')
      value = field.xpath('.//option[@selected]').map do |option|
        # merge_param!(params, field['name'], (option['value'] || option.text).to_s)
        (option['value'] || option.text).to_s
      end
      params[name] = value unless value.empty?
    else
      option = field.xpath('.//option[@selected]').first || field.xpath('.//option').first
      # merge_param!(params, field['name'], (option['value'] || option.text).to_s) if option
      params[name] = (option['value'] || option.text).to_s if option
    end
  end


# frozen_string_literal: true

require 'capybara/rack_test/errors'
require 'capybara/node/whitespace_normalizer'

class Capybara::RackTest::Node < Capybara::Driver::Node
  include Capybara::Node::WhitespaceNormalizer

  BLOCK_ELEMENTS = %w[p h1 h2 h3 h4 h5 h6 ol ul pre address blockquote dl div fieldset form hr noscript table].freeze

  def all_text
    normalize_spacing(native.text)
  end

  def visible_text
    normalize_visible_spacing(displayed_text)
  end

  def [](name)
    string_node[name]
  end

  def style(_styles)
    raise NotImplementedError, 'The rack_test driver does not process CSS'
  end

  def value
    string_node.value
  end

  def set(value, **options)
    return if disabled? || readonly?

    warn "Options passed to Node#set but the RackTest driver doesn't support any - ignoring" unless options.empty?

    if value.is_a?(Array) && !multiple?
      raise TypeError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
    end

    if radio? then set_radio(value)
    elsif checkbox? then set_checkbox(value)
    elsif range? then set_range(value)
    elsif input_field? then set_input(value)
    elsif textarea? then native['_capybara_raw_value'] = value.to_s
    end
  end

  def select_option
    return if disabled?

    deselect_options unless select_node.multiple?
    native['selected'] = 'selected'
  end

  def unselect_option
    raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?

    native.remove_attribute('selected')
  end

  def click(keys = [], **options)
    options.delete(:offset)
    raise ArgumentError, 'The RackTest driver does not support click options' unless keys.empty? && options.empty?

    if link?
      follow_link
    elsif submits?
      associated_form = form
      Capybara::RackTest::Form.new(driver, associated_form).submit(self) if associated_form
    elsif checkable?
      set(!checked?)
    elsif tag_name == 'label'
      click_label
    elsif (details = native.xpath('.//ancestor-or-self::details').last)
      toggle_details(details)
    end
  end

  def tag_name
    native.node_name
  end

  def visible?
    string_node.visible?
  end

  def checked?
    string_node.checked?
  end

  def selected?
    string_node.selected?
  end

  def disabled?
    return true if string_node.disabled?

    if %w[option optgroup].include? tag_name
      find_xpath(OPTION_OWNER_XPATH)[0].disabled?
    else
      !find_xpath(DISABLED_BY_FIELDSET_XPATH).empty?
    end
  end

  def readonly?
    # readonly attribute not valid on these input types
    return false if input_field? && %w[hidden range color checkbox radio file submit image reset button].include?(type)

    super
  end

  def path
    native.path
  end

  def find_xpath(locator, **_hints)
    native.xpath(locator).map { |el| self.class.new(driver, el) }
  end

  def find_css(locator, **_hints)
    native.css(locator, Capybara::RackTest::CSSHandlers.new).map { |el| self.class.new(driver, el) }
  end

  public_instance_methods(false).each do |meth_name|
    alias_method "unchecked_#{meth_name}", meth_name
    private "unchecked_#{meth_name}" # rubocop:disable Style/AccessModifierDeclarations

    class_eval <<~METHOD, __FILE__, __LINE__ + 1
      def #{meth_name}(...)
        stale_check
        method(:"unchecked_#{meth_name}").call(...)
      end
    METHOD
  end

protected

  # @api private
  def displayed_text(check_ancestor: true)
    if !string_node.visible?(check_ancestor)
      ''
    elsif native.text?
      native
        .text
        .delete(REMOVED_CHARACTERS)
        .tr(SQUEEZED_SPACES, ' ')
        .squeeze(' ')
    elsif native.element?
      text = native.children.map do |child|
        Capybara::RackTest::Node.new(driver, child).displayed_text(check_ancestor: false)
      end.join || ''
      text = "\n#{text}\n" if BLOCK_ELEMENTS.include?(tag_name)
      text
    else # rubocop:disable Lint/DuplicateBranch
      ''
    end
  end

private

  def stale_check
    raise Capybara::RackTest::Errors::StaleElementReferenceError unless native.document == driver.dom
  end

  def deselect_options
    select_node.find_xpath('.//option[@selected]').each { |node| node.native.remove_attribute('selected') }
  end

  def string_node
    @string_node ||= Capybara::Node::Simple.new(native)
  end

  # a reference to the select node if this is an option node
  def select_node
    find_xpath('./ancestor::select[1]').first
  end

  def type
    native[:type]
  end

  def form
    if native[:form]
      native.xpath("//form[@id='#{native[:form]}']")
    else
      native.ancestors('form')
    end.first
  end

  def set_radio(_value) # rubocop:disable Naming/AccessorMethodName
    other_radios_xpath = XPath.generate { |xp| xp.anywhere(:input)[xp.attr(:name) == self[:name]] }.to_s
    driver.dom.xpath(other_radios_xpath).each { |node| node.remove_attribute('checked') }
    native['checked'] = 'checked'
  end

  def set_checkbox(value) # rubocop:disable Naming/AccessorMethodName
    if value && !native['checked']
      native['checked'] = 'checked'
    elsif !value && native['checked']
      native.remove_attribute('checked')
    end
  end

  def set_range(value) # rubocop:disable Naming/AccessorMethodName
    min, max, step = (native['min'] || 0).to_f, (native['max'] || 100).to_f, (native['step'] || 1).to_f
    value = value.to_f
    value = value.clamp(min, max)
    value = (((value - min) / step).round * step) + min
    native['value'] = value.clamp(min, max)
  end

  def set_input(value) # rubocop:disable Naming/AccessorMethodName
    if text_or_password? && attribute_is_not_blank?(:maxlength)
      # Browser behavior for maxlength="0" is inconsistent, so we stick with
      # Firefox, allowing no input
      value = value.to_s[0...self[:maxlength].to_i]
    end
    if value.is_a?(Array) # Assert multiple attribute is present
      value.each do |val|
        new_native = native.clone
        new_native.remove_attribute('value')
        native.add_next_sibling(new_native)
        new_native['value'] = val.to_s
      end
      native.remove
    else
      value.to_s.tap do |set_value|
        if set_value.end_with?("\n") && (form&.css('input, textarea')&.count == 1) # rubocop:disable Style/CollectionQuerying
          native['value'] = set_value.to_s.chop
          Capybara::RackTest::Form.new(driver, form).submit(self)
        else
          native['value'] = set_value
        end
      end
    end
  end

  def attribute_is_not_blank?(attribute)
    self[attribute] && !self[attribute].empty?
  end

  def follow_link
    method = self['data-method'] || self['data-turbo-method'] if driver.options[:respect_data_method]
    method ||= :get
    driver.follow(method, self[:href].to_s)
  end

  def click_label
    labelled_control = if native[:for]
      find_xpath("//input[@id='#{native[:for]}']")
    else
      find_xpath('.//input')
    end.first

    labelled_control.set(!labelled_control.checked?) if checkbox_or_radio?(labelled_control)
  end

  def toggle_details(details = nil)
    details ||= native.xpath('.//ancestor-or-self::details').last
    return unless details

    if details.has_attribute?('open')
      details.remove_attribute('open')
    else
      details.set_attribute('open', 'open')
    end
  end

  def link?
    tag_name == 'a' && !self[:href].nil?
  end

  def submits?
    (tag_name == 'input' && %w[submit image].include?(type)) || (tag_name == 'button' && [nil, 'submit'].include?(type))
  end

  def checkable?
    tag_name == 'input' && %w[checkbox radio].include?(type)
  end

protected

  def checkbox_or_radio?(field = self)
    field&.checkbox? || field&.radio?
  end

  def checkbox?
    input_field? && type == 'checkbox'
  end

  def radio?
    input_field? && type == 'radio'
  end

  def text_or_password?
    input_field? && %w[text password].include?(type)
  end

  def input_field?
    tag_name == 'input'
  end

  def textarea?
    tag_name == 'textarea'
  end

  def range?
    input_field? && type == 'range'
  end

  OPTION_OWNER_XPATH = XPath.parent(:optgroup, :select, :datalist).to_s.freeze
  DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
    x.parent(:fieldset)[
      XPath.attr(:disabled)
    ] + x.ancestor[
      ~x.self(:legend) |
      x.preceding_sibling(:legend)
    ][
      x.parent(:fieldset)[
        x.attr(:disabled)
      ]
    ]
  end.to_s.freeze
end

class Capybara::RackTest::Node < < Capybara::Driver::Node


def click(keys = [], **options)
    options.delete(:offset)
    raise ArgumentError, 'The RackTest driver does not support click options' unless keys.empty? && options.empty?

    if link?
      follow_link
    elsif submits?
      associated_form = form
      Capybara::RackTest::Form.new(driver, associated_form).submit(self) if associated_form
    elsif checkable?
      set(!checked?)
    elsif tag_name == 'label'
      click_label
    elsif (details = native.xpath('.//ancestor-or-self::details').last)
      toggle_details(details)
    end
  end


def displayed_text(check_ancestor: true)
    if !string_node.visible?(check_ancestor)
      ''
    elsif native.text?
      native
        .text
        .delete(REMOVED_CHARACTERS)
        .tr(SQUEEZED_SPACES, ' ')
        .squeeze(' ')
    elsif native.element?
      text = native.children.map do |child|
        Capybara::RackTest::Node.new(driver, child).displayed_text(check_ancestor: false)
      end.join || ''
      text = "\n#{text}\n" if BLOCK_ELEMENTS.include?(tag_name)
      text
    else # rubocop:disable Lint/DuplicateBranch
      ''
    end
  end


def set_input(value) # rubocop:disable Naming/AccessorMethodName
    if text_or_password? && attribute_is_not_blank?(:maxlength)
      # Browser behavior for maxlength="0" is inconsistent, so we stick with
      # Firefox, allowing no input
      value = value.to_s[0...self[:maxlength].to_i]
    end
    if value.is_a?(Array) # Assert multiple attribute is present
      value.each do |val|
        new_native = native.clone
        new_native.remove_attribute('value')
        native.add_next_sibling(new_native)
        new_native['value'] = val.to_s
      end
      native.remove
    else
      value.to_s.tap do |set_value|
        if set_value.end_with?("\n") && (form&.css('input, textarea')&.count == 1) # rubocop:disable Style/CollectionQuerying
          native['value'] = set_value.to_s.chop
          Capybara::RackTest::Form.new(driver, form).submit(self)
        else
          native['value'] = set_value
        end
      end
    end
  end


# frozen_string_literal: true

require 'capybara/selector/selector'

module Capybara
  class Selector
    class CSS
      def self.escape(str)
        value = str.dup
        out = +''
        out << value.slice!(0...1) if value.match?(/^[-_]/)
        out << (value[0].match?(NMSTART) ? value.slice!(0...1) : escape_char(value.slice!(0...1)))
        out << value.gsub(/[^a-zA-Z0-9_-]/) { |char| escape_char char }
        out
      end

      def self.escape_char(char)
        char.match?(%r{[ -/:-~]}) ? "\\#{char}" : format('\\%06<hex>x', hex: char.ord)
      end

      def self.split(css)
        Splitter.new.split(css)
      end

      S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}'
      H = /[0-9a-fA-F]/
      UNICODE  = /\\#{H}{1,6}[ \t\r\n\f]?/
      NONASCII = /[#{S}]/
      ESCAPE   = /#{UNICODE}|\\[ -~#{S}]/
      NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/

      class Splitter
        def split(css)
          selectors = []
          StringIO.open(css.to_s) do |str|
            selector = +''
            while (char = str.getc)
              case char
              when '['
                selector << parse_square(str)
              when '('
                selector << parse_paren(str)
              when '"', "'"
                selector << parse_string(char, str)
              when '\\'
                selector << (char + str.getc)
              when ','
                selectors << selector.strip
                selector.clear
              else
                selector << char
              end
            end
            selectors << selector.strip
          end
          selectors
        end

      private

        def parse_square(strio)
          parse_block('[', ']', strio)
        end

        def parse_paren(strio)
          parse_block('(', ')', strio)
        end

        def parse_block(start, final, strio)
          block = start
          while (char = strio.getc)
            case char
            when final
              return block + char
            when '\\'
              block += char + strio.getc
            when '"', "'"
              block += parse_string(char, strio)
            else
              block += char
            end
          end
          raise ArgumentError, "Invalid CSS Selector - Block end '#{final}' not found"
        end

        def parse_string(quote, strio)
          string = quote
          while (char = strio.getc)
            string += char
            case char
            when quote
              return string
            when '\\'
              string += strio.getc
            end
          end
          raise ArgumentError, 'Invalid CSS Selector - string end not found'
        end
      end
    end
  end
end


def parse_block(start, final, strio)
          block = start
          while (char = strio.getc)
            case char
            when final
              return block + char
            when '\\'
              block += char + strio.getc
            when '"', "'"
              block += parse_string(char, strio)
            else
              block += char
            end
          end
          raise ArgumentError, "Invalid CSS Selector - Block end '#{final}' not found"
        end


def parse_string(quote, strio)
          string = quote
          while (char = strio.getc)
            string += char
            case char
            when quote
              return string
            when '\\'
              string += strio.getc
            end
          end
          raise ArgumentError, 'Invalid CSS Selector - string end not found'
        end


# frozen_string_literal: true

require 'capybara/selector/filter_set'
require 'capybara/selector/css'
require 'capybara/selector/regexp_disassembler'
require 'capybara/selector/builders/xpath_builder'
require 'capybara/selector/builders/css_builder'

module Capybara
  class Selector
    class Definition
      attr_reader :name, :expressions

      extend Forwardable

      def initialize(name, locator_type: nil, raw_locator: false, supports_exact: nil, &block)
        @name = name
        @filter_set = Capybara::Selector::FilterSet.add(name)
        @match = nil
        @label = nil
        @failure_message = nil
        @expressions = {}
        @expression_filters = {}
        @locator_filter = nil
        @default_visibility = nil
        @locator_type = locator_type
        @raw_locator = raw_locator
        @supports_exact = supports_exact
        instance_eval(&block)
      end

      def custom_filters
        warn "Deprecated: Selector#custom_filters is not valid when same named expression and node filter exist - don't use"
        node_filters.merge(expression_filters).freeze
      end

      def node_filters
        @filter_set.node_filters
      end

      def expression_filters
        @filter_set.expression_filters
      end

      ##
      #
      # Define a selector by an xpath expression
      #
      # @overload xpath(*expression_filters, &block)
      #   @param [Array<Symbol>] expression_filters ([])  Names of filters that are implemented via this expression, if not specified the names of any keyword parameters in the block will be used
      #   @yield [locator, options]                       The block to use to generate the XPath expression
      #   @yieldparam [String] locator                    The locator string passed to the query
      #   @yieldparam [Hash] options                      The options hash passed to the query
      #   @yieldreturn [#to_xpath, #to_s]                 An object that can produce an xpath expression
      #
      # @overload xpath()
      # @return [#call]                             The block that will be called to generate the XPath expression
      #
      def xpath(*allowed_filters, &block)
        expression(:xpath, allowed_filters, &block)
      end

      ##
      #
      # Define a selector by a CSS selector
      #
      # @overload css(*expression_filters, &block)
      #   @param [Array<Symbol>] expression_filters ([])  Names of filters that can be implemented via this CSS selector
      #   @yield [locator, options]                   The block to use to generate the CSS selector
      #   @yieldparam [String] locator               The locator string passed to the query
      #   @yieldparam [Hash] options                 The options hash passed to the query
      #   @yieldreturn [#to_s]                        An object that can produce a CSS selector
      #
      # @overload css()
      # @return [#call]                             The block that will be called to generate the CSS selector
      #
      def css(*allowed_filters, &block)
        expression(:css, allowed_filters, &block)
      end

      ##
      #
      # Automatic selector detection
      #
      # @yield [locator]                   This block takes the passed in locator string and returns whether or not it matches the selector
      # @yieldparam [String], locator      The locator string used to determine if it matches the selector
      # @yieldreturn [Boolean]             Whether this selector matches the locator string
      # @return [#call]                    The block that will be used to detect selector match
      #
      def match(&block)
        @match = block if block
        @match
      end

      ##
      #
      # Set/get a descriptive label for the selector
      #
      # @overload label(label)
      #   @param [String] label            A descriptive label for this selector - used in error messages
      # @overload label()
      # @return [String]                 The currently set label
      #
      def label(label = nil)
        @label = label if label
        @label
      end

      ##
      #
      # Description of the selector
      #
      # @!method description(options)
      #   @param [Hash] options            The options of the query used to generate the description
      #   @return [String]                 Description of the selector when used with the options passed
      def_delegator :@filter_set, :description

      ##
      #
      #  Should this selector be used for the passed in locator
      #
      #  This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
      #
      # @param [String] locator     The locator passed to the query
      # @return [Boolean]           Whether or not to use this selector
      #
      def match?(locator)
        @match&.call(locator)
      end

      ##
      #
      # Define a node filter for use with this selector
      #
      # @!method node_filter(name, *types, options={}, &block)
      #   @param [Symbol, Regexp] name            The filter name
      #   @param [Array<Symbol>] types    The types of the filter - currently valid types are [:boolean]
      #   @param [Hash] options ({})      Options of the filter
      #   @option options [Array<>] :valid_values Valid values for this filter
      #   @option options :default        The default value of the filter (if any)
      #   @option options :skip_if        Value of the filter that will cause it to be skipped
      #   @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter.  If not provided the filter will be used for options matching the filter name.
      #
      # If a Symbol is passed for the name the block should accept | node, option_value |, while if a Regexp
      # is passed for the name the block should accept | node, option_name, option_value |. In either case
      # the block should return `true` if the node passes the filer or `false` if it doesn't

      ##
      #
      # Define an expression filter for use with this selector
      #
      # @!method expression_filter(name, *types, matcher: nil, **options, &block)
      #   @param [Symbol, Regexp] name            The filter name
      #   @param [Regexp] matcher (nil)   A Regexp used to check whether a specific option is handled by this filter
      #   @param [Array<Symbol>] types    The types of the filter - currently valid types are [:boolean]
      #   @param [Hash] options ({})      Options of the filter
      #   @option options [Array<>] :valid_values Valid values for this filter
      #   @option options :default        The default value of the filter (if any)
      #   @option options :skip_if        Value of the filter that will cause it to be skipped
      #   @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter.  If not provided the filter will be used for options matching the filter name.
      #
      # If a Symbol is passed for the name the block should accept | current_expression, option_value |, while if a Regexp
      # is passed for the name the block should accept | current_expression, option_name, option_value |. In either case
      # the block should return the modified expression

      def_delegators :@filter_set, :node_filter, :expression_filter, :filter

      def locator_filter(*types, **options, &block)
        types.each { |type| options[type] = true }
        @locator_filter = Capybara::Selector::Filters::LocatorFilter.new(block, **options) if block
        @locator_filter
      end

      def filter_set(name, filters_to_use = nil)
        @filter_set.import(name, filters_to_use)
      end

      def_delegator :@filter_set, :describe

      def describe_expression_filters(&block)
        if block
          describe(:expression_filters, &block)
        else
          describe(:expression_filters) do |**options|
            describe_all_expression_filters(**options)
          end
        end
      end

      def describe_all_expression_filters(**opts)
        expression_filters.map do |ef_name, ef|
          if ef.matcher?
            handled_custom_options(ef, opts).map { |option, value| " with #{ef_name}[#{option} => #{value}]" }.join
          elsif opts.key?(ef_name)
            " with #{ef_name} #{opts[ef_name]}"
          end
        end.join
      end

      def describe_node_filters(&block)
        describe(:node_filters, &block)
      end

      ##
      #
      # Set the default visibility mode that should be used if no visible option is passed when using the selector.
      # If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
      #
      # @param [Symbol] default_visibility  Only find elements with the specified visibility:
      #                                              * :all - finds visible and invisible elements.
      #                                              * :hidden - only finds invisible elements.
      #                                              * :visible - only finds visible elements.
      def visible(default_visibility = nil, &block)
        @default_visibility = block || default_visibility
      end

      def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
        vis = if @default_visibility.respond_to?(:call)
          @default_visibility.call(options)
        else
          @default_visibility
        end
        vis.nil? ? fallback : vis
      end

      # @api private
      def raw_locator?
        !!@raw_locator
      end

      # @api private
      def supports_exact?
        @supports_exact
      end

      def default_format
        return nil if @expressions.keys.empty?

        if @expressions.size == 1
          @expressions.keys.first
        else
          :xpath
        end
      end

      # @api private
      def locator_types
        return nil unless @locator_type

        Array(@locator_type)
      end

    private

      def handled_custom_options(filter, options)
        options.select do |option, _|
          filter.handles_option?(option) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(option)
        end
      end

      def parameter_names(block)
        key_types = %i[key keyreq]
        # user filter_map when we drop dupport for 2.6
        # block.parameters.select { |(type, _name)| key_types.include? type }.map { |(_, name)| name }
        block.parameters.filter_map { |(type, name)| name if key_types.include? type }
      end

      def expression(type, allowed_filters, &block)
        if block
          @expressions[type] = block
          allowed_filters = parameter_names(block) if allowed_filters.empty?
          allowed_filters.flatten.each do |ef|
            expression_filters[ef] = Capybara::Selector::Filters::IdentityExpressionFilter.new(ef)
          end
        end
        @expressions[type]
      end
    end
  end
end


def expression(type, allowed_filters, &block)
        if block
          @expressions[type] = block
          allowed_filters = parameter_names(block) if allowed_filters.empty?
          allowed_filters.flatten.each do |ef|
            expression_filters[ef] = Capybara::Selector::Filters::IdentityExpressionFilter.new(ef)
          end
        end
        @expressions[type]
      end


# frozen_string_literal: true

require 'capybara/selector/filter'

module Capybara
  class Selector
    class FilterSet
      attr_reader :node_filters, :expression_filters

      def initialize(name, &block)
        @name = name
        @node_filters = {}
        @expression_filters = {}
        @descriptions = Hash.new { |hsh, key| hsh[key] = [] }
        instance_eval(&block) if block
      end

      def node_filter(names, *types, **options, &block)
        Array(names).each do |name|
          add_filter(name, Filters::NodeFilter, *types, **options, &block)
        end
      end
      alias_method :filter, :node_filter

      def expression_filter(name, *types, **options, &block)
        add_filter(name, Filters::ExpressionFilter, *types, **options, &block)
      end

      def describe(what = nil, &block)
        case what
        when nil
          undeclared_descriptions.push block
        when :node_filters
          node_filter_descriptions.push block
        when :expression_filters
          expression_filter_descriptions.push block
        else
          raise ArgumentError, 'Unknown description type'
        end
      end

      def description(node_filters: true, expression_filters: true, **options)
        opts = options_with_defaults(options)
        description = +''
        description << undeclared_descriptions.map { |desc| desc.call(**opts).to_s }.join
        description << expression_filter_descriptions.map { |desc| desc.call(**opts).to_s }.join if expression_filters
        description << node_filter_descriptions.map { |desc| desc.call(**opts).to_s }.join if node_filters
        description
      end

      def descriptions
        Capybara::Helpers.warn 'DEPRECATED: FilterSet#descriptions is deprecated without replacement'
        [undeclared_descriptions, node_filter_descriptions, expression_filter_descriptions].flatten
      end

      def import(name, filters = nil)
        filter_selector = filters.nil? ? ->(*) { true } : ->(filter_name, _) { filters.include? filter_name }

        self.class[name].tap do |f_set|
          expression_filters.merge!(f_set.expression_filters.select(&filter_selector))
          node_filters.merge!(f_set.node_filters.select(&filter_selector))
          f_set.undeclared_descriptions.each { |desc| describe(&desc) }
          f_set.expression_filter_descriptions.each { |desc| describe(:expression_filters, &desc) }
          f_set.node_filter_descriptions.each { |desc| describe(:node_filters, &desc) }
        end
        self
      end

      class << self
        def all
          @filter_sets ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
        end

        def [](name)
          all.fetch(name.to_sym) { |set_name| raise ArgumentError, "Unknown filter set (:#{set_name})" }
        end

        def add(name, &block)
          all[name.to_sym] = FilterSet.new(name.to_sym, &block)
        end

        def remove(name)
          all.delete(name.to_sym)
        end
      end

    protected

      def undeclared_descriptions
        @descriptions[:undeclared]
      end

      def node_filter_descriptions
        @descriptions[:node_filters]
      end

      def expression_filter_descriptions
        @descriptions[:expression_filters]
      end

    private

      def options_with_defaults(options)
        expression_filters
          .chain(node_filters)
          .filter_map { |name, filter| [name, filter.default] if filter.default? }
          .to_h.merge!(options)
      end

      def add_filter(name, filter_class, *types, matcher: nil, **options, &block)
        types.each { |type| options[type] = true }
        if matcher && options[:default]
          raise 'ArgumentError', ':default option is not supported for filters with a :matcher option'
        end

        filter = filter_class.new(name, matcher, block, **options)
        (filter_class <= Filters::ExpressionFilter ? @expression_filters : @node_filters)[name] = filter
      end
    end
  end
end


def import(name, filters = nil)
        filter_selector = filters.nil? ? ->(*) { true } : ->(filter_name, _) { filters.include? filter_name }

        self.class[name].tap do |f_set|
          expression_filters.merge!(f_set.expression_filters.select(&filter_selector))
          node_filters.merge!(f_set.node_filters.select(&filter_selector))
          f_set.undeclared_descriptions.each { |desc| describe(&desc) }
          f_set.expression_filter_descriptions.each { |desc| describe(:expression_filters, &desc) }
          f_set.node_filter_descriptions.each { |desc| describe(:node_filters, &desc) }
        end
        self
      end


# frozen_string_literal: true

require 'regexp_parser'

module Capybara
  class Selector
    # @api private
    class RegexpDisassembler
      def initialize(regexp)
        @regexp = regexp
      end

      def alternated_substrings
        @alternated_substrings ||= begin
          or_strings = process(alternation: true)
          remove_or_covered(or_strings)
          or_strings.any?(&:empty?) ? [] : or_strings
        end
      end

      def substrings
        @substrings ||= begin
          strs = process(alternation: false).first
          remove_and_covered(strs)
        end
      end

    private

      def remove_and_covered(strings)
        # delete_if is documented to modify the array after every block iteration - this doesn't appear to be true
        # uniq the strings to prevent identical strings from removing each other
        strings.uniq!

        # If we have "ab" and "abcd" required - only need to check for "abcd"
        strings.delete_if do |sub_string|
          strings.any? do |cover_string|
            next if sub_string.equal? cover_string

            cover_string.include?(sub_string)
          end
        end
      end

      def remove_or_covered(or_series)
        # If we are going to match `("a" and "b") or ("ade" and "bce")` it only makes sense to match ("a" and "b")

        # Ensure minimum sets of strings are being or'd
        or_series.each { |strs| remove_and_covered(strs) }

        # Remove any of the alternated string series that fully contain any other string series
        or_series.delete_if do |and_strs|
          or_series.any? do |and_strs2|
            next if and_strs.equal? and_strs2

            remove_and_covered(and_strs + and_strs2) == and_strs
          end
        end
      end

      def process(alternation:)
        strs = extract_strings(Regexp::Parser.parse(@regexp), alternation: alternation)
        strs = collapse(combine(strs).map(&:flatten))
        strs.each { |str| str.map!(&:upcase) } if @regexp.casefold?
        strs
      end

      def combine(strs)
        suffixes = [[]]
        strs.reverse_each do |str|
          if str.is_a? Set
            prefixes = str.flat_map { |s| combine(s) }
            suffixes = prefixes.product(suffixes).map { |pair| pair.flatten(1) }
          else
            suffixes.each { |arr| arr.unshift str }
          end
        end
        suffixes
      end

      def collapse(strs)
        strs.map do |substrings|
          substrings.slice_before(&:nil?).map(&:join).reject(&:empty?).uniq
        end
      end

      def extract_strings(expression, alternation: false)
        Expression.new(expression).extract_strings(alternation)
      end

      # @api private
      class Expression
        def initialize(exp)
          @exp = exp
        end

        def extract_strings(process_alternatives)
          strings = []
          each do |exp|
            next if exp.ignore?

            next strings.push(nil) if exp.optional? && !process_alternatives

            next strings.push(exp.alternative_strings) if exp.alternation? && process_alternatives

            strings.concat(exp.strings(process_alternatives))
          end
          strings
        end

      protected

        def alternation?
          (type == :meta) && !terminal?
        end

        def optional?
          min_repeat.zero?
        end

        def terminal?
          @exp.terminal?
        end

        def strings(process_alternatives)
          if indeterminate?
            [nil]
          elsif terminal?
            terminal_strings
          elsif optional?
            optional_strings
          else
            repeated_strings(process_alternatives)
          end
        end

        def terminal_strings
          text = case @exp.type
          when :literal then @exp.text
          when :escape then @exp.char
          else
            return [nil]
          end

          optional? ? options_set(text) : repeat_set(text)
        end

        def optional_strings
          options_set(extract_strings(true))
        end

        def repeated_strings(process_alternatives)
          repeat_set extract_strings(process_alternatives)
        end

        def alternative_strings
          alts = alternatives.map { |sub_exp| sub_exp.extract_strings(alternation: true) }
          alts.all?(&:any?) ? Set.new(alts) : nil
        end

        def ignore?
          [Regexp::Expression::Assertion::NegativeLookahead,
           Regexp::Expression::Assertion::NegativeLookbehind].any? { |klass| @exp.is_a? klass }
        end

      private

        def indeterminate?
          %i[meta set].include?(type)
        end

        def min_repeat
          @exp.repetitions.begin
        end

        def max_repeat
          @exp.repetitions.end
        end

        def fixed_repeat?
          min_repeat == max_repeat
        end

        def type
          @exp.type
        end

        def repeat_set(str)
          strs = Array(str * min_repeat)
          strs.push(nil) unless fixed_repeat?
          strs
        end

        def options_set(strs)
          strs = [Set.new([[''], Array(strs)])]
          strs.push(nil) unless max_repeat == 1
          strs
        end

        def alternatives
          @exp.alternatives.map { |exp| Expression.new(exp) }
        end

        def each
          @exp.each { |exp| yield Expression.new(exp) }
        end
      end
      private_constant :Expression
    end
  end
end


def extract_strings(process_alternatives)
          strings = []
          each do |exp|
            next if exp.ignore?

            next strings.push(nil) if exp.optional? && !process_alternatives

            next strings.push(exp.alternative_strings) if exp.alternation? && process_alternatives

            strings.concat(exp.strings(process_alternatives))
          end
          strings
        end


def strings(process_alternatives)
          if indeterminate?
            [nil]
          elsif terminal?
            terminal_strings
          elsif optional?
            optional_strings
          else
            repeated_strings(process_alternatives)
          end
        end


# frozen_string_literal: true

module Capybara
  class Selector < SimpleDelegator
    class << self
      def all
        @definitions ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
      end

      def [](name)
        all.fetch(name.to_sym) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
      end

      def add(name, **options, &block)
        all[name.to_sym] = Definition.new(name.to_sym, **options, &block)
      end

      def update(name, &block)
        self[name].instance_eval(&block)
      end

      def remove(name)
        all.delete(name.to_sym)
      end

      def for(locator)
        all.values.find { |sel| sel.match?(locator) }
      end
    end

    attr_reader :errors

    def initialize(definition, config:, format:)
      definition = self.class[definition] unless definition.is_a? Definition
      super(definition)
      @definition = definition
      @config = config
      @format = format
      @errors = []
    end

    def format
      @format || @definition.default_format
    end
    alias_method :current_format, :format

    def enable_aria_label
      @config[:enable_aria_label]
    end

    def enable_aria_role
      @config[:enable_aria_role]
    end

    def test_id
      @config[:test_id]
    end

    def call(locator, **options)
      if format
        raise ArgumentError, "Selector #{@name} does not support #{format}" unless expressions.key?(format)

        instance_exec(locator, **options, &expressions[format])
      else
        warn 'Selector has no format'
      end
    ensure
      unless locator_valid?(locator)
        Capybara::Helpers.warn(
          "Locator #{locator.class}:#{locator.inspect} for selector #{name.inspect} must #{locator_description}. " \
          'This will raise an error in a future version of Capybara. ' \
          "Called from: #{Capybara::Helpers.filter_backtrace(caller)}"
        )
      end
    end

    def add_error(error_msg)
      errors << error_msg
    end

    def expression_for(name, locator, config: @config, format: current_format, **options)
      Selector.new(name, config: config, format: format).call(locator, **options)
    end

    # @api private
    def with_filter_errors(errors)
      old_errors = @errors
      @errors = errors
      yield
    ensure
      @errors = old_errors
    end

    # @api private
    def builder(expr = nil)
      case format
      when :css
        Capybara::Selector::CSSBuilder
      when :xpath
        Capybara::Selector::XPathBuilder
      else
        raise NotImplementedError, "No builder exists for selector of type #{default_format}"
      end.new(expr)
    end

  private

    def locator_description
      locator_types.group_by { |lt| lt.is_a? Symbol }.map do |symbol, types_or_methods|
        if symbol
          "respond to #{types_or_methods.join(' or ')}"
        else
          "be an instance of #{types_or_methods.join(' or ')}"
        end
      end.join(' or ')
    end

    def locator_valid?(locator)
      return true unless locator && locator_types

      locator_types&.any? do |type_or_method|
        type_or_method.is_a?(Symbol) ? locator.respond_to?(type_or_method) : type_or_method === locator # rubocop:disable Style/CaseEquality
      end
    end

    def locate_field(xpath, locator, **_options)
      return xpath if locator.nil?

      locate_xpath = xpath # Need to save original xpath for the label wrap
      locator = locator.to_s
      attr_matchers = [XPath.attr(:id) == locator,
                       XPath.attr(:name) == locator,
                       XPath.attr(:placeholder) == locator,
                       XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)].reduce(:|)
      attr_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
      attr_matchers |= XPath.attr(test_id) == locator if test_id

      locate_xpath = locate_xpath[attr_matchers]
      locate_xpath + locate_label(locator).descendant(xpath)
    end

    def locate_label(locator)
      XPath.descendant(:label)[XPath.string.n.is(locator)]
    end

    def find_by_attr(attribute, value)
      finder_name = "find_by_#{attribute}_attr"
      if respond_to?(finder_name, true)
        send(finder_name, value)
      else
        value ? XPath.attr(attribute) == value : nil
      end
    end

    def find_by_class_attr(classes)
      Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&)
    end
  end
end


def builder(expr = nil)
      case format
      when :css
        Capybara::Selector::CSSBuilder
      when :xpath
        Capybara::Selector::XPathBuilder
      else
        raise NotImplementedError, "No builder exists for selector of type #{default_format}"
      end.new(expr)
    end


def locate_field(xpath, locator, **_options)
      return xpath if locator.nil?

      locate_xpath = xpath # Need to save original xpath for the label wrap
      locator = locator.to_s
      attr_matchers = [XPath.attr(:id) == locator,
                       XPath.attr(:name) == locator,
                       XPath.attr(:placeholder) == locator,
                       XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)].reduce(:|)
      attr_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
      attr_matchers |= XPath.attr(test_id) == locator if test_id

      locate_xpath = locate_xpath[attr_matchers]
      locate_xpath + locate_label(locator).descendant(xpath)
    end