defmodule Wormwood.GQLCase do
  @moduledoc """
  This module defines a few helpful macros when testing against an Absinthe GraphQL schema.
  It essentially registers an Absinthe schema and a GQL document to the module they're called in.
  """

  alias Wormwood.GQLLoader

  defmacro __using__(_opts) do
    quote do
      import Wormwood.GQLCase
    end
  end

  @doc """
  Call this macro in the module you wish to load your GQL documents in.

  It takes 3 arguments, the first is the query_name that you want to query.

  ```elixir
   defmodule MyCoolApplication.MyModule do
    load_gql :my_query, MyCoolApplication.MyAbsintheSchema, "assets/js/queries/MyQuery.gql"
    # ...
  ```

  After you define the query name at up code, you can call the query with
  ```elixir
  result = query_gql_by(:my_query, variables: %{}, context: %{})
  {:ok, query_data} = result
  ```

  """
  defmacro load_gql(query_name, schema, file_path) when is_atom(query_name) do
    quote do
      document = GQLLoader.load_file!(unquote(file_path))
      Module.put_attribute(unquote(__CALLER__.module), :_wormwood_gql_schema, unquote(schema))
      Module.register_attribute(unquote(__CALLER__.module), unquote(query_name), persist: true)
      Module.put_attribute(unquote(__CALLER__.module), unquote(query_name), document)
    end
  end

  @doc """
  Call this macro in the module you wish to load your GQL document in.

  It takes 2 arguments, the first is your Absinthe schema module, the second is a path to a GQL file that contains a GraphQL query or mutation.

  For example:
  ```elixir
  defmodule MyCoolApplication.MyModule do
    load_gql MyCoolApplication.MyAbsintheSchema, "assets/js/queries/MyQuery.gql"
    # ...
  ```
  """
  defmacro load_gql(schema, file_path) do
    quote do
      if Module.get_attribute(unquote(__CALLER__.module), :_wormwood_gql_query) != nil do
        raise WormwoodSetupError, reason: :double_declaration
      end

      document = GQLLoader.load_file!(unquote(file_path))

      Module.put_attribute(unquote(__CALLER__.module), :_wormwood_gql_query, document)
      Module.put_attribute(unquote(__CALLER__.module), :_wormwood_gql_schema, unquote(schema))
    end
  end

  @doc """
  Call this macro in the module where you want to query using a static string as your query.

  It takes 3 arguments, the first is the query_name that you want to query, the second is your Absinthe schema module and
  the third is a string of a GQL query or mutation.

  ```elixir
   defmodule MyCoolApplication.MyModule do
    set_gql :my_query, MyCoolApplication.MyAbsintheSchema, "query { some { cool { gql { id } }}}"
    # ...
  ```

  After you define the query name at up code, you can call the query with
  ```elixir
  result = query_gql_by(:my_query, variables: %{}, context: %{})
  {:ok, query_data} = result
  ```
  """
  defmacro set_gql(query_name, schema, query_string) when is_atom(query_name) do
    quote do
      document = GQLLoader.load_string!(unquote(query_string))
      Module.put_attribute(unquote(__CALLER__.module), :_wormwood_gql_schema, unquote(schema))
      Module.register_attribute(unquote(__CALLER__.module), unquote(query_name), persist: true)
      Module.put_attribute(unquote(__CALLER__.module), unquote(query_name), document)
    end
  end

  @doc """
  Call this macro in the module where you want to query using a static string as your query.

  It takes 2 arguments, the first is your Absinthe schema module, the second is a string of a GQL query or mutation.
  This still supports imports, they will be resolved from the current working directory. (Most likely the top level of your app)

  For example:
  ```elixir
  defmodule MyCoolApplication.MyModule do
    set_gql MyCoolApplication.MyAbsintheSchema, "query { some { cool { gql { id } }}}"
    # ...
  ```
  """
  defmacro set_gql(schema, query_string) do
    quote do
      if Module.get_attribute(unquote(__CALLER__.module), :_wormwood_gql_query) != nil do
        raise WormwoodSetupError, reason: :double_declaration
      end

      document = GQLLoader.load_string!(unquote(query_string))

      Module.put_attribute(unquote(__CALLER__.module), :_wormwood_gql_query, document)
      Module.put_attribute(unquote(__CALLER__.module), :_wormwood_gql_schema, unquote(schema))
    end
  end

  @doc """
  Call this macro in the module you've loaded a document into using `load_gql/3`.

  For example:
  ```elixir
  result = query_gql_by(:my_query, variables: %{}, context: %{})
  {:ok, query_data} = result
  ```
  """
  defmacro query_gql_by(query_name, options \\ []) do
    quote do
      attribute =
        unquote(__CALLER__.module).__info__(:attributes)[unquote(query_name)]
        |> case do
          nil -> nil
          list -> List.last(list)
        end

      if is_nil(attribute) do
        raise WormwoodSetupError, reason: :missing_declaration
      end

      Absinthe.run(
        attribute,
        @_wormwood_gql_schema,
        unquote(options)
      )
    end
  end

  @doc """
  Call this macro in the module you've loaded a document into using `load_gql` or `set_gql`.

  Calling this will execute the document loaded into the module against the schema loaded in the module.
  It accepts a keyword list for `options` that are passed into [`Absinthe.run/3`](https://hexdocs.pm/absinthe/Absinthe.html#run/3).

  Please see the [Absinthe docs](https://hexdocs.pm/absinthe/Absinthe.html#run/3-options) for more information on the options that can be passed to this macro.

  Returns a tuple of the query result from the [`Absinthe.run/3`](https://hexdocs.pm/absinthe/Absinthe.html#run/3) call.

  For example:
  ```elixir
  result = query_gql(variables: %{}, context: %{})
  {:ok, query_data} = result
  ```
  """
  defmacro query_gql(options \\ []) do
    quote do
      if is_nil(@_wormwood_gql_query) do
        raise WormwoodSetupError, reason: :missing_declaration
      end

      Absinthe.run(
        @_wormwood_gql_query,
        @_wormwood_gql_schema,
        unquote(options)
      )
    end
  end

  @doc """
  Call this macro in the module you've loaded a document into using `load_gql` or `set_gql`.

  Calling this will execute the document loaded into the module against the schema loaded in the module.
  Absinthe will use the phases in the "pipeline_phases" list argument when running.
  It also accepts a keyword list for `options` that are passed into [`Absinthe.run/3`](https://hexdocs.pm/absinthe/Absinthe.html#run/3).

  Please see the [Absinthe docs](https://hexdocs.pm/absinthe/Absinthe.html#run/3-options) for more information on the options that can be passed to this macro.

  Returns a tuple of the query result from the [`Absinthe.Pipeline.run/2`](https://hexdocs.pm/absinthe/Absinthe.Pipeline.html#run/2) call.

  For example:
  ```elixir
  pipeline = [Absinthe.Phase.Parse, Absinthe.Phase.Blueprint]
  result = query_gql_with_pipeline(pipeline)
  assert {:ok, %Absinthe.Blueprint{} = _blueprint, _pipeline} = result
  ```
  """
  defmacro query_gql_with_pipeline(pipeline_phases \\ [], options \\ []) do
    quote do
      if is_nil(@_wormwood_gql_query) do
        raise WormwoodSetupError, reason: :missing_declaration
      end

      options_list =
        unquote(options)
        |> Keyword.put(:schema, @_wormwood_gql_schema)
        |> Absinthe.Pipeline.options()

      pipeline = Enum.map(unquote(pipeline_phases), fn phase -> {phase, options_list} end)

      Absinthe.Pipeline.run(@_wormwood_gql_query, pipeline)
    end
  end
end
