defmodule Chaperon.Action.Async do
  @moduledoc """
  Implementation module for asynchronous actions (function calls into a
  `Chaperon.Scenario` module).
  """

  defstruct module: nil,
            function: nil,
            args: [],
            task_name: nil

  @type t :: %Chaperon.Action.Async{
          module: module,
          function: atom,
          args: [any],
          task_name: atom
        }
end

defimpl Chaperon.Actionable, for: Chaperon.Action.Async do
  alias Chaperon.Session
  use Chaperon.Session.Logging

  def run(
        action = %{
          module: mod,
          function: func_name,
          args: args,
          task_name: task_name
        },
        session
      ) do
    session
    |> log_debug("Async #{task_name} : #{mod}.#{func_name}(#{inspect(args)})")

    task = action |> execute_task(session)

    session
    |> Session.add_async_task(task_name, task)
    |> Session.ok()
  end

  def abort(action, session) do
    {:ok, action, session}
  end

  defp execute_task(
         %{
           module: mod,
           function: func_name,
           args: args,
           task_name: task_name
         },
         session
       ) do
    session = session |> Session.fork()

    Task.async(fn ->
      session
      |> Session.time(task_name, fn sess ->
        apply(mod, func_name, [sess | args])
      end)
    end)
  end
end

defimpl String.Chars, for: Chaperon.Action.Async do
  def to_string(%{module: mod, function: func_name, args: args}) do
    "Async[#{mod}.#{func_name}/#{Enum.count(args)}]"
  end
end
