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

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

defmodule Code.Formatter.IntegrationTest do
  use ExUnit.Case, async: true

  import CodeFormatterHelpers

  test "empty documents" do
    assert_format "   ", ""
    assert_format "\n", ""
    assert_format ";", ""
  end

  test "function with multiple calls and case" do
    assert_same """
    def equivalent(string1, string2) when is_binary(string1) and is_binary(string2) do
      quoted1 = Code.string_to_quoted!(string1)
      quoted2 = Code.string_to_quoted!(string2)

      case not_equivalent(quoted1, quoted2) do
        {left, right} -> {:error, left, right}
        nil -> :ok
      end
    end
    """
  end

  test "function with long pipeline" do
    assert_same ~S"""
    def to_algebra!(string, opts \\ []) when is_binary(string) and is_list(opts) do
      string
      |> Code.string_to_quoted!(wrap_literals_in_blocks: true, unescape: false)
      |> block_to_algebra(state(opts))
      |> elem(0)
    end
    """
  end

  test "case with multiple multi-line arrows" do
    assert_same ~S"""
    case meta[:format] do
      :list_heredoc ->
        string = list |> List.to_string() |> escape_string(:heredoc)
        {@single_heredoc |> line(string) |> concat(@single_heredoc) |> force_unfit(), state}

      :charlist ->
        string = list |> List.to_string() |> escape_string(@single_quote)
        {@single_quote |> concat(string) |> concat(@single_quote), state}

      _other ->
        list_to_algebra(list, state)
    end
    """
  end

  test "function with long guards" do
    assert_same """
    defp module_attribute_read?({:@, _, [{var, _, var_context}]})
         when is_atom(var) and is_atom(var_context) do
      Macro.classify_atom(var) == :identifier
    end
    """
  end

  test "anonymous function with single clause and blocks" do
    assert_same """
    {args_doc, state} =
      Enum.reduce(args, {[], state}, fn quoted, {acc, state} ->
        {doc, state} = quoted_to_algebra(quoted, :block, state)
        doc = doc |> concat(nest(break(""), :reset)) |> group()
        {[doc | acc], state}
      end)
    """
  end

  test "anonymous function with long single clause and blocks" do
    assert_same """
    {function_count, call_count, total_time} =
      Enum.reduce(call_results, {0, 0, 0}, fn {_, {count, time}},
                                              {function_count, call_count, total_time} ->
        {function_count + 1, call_count + count, total_time + time}
      end)
    """
  end

  test "cond with long clause args" do
    assert_same """
    cond do
      parent_prec == prec and parent_assoc == side ->
        binary_op_to_algebra(op, op_string, left, right, context, state, op_info, nesting)

      parent_op in @required_parens_on_binary_operands or parent_prec > prec or
          (parent_prec == prec and parent_assoc != side) ->
        {operand, state} =
          binary_op_to_algebra(op, op_string, left, right, context, state, op_info, 2)

        {concat(concat("(", nest(operand, 1)), ")"), state}

      true ->
        binary_op_to_algebra(op, op_string, left, right, context, state, op_info, 2)
    end
    """
  end

  test "type with multiple |" do
    assert_same """
    @type t ::
            binary
            | :doc_nil
            | :doc_line
            | doc_string
            | doc_cons
            | doc_nest
            | doc_break
            | doc_group
            | doc_color
            | doc_force
            | doc_cancel
    """
  end

  test "spec with when keywords and |" do
    assert_same """
    @spec send(dest, msg, [option]) :: :ok | :noconnect | :nosuspend
          when dest: pid | port | atom | {atom, node},
               msg: any,
               option: :noconnect | :nosuspend
    """

    assert_same """
    @spec send(dest, msg, [option]) :: :ok | :noconnect | :nosuspend
          when dest:
                 pid
                 | port
                 | atom
                 | {atom, node}
                 | and_a_really_long_type_to_force_a_line_break
                 | followed_by_another_really_long_type
    """

    assert_same """
    @callback get_and_update(data, key, (value -> {get_value, value} | :pop)) :: {get_value, data}
              when get_value: var, data: container
    """
  end

  test "spec with multiple keys on type" do
    assert_same """
    @spec foo(%{(String.t() | atom) => any}) :: any
    """
  end

  test "multiple whens with new lines" do
    assert_same """
    def sleep(timeout)
        when is_integer(timeout) and timeout >= 0
        when timeout == :infinity do
      receive after: (timeout -> :ok)
    end
    """
  end

  test "function with operator and pipeline" do
    assert_same """
    defp apply_next_break_fits?({fun, meta, args}) when is_atom(fun) and is_list(args) do
      meta[:terminator] in [@double_heredoc, @single_heredoc] and
        fun |> Atom.to_string() |> String.starts_with?("sigil_")
    end
    """
  end

  test "mixed parens and no parens calls with anonymous function" do
    assert_same ~S"""
    node interface do
      resolve_type(fn
        %{__struct__: str}, _ ->
          str |> Model.Node.model_to_node_type()

        value, _ ->
          Logger.warning("Could not extract node type from value: #{inspect(value)}")
          nil
      end)
    end
    """
  end

  test "long defstruct definition" do
    assert_same """
    defstruct name: nil,
              module: nil,
              schema: nil,
              alias: nil,
              base_module: nil,
              web_module: nil,
              basename: nil,
              file: nil,
              test_file: nil
    """
  end

  test "mix of operators and arguments" do
    assert_same """
    def count(%{path: path, line_or_bytes: bytes}) do
      case File.stat(path) do
        {:ok, %{size: 0}} -> {:error, __MODULE__}
        {:ok, %{size: size}} -> {:ok, div(size, bytes) + if(rem(size, bytes) == 0, do: 0, else: 1)}
        {:error, reason} -> raise File.Error, reason: reason, action: "stream", path: path
      end
    end
    """
  end

  test "mix of left and right operands" do
    assert_same """
    defp server_get_modules(handlers) do
      for(handler(module: module) <- handlers, do: module)
      |> :ordsets.from_list()
      |> :ordsets.to_list()
    end
    """

    assert_same """
    neighbours = for({_, _} = t <- neighbours, do: t) |> :sets.from_list()
    """
  end

  test "long expression with single line anonymous function" do
    assert_same """
    for_many(uniq_list_of(integer(1..10000)), fn list ->
      assert Enum.uniq(list) == list
    end)
    """
  end

  test "long comprehension" do
    assert_same """
    for %{app: app, opts: opts, top_level: true} <- Mix.Dep.cached(),
        Keyword.get(opts, :app, true),
        Keyword.get(opts, :runtime, true),
        not Keyword.get(opts, :optional, false),
        app not in included_applications,
        app not in included_applications,
        do: app
    """
  end

  test "short comprehensions" do
    assert_same """
    for {protocol, :protocol, _beam} <- removed_metadata,
        remove_consolidated(protocol, output),
        do: {protocol, true},
        into: %{}
    """
  end

  test "comprehensions with when" do
    assert_same """
    for {key, value} when is_atom(key) <- Map.to_list(map),
        key = Atom.to_string(key),
        String.starts_with?(key, hint) do
      %{kind: :map_key, name: key, value_is_map: is_map(value)}
    end
    """

    assert_same """
    with {_, doc} when unquote(doc_attr?) <-
           Module.get_attribute(__MODULE__, unquote(name), unquote(escaped)),
         do: doc
    """
  end

  test "next break fits followed by inline tuple" do
    assert_same """
    assert ExUnit.Filters.eval([line: "1"], [:line], %{line: 3, describe_line: 2}, tests) ==
             {:error, "due to line filter"}
    """
  end

  test "try/catch with clause comment" do
    assert_same """
    def format_error(reason) do
      try do
        do_format_error(reason)
      catch
        # A user could create an error that looks like a built-in one
        # causing an error.
        :error, _ ->
          inspect(reason)
      end
    end
    """
  end

  test "case with when and clause comment" do
    assert_same """
    case decomposition do
      # Decomposition
      <<h, _::binary>> when h != ?< ->
        decomposition =
          decomposition
          |> :binary.split(" ", [:global])
          |> Enum.map(&String.to_integer(&1, 16))

        Map.put(dacc, String.to_integer(codepoint, 16), decomposition)

      _ ->
        dacc
    end
    """
  end

  test "do-end inside binary" do
    assert_same """
    <<if true do
        "hello"
      end::binary>>
    """
  end

  test "anonymous function with parens around integer argument" do
    bad = """
    fn (1) -> "hello" end
    """

    assert_format bad, """
    fn 1 -> "hello" end
    """
  end

  test "no parens keywords at the end of the line" do
    bad = """
    defmodule Mod do
      def token_list_downcase(<<char, rest::binary>>, acc) when is_whitespace(char) or is_comma(char), do: token_list_downcase(rest, acc)
      def token_list_downcase(some_really_long_arg11, some_really_long_arg22, some_really_long_arg33), do: token_list_downcase(rest, acc)
    end
    """

    assert_format bad, """
    defmodule Mod do
      def token_list_downcase(<<char, rest::binary>>, acc) when is_whitespace(char) or is_comma(char),
        do: token_list_downcase(rest, acc)

      def token_list_downcase(some_really_long_arg11, some_really_long_arg22, some_really_long_arg33),
        do: token_list_downcase(rest, acc)
    end
    """
  end

  test "do at the end of the line" do
    bad = """
    foo bar, baz, quux do
      :ok
    end
    """

    good = """
    foo bar,
        baz,
        quux do
      :ok
    end
    """

    assert_format bad, good, line_length: 18
  end

  test "keyword lists in last line" do
    assert_same """
    content =
      config(VeryLongModuleNameThatWillCauseBreak, "new.html",
        conn: conn,
        changeset: changeset,
        categories: categories
      )
    """

    assert_same """
    content =
      config VeryLongModuleNameThatWillCauseBreak, "new.html",
        conn: conn,
        changeset: changeset,
        categories: categories
    """
  end

  test "keyword list at line limit" do
    bad = """
    pre()
    config(arg, foo: bar)
    post()
    """

    good = """
    pre()

    config(arg,
      foo: bar
    )

    post()
    """

    assert_format bad, good, line_length: 20
  end

  test "do at the end of the line with single argument" do
    bad = """
    defmodule Location do
      def new(line, column) when is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
        %{column: column, line: line}
      end
    end
    """

    assert_format bad, """
    defmodule Location do
      def new(line, column)
          when is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
        %{column: column, line: line}
      end
    end
    """
  end

  test "tuples as trees" do
    bad = """
    @document Parser.parse(
      {"html", [], [
         {"head", [], []},
         {"body", [], [
              {"div", [], [
                  {"p", [], ["1"]},
                  {"p", [], ["2"]},
                  {"div", [], [
                      {"p", [], ["3"]},
                      {"p", [], ["4"]}]},
                  {"p", [], ["5"]}]}]}]})
    """

    assert_format bad, """
    @document Parser.parse(
                {"html", [],
                 [
                   {"head", [], []},
                   {"body", [],
                    [
                      {"div", [],
                       [
                         {"p", [], ["1"]},
                         {"p", [], ["2"]},
                         {"div", [],
                          [
                            {"p", [], ["3"]},
                            {"p", [], ["4"]}
                          ]},
                         {"p", [], ["5"]}
                       ]}
                    ]}
                 ]}
              )
    """
  end

  test "nested tuples as lines" do
    assert_same """
                {:ok,
                 {1, 2, 3,
                  4, 5}} =
                  call()
                """,
                line_length: 10
  end

  test "first argument in a call without parens with comments" do
    assert_same """
    with bar ::
           :ok
           | :invalid
           # | :unknown
           | :other
    """

    assert_same """
    @spec bar ::
            :ok
            | :invalid
            # | :unknown
            | :other
    """
  end

  test "when with keywords inside call" do
    assert_same """
    quote((bar(foo(1)) when bat: foo(1)), [])
    """

    assert_same """
    quote(do: (bar(foo(1)) when bat: foo(1)), line: 1)
    """

    assert_same """
    typespec(quote(do: (bar(foo(1)) when bat: foo(1))), [foo: 1], [])
    """
  end

  test "false positive sigil" do
    assert_same """
    def sigil_d(<<year::2-bytes, "-", month::2-bytes, "-", day::2-bytes>>, calendar) do
      ymd(year, month, day, calendar)
    end
    """
  end

  test "newline after stab" do
    assert_same """
    capture_io(":erl. mof*,,l", fn ->
      assert :io.scan_erl_form(~c">") == {:ok, [{:":", 1}, {:atom, 1, :erl}, {:dot, 1}], 1}

      expected_tokens = [{:atom, 1, :mof}, {:*, 1}, {:",", 1}, {:",", 1}, {:atom, 1, :l}]
      assert :io.scan_erl_form(~c">") == {:ok, expected_tokens, 1}

      assert :io.scan_erl_form(~c">") == {:eof, 1}
    end)
    """
  end

  test "capture with operators" do
    assert_same """
    "this works" |> (&String.upcase/1) |> (&String.downcase/1)
    """

    assert_same """
    "this works" || (&String.upcase/1) || (&String.downcase/1)
    """

    assert_same """
    "this works" == (&String.upcase/1) == (&String.downcase/1)
    """

    bad = """
    "this works" = (&String.upcase/1) = (&String.downcase/1)
    """

    assert_format bad, """
    "this works" = (&String.upcase/1) = &String.downcase/1
    """

    bad = """
    "this works" ++ (&String.upcase/1) ++ (&String.downcase/1)
    """

    assert_format bad, """
    "this works" ++ (&String.upcase/1) ++ &String.downcase/1
    """

    bad = """
    "this works" +++ (&String.upcase/1) +++ (&String.downcase/1)
    """

    assert_format bad, """
    "this works" +++ (&String.upcase/1) +++ &String.downcase/1
    """

    bad = """
    "this works" | (&String.upcase/1) | (&String.downcase/1)
    """

    assert_format bad, """
    "this works" | (&String.upcase/1) | &String.downcase/1
    """

    bad = ~S"""
    "this works" \\ (&String.upcase/1) \\ (&String.downcase/1)
    """

    assert_format bad, ~S"""
    "this works" \\ &String.upcase/1 \\ &String.downcase/1
    """
  end

  test "multiline expression inside interpolation" do
    bad = ~S"""
    Logger.info("Example: #{
      inspect(%{
        a: 1,
        b: 2
      })
    }")
    """

    assert_format bad, ~S"""
    Logger.info("Example: #{inspect(%{a: 1, b: 2})}")
    """
  end

  test "comment inside operator with when" do
    bad = """
    raise function(x) ::
            # Comment
            any
    """

    assert_format bad, """
    # Comment
    raise function(x) ::
            any
    """

    bad = """
    raise function(x) ::
            # Comment
            any
          when x: any
    """

    assert_format bad, """
    raise function(x) ::
            any
          # Comment
          when x: any
    """

    bad = """
    @spec function(x) ::
            # Comment
            any
          when x: any
    """

    assert_format bad, """
    @spec function(x) ::
            any
          # Comment
          when x: any
    """

    bad = """
    @spec function(x) ::
            # Comment
            any
          when x
          when y
    """

    assert_format bad, """
    @spec function(x) ::
            any
          # Comment
          when x
          when y
    """
  end

  test "nested heredocs with multi-line string in interpolation" do
    bad = ~S'''
    def foo do
      """
      #{(feature_flag(:feature_x) && "
      new_field
      " || "")}
      """
    end
    '''

    good = ~S'''
    def foo do
      """
      #{(feature_flag(:feature_x) && "
      new_field
      ") || ""}
      """
    end
    '''

    assert_format bad, good
  end

  test "functions with infinity line length" do
    assert_same ~S"""
                x = fn ->
                  {:ok, pid} = Repl.start_link({self(), opts})
                  assert Exception.message(error) =~ msg
                end
                """,
                line_length: :infinity

    assert_same ~S"""
                capture_log(fn x ->
                  {:ok, pid} = Repl.start_link({self(), opts})
                  assert Exception.message(error) =~ msg
                end) =~ msg
                """,
                line_length: :infinity

    assert_same ~S"""
                capture_log(fn ->
                  {:ok, pid} = Repl.start_link({self(), opts})
                  assert Exception.message(error) =~ msg
                end) =~ msg
                """,
                line_length: :infinity

    assert_same ~S"""
                capture_log(fn x ->
                  {:ok, pid} = Repl.start_link({self(), opts})
                  assert Exception.message(error) =~ msg
                end) =~ msg
                """,
                line_length: :infinity
  end

  test "functions without parentheses within do: keyword" do
    assert_format ~S"defmodule Foo, do: foo bar, baz",
                  ~S"defmodule Foo, do: foo(bar, baz)"
  end
end
