defmodule Absinthe.Phase.Schema.Validation.KnownDirectives do
  @moduledoc false

  alias Absinthe.{Blueprint, Phase}

  use Absinthe.Phase

  @doc """
  Run the validation.
  """
  @spec run(Blueprint.t(), Keyword.t()) :: Phase.result_t()
  def run(input, _options \\ []) do
    result = Blueprint.postwalk(input, &handle_node/1)
    {:ok, result}
  end

  defp handle_node(%Blueprint.Directive{schema_node: nil} = node) do
    node
    |> put_error(error_unknown(node))
  end

  defp handle_node(%Blueprint.Directive{} = node) do
    node
  end

  defp handle_node(%{directives: []} = node) do
    node
  end

  defp handle_node(%{directives: _} = node) do
    node
    |> check_directives
    |> inherit_invalid(node.directives, :bad_directive)
  end

  defp handle_node(node) do
    node
  end

  defp check_directives(node) do
    placement = Blueprint.Directive.placement(node)

    directives =
      for directive <- node.directives do
        if directive.schema_node do
          if placement in directive.schema_node.locations do
            directive
          else
            directive
            |> put_error(error_misplaced(directive, placement))
            |> flag_invalid(:bad_placement)
          end
        else
          directive
        end
      end

    %{node | directives: directives}
  end

  # Generate the error for the node
  @spec error_unknown(Blueprint.node_t()) :: Phase.Error.t()
  defp error_unknown(node) do
    %Phase.Error{
      phase: __MODULE__,
      message: "Unknown directive `#{node.name}'.",
      locations: [node.__reference__.location]
    }
  end

  @spec error_misplaced(Blueprint.node_t(), atom) :: Phase.Error.t()
  defp error_misplaced(node, placement) do
    placement_name = placement |> to_string |> String.upcase()

    %Phase.Error{
      phase: __MODULE__,
      message: "Directive `#{node.name}' may not be used on #{placement_name}.",
      locations: [node.__reference__.location]
    }
  end
end
