lib/bylaw/html/check/require_input_autocomplete.ex

defmodule Bylaw.HTML.Check.RequireInputAutocomplete do
  @moduledoc """
  Validates that rendered input fields define a non-blank `autocomplete` attribute.

  This check inspects rendered `<input>` elements that accept user-entered
  values and flags fields without an explicit autocomplete purpose. Use a
  specific autocomplete token where possible, or `autocomplete="off"` when a
  field intentionally should not be autofilled.

  ## Examples

  Bad:

      <input type="email" name="user[email]">

  Why this is bad:

  The browser and assistive technology cannot identify the expected input
  purpose from the rendered markup.

  Better:

      <input type="email" name="user[email]" autocomplete="email">

  Why this is better:

  The field exposes its input purpose in a machine-readable way.

  Bad:

      <input name="search" autocomplete="">

  Why this is bad:

  A blank autocomplete value is equivalent to leaving the purpose unspecified.

  Better:

      <input name="search" autocomplete="off">

  Why this is better:

  The rendered markup documents that autocomplete was considered and disabled
  intentionally.

  ## Notes

  This check ignores input controls where autocomplete is not meaningful:
  `button`, `checkbox`, `file`, `hidden`, `image`, `radio`, `reset`, and
  `submit`. Inputs without a `type` attribute are treated as text inputs.

  The check only verifies that a non-blank `autocomplete` value is present. It
  does not validate autocomplete token grammar or judge whether the chosen token
  matches the field's semantic purpose.

  This check runs on rendered HTML, so dynamic `autocomplete` attributes are
  evaluated after rendering.

  ## Options

  This check has no check-specific options. Add the module directly to the
  explicit checks list:

      Bylaw.HTML.Check.RequireInputAutocomplete

  ## Usage

  Add this module to the explicit check list passed through `Bylaw.HTML`.
  See `Bylaw.HTML` for the full rendered HTML validation setup.
  """

  @behaviour Bylaw.HTML.Check

  alias Bylaw.HTML.Issue

  @ignored_input_types ~w(button checkbox file hidden image radio reset submit)

  @doc """
  Implements the `Bylaw.HTML.Check` validation callback.
  """
  @impl Bylaw.HTML.Check
  @spec validate(Bylaw.HTML.Check.context()) :: Bylaw.HTML.Check.result()
  def validate(%{document: document}) do
    document
    |> LazyHTML.query("input")
    |> Enum.reject(&ignored_input?/1)
    |> Enum.filter(&missing_autocomplete?/1)
    |> Enum.map(&issue_for/1)
    |> result()
  end

  def validate(context) do
    raise ArgumentError,
          "expected context to be a map with a parsed document, got: #{inspect(context)}"
  end

  defp ignored_input?(element) do
    input_type = element |> input_type() |> String.downcase()

    input_type in @ignored_input_types
  end

  defp input_type(element) do
    element
    |> LazyHTML.attribute("type")
    |> List.first()
    |> case do
      nil -> "text"
      type -> String.trim(type)
    end
  end

  defp missing_autocomplete?(element) do
    element
    |> autocomplete_value()
    |> blank?()
  end

  defp autocomplete_value(element) do
    element
    |> LazyHTML.attribute("autocomplete")
    |> List.first()
  end

  defp blank?(nil), do: true
  defp blank?(value), do: String.trim(value) == ""

  defp issue_for(element) do
    %Issue{
      check: __MODULE__,
      message: "expected <input> to define a non-blank autocomplete attribute",
      tag: "input",
      snippet: LazyHTML.to_html(element)
    }
  end

  defp result([]), do: :ok
  defp result(issues), do: {:error, issues}
end