# 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.Normalizer.FormatterASTTest do
  use ExUnit.Case, async: true

  defmacro assert_same(good, opts \\ []) do
    quote bind_quoted: [good: good, opts: opts], location: :keep do
      assert IO.iodata_to_binary(Code.format_string!(good, opts)) ==
               string_to_string(good, opts)
    end
  end

  def string_to_string(good, opts) do
    line_length = Keyword.get(opts, :line_length, 98)
    good = String.trim(good)

    to_quoted_opts =
      Keyword.merge(
        [
          literal_encoder: &{:ok, {:__block__, &2, [&1]}},
          token_metadata: true,
          unescape: false
        ],
        opts
      )

    {quoted, comments} = Code.string_to_quoted_with_comments!(good, to_quoted_opts)

    to_algebra_opts = Keyword.merge([comments: comments, escape: false], opts)

    quoted
    |> Code.quoted_to_algebra(to_algebra_opts)
    |> Inspect.Algebra.format(line_length)
    |> IO.iodata_to_binary()
  end

  describe "integers" do
    test "in decimal base" do
      assert_same "0"
      assert_same "100"
      assert_same "007"
      assert_same "10000"
      assert_same "100_00"
    end

    test "in binary base" do
      assert_same "0b0"
      assert_same "0b1"
      assert_same "0b101"
      assert_same "0b01"
      assert_same "0b111_111"
    end

    test "in octal base" do
      assert_same "0o77"
      assert_same "0o0"
      assert_same "0o01"
      assert_same "0o777_777"
    end

    test "in hex base" do
      assert_same "0x1"
      assert_same "0x01"
    end

    test "as chars" do
      assert_same "?a"
      assert_same "?1"
      assert_same "?è"
      assert_same "??"
      assert_same "?\\\\"
      assert_same "?\\s"
      assert_same "?🎾"
    end
  end

  describe "floats" do
    test "with normal notation" do
      assert_same "0.0"
      assert_same "1.0"
      assert_same "123.456"
      assert_same "0.0000001"
      assert_same "001.100"
      assert_same "0_10000_0.000_000"
    end

    test "with scientific notation" do
      assert_same "1.0e1"
      assert_same "1.0e-1"
      assert_same "1.0e01"
      assert_same "1.0e-01"
      assert_same "001.100e-010"
      assert_same "0_100_0000.100e-010"
    end
  end

  describe "atoms" do
    test "true, false, nil" do
      assert_same "nil"
      assert_same "true"
      assert_same "false"
    end

    test "without escapes" do
      assert_same ~S[:foo]
    end

    test "with escapes" do
      assert_same ~S[:"f\a\b\ro"]
    end

    test "with unicode" do
      assert_same ~S[:ólá]
    end

    test "does not reformat aliases" do
      assert_same ~S[:"Elixir.String"]
      assert_same ~S[:"Elixir"]
    end

    test "quoted operators" do
      assert_same ~S[:"::"]
      assert_same ~S[:"..//"]
      assert_same ~S{["..//": 1]}
    end

    test "with interpolation" do
      assert_same ~S[:"one #{2} three"]
    end

    test "with escapes and interpolation" do
      assert_same ~S[:"one\n\"#{2}\"\nthree"]
    end
  end

  describe "strings" do
    test "without escapes" do
      assert_same ~S["foo"]
    end

    test "with escapes" do
      assert_same ~S["\x0A"]
      assert_same ~S["f\a\b\ro"]
      assert_same ~S["double \" quote"]
    end

    test "keeps literal new lines" do
      assert_same """
      "fo
      o"
      """
    end

    test "with interpolation" do
      assert_same ~S["one #{} three"]
      assert_same ~S["one #{2} three"]
    end

    test "with escaped interpolation" do
      assert_same ~S["one\#{two}three"]
    end

    test "with escapes and interpolation" do
      assert_same ~S["one\n\"#{2}\"\nthree"]
    end
  end

  describe "lists" do
    test "on module attribute" do
      assert_same ~S"@foo [1]"
    end
  end

  describe "charlists" do
    test "without escapes" do
      assert_same ~S[~c""]
      assert_same ~S[~c" "]
      assert_same ~S[~c"foo"]
    end

    test "with escapes" do
      assert_same ~S[~c"f\a\b\ro"]
      assert_same ~S[~c'single \' quote']
      assert_same ~S[~c"double \" quote"]
    end

    test "keeps literal new lines" do
      assert_same """
      ~c"fo
      o"
      """
    end

    test "with interpolation" do
      assert_same ~S[~c"one #{2} three"]
    end

    test "with escape and interpolation" do
      assert_same ~S[~c'one\n\'#{2}\'\nthree']
    end
  end

  describe "string heredocs" do
    test "without escapes" do
      assert_same ~S'''
      """
      hello
      """
      '''
    end

    test "with escapes" do
      assert_same ~S'''
      """
      f\a\b\ro
      """
      '''

      assert_same ~S'''
      """
      multiple "\"" quotes
      """
      '''
    end

    test "with interpolation" do
      assert_same ~S'''
      """
      one
      #{2}
      three
      """
      '''

      assert_same ~S'''
      """
      one
      "
      #{2}
      "
      three
      """
      '''
    end

    test "nested with empty lines" do
      assert_same ~S'''
      nested do
        """

        foo


        bar

        """
      end
      '''
    end

    test "nested with empty lines and interpolation" do
      assert_same ~S'''
      nested do
        """

        #{foo}


        #{bar}

        """
      end
      '''

      assert_same ~S'''
      nested do
        """
        #{foo}


        #{bar}
        """
      end
      '''
    end

    test "with escaped new lines" do
      source =
        String.trim(~S'''
        """
        one\
        #{"two"}\
        three\
        """
        ''')

      assert_same source

      # This is the same default as assert_same
      assert string_to_string(source, unescape: false, escape: false) == source

      # If we unescape, we cannot keep the original representation
      # but we must keep it as a valid heredoc
      assert string_to_string(source, unescape: true, escape: true) ==
               String.trim(~S'''
               """
               one#{"two"}three
               """
               ''')
    end

    test "with empty escaped new lines" do
      source =
        String.trim(~S'''
        """
        one\
        #{"two"}\
        three\
        """
        ''')

      assert_same source

      # This is the same default as assert_same
      assert string_to_string(source, unescape: false, escape: false) == source

      # If we unescape, we cannot keep the original representation
      # but we must keep it as a valid heredoc
      assert string_to_string(source, unescape: true, escape: true) ==
               String.trim(~S'''
               """
               one#{"two"}three
               """
               ''')
    end
  end

  test "charlist heredocs" do
    ExUnit.CaptureIO.capture_io(:stderr, fn ->
      # without escapes
      assert_same ~S"""
      ~c'''
      hello
      '''
      """

      # with escapes
      assert_same ~S"""
      ~c'''
      f\a\b\ro
      '''
      """

      # with quotes
      assert_same ~S"""
      ~c'''
      multiple "\"" quotes
      '''
      """

      # with interpolation
      assert_same ~S"""
      ~c'''
      one
      #{2}
      three
      '''
      """

      assert_same ~S"""
      ~c'''
      one
      "
      #{2}
      "
      three
      '''
      """

      # with empty escaped new lines
      source =
        String.trim(~S"""
        '''
        one\
        #{"two"}\
        three\
        '''
        """)

      assert_same source

      # This is the same default as assert_same
      assert string_to_string(source, unescape: false, escape: false) == source

      # If we unescape, we cannot keep the original representation
      # but we must keep it as a valid heredoc
      assert string_to_string(source, unescape: true, escape: true) ==
               String.trim(~S"""
               '''
               one#{"two"}three
               '''
               """)
    end)
  end

  describe "keyword list" do
    test "blocks" do
      assert_same ~S"""
      defmodule Example do
        def sample, do: :ok
      end
      """

      assert_same ~S"""
      with true, do: :ok, else: (_ -> :ok)
      """
    end

    test "omitting brackets" do
      assert_same ~S"""
      @type foo :: a when b: :c
      """
    end

    test "last tuple element as keyword list keeps its format" do
      assert_same ~S"{:wrapped, [opt1: true, opt2: false]}"
      assert_same ~S"{:unwrapped, opt1: true, opt2: false}"
      assert_same ~S"{:wrapped, 1, [opt1: true, opt2: false]}"
      assert_same ~S"{:unwrapped, 1, opt1: true, opt2: false}"
    end

    test "on module attribute" do
      assert_same ~S"""
      @foo a: b,
           c: d
      """

      assert_same ~S"@foo [
        a: b,
        c: d
      ]"
    end
  end

  describe "preserves user choice on parenthesis" do
    test "in functions with do blocks" do
      assert_same(~S"""
      foo Bar do
        :ok
      end
      """)

      assert_same(~S"""
      foo(Bar) do
        :ok
      end
      """)
    end
  end

  describe "preserves formatting for sigils" do
    test "without interpolation" do
      assert_same ~S[~s(foo)]
      assert_same ~S[~s{foo bar}]
      assert_same ~S[~r/Bar Baz/]
      assert_same ~S[~w<>]
      assert_same ~S[~W()]
    end

    test "with escapes" do
      assert_same ~S[~s(foo \) bar)]
      assert_same ~S[~s(f\a\b\ro)]

      assert_same ~S"""
      ~S(foo\
      bar)
      """
    end

    test "with nested new lines" do
      assert_same ~S"""
      foo do
        ~S(foo\
      bar)
      end
      """

      assert_same ~S"""
      foo do
        ~s(#{bar}
      )
      end
      """
    end

    test "with interpolation" do
      assert_same ~S[~s(one #{2} three)]
    end

    test "with modifiers" do
      assert_same ~S[~w(one two three)a]
      assert_same ~S[~z(one two three)foo]
    end

    test "with heredoc syntax" do
      assert_same ~S"""
      ~s'''
      one\a
      #{:two}\r
      three\0
      '''
      """

      assert_same ~S'''
      ~s"""
      one\a
      #{:two}\r
      three\0
      """
      '''
    end

    test "with heredoc syntax and modifier" do
      assert_same ~S"""
      ~s'''
      foo
      '''rsa
      """
    end

    test "with empty escaped new lines" do
      source =
        String.trim(~S"""
        ~c'''
        one\
        #{"two"}\
        three\
        '''
        """)

      assert_same source

      # Heredoc in sigils always preserves newlines regardless of escape/unescape
      assert string_to_string(source, unescape: false, escape: false) == source
      assert string_to_string(source, unescape: true, escape: true) == source
    end
  end

  describe "preserves comments formatting" do
    test "before and after expressions" do
      assert_same """
      # before comment
      :hello
      """

      assert_same """
      :hello
      # after comment
      """

      assert_same """
      # before comment
      :hello
      # after comment
      """
    end

    test "empty comment" do
      assert_same """
      #
      :foo
      """
    end

    test "handles comments with unescaped literal" do
      assert_same """
                  # before
                  Mix.install([:foo])
                  # after
                  """,
                  literal_encoder: fn literal, _ -> {:ok, literal} end

      assert_same """
                  # before
                  Mix.install([1 + 2, :foo])
                  # after
                  """,
                  literal_encoder: fn literal, _ -> {:ok, literal} end

      assert_same """
                  # before
                  Mix.install([:foo, 1 + 2])
                  # after
                  """,
                  literal_encoder: fn literal, _ -> {:ok, literal} end

      assert_same """
                  block do
                    # before 1
                    1 + 1

                    # before 2
                    2 + 2
                  end
                  """,
                  literal_encoder: fn literal, _ -> {:ok, literal} end

      assert_same """
                  block do
                    # before 1
                    Mix.install([1 + 1])

                    # before 2
                    Mix.install([2 + 2])
                  end
                  """,
                  literal_encoder: fn literal, _ -> {:ok, literal} end
    end

    test "before and after expressions with newlines" do
      assert_same """
      # before comment
      # second line

      :hello

      # middle comment 1

      #

      # middle comment 2

      :world

      # after comment
      # second line
      """
    end

    test "interpolation with comment outside before and after" do
      assert_same ~S"""
      # comment
      IO.puts("Hello #{world}")
      """

      assert_same ~S"""
      IO.puts("Hello #{world}")
      # comment
      """
    end

    test "blocks with keyword list" do
      assert_same ~S"""
      defp sample do
        [
          # comment
          {:a, "~> 1.2"}
        ]
      end
      """

      assert_same ~S"""
      defp sample do
        [
          # comment
          {:a, "~> 1.2"},
          {:b, "~> 1.2"}
        ]
      end
      """
    end

    test "keyword literals with variable values" do
      assert_same(~S"""
      foo = foo()
      [foo: foo]
      """)
    end
  end
end
