if Phoenix.Sync.sandbox_enabled?() do
  defmodule Phoenix.Sync.Sandbox.StackRegistry do
    @moduledoc false

    use GenServer

    def start_link(_) do
      GenServer.start_link(__MODULE__, [], name: __MODULE__)
    end

    def register(registry \\ __MODULE__, pid, stack_id) do
      GenServer.call(registry, {:register, pid, stack_id})
    end

    def lookup(registry \\ __MODULE__, pid) do
      GenServer.call(registry, {:lookup, pid})
    end

    def configure(registry \\ __MODULE__, stack_id, api, client) do
      GenServer.call(registry, {:configure, self(), stack_id, api, client})
    end

    # api should be removed when test pid exits
    # registered pid should be removed when it exits

    def get_api(registry \\ __MODULE__, stack_id) do
      GenServer.call(registry, {:get, :api, stack_id})
    end

    def get_client(registry \\ __MODULE__, stack_id) do
      GenServer.call(registry, {:get, :client, stack_id})
    end

    def shared_mode(registry \\ __MODULE__, owner, stack_id) do
      GenServer.call(registry, {:shared_mode, owner, stack_id})
    end

    ## callbacks

    def init(_) do
      {:ok, %{stack_pids: %{}, stacks: %{}, shared: nil}}
    end

    def handle_call({:register, pid, stack_id}, _from, state) do
      _ref = Process.monitor(pid, tag: {:down, :register})

      state = Map.update!(state, :stack_pids, &Map.put(&1, pid, stack_id))

      {:reply, :ok, state}
    end

    def handle_call({:lookup, _pid}, _from, %{shared: {_owner, stack_id}} = state) do
      {:reply, stack_id, state}
    end

    def handle_call({:lookup, pid}, _from, state) do
      {:reply, Map.get(state.stack_pids, pid), state}
    end

    def handle_call({:configure, pid, stack_id, api, client}, _from, state) do
      _ref = Process.monitor(pid, tag: {:down, :stack, stack_id})

      state = Map.update!(state, :stacks, &Map.put(&1, stack_id, %{api: api, client: client}))

      {:reply, :ok, state}
    end

    def handle_call({:get, key, stack_id}, _from, state) when key in [:api, :client] do
      {:reply, get_config(state, stack_id, key), state}
    end

    # shared mode only works for non-async tests, which means that only
    # one test is running at any time, so we can set the shared pid globally
    def handle_call({:shared_mode, owner, stack_id}, _from, %{shared: nil} = state)
        when is_pid(owner) do
      _ref = Process.monitor(owner, tag: {:down, :shared, stack_id})
      {:reply, :ok, Map.put(state, :shared, {owner, stack_id})}
    end

    def handle_call({:shared_mode, owner, stack_id}, _from, %{shared: {owner, stack_id}} = state) do
      {:reply, :ok, state}
    end

    def handle_call(
          {:shared_mode, _owner, _stack_id},
          _from,
          %{shared: {owner, stack_id}} = state
        ) do
      {:reply,
       {:error,
        "Shared mode already registered to pid #{inspect(owner)} for stack #{inspect(stack_id)}"},
       state}
    end

    def handle_info({{:down, :stack, stack_id}, _ref, :process, _pid, _reason}, state) do
      state = Map.update!(state, :stacks, &Map.delete(&1, stack_id))

      {:noreply, state}
    end

    def handle_info(
          {{:down, :shared, stack_id}, _ref, :process, pid, _reason},
          %{shared: {pid, stack_id}} = state
        ) do
      {:noreply, Map.put(state, :shared, nil)}
    end

    def handle_info({{:down, :register}, _ref, :process, pid, _reason}, state) do
      state = Map.update!(state, :stack_pids, &Map.delete(&1, pid))

      {:noreply, state}
    end

    defp get_config(state, stack_id, key) do
      with {:ok, config} <- Map.fetch(state.stacks, stack_id) do
        Map.fetch(config, key)
      end
    end
  end
end
