Code.require_file "test_helper.exs", __DIR__

defmodule ModuleTest.ToBeUsed do
  def value, do: 1

  defmacro __using__(_) do
    target = __CALLER__.module
    Module.put_attribute(target, :has_callback, true)
    Module.put_attribute(target, :before_compile, __MODULE__)
    Module.put_attribute(target, :after_compile, __MODULE__)
    Module.put_attribute(target, :before_compile, {__MODULE__, :callback})
    quote do: (def line, do: __ENV__.line)
  end

  defmacro __before_compile__(env) do
    quote do: (def before_compile, do: unquote(env.vars))
  end

  defmacro __after_compile__(%Macro.Env{module: ModuleTest.ToUse, vars: []}, bin) when is_binary(bin) do
    # IO.puts "HELLO"
  end

  defmacro callback(env) do
    value = Module.get_attribute(env.module, :has_callback)
    quote do
      def callback_value(true), do: unquote(value)
    end
  end
end

defmodule ModuleTest.ToUse do
  32 = __ENV__.line # Moving the next line around can make tests fail
  var = 1
  _ = var # Not available in callbacks
  def callback_value(false), do: false
  use ModuleTest.ToBeUsed
end

defmodule ModuleTest do
  use ExUnit.Case, async: true

  doctest Module

  Module.register_attribute __MODULE__, :register_example, accumulate: true, persist: true
  @register_example :it_works
  @register_example :still_works

  contents = quote do
    def eval_quoted_info, do: {__MODULE__, __ENV__.file, __ENV__.line}
  end
  Module.eval_quoted __MODULE__, contents, [], file: "sample.ex", line: 13

  defmacrop in_module(block) do
    quote do
      defmodule Temp, unquote(block)
      :code.purge(Temp)
      :code.delete(Temp)
    end
  end

  test "module attributes returns value" do
    in_module do
      assert (@return [:foo, :bar]) == [:foo, :bar]
      _ = @return
    end
  end

  test "in memory modules are tagged as so" do
    assert :code.which(__MODULE__) == :in_memory
  end

  ## Eval

  test "executes eval_quoted definitions" do
    assert eval_quoted_info() == {ModuleTest, "sample.ex", 13}
  end

  test "retrieves line from macros" do
    assert ModuleTest.ToUse.line == 36
  end

  ## Callbacks

  test "executes custom before_compile callback" do
    assert ModuleTest.ToUse.callback_value(true) == true
    assert ModuleTest.ToUse.callback_value(false) == false
  end

  test "executes default before_compile callback" do
    assert ModuleTest.ToUse.before_compile == []
  end

  def __on_definition__(env, kind, name, args, guards, expr) do
    Process.put(env.module, :called)
    assert env.module == ModuleTest.OnDefinition
    assert kind == :def
    assert name == :hello
    assert [{:foo, _, _}, {:bar, _, _}] = args
    assert [] = guards
    assert {:+, _, [{:foo, _, nil}, {:bar, _, nil}]} = expr
  end

  test "executes on definition callback" do
    defmodule OnDefinition do
      @on_definition ModuleTest

      def hello(foo, bar) do
        foo + bar
      end
    end

    assert Process.get(ModuleTest.OnDefinition) == :called
  end

  defmacro __before_compile__(_) do
    quote do
      def constant, do: 1
      defoverridable constant: 0
    end
  end

  test "may set overridable inside before_compile callback" do
    defmodule OverridableWithBeforeCompile do
      @before_compile ModuleTest
    end
    assert OverridableWithBeforeCompile.constant == 1
  end

  ## Attributes

  test "reserved attributes" do
    assert List.keyfind(ExUnit.Server.__info__(:attributes), :behaviour, 0) == {:behaviour, [GenServer]}
  end

  test "registered attributes" do
    assert [{:register_example, [:it_works]}, {:register_example, [:still_works]}] ==
      Enum.filter __MODULE__.__info__(:attributes), &match?({:register_example, _}, &1)
  end

  @some_attribute [1]
  @other_attribute [3, 2, 1]

  test "inside function attributes" do
    assert @some_attribute == [1]
    assert @other_attribute == [3, 2, 1]
  end

  test "@compile autoload attribute" do
    defmodule NoAutoload do
      @compile {:autoload, false}
    end
    refute :code.is_loaded(NoAutoload)
  end

  ## Naming

  test "concat" do
    assert Module.concat(Foo, Bar)  == Foo.Bar
    assert Module.concat(Foo, :Bar) == Foo.Bar
    assert Module.concat(Foo, "Bar") == Foo.Bar
    assert Module.concat(Foo, Bar.Baz) == Foo.Bar.Baz
    assert Module.concat(Foo, "Bar.Baz") == Foo.Bar.Baz
    assert Module.concat(Bar, nil) == Elixir.Bar
  end

  test "safe concat" do
    assert Module.safe_concat(Foo, :Bar) == Foo.Bar
    assert_raise ArgumentError, fn ->
      Module.safe_concat SafeConcat, Doesnt.Exist
    end
  end

  test "split" do
    module = Very.Long.Module.Name.And.Even.Longer
    assert Module.split(module) == ["Very", "Long", "Module", "Name", "And", "Even", "Longer"]
    assert Module.split("Elixir.Very.Long") == ["Very", "Long"]
    assert_raise FunctionClauseError, fn ->
      Module.split(:just_an_atom)
    end
    assert_raise FunctionClauseError, fn ->
      Module.split("Foo")
    end
    assert Module.concat(Module.split(module)) == module
  end

  test "__MODULE__" do
    assert Code.eval_string("__MODULE__.Foo") |> elem(0) == Foo
  end

  test "__ENV__.file" do
    assert Path.basename(__ENV__.file) == "module_test.exs"
  end

  @file "sample.ex"
  test "__ENV__.file with module attribute" do
    assert __ENV__.file == "sample.ex"
  end

  ## Creation

  test "defmodule" do
    assert match?({:module, Defmodule, binary, 3} when is_binary(binary), defmodule Defmodule do
      1 + 2
    end)
  end

  test "defmodule with atom" do
    assert match?({:module, :root_defmodule, _, _}, defmodule :root_defmodule do
      :ok
    end)
  end

  test "defmodule with alias as atom" do
    defmodule :"Elixir.ModuleTest.RawModule" do
      def hello, do: :world
    end

    assert RawModule.hello == :world
  end

  test "create" do
    contents =
      quote do
        def world, do: true
      end
    {:module, ModuleCreateSample, _, _} =
      Module.create(ModuleCreateSample, contents, __ENV__)
    assert ModuleCreateSample.world
  end

  test "create with elixir as a name" do
    contents =
      quote do
        def world, do: true
      end
    assert_raise CompileError, fn ->
      {:module, Elixir, _, _} =
        Module.create(Elixir, contents, __ENV__)
    end
  end

  test "create with aliases/var hygiene" do
    contents =
      quote do
        alias List, as: L
        def test do
          L.flatten([1, [2], 3])
        end
      end

    Module.create ModuleHygiene, contents, __ENV__
    assert ModuleHygiene.test == [1, 2, 3]
  end

  test "ensure function clauses are ordered" do
    {_, _, binary, _} =
      defmodule Ordered do
        def foo(:foo), do: :bar
        def baz(:baz), do: :bat
      end
    atoms = :beam_lib.chunks(binary, [:atoms])
    assert :erlang.phash2(atoms) == 53987778
  end

  # TODO: Remove this check once we depend only on 19
  if :erlang.system_info(:otp_release) >= '19' do
    test "create with generated true does not emit warnings" do
      contents =
        quote generated: true do
          def world, do: true
          def world, do: false
        end
      {:module, ModuleCreateGenerated, _, _} =
        Module.create(ModuleCreateGenerated, contents, __ENV__)
      assert ModuleCreateGenerated.world
    end
  end

  test "no function in module body" do
    in_module do
      assert __ENV__.function == nil
    end
  end

  ## Definitions

  test "defines?" do
    in_module do
      refute Module.defines? __MODULE__, {:foo, 0}
      def foo(), do: bar()
      assert Module.defines? __MODULE__, {:foo, 0}
      assert Module.defines? __MODULE__, {:foo, 0}, :def

      refute Module.defines? __MODULE__, {:bar, 0}, :defp
      defp bar(), do: :ok
      assert Module.defines? __MODULE__, {:bar, 0}, :defp

      refute Module.defines? __MODULE__, {:baz, 0}, :defmacro
      defmacro baz(), do: :ok
      assert Module.defines? __MODULE__, {:baz, 0}, :defmacro
    end
  end

  test "definitions in" do
    in_module do
      def foo(1, 2, 3), do: 4

      assert Module.definitions_in(__MODULE__)        == [foo: 3]
      assert Module.definitions_in(__MODULE__, :def)  == [foo: 3]
      assert Module.definitions_in(__MODULE__, :defp) == []
    end
  end
end
