# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec

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

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

  import ExUnit.CaptureIO

  describe "defguard(p) usage" do
    defmodule GuardsInMacros do
      defguard is_foo(atom) when atom == :foo

      defmacro is_compile_time_foo(atom) when is_foo(atom) do
        quote do: unquote(__MODULE__).is_foo(unquote(atom))
      end
    end

    test "guards can be used in other macros in the same module" do
      require GuardsInMacros
      assert GuardsInMacros.is_foo(:foo)
      refute GuardsInMacros.is_foo(:baz)
      assert GuardsInMacros.is_compile_time_foo(:foo)
    end

    defmodule GuardsInFuns do
      defguard is_foo(atom) when atom == :foo
      defguard is_equal(foo, bar) when foo == bar

      def foo?(atom) when is_foo(atom) do
        is_foo(atom)
      end
    end

    test "guards can be used in other funs in the same module" do
      require GuardsInFuns
      assert GuardsInFuns.is_foo(:foo)
      refute GuardsInFuns.is_foo(:bar)
    end

    test "guards do not change code evaluation semantics" do
      require GuardsInFuns
      x = 1
      assert GuardsInFuns.is_equal(x = 2, x) == false
      assert x == 2
    end

    defmodule MacrosInGuards do
      defmacro is_foo(atom) do
        quote do
          unquote(atom) == :foo
        end
      end

      defguard is_foobar(atom) when is_foo(atom) or atom == :bar
    end

    test "macros can be used in other guards in the same module" do
      require MacrosInGuards
      assert MacrosInGuards.is_foobar(:foo)
      assert MacrosInGuards.is_foobar(:bar)
      refute MacrosInGuards.is_foobar(:baz)
    end

    defmodule UnquotedInGuardCall do
      @value :foo

      defguard unquote(String.to_atom("is_#{@value}"))(x) when x == unquote(@value)
    end

    test "guards names can be defined dynamically using unquote" do
      require UnquotedInGuardCall
      assert UnquotedInGuardCall.is_foo(:foo)
      refute UnquotedInGuardCall.is_foo(:bar)
    end

    defmodule GuardsInGuards do
      defguard is_foo(atom) when atom == :foo
      defguard is_foobar(atom) when is_foo(atom) or atom == :bar
    end

    test "guards can be used in other guards in the same module" do
      require GuardsInGuards
      assert GuardsInGuards.is_foobar(:foo)
      assert GuardsInGuards.is_foobar(:bar)
      refute GuardsInGuards.is_foobar(:baz)
    end

    defmodule DefaultArgs do
      defguard is_divisible(value, remainder \\ 2)
               when is_integer(value) and rem(value, remainder) == 0
    end

    test "permits default values in args" do
      require DefaultArgs
      assert DefaultArgs.is_divisible(2)
      refute DefaultArgs.is_divisible(1)
      assert DefaultArgs.is_divisible(3, 3)
      refute DefaultArgs.is_divisible(3, 4)
    end

    test "doesn't allow matching in args" do
      assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
        defmodule Integer.Args do
          defguard foo(value, 1) when is_integer(value)
        end
      end

      assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
        defmodule String.Args do
          defguard foo(value, "string") when is_integer(value)
        end
      end

      assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
        defmodule Atom.Args do
          defguard foo(value, :atom) when is_integer(value)
        end
      end

      assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
        defmodule Tuple.Args do
          defguard foo(value, {foo, bar}) when is_integer(value)
        end
      end
    end

    defmodule GuardFromMacro do
      defmacro __using__(_) do
        quote do
          defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
        end
      end
    end

    test "defguard defines a guard from inside another macro" do
      defmodule UseGuardFromMacro do
        use GuardFromMacro

        def assert! do
          assert is_even(0)
          refute is_even(1)
        end
      end

      UseGuardFromMacro.assert!()
    end

    defmodule IntegerPrivateGuards do
      defguardp is_even(value) when is_integer(value) and rem(value, 2) == 0

      def even_and_large?(value) when is_even(value) and value > 100, do: true
      def even_and_large?(_), do: false

      def even_and_small?(value) do
        if is_even(value) and value <= 100, do: true, else: false
      end
    end

    test "defguardp defines private guards that work inside and outside guard clauses" do
      assert IntegerPrivateGuards.even_and_large?(102)
      refute IntegerPrivateGuards.even_and_large?(98)
      refute IntegerPrivateGuards.even_and_large?(99)
      refute IntegerPrivateGuards.even_and_large?(103)

      assert IntegerPrivateGuards.even_and_small?(98)
      refute IntegerPrivateGuards.even_and_small?(99)
      refute IntegerPrivateGuards.even_and_small?(102)
      refute IntegerPrivateGuards.even_and_small?(103)

      assert_compile_error(~r"cannot find or invoke local is_even/1", fn ->
        defmodule IntegerPrivateGuardUtils do
          import IntegerPrivateGuards

          def even_and_large?(value) when is_even(value) and value > 100, do: true
          def even_and_large?(_), do: false
        end
      end)

      assert_compile_error(~r"undefined function is_even/1", fn ->
        defmodule IntegerPrivateFunctionUtils do
          import IntegerPrivateGuards

          def even_and_small?(value) do
            if is_even(value) and value <= 100, do: true, else: false
          end
        end
      end)
    end

    test "requires a proper macro name" do
      assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
        defmodule(LiteralUsage, do: defguard("literal is bad"))
      end

      assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
        defmodule(RemoteUsage, do: defguard(Remote.call(is_bad)))
      end
    end

    test "handles overriding appropriately" do
      assert_compile_error(~r"defmacro (.*?) already defined as def", fn ->
        defmodule OverriddenFunUsage do
          def foo(bar), do: bar
          defguard foo(bar) when bar
        end
      end)

      assert_compile_error(~r"defmacro (.*?) already defined as defp", fn ->
        defmodule OverriddenPrivateFunUsage do
          defp foo(bar), do: bar
          defguard foo(bar) when bar
        end
      end)

      assert_compile_error(~r"defmacro (.*?) already defined as defmacrop", fn ->
        defmodule OverriddenPrivateFunUsage do
          defmacrop foo(bar), do: bar
          defguard foo(bar) when bar
        end
      end)

      assert_compile_error(~r"defmacrop (.*?) already defined as def", fn ->
        defmodule OverriddenFunUsage do
          def foo(bar), do: bar
          defguardp foo(bar) when bar
        end
      end)

      assert_compile_error(~r"defmacrop (.*?) already defined as defp", fn ->
        defmodule OverriddenPrivateFunUsage do
          defp foo(bar), do: bar
          defguardp foo(bar) when bar
        end
      end)

      assert_compile_error(~r"defmacrop (.*?) already defined as defmacro", fn ->
        defmodule OverriddenPrivateFunUsage do
          defmacro foo(bar), do: bar
          defguardp foo(bar) when bar
        end
      end)
    end

    test "does not allow multiple guard clauses" do
      assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
        defmodule MultiGuardUsage do
          defguardp foo(bar, baz) when bar == 1 when baz == 2
        end
      end
    end

    test "does not accept a block" do
      assert_compile_error(~r"undefined function defguard/2", fn ->
        defmodule OnelinerBlockUsage do
          defguard(foo(bar), do: one_liner)
        end
      end)

      assert_compile_error(~r"undefined function defguard/2", fn ->
        defmodule MultilineBlockUsage do
          defguard foo(bar) do
            multi
            liner
          end
        end
      end)

      assert_compile_error(~r"undefined function defguard/2", fn ->
        defmodule ImplAndBlockUsage do
          defguard(foo(bar) when both_given, do: error)
        end
      end)
    end
  end

  describe "defguard(p) compilation" do
    test "refuses to compile nonsensical code" do
      assert_compile_error("cannot find or invoke local undefined/1", fn ->
        defmodule UndefinedUsage do
          defguard foo(function) when undefined(function)
        end
      end)
    end

    test "fails on expressions not allowed in guards" do
      # Slightly unique errors

      assert_raise ArgumentError, ~r{invalid right argument for operator "in"}, fn ->
        defmodule RuntimeListUsage do
          defguard foo(bar, baz) when bar in baz
        end
      end

      assert_compile_error("cannot invoke remote function", fn ->
        defmodule BadErlangFunctionUsage do
          defguard foo(bar) when :erlang.binary_to_atom("foo")
        end
      end)

      assert_compile_error("cannot invoke remote function", fn ->
        defmodule SendUsage do
          defguard foo(bar) when send(self(), :baz)
        end
      end)

      # Consistent errors

      assert_raise ArgumentError, ~r"invalid expression in guard, ! is not allowed", fn ->
        defmodule SoftNegationLogicUsage do
          defguard foo(logic) when !logic
        end
      end

      assert_raise ArgumentError, ~r"invalid expression in guard, && is not allowed", fn ->
        defmodule SoftAndLogicUsage do
          defguard foo(soft, logic) when soft && logic
        end
      end

      assert_raise ArgumentError, ~r"invalid expression in guard, || is not allowed", fn ->
        defmodule SoftOrLogicUsage do
          defguard foo(soft, logic) when soft || logic
        end
      end

      assert_compile_error(
        "cannot invoke remote function :erlang\.is_record/2 inside a guard",
        fn ->
          defmodule IsRecord2Usage do
            defguard foo(rec) when :erlang.is_record(rec, :tag)
          end
        end
      )

      assert_compile_error(
        "cannot invoke remote function :erlang\.is_record/3 inside a guard",
        fn ->
          defmodule IsRecord3Usage do
            defguard foo(rec) when :erlang.is_record(rec, :tag, 7)
          end
        end
      )

      assert_compile_error(
        ~r"cannot invoke remote function :erlang\.\+\+/2 inside a guard",
        fn ->
          defmodule ListSubtractionUsage do
            defguard foo(list) when list ++ []
          end
        end
      )

      assert_compile_error(
        "cannot invoke remote function :erlang\.\-\-/2 inside a guard",
        fn ->
          defmodule ListSubtractionUsage do
            defguard foo(list) when list -- []
          end
        end
      )

      assert_compile_error("invalid expression in guard", fn ->
        defmodule LocalCallUsage do
          defguard foo(local, call) when local.(call)
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule ComprehensionUsage do
          defguard foo(bar) when for(x <- [1, 2, 3], do: x * bar)
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule AliasUsage do
          defguard foo(bar) when alias(bar)
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule ImportUsage do
          defguard foo(bar) when import(bar)
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule RequireUsage do
          defguard foo(bar) when require(bar)
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule SuperUsage do
          defguard foo(bar) when super(bar)
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule SpawnUsage do
          defguard foo(bar) when spawn(& &1)
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule ReceiveUsage do
          defguard foo(bar) when receive(do: (baz -> baz))
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule CaseUsage do
          defguard foo(bar) when case(bar, do: (baz -> :baz))
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule CondUsage do
          defguard foo(bar) when cond(do: (bar -> :baz))
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule TryUsage do
          defguard foo(bar) when try(do: (baz -> baz))
        end
      end)

      assert_compile_error("invalid expression in guard", fn ->
        defmodule WithUsage do
          defguard foo(bar) when with(do: (baz -> baz))
        end
      end)

      assert_compile_error(
        "cannot invoke remote function inside a guard. " <>
          "If you want to do a map lookup instead, please remove parens from map.field()",
        fn ->
          defmodule MapDot do
            def map_dot(map) when map.field(), do: true
          end
        end
      )

      assert_compile_error("cannot invoke remote function Module.fun/0 inside a guard", fn ->
        defmodule MapDot do
          def map_dot(map) when Module.fun(), do: true
        end
      end)
    end
  end

  describe "defguard(p) expansion" do
    defguard with_unused_vars(foo, bar, _baz) when foo + bar

    test "doesn't obscure unused variables" do
      args = quote(do: [1 + 1, 2 + 2, 3 + 3])

      assert expand_defguard_to_string(:with_unused_vars, args, :guard) == """
             :erlang.+(1 + 1, 2 + 2)
             """

      assert expand_defguard_to_string(:with_unused_vars, args, nil) == """
             {arg1, arg2} = {1 + 1, 2 + 2}
             :erlang.+(arg1, arg2)
             """
    end

    defguard with_reused_vars(foo, bar, baz) when foo + foo + bar + baz

    test "handles re-used variables" do
      args = quote(do: [1 + 1, 2 + 2, 3 + 3])

      assert expand_defguard_to_string(:with_reused_vars, args, :guard) == """
             :erlang.+(:erlang.+(:erlang.+(1 + 1, 1 + 1), 2 + 2), 3 + 3)
             """

      assert expand_defguard_to_string(:with_reused_vars, args, nil) == """
             {arg1, arg2, arg3} = {1 + 1, 2 + 2, 3 + 3}
             :erlang.+(:erlang.+(:erlang.+(arg1, arg1), arg2), arg3)
             """
    end

    defguard with_or_and_or(foo, bar, baz) when foo or (bar and (baz or foo))

    test "expands conditionals" do
      args = quote(do: [1 + 1, 2 + 2, 3 + 3])

      assert expand_defguard_to_string(:with_or_and_or, args, :guard) == """
             :erlang.orelse(1 + 1, :erlang.andalso(2 + 2, :erlang.orelse(3 + 3, 1 + 1)))
             """

      assert expand_defguard_to_string(:with_or_and_or, args, nil) == """
             {arg1, arg2, arg3} = {1 + 1, 2 + 2, 3 + 3}
             :erlang.orelse(arg1, :erlang.andalso(arg2, :erlang.orelse(arg3, arg1)))
             """
    end

    defguard in_list(a) when Kernel.in(a, [:test])

    test "expands remote functions" do
      assert expand_defguard_to_string(:in_list, [:not_test], :guard) == """
             :erlang.\"=:=\"(:not_test, :test)
             """
    end

    defp expand_defguard_to_string(fun, args, context) do
      {{:., [], [__MODULE__, fun]}, [], args}
      |> Macro.expand(%{__ENV__ | context: context})
      |> Macro.to_string()
      |> Kernel.<>("\n")
    end
  end

  defp assert_compile_error(message, fun) do
    assert capture_io(:stderr, fn ->
             assert_raise CompileError, fun
           end) =~ message
  end
end
