# 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.TracersTest do
  use ExUnit.Case

  defp compile_string(string) do
    string
    |> Code.string_to_quoted!(columns: true)
    |> Code.compile_quoted()
  end

  def trace(event, %Macro.Env{} = env) do
    for {pid, _} <- Registry.lookup(__MODULE__, :tracers) do
      send(pid, {event, env})
    end

    :ok
  end

  setup_all do
    start_supervised!({Registry, keys: :duplicate, name: __MODULE__})
    Code.put_compiler_option(:tracers, [__MODULE__])

    on_exit(fn ->
      Code.put_compiler_option(:tracers, [])
    end)
  end

  setup do
    Registry.register(__MODULE__, :tracers, :unused)
    :ok
  end

  test "traces start and stop" do
    compile_string("""
    Foo
    """)

    assert_received {:start, %{lexical_tracker: pid}} when is_pid(pid)
    assert_received {:stop, %{lexical_tracker: pid}} when is_pid(pid)
  end

  test "traces alias references" do
    compile_string("""
    Foo
    """)

    assert_received {{:alias_reference, meta, Foo}, _}
    assert meta[:line] == 1
    assert meta[:column] == 1
  end

  test "traces aliases" do
    compile_string("""
    alias Hello.World
    World

    alias Foo, as: Bar, warn: true
    Bar
    """)

    assert_received {{:alias, meta, Hello.World, World, []}, _}
    assert meta[:line] == 1
    assert meta[:column] == 1
    assert_received {{:alias_expansion, meta, World, Hello.World}, _}
    assert meta[:line] == 2
    assert meta[:column] == 1

    assert_received {{:alias, meta, Foo, Bar, [as: Bar, warn: true]}, _}
    assert meta[:line] == 4
    assert meta[:column] == 1
    assert_received {{:alias_expansion, meta, Bar, Foo}, _}
    assert meta[:line] == 5
    assert meta[:column] == 1
  end

  test "traces imports" do
    compile_string("""
    import Integer, only: [is_odd: 1, parse: 1]
    true = is_odd(1)
    {1, ""} = parse("1")
    """)

    assert_received {{:import, meta, Integer, only: [is_odd: 1, parse: 1]}, _}
    assert meta[:line] == 1
    assert meta[:column] == 1

    assert_received {{:imported_macro, meta, Integer, :is_odd, 1}, _}
    assert meta[:line] == 2
    assert meta[:column] == 8

    assert_received {{:imported_function, meta, Integer, :parse, 1}, _}
    assert meta[:line] == 3
    assert meta[:column] == 11

    refute_received {{:remote_function, _, Integer, :parse, 1}, _}
  end

  test "traces imports via capture" do
    compile_string("""
    import Integer, only: [is_odd: 1, parse: 1]
    &is_odd/1
    &parse/1
    """)

    assert_received {{:import, meta, Integer, only: [is_odd: 1, parse: 1]}, _}
    assert meta[:line] == 1
    assert meta[:column] == 1

    assert_received {{:imported_macro, meta, Integer, :is_odd, 1}, _}
    assert meta[:line] == 2
    assert meta[:column] == 2

    assert_received {{:imported_function, meta, Integer, :parse, 1}, _}
    assert meta[:line] == 3
    assert meta[:column] == 2

    refute_received {{:remote_function, _meta, Integer, :parse, 1}, _}
  end

  test "traces structs" do
    compile_string("""
    %URI{path: "/"}
    """)

    assert_received {{:struct_expansion, meta, URI, [:path]}, _}
    assert meta[:line] == 1
    assert meta[:column] == 1
  end

  test "traces remote" do
    compile_string("""
    require Integer
    true = Integer.is_odd(1)
    {1, ""} = Integer.parse("1")
    "foo" = Atom.to_string(:foo)
    """)

    assert_received {{:remote_macro, meta, Integer, :is_odd, 1}, _}
    assert meta[:line] == 2
    assert meta[:column] == 16

    assert_received {{:remote_function, meta, Integer, :parse, 1}, _}
    assert meta[:line] == 3
    assert meta[:column] == 19

    assert_received {{:remote_function, meta, Atom, :to_string, 1}, _}
    assert meta[:line] == 4
    assert meta[:column] == 14
  end

  test "traces remote via captures" do
    compile_string("""
    require Integer
    &Integer.is_odd/1
    &Integer.parse/1
    """)

    assert_received {{:remote_macro, meta, Integer, :is_odd, 1}, _}
    assert meta[:line] == 2
    assert meta[:column] == 10

    assert_received {{:remote_function, meta, Integer, :parse, 1}, _}
    assert meta[:line] == 3
    assert meta[:column] == 10
  end

  test "traces locals" do
    compile_string("""
    defmodule Sample do
      defmacro foo(arg), do: arg
      def bar(arg), do: arg
      def baz(arg), do: foo(arg) + bar(arg)
    end
    """)

    assert_received {{:local_macro, meta, :foo, 1}, _}
    assert meta[:line] == 4
    assert meta[:column] == 21

    assert_received {{:local_function, meta, :bar, 1}, _}
    assert meta[:line] == 4
    assert meta[:column] == 32
  after
    :code.purge(Sample)
    :code.delete(Sample)
  end

  test "traces locals with capture" do
    compile_string("""
    defmodule Sample do
      defmacro foo(arg), do: arg
      def bar(arg), do: arg
      def baz(_), do: {&foo/1, &bar/1}
    end
    """)

    assert_received {{:local_macro, meta, :foo, 1}, _}
    assert meta[:line] == 4
    assert meta[:column] == 21

    assert_received {{:local_function, meta, :bar, 1}, _}
    assert meta[:line] == 4
    assert meta[:column] == 29
  after
    :code.purge(Sample)
    :code.delete(Sample)
  end

  test "traces modules" do
    compile_string("""
    defmodule Sample do
      :ok
    end
    """)

    assert_received {:defmodule, %{module: Sample, function: nil}}
    assert_received {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}}
  after
    :code.purge(Sample)
    :code.delete(Sample)
  end

  test "traces dynamic modules" do
    compile_string("""
    Module.create(Sample, :ok, __ENV__)
    """)

    assert_received {:defmodule, %{module: Sample, function: nil}}
    assert_received {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}}
  after
    :code.purge(Sample)
    :code.delete(Sample)
  end

  test "traces module attribute expansion" do
    compile_string("""
    defmodule TracersModuleAttribute do
      @module URI
      @module
    end
    """)

    assert_received {{:alias_reference, [line: 3], URI}, %{file: "@module"}}
  end

  test "traces string interpolation" do
    compile_string("""
    arg = 1 + 2
    "foo\#{arg}"
    """)

    assert_received {{:remote_macro, meta, Kernel, :to_string, 1}, _env}
    assert meta[:from_interpolation]
  end

  test "traces bracket access" do
    compile_string("""
    foo = %{bar: 3}
    foo[:bar]
    """)

    assert_received {{:remote_function, meta, Access, :get, 2}, _env}
    assert meta[:from_brackets]

    compile_string("""
    defmodule TracerBracketAccess do
      @foo %{bar: 3}
      def a() do
        @foo[:bar]
      end
    end
    """)

    assert_received {{:remote_function, meta, Access, :get, 2}, _env}
    assert meta[:from_brackets]

    compile_string("""
    %{bar: 3}[:bar]
    """)

    assert_received {{:remote_function, meta, Access, :get, 2}, _env}
    assert meta[:from_brackets]
  end

  test "traces on_load" do
    compile_string("""
    defmodule TracerOnLoad do
      @on_load :init
      def init, do: :ok
    end
    """)

    assert_received {{:local_function, meta, :init, 0}, _}
    assert meta[:line] == 1
  end

  def __before_compile__(_), do: :ok
  def __after_compile__(_, _), do: :ok
  def __after_verify__(_), do: :ok
  def __on_definition__(_, _, _, _, _, _), do: :ok

  test "traces compile time attributes" do
    compile_string("""
    defmodule TracerCompileAttributes do
      @before_compile Kernel.TracersTest
      @after_compile Kernel.TracersTest
      @on_definition Kernel.TracersTest
      @after_verify Kernel.TracersTest
      def hello, do: :world
    end
    """)

    assert_received {{:remote_function, meta, __MODULE__, :__before_compile__, 1}, _}
    assert meta[:line] == 1

    assert_received {{:remote_function, meta, __MODULE__, :__after_compile__, 2}, _}
    assert meta[:line] == 1

    assert_received {{:remote_function, meta, __MODULE__, :__after_verify__, 1}, _}
    assert meta[:line] == 1

    assert_received {{:remote_function, meta, __MODULE__, :__on_definition__, 6}, _}
    assert meta[:line] == 6
  end

  test "traces super" do
    compile_string("""
    defmodule TracerOverridable do
      def local(x), do: x
      defoverridable [local: 1]
      def local(x), do: super(x)

      defmacro macro(x), do: x
      defoverridable [macro: 1]
      defmacro macro(x), do: super(x)

      def capture(x), do: x
      defoverridable [capture: 1]
      def capture(x), do: tap(x, &super/1)

      def capture_arg(x), do: x
      defoverridable [capture_arg: 1]
      def capture_arg(x), do: tap(x, &super(&1))
    end
    """)

    assert_received {{:local_function, _, :"local (overridable 1)", 1}, _}
    assert_received {{:local_function, _, :"macro (overridable 1)", 1}, _}
    assert_received {{:local_function, _, :"capture (overridable 1)", 1}, _}
    assert_received {{:local_function, _, :"capture_arg (overridable 1)", 1}, _}
    refute_received {{:local_function, _, _, _}, _}
  end

  test "does not trace bind quoted twice" do
    compile_string("""
    quote bind_quoted: [foo: List.flatten([])] do
      foo
    end
    """)

    assert_received {{:remote_function, _, List, :flatten, 1}, _}
    refute_received {{:remote_function, _, List, :flatten, 1}, _}
  end

  test "does not trace captures twice" do
    compile_string("""
    &List.flatten/1
    """)

    assert_received {{:remote_function, _, List, :flatten, 1}, _}
    refute_received {{:remote_function, _, List, :flatten, 1}, _}
  end

  """
  # Make sure this module is compiled with column information
  defmodule MacroWithColumn do
    defmacro some_macro(list) do
      quote do
        Enum.map(unquote(list), fn str -> String.upcase(str) end)
      end
    end
  end
  """
  |> Code.string_to_quoted!(columns: true)
  |> Code.compile_quoted()

  test "traces quoted from macro expansion without column information" do
    compile_string("""
    require MacroWithColumn
    MacroWithColumn.some_macro(["hello", "world", "!"])
    """)

    assert_received {{:alias_reference, meta, Enum}, _env}
    refute meta[:column]
  end
end
