Code.require_file("../test_helper.exs", __DIR__)

defmodule Kernel.ImplTest do
  use ExUnit.Case, async: true

  defp capture_err(fun) do
    ExUnit.CaptureIO.capture_io(:stderr, fun)
  end

  defp purge(module) do
    :code.purge(module)
    :code.delete(module)
  end

  setup do
    on_exit(fn -> purge(Kernel.ImplTest.ImplAttributes) end)
  end

  defprotocol AProtocol do
    def foo(term)
    def bar(term)
  end

  defmodule Behaviour do
    @callback foo() :: any
  end

  defmodule BehaviourWithArgument do
    @callback foo(any) :: any
  end

  defmodule BehaviourWithThreeArguments do
    @callback foo(any, any, any) :: any
  end

  defmodule UseBehaviourWithoutImpl do
    @callback foo_without_impl() :: any
    @callback bar_without_impl() :: any
    @callback baz_without_impl() :: any

    defmacro __using__(_opts) do
      quote do
        @behaviour Kernel.ImplTest.UseBehaviourWithoutImpl
        def foo_without_impl(), do: :auto_generated
      end
    end
  end

  defmodule UseBehaviourWithImpl do
    @callback foo_with_impl() :: any
    @callback bar_with_impl() :: any
    @callback baz_with_impl() :: any

    defmacro __using__(_opts) do
      quote do
        @behaviour Kernel.ImplTest.UseBehaviourWithImpl
        @impl true
        def foo_with_impl(), do: :auto_generated
        def bar_with_impl(), do: :auto_generated
      end
    end
  end

  defmodule MacroBehaviour do
    @macrocallback bar :: any
  end

  defmodule ManualBehaviour do
    def behaviour_info(:callbacks), do: [foo: 0]
    def behaviour_info(:optional_callbacks), do: :undefined
  end

  test "sets @impl to boolean" do
    defmodule ImplAttributes do
      @behaviour Behaviour

      @impl true
      def foo(), do: :ok

      @impl false
      def foo(term) do
        term
      end
    end
  end

  test "sets @impl to nil" do
    assert_raise ArgumentError, ~r/should be a module or a boolean/, fn ->
      defmodule ImplAttributes do
        @behaviour Behaviour
        @impl nil
        def foo(), do: :ok
      end
    end
  end

  test "sets @impl to behaviour" do
    defmodule ImplAttributes do
      @behaviour Behaviour
      @impl Behaviour
      def foo(), do: :ok
    end
  end

  test "does not set @impl" do
    defmodule ImplAttributes do
      @behaviour Behaviour
      def foo(), do: :ok
    end
  end

  test "sets @impl to boolean on manual behaviour" do
    defmodule ImplAttributes do
      @behaviour ManualBehaviour

      @impl true
      def foo(), do: :ok
    end
  end

  test "warns for undefined value" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour :abc

               @impl :abc
               def foo(), do: :ok
             end
             """)
           end) =~
             "got \"@impl :abc\" for function foo/0 but this behaviour does not specify such callback. There are no known callbacks"
  end

  test "warns for callbacks without impl and @impl has been set before" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @behaviour Kernel.ImplTest.MacroBehaviour

               @impl true
               def foo(), do: :ok

               defmacro bar(), do: :ok
             end
             """)
           end) =~
             "module attribute @impl was not set for macro bar/0 callback (specified in Kernel.ImplTest.MacroBehaviour)"
  end

  test "warns for callbacks without impl and @impl has been set after" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @behaviour Kernel.ImplTest.MacroBehaviour

               defmacro bar(), do: :ok

               @impl true
               def foo(), do: :ok
             end
             """)
           end) =~
             "module attribute @impl was not set for macro bar/0 callback (specified in Kernel.ImplTest.MacroBehaviour)"
  end

  test "warns when @impl is set on private function" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @impl true
               defp foo(), do: :ok
             end
             """)
           end) =~
             "function foo/0 is private, @impl attribute is always discarded for private functions/macros"
  end

  test "warns when @impl is set and no function" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @impl true
             end
             """)
           end) =~ "module attribute @impl was set but no definition follows it"
  end

  test "warns for @impl true and no behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @impl true
               def foo(), do: :ok
             end
             """)
           end) =~ "got \"@impl true\" for function foo/0 but no behaviour was declared"
  end

  test "warns for @impl true with callback name not in behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @impl true
               def bar(), do: :ok
             end
             """)
           end) =~
             "got \"@impl true\" for function bar/0 but no behaviour specifies such callback"
  end

  test "warns for @impl true with macro callback name not in behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.MacroBehaviour
               @impl true
               defmacro foo(), do: :ok
             end
             """)
           end) =~ "got \"@impl true\" for macro foo/0 but no behaviour specifies such callback"
  end

  test "warns for @impl true with callback kind not in behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.MacroBehaviour
               @impl true
               def foo(), do: :ok
             end
             """)
           end) =~
             "got \"@impl true\" for function foo/0 but no behaviour specifies such callback"
  end

  test "warns for @impl true with wrong arity" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @impl true
               def foo(arg), do: arg
             end
             """)
           end) =~
             "got \"@impl true\" for function foo/1 but no behaviour specifies such callback"
  end

  test "warns for @impl false and there are no callbacks" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @impl false
               def baz(term), do: term
             end
             """)
           end) =~ "got \"@impl false\" for function baz/1 but no behaviour was declared"
  end

  test "warns for @impl false and it is a callback" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @impl false
               def foo(), do: :ok
             end
             """)
           end) =~
             "got \"@impl false\" for function foo/0 but it is a callback specified in Kernel.ImplTest.Behaviour"
  end

  test "warns for @impl module and no behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @impl Kernel.ImplTest.Behaviour
               def foo(), do: :ok
             end
             """)
           end) =~
             "got \"@impl Kernel.ImplTest.Behaviour\" for function foo/0 but no behaviour was declared"
  end

  test "warns for @impl module with callback name not in behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @impl Kernel.ImplTest.Behaviour
               def bar(), do: :ok
             end
             """)
           end) =~
             "got \"@impl Kernel.ImplTest.Behaviour\" for function bar/0 but this behaviour does not specify such callback"
  end

  test "warns for @impl module with macro callback name not in behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.MacroBehaviour
               @impl Kernel.ImplTest.MacroBehaviour
               defmacro foo(), do: :ok
             end
             """)
           end) =~
             "got \"@impl Kernel.ImplTest.MacroBehaviour\" for macro foo/0 but this behaviour does not specify such callback"
  end

  test "warns for @impl module with macro callback kind not in behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.MacroBehaviour
               @impl Kernel.ImplTest.MacroBehaviour
               def foo(), do: :ok
             end
             """)
           end) =~
             "got \"@impl Kernel.ImplTest.MacroBehaviour\" for function foo/0 but this behaviour does not specify such callback"
  end

  test "warns for @impl module and callback belongs to another known module" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @behaviour Kernel.ImplTest.MacroBehaviour
               @impl Kernel.ImplTest.Behaviour
               def bar(), do: :ok
             end
             """)
           end) =~
             "got \"@impl Kernel.ImplTest.Behaviour\" for function bar/0 but this behaviour does not specify such callback"
  end

  test "warns for @impl module and callback belongs to another unknown module" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @impl Kernel.ImplTest.MacroBehaviour
               def bar(), do: :ok
             end
             """)
           end) =~
             "got \"@impl Kernel.ImplTest.MacroBehaviour\" for function bar/0 but this behaviour was not declared with @behaviour"
  end

  test "does not warn for @impl when the function with default conforms with several typespecs" do
    Code.eval_string(~S"""
    defmodule Kernel.ImplTest.ImplAttributes do
      @behaviour Kernel.ImplTest.Behaviour
      @behaviour Kernel.ImplTest.BehaviourWithArgument

      @impl true
      def foo(args \\ []), do: args
    end
    """)
  end

  test "does not warn for @impl when the function conforms to behaviour but has default value for arg" do
    Code.eval_string(~S"""
    defmodule Kernel.ImplTest.ImplAttributes do
      @behaviour Kernel.ImplTest.BehaviourWithArgument

      @impl true
      def foo(args \\ []), do: args
    end
    """)
  end

  test "does not warn for @impl when the function conforms to behaviour but has additional trailing default args" do
    Code.eval_string(~S"""
    defmodule Kernel.ImplTest.ImplAttributes do
      @behaviour Kernel.ImplTest.BehaviourWithArgument

      @impl true
      def foo(arg_1, _args \\ []), do: arg_1
    end
    """)
  end

  test "does not warn for @impl when the function conforms to behaviour but has additional leading default args" do
    Code.eval_string(~S"""
    defmodule Kernel.ImplTest.ImplAttributes do
      @behaviour Kernel.ImplTest.BehaviourWithArgument

      @impl true
      def foo(_defaulted_arg \\ [], args), do: args
    end
    """)
  end

  test "does not warn for @impl when the function has more args than callback, but they're all defaulted" do
    Code.eval_string(~S"""
    defmodule Kernel.ImplTest.ImplAttributes do
      @behaviour Kernel.ImplTest.BehaviourWithArgument

      @impl true
      def foo(args \\ [], _bar \\ []), do: args
    end
    """)
  end

  test "does not warn for @impl with defaults when the same function is defined multiple times" do
    Code.eval_string(~S"""
    defmodule Kernel.ImplTest.ImplAttributes do
      @behaviour Kernel.ImplTest.BehaviourWithArgument
      @behaviour Kernel.ImplTest.BehaviourWithThreeArguments

      @impl Kernel.ImplTest.BehaviourWithArgument
      def foo(_foo \\ [], _bar \\ []), do: :ok

      @impl Kernel.ImplTest.BehaviourWithThreeArguments
      def foo(_foo, _bar, _baz, _qux \\ []), do: :ok
    end
    """)
  end

  test "does not warn for no @impl when overriding callback" do
    Code.eval_string(~S"""
    defmodule Kernel.ImplTest.ImplAttributes do
      @behaviour Kernel.ImplTest.Behaviour

      def foo(), do: :overridable

      defoverridable Kernel.ImplTest.Behaviour

      def foo(), do: :overridden
    end
    """)
  end

  test "does not warn for overridable function missing @impl" do
    Code.eval_string(~S"""
    defmodule Kernel.ImplTest.ImplAttributes do
      @behaviour Kernel.ImplTest.Behaviour

      def foo(), do: :overridable

      defoverridable Kernel.ImplTest.Behaviour

      @impl Kernel.ImplTest.Behaviour
      def foo(), do: :overridden
    end
    """)
  end

  test "warns correctly for missing @impl only for end-user implemented function" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @behaviour Kernel.ImplTest.MacroBehaviour

               def foo(), do: :overridable

               defoverridable Kernel.ImplTest.Behaviour

               def foo(), do: :overridden

               @impl true
               defmacro bar(), do: :overridden
             end
             """)
           end) =~
             "module attribute @impl was not set for function foo/0 callback (specified in Kernel.ImplTest.Behaviour)"
  end

  test "warns correctly for incorrect @impl in overridable callback" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Kernel.ImplTest.ImplAttributes do
               @behaviour Kernel.ImplTest.Behaviour
               @behaviour Kernel.ImplTest.MacroBehaviour

               @impl Kernel.ImplTest.MacroBehaviour
               def foo(), do: :overridable

               defoverridable Kernel.ImplTest.Behaviour

               @impl Kernel.ImplTest.Behaviour
               def foo(), do: :overridden
             end
             """)
           end) =~
             "got \"@impl Kernel.ImplTest.MacroBehaviour\" for function foo/0 but this behaviour does not specify such callback"
  end

  test "warns only for non-generated functions in non-generated @impl" do
    message =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Kernel.ImplTest.ImplAttributes do
          @behaviour Kernel.ImplTest.Behaviour
          use Kernel.ImplTest.UseBehaviourWithoutImpl

          @impl true
          def bar_without_impl(), do: :overridden
          def baz_without_impl(), do: :overridden

          defdelegate foo(), to: __MODULE__, as: :baz
          def baz(), do: :ok
        end
        """)
      end)

    assert message =~
             "module attribute @impl was not set for function baz_without_impl/0 callback"

    assert message =~
             "module attribute @impl was not set for function foo/0 callback"

    refute message =~ "foo_without_impl/0"
  end

  test "warns only for non-generated functions in non-generated @impl in protocols" do
    message =
      capture_err(fn ->
        Code.eval_string("""
        defimpl  Kernel.ImplTest.AProtocol, for: List do
          @impl true
          def foo(_list), do: :ok

          defdelegate bar(list), to: __MODULE__, as: :baz
          def baz(_list), do: :ok
        end
        """)
      end)

    assert message =~
             "module attribute @impl was not set for function bar/1 callback"
  end

  test "warns only for generated functions in generated @impl" do
    message =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Kernel.ImplTest.ImplAttributes do
          use Kernel.ImplTest.UseBehaviourWithImpl
          def baz_with_impl(), do: :overridden
        end
        """)
      end)

    assert message =~ "module attribute @impl was not set for function bar_with_impl/0 callback"
    refute message =~ "foo_with_impl/0"
  end

  test "does not warn for overridable callback when using __before_compile__/1 hook" do
    Code.eval_string(~S"""
    defmodule BeforeCompile do
      defmacro __before_compile__(_) do
        quote do
          @behaviour Kernel.ImplTest.Behaviour

          def foo(), do: :overridable

          defoverridable Kernel.ImplTest.Behaviour
        end
      end
    end

    defmodule Kernel.ImplTest.ImplAttributes do
      @before_compile BeforeCompile
      @behaviour Kernel.ImplTest.MacroBehaviour

      defmacro bar(), do: :overridable

      defoverridable Kernel.ImplTest.MacroBehaviour

      @impl Kernel.ImplTest.MacroBehaviour
      defmacro bar(), do: :overridden
    end
    """)
  end
end
