defmodule IbkrApi.Support.HTTPSandbox do
  @moduledoc """
  HTTP sandbox for mocking HTTP requests in tests.

  Stores response functions in a Registry under the PID of the test process.
  In test, the IbkrApi.SharedUtils.HTTP module will check this sandbox before
  making actual HTTP requests.
  """

  @sleep 10
  @state "state"
  @registry :http_sandbox
  @keys :unique

  @type action :: :get | :post | :put | :delete
  @type url :: String.t()
  @type body :: map() | String.t()
  @type headers :: keyword() | list()
  @type options :: keyword()

  @doc """
  Start the HTTP sandbox registry.
  Should be called from test_helper.exs
  """
  @spec start_link :: {:error, any} | {:ok, pid}
  def start_link do
    Registry.start_link(keys: @keys, name: @registry)
  end

  @doc """
  Checks if the HTTP sandbox is disabled.
  Used by the HTTP client to determine whether to use mock responses.

  Always returns false in tests, meaning the sandbox is always enabled.
  """
  @spec sandbox_disabled? :: boolean
  def sandbox_disabled? do
    false
  end

  @doc """
  Retrieves the response for a HEAD request
  """
  @spec head_response(url, headers, options) :: any
  def head_response(url, headers, options) do
    func = find!(:head, url)

    case :erlang.fun_info(func)[:arity] do
      0 -> func.()
      3 -> func.(url, headers, options)
      _ -> raise_arity_error(func)
    end
  end

  @doc """
  Retrieves the response for a GET request
  """
  @spec get_response(url, headers, options) :: any
  def get_response(url, headers, options) do
    func = find!(:get, url)

    case :erlang.fun_info(func)[:arity] do
      0 -> func.()
      3 -> func.(url, headers, options)
      _ -> raise_arity_error(func)
    end
  end

  @doc """
  Retrieves the response for a POST request
  """
  @spec post_response(url, body, headers, options) :: any
  def post_response(url, body, headers, options) do
    func = find!(:post, url)

    case :erlang.fun_info(func)[:arity] do
      0 -> func.()
      1 -> func.(body)
      3 -> func.(url, headers, options)
      4 -> func.(url, body, headers, options)
      _ -> raise_arity_error(func)
    end
  end

  @doc """
  Retrieves the response for a PUT request
  """
  @spec put_response(url, body, headers, options) :: any
  def put_response(url, body, headers, options) do
    func = find!(:put, url)

    case :erlang.fun_info(func)[:arity] do
      0 -> func.()
      1 -> func.(body)
      3 -> func.(url, headers, options)
      4 -> func.(url, body, headers, options)
      _ -> raise_arity_error(func)
    end
  end

  @doc """
  Retrieves the response for a DELETE request
  """
  @spec delete_response(url, headers, options) :: any
  def delete_response(url, headers, options) do
    func = find!(:delete, url)

    case :erlang.fun_info(func)[:arity] do
      0 -> func.()
      3 -> func.(url, headers, options)
      _ -> raise_arity_error(func)
    end
  end

  @doc """
  Sets sandbox responses for GET requests.

  ## Examples

      HTTPSandbox.set_get_responses([
        {"https://localhost:5000/v1/api/iserver/auth/status", fn ->
          {:ok, %{
            "authenticated" => true,
            "competing" => false,
            "connected" => true,
            "message" => ""
          }, %IbkrApi.SharedUtils.HTTP.Response{status: 200}}
        end}
      ])
  """
  @spec set_get_responses([{String.t(), fun}]) :: :ok
  def set_get_responses(tuples) do
    tuples
    |> Map.new(fn {url, func} -> {{:get, url}, func} end)
    |> register_responses()
  end

  @doc """
  Sets sandbox responses for POST requests.

  ## Examples

      HTTPSandbox.set_post_responses([
        {"https://localhost:5000/v1/api/iserver/account", fn ->
          {:ok, %{"acctId" => "U12345678"}, %IbkrApi.SharedUtils.HTTP.Response{status: 200}}
        end}
      ])
  """
  @spec set_post_responses([{String.t(), fun}]) :: :ok
  def set_post_responses(tuples) do
    tuples
    |> Map.new(fn {url, func} -> {{:post, url}, func} end)
    |> register_responses()
  end

  @doc """
  Sets sandbox responses for PUT requests.

  ## Examples

      HTTPSandbox.set_put_responses([
        {"https://localhost:5000/v1/api/iserver/account/orders", fn ->
          {:ok, %{"id" => "12345"}, %IbkrApi.SharedUtils.HTTP.Response{status: 200}}
        end}
      ])
  """
  @spec set_put_responses([{String.t(), fun}]) :: :ok
  def set_put_responses(tuples) do
    tuples
    |> Map.new(fn {url, func} -> {{:put, url}, func} end)
    |> register_responses()
  end

  @doc """
  Sets sandbox responses for DELETE requests.

  ## Examples

      HTTPSandbox.set_delete_responses([
        {"https://localhost:5000/v1/api/iserver/account/order/12345", fn ->
          {:ok, %{"id" => "12345"}, %IbkrApi.SharedUtils.HTTP.Response{status: 200}}
        end}
      ])
  """
  @spec set_delete_responses([{String.t(), fun}]) :: :ok
  def set_delete_responses(tuples) do
    tuples
    |> Map.new(fn {url, func} -> {{:delete, url}, func} end)
    |> register_responses()
  end

  @doc """
  Sets sandbox responses for HEAD requests.

  ## Examples

      HTTPSandbox.set_head_responses([
        {"https://localhost:5000/v1/api/iserver/auth/status", fn ->
          {:ok, %{}, %IbkrApi.SharedUtils.HTTP.Response{status: 200}}
        end}
      ])
  """
  @spec set_head_responses([{String.t(), fun}]) :: :ok
  def set_head_responses(tuples) do
    tuples
    |> Map.new(fn {url, func} -> {{:head, url}, func} end)
    |> register_responses()
  end

  @doc """
  Finds the appropriate response function for the given action and URL.
  Returns the function or raises an error.
  """
  @spec find!(action, url) :: fun
  def find!(action, url) do
    case Registry.lookup(@registry, @state) do
      [{_pid, funcs}] ->
        find_response!(funcs, action, url)

      [] ->
        raise """
        No HTTP sandbox responses registered for #{inspect(self())}
        Action: #{inspect(action)}
        URL: #{inspect(url)}

        ======= Use: =======
        #{format_example(action, url)}
        === in your test ===
        """
    end
  end

  # Register responses in the registry
  defp register_responses(responses) do
    case Registry.lookup(@registry, @state) do
      [{_pid, _existing}] ->
        Registry.update_value(@registry, @state, &Map.merge(&1, responses))
      [] ->
        Registry.register(@registry, @state, responses)
    end

    Process.sleep(@sleep)
    :ok
  end

  # Find the appropriate response function
  defp find_response!(funcs, action, url) do
    key = {action, url}

    case Map.get(funcs, key) do
      nil ->
        # Try regex matching
        regex_match = Enum.find(funcs, fn {{act, pattern}, _} ->
          act == action && is_struct(pattern, Regex) && Regex.match?(pattern, url)
        end)

        case regex_match do
          {{_, _}, func} when is_function(func) -> func
          nil -> raise_function_not_found(action, url, funcs)
        end

      func when is_function(func) ->
        func
    end
  end

  # Helper for raising arity errors
  defp raise_arity_error(func) do
    raise """
    This function's signature is not supported.
    #{inspect(func)}
    Please provide a function with appropriate arity for the HTTP method.
    """
  end

  # Helper for raising function not found errors
  defp raise_function_not_found(action, url, funcs) do
    functions_text =
      Enum.map_join(funcs, "\n", fn {k, v} -> "#{inspect(k)}    =>    #{inspect(v)}" end)

    raise """
    Function not found for {#{inspect(action)}, #{inspect(url)}} in #{inspect(self())}

    Found:
    #{functions_text}

    ======= Use: =======
    #{format_example(action, url)}
    === in your test ===
    """
  end

  # Helper for generating example usage
  defp format_example(action, url) do
    """
    alias IbkrApi.Support.HTTPSandbox

    setup do
      HTTPSandbox.set_#{action}_responses([
        {#{inspect(url)}, fn ->
          {:ok, response_data, %IbkrApi.SharedUtils.HTTP.Response{status: 200}}
        end}
        # or for regex matching:
        {~r|https://localhost:5000/v1/api|, fn -> response end}
      ])
    end
    """
  end
end
