defmodule EventEmitter do
  @moduledoc false
  # This module offers telemetry style event emission for testing purposes.

  # If you'd like to emit a message to the event stream, you can call `emit/1` from your
  # implementation code. This is macro, and will not result in any AST injection when not being
  # compiled in the test env.

  # ```elixir
  # defmodule ImplMod do
  #   use EventEmitter, :emitter

  #   def implementation do
  #     # some logic

  #     emit %{
  #       name: :implementation,
  #       module: __MODULE__,
  #       metadata: %{unique_identifier: some_variable}
  #     }
  #   end
  # end
  # ```

  # If you'd like to await on a message emitted by implementation code, you can call `await/3` from
  # your test code after registering a handler for your test process

  # ```elixir
  # defmodule TestMod do
  #   use EventEmitter, :receiver

  #   test "some test" do
  #     EventEmitter.add_handler(self())

  #     # some tricky asynchronous code

  #     await :implementation, __MODULE__, %{unique_identifier: some_variable}
  #   end
  # end
  # ```

  # You can use EventEmitter by starting it in your test helper.

  # ```elixir
  # # test_helper.exs

  # EventEmitter.start_link([])

  # ExUnit.start()
  # ```

  use GenServer

  @type event :: %{
          optional(:metadata) => map(),
          required(:name) => String.t()
        }

  def emitter do
    quote do
      import EventEmitter, only: [emit: 1]
    end
  end

  def receiver do
    quote do
      import EventEmitter, only: [await: 3]
    end
  end

  defmacro __using__(which) do
    apply(__MODULE__, which, [])
  end

  defmacro emit(event) do
    if Mix.env() == :test do
      quote do
        EventEmitter.emit_event(unquote(event))
      end
    else
      nil
    end
  end

  def await(name, metadata, module) do
    e = {:event, %{metadata: metadata, name: name, module: module}}

    receive do
      ^e -> :ok
    end
  end

  def start_link(args) do
    GenServer.start_link(__MODULE__, args, name: __MODULE__)
  end

  def add_handler(pid), do: GenServer.call(__MODULE__, {:add_handler, pid})
  def emit_event(event), do: GenServer.cast(__MODULE__, {:event, event})

  @impl GenServer
  def init(_) do
    {:ok, %{handlers: []}}
  end

  @impl GenServer
  def handle_call({:add_handler, pid}, _, state) do
    {:reply, pid, %{state | handlers: [pid | state.handlers]}}
  end

  @impl GenServer
  def handle_cast({:event, event}, %{handlers: handlers} = state) do
    for h <- handlers do
      send(h, {:event, event})
    end

    {:noreply, state}
  end
end
