defmodule Patch.Assertions do
  alias Patch.MissingCall
  alias Patch.Mock
  alias Patch.Mock.History
  alias Patch.UnexpectedCall

  @doc """
  Asserts that the given module and function has been called with any arity.

  ```elixir
  patch(Example, :function, :patch)

  Patch.Assertions.assert_any_call(Example, :function)   # fails

  Example.function(1, 2, 3)

  Patch.Asertions.assert_any_call(Example, :function)   # passes
  ```

  There are convenience delegates in the Developer Interface, `Patch.assert_any_call/1` and
  `Patch.assert_any_call/2` which should be preferred over calling this function directly.
  """
  @spec assert_any_call(module :: module(), function :: atom()) :: nil
  def assert_any_call(module, function) do
    history =
      module
      |> Mock.history()
      |> History.Tagged.for_function(function)

    unless History.Tagged.any?(history) do
      message = """
      \n
      Expected any call to the following function:

        #{inspect(module)}.#{to_string(function)}

      Calls which were received (matching calls are marked with *):

      #{History.Tagged.format(history, module)}
      """

      raise MissingCall, message: message
    end
  end

  @doc """
  Given a call will assert that a matching call was observed by the patched function.

  This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
  `assert_receive/3` and `assert_received/2`.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  Patch.Assertions.assert_called(Example, :function, [1, 2, 3])   # passes
  Patch.Assertions.assert_called(Example, :function, [1, _, 3])   # passes
  Patch.Assertions.assert_called(Example, :function, [4, 5, 6])   # fails
  Patch.Assertions.assert_called(Example, :function, [4, _, 6])   # fails
  ```

  There is a convenience macro in the Developer Interface, `Patch.assert_called/1` which should be
  preferred over calling this macro directly.
  """
  @spec assert_called(call :: Macro.t()) :: Macro.t()
  defmacro assert_called(call) do
    {module, function, patterns} = Macro.decompose_call(call)

    quote do
      history =
        unquote(module)
        |> Patch.Mock.history()
        |> Patch.Mock.History.Tagged.for_call(unquote(call))

      unless Patch.Mock.History.Tagged.any?(history) do
        message = """
        \n
        Expected but did not receive the following call:

          #{inspect(unquote(module))}.#{to_string(unquote(function))}(#{Patch.Assertions.format_patterns(unquote(patterns))})

        Calls which were received (matching calls are marked with *):

        #{Patch.Mock.History.Tagged.format(history, unquote(module))}
        """

        raise MissingCall, message: message
      end

      {:ok, {unquote(function), arguments}} = Patch.Mock.History.Tagged.first(history)
      Patch.Macro.match(unquote(patterns), arguments)
    end
  end

  @doc """
  Given a call will assert that a matching call was observed exactly the number of times provided
  by the patched function.

  This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
  `assert_receive/3` and `assert_received/2`.  The value bound will be the from the latest call.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  Patch.Assertions.assert_called(Example, :function, [1, 2, 3], 1)   # passes
  Patch.Assertions.assert_called(Example, :function, [1, _, 3], 1)  # passes

  Example.function(1, 2, 3)

  Patch.Assertions.assert_called(Example, :function, [1, 2, 3], 2)   # passes
  Patch.Assertions.assert_called(Example, :function, [1, _, 3], 2)  # passes
  ```

  There is a convenience macro in the Developer Interface, `Patch.assert_called/2` which
  should be preferred over calling this macro directly.
  """
  @spec assert_called(call :: Macro.t(), count :: non_neg_integer()) :: Macro.t()
  defmacro assert_called(call, count) do
    {module, function, patterns} = Macro.decompose_call(call)

    quote do
      history =
        unquote(module)
        |> Patch.Mock.history()
        |> Patch.Mock.History.Tagged.for_call(unquote(call))

      call_count = Patch.Mock.History.Tagged.count(history)

      unless call_count == unquote(count) do
        exception =
          if call_count < unquote(count) do
            MissingCall
          else
            UnexpectedCall
          end

        message = """
        \n
        Expected #{unquote(count)} of the following calls, but found #{call_count}:

          #{inspect(unquote(module))}.#{to_string(unquote(function))}(#{Patch.Assertions.format_patterns(unquote(patterns))})

        Calls which were received (matching calls are marked with *):

        #{Patch.Mock.History.Tagged.format(history, unquote(module))}
        """

        raise exception, message
      end

      {:ok, {unquote(function), arguments}} = Patch.Mock.History.Tagged.first(history)
      Patch.Macro.match(unquote(patterns), arguments)
    end
  end

  @doc """
  Given a call will assert that a matching call was observed exactly once by the patched function.

  This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
  `assert_receive/3` and `assert_received/2`.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  Patch.Assertions.assert_called_once(Example, :function, [1, 2, 3])   # passes
  Patch.Assertions.assert_called_once(Example, :function, [1, _, 3])  # passes

  Example.function(1, 2, 3)

  Patch.Assertions.assert_called_once(Example, :function, [1, 2, 3])   # fails
  Patch.Assertions.assert_called_once(Example, :function, [1, _, 3])  # fails
  ```

  There is a convenience macro in the Developer Interface, `Patch.assert_called_once/1` which
  should be preferred over calling this macro directly.
  """
  @spec assert_called_once(call :: Macro.t()) :: Macro.t()
  defmacro assert_called_once(call) do
    {module, function, patterns} = Macro.decompose_call(call)

    quote do
      history =
        unquote(module)
        |> Patch.Mock.history()
        |> Patch.Mock.History.Tagged.for_call(unquote(call))

      call_count = Patch.Mock.History.Tagged.count(history)

      unless call_count == 1 do
        exception =
          if call_count == 0 do
            MissingCall
          else
            UnexpectedCall
          end

        message = """
        \n
        Expected the following call to occur exactly once, but call occurred #{call_count} times:

          #{inspect(unquote(module))}.#{to_string(unquote(function))}(#{Patch.Assertions.format_patterns(unquote(patterns))})

        Calls which were received (matching calls are marked with *):

        #{Patch.Mock.History.Tagged.format(history, unquote(module))}
        """

        raise exception, message
      end

      {:ok, {unquote(function), arguments}} = Patch.Mock.History.Tagged.first(history)
      Patch.Macro.match(unquote(patterns), arguments)
    end
  end

  @doc """
  Refutes that the given module and function has been called with any arity.

  ```elixir
  patch(Example, :function, :patch)

  Patch.Assertions.refute_any_call(Example, :function)   # passes

  Example.function(1, 2, 3)

  Patch.Assertions.refute_any_call(Example, :function)   # fails
  ```

  There are convenience delegates in the Developer Interface, `Patch.refute_any_call/1` and
  `Patch.refute_any_call/2` which should be preferred over calling this function directly.
  """
  @spec refute_any_call(module :: module(), function :: atom()) :: nil
  def refute_any_call(module, function) do
    history =
      module
      |> Mock.history()
      |> History.Tagged.for_function(function)

    if History.Tagged.any?(history) do
      message = """
      \n
      Unexpected call received, expected no calls:

        #{inspect(module)}.#{to_string(function)}

      Calls which were received (matching calls are marked with *):

      #{History.Tagged.format(history, module)}
      """

      raise UnexpectedCall, message: message
    end
  end

  @doc """
  Given a call will refute that a matching call was observed by the patched function.

  This macro fully supports patterns.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  Patch.Assertions.refute_called(Example, :function, [4, 5, 6])   # passes
  Patch.Assertions.refute_called(Example, :function, [4, _, 6])  # passes
  Patch.Assertions.refute_called(Example, :function, [1, 2, 3])   # fails
  Patch.Assertions.refute_called(Example, :function, [1, _, 3])  # passes
  ```

  There is a convenience macro in the Developer Interface, `Patch.refute_called/1` which should be
  preferred over calling this macro directly.
  """
  @spec refute_called(call :: Macro.t()) :: Macro.t()
  defmacro refute_called(call) do
    {module, function, patterns} = Macro.decompose_call(call)

    quote do
      history =
        unquote(module)
        |> Patch.Mock.history()
        |> Patch.Mock.History.Tagged.for_call(unquote(call))

      if Patch.Mock.History.Tagged.any?(history) do
        message = """
        \n
        Unexpected call received:

          #{inspect(unquote(module))}.#{to_string(unquote(function))}(#{Patch.Assertions.format_patterns(unquote(patterns))})

        Calls which were received (matching calls are marked with *):

        #{Patch.Mock.History.Tagged.format(history, unquote(module))}
        """

        raise UnexpectedCall, message: message
      end
    end
  end

  @doc """
  Given a call will refute that a matching call was observed exactly the number of times provided
  by the patched function.

  This macro fully supports patterns.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  Patch.Assertions.refute_called(Example, :function, [1, 2, 3], 2)   # passes
  Patch.Assertions.refute_called(Example, :function, [1, _, 3], 2)  # passes

  Example.function(1, 2, 3)

  Patch.Assertions.refute_called(Example, :function, [1, 2, 3], 1)   # passes
  Patch.Assertions.refute_called(Example, :function, [1, _, 3], 1)  # passes
  ```

  There is a convenience macro in the Developer Interface, `Patch.refute_called/2` which
  should be preferred over calling this macro directly.
  """
  @spec refute_called(call :: Macro.t(), count :: non_neg_integer()) :: Macro.t()
  defmacro refute_called(call, count) do
    {module, function, patterns} = Macro.decompose_call(call)

    quote do
      history =
        unquote(module)
        |> Patch.Mock.history()
        |> Patch.Mock.History.Tagged.for_call(unquote(call))

      call_count = Patch.Mock.History.Tagged.count(history)

      if call_count == unquote(count) do
        message = """
        \n
        Expected any count except #{unquote(count)} of the following calls, but found #{call_count}:

          #{inspect(unquote(module))}.#{to_string(unquote(function))}(#{Patch.Assertions.format_patterns(unquote(patterns))})

        Calls which were received (matching calls are marked with *):

        #{Patch.Mock.History.Tagged.format(history, unquote(module))}
        """

        raise UnexpectedCall, message
      end
    end
  end

  @doc """
  Given a call will refute that a matching call was observed exactly once by the patched function.

  This macro fully supports patterns.

  ```elixir
  patch(Example, :function, :patch)

  Example.function(1, 2, 3)

  Patch.Assertions.refute_called_once(Example, :function, [1, 2, 3])   # fails
  Patch.Assertions.refute_called_once(Example, :function, [1, _, 3])  # fails

  Example.function(1, 2, 3)

  Patch.Assertions.refute_called_once(Example, :function, [1, 2, 3])   # passes
  Patch.Assertions.refute_called_once(Example, :function, [1, _, 3])  # passes
  ```

  There is a convenience macro in the Developer Interface, `Patch.refute_called_once/1` which
  should be preferred over calling this macro directly.
  """
  @spec refute_called_once(call :: Macro.t()) :: Macro.t()
  defmacro refute_called_once(call) do
    {module, function, patterns} = Macro.decompose_call(call)

    quote do
      history =
        unquote(module)
        |> Patch.Mock.history()
        |> Patch.Mock.History.Tagged.for_call(unquote(call))

      call_count = Patch.Mock.History.Tagged.count(history)

      if call_count == 1 do
        message = """
        \n
        Expected the following call to occur any number of times but once, but it occurred once:

          #{inspect(unquote(module))}.#{to_string(unquote(function))}(#{Patch.Assertions.format_patterns(unquote(patterns))})

        Calls which were received (matching calls are marked with *):

        #{Patch.Mock.History.Tagged.format(history, unquote(module))}
        """

        raise UnexpectedCall, message
      end
    end
  end

  @doc """
  Formats the AST for a list of patterns AST as they would appear in an argument list.
  """
  @spec format_patterns(patterns :: [term()]) :: String.t()
  defmacro format_patterns(patterns) do
    string = Macro.to_string(patterns)
    stop = max(String.length(string) - 2, 0)
    String.slice(string, 1, stop)
  end
end
