# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT

defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
  @moduledoc """
  Validate that the password and password confirmation match.

  This check is only performed when the `confirmation_required?` DSL option is
  set to `true`.

  You can use this validation in your own actions where you want to validate
  that the password and the password confirmation arguments match.  If you're
  not using one of the actions generated by the password strategy then you'll
  need to manually pass the strategy name in the changeset context.  Eg:

  ```elixir
  Changeset.new(user, %{})
  |> Changeset.set_context(%{strategy_name: :password})
  |> Changeset.for_update(:change_password, params)
  |> Accounts.update()
  ```

  or by adding it statically in your action definition:

  ```elixir
  update :change_password do
    change set_context(%{strategy_name: :password})
    change AshAuthentication.Strategy.Password.HashPasswordChange
  end
  ```
  """

  use Ash.Resource.Validation

  alias Ash.{
    Changeset,
    Error.Changes.InvalidArgument,
    Error.Framework.AssumptionFailed,
    Resource.Validation
  }

  alias AshAuthentication.Info

  @doc """
  Validates that the password and password confirmation fields contain
  equivalent values - if confirmation is required.
  """
  @impl true
  @spec validate(Changeset.t(), keyword, Validation.Context.t()) ::
          :ok | {:error, String.t() | Exception.t()}
  def validate(changeset, options, _context) do
    case Info.find_strategy(changeset, options) do
      {:ok, %{confirmation_required?: true} = strategy} ->
        validate_password_confirmation(changeset, strategy)

      {:ok, _} ->
        :ok

      :error ->
        {:error,
         AssumptionFailed.exception(
           message: "Action does not correlate with an authentication strategy"
         )}
    end
  end

  @impl true
  def atomic(changeset, opts, context) do
    validate(changeset, opts, context)
  end

  defp validate_password_confirmation(changeset, strategy) do
    password = Changeset.get_argument(changeset, strategy.password_field)
    confirmation = Changeset.get_argument(changeset, strategy.password_confirmation_field)

    if password == confirmation do
      :ok
    else
      {:error,
       InvalidArgument.exception(
         field: strategy.password_confirmation_field,
         message: "does not match"
       )}
    end
  end
end
