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

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

require EEx

defmodule EExTest.Compiled do
  def before_compile do
    {__ENV__.line, hd(tl(get_stacktrace()))}
  end

  EEx.function_from_string(:def, :string_sample, "<%= a + b %>", [:a, :b])

  filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
  EEx.function_from_file(:defp, :private_file_sample, filename, [:bar])

  filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
  EEx.function_from_file(:def, :public_file_sample, filename, [:bar])

  def file_sample(arg), do: private_file_sample(arg)

  def after_compile do
    {__ENV__.line, hd(tl(get_stacktrace()))}
  end

  @file "unknown"
  def unknown do
    {__ENV__.line, hd(tl(get_stacktrace()))}
  end

  defp get_stacktrace do
    try do
      :erlang.error("failed")
    rescue
      _ -> __STACKTRACE__
    end
  end
end

defmodule Clause do
  defmacro defclause(expr, block) do
    quote do
      def unquote(expr), unquote(block)
    end
  end
end

defmodule EExTest do
  use ExUnit.Case, async: true

  doctest EEx
  doctest EEx.Engine
  doctest EEx.SmartEngine

  describe "evaluates" do
    test "simple string" do
      assert_eval("foo bar", "foo bar")
    end

    test "Unicode" do
      template = """
        • <%= "•" %> •
        <%= "Jößé Vâlìm" %> Jößé Vâlìm
      """

      assert_eval("  • • •\n  Jößé Vâlìm Jößé Vâlìm\n", template)
    end

    test "no spaces" do
      string = """
      <%=cond do%>
      <%false ->%>
        this
      <%true ->%>
        that
      <%end%>
      """

      expected = "\n  that\n\n"
      assert_eval(expected, string, [])
    end

    test "trim mode" do
      string = "<%= 123 %> \n  \n  <%= 789 %>"
      expected = "123\n789"
      assert_eval(expected, string, [], trim: true)

      string = "<%= 123 %> \n456\n  <%= 789 %>"
      expected = "123\n456\n789"
      assert_eval(expected, string, [], trim: true)

      string = "<%= 123 %> \n\n456\n\n  <%= 789 %>"
      expected = "123\n456\n789"
      assert_eval(expected, string, [], trim: true)

      string = "<%= 123 %> \n  \n456\n  \n  <%= 789 %>"
      expected = "123\n456\n789"
      assert_eval(expected, string, [], trim: true)

      string = "\n  <%= 123 %> \n  <%= 456 %>  \n  <%= 789 %>  \n"
      expected = "123\n456\n789"
      assert_eval(expected, string, [], trim: true)

      string = "\r\n  <%= 123 %> \r\n  <%= 456 %>  \r\n  <%= 789 %>  \r\n"
      expected = "123\n456\n789"
      assert_eval(expected, string, [], trim: true)
    end

    test "trim mode with middle expression" do
      string = """
      <%= cond do %>
      <% false -> %>
        this
      <% true -> %>
        that
      <% end %>
      """

      expected = "\n  that\n"
      assert_eval(expected, string, [], trim: true)
    end

    test "trim mode with multiple lines" do
      string = """
      <%= "First line" %>
      <%= "Second line" %>
      <%= "Third line" %>
      <%= "Fourth line" %>
      """

      expected = "First line\nSecond line\nThird line\nFourth line"
      assert_eval(expected, string, [], trim: true)
    end

    test "trim mode with no spaces" do
      string = """
      <%=if true do%>
        this
      <%else%>
        that
      <%end%>
      """

      expected = "\n  this\n"
      assert_eval(expected, string, [], trim: true)

      string = """
      <%=cond do%>
      <%false ->%>
        this
      <%true ->%>
        that
      <%end%>
      """

      expected = "\n  that\n"
      assert_eval(expected, string, [], trim: true)
    end

    test "embedded code" do
      assert_eval("foo bar", "foo <%= :bar %>")
    end

    test "embedded code with binding" do
      assert EEx.eval_string("foo <%= bar %>", bar: 1) == "foo 1"
    end

    test "embedded code with do end when true" do
      assert_eval("foo bar", "foo <%= if true do %>bar<% end %>")
    end

    test "embedded code with do end when false" do
      assert_eval("foo ", "foo <%= if false do %>bar<% end %>")
    end

    test "embedded code with do preceded by bracket" do
      assert_eval("foo bar", "foo <%= if {true}do %>bar<% end %>")
      assert_eval("foo bar", "foo <%= if (true)do %>bar<% end %>")
      assert_eval("foo bar", "foo <%= if [true]do %>bar<% end %>")
    end

    test "embedded code with do end and expression" do
      assert_eval("foo bar", "foo <%= if true do %><%= :bar %><% end %>")
    end

    test "embedded code with do end and multiple expressions" do
      assert_eval(
        "foo bar baz",
        "foo <%= if true do %>bar <% Process.put(:eex_text, 1) %><%= :baz %><% end %>"
      )

      assert Process.get(:eex_text) == 1
    end

    test "embedded code with middle expression" do
      assert_eval("foo bar", "foo <%= if true do %>bar<% else %>baz<% end %>")
    end

    test "embedded code with evaluated middle expression" do
      assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
    end

    test "embedded code with multi-line comments in do end" do
      assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")

      assert_eval(
        "foo\n\nbar\n",
        "foo\n<%= case true do %>\n<%!-- comment --%>\n<% true -> %>\nbar\n<% end %>"
      )
    end

    test "embedded code with nested do end" do
      assert_eval("foo bar", "foo <%= if true do %><%= if true do %>bar<% end %><% end %>")
    end

    test "embedded code with nested do end with middle expression" do
      assert_eval(
        "foo baz",
        "foo <%= if true do %><%= if false do %>bar<% else %>baz<% end %><% end %>"
      )
    end

    test "embedded code with end followed by bracket" do
      assert_eval(
        " 101  102  103 ",
        "<%= Enum.map([1, 2, 3], fn x -> %> <%= 100 + x %> <% end) %>"
      )

      assert_eval(
        " 101  102  103 ",
        "<%= Enum.map([1, 2, 3], fn x ->\n%> <%= 100 + x %> <% end) %>"
      )

      assert_eval(
        " 101  102  103 ",
        "<%= apply Enum, :map, [[1, 2, 3], fn x -> %> <%= 100 + x %> <% end] %>"
      )

      assert_eval(
        " 101  102  103 ",
        "<%= #{__MODULE__}.tuple_map {[1, 2, 3], fn x -> %> <%= 100 + x %> <% end} %>"
      )

      assert_eval(
        " 101  102  103 ",
        "<%= apply(Enum, :map, [[1, 2, 3], fn x -> %> <%= 100 + x %> <% end]) %>"
      )

      assert_eval(
        " 101  102  103 ",
        "<%= Enum.map([1, 2, 3], (fn x -> %> <%= 100 + x %> <% end) ) %>"
      )
    end

    test "embedded code with variable definition" do
      assert_eval("foo 1", "foo <% bar = 1 %><%= bar %>")
    end

    test "embedded code with require" do
      assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
    end

    test "with end of token" do
      assert_eval("foo bar %>", "foo bar %>")
    end
  end

  describe "raises syntax errors" do
    test "with relative file information" do
      message = """
      foobar.eex:1:5: expected closing '%>' for EEx expression
        |
      1 | foo <%= bar
        |     ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo <%= bar", file: Path.join(File.cwd!(), "foobar.eex"))
      end
    end

    test "when <%!-- is not closed" do
      message = """
      my_file.eex:1:5: expected closing '--%>' for EEx expression
        |
      1 | foo <%!-- bar
        |     ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo <%!-- bar", file: "my_file.eex")
      end
    end

    test "when the token is invalid" do
      message = """
      nofile:1:5: expected closing '%>' for EEx expression
        |
      1 | foo <%= bar
        |     ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo <%= bar")
      end
    end

    test "when middle expression is found without a start expression" do
      message = """
      nofile:5:1: unexpected middle of expression <% else %>
        |
      2 | <%= "content" %>
      3 | <%= if true %>
      4 |   <%= "foo" %>
      5 | <% else %>
        | ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string(
          ~s(<h1>Hi!</h1>\n<%= "content" %>\n<%= if true %>\n  <%= "foo" %>\n<% else %>\n  bar<% end %>)
        )
      end
    end

    test "proper format line number of code snippet" do
      message = """
      nofile:11:1: unexpected middle of expression <% else %>
         |
       8 | <%= "content" %>
       9 | <%= if true %>
      10 |   <%= "foo" %>
      11 | <% else %>
         | ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string(
          ~s(\n\n\n\n\n\n<h1>Hi!</h1>\n<%= "content" %>\n<%= if true %>\n  <%= "foo" %>\n<% else %>\n  bar<% end %>)
        )
      end
    end

    test "when there is only middle expression" do
      message = """
      nofile:1:1: unexpected middle of expression <% else %>
        |
      1 | <% else %>
        | ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string(~s(<% else %>))
      end
    end

    test "when it is missing a `do` in case expr" do
      message = """
      nofile:3:3: unexpected middle of expression <% :something -> %>
        |
      1 | content
      2 | <%= case @var %>
      3 |   <% :something -> %>
        |   ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("content\n<%= case @var %>\n  <% :something -> %>\n    bar<% end %>")
      end
    end

    test "when it is a `do` in cond expr" do
      message = """
      nofile:3:3: unexpected middle of expression <% true -> %>
        |
      1 | content
      2 | <%= cond %>
      3 |   <% true -> %>
        |   ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("content\n<%= cond %>\n  <% true -> %>\n    bar<% end %>")
      end
    end

    test "when end expression is found without a start expression" do
      message = """
      nofile:1:5: unexpected end of expression <% end %>
        |
      1 | foo <% end %>
        |     ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo <% end %>")
      end
    end

    test "when start expression is found without an end expression" do
      message = """
      nofile:2:5: expected a closing '<% end %>' for block expression in EEx
        |
      1 | foo
      2 | <%= if true do %>
        |     ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo\n<%= if true do %>\nfoo\n")
      end

      message = """
      nofile:3:3: expected a closing '<% end %>' for block expression in EEx
        |
      1 | foo
      2 | <%=
      3 |   if true do %>
        |   ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo\n<%=\n  if true do %>\nfoo\n", indentation: 0)
      end

      message = """
      nofile:3:6: expected a closing '<% end %>' for block expression in EEx
        |
      1 |    foo
      2 |    <%=
      3 |      if true do %>
        |      ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo\n<%=\n  if true do %>\nfoo\n", indentation: 3)
      end
    end

    test "when start expression with middle expression is found without an end expression" do
      message = """
      nofile:2:5: expected a closing '<% end %>' for block expression in EEx
        |
      1 | foo
      2 | <%= if true do %>
        |     ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo\n<%= if true do %>\nfoo\n<% else %>\n")
      end
    end

    test "when multiple start expressions is found without an end expression" do
      message = """
      nofile:5:5: expected a closing '<% end %>' for block expression in EEx
        |
      2 | <%= if true do %>
      3 |   <%= @something %>
      4 |\s
      5 | <%= if @var do %>
        |     ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string(
          "foo\n<%= if true do %>\n  <%= @something %>\n\n<%= if @var do %>\nfoo\n"
        )
      end
    end

    test "when nested end expression is found without a start expression" do
      message = """
      nofile:1:31: unexpected end of expression <% end %>
        |
      1 | foo <%= if true do %><% end %><% end %>
        |                               ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo <%= if true do %><% end %><% end %>")
      end
    end

    test "when trying to use marker '|' without implementation" do
      msg =
        ~r/unsupported EEx syntax <%| %> \(the syntax is valid but not supported by the current EEx engine\)/

      assert_raise EEx.SyntaxError, msg, fn ->
        EEx.compile_string("<%| true %>")
      end
    end

    test "when trying to use marker '/' without implementation" do
      msg =
        ~r/unsupported EEx syntax <%\/ %> \(the syntax is valid but not supported by the current EEx engine\)/

      assert_raise EEx.SyntaxError, msg, fn ->
        EEx.compile_string("<%/ true %>")
      end
    end

    test "from Elixir parser" do
      line = __ENV__.line + 6

      message =
        assert_raise TokenMissingError, fn ->
          EEx.compile_string(
            """
            <li>
              <strong>Some:</strong>
              <%= true && @some[ %>
            </li>
            """,
            file: __ENV__.file,
            line: line,
            indentation: 12
          )
        end

      assert message |> Exception.message() |> strip_ansi() =~ """
                  │
              514 │                   true && @some[\s
                  │                                │ └ missing closing delimiter (expected "]")
                  │                                └ unclosed delimiter
             """
    end

    test "from Elixir parser with line breaks" do
      line = __ENV__.line + 6

      message =
        assert_raise TokenMissingError, fn ->
          EEx.compile_string(
            """
            <li>
              <strong>Some:</strong>
              <%= true &&
                @some[ %>
            </li>
            """,
            file: __ENV__.file,
            line: line,
            indentation: 12
          )
        end

      assert message |> Exception.message() |> strip_ansi() =~ """
                  │
              #{line + 3} │                 @some[\s
                  │                      │ └ missing closing delimiter (expected "]")
                  │                      └ unclosed delimiter
             """
    end

    test "honor line numbers" do
      assert_raise EEx.SyntaxError,
                   "nofile:100:6: expected closing '%>' for EEx expression",
                   fn ->
                     EEx.compile_string("foo\n bar <%= baz", line: 99)
                   end
    end

    test "honor file names" do
      message = """
      my_file.eex:1:5: expected closing '%>' for EEx expression
        |
      1 | foo <%= bar
        |     ^\
      """

      assert_raise EEx.SyntaxError, message, fn ->
        EEx.compile_string("foo <%= bar", file: "my_file.eex")
      end
    end

    test "unsupported marker error carries template location metadata" do
      error =
        assert_raise EEx.SyntaxError, fn ->
          EEx.compile_string("<%/ true %>", file: "sample.eex", line: 7)
        end

      assert error.file == "sample.eex"
      assert error.line == 7
      assert error.column == 1
      assert Exception.message(error) =~ "sample.eex:7:1:"
    end
  end

  describe "warnings" do
    test "when middle expression has a modifier" do
      assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
               EEx.compile_string("foo <%= if true do %>true<%= else %>false<% end %>")
             end) =~ ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= else %>\"]
    end

    test "when end expression has a modifier" do
      assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
               EEx.compile_string("foo <%= if true do %>true<% else %>false<%= end %>")
             end) =~
               ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
    end

    test "unused \"do\" block without \"<%=\" modifier" do
      assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
               EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
             end) =~ "the contents of this expression won't be output"

      # These are fine though
      EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
      EEx.compile_string("<% foo = if true do %>Hello<% end %>")
    end

    test "from tokenizer" do
      warning =
        ExUnit.CaptureIO.capture_io(:stderr, fn ->
          EEx.compile_string(~s'<%= :"foo" %>', file: "tokenizer.ex")
        end)

      assert warning =~ "found quoted atom \"foo\" but the quotes are not required"
      assert warning =~ "tokenizer.ex:1:5"
    end
  end

  describe "environment" do
    test "respects line numbers" do
      expected = """
      foo
      2
      """

      string = """
      foo
      <%= __ENV__.line %>
      """

      assert_eval(expected, string)
    end

    test "respects line numbers inside nested expressions" do
      expected = """
      foo

      3

      5
      """

      string = """
      foo
      <%= if true do %>
      <%= __ENV__.line %>
      <% end %>
      <%= __ENV__.line %>
      """

      assert_eval(expected, string)
    end

    test "respects line numbers inside start expression" do
      expected = """
      foo

      true

      5
      """

      string = """
      foo
      <%= if __ENV__.line == 2 do %>
      <%= true %>
      <% end %>
      <%= __ENV__.line %>
      """

      assert_eval(expected, string)
    end

    test "respects line numbers inside middle expression with ->" do
      expected = """
      foo

      true

      7
      """

      string = """
      foo
      <%= cond do %>
      <% false -> %> false
      <% __ENV__.line == 4 -> %>
      <%= true %>
      <% end %>
      <%= __ENV__.line %>
      """

      assert_eval(expected, string)
    end

    test "respects line number inside middle expressions with keywords" do
      expected = """
      foo

      5

      7
      """

      string = """
      foo
      <%= if false do %>
      <%= __ENV__.line %>
      <% else %>
      <%= __ENV__.line %>
      <% end %>
      <%= __ENV__.line %>
      """

      assert_eval(expected, string)
    end

    test "respects files" do
      assert_eval("sample.ex", "<%= __ENV__.file %>", [], file: "sample.ex")
    end
  end

  describe "clauses" do
    test "inside functions" do
      expected = """

      Number 1

      Number 2

      Number 3

      """

      string = """
      <%= Enum.map [1, 2, 3], fn x -> %>
      Number <%= x %>
      <% end %>
      """

      assert_eval(expected, string)
    end

    test "inside multiple functions" do
      expected = """

      A 1

      B 2

      A 3

      """

      string = """
      <%= #{__MODULE__}.switching_map [1, 2, 3], fn x -> %>
      A <%= x %>
      <% end, fn x -> %>
      B <%= x %>
      <% end %>
      """

      assert_eval(expected, string)
    end

    test "inside callback and do block" do
      expected = """


      A 1

      B 2

      A 3

      """

      string = """
      <% require #{__MODULE__} %>
      <%= #{__MODULE__}.switching_macro [1, 2, 3], fn x -> %>
      A <%= x %>
      <% end do %>
      B <%= x %>
      <% end %>
      """

      assert_eval(expected, string)
    end

    test "inside cond" do
      expected = """
      foo

      true

      """

      string = """
      foo
      <%= cond do %>
      <% false -> %> false
      <% fn -> 1 end -> %>
      <%= true %>
      <% end %>
      """

      assert_eval(expected, string)
    end

    test "inside cond with do end" do
      string = """
      <% y = ["a", "b", "c"] %>
      <%= cond do %>
       <% "a" in y -> %>
        Good
       <% true -> %>
        <%= if true do %>true<% else %>false<% end %>
        Bad
      <% end %>
      """

      assert_eval("\n\n  Good\n \n", string)
    end

    test "line and column meta" do
      indentation = 12

      ast =
        EEx.compile_string(
          """
          <%= f() %> <% f() %>
            <%= f fn -> %>
              <%= f() %>
            <% end %>
          """,
          indentation: indentation
        )

      {_, calls} =
        Macro.prewalk(ast, [], fn
          {:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
          other, acc -> {other, acc}
        end)

      assert Enum.reverse(calls) == [
               [line: 1, column: indentation + 5],
               [line: 1, column: indentation + 15],
               [line: 2, column: indentation + 7],
               [line: 3, column: indentation + 9]
             ]
    end
  end

  describe "buffers" do
    test "inside comprehensions" do
      string = """
      <%= for _name <- packages || [] do %>
      <% end %>
      <%= all || :done %>
      """

      assert_eval("\ndone\n", string, packages: nil, all: nil)
    end
  end

  describe "from file" do
    test "evaluates the source" do
      filename = Path.join(__DIR__, "fixtures/eex_template.eex")
      result = EEx.eval_file(filename)
      assert_normalized_newline_equal("foo bar.\n", result)
    end

    test "evaluates the source with bindings" do
      filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
      result = EEx.eval_file(filename, bar: 1)
      assert_normalized_newline_equal("foo 1\n", result)
    end

    test "raises an Exception when file is missing" do
      msg = "could not read file \"non-existent.eex\": no such file or directory"

      assert_raise File.Error, msg, fn ->
        filename = "non-existent.eex"
        EEx.compile_file(filename)
      end
    end

    test "sets external resource attribute" do
      assert EExTest.Compiled.__info__(:attributes)[:external_resource] ==
               [Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")]
    end

    test "supports t:Path.t() paths" do
      filename = to_charlist(Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex"))
      result = EEx.eval_file(filename, bar: 1)
      assert_normalized_newline_equal("foo 1\n", result)
    end

    test "supports overriding file and line through options" do
      filename = Path.join(__DIR__, "fixtures/eex_template_with_syntax_error.eex")

      assert_raise EEx.SyntaxError,
                   "my_file.eex:10:5: expected closing '%>' for EEx expression",
                   fn ->
                     EEx.eval_file(filename, _bindings = [], file: "my_file.eex", line: 10)
                   end
    end
  end

  describe "precompiled" do
    test "from string" do
      assert EExTest.Compiled.string_sample(1, 2) == "3"
    end

    test "from file" do
      assert_normalized_newline_equal("foo 1\n", EExTest.Compiled.file_sample(1))
      assert_normalized_newline_equal("foo 1\n", EExTest.Compiled.public_file_sample(1))
    end

    test "from file does not affect backtrace" do
      file = to_charlist(Path.relative_to_cwd(__ENV__.file))

      assert EExTest.Compiled.before_compile() ==
               {11, {EExTest.Compiled, :before_compile, 0, [file: file, line: 11]}}

      assert EExTest.Compiled.after_compile() ==
               {25, {EExTest.Compiled, :after_compile, 0, [file: file, line: 25]}}

      assert EExTest.Compiled.unknown() ==
               {30, {EExTest.Compiled, :unknown, 0, [file: ~c"unknown", line: 30]}}
    end
  end

  defmodule TestEngine do
    @behaviour EEx.Engine

    def init(_opts) do
      "INIT"
    end

    def handle_body(body) do
      "BODY(#{body})"
    end

    def handle_begin(_) do
      "BEGIN"
    end

    def handle_end(buffer) do
      buffer <> ":END"
    end

    def handle_text(buffer, meta, text) do
      buffer <> ":TEXT-#{meta[:line]}-#{meta[:column]}(#{String.trim(text)})"
    end

    def handle_expr(buffer, "/", expr) do
      buffer <> ":DIV(#{Macro.to_string(expr)})"
    end

    def handle_expr(buffer, "=", expr) do
      buffer <> ":EQUAL(#{Macro.to_string(expr)})"
    end

    def handle_expr(buffer, mark, expr) do
      EEx.Engine.handle_expr(buffer, mark, expr)
    end
  end

  describe "custom engines" do
    test "text" do
      assert_eval("BODY(INIT:TEXT-1-1(foo))", "foo", [], engine: TestEngine)
    end

    test "custom marker" do
      assert_eval("BODY(INIT:TEXT-1-1(foo):DIV(:bar))", "foo <%/ :bar %>", [], engine: TestEngine)
    end

    test "begin/end" do
      assert_eval(
        ~s[BODY(INIT:TEXT-1-1(foo):EQUAL(if do\n  "BEGIN:TEXT-1-17(this):END"\nelse\n  "BEGIN:TEXT-1-31(that):END"\nend))],
        "foo <%= if do %>this<% else %>that<% end %>",
        [],
        engine: TestEngine
      )
    end

    test "not implemented custom marker" do
      msg =
        ~r/unsupported EEx syntax <%| %> \(the syntax is valid but not supported by the current EEx engine\)/

      assert_raise EEx.SyntaxError, msg, fn ->
        assert_eval({:wrapped, "foo baz"}, "foo <%| :bar %>", [], engine: TestEngine)
      end
    end
  end

  describe "parser options" do
    test "customizes parsed code" do
      atoms_encoder = fn "not_jose", _ -> {:ok, :jose} end

      assert_eval("valid", "<%= not_jose %>", [jose: "valid"],
        parser_options: [static_atoms_encoder: atoms_encoder]
      )
    end
  end

  @strip_ansi [IO.ANSI.green(), IO.ANSI.red(), IO.ANSI.reset()]

  defp strip_ansi(doc) do
    String.replace(doc, @strip_ansi, "")
  end

  defp assert_eval(expected, actual, binding \\ [], opts \\ []) do
    opts = Keyword.merge([file: __ENV__.file, engine: opts[:engine] || EEx.Engine], opts)
    result = EEx.eval_string(actual, binding, opts)
    assert result == expected
  end

  defp assert_normalized_newline_equal(expected, actual) do
    assert String.replace(expected, "\r\n", "\n") == String.replace(actual, "\r\n", "\n")
  end

  def tuple_map({list, callback}) do
    Enum.map(list, callback)
  end

  def switching_map(list, a, b) do
    list
    |> Enum.with_index()
    |> Enum.map(fn
      {element, index} when rem(index, 2) == 0 -> a.(element)
      {element, index} when rem(index, 2) == 1 -> b.(element)
    end)
  end

  defmacro switching_macro(list, a, do: block) do
    quote do
      b = fn var!(x) ->
        unquote(block)
      end

      unquote(__MODULE__).switching_map(unquote(list), unquote(a), b)
    end
  end
end
