module Watir
  class RadioSet
    extend Forwardable
    include Watir::Exception
    include Enumerable

    delegate %i[exists? present? visible? browser] => :source

    attr_reader :source, :frame

    def initialize(query_scope, selector)
      raise ArgumentError, "invalid argument: #{selector.inspect}" unless selector.is_a? Hash

      @source = Radio.new(query_scope, selector)
      @frame = @source.parent(tag_name: :form)
    end

    #
    # Yields each Radio associated with this set.
    #
    # @example
    #   radio_set = browser.radio_set
    #   radio_set.each do |radio|
    #     puts radio.text
    #   end
    #
    # @yieldparam [Watir::RadioSet] element iterate through the radio buttons.
    #

    def each(&block)
      radios.each(&block)
    end

    #
    # Get the n'th radio button in this set
    #
    # @return Watir::Radio
    #

    def [](idx)
      radios[idx]
    end

    #
    # @return Watir::Radio
    #

    def radio(opt = {})
      n = name
      if !n.empty? && (!opt[:name] || opt[:name] == n)
        frame.radio(opt.merge(name: n))
      elsif n.empty?
        return source
      else
        raise Watir::Exception::UnknownObjectException, "#{opt[:name]} does not match name of RadioSet: #{n}"
      end
    end

    #
    # @return Watir::RadioCollection
    #

    def radios(opt = {})
      n = name
      if !n.empty? && (!opt[:name] || opt[:name] == n)
        element_call(:wait_for_present) { frame.radios(opt.merge(name: n)) }
      elsif n.empty?
        Watir::RadioCollection.new(frame, element: source.wd)
      else
        raise Watir::Exception::UnknownObjectException, "#{opt[:name]} does not match name of RadioSet: #{n}"
      end
    end

    #
    # Returns true if any radio buttons in the set are enabled.
    #
    # @return [Boolean]
    #

    def enabled?
      any?(&:enabled?)
    end

    #
    # Returns true if all radio buttons in the set are disabled.
    #
    # @return [Boolean]
    #

    def disabled?
      !enabled?
    end

    #
    # Returns the name attribute for the set.
    #
    # @return [String]
    #

    def name
      @name ||= source.name
    end

    #
    # If RadioSet exists, this always returns 'radio'.
    #
    # @return [String]
    #

    def type
      assert_exists
      'radio'
    end

    #
    # Returns true if the radio set has one or more radio buttons where label matches the given value.
    #
    # @param [String, Regexp] str_or_rx
    # @return [Boolean]
    #

    def include?(str_or_rx)
      radio(label: str_or_rx).exist?
    end

    #
    # Select the radio button whose value or label matches the given string.
    #
    # @param [String, Regexp] str_or_rx
    # @raise [Watir::Exception::UnknownObjectException] if the Radio does not exist.
    # @return [String] The value or text of the radio selected.
    #

    def select(str_or_rx)
      found_by_value = radio(value: str_or_rx)
      found_by_text = radio(label: str_or_rx)

      if found_by_value.exist?
        found_by_value.click unless found_by_value.selected?
        return found_by_value.value
      elsif found_by_text.exist?
        found_by_text.click unless found_by_text.selected?
        return found_by_text.text
      else
        raise UnknownObjectException, "Unable to locate radio matching #{str_or_rx.inspect}"
      end
    end

    #
    # Returns true if any of the radio button label matches the given value.
    #
    # @param [String, Regexp] str_or_rx
    # @raise [Watir::Exception::UnknownObjectException] if the options do not exist
    # @return [Boolean]
    #

    def selected?(str_or_rx)
      found = frame.radio(label: str_or_rx)

      return found.selected? if found.exist?

      raise UnknownObjectException, "Unable to locate radio matching #{str_or_rx.inspect}"
    end

    #
    # Returns the value of the selected radio button in the set.
    # Returns nil if no radio is selected.
    #
    # @return [String, nil]
    #

    def value
      sel = selected
      sel&.value
    end

    #
    # Returns the text of the selected radio button in the set.
    # Returns nil if no option is selected.
    #
    # @return [String, nil]
    #

    def text
      sel = selected
      sel&.text
    end

    #
    # Returns the selected Radio element.
    # Returns nil if no radio button is selected.
    #
    # @return [Watir::Radio, nil]
    #

    def selected
      find(&:selected?)
    end

    #
    # Returns true if two elements are equal.
    #
    # @example
    #   browser.radio_set(id: 'new_user_newsletter_yes') == browser.radio_set(id: 'new_user_newsletter_no')
    #   #=> true
    #

    def ==(other)
      return false unless other.is_a?(self.class)

      radios == other.radios
    end
    alias eql? ==

    # Ruby 2.4+ complains about using #delegate to do this
    %i[assert_exists element_call].each do |method|
      define_method(method) do |*args, &blk|
        source.send(method, *args, &blk)
      end
    end
  end # RadioSet

  module Container
    def radio_set(*args)
      RadioSet.new(self, extract_selector(args).merge(tag_name: 'input', type: 'radio'))
    end

    Watir.tag_to_class[:radio_set] = RadioSet
  end # Container
end # Watir