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

defmodule IEx.HelpersTest do
  use IEx.Case

  import IEx.Helpers

  @compile {:no_warn_undefined, [:sample, Sample, Sample2]}

  describe "whereami" do
    test "is disabled by default" do
      assert capture_iex("whereami()") =~ "Pry session is not currently enabled"
    end

    test "shows current location for custom envs" do
      whereami = capture_iex("whereami()", [], env: %{__ENV__ | line: 3})
      assert whereami =~ "test/iex/helpers_test.exs:3"
      assert whereami =~ "3: defmodule IEx.HelpersTest do"
    end

    test "prints message when location is not available" do
      whereami = capture_iex("whereami()", [], env: %{__ENV__ | line: 30000})
      assert whereami =~ "test/iex/helpers_test.exs:30000"
      assert whereami =~ "Could not extract source snippet. Location is not available."

      whereami = capture_iex("whereami()", [], env: %{__ENV__ | file: "nofile", line: 1})
      assert whereami =~ "nofile:1"
      assert whereami =~ "Could not extract source snippet. Location is not available."
    end
  end

  describe "breakpoints" do
    setup do
      on_exit(fn -> IEx.Pry.remove_breaks() end)
    end

    test "sets up a breakpoint with capture syntax" do
      assert break!(URI.decode_query() / 2) == 1
      assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 1}]
    end

    test "sets up a breakpoint with call syntax" do
      assert break!(URI.decode_query(_, %{})) == 1
      assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 1}]
    end

    test "sets up a breakpoint with guards syntax" do
      assert break!(URI.decode_query(_, map) when is_map(map)) == 1
      assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 1}]
    end

    test "sets up a breakpoint on the given module" do
      assert break!(URI, :decode_query, 2) == 1
      assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 1}]
    end

    test "resets breaks on the given ID" do
      assert break!(URI, :decode_query, 2) == 1
      assert reset_break(1) == :ok
      assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}]
    end

    test "resets breaks on the given module" do
      assert break!(URI, :decode_query, 2) == 1
      assert reset_break(URI, :decode_query, 2) == :ok
      assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}]
    end

    test "removes breaks in the given module" do
      assert break!(URI.decode_query() / 2) == 1
      assert remove_breaks(URI) == :ok
      assert IEx.Pry.breaks() == []
    end

    test "removes breaks on all modules" do
      assert break!(URI.decode_query() / 2) == 1
      assert remove_breaks() == :ok
      assert IEx.Pry.breaks() == []
    end

    test "errors when setting up a breakpoint with invalid guard" do
      assert_raise CompileError, ~r"cannot find or invoke local is_whatever/1", fn ->
        break!(URI.decode_query(_, map) when is_whatever(map))
      end
    end

    test "errors when setting up a break with no beam" do
      assert_raise RuntimeError,
                   "could not set breakpoint, could not find .beam file for IEx.HelpersTest",
                   fn -> break!(__MODULE__, :setup, 1) end
    end

    test "errors when setting up a break for unknown function" do
      assert_raise RuntimeError,
                   "could not set breakpoint, unknown function/macro URI.unknown/2",
                   fn -> break!(URI, :unknown, 2) end
    end

    test "errors for non-Elixir modules" do
      assert_raise RuntimeError,
                   "could not set breakpoint, module :elixir was not written in Elixir",
                   fn -> break!(:elixir, :unknown, 2) end
    end

    test "prints table with breaks" do
      break!(URI, :decode_query, 2)

      assert capture_io(fn -> breaks() end) == """

              ID   Module.function/arity   Pending stops
             ---- ----------------------- ---------------
              1    URI.decode_query/2      1

             """

      assert capture_io(fn -> URI.decode_query("foo=bar", %{}) end) != ""

      assert capture_io(fn -> breaks() end) == """

              ID   Module.function/arity   Pending stops
             ---- ----------------------- ---------------
              1    URI.decode_query/2      0

             """

      assert capture_io(fn -> URI.decode_query("foo=bar", %{}) end) == ""

      assert capture_io(fn -> breaks() end) == """

              ID   Module.function/arity   Pending stops
             ---- ----------------------- ---------------
              1    URI.decode_query/2      0

             """
    end

    test "does not print table when there are no breaks" do
      assert capture_io(fn -> breaks() end) == "No breakpoints set\n"
    end
  end

  describe "open" do
    @iex_helpers "iex/lib/iex/helpers.ex"
    @elixir_erl "elixir/src/elixir.erl"
    @lists_erl "#{:code.lib_dir(:stdlib, :src)}/lists.erl"
    @httpc_erl "src/http_client/httpc.erl"
    @editor System.get_env("ELIXIR_EDITOR")

    test "opens __FILE__ and __LINE__" do
      System.put_env("ELIXIR_EDITOR", "echo __LINE__:__FILE__")

      assert capture_iex("open({#{inspect(__ENV__.file)}, 3})") |> maybe_trim_quotes() ==
               "3:#{__ENV__.file}"
    after
      System.put_env("ELIXIR_EDITOR", @editor)
    end

    test "opens Elixir module" do
      assert capture_iex("open(IEx.Helpers)") |> maybe_trim_quotes() =~ ~r/#{@iex_helpers}:1$/
    end

    test "opens function" do
      assert capture_iex("open(h)") |> maybe_trim_quotes() =~ ~r/#{@iex_helpers}:\d+$/
    end

    test "opens function/arity" do
      assert capture_iex("open(b/1)") |> maybe_trim_quotes() =~ ~r/#{@iex_helpers}:\d+$/
      assert capture_iex("open(h/0)") |> maybe_trim_quotes() =~ ~r/#{@iex_helpers}:\d+$/
    end

    test "opens module.function" do
      assert capture_iex("open(IEx.Helpers.b)") |> maybe_trim_quotes() =~ ~r/#{@iex_helpers}:\d+$/
      assert capture_iex("open(IEx.Helpers.h)") |> maybe_trim_quotes() =~ ~r/#{@iex_helpers}:\d+$/
    end

    test "opens module.function/arity" do
      assert capture_iex("open(IEx.Helpers.b/1)") |> maybe_trim_quotes() =~
               ~r/#{@iex_helpers}:\d+$/

      assert capture_iex("open(IEx.Helpers.h/0)") |> maybe_trim_quotes() =~
               ~r/#{@iex_helpers}:\d+$/
    end

    test "opens Erlang module" do
      assert capture_iex("open(:elixir)") |> maybe_trim_quotes() =~ ~r/#{@elixir_erl}:\d+$/
    end

    test "opens Erlang module.function" do
      assert capture_iex("open(:elixir.start)") |> maybe_trim_quotes() =~ ~r/#{@elixir_erl}:\d+$/
    end

    test "opens Erlang module.function/arity" do
      assert capture_iex("open(:elixir.start/2)") |> maybe_trim_quotes() =~
               ~r/#{@elixir_erl}:\d+$/
    end

    # Some installations remove the source file once Erlang is compiled. See #7348.
    if File.regular?(@lists_erl) do
      test "opens OTP lists module" do
        assert capture_iex("open(:lists)") |> maybe_trim_quotes() =~ ~r/#{@lists_erl}:\d+$/
      end

      test "opens OTP lists module.function" do
        assert capture_iex("open(:lists.reverse)") |> maybe_trim_quotes() =~
                 ~r/#{@lists_erl}:\d+$/
      end

      test "opens OTP lists module.function/arity" do
        assert capture_iex("open(:lists.reverse/1)") |> maybe_trim_quotes() =~
                 ~r/#{@lists_erl}:\d+$/
      end
    end

    # Some installations remove the source file once Erlang is compiled. See #7348.
    if File.regular?(@httpc_erl) do
      test "opens OTP httpc module" do
        assert capture_iex("open(:httpc)") |> maybe_trim_quotes() =~ ~r/#{@httpc_erl}:\d+$/
      end

      test "opens OTP httpc module.function" do
        assert capture_iex("open(:httpc.request)") |> maybe_trim_quotes() =~
                 ~r/#{@httpc_erl}:\d+$/
      end

      test "opens OTP httpc module.function/arity" do
        assert capture_iex("open(:httpc.request/1)") |> maybe_trim_quotes() =~
                 ~r/#{@httpc_erl}:\d+$/
      end
    end

    test "errors OTP preloaded module" do
      assert capture_iex("open(:init)") =~ ~r"(Could not open)|(Invalid arguments)"
    end

    test "errors if module is not available" do
      assert capture_iex("open(:unknown)") == "Could not open: :unknown. Module is not available."
    end

    test "errors if module.function is not available" do
      assert capture_iex("open(:unknown.unknown)") ==
               "Could not open: :unknown.unknown. Module is not available."

      assert capture_iex("open(:elixir.unknown)") ==
               "Could not open: :elixir.unknown. Function/macro is not available."

      assert capture_iex("open(:lists.unknown)") ==
               "Could not open: :lists.unknown. Function/macro is not available."

      assert capture_iex("open(:httpc.unknown)") ==
               "Could not open: :httpc.unknown. Function/macro is not available."
    end

    test "errors if module.function/arity is not available" do
      assert capture_iex("open(:unknown.start/10)") ==
               "Could not open: :unknown.start/10. Module is not available."

      assert capture_iex("open(:elixir.start/10)") ==
               "Could not open: :elixir.start/10. Function/macro is not available."

      assert capture_iex("open(:lists.reverse/10)") ==
               "Could not open: :lists.reverse/10. Function/macro is not available."

      assert capture_iex("open(:httpc.request/10)") ==
               "Could not open: :httpc.request/10. Function/macro is not available."
    end

    test "errors if module is in-memory" do
      assert capture_iex("defmodule Foo, do: nil ; open(Foo)") =~
               ~r"Invalid arguments for open helper:"
    after
      cleanup_modules([Foo])
    end

    test "opens the current pry location" do
      assert capture_iex("open()", [], env: %{__ENV__ | line: 3}) |> maybe_trim_quotes() ==
               "#{__ENV__.file}:3"
    end

    test "errors if prying is not available" do
      assert capture_iex("open()") == "Pry session is not currently enabled"
    end

    test "opens given {file, line}" do
      assert capture_iex("open({#{inspect(__ENV__.file)}, 3})") |> maybe_trim_quotes() ==
               "#{__ENV__.file}:3"
    end

    test "errors when given {file, line} is not available" do
      assert capture_iex("open({~s[foo], 3})") ==
               "Could not open: \"foo\". File is not available."
    end

    defp maybe_trim_quotes(string) do
      case :os.type() do
        {:win32, _} -> String.replace(string, "\"", "")
        _ -> string
      end
    end
  end

  describe "clear" do
    test "clear the screen with ansi" do
      Application.put_env(:elixir, :ansi_enabled, true)
      assert capture_iex("clear()") == "\e[H\e[2J"

      Application.put_env(:elixir, :ansi_enabled, false)

      assert capture_iex("clear()") =~
               "Cannot clear the screen because ANSI escape codes are not enabled on this shell"
    after
      Application.delete_env(:elixir, :ansi_enabled)
    end
  end

  describe "runtime_info" do
    test "shows VM information" do
      assert "\n## System and architecture" <> _ = capture_io(fn -> runtime_info() end)
    end
  end

  describe "h" do
    test "shows help" do
      help = capture_iex("h()")
      assert help =~ "IEx.Helpers"
      assert help =~ "Welcome to Interactive Elixir"
    end

    test "prints non-Elixir module specs" do
      assert capture_io(fn -> h(:timer.sleep() / 1) end) =~
               "@spec sleep(time) :: :ok when time: timeout()"
    end

    test "prints module documentation" do
      assert "\n                                  IEx.Helpers\n\nWelcome to Interactive Elixir" <>
               _ = capture_io(fn -> h(IEx.Helpers) end)

      assert capture_io(fn -> h(:whatever) end) ==
               "Could not load module :whatever, got: nofile\n"
    end

    test "prints function/macro documentation" do
      pwd_h = """

                                         def pwd()

      Prints the current working directory.

      """

      c_h = """

                              def c(files, path \\\\ :in_memory)

      Compiles the given files.
      """

      eq_h = """

                                     def left == right

        @spec term() == term() :: boolean()

      guard: true

      Equal to operator. Returns `true` if the two terms are equal.
      """

      def_h = """

                              defmacro def(call, expr \\\\ nil)

      Defines a public function with the given name and body.
      """

      assert capture_io(fn -> h(IEx.Helpers.pwd() / 0) end) =~ pwd_h
      assert capture_io(fn -> h(IEx.Helpers.c() / 2) end) =~ c_h
      assert capture_io(fn -> h(== / 2) end) =~ eq_h
      assert capture_io(fn -> h(def / 2) end) =~ def_h

      assert capture_io(fn -> h(IEx.Helpers.c() / 1) end) =~ c_h
      assert capture_io(fn -> h(pwd) end) =~ pwd_h
      assert capture_io(fn -> h(def) end) =~ def_h
    end

    test "prints sigil documentation" do
      assert capture_io(fn -> h(~w//) end) =~ "Handles the sigil `~w` for list of words"
    end

    test "prints __info__ documentation" do
      h_output_module = capture_io(fn -> h(Module.__info__()) end)
      assert capture_io(fn -> h(Module.UnlikelyTo.Exist.__info__()) end) == h_output_module
      assert capture_io(fn -> h(Module.UnlikelyTo.Exist.__info__() / 1) end) == h_output_module

      assert capture_io(fn -> h(__info__) end) ==
               "No documentation for Kernel.__info__ was found\n"
    end

    test "prints documentation metadata" do
      content = """
      defmodule Sample do
        @moduledoc "Sample module"
        @moduledoc deprecated: "Use OtherSample", since: "1.2.3", authors: ["Alice", "Bob"]
        @doc "With metadata"
        @doc since: "1.2.3", author: "Alice"
        @deprecated "Use OtherSample.with_metadata/0"
        def with_metadata(), do: 0
        @doc "Without metadata"
        def without_metadata(), do: 1
      end
      """

      filename = "sample.ex"

      with_file(filename, content, fn ->
        assert c(filename, ".") == [Sample]

        assert capture_io(fn -> h(Sample) end) == """

                                                    Sample

               deprecated: Use OtherSample
               since: 1.2.3

               Sample module

               """

        assert capture_io(fn -> h(Sample.with_metadata()) end) == """

                                             def with_metadata()

               deprecated: Use OtherSample.with_metadata/0
               since: 1.2.3

               With metadata

               """

        assert capture_io(fn -> h(Sample.without_metadata()) end) == """

                                            def without_metadata()

               Without metadata

               """
      end)
    after
      cleanup_modules([Sample])
    end

    test "considers underscored functions without docs by default" do
      content = """
      defmodule Sample do
        def __foo__(), do: 0
        @doc "Bar doc"
        def __bar__(), do: 1
      end
      """

      filename = "sample.ex"

      with_file(filename, content, fn ->
        assert c(filename, ".") == [Sample]

        assert capture_io(fn -> h(Sample.__foo__()) end) ==
                 "No documentation for Sample.__foo__ was found\n"

        assert capture_io(fn -> h(Sample.__bar__()) end) == """

                                                def __bar__()

               Bar doc

               """

        assert capture_io(fn -> h(Sample.__foo__() / 0) end) ==
                 "No documentation for Sample.__foo__/0 was found\n"

        assert capture_io(fn -> h(Sample.__bar__() / 0) end) == """

                                                def __bar__()

               Bar doc

               """
      end)
    after
      cleanup_modules([Sample])
    end

    test "prints callback documentation when function docs are not available" do
      behaviour = """
      defmodule MyBehaviour do
        @doc "Docs for MyBehaviour.first"
        @callback first(integer) :: integer
        @callback second(integer) :: integer
        @callback second(integer, integer) :: integer
      end
      """

      impl = """
      defmodule Impl do
        @behaviour MyBehaviour
        @doc delegate_to: {Foo, :bar, 3}
        def first(0), do: 0
        @doc "Docs for Impl.second/1"
        def second(0), do: 0
        @doc "Docs for Impl.second/2"
        def second(0, 0), do: 0
      end
      """

      files = ["my_behaviour.ex", "impl.ex"]

      with_file(files, [behaviour, impl], fn ->
        assert c(files, ".") |> Enum.sort() == [Impl, MyBehaviour]

        assert capture_io(fn -> h(Impl.first() / 1) end) == """

                                                def first(int)

               delegate_to: Foo.bar/3

               Impl.first/1 has no docs but is a callback for behaviour MyBehaviour. Showing
               callback docs instead.

               @callback first(integer()) :: integer()

               Docs for MyBehaviour.first

               """

        assert capture_io(fn -> h(Impl.second() / 1) end) == """

                                               def second(int)

               Docs for Impl.second/1

               """

        assert capture_io(fn -> h(Impl.second() / 2) end) == """

                                            def second(int1, int2)

               Docs for Impl.second/2

               """

        assert capture_io(fn -> h(Impl.first()) end) == capture_io(fn -> h(Impl.first() / 1) end)

        assert capture_io(fn -> h(Impl.second()) end) == """

                                               def second(int)

               Docs for Impl.second/1


                                            def second(int1, int2)

               Docs for Impl.second/2

               """

        assert capture_io(fn -> h(MyBehaviour.first()) end) == """
               No documentation for function MyBehaviour.first was found, but there is a callback with the same name.
               You can view callback documentation with the b/1 helper.\n
               """

        assert capture_io(fn -> h(MyBehaviour.second() / 2) end) == """
               No documentation for function MyBehaviour.second/2 was found, but there is a callback with the same name.
               You can view callback documentation with the b/1 helper.\n
               """

        assert capture_io(fn -> h(MyBehaviour.second() / 3) end) ==
                 "No documentation for MyBehaviour.second/3 was found\n"
      end)
    after
      cleanup_modules([Impl, MyBehaviour])
    end

    test "prints protocol function docs" do
      output = capture_io(fn -> h(Enumerable.reduce()) end)
      assert output =~ "@spec reduce(t(), acc(), reducer()) :: result()"
      assert output =~ "Reduces the `enumerable`"
    end

    test "prints documentation for delegates" do
      filename = "delegate.ex"

      content = """
      defmodule Delegator do
        defdelegate func1, to: Delegated
        @doc "Delegator func2 doc"
        defdelegate func2, to: Delegated
      end
      defmodule Delegated do
        def func1, do: 1
        def func2, do: 2
      end
      """

      with_file(filename, content, fn ->
        assert c(filename, ".") |> Enum.sort() == [Delegated, Delegator]

        assert capture_io(fn -> h(Delegator.func1()) end) == """

                                                 def func1()

               delegate_to: Delegated.func1/0

               """

        assert capture_io(fn -> h(Delegator.func2()) end) == """

                                                 def func2()

               delegate_to: Delegated.func2/0

               Delegator func2 doc

               """
      end)
    after
      cleanup_modules([Delegated, Delegator])
    end

    test "prints type documentation when function docs are not available" do
      content = """
      defmodule MyTypes do
        @type first() :: any()
        @type second(a, b) :: {a, b}
      end
      """

      filename = "my_types.ex"

      with_file(filename, content, fn ->
        assert c(filename, ".") == [MyTypes]

        assert capture_io(fn -> h(MyTypes.first()) end) == """
               No documentation for function MyTypes.first was found, but there is a type with the same name.
               You can view type documentation with the t/1 helper.\n
               """

        assert capture_io(fn -> h(MyTypes.second() / 2) end) == """
               No documentation for function MyTypes.second/2 was found, but there is a type with the same name.
               You can view type documentation with the t/1 helper.\n
               """
      end)
    after
      cleanup_modules([MyTypes])
    end

    test "prints modules compiled without docs" do
      Code.compiler_options(docs: false)

      content = """
      defmodule Sample do
        @spec foo(any()) :: any()
        def foo(arg), do: arg
      end
      """

      filename = "sample.ex"

      with_file(filename, content, fn ->
        assert c(filename, ".") == [Sample]

        assert capture_io(fn -> h(Sample.foo() / 1) end) == """

                                                 Sample.foo/1

                 @spec foo(any()) :: any()

               Module was compiled without docs. Showing only specs.

               """
      end)
    after
      Code.compiler_options(docs: true)
      cleanup_modules([Sample])
    end

    test "does not print docs for @doc false functions" do
      # Here we assert that @doc false works and that we are not leaking
      # IEx.Pry internal functions.
      assert capture_io(fn -> h(IEx.Pry.child_spec()) end) ==
               "No documentation for IEx.Pry.child_spec was found\n"
    end
  end

  describe "b" do
    test "lists all callbacks for an Elixir module" do
      assert capture_io(fn -> b(Mix) end) == "No callbacks for Mix were found\n"
      assert capture_io(fn -> b(NoMix) end) == "Could not load module NoMix, got: nofile\n"

      assert capture_io(fn -> b(Mix.SCM) end) =~ """
             @callback accepts_options(app :: atom(), opts()) :: opts() | nil

             @callback checked_out?(opts()) :: boolean()
             """
    end

    test "lists all callbacks for an Erlang module" do
      output = capture_io(fn -> b(:gen_server) end)

      assert output =~ "@callback init(args :: term()) ::"
      assert output =~ "@callback handle_cast("
      assert output =~ "@callback handle_info("
    end

    test "lists all macrocallbacks for a module" do
      filename = "macrocallbacks.ex"

      content = """
      defmodule Macrocallbacks do
        @macrocallback test(:foo) :: integer
        @macrocallback test(:bar) :: var when var: integer
      end
      """

      with_file(filename, content, fn ->
        assert c(filename, ".") == [Macrocallbacks]
        callbacks = capture_io(fn -> b(Macrocallbacks) end)
        assert callbacks =~ "@macrocallback test(:foo) :: integer()\n"
        assert callbacks =~ "@macrocallback test(:bar) :: var when var: integer()\n"
      end)
    after
      cleanup_modules([Macrocallbacks])
    end

    test "lists all callbacks for a protocol" do
      assert capture_io(fn -> b(Enumerable) end) =~ """
             @callback count(t()) :: {:ok, non_neg_integer()} | {:error, module()}

             @callback member?(t(), term()) :: {:ok, boolean()} | {:error, module()}

             @callback reduce(t(), acc(), reducer()) :: result()
             """
    end

    test "prints protocol function docs" do
      output = capture_io(fn -> b(Enumerable.reduce()) end)
      assert output =~ "@callback reduce(t(), acc(), reducer()) :: result()"
      assert output =~ "Reduces the `enumerable`"
    end

    test "lists callback with multiple clauses" do
      filename = "multiple_clauses_callback.ex"

      content = """
      defmodule MultipleClauseCallback do
        @doc "callback"
        @callback test(:foo) :: integer
        @callback test(:bar) :: [integer]
      end
      """

      with_file(filename, content, fn ->
        assert c(filename, ".") == [MultipleClauseCallback]

        assert capture_io(fn -> b(MultipleClauseCallback) end) =~ """
               @callback test(:foo) :: integer()
               @callback test(:bar) :: [integer()]
               """

        assert capture_io(fn -> b(MultipleClauseCallback.test()) end) =~ """
               @callback test(:foo) :: integer()
               @callback test(:bar) :: [integer()]

               callback
               """
      end)
    after
      cleanup_modules([MultipleClauseCallback])
    end

    test "prints callback documentation" do
      assert capture_io(fn -> b(Mix.Task.stop()) end) ==
               "No documentation for Mix.Task.stop was found\n"

      assert capture_io(fn -> b(Mix.Task.run()) end) =~
               "@callback run(command_line_args :: [binary()]) :: any()\n\nA task needs to implement `run`"

      assert capture_io(fn -> b(NoMix.run()) end) == "Could not load module NoMix, got: nofile\n"

      assert capture_io(fn -> b(Exception.message() / 1) end) ==
               "@callback message(t()) :: String.t()\n\n"

      assert capture_io(fn -> b(:gen_server.handle_cast() / 2) end) =~
               "@callback handle_cast("
    end

    test "prints callback documentation metadata" do
      filename = "callback_with_metadata.ex"

      content = """
      defmodule CallbackWithMetadata do
        @doc "callback"
        @doc since: "1.2.3", deprecated: "Use handle_test/1", purpose: :test
        @callback test(:foo) :: integer
      end
      """

      with_file(filename, content, fn ->
        assert c(filename, ".") == [CallbackWithMetadata]

        assert capture_io(fn -> b(CallbackWithMetadata.test()) end) == """
               @callback test(:foo) :: integer()

               deprecated: Use handle_test/1
               since: 1.2.3

               callback

               """
      end)
    after
      cleanup_modules([CallbackWithMetadata])
    end

    test "prints optional callback" do
      filename = "optional_callbacks.ex"

      content = """
      defmodule OptionalCallbacks do
        @doc "callback"
        @callback optional_callback(:foo) :: integer
        @macrocallback optional_macrocallback(:bar) :: atom
        @optional_callbacks optional_callback: 1, optional_macrocallback: 1
      end
      """

      with_file(filename, content, fn ->
        assert c(filename, ".") == [OptionalCallbacks]

        assert capture_io(fn -> b(OptionalCallbacks) end) =~ """
               @callback optional_callback(:foo) :: integer()

               @macrocallback optional_macrocallback(:bar) :: atom()

               @optional_callbacks optional_callback: 1, optional_macrocallback: 1

               """
      end)
    after
      cleanup_modules([OptionalCallbacks])
    end

    test "does not print docs for @doc false callbacks" do
      filename = "hidden_callbacks.ex"

      content = """
      defmodule HiddenCallbacks do
        @doc false
        @callback hidden_callback() :: integer

        @doc false
        @macrocallback hidden_macrocallback() :: integer
      end
      """

      with_file(filename, content, fn ->
        assert c(filename, ".") == [HiddenCallbacks]

        assert capture_io(fn -> b(HiddenCallbacks) end) =~
                 "No callbacks for HiddenCallbacks were found\n"
      end)
    after
      cleanup_modules([HiddenCallbacks])
    end
  end

  describe "t" do
    test "prints when there is no type information or the type is private" do
      assert capture_io(fn -> t(IEx) end) == "No type information for IEx was found\n"

      assert capture_io(fn -> t(Enum.doesnt_exist()) end) ==
               "No type information for Enum.doesnt_exist was found or " <>
                 "Enum.doesnt_exist is private\n"

      contents = """
      defmodule TypeSample do
        @type public_so_t_doesnt_warn() :: t()
        @typep t() :: term()
      end
      """

      filename = "typesample.ex"

      with_file(filename, contents, fn ->
        assert c(filename, ".") == [TypeSample]

        assert capture_io(fn -> t(TypeSample.t() / 0) end) ==
                 "No type information for TypeSample.t was found or TypeSample.t is private\n"
      end)
    after
      cleanup_modules([TypeSample])
    end

    test "prints all types in module" do
      # Test that it shows at least two types
      assert Enum.count(capture_io(fn -> t(Enum) end) |> String.split("\n"), fn line ->
               String.starts_with?(line, "@type")
             end) >= 2
    end

    test "prints private types" do
      assert capture_io(fn -> t(Date.Range) end) =~ "@typep days"
    end

    test "prints type information" do
      assert "@type t() ::" <> _ = capture_io(fn -> t(Enum.t()) end)
      assert capture_io(fn -> t(Enum.t()) end) == capture_io(fn -> t(Enum.t() / 0) end)
      assert "@type child_spec() ::" <> _ = capture_io(fn -> t(:supervisor.child_spec()) end)
      assert capture_io(fn -> t(URI.t()) end) == capture_io(fn -> t(URI.t() / 0) end)
    end

    test "sorts types alphabetically" do
      unsorted =
        capture_io(fn -> t(Enum) end)
        |> String.split("\n")
        |> Enum.reject(&(&1 == ""))
        |> Enum.map(&String.replace(&1, ~r/@(type|opaque) /, ""))

      assert unsorted == Enum.sort(unsorted)
    end

    test "prints type documentation" do
      content = """
      defmodule TypeSample do
        @typedoc "An ID with description."
        @type id_with_desc :: {number, String.t}
        @type unquote(:"?")() :: :question_mark
      end
      """

      filename = "typesample.ex"

      with_file(filename, content, fn ->
        assert c(filename, ".") == [TypeSample]

        assert capture_io(fn -> t(TypeSample.id_with_desc() / 0) end) == """
               @type id_with_desc() :: {number(), String.t()}

               An ID with description.

               """

        assert capture_io(fn -> t(TypeSample.id_with_desc()) end) == """
               @type id_with_desc() :: {number(), String.t()}

               An ID with description.

               """

        assert capture_io(fn -> t(TypeSample."?"()) end) == """
               @type ?() :: :question_mark

               """
      end)
    after
      cleanup_modules([TypeSample])
    end

    test "prints type documentation metadata" do
      content = """
      defmodule TypeSample do
        @typedoc "An ID with description."
        @typedoc since: "1.2.3", deprecated: "Use t/0", purpose: :test
        @type id_with_desc :: {number, String.t}
      end
      """

      filename = "typesample.ex"

      with_file(filename, content, fn ->
        assert c(filename, ".") == [TypeSample]

        assert capture_io(fn -> t(TypeSample.id_with_desc()) end) == """
               @type id_with_desc() :: {number(), String.t()}

               deprecated: Use t/0
               since: 1.2.3

               An ID with description.

               """
      end)
    after
      cleanup_modules([TypeSample])
    end
  end

  describe "v" do
    test "returns history" do
      assert "** (RuntimeError) v(0) is out of bounds" <> _ = capture_iex("v(0)")
      assert "** (RuntimeError) v(1) is out of bounds" <> _ = capture_iex("v(1)")
      assert "** (RuntimeError) v(-1) is out of bounds" <> _ = capture_iex("v(-1)")
      assert capture_iex("1\n2\nv(2)") == "1\n2\n2"
      assert capture_iex("1\n2\nv(2)") == capture_iex("1\n2\nv(-1)")
      assert capture_iex("1\n2\nv(2)") == capture_iex("1\n2\nv()")
    end
  end

  describe "flush" do
    test "flushes messages" do
      assert capture_io(fn ->
               send(self(), :hello)
               flush()
             end) == ":hello\n"
    end
  end

  describe "pwd" do
    test "prints the working directory" do
      File.cd!(iex_path(), fn ->
        assert capture_io(fn -> pwd() end) =~ ~r"lib[\\/]iex\n$"
      end)
    end
  end

  describe "ls" do
    test "lists the current directory" do
      File.cd!(iex_path(), fn ->
        paths =
          capture_io(fn -> ls() end)
          |> String.split()
          |> Enum.map(&String.trim/1)

        assert "ebin" in paths
        assert "mix.exs" in paths
      end)
    end

    test "lists the given directory" do
      assert capture_io(fn -> ls("~") end) == capture_io(fn -> ls(System.user_home()) end)
    end
  end

  describe "exports" do
    test "prints module exports" do
      exports = capture_io(fn -> exports(IEx.Autocomplete) end)
      assert exports == "expand/1      expand/2      exports/1     remsh/1       \n"
    end

    test "handles long function names" do
      exports = capture_io(fn -> exports(Calendar.UTCOnlyTimeZoneDatabase) end)

      assert exports ==
               "time_zone_period_from_utc_iso_days/2 time_zone_periods_from_wall_datetime/2 \n"
    end
  end

  describe "import_file" do
    test "imports a file" do
      with_file("dot-iex", "variable = :hello\nimport IO", fn ->
        capture_io(:stderr, fn ->
          assert "** (CompileError) iex:1: undefined function variable/0" <> _ =
                   capture_iex("variable")
        end)

        assert "** (CompileError) iex:1: undefined function puts/1" <> _ =
                 capture_iex("puts \"hi\"")

        assert capture_iex("import_file \"dot-iex\"\nvariable\nputs \"hi\"") ==
                 "IO\n:hello\nhi\n:ok"
      end)
    end

    test "imports a file that imports another file" do
      dot = "parent = true\nimport_file \"dot-iex-1\""
      dot_1 = "variable = :hello\nimport IO"

      with_file(["dot-iex", "dot-iex-1"], [dot, dot_1], fn ->
        capture_io(:stderr, fn ->
          assert "** (CompileError) iex:1: undefined function parent/0" <> _ =
                   capture_iex("parent")
        end)

        assert "** (CompileError) iex:1: undefined function puts/1" <> _ =
                 capture_iex("puts \"hi\"")

        assert capture_iex("import_file \"dot-iex\"\nvariable\nputs \"hi\"\nparent") ==
                 "IO\n:hello\nhi\n:ok\ntrue"
      end)
    end

    test "raises if file is missing" do
      failing = capture_iex("import_file \"nonexistent\"")
      assert "** (File.Error) could not read file" <> _ = failing
      assert failing =~ "no such file or directory"
    end

    test "does not raise if file is missing and using import_file_if_available" do
      assert "nil" == capture_iex("import_file_if_available \"nonexistent\"")
    end

    test "circular imports" do
      dot_1 = "import_file \"dot-iex-2\""
      dot_2 = "import_file \"dot-iex-1\""

      with_file(["dot-iex-1", "dot-iex-2"], [dot_1, dot_2], fn ->
        assert capture_io(:stderr, fn ->
                 assert capture_iex(":ok", [], dot_iex_path: "dot-iex-1") == ":ok"
               end) =~ "dot-iex-2 was already imported, skipping circular file imports"
      end)
    end
  end

  describe "import_if_available" do
    test "imports a module only if available" do
      assert "nil" == capture_iex("import_if_available NoSuchModule")
      assert "[1, 2, 3]" == capture_iex("import_if_available Integer; digits 123")

      assert "[1, 2, 3]" ==
               capture_iex("import_if_available Integer, only: [digits: 1]; digits 123")
    end
  end

  describe "use_if_available" do
    test "uses a module only if available" do
      assert "nil" == capture_iex("use_if_available NoSuchModule")
    end
  end

  describe "iex> |> (and other binary operators)" do
    test "passes previous result to the pipe" do
      Enum.each([:~>>, :<<~, :~>, :<~, :<~>], fn op ->
        assert capture_iex("42\n  #{op} IO.puts()") =~ "undefined function #{op}/2"
      end)

      assert capture_iex("+42") =~ "42"
      assert capture_iex("-42") =~ "-42"
      assert capture_iex("42\n |> IO.inspect(label: \"foo\")") =~ "foo: 42"
      assert capture_iex("[42]\n++ [24]\n|> IO.inspect(label: \"foo\")") =~ "foo: [42, 24]"
      assert capture_iex("|> IO.puts()") =~ "(RuntimeError) v(-1) is out of bounds"
    end

    test "raises if previous expression was a match" do
      assert capture_iex("x = 42\n|> IO.puts()") =~
               "surround the whole pipeline with parentheses '|>'"

      assert capture_iex("%{x: x} = %{x: 42}\n|> IO.puts()") =~
               "surround the whole pipeline with parentheses '|>'"

      assert capture_iex("%{x: x} = map = %{x: 42}\n|> IO.puts()") =~
               "surround the whole pipeline with parentheses '|>'"
    end
  end

  describe "c" do
    test "compiles a file" do
      assert_raise UndefinedFunctionError, ~r"function Sample\.run/0 is undefined", fn ->
        Sample.run()
      end

      filename = "sample.ex"

      with_file(filename, test_module_code(), fn ->
        assert c(Path.expand(filename)) == [Sample]
        refute File.exists?("Elixir.Sample.beam")
        assert Sample.run() == :run
      end)
    after
      cleanup_modules([Sample])
    end

    test "handles errors" do
      ExUnit.CaptureIO.capture_io(fn ->
        with_file("sample.ex", "raise \"oops\"", fn ->
          assert_raise CompileError, fn -> c("sample.ex") end
        end)
      end)
    end

    test "compiles a file with multiple modules " do
      assert_raise UndefinedFunctionError, ~r"function Sample.run/0 is undefined", fn ->
        Sample.run()
      end

      filename = "sample.ex"

      with_file(filename, test_module_code() <> "\n" <> another_test_module(), fn ->
        assert c(filename) |> Enum.sort() == [Sample, Sample2]
        assert Sample.run() == :run
        assert Sample2.hello() == :world
      end)
    after
      cleanup_modules([Sample, Sample2])
    end

    test "compiles multiple modules" do
      assert_raise UndefinedFunctionError, ~r"function Sample.run/0 is undefined", fn ->
        Sample.run()
      end

      filenames = ["sample1.ex", "sample2.ex"]

      with_file(filenames, [test_module_code(), another_test_module()], fn ->
        assert c(filenames) |> Enum.sort() == [Sample, Sample2]
        assert Sample.run() == :run
        assert Sample2.hello() == :world
      end)
    after
      cleanup_modules([Sample, Sample2])
    end

    test "compiles Erlang modules" do
      assert_raise UndefinedFunctionError, ~r"function :sample.hello/0 is undefined", fn ->
        :sample.hello()
      end

      filename = "sample.erl"

      with_file(filename, erlang_module_code(), fn ->
        assert c(filename) == [:sample]
        assert :sample.hello() == :world
        refute File.exists?("sample.beam")
      end)
    after
      cleanup_modules([:sample])
    end

    test "skips unknown files" do
      assert_raise UndefinedFunctionError, ~r"function :sample.hello/0 is undefined", fn ->
        :sample.hello()
      end

      filenames = ["sample.erl", "not_found.ex", "sample2.ex"]

      with_file(filenames, [erlang_module_code(), "", another_test_module()], fn ->
        assert c(filenames) |> Enum.sort() == [Sample2, :sample]
        assert :sample.hello() == :world
        assert Sample2.hello() == :world
      end)
    after
      cleanup_modules([:sample, Sample2])
    end

    test "compiles file in path" do
      assert_raise UndefinedFunctionError, ~r"function Sample\.run/0 is undefined", fn ->
        Sample.run()
      end

      filename = "sample.ex"

      with_file(filename, test_module_code(), fn ->
        assert c(filename, ".") == [Sample]
        assert File.exists?("Elixir.Sample.beam")
        assert Sample.run() == :run
      end)
    after
      cleanup_modules([Sample])
    end
  end

  describe "l" do
    test "returns error tuple for nonexistent modules" do
      assert l(:nonexistent_module) == {:error, :nofile}
    end

    test "loads a given module" do
      assert_raise UndefinedFunctionError,
                   ~r"function Sample.run/0 is undefined",
                   fn -> Sample.run() end

      filename = "sample.ex"

      with_file(filename, test_module_code(), fn ->
        assert c(filename, ".") == [Sample]
        assert Sample.run() == :run

        File.write!(filename, "defmodule Sample do end")
        elixirc(["sample.ex"])
        assert l(Sample) == {:module, Sample}

        assert_raise UndefinedFunctionError,
                     ~r"function Sample.run/0 is undefined",
                     fn -> Sample.run() end
      end)
    after
      # Clean up the old version left over after l()
      cleanup_modules([Sample])
    end
  end

  describe "nl" do
    @tag :capture_log
    test "loads a given module on the given nodes" do
      assert nl([node()], :lists) == {:ok, [{:nonode@nohost, :error, :sticky_directory}]}
      assert nl(:nonexistent_module) == {:error, :nofile}
      assert nl([:nosuchnode@badhost], Enum) == {:ok, [{:nosuchnode@badhost, :badrpc, :nodedown}]}
      assert nl([node()], Enum) == {:ok, [{:nonode@nohost, :loaded, Enum}]}
    end
  end

  describe "r" do
    test "raises when reloading a nonexistent module" do
      assert_raise ArgumentError, "could not load nor find module: :nonexistent_module", fn ->
        r(:nonexistent_module)
      end
    end

    test "reloads Elixir modules" do
      message = ~r"function Sample.run/0 is undefined \(module Sample is not available\)"

      assert_raise UndefinedFunctionError, message, fn ->
        Sample.run()
      end

      filename = "sample.ex"

      with_file(filename, test_module_code(), fn ->
        assert capture_io(:stderr, fn ->
                 assert c(filename, ".") == [Sample]
                 assert Sample.run() == :run

                 File.write!(filename, "defmodule Sample do end")
                 assert {:reloaded, [Sample]} = r(Sample)
                 assert {:reloaded, [Sample]} = r([Sample])

                 message = "function Sample.run/0 is undefined or private"

                 assert_raise UndefinedFunctionError, message, fn ->
                   Sample.run()
                 end
               end) =~
                 "redefining module Sample (current version loaded from ./Elixir.Sample.beam)"
      end)
    after
      # Clean up old version produced by the r helper
      cleanup_modules([Sample])
    end

    test "reloads Erlang modules" do
      assert_raise UndefinedFunctionError, ~r"function :sample.hello/0 is undefined", fn ->
        :sample.hello()
      end

      filename = "sample.erl"

      with_file(filename, erlang_module_code(), fn ->
        assert c(filename, ".") == [:sample]
        assert :sample.hello() == :world

        File.write!(filename, other_erlang_module_code())
        assert {:reloaded, [:sample]} = r(:sample)
        assert {:reloaded, [:sample]} = r([:sample])
        assert :sample.hello() == :bye
      end)
    after
      cleanup_modules([:sample])
    end
  end

  describe "pid/1,3" do
    test "builds a PID from string" do
      assert inspect(pid("0.32767.3276")) == "#PID<0.32767.3276>"
      assert inspect(pid("0.5.6")) == "#PID<0.5.6>"

      assert_raise ArgumentError, fn ->
        pid("0.6.-6")
      end
    end

    test "builds a PID from atom" do
      assert inspect(pid(:init)) == "#PID<0.0.0>"

      assert_raise ArgumentError, fn ->
        pid(:random_atom_ZM6pX6VwQx)
      end
    end

    test "builds a PID from integers" do
      assert inspect(pid(0, 32767, 3276)) == "#PID<0.32767.3276>"
      assert inspect(pid(0, 5, 6)) == "#PID<0.5.6>"

      assert_raise FunctionClauseError, fn ->
        pid(0, 6, -6)
      end
    end
  end

  describe "port" do
    test "builds a port from string" do
      assert inspect(port("0.8080")) == "#Port<0.8080>"
      assert inspect(port("0.0")) == "#Port<0.0>"

      assert_raise ArgumentError, fn ->
        port("0.-6")
      end
    end

    test "builds a port from integers" do
      assert inspect(port(0, 8080)) == "#Port<0.8080>"
      assert inspect(port(0, 0)) == "#Port<0.0>"

      assert_raise FunctionClauseError, fn ->
        port(-1, -6)
      end
    end
  end

  describe "ref" do
    test "builds a ref from string" do
      ref = make_ref()
      [_, inner, _] = String.split(inspect(ref), ["<", ">"])
      assert ref(inner) == ref

      assert_raise ArgumentError, fn ->
        ref("0.6.6.-6")
      end
    end

    test "builds a ref from integers" do
      ref = make_ref()
      [_, inner, _] = String.split(inspect(ref), ["<", ">"])
      [p1, p2, p3, p4] = inner |> String.split(".") |> Enum.map(&String.to_integer/1)
      assert ref(p1, p2, p3, p4) == ref

      assert_raise FunctionClauseError, fn ->
        ref(0, 6, 6, -6)
      end
    end
  end

  describe "i" do
    test "prints information about the data type" do
      assert capture_io(fn -> i(:ok) end) =~ """
             Term
               :ok
             Data type
               Atom
             Reference modules
               Atom\
             """
    end

    test "handles functions that don't display result" do
      assert capture_io(fn -> i(IEx.dont_display_result()) end) =~ """
             Term
               :"do not show this result in output"
             Data type
               Atom
             Description
               This atom is returned by IEx when a function that should not print its
               return value on screen is executed.\
             """
    end

    defmodule MyIExInfoModule do
      defstruct []

      defimpl IEx.Info do
        def info(_), do: [{"A", "it's A"}, {:b, "it's :b"}, {'c', "it's 'c'"}]
      end
    end

    test "uses the IEx.Info protocol" do
      assert capture_io(fn -> i(%MyIExInfoModule{}) end) =~ """
             Term
               %IEx.HelpersTest.MyIExInfoModule{}
             A
               it's A
             b
               it's :b
             c
               it's 'c'
             """
    after
      cleanup_modules([MyIExInfoModule])
    end
  end

  defp test_module_code do
    """
    defmodule Sample do
      def run do
        :run
      end
    end
    """
  end

  defp another_test_module do
    """
    defmodule Sample2 do
      def hello do
        :world
      end
    end
    """
  end

  defp erlang_module_code do
    """
    -module(sample).
    -export([hello/0]).
    hello() -> world.
    """
  end

  defp other_erlang_module_code do
    """
    -module(sample).
    -export([hello/0]).
    hello() -> bye.
    """
  end

  defp cleanup_modules(mods) do
    Enum.each(mods, fn mod ->
      File.rm("#{mod}.beam")
      :code.purge(mod)
      true = :code.delete(mod)
    end)
  end

  defp with_file(names, codes, fun) when is_list(names) and is_list(codes) do
    Enum.each(Enum.zip(names, codes), fn {name, code} ->
      File.write!(name, code)
    end)

    try do
      fun.()
    after
      Enum.each(names, &File.rm/1)
    end
  end

  defp with_file(name, code, fun) do
    with_file(List.wrap(name), List.wrap(code), fun)
  end

  defp elixirc(args) do
    executable = Path.expand("../../../../bin/elixirc", __DIR__)
    System.cmd("#{executable}#{executable_extension()}", args, stderr_to_stdout: true)
  end

  defp iex_path do
    Path.expand("../..", __DIR__)
  end

  if match?({:win32, _}, :os.type()) do
    defp executable_extension, do: ".bat"
  else
    defp executable_extension, do: ""
  end
end
